@mcpmesh/sdk 1.3.4 → 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.
- package/dist/__tests__/a2a/a2a-bearer.spec.d.ts +2 -0
- package/dist/__tests__/a2a/a2a-bearer.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/a2a-bearer.spec.js +58 -0
- package/dist/__tests__/a2a/a2a-bearer.spec.js.map +1 -0
- package/dist/__tests__/a2a/a2a-client.spec.d.ts +2 -0
- package/dist/__tests__/a2a/a2a-client.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/a2a-client.spec.js +334 -0
- package/dist/__tests__/a2a/a2a-client.spec.js.map +1 -0
- package/dist/__tests__/a2a/a2a-job.spec.d.ts +2 -0
- package/dist/__tests__/a2a/a2a-job.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/a2a-job.spec.js +255 -0
- package/dist/__tests__/a2a/a2a-job.spec.js.map +1 -0
- package/dist/__tests__/a2a/a2a-stream.spec.d.ts +2 -0
- package/dist/__tests__/a2a/a2a-stream.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/a2a-stream.spec.js +278 -0
- package/dist/__tests__/a2a/a2a-stream.spec.js.map +1 -0
- package/dist/__tests__/a2a/agent-a2a-config.spec.d.ts +2 -0
- package/dist/__tests__/a2a/agent-a2a-config.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/agent-a2a-config.spec.js +262 -0
- package/dist/__tests__/a2a/agent-a2a-config.spec.js.map +1 -0
- package/dist/__tests__/a2a/producer/auth-filter.spec.d.ts +2 -0
- package/dist/__tests__/a2a/producer/auth-filter.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/producer/auth-filter.spec.js +127 -0
- package/dist/__tests__/a2a/producer/auth-filter.spec.js.map +1 -0
- package/dist/__tests__/a2a/producer/card-builder.spec.d.ts +2 -0
- package/dist/__tests__/a2a/producer/card-builder.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/producer/card-builder.spec.js +113 -0
- package/dist/__tests__/a2a/producer/card-builder.spec.js.map +1 -0
- package/dist/__tests__/a2a/producer/dispatcher.spec.d.ts +2 -0
- package/dist/__tests__/a2a/producer/dispatcher.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/producer/dispatcher.spec.js +850 -0
- package/dist/__tests__/a2a/producer/dispatcher.spec.js.map +1 -0
- package/dist/__tests__/a2a/producer/mount-surface-push.spec.d.ts +2 -0
- package/dist/__tests__/a2a/producer/mount-surface-push.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/producer/mount-surface-push.spec.js +164 -0
- package/dist/__tests__/a2a/producer/mount-surface-push.spec.js.map +1 -0
- package/dist/__tests__/a2a/producer/mount.spec.d.ts +2 -0
- package/dist/__tests__/a2a/producer/mount.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/producer/mount.spec.js +433 -0
- package/dist/__tests__/a2a/producer/mount.spec.js.map +1 -0
- package/dist/__tests__/a2a/producer/public-url-cache.spec.d.ts +2 -0
- package/dist/__tests__/a2a/producer/public-url-cache.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/producer/public-url-cache.spec.js +116 -0
- package/dist/__tests__/a2a/producer/public-url-cache.spec.js.map +1 -0
- package/dist/__tests__/a2a/producer/sse-emitter.spec.d.ts +2 -0
- package/dist/__tests__/a2a/producer/sse-emitter.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/producer/sse-emitter.spec.js +754 -0
- package/dist/__tests__/a2a/producer/sse-emitter.spec.js.map +1 -0
- package/dist/__tests__/a2a/producer/state-translator.spec.d.ts +2 -0
- package/dist/__tests__/a2a/producer/state-translator.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/producer/state-translator.spec.js +124 -0
- package/dist/__tests__/a2a/producer/state-translator.spec.js.map +1 -0
- package/dist/__tests__/a2a/producer/task-store.spec.d.ts +2 -0
- package/dist/__tests__/a2a/producer/task-store.spec.d.ts.map +1 -0
- package/dist/__tests__/a2a/producer/task-store.spec.js +180 -0
- package/dist/__tests__/a2a/producer/task-store.spec.js.map +1 -0
- package/dist/__tests__/agent-add-tool.spec.d.ts +2 -0
- package/dist/__tests__/agent-add-tool.spec.d.ts.map +1 -0
- package/dist/__tests__/agent-add-tool.spec.js +483 -0
- package/dist/__tests__/agent-add-tool.spec.js.map +1 -0
- package/dist/__tests__/api-runtime-race.spec.d.ts +2 -0
- package/dist/__tests__/api-runtime-race.spec.d.ts.map +1 -0
- package/dist/__tests__/api-runtime-race.spec.js +193 -0
- package/dist/__tests__/api-runtime-race.spec.js.map +1 -0
- package/dist/__tests__/claim-dispatcher.spec.d.ts +2 -0
- package/dist/__tests__/claim-dispatcher.spec.d.ts.map +1 -0
- package/dist/__tests__/claim-dispatcher.spec.js +408 -0
- package/dist/__tests__/claim-dispatcher.spec.js.map +1 -0
- package/dist/__tests__/inbound-job-dispatch.spec.d.ts +2 -0
- package/dist/__tests__/inbound-job-dispatch.spec.d.ts.map +1 -0
- package/dist/__tests__/inbound-job-dispatch.spec.js +185 -0
- package/dist/__tests__/inbound-job-dispatch.spec.js.map +1 -0
- package/dist/__tests__/job-controller-progress.spec.d.ts +2 -0
- package/dist/__tests__/job-controller-progress.spec.d.ts.map +1 -0
- package/dist/__tests__/job-controller-progress.spec.js +85 -0
- package/dist/__tests__/job-controller-progress.spec.js.map +1 -0
- package/dist/__tests__/jobs-cancel-route.spec.d.ts +2 -0
- package/dist/__tests__/jobs-cancel-route.spec.d.ts.map +1 -0
- package/dist/__tests__/jobs-cancel-route.spec.js +88 -0
- package/dist/__tests__/jobs-cancel-route.spec.js.map +1 -0
- package/dist/__tests__/llm-agent-stream.test.d.ts +14 -0
- package/dist/__tests__/llm-agent-stream.test.d.ts.map +1 -0
- package/dist/__tests__/llm-agent-stream.test.js +341 -0
- package/dist/__tests__/llm-agent-stream.test.js.map +1 -0
- package/dist/__tests__/llm-provider.test.js +22 -1
- package/dist/__tests__/llm-provider.test.js.map +1 -1
- package/dist/__tests__/media-resolver.test.js +40 -0
- package/dist/__tests__/media-resolver.test.js.map +1 -1
- package/dist/__tests__/mesh-job-submitter.spec.d.ts +2 -0
- package/dist/__tests__/mesh-job-submitter.spec.d.ts.map +1 -0
- package/dist/__tests__/mesh-job-submitter.spec.js +110 -0
- package/dist/__tests__/mesh-job-submitter.spec.js.map +1 -0
- package/dist/__tests__/proxy-stream.test.d.ts +9 -0
- package/dist/__tests__/proxy-stream.test.d.ts.map +1 -0
- package/dist/__tests__/proxy-stream.test.js +347 -0
- package/dist/__tests__/proxy-stream.test.js.map +1 -0
- package/dist/__tests__/resolver-meshjob.spec.d.ts +26 -0
- package/dist/__tests__/resolver-meshjob.spec.d.ts.map +1 -0
- package/dist/__tests__/resolver-meshjob.spec.js +201 -0
- package/dist/__tests__/resolver-meshjob.spec.js.map +1 -0
- package/dist/__tests__/schema-verdict-policy.test.d.ts +6 -0
- package/dist/__tests__/schema-verdict-policy.test.d.ts.map +1 -0
- package/dist/__tests__/schema-verdict-policy.test.js +126 -0
- package/dist/__tests__/schema-verdict-policy.test.js.map +1 -0
- package/dist/__tests__/sse-stream.test.d.ts +12 -0
- package/dist/__tests__/sse-stream.test.d.ts.map +1 -0
- package/dist/__tests__/sse-stream.test.js +170 -0
- package/dist/__tests__/sse-stream.test.js.map +1 -0
- package/dist/a2a/a2a-bearer.d.ts +27 -0
- package/dist/a2a/a2a-bearer.d.ts.map +1 -0
- package/dist/a2a/a2a-bearer.js +63 -0
- package/dist/a2a/a2a-bearer.js.map +1 -0
- package/dist/a2a/a2a-client.d.ts +114 -0
- package/dist/a2a/a2a-client.d.ts.map +1 -0
- package/dist/a2a/a2a-client.js +405 -0
- package/dist/a2a/a2a-client.js.map +1 -0
- package/dist/a2a/a2a-event.d.ts +25 -0
- package/dist/a2a/a2a-event.d.ts.map +1 -0
- package/dist/a2a/a2a-event.js +9 -0
- package/dist/a2a/a2a-event.js.map +1 -0
- package/dist/a2a/a2a-job.d.ts +58 -0
- package/dist/a2a/a2a-job.d.ts.map +1 -0
- package/dist/a2a/a2a-job.js +264 -0
- package/dist/a2a/a2a-job.js.map +1 -0
- package/dist/a2a/a2a-stream.d.ts +39 -0
- package/dist/a2a/a2a-stream.d.ts.map +1 -0
- package/dist/a2a/a2a-stream.js +290 -0
- package/dist/a2a/a2a-stream.js.map +1 -0
- package/dist/a2a/errors.d.ts +29 -0
- package/dist/a2a/errors.d.ts.map +1 -0
- package/dist/a2a/errors.js +48 -0
- package/dist/a2a/errors.js.map +1 -0
- package/dist/a2a/index.d.ts +12 -0
- package/dist/a2a/index.d.ts.map +1 -0
- package/dist/a2a/index.js +11 -0
- package/dist/a2a/index.js.map +1 -0
- package/dist/a2a/producer/auth-filter.d.ts +34 -0
- package/dist/a2a/producer/auth-filter.d.ts.map +1 -0
- package/dist/a2a/producer/auth-filter.js +39 -0
- package/dist/a2a/producer/auth-filter.js.map +1 -0
- package/dist/a2a/producer/card-builder.d.ts +59 -0
- package/dist/a2a/producer/card-builder.d.ts.map +1 -0
- package/dist/a2a/producer/card-builder.js +59 -0
- package/dist/a2a/producer/card-builder.js.map +1 -0
- package/dist/a2a/producer/dispatcher.d.ts +276 -0
- package/dist/a2a/producer/dispatcher.d.ts.map +1 -0
- package/dist/a2a/producer/dispatcher.js +896 -0
- package/dist/a2a/producer/dispatcher.js.map +1 -0
- package/dist/a2a/producer/index.d.ts +26 -0
- package/dist/a2a/producer/index.d.ts.map +1 -0
- package/dist/a2a/producer/index.js +23 -0
- package/dist/a2a/producer/index.js.map +1 -0
- package/dist/a2a/producer/mount.d.ts +75 -0
- package/dist/a2a/producer/mount.d.ts.map +1 -0
- package/dist/a2a/producer/mount.js +422 -0
- package/dist/a2a/producer/mount.js.map +1 -0
- package/dist/a2a/producer/public-url-cache.d.ts +73 -0
- package/dist/a2a/producer/public-url-cache.d.ts.map +1 -0
- package/dist/a2a/producer/public-url-cache.js +0 -0
- package/dist/a2a/producer/public-url-cache.js.map +1 -0
- package/dist/a2a/producer/registry.d.ts +138 -0
- package/dist/a2a/producer/registry.d.ts.map +1 -0
- package/dist/a2a/producer/registry.js +117 -0
- package/dist/a2a/producer/registry.js.map +1 -0
- package/dist/a2a/producer/sse-emitter.d.ts +85 -0
- package/dist/a2a/producer/sse-emitter.d.ts.map +1 -0
- package/dist/a2a/producer/sse-emitter.js +405 -0
- package/dist/a2a/producer/sse-emitter.js.map +1 -0
- package/dist/a2a/producer/state-translator.d.ts +63 -0
- package/dist/a2a/producer/state-translator.d.ts.map +1 -0
- package/dist/a2a/producer/state-translator.js +108 -0
- package/dist/a2a/producer/state-translator.js.map +1 -0
- package/dist/a2a/producer/task-store.d.ts +128 -0
- package/dist/a2a/producer/task-store.d.ts.map +1 -0
- package/dist/a2a/producer/task-store.js +128 -0
- package/dist/a2a/producer/task-store.js.map +1 -0
- package/dist/agent.d.ts +99 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +754 -19
- package/dist/agent.js.map +1 -1
- package/dist/api-runtime.d.ts +25 -0
- package/dist/api-runtime.d.ts.map +1 -1
- package/dist/api-runtime.js +75 -2
- package/dist/api-runtime.js.map +1 -1
- package/dist/claim-dispatcher.d.ts +126 -0
- package/dist/claim-dispatcher.d.ts.map +1 -0
- package/dist/claim-dispatcher.js +478 -0
- package/dist/claim-dispatcher.js.map +1 -0
- package/dist/express.d.ts.map +1 -1
- package/dist/express.js +33 -6
- package/dist/express.js.map +1 -1
- package/dist/inbound-job-dispatch.d.ts +105 -0
- package/dist/inbound-job-dispatch.d.ts.map +1 -0
- package/dist/inbound-job-dispatch.js +335 -0
- package/dist/inbound-job-dispatch.js.map +1 -0
- package/dist/index.d.ts +40 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -3
- package/dist/index.js.map +1 -1
- package/dist/job-context.d.ts +107 -0
- package/dist/job-context.d.ts.map +1 -0
- package/dist/job-context.js +95 -0
- package/dist/job-context.js.map +1 -0
- package/dist/jobs-cancel-route.d.ts +36 -0
- package/dist/jobs-cancel-route.d.ts.map +1 -0
- package/dist/jobs-cancel-route.js +60 -0
- package/dist/jobs-cancel-route.js.map +1 -0
- package/dist/jobs-helper-tools.d.ts +48 -0
- package/dist/jobs-helper-tools.d.ts.map +1 -0
- package/dist/jobs-helper-tools.js +133 -0
- package/dist/jobs-helper-tools.js.map +1 -0
- package/dist/llm-agent.d.ts +62 -53
- package/dist/llm-agent.d.ts.map +1 -1
- package/dist/llm-agent.js +211 -292
- package/dist/llm-agent.js.map +1 -1
- package/dist/llm-provider.d.ts +11 -4
- package/dist/llm-provider.d.ts.map +1 -1
- package/dist/llm-provider.js +57 -4
- package/dist/llm-provider.js.map +1 -1
- package/dist/llm.d.ts +4 -1
- package/dist/llm.d.ts.map +1 -1
- package/dist/llm.js +7 -17
- package/dist/llm.js.map +1 -1
- package/dist/media/resolver.d.ts.map +1 -1
- package/dist/media/resolver.js +3 -2
- package/dist/media/resolver.js.map +1 -1
- package/dist/mesh-job-submitter.d.ts +83 -0
- package/dist/mesh-job-submitter.d.ts.map +1 -0
- package/dist/mesh-job-submitter.js +143 -0
- package/dist/mesh-job-submitter.js.map +1 -0
- package/dist/provider-handlers/gemini-handler.js +5 -0
- package/dist/provider-handlers/gemini-handler.js.map +1 -1
- package/dist/proxy.d.ts +40 -0
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +375 -2
- package/dist/proxy.js.map +1 -1
- package/dist/resolver-meshjob.d.ts +170 -0
- package/dist/resolver-meshjob.d.ts.map +1 -0
- package/dist/resolver-meshjob.js +159 -0
- package/dist/resolver-meshjob.js.map +1 -0
- package/dist/route.d.ts +4 -0
- package/dist/route.d.ts.map +1 -1
- package/dist/route.js.map +1 -1
- package/dist/schema-normalize.d.ts +62 -0
- package/dist/schema-normalize.d.ts.map +1 -0
- package/dist/schema-normalize.js +128 -0
- package/dist/schema-normalize.js.map +1 -0
- package/dist/sse-stream.d.ts +44 -0
- package/dist/sse-stream.d.ts.map +1 -0
- package/dist/sse-stream.js +173 -0
- package/dist/sse-stream.js.map +1 -0
- package/dist/tool-worker-entry.d.ts +21 -0
- package/dist/tool-worker-entry.d.ts.map +1 -0
- package/dist/tool-worker-entry.js +162 -0
- package/dist/tool-worker-entry.js.map +1 -0
- package/dist/tool-worker-pool.d.ts +49 -0
- package/dist/tool-worker-pool.d.ts.map +1 -0
- package/dist/tool-worker-pool.js +272 -0
- package/dist/tool-worker-pool.js.map +1 -0
- package/dist/types.d.ts +351 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -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
|