@@ -54,13 +54,13 @@ public struct NodeHandlerContext: Sendable {
5454 public let invocationID : String
5555
5656 private let sendMessageImpl : @Sendable ( String, String, String, NodeMessageMode? , [ String: JSONValue] ? ) async throws -> JSONValue
57- private let spawnAgentImpl : @Sendable ( JSONValue) async throws -> JSONValue
57+ private let spawnAgentImpl : @Sendable ( [ String : JSONValue] ) async throws -> JSONValue
5858
5959 init (
6060 node: NodeInfo ,
6161 invocationID: String ,
6262 sendMessageImpl: @escaping @Sendable ( String, String, String, NodeMessageMode? , [ String: JSONValue] ? ) async throws -> JSONValue ,
63- spawnAgentImpl: @escaping @Sendable ( JSONValue) async throws -> JSONValue
63+ spawnAgentImpl: @escaping @Sendable ( [ String : JSONValue] ) async throws -> JSONValue
6464 ) {
6565 self . node = node
6666 self . invocationID = invocationID
@@ -83,9 +83,10 @@ public struct NodeHandlerContext: Sendable {
8383
8484 /// Spawn an agent through the node's capacity executor. Capacity-direct: it
8585 /// never re-enters action dispatch, so a `spawn:<harness>` shadow handler
86- /// that delegates cannot recurse into itself. Resolves with the spawn
87- /// placement.
88- public func spawnAgent( _ input: JSONValue ) async throws -> JSONValue {
86+ /// that delegates cannot recurse into itself. The spawn spec is a JSON
87+ /// object (the engine's `node.spawn` input is object-typed). Resolves with
88+ /// the spawn placement.
89+ public func spawnAgent( _ input: [ String : JSONValue ] ) async throws -> JSONValue {
8990 try await spawnAgentImpl ( input)
9091 }
9192}
@@ -109,25 +110,33 @@ public struct NodeRegisterReplyData: Equatable, Sendable {
109110 public let raw : JSONValue
110111
111112 static func parse( _ json: JSONValue ) throws -> NodeRegisterReplyData {
113+ // A malformed reply is a deterministic registration failure (thrown as a
114+ // NodeRegistrationError so it hard-fails rather than being retried as a
115+ // transient drop).
112116 guard case . object( let obj) = json,
113117 case . object( let providerObj) ? = obj [ " provider " ] ,
114118 case . string( let name) ? = providerObj [ " name " ] ,
115119 case . string( let instanceID) ? = providerObj [ " instance_id " ]
116120 else {
117- throw NodeProviderError . invalidRegisterReply
121+ throw NodeRegistrationError ( code : " invalid_register_reply " , message : " node.register reply is malformed " )
118122 }
119123
120- var acceptedCapabilities : [ NodeCapabilityAcceptance ] = [ ]
121- if case . array( let items) ? = obj [ " accepted_capabilities " ] {
122- acceptedCapabilities = items. compactMap { item -> NodeCapabilityAcceptance ? in
123- guard case . object( let capObj) = item,
124- case . string( let capName) ? = capObj [ " name " ] ,
125- case . bool( let accepted) ? = capObj [ " accepted " ]
126- else { return nil }
127- let kind : String = { if case . string( let k) ? = capObj [ " kind " ] { return k } else { return " action " } } ( )
128- let reason : String ? = { if case . string( let r) ? = capObj [ " reason " ] { return r } else { return nil } } ( )
129- return NodeCapabilityAcceptance ( name: capName, kind: kind, accepted: accepted, reason: reason)
124+ // The reply must carry a well-formed acceptance list (the engine always
125+ // does). A missing/corrupt list means acceptance can't be confirmed, so
126+ // fail registration rather than assume success.
127+ guard case . array( let items) ? = obj [ " accepted_capabilities " ] else {
128+ throw NodeRegistrationError ( code: " invalid_register_reply " , message: " node.register reply is missing accepted_capabilities " )
129+ }
130+ let acceptedCapabilities : [ NodeCapabilityAcceptance ] = items. map { item -> NodeCapabilityAcceptance in
131+ // A malformed entry can't be confirmed accepted — count it as
132+ // rejected (fail-safe) rather than dropping it.
133+ guard case . object( let capObj) = item, case . string( let capName) ? = capObj [ " name " ] else {
134+ return NodeCapabilityAcceptance ( name: " <unknown> " , kind: " action " , accepted: false , reason: " malformed acceptance entry " )
130135 }
136+ let accepted : Bool = { if case . bool( let a) ? = capObj [ " accepted " ] { return a } else { return false } } ( )
137+ let kind : String = { if case . string( let k) ? = capObj [ " kind " ] { return k } else { return " action " } } ( )
138+ let reason : String ? = { if case . string( let r) ? = capObj [ " reason " ] { return r } else { return nil } } ( )
139+ return NodeCapabilityAcceptance ( name: capName, kind: kind, accepted: accepted, reason: reason)
131140 }
132141
133142 return NodeRegisterReplyData ( providerName: name, instanceID: instanceID, acceptedCapabilities: acceptedCapabilities, raw: json)
@@ -682,12 +691,12 @@ public actor NodeProvider {
682691 return try await request ( payload)
683692 }
684693
685- private func spawnAgentFrame( _ input: JSONValue ) async throws -> JSONValue {
694+ private func spawnAgentFrame( _ input: [ String : JSONValue ] ) async throws -> JSONValue {
686695 // Capacity-direct: the engine always targets this connection's own node,
687696 // so the frame carries no node target.
688697 try await request ( [
689698 " type " : . string( " node.spawn " ) ,
690- " input " : input
699+ " input " : . object ( input)
691700 ] )
692701 }
693702
0 commit comments