@mcpmesh/sdk 1.4.1 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/dist/__tests__/a2a/a2a-bearer.spec.d.ts +2 -0
  2. package/dist/__tests__/a2a/a2a-bearer.spec.d.ts.map +1 -0
  3. package/dist/__tests__/a2a/a2a-bearer.spec.js +58 -0
  4. package/dist/__tests__/a2a/a2a-bearer.spec.js.map +1 -0
  5. package/dist/__tests__/a2a/a2a-client.spec.d.ts +2 -0
  6. package/dist/__tests__/a2a/a2a-client.spec.d.ts.map +1 -0
  7. package/dist/__tests__/a2a/a2a-client.spec.js +334 -0
  8. package/dist/__tests__/a2a/a2a-client.spec.js.map +1 -0
  9. package/dist/__tests__/a2a/a2a-job.spec.d.ts +2 -0
  10. package/dist/__tests__/a2a/a2a-job.spec.d.ts.map +1 -0
  11. package/dist/__tests__/a2a/a2a-job.spec.js +255 -0
  12. package/dist/__tests__/a2a/a2a-job.spec.js.map +1 -0
  13. package/dist/__tests__/a2a/a2a-stream.spec.d.ts +2 -0
  14. package/dist/__tests__/a2a/a2a-stream.spec.d.ts.map +1 -0
  15. package/dist/__tests__/a2a/a2a-stream.spec.js +278 -0
  16. package/dist/__tests__/a2a/a2a-stream.spec.js.map +1 -0
  17. package/dist/__tests__/a2a/agent-a2a-config.spec.d.ts +2 -0
  18. package/dist/__tests__/a2a/agent-a2a-config.spec.d.ts.map +1 -0
  19. package/dist/__tests__/a2a/agent-a2a-config.spec.js +262 -0
  20. package/dist/__tests__/a2a/agent-a2a-config.spec.js.map +1 -0
  21. package/dist/__tests__/a2a/producer/auth-filter.spec.d.ts +2 -0
  22. package/dist/__tests__/a2a/producer/auth-filter.spec.d.ts.map +1 -0
  23. package/dist/__tests__/a2a/producer/auth-filter.spec.js +127 -0
  24. package/dist/__tests__/a2a/producer/auth-filter.spec.js.map +1 -0
  25. package/dist/__tests__/a2a/producer/card-builder.spec.d.ts +2 -0
  26. package/dist/__tests__/a2a/producer/card-builder.spec.d.ts.map +1 -0
  27. package/dist/__tests__/a2a/producer/card-builder.spec.js +113 -0
  28. package/dist/__tests__/a2a/producer/card-builder.spec.js.map +1 -0
  29. package/dist/__tests__/a2a/producer/dispatcher.spec.d.ts +2 -0
  30. package/dist/__tests__/a2a/producer/dispatcher.spec.d.ts.map +1 -0
  31. package/dist/__tests__/a2a/producer/dispatcher.spec.js +850 -0
  32. package/dist/__tests__/a2a/producer/dispatcher.spec.js.map +1 -0
  33. package/dist/__tests__/a2a/producer/mount-surface-push.spec.d.ts +2 -0
  34. package/dist/__tests__/a2a/producer/mount-surface-push.spec.d.ts.map +1 -0
  35. package/dist/__tests__/a2a/producer/mount-surface-push.spec.js +164 -0
  36. package/dist/__tests__/a2a/producer/mount-surface-push.spec.js.map +1 -0
  37. package/dist/__tests__/a2a/producer/mount.spec.d.ts +2 -0
  38. package/dist/__tests__/a2a/producer/mount.spec.d.ts.map +1 -0
  39. package/dist/__tests__/a2a/producer/mount.spec.js +433 -0
  40. package/dist/__tests__/a2a/producer/mount.spec.js.map +1 -0
  41. package/dist/__tests__/a2a/producer/public-url-cache.spec.d.ts +2 -0
  42. package/dist/__tests__/a2a/producer/public-url-cache.spec.d.ts.map +1 -0
  43. package/dist/__tests__/a2a/producer/public-url-cache.spec.js +116 -0
  44. package/dist/__tests__/a2a/producer/public-url-cache.spec.js.map +1 -0
  45. package/dist/__tests__/a2a/producer/sse-emitter.spec.d.ts +2 -0
  46. package/dist/__tests__/a2a/producer/sse-emitter.spec.d.ts.map +1 -0
  47. package/dist/__tests__/a2a/producer/sse-emitter.spec.js +754 -0
  48. package/dist/__tests__/a2a/producer/sse-emitter.spec.js.map +1 -0
  49. package/dist/__tests__/a2a/producer/state-translator.spec.d.ts +2 -0
  50. package/dist/__tests__/a2a/producer/state-translator.spec.d.ts.map +1 -0
  51. package/dist/__tests__/a2a/producer/state-translator.spec.js +124 -0
  52. package/dist/__tests__/a2a/producer/state-translator.spec.js.map +1 -0
  53. package/dist/__tests__/a2a/producer/task-store.spec.d.ts +2 -0
  54. package/dist/__tests__/a2a/producer/task-store.spec.d.ts.map +1 -0
  55. package/dist/__tests__/a2a/producer/task-store.spec.js +180 -0
  56. package/dist/__tests__/a2a/producer/task-store.spec.js.map +1 -0
  57. package/dist/__tests__/agent-add-tool.spec.d.ts +2 -0
  58. package/dist/__tests__/agent-add-tool.spec.d.ts.map +1 -0
  59. package/dist/__tests__/agent-add-tool.spec.js +483 -0
  60. package/dist/__tests__/agent-add-tool.spec.js.map +1 -0
  61. package/dist/__tests__/api-runtime-race.spec.d.ts +2 -0
  62. package/dist/__tests__/api-runtime-race.spec.d.ts.map +1 -0
  63. package/dist/__tests__/api-runtime-race.spec.js +193 -0
  64. package/dist/__tests__/api-runtime-race.spec.js.map +1 -0
  65. package/dist/__tests__/claim-dispatcher.spec.d.ts +2 -0
  66. package/dist/__tests__/claim-dispatcher.spec.d.ts.map +1 -0
  67. package/dist/__tests__/claim-dispatcher.spec.js +408 -0
  68. package/dist/__tests__/claim-dispatcher.spec.js.map +1 -0
  69. package/dist/__tests__/inbound-job-dispatch.spec.d.ts +2 -0
  70. package/dist/__tests__/inbound-job-dispatch.spec.d.ts.map +1 -0
  71. package/dist/__tests__/inbound-job-dispatch.spec.js +185 -0
  72. package/dist/__tests__/inbound-job-dispatch.spec.js.map +1 -0
  73. package/dist/__tests__/job-controller-progress.spec.d.ts +2 -0
  74. package/dist/__tests__/job-controller-progress.spec.d.ts.map +1 -0
  75. package/dist/__tests__/job-controller-progress.spec.js +85 -0
  76. package/dist/__tests__/job-controller-progress.spec.js.map +1 -0
  77. package/dist/__tests__/jobs-cancel-route.spec.d.ts +2 -0
  78. package/dist/__tests__/jobs-cancel-route.spec.d.ts.map +1 -0
  79. package/dist/__tests__/jobs-cancel-route.spec.js +88 -0
  80. package/dist/__tests__/jobs-cancel-route.spec.js.map +1 -0
  81. package/dist/__tests__/llm-agent-stream.test.d.ts +14 -0
  82. package/dist/__tests__/llm-agent-stream.test.d.ts.map +1 -0
  83. package/dist/__tests__/llm-agent-stream.test.js +341 -0
  84. package/dist/__tests__/llm-agent-stream.test.js.map +1 -0
  85. package/dist/__tests__/mesh-job-submitter.spec.d.ts +2 -0
  86. package/dist/__tests__/mesh-job-submitter.spec.d.ts.map +1 -0
  87. package/dist/__tests__/mesh-job-submitter.spec.js +110 -0
  88. package/dist/__tests__/mesh-job-submitter.spec.js.map +1 -0
  89. package/dist/__tests__/proxy-stream.test.d.ts +9 -0
  90. package/dist/__tests__/proxy-stream.test.d.ts.map +1 -0
  91. package/dist/__tests__/proxy-stream.test.js +347 -0
  92. package/dist/__tests__/proxy-stream.test.js.map +1 -0
  93. package/dist/__tests__/resolver-meshjob.spec.d.ts +26 -0
  94. package/dist/__tests__/resolver-meshjob.spec.d.ts.map +1 -0
  95. package/dist/__tests__/resolver-meshjob.spec.js +201 -0
  96. package/dist/__tests__/resolver-meshjob.spec.js.map +1 -0
  97. package/dist/__tests__/schema-verdict-policy.test.d.ts +6 -0
  98. package/dist/__tests__/schema-verdict-policy.test.d.ts.map +1 -0
  99. package/dist/__tests__/schema-verdict-policy.test.js +126 -0
  100. package/dist/__tests__/schema-verdict-policy.test.js.map +1 -0
  101. package/dist/__tests__/sse-stream.test.d.ts +12 -0
  102. package/dist/__tests__/sse-stream.test.d.ts.map +1 -0
  103. package/dist/__tests__/sse-stream.test.js +170 -0
  104. package/dist/__tests__/sse-stream.test.js.map +1 -0
  105. package/dist/a2a/a2a-bearer.d.ts +27 -0
  106. package/dist/a2a/a2a-bearer.d.ts.map +1 -0
  107. package/dist/a2a/a2a-bearer.js +63 -0
  108. package/dist/a2a/a2a-bearer.js.map +1 -0
  109. package/dist/a2a/a2a-client.d.ts +114 -0
  110. package/dist/a2a/a2a-client.d.ts.map +1 -0
  111. package/dist/a2a/a2a-client.js +405 -0
  112. package/dist/a2a/a2a-client.js.map +1 -0
  113. package/dist/a2a/a2a-event.d.ts +25 -0
  114. package/dist/a2a/a2a-event.d.ts.map +1 -0
  115. package/dist/a2a/a2a-event.js +9 -0
  116. package/dist/a2a/a2a-event.js.map +1 -0
  117. package/dist/a2a/a2a-job.d.ts +58 -0
  118. package/dist/a2a/a2a-job.d.ts.map +1 -0
  119. package/dist/a2a/a2a-job.js +264 -0
  120. package/dist/a2a/a2a-job.js.map +1 -0
  121. package/dist/a2a/a2a-stream.d.ts +39 -0
  122. package/dist/a2a/a2a-stream.d.ts.map +1 -0
  123. package/dist/a2a/a2a-stream.js +290 -0
  124. package/dist/a2a/a2a-stream.js.map +1 -0
  125. package/dist/a2a/errors.d.ts +29 -0
  126. package/dist/a2a/errors.d.ts.map +1 -0
  127. package/dist/a2a/errors.js +48 -0
  128. package/dist/a2a/errors.js.map +1 -0
  129. package/dist/a2a/index.d.ts +12 -0
  130. package/dist/a2a/index.d.ts.map +1 -0
  131. package/dist/a2a/index.js +11 -0
  132. package/dist/a2a/index.js.map +1 -0
  133. package/dist/a2a/producer/auth-filter.d.ts +34 -0
  134. package/dist/a2a/producer/auth-filter.d.ts.map +1 -0
  135. package/dist/a2a/producer/auth-filter.js +39 -0
  136. package/dist/a2a/producer/auth-filter.js.map +1 -0
  137. package/dist/a2a/producer/card-builder.d.ts +59 -0
  138. package/dist/a2a/producer/card-builder.d.ts.map +1 -0
  139. package/dist/a2a/producer/card-builder.js +59 -0
  140. package/dist/a2a/producer/card-builder.js.map +1 -0
  141. package/dist/a2a/producer/dispatcher.d.ts +276 -0
  142. package/dist/a2a/producer/dispatcher.d.ts.map +1 -0
  143. package/dist/a2a/producer/dispatcher.js +896 -0
  144. package/dist/a2a/producer/dispatcher.js.map +1 -0
  145. package/dist/a2a/producer/index.d.ts +26 -0
  146. package/dist/a2a/producer/index.d.ts.map +1 -0
  147. package/dist/a2a/producer/index.js +23 -0
  148. package/dist/a2a/producer/index.js.map +1 -0
  149. package/dist/a2a/producer/mount.d.ts +75 -0
  150. package/dist/a2a/producer/mount.d.ts.map +1 -0
  151. package/dist/a2a/producer/mount.js +422 -0
  152. package/dist/a2a/producer/mount.js.map +1 -0
  153. package/dist/a2a/producer/public-url-cache.d.ts +73 -0
  154. package/dist/a2a/producer/public-url-cache.d.ts.map +1 -0
  155. package/dist/a2a/producer/public-url-cache.js +0 -0
  156. package/dist/a2a/producer/public-url-cache.js.map +1 -0
  157. package/dist/a2a/producer/registry.d.ts +138 -0
  158. package/dist/a2a/producer/registry.d.ts.map +1 -0
  159. package/dist/a2a/producer/registry.js +117 -0
  160. package/dist/a2a/producer/registry.js.map +1 -0
  161. package/dist/a2a/producer/sse-emitter.d.ts +85 -0
  162. package/dist/a2a/producer/sse-emitter.d.ts.map +1 -0
  163. package/dist/a2a/producer/sse-emitter.js +405 -0
  164. package/dist/a2a/producer/sse-emitter.js.map +1 -0
  165. package/dist/a2a/producer/state-translator.d.ts +63 -0
  166. package/dist/a2a/producer/state-translator.d.ts.map +1 -0
  167. package/dist/a2a/producer/state-translator.js +108 -0
  168. package/dist/a2a/producer/state-translator.js.map +1 -0
  169. package/dist/a2a/producer/task-store.d.ts +128 -0
  170. package/dist/a2a/producer/task-store.d.ts.map +1 -0
  171. package/dist/a2a/producer/task-store.js +128 -0
  172. package/dist/a2a/producer/task-store.js.map +1 -0
  173. package/dist/agent.d.ts +72 -0
  174. package/dist/agent.d.ts.map +1 -1
  175. package/dist/agent.js +618 -13
  176. package/dist/agent.js.map +1 -1
  177. package/dist/api-runtime.d.ts +25 -0
  178. package/dist/api-runtime.d.ts.map +1 -1
  179. package/dist/api-runtime.js +75 -2
  180. package/dist/api-runtime.js.map +1 -1
  181. package/dist/claim-dispatcher.d.ts +126 -0
  182. package/dist/claim-dispatcher.d.ts.map +1 -0
  183. package/dist/claim-dispatcher.js +478 -0
  184. package/dist/claim-dispatcher.js.map +1 -0
  185. package/dist/express.d.ts.map +1 -1
  186. package/dist/express.js +33 -6
  187. package/dist/express.js.map +1 -1
  188. package/dist/inbound-job-dispatch.d.ts +105 -0
  189. package/dist/inbound-job-dispatch.d.ts.map +1 -0
  190. package/dist/inbound-job-dispatch.js +335 -0
  191. package/dist/inbound-job-dispatch.js.map +1 -0
  192. package/dist/index.d.ts +37 -4
  193. package/dist/index.d.ts.map +1 -1
  194. package/dist/index.js +29 -3
  195. package/dist/index.js.map +1 -1
  196. package/dist/job-context.d.ts +107 -0
  197. package/dist/job-context.d.ts.map +1 -0
  198. package/dist/job-context.js +95 -0
  199. package/dist/job-context.js.map +1 -0
  200. package/dist/jobs-cancel-route.d.ts +36 -0
  201. package/dist/jobs-cancel-route.d.ts.map +1 -0
  202. package/dist/jobs-cancel-route.js +60 -0
  203. package/dist/jobs-cancel-route.js.map +1 -0
  204. package/dist/jobs-helper-tools.d.ts +48 -0
  205. package/dist/jobs-helper-tools.d.ts.map +1 -0
  206. package/dist/jobs-helper-tools.js +133 -0
  207. package/dist/jobs-helper-tools.js.map +1 -0
  208. package/dist/llm-agent.d.ts +62 -53
  209. package/dist/llm-agent.d.ts.map +1 -1
  210. package/dist/llm-agent.js +211 -292
  211. package/dist/llm-agent.js.map +1 -1
  212. package/dist/llm-provider.d.ts +4 -4
  213. package/dist/llm.d.ts +4 -1
  214. package/dist/llm.d.ts.map +1 -1
  215. package/dist/llm.js +7 -17
  216. package/dist/llm.js.map +1 -1
  217. package/dist/mesh-job-submitter.d.ts +83 -0
  218. package/dist/mesh-job-submitter.d.ts.map +1 -0
  219. package/dist/mesh-job-submitter.js +143 -0
  220. package/dist/mesh-job-submitter.js.map +1 -0
  221. package/dist/proxy.d.ts +30 -0
  222. package/dist/proxy.d.ts.map +1 -1
  223. package/dist/proxy.js +351 -1
  224. package/dist/proxy.js.map +1 -1
  225. package/dist/resolver-meshjob.d.ts +170 -0
  226. package/dist/resolver-meshjob.d.ts.map +1 -0
  227. package/dist/resolver-meshjob.js +159 -0
  228. package/dist/resolver-meshjob.js.map +1 -0
  229. package/dist/route.d.ts +4 -0
  230. package/dist/route.d.ts.map +1 -1
  231. package/dist/route.js.map +1 -1
  232. package/dist/schema-normalize.d.ts +62 -0
  233. package/dist/schema-normalize.d.ts.map +1 -0
  234. package/dist/schema-normalize.js +128 -0
  235. package/dist/schema-normalize.js.map +1 -0
  236. package/dist/sse-stream.d.ts +44 -0
  237. package/dist/sse-stream.d.ts.map +1 -0
  238. package/dist/sse-stream.js +173 -0
  239. package/dist/sse-stream.js.map +1 -0
  240. package/dist/types.d.ts +351 -9
  241. package/dist/types.d.ts.map +1 -1
  242. package/package.json +4 -3
@@ -0,0 +1,896 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { JobProxy } from "@mcpmesh/core";
3
+ import { A2A_CANCELED, A2A_COMPLETED, A2A_FAILED, A2A_WORKING, fromMesh, isTerminal, meshStatusOf, } from "./state-translator.js";
4
+ /** JSON-RPC: Parse error (request body is not valid JSON). Spec §4.1. */
5
+ export const JSONRPC_PARSE_ERROR = -32700;
6
+ /** JSON-RPC: Invalid Request (well-formed JSON but missing `method` field). Spec §4.1. */
7
+ export const JSONRPC_INVALID_REQUEST = -32600;
8
+ /** JSON-RPC: Method not found (unknown `tasks/*` verb). Spec §4.1. */
9
+ export const JSONRPC_METHOD_NOT_FOUND = -32601;
10
+ /** JSON-RPC: Invalid params (missing or unknown task id). Spec §4.4. */
11
+ export const JSONRPC_INVALID_PARAMS = -32602;
12
+ /**
13
+ * Build an Express request handler that dispatches `POST {path}` requests
14
+ * for a single surface.
15
+ *
16
+ * Routes JSON-RPC envelopes that return a single response body
17
+ * (`tasks/send`, `tasks/get`, `tasks/cancel`). SSE verbs
18
+ * (`tasks/sendSubscribe`, `tasks/resubscribe`) are dispatched by a
19
+ * sibling middleware ({@link buildSseDispatcherMiddleware}) that the
20
+ * mount wires in front of this one — when the SSE middleware sees an
21
+ * SSE-eligible method it consumes the request; otherwise it calls
22
+ * `next()` and execution falls through here.
23
+ */
24
+ export function buildDispatcherMiddleware(deps) {
25
+ const { surface, handler, taskStore, routeRegistry, jobSubmitterProvider } = deps;
26
+ return async function a2aDispatcher(req, res) {
27
+ // Spec §4.1: malformed body → HTTP 400 + JSON-RPC -32700. Express's
28
+ // `express.json()` middleware should have parsed the body; defensively
29
+ // handle the case where it wasn't installed by checking for a missing
30
+ // body object (req.body will be `undefined` then).
31
+ const body = req.body;
32
+ if (body === undefined || body === null) {
33
+ writeJsonRpcParseErrorHttp400(res, "Parse error: request body is empty or not parsed (did you install express.json()?)");
34
+ return;
35
+ }
36
+ if (typeof body !== "object" || Array.isArray(body)) {
37
+ writeJsonRpcParseErrorHttp400(res, "Parse error: request body must be a JSON-RPC object");
38
+ return;
39
+ }
40
+ const envelope = body;
41
+ const reqId = extractRequestId(envelope);
42
+ const method = envelope["method"];
43
+ if (typeof method !== "string" || method.length === 0) {
44
+ // Spec §4.1: well-formed JSON missing the required `method` member is
45
+ // an Invalid Request (-32600), NOT Method not found (-32601). Without
46
+ // this guard the default branch would emit a misleading
47
+ // "Method not implemented: 'null'" — the bug surfaced in issue #934
48
+ // when the Java body-read path silently dropped the parsed body.
49
+ writeJsonRpc(res, jsonRpcError(reqId, JSONRPC_INVALID_REQUEST, "Invalid Request: 'method' field is required and must be a string"));
50
+ return;
51
+ }
52
+ const params = readParams(envelope["params"]);
53
+ switch (method) {
54
+ case "tasks/send":
55
+ await handleTasksSend(req, res, reqId, params, {
56
+ surface,
57
+ handler,
58
+ taskStore,
59
+ routeRegistry,
60
+ jobSubmitterProvider,
61
+ });
62
+ return;
63
+ case "tasks/get":
64
+ await handleTasksGet(res, reqId, params, taskStore);
65
+ return;
66
+ case "tasks/cancel":
67
+ await handleTasksCancel(res, reqId, params, taskStore);
68
+ return;
69
+ case "tasks/sendSubscribe":
70
+ case "tasks/resubscribe":
71
+ // SSE methods are handled by the sibling SSE-middleware. If we
72
+ // see them here it means the SSE middleware fell through — most
73
+ // commonly because the client did not send `Accept: text/event-stream`.
74
+ // Surface a clear error matching Java's MeshA2ADispatcher.
75
+ writeJsonRpc(res, jsonRpcError(reqId, JSONRPC_METHOD_NOT_FOUND, `Method '${method}' requires an SSE-capable client. ` +
76
+ `Set 'Accept: text/event-stream' or use a streaming HTTP client.`));
77
+ return;
78
+ default:
79
+ writeJsonRpc(res, jsonRpcError(reqId, JSONRPC_METHOD_NOT_FOUND, `Method not implemented: '${method}'. ` +
80
+ `Supported A2A v1.0 methods: tasks/send, tasks/get, tasks/cancel, tasks/sendSubscribe, tasks/resubscribe.`));
81
+ return;
82
+ }
83
+ };
84
+ }
85
+ // ─────────────────────────────────────────────────────────────────────────
86
+ // tasks/send
87
+ // ─────────────────────────────────────────────────────────────────────────
88
+ async function handleTasksSend(_req, res, reqId, params, deps) {
89
+ // Spec §4.2: extract (task_id, session_id, message).
90
+ let taskId = stringFromParams(params, "id");
91
+ if (!taskId) {
92
+ taskId = randomUUID();
93
+ }
94
+ let sessionId = stringFromParams(params, "sessionId");
95
+ if (!sessionId) {
96
+ sessionId = taskId;
97
+ }
98
+ const message = mapFromParams(params, "message");
99
+ // Spec §4.3: duplicate in-flight task_id → -32602 already in use.
100
+ // Terminal entries within the eviction window are also rejected.
101
+ // Atomic reservation — closes the race between two concurrent
102
+ // tasks/send requests with the same id (the pre-check + handler
103
+ // await yields control, so a separate `contains()` then `put()`
104
+ // would let both slip through).
105
+ const reserved = deps.taskStore.reserveTask(taskId, {
106
+ sessionId,
107
+ requestMessage: hasOwn(message) ? message : undefined,
108
+ jobProxy: null,
109
+ });
110
+ if (!reserved) {
111
+ writeJsonRpc(res, jsonRpcError(reqId, JSONRPC_INVALID_PARAMS, `A2A task id '${taskId}' is already in use`));
112
+ return;
113
+ }
114
+ // Resolve dependencies the same way mesh.route() does — via the shared
115
+ // RouteRegistry. The surface registered a synthetic route at mount time
116
+ // (see mount.ts); resolved McpMeshTool proxies surface here keyed by
117
+ // capability name.
118
+ const resolvedDeps = deps.routeRegistry.getDependenciesForRoute(deps.surface.routeId);
119
+ // Ensure every declared capability key is present (null when unresolved)
120
+ // — the user's destructure shouldn't crash on a partially-resolved
121
+ // dependency graph.
122
+ for (const dep of deps.surface.dependencies) {
123
+ if (resolvedDeps[dep.capability] === undefined) {
124
+ resolvedDeps[dep.capability] = null;
125
+ }
126
+ }
127
+ const jobSubmitter = deps.jobSubmitterProvider
128
+ ? deps.jobSubmitterProvider()
129
+ : null;
130
+ let handlerResult;
131
+ try {
132
+ handlerResult = await deps.handler(resolvedDeps, message, jobSubmitter);
133
+ }
134
+ catch (err) {
135
+ // Spec §4.3 "Response — handler raised": exceptions become
136
+ // state=failed Tasks, NOT JSON-RPC errors. Upgrade the placeholder
137
+ // reservation to a terminal failed envelope.
138
+ const errorText = errorTextOf(err);
139
+ const envelope = buildFailedTask(taskId, sessionId, message, errorText);
140
+ deps.taskStore.put(taskId, {
141
+ sessionId,
142
+ requestMessage: hasOwn(message) ? message : undefined,
143
+ terminalEnvelope: envelope,
144
+ terminalAt: Date.now(),
145
+ jobProxy: null,
146
+ });
147
+ writeJsonRpc(res, jsonRpcSuccess(reqId, envelope));
148
+ return;
149
+ }
150
+ // Spec §4.3 long-running branch: handler returned a JobProxy →
151
+ // park the task and respond with state=working immediately. The
152
+ // client polls tasks/get / tasks/sendSubscribe for progress and
153
+ // the terminal artifact.
154
+ if (isJobProxy(handlerResult)) {
155
+ const envelope = buildWorkingTask(taskId, sessionId, message);
156
+ parkLongRunning(deps.taskStore, taskId, sessionId, message, handlerResult);
157
+ writeJsonRpc(res, jsonRpcSuccess(reqId, envelope));
158
+ return;
159
+ }
160
+ // Sync path: handler returned a value → state=completed envelope.
161
+ // Upgrade the placeholder reservation to the terminal record.
162
+ const envelope = buildCompletedTask(taskId, sessionId, message, handlerResult);
163
+ deps.taskStore.put(taskId, {
164
+ sessionId,
165
+ requestMessage: hasOwn(message) ? message : undefined,
166
+ terminalEnvelope: envelope,
167
+ terminalAt: Date.now(),
168
+ jobProxy: null,
169
+ });
170
+ writeJsonRpc(res, jsonRpcSuccess(reqId, envelope));
171
+ }
172
+ // ─────────────────────────────────────────────────────────────────────────
173
+ // tasks/get
174
+ // ─────────────────────────────────────────────────────────────────────────
175
+ async function handleTasksGet(res, reqId, params, taskStore) {
176
+ const taskId = stringFromParams(params, "id");
177
+ if (!taskId) {
178
+ writeJsonRpc(res, jsonRpcError(reqId, JSONRPC_INVALID_PARAMS, "Invalid params: 'id' is required for tasks/get"));
179
+ return;
180
+ }
181
+ const record = taskStore.get(taskId);
182
+ if (!record) {
183
+ writeJsonRpc(res, jsonRpcError(reqId, JSONRPC_INVALID_PARAMS, `Unknown task id: ${taskId}`));
184
+ return;
185
+ }
186
+ if (record.terminalEnvelope) {
187
+ writeJsonRpc(res, jsonRpcSuccess(reqId, record.terminalEnvelope));
188
+ return;
189
+ }
190
+ // Non-terminal record: pull live status from the parked JobProxy.
191
+ // Per spec §4.4 "transient unreachability": if status() throws we
192
+ // return state=working with the error text in status.message rather
193
+ // than a JSON-RPC error — the registry's transient failure isn't
194
+ // authoritative evidence the job is dead.
195
+ const envelope = await buildTaskFromLiveStatus(taskStore, taskId, record);
196
+ writeJsonRpc(res, jsonRpcSuccess(reqId, envelope));
197
+ }
198
+ // ─────────────────────────────────────────────────────────────────────────
199
+ // tasks/cancel
200
+ // ─────────────────────────────────────────────────────────────────────────
201
+ async function handleTasksCancel(res, reqId, params, taskStore) {
202
+ const taskId = stringFromParams(params, "id");
203
+ if (!taskId) {
204
+ writeJsonRpc(res, jsonRpcError(reqId, JSONRPC_INVALID_PARAMS, "Invalid params: 'id' is required for tasks/cancel"));
205
+ return;
206
+ }
207
+ const record = taskStore.get(taskId);
208
+ if (!record) {
209
+ writeJsonRpc(res, jsonRpcError(reqId, JSONRPC_INVALID_PARAMS, `Unknown task id: ${taskId}`));
210
+ return;
211
+ }
212
+ // Idempotent ack: already-terminal task → echo the cached envelope.
213
+ // Spec §4.5 "Idempotent; best-effort".
214
+ if (record.terminalEnvelope) {
215
+ writeJsonRpc(res, jsonRpcSuccess(reqId, record.terminalEnvelope));
216
+ return;
217
+ }
218
+ const reason = stringFromParams(params, "reason") ?? undefined;
219
+ const proxy = record.jobProxy ?? null;
220
+ let cancelThrew = false;
221
+ if (proxy) {
222
+ try {
223
+ await proxy.cancel(reason);
224
+ }
225
+ catch {
226
+ // Spec §4.5: cancel exceptions are logged and swallowed — the
227
+ // underlying job may already be terminal.
228
+ cancelThrew = true;
229
+ }
230
+ }
231
+ // Re-read status post-cancel so the response reflects the latest
232
+ // state. Java's BLOCKER fix from #934: if BOTH cancel() AND status()
233
+ // throw (double-failure), synthesize a state=canceled envelope rather
234
+ // than propagating exceptions (spec §4.5 fallback).
235
+ let envelope;
236
+ let statusThrew = false;
237
+ if (proxy) {
238
+ try {
239
+ envelope = await buildTaskFromLiveStatusInternal(taskId, record, proxy);
240
+ }
241
+ catch {
242
+ statusThrew = true;
243
+ envelope = buildCanceledTask(taskId, record.sessionId, record.requestMessage, reason);
244
+ }
245
+ }
246
+ else {
247
+ // Lost-JobProxy on a non-terminal record — spec §4.5 best-effort
248
+ // cancel says synthesize state=canceled rather than returning an
249
+ // error. Match Java's behaviour.
250
+ envelope = buildCanceledTask(taskId, record.sessionId, record.requestMessage, reason);
251
+ }
252
+ // If the post-cancel state is terminal, mark the record so future
253
+ // tasks/get calls hit the cached envelope and don't re-poll a closed
254
+ // JobProxy.
255
+ const statusObj = envelope["status"];
256
+ const state = typeof statusObj?.["state"] === "string" ? statusObj["state"] : null;
257
+ if (state && isTerminal(state)) {
258
+ taskStore.markTerminal(taskId, envelope);
259
+ }
260
+ else if (cancelThrew && statusThrew) {
261
+ // Double-failure path already produced a synthesized canceled
262
+ // envelope above — mark terminal so the client sees a stable cached
263
+ // response on retry.
264
+ taskStore.markTerminal(taskId, envelope);
265
+ }
266
+ else {
267
+ // Status didn't show terminal yet but cancel was requested —
268
+ // synthesize a canceled envelope so the client gets a clean terminal
269
+ // response. Matches Python's a2a.py:817-826 fallback.
270
+ const synth = buildCanceledTask(taskId, record.sessionId, record.requestMessage, reason);
271
+ taskStore.markTerminal(taskId, synth);
272
+ envelope = synth;
273
+ }
274
+ writeJsonRpc(res, jsonRpcSuccess(reqId, envelope));
275
+ }
276
+ // ─────────────────────────────────────────────────────────────────────────
277
+ // tasks/sendSubscribe + tasks/resubscribe (SSE plan builders)
278
+ // ─────────────────────────────────────────────────────────────────────────
279
+ /**
280
+ * Build the SSE stream plan for a `tasks/sendSubscribe` request. The
281
+ * dispatcher invokes the user handler eagerly (before the stream opens)
282
+ * so handler exceptions become a single SSE failed frame, not an opaque
283
+ * HTTP error mid-stream (spec §4.6).
284
+ */
285
+ export async function buildSendSubscribeStream(reqId, params, deps) {
286
+ let taskId = stringFromParams(params, "id");
287
+ if (!taskId) {
288
+ taskId = randomUUID();
289
+ }
290
+ let sessionId = stringFromParams(params, "sessionId");
291
+ if (!sessionId) {
292
+ sessionId = taskId;
293
+ }
294
+ const message = mapFromParams(params, "message");
295
+ // Atomic reservation — closes the race between two concurrent
296
+ // tasks/sendSubscribe requests with the same id (await deps.handler
297
+ // yields control between a separate `contains()` and `put()`).
298
+ const reserved = deps.taskStore.reserveTask(taskId, {
299
+ sessionId,
300
+ requestMessage: hasOwn(message) ? message : undefined,
301
+ jobProxy: null,
302
+ });
303
+ if (!reserved) {
304
+ // Duplicate in-flight task_id — surface as a single SSE failed
305
+ // event so the SSE client sees a structured A2A failure rather
306
+ // than an opaque HTTP error (Python a2a.py:1143-1149).
307
+ return sseSingleFrame(buildStatusUpdateFrame(reqId, taskId, A2A_FAILED, `A2A task id '${taskId}' is already in use`, true, null));
308
+ }
309
+ const resolvedDeps = deps.routeRegistry.getDependenciesForRoute(deps.surface.routeId);
310
+ for (const dep of deps.surface.dependencies) {
311
+ if (resolvedDeps[dep.capability] === undefined) {
312
+ resolvedDeps[dep.capability] = null;
313
+ }
314
+ }
315
+ const jobSubmitter = deps.jobSubmitterProvider
316
+ ? deps.jobSubmitterProvider()
317
+ : null;
318
+ let handlerResult;
319
+ try {
320
+ handlerResult = await deps.handler(resolvedDeps, message, jobSubmitter);
321
+ }
322
+ catch (err) {
323
+ const errorText = errorTextOf(err);
324
+ // Cache the failed envelope so a subsequent tasks/get returns it
325
+ // consistently with the JSON-RPC path.
326
+ const failed = buildFailedTask(taskId, sessionId, message, errorText);
327
+ deps.taskStore.put(taskId, {
328
+ sessionId,
329
+ requestMessage: hasOwn(message) ? message : undefined,
330
+ terminalEnvelope: failed,
331
+ terminalAt: Date.now(),
332
+ jobProxy: null,
333
+ });
334
+ return sseSingleFrame(buildStatusUpdateFrame(reqId, taskId, A2A_FAILED, errorText, true, null));
335
+ }
336
+ if (isJobProxy(handlerResult)) {
337
+ parkLongRunning(deps.taskStore, taskId, sessionId, message, handlerResult);
338
+ return sseLongRunning(reqId, taskId, handlerResult);
339
+ }
340
+ // Sync handler over tasks/sendSubscribe: per spec §5.3, emit one
341
+ // artifact event then one final status event (state=completed).
342
+ const artifactFrame = buildArtifactUpdateFrame(reqId, taskId, handlerResult);
343
+ const terminalFrame = buildStatusUpdateFrame(reqId, taskId, A2A_COMPLETED, null, true, null);
344
+ // Cache the resulting envelope so a follow-up tasks/get returns the
345
+ // same payload deterministically.
346
+ const envelope = buildCompletedTask(taskId, sessionId, message, handlerResult);
347
+ deps.taskStore.put(taskId, {
348
+ sessionId,
349
+ requestMessage: hasOwn(message) ? message : undefined,
350
+ terminalEnvelope: envelope,
351
+ terminalAt: Date.now(),
352
+ jobProxy: null,
353
+ });
354
+ return sseSyncCompleted(reqId, taskId, artifactFrame, terminalFrame);
355
+ }
356
+ /**
357
+ * Build the SSE stream plan for a `tasks/resubscribe` request (spec §4.7).
358
+ * Looks up the parked task, emits an initial state=working event, then
359
+ * resumes polling from the registry's current view (no replay).
360
+ */
361
+ export function buildResubscribeStream(reqId, params, taskStore) {
362
+ const taskId = stringFromParams(params, "id");
363
+ if (!taskId) {
364
+ // Spec §4.7 errors: return JSON-RPC, not SSE — the response has not
365
+ // been promoted to text/event-stream yet.
366
+ return sseError(jsonRpcError(reqId, JSONRPC_INVALID_PARAMS, "Invalid params: 'id' is required for tasks/resubscribe"), 200);
367
+ }
368
+ const record = taskStore.get(taskId);
369
+ if (!record) {
370
+ return sseError(jsonRpcError(reqId, JSONRPC_INVALID_PARAMS, `Unknown task id: ${taskId}`), 200);
371
+ }
372
+ if (record.terminalEnvelope) {
373
+ // Already terminal — emit ONE terminal status event and close.
374
+ // No replay per Python's a2a.py:1175-1178.
375
+ const env = record.terminalEnvelope;
376
+ const statusObj = env["status"];
377
+ let state = A2A_COMPLETED;
378
+ let msgText = null;
379
+ if (statusObj) {
380
+ const st = statusObj["state"];
381
+ if (typeof st === "string")
382
+ state = st;
383
+ const msg = statusObj["message"];
384
+ if (msg && typeof msg === "object") {
385
+ const parts = msg["parts"];
386
+ if (Array.isArray(parts) && parts.length > 0) {
387
+ const first = parts[0];
388
+ const text = first?.["text"];
389
+ if (text !== undefined && text !== null) {
390
+ msgText = String(text);
391
+ }
392
+ }
393
+ }
394
+ }
395
+ return sseSingleFrame(buildStatusUpdateFrame(reqId, taskId, state, msgText, true, null));
396
+ }
397
+ const proxy = record.jobProxy ?? null;
398
+ if (!proxy) {
399
+ // Non-terminal record without a JobProxy is an inconsistent state.
400
+ // Java's BLOCKER fix from #934: emit a single failed terminal event
401
+ // so the client doesn't hang the SSE connection.
402
+ return sseSingleFrame(buildStatusUpdateFrame(reqId, taskId, A2A_FAILED, "Task state inconsistent: no live JobProxy and no terminal envelope", true, null));
403
+ }
404
+ return sseLongRunning(reqId, taskId, proxy);
405
+ }
406
+ // ─────────────────────────────────────────────────────────────────────────
407
+ // Live-status poll (shared by tasks/get + SSE emitter terminal frame)
408
+ // ─────────────────────────────────────────────────────────────────────────
409
+ /**
410
+ * Pull the latest status from a parked task's `JobProxy` and project it
411
+ * into an A2A v1.0 Task envelope. Handles the "transient unreachability"
412
+ * branch per spec §4.4 by returning `state=working` with the error text
413
+ * in `status.message` rather than throwing — matches Python's
414
+ * `a2a.py:718-735` behavior.
415
+ *
416
+ * Used by `tasks/get` (top-level dispatcher) and exposed via
417
+ * {@link projectLiveStatus} for the SSE emitter's terminal-frame
418
+ * synthesis.
419
+ */
420
+ export async function buildTaskFromLiveStatus(taskStore, taskId, record) {
421
+ const proxy = record.jobProxy ?? null;
422
+ if (!proxy) {
423
+ return buildWorkingTask(taskId, record.sessionId, record.requestMessage);
424
+ }
425
+ const envelope = await buildTaskFromLiveStatusInternal(taskId, record, proxy);
426
+ // Persist terminal envelopes so subsequent tasks/get calls hit the cache
427
+ // and don't re-poll the JobProxy (spec §4.4 / Appendix B item 5). For
428
+ // working / live states leave the record as-is — the next tasks/get
429
+ // should re-poll for fresh progress.
430
+ const statusObj = envelope["status"];
431
+ const state = statusObj && typeof statusObj["state"] === "string"
432
+ ? statusObj["state"]
433
+ : null;
434
+ if (state === A2A_COMPLETED || state === A2A_FAILED || state === A2A_CANCELED) {
435
+ taskStore.markTerminal(taskId, envelope);
436
+ }
437
+ return envelope;
438
+ }
439
+ async function buildTaskFromLiveStatusInternal(taskId, record, proxy) {
440
+ let status;
441
+ try {
442
+ const raw = (await proxy.status());
443
+ status = (raw && typeof raw === "object" && !Array.isArray(raw))
444
+ ? raw
445
+ : {};
446
+ }
447
+ catch (err) {
448
+ // Spec §4.4: transient unreachability → state=working + error text in
449
+ // status.message. Do NOT escalate to JSON-RPC error.
450
+ return buildWorkingTask(taskId, record.sessionId, record.requestMessage, `status unavailable: ${errorTextOf(err)}`);
451
+ }
452
+ const meshState = meshStatusOf(status);
453
+ const a2aState = fromMesh(meshState);
454
+ // On completed, attempt proxy.wait(timeoutSecs=1) to fetch the final
455
+ // artifact synchronously. Tight timeout per spec §4.4 so we don't
456
+ // block on a transiently-unreachable payload — fall back to no
457
+ // artifact in that case. On failed/canceled, do NOT call wait() —
458
+ // it would throw and the error text is already in status.error /
459
+ // status.progress_message.
460
+ let finalResult = null;
461
+ let hasFinalResult = false;
462
+ if (a2aState === A2A_COMPLETED) {
463
+ try {
464
+ finalResult = await proxy.wait(1.0);
465
+ hasFinalResult = true;
466
+ }
467
+ catch {
468
+ // Best-effort: artifact omitted if the result payload is
469
+ // transiently unreachable.
470
+ }
471
+ }
472
+ return buildTaskFromStatus(taskId, record.sessionId, record.requestMessage, a2aState, status, finalResult, hasFinalResult);
473
+ }
474
+ /**
475
+ * Project the live status of a parked task into an A2A v1.0 Task
476
+ * envelope. Exposed for the SSE emitter so it can render terminal
477
+ * frames consistently with `tasks/get`.
478
+ */
479
+ export async function projectLiveStatus(taskStore, taskId) {
480
+ const record = taskStore.get(taskId);
481
+ if (!record)
482
+ return null;
483
+ if (record.terminalEnvelope)
484
+ return record.terminalEnvelope;
485
+ return buildTaskFromLiveStatus(taskStore, taskId, record);
486
+ }
487
+ // ─────────────────────────────────────────────────────────────────────────
488
+ // Task envelope builders (spec §4.3)
489
+ // ─────────────────────────────────────────────────────────────────────────
490
+ /**
491
+ * Build a `state=completed` Task envelope (spec §4.3). The handler result
492
+ * is stringified per the spec — string returns pass through verbatim,
493
+ * everything else is JSON-stringified. `null` / `undefined` produce an
494
+ * empty-string artifact.
495
+ *
496
+ * Exported for testability + the symmetry with the SSE artifact frame
497
+ * builder.
498
+ */
499
+ export function buildCompletedTask(taskId, sessionId, requestMessage, result) {
500
+ const text = stringifyResult(result);
501
+ return {
502
+ id: taskId,
503
+ sessionId,
504
+ status: {
505
+ state: A2A_COMPLETED,
506
+ timestamp: utcIso8601(),
507
+ },
508
+ artifacts: [
509
+ {
510
+ name: "result",
511
+ // Appendix A: parts[0].type MUST be emitted as "text" for forward
512
+ // compatibility even though consumers ignore it.
513
+ parts: [{ type: "text", text }],
514
+ index: 0,
515
+ },
516
+ ],
517
+ history: historyOf(requestMessage),
518
+ };
519
+ }
520
+ /**
521
+ * Build a `state=failed` Task envelope (spec §4.3 "handler raised"
522
+ * branch). The error text is folded into `status.message.parts[0].text`.
523
+ */
524
+ export function buildFailedTask(taskId, sessionId, requestMessage, errorText) {
525
+ return {
526
+ id: taskId,
527
+ sessionId,
528
+ status: {
529
+ state: A2A_FAILED,
530
+ timestamp: utcIso8601(),
531
+ message: {
532
+ role: "agent",
533
+ parts: [{ type: "text", text: errorText }],
534
+ },
535
+ },
536
+ artifacts: [],
537
+ history: historyOf(requestMessage),
538
+ };
539
+ }
540
+ /**
541
+ * Build a `state=working` Task envelope (spec §4.3 long-running branch /
542
+ * spec §4.4 transient unreachability fallback).
543
+ */
544
+ export function buildWorkingTask(taskId, sessionId, requestMessage, progressMessage, progress) {
545
+ const status = {
546
+ state: A2A_WORKING,
547
+ timestamp: utcIso8601(),
548
+ };
549
+ if (progressMessage && progressMessage.length > 0) {
550
+ status.message = {
551
+ role: "agent",
552
+ parts: [{ type: "text", text: progressMessage }],
553
+ };
554
+ }
555
+ const envelope = {
556
+ id: taskId,
557
+ sessionId,
558
+ status,
559
+ artifacts: [],
560
+ history: historyOf(requestMessage ?? {}),
561
+ };
562
+ if (progress !== undefined && progress !== null) {
563
+ envelope.metadata = { progress };
564
+ }
565
+ return envelope;
566
+ }
567
+ /**
568
+ * Build a `state=canceled` Task envelope (spec §4.5 cancel fallback / spec
569
+ * §7.2). Used when:
570
+ * - `tasks/cancel` post-cancel status read didn't show terminal yet, OR
571
+ * - both `proxy.cancel()` AND `proxy.status()` threw, OR
572
+ * - the parked task lost its `JobProxy` reference.
573
+ */
574
+ export function buildCanceledTask(taskId, sessionId, requestMessage, reason) {
575
+ const status = {
576
+ state: A2A_CANCELED,
577
+ timestamp: utcIso8601(),
578
+ };
579
+ if (reason && reason.length > 0) {
580
+ status.message = {
581
+ role: "agent",
582
+ parts: [{ type: "text", text: reason }],
583
+ };
584
+ }
585
+ return {
586
+ id: taskId,
587
+ sessionId,
588
+ status,
589
+ artifacts: [],
590
+ history: historyOf(requestMessage ?? {}),
591
+ };
592
+ }
593
+ /**
594
+ * Build a Task envelope from a `JobProxy.status()` result dict (spec
595
+ * §4.4). Mirrors Python's `_build_task_from_status` and Java's
596
+ * `MeshA2ADispatcher.buildTaskFromStatus` — folds `error` /
597
+ * `progress_message` into A2A `status.message`, materialises an artifact
598
+ * for completed tasks when the final result is available, and lifts
599
+ * `progress` to `metadata.progress`.
600
+ *
601
+ * Per Appendix A, `progress` is emitted as a real JSON number (no
602
+ * stringification) and `parts[0].type` is always `"text"`.
603
+ */
604
+ export function buildTaskFromStatus(taskId, sessionId, requestMessage, a2aState, meshStatus, finalResult, hasFinalResult) {
605
+ const status = {
606
+ state: a2aState,
607
+ timestamp: utcIso8601(),
608
+ };
609
+ let msgText = null;
610
+ if (a2aState === A2A_FAILED) {
611
+ const err = meshStatus["error"];
612
+ if (err !== null && err !== undefined && String(err).length > 0) {
613
+ msgText = String(err);
614
+ }
615
+ else {
616
+ const pm = meshStatus["progress_message"];
617
+ if (pm !== null && pm !== undefined && String(pm).length > 0) {
618
+ msgText = String(pm);
619
+ }
620
+ }
621
+ }
622
+ else {
623
+ const pm = meshStatus["progress_message"];
624
+ if (pm !== null && pm !== undefined && String(pm).length > 0) {
625
+ msgText = String(pm);
626
+ }
627
+ }
628
+ if (msgText) {
629
+ status.message = {
630
+ role: "agent",
631
+ parts: [{ type: "text", text: msgText }],
632
+ };
633
+ }
634
+ const artifacts = [];
635
+ if (hasFinalResult && a2aState === A2A_COMPLETED) {
636
+ artifacts.push({
637
+ name: "result",
638
+ parts: [{ type: "text", text: stringifyResult(finalResult) }],
639
+ index: 0,
640
+ });
641
+ }
642
+ const envelope = {
643
+ id: taskId,
644
+ sessionId,
645
+ status,
646
+ artifacts,
647
+ history: historyOf(requestMessage ?? {}),
648
+ };
649
+ // Appendix A: `metadata.progress` MUST be a real JSON number. If the
650
+ // underlying status payload exposes `progress` as a non-number
651
+ // (string, boolean, object), omit `metadata` entirely rather than
652
+ // emitting a spec-violating typed-value. Mirrors the SSE emitter's
653
+ // `typeof progress === "number" ? progress : null` coercion.
654
+ const progressRaw = meshStatus["progress"];
655
+ if (typeof progressRaw === "number") {
656
+ envelope.metadata = { progress: progressRaw };
657
+ }
658
+ return envelope;
659
+ }
660
+ /**
661
+ * Stringify a handler return value as the text body of the `result`
662
+ * artifact. `string` returns pass through verbatim; everything else is
663
+ * JSON-stringified. Non-serializable returns fall back to `String(value)`
664
+ * so the artifact is always well-formed (mirrors Python's `default=str`
665
+ * coercion in `a2a.py:403`).
666
+ */
667
+ export function stringifyResult(value) {
668
+ if (value === null || value === undefined) {
669
+ return "";
670
+ }
671
+ if (typeof value === "string") {
672
+ return value;
673
+ }
674
+ try {
675
+ // JSON.stringify returns `undefined` (the literal value, NOT a string)
676
+ // for functions / symbols / and undefined itself. Coerce via String()
677
+ // so the artifact text body is always a real string.
678
+ const serialized = JSON.stringify(value);
679
+ return typeof serialized === "string" ? serialized : String(value);
680
+ }
681
+ catch {
682
+ return String(value);
683
+ }
684
+ }
685
+ function historyOf(requestMessage) {
686
+ if (!requestMessage || !hasOwn(requestMessage)) {
687
+ return [];
688
+ }
689
+ return [{ ...requestMessage }];
690
+ }
691
+ // ─────────────────────────────────────────────────────────────────────────
692
+ // SSE frame builders (spec §5)
693
+ // ─────────────────────────────────────────────────────────────────────────
694
+ /**
695
+ * Build a JSON-RPC envelope carrying an A2A v1.0 `TaskStatusUpdateEvent`
696
+ * (spec §5.2).
697
+ *
698
+ * Per Appendix A:
699
+ * - `final` MUST be a real JSON boolean (no stringification).
700
+ * - `progress` (when non-null) MUST be a JSON number.
701
+ * - `parts[0].type` MUST be emitted as `"text"`.
702
+ *
703
+ * @param reqId JSON-RPC request id to echo
704
+ * @param taskId A2A task id
705
+ * @param a2aState one of the four enumerated A2A states
706
+ * @param messageText optional text for `status.message.parts[0].text`
707
+ * @param finalFlag `true` only on the terminal frame
708
+ * @param progress optional numeric progress; emitted as `metadata.progress`
709
+ */
710
+ export function buildStatusUpdateFrame(reqId, taskId, a2aState, messageText, finalFlag, progress) {
711
+ const status = {
712
+ state: a2aState,
713
+ timestamp: utcIso8601(),
714
+ };
715
+ if (messageText && messageText.length > 0) {
716
+ status.message = {
717
+ role: "agent",
718
+ parts: [{ type: "text", text: messageText }],
719
+ };
720
+ }
721
+ const result = {
722
+ id: taskId,
723
+ status,
724
+ // Appendix A: real boolean, not a string.
725
+ final: finalFlag,
726
+ };
727
+ if (progress !== null && progress !== undefined) {
728
+ result.metadata = { progress };
729
+ }
730
+ return {
731
+ jsonrpc: "2.0",
732
+ id: reqId ?? null,
733
+ result,
734
+ };
735
+ }
736
+ /**
737
+ * Build a JSON-RPC envelope carrying an A2A v1.0 `TaskArtifactUpdateEvent`
738
+ * (spec §5.2). The handler result is stringified per the
739
+ * {@link stringifyResult} contract.
740
+ */
741
+ export function buildArtifactUpdateFrame(reqId, taskId, value) {
742
+ return {
743
+ jsonrpc: "2.0",
744
+ id: reqId ?? null,
745
+ result: {
746
+ id: taskId,
747
+ artifact: {
748
+ name: "result",
749
+ parts: [{ type: "text", text: stringifyResult(value) }],
750
+ index: 0,
751
+ },
752
+ },
753
+ };
754
+ }
755
+ // ─────────────────────────────────────────────────────────────────────────
756
+ // JobProxy detection
757
+ // ─────────────────────────────────────────────────────────────────────────
758
+ /**
759
+ * `instanceof`-based detection with duck-type fallback. JobProxy is a
760
+ * napi-rs class that survives across module loaders; using `instanceof`
761
+ * first matches Java's branching. The duck-type fallback covers any
762
+ * future case where a JobProxy-shaped object is returned (subclass /
763
+ * test double).
764
+ */
765
+ function isJobProxy(value) {
766
+ if (value instanceof JobProxy)
767
+ return true;
768
+ if (value === null || value === undefined)
769
+ return false;
770
+ if (typeof value !== "object")
771
+ return false;
772
+ const v = value;
773
+ return (typeof v["status"] === "function" &&
774
+ typeof v["wait"] === "function" &&
775
+ typeof v["cancel"] === "function" &&
776
+ typeof v["jobId"] === "string");
777
+ }
778
+ // ─────────────────────────────────────────────────────────────────────────
779
+ // Task store helpers
780
+ // ─────────────────────────────────────────────────────────────────────────
781
+ function parkLongRunning(taskStore, taskId, sessionId, message, proxy) {
782
+ taskStore.put(taskId, {
783
+ sessionId,
784
+ requestMessage: hasOwn(message) ? message : undefined,
785
+ // terminalEnvelope + terminalAt undefined: non-terminal record.
786
+ jobProxy: proxy,
787
+ });
788
+ }
789
+ // ─────────────────────────────────────────────────────────────────────────
790
+ // JSON-RPC helpers
791
+ // ─────────────────────────────────────────────────────────────────────────
792
+ function jsonRpcSuccess(reqId, result) {
793
+ return { jsonrpc: "2.0", id: reqId ?? null, result };
794
+ }
795
+ function jsonRpcError(reqId, code, message) {
796
+ return {
797
+ jsonrpc: "2.0",
798
+ error: { code, message },
799
+ id: reqId ?? null,
800
+ };
801
+ }
802
+ function writeJsonRpc(res, body) {
803
+ res.status(200).type("application/json").send(JSON.stringify(body));
804
+ }
805
+ function writeJsonRpcParseErrorHttp400(res, message) {
806
+ res.status(400).type("application/json").send(JSON.stringify({
807
+ jsonrpc: "2.0",
808
+ error: { code: JSONRPC_PARSE_ERROR, message },
809
+ id: null,
810
+ }));
811
+ }
812
+ // ─────────────────────────────────────────────────────────────────────────
813
+ // Param + envelope parsing
814
+ // ─────────────────────────────────────────────────────────────────────────
815
+ function extractRequestId(envelope) {
816
+ // Spec §4.1: `id` MAY be any JSON value the client picks; echo back
817
+ // verbatim (including `null` and 0). Returning `undefined` here causes
818
+ // `jsonRpcSuccess` / `jsonRpcError` to substitute `null` per JSON-RPC.
819
+ if (!Object.prototype.hasOwnProperty.call(envelope, "id")) {
820
+ return undefined;
821
+ }
822
+ return envelope["id"];
823
+ }
824
+ function readParams(params) {
825
+ if (params === null || params === undefined)
826
+ return {};
827
+ if (typeof params !== "object" || Array.isArray(params))
828
+ return {};
829
+ return params;
830
+ }
831
+ function stringFromParams(params, key) {
832
+ const v = params[key];
833
+ if (v === null || v === undefined)
834
+ return null;
835
+ if (typeof v === "string")
836
+ return v.length === 0 ? null : v;
837
+ return String(v);
838
+ }
839
+ function mapFromParams(params, key) {
840
+ const v = params[key];
841
+ if (v === null || v === undefined)
842
+ return {};
843
+ if (typeof v !== "object" || Array.isArray(v))
844
+ return {};
845
+ return { ...v };
846
+ }
847
+ function hasOwn(obj) {
848
+ if (!obj)
849
+ return false;
850
+ for (const _key in obj) {
851
+ if (Object.prototype.hasOwnProperty.call(obj, _key))
852
+ return true;
853
+ }
854
+ return false;
855
+ }
856
+ function errorTextOf(err) {
857
+ if (err instanceof Error) {
858
+ return err.message || err.name || "Error";
859
+ }
860
+ if (typeof err === "string")
861
+ return err;
862
+ try {
863
+ // JSON.stringify returns `undefined` (the literal value, NOT a string)
864
+ // for functions / symbols / and undefined itself. Coerce via String()
865
+ // so callers always receive a real string.
866
+ const serialized = JSON.stringify(err);
867
+ return typeof serialized === "string" ? serialized : String(err);
868
+ }
869
+ catch {
870
+ return String(err);
871
+ }
872
+ }
873
+ /**
874
+ * UTC ISO-8601 with the `Z` suffix (NOT `+00:00`) per spec §5.2 /
875
+ * Appendix A. `Date.prototype.toISOString()` already emits the right form
876
+ * (`2026-05-11T12:34:56.789Z`).
877
+ */
878
+ function utcIso8601() {
879
+ return new Date().toISOString();
880
+ }
881
+ function sseError(errorBody, httpStatus) {
882
+ return { kind: "error", errorBody, httpStatus };
883
+ }
884
+ function sseSingleFrame(frame) {
885
+ return { kind: "single-frame", frame };
886
+ }
887
+ function sseSyncCompleted(reqId, taskId, artifactFrame, terminalFrame) {
888
+ return { kind: "sync-completed", reqId, taskId, artifactFrame, terminalFrame };
889
+ }
890
+ function sseLongRunning(reqId, taskId, proxy) {
891
+ return { kind: "long-running", reqId, taskId, proxy };
892
+ }
893
+ // Re-export helpers under more obvious names for the SSE emitter so it
894
+ // doesn't need to import every individual builder.
895
+ export { isJobProxy as __isJobProxyForTests, };
896
+ //# sourceMappingURL=dispatcher.js.map