@nifrajs/a2a 3.2.0

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/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @nifrajs/a2a
2
+
3
+ Mount a nifra agent as an [Agent2Agent (A2A)](https://a2a-protocol.org) protocol server. The agent card is served on GET, the A2A 1.0 JSON-RPC binding on POST: `SendMessage` runs one bounded agent turn, `SendStreamingMessage` streams step evidence over SSE, `GetTask` projects the stored turn state.
4
+
5
+ A protocol bridge over [`@nifrajs/agent`](../agent) - the request body reuses core's single bounded, prototype-guarded trust boundary, and the model, durable state store, and approval transport are injected per request through the same `ports` factory `mountAgent` uses. Dependency-free beyond the two nifra peers.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @nifrajs/a2a @nifrajs/agent
11
+ ```
12
+
13
+ ## Mount
14
+
15
+ ```ts
16
+ import { server } from "@nifrajs/core"
17
+ import { mountA2A } from "@nifrajs/a2a"
18
+ import { agent } from "./agent" // an AgentDefinition
19
+
20
+ const app = server()
21
+ mountA2A(app, {
22
+ agent,
23
+ card: { url: "https://api.example.com/a2a", version: "1.0.0" },
24
+ ports: (c) => ({
25
+ model: myModelPort, // scope to the request subject
26
+ capabilities: ["search.read"],
27
+ state: myStateStore, // required for GetTask and resume
28
+ }),
29
+ })
30
+ // GET /.well-known/agent-card.json - the agent card
31
+ // POST /a2a - SendMessage | SendStreamingMessage | GetTask
32
+ ```
33
+
34
+ ## Task mapping
35
+
36
+ One A2A task is one agent turn: `taskId` is the runner's `turnId`.
37
+
38
+ | Run result | Task state |
39
+ | --- | --- |
40
+ | completed with output | `TASK_STATE_COMPLETED` + an `output` artifact |
41
+ | completed with a typed error | `TASK_STATE_FAILED` (error in `metadata`) |
42
+ | suspended (approval, budget, model, max turns) | `TASK_STATE_INPUT_REQUIRED` |
43
+ | suspended (cancelled) | `TASK_STATE_CANCELED` |
44
+
45
+ Agent input is `message.metadata.input` when present, otherwise the first `text` part. Structured output returns as a `text` part carrying JSON.
46
+
47
+ ## Human-in-the-loop resume
48
+
49
+ A suspended task's status message carries the pending continuation in `metadata`. Send it back to continue the same task - the runtime keeps state token-only, so a suspended tool's input must be replayed in `continuation.input`:
50
+
51
+ ```jsonc
52
+ {
53
+ "jsonrpc": "2.0", "id": 2, "method": "SendMessage",
54
+ "params": {
55
+ "message": {
56
+ "messageId": "m2", "role": "ROLE_USER", "parts": [],
57
+ "taskId": "<task id from the suspension>",
58
+ "metadata": {
59
+ "input": { "prompt": "go" },
60
+ "resume": {
61
+ "continuation": { "kind": "approval", "tool": "search.read", "effectId": "…", "input": { "q": "…" } },
62
+ "approval": { "granted": true }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ Cross-request registries (cancellation, subscription fan-out, push notifications) are deliberately out of scope for the stateless seam; those spec methods answer `UnsupportedOperationError` (-32004).
71
+
72
+ The seam performs no authentication or authorization - wrap it with your app's route guards and scope every port in `ports(c)` to the caller.
73
+
74
+ ## Docs
75
+
76
+ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one core, five UI libraries, every runtime. Scaffold a new app with `bun create nifra`.
77
+
78
+ For AI agents, see [`LLM.md`](./LLM.md) and the full corpus
79
+ [`../../llms-full.txt`](../../llms-full.txt).
@@ -0,0 +1,107 @@
1
+ /**
2
+ * `@nifrajs/a2a` - mount a nifra agent as an Agent2Agent (A2A) protocol server.
3
+ *
4
+ * The package is a protocol bridge over the `@nifrajs/agent` runner, mirroring the `mountAgent`
5
+ * seam: HTTP in, the bounded runner in the middle, A2A JSON-RPC out. The request body is read
6
+ * through core's single bounded, proto-guarded framing lane (`@nifrajs/core/edge-kit`); the model,
7
+ * durable state store, and approval transport arrive through a per-request `ports` factory and never
8
+ * live in this package.
9
+ *
10
+ * Wire contract (A2A protocol version 1.0, JSON-RPC binding):
11
+ * - `GET {cardPath}` (default `/.well-known/agent-card.json`) -> the agent card.
12
+ * - `POST {path}` (default `/a2a`) -> a JSON-RPC 2.0 request:
13
+ * - `SendMessage` runs one turn and returns `{ task }` with a terminal or interrupted status.
14
+ * - `SendStreamingMessage` responds `text/event-stream`; each SSE `data:` field is a complete
15
+ * JSON-RPC response whose result is `{ statusUpdate }` per step-evidence item, then
16
+ * `{ artifactUpdate }` for a produced output, then a final `{ statusUpdate }` with
17
+ * `final: true`.
18
+ * - `GetTask` projects the stored turn state for a task id (requires `ports.state`).
19
+ * - Every other spec method returns `UnsupportedOperationError` (-32004): the seam is stateless
20
+ * per request, so cross-request cancellation and subscription registries belong to the caller.
21
+ * - A task is one agent turn: `taskId` == the runner's `turnId`. A suspended run surfaces as
22
+ * `TASK_STATE_INPUT_REQUIRED` whose status message carries the pending continuation in
23
+ * `metadata`. To resume, send the continuation back via `message.taskId` +
24
+ * `message.metadata.resume` (`{ continuation, approval? }`); the runtime keeps state token-only,
25
+ * so a suspended tool's input must be replayed in `continuation.input`.
26
+ * - Agent input is `message.metadata.input` when present, otherwise the first `text` part.
27
+ * Structured output is returned as a `text` part carrying JSON.
28
+ * - The seam performs no authentication or authorization. Wrap it with the app's own route guards,
29
+ * and scope the store/model returned by `ports` to the caller.
30
+ */
31
+ import { type AgentDefinition, type AgentPorts } from "@nifrajs/agent";
32
+ import { type ProtoPoisoning } from "@nifrajs/core/edge-kit";
33
+ import type { StandardSchemaV1 } from "@nifrajs/core/schema";
34
+ export declare const A2A_PROTOCOL_VERSION = "1.0";
35
+ export declare const AGENT_CARD_WELL_KNOWN_PATH = "/.well-known/agent-card.json";
36
+ export declare const A2A_ERROR_CODES: {
37
+ readonly parseError: -32700;
38
+ readonly invalidRequest: -32600;
39
+ readonly methodNotFound: -32601;
40
+ readonly invalidParams: -32602;
41
+ readonly internalError: -32603;
42
+ readonly taskNotFound: -32001;
43
+ readonly unsupportedOperation: -32004;
44
+ };
45
+ export type A2ATaskState = "TASK_STATE_SUBMITTED" | "TASK_STATE_WORKING" | "TASK_STATE_INPUT_REQUIRED" | "TASK_STATE_COMPLETED" | "TASK_STATE_FAILED" | "TASK_STATE_CANCELED";
46
+ export interface A2AAgentSkill {
47
+ readonly id: string;
48
+ readonly name: string;
49
+ readonly description: string;
50
+ readonly tags: readonly string[];
51
+ }
52
+ export interface A2AAgentCard {
53
+ readonly protocolVersion: string;
54
+ readonly name: string;
55
+ readonly description: string;
56
+ readonly url: string;
57
+ readonly version: string;
58
+ readonly capabilities: {
59
+ readonly streaming: boolean;
60
+ };
61
+ readonly defaultInputModes: readonly string[];
62
+ readonly defaultOutputModes: readonly string[];
63
+ readonly skills: readonly A2AAgentSkill[];
64
+ }
65
+ /** Caller-supplied card identity; everything else derives from the agent definition. */
66
+ export interface A2ACardInfo {
67
+ /** Public URL of the JSON-RPC endpoint, as advertised to A2A clients. */
68
+ readonly url: string;
69
+ /** Version of the exposed agent (the deployment's, not the protocol's). */
70
+ readonly version: string;
71
+ readonly name?: string;
72
+ readonly description?: string;
73
+ }
74
+ /** The structural slice of a route context the seam needs. */
75
+ export interface A2ARouteContext {
76
+ readonly req: Request;
77
+ }
78
+ /** The structural slice of a nifra server `mountA2A` needs. */
79
+ export interface A2AMountableApp {
80
+ get(path: string, handler: (c: A2ARouteContext) => Response | Promise<Response>): unknown;
81
+ post(path: string, handler: (c: A2ARouteContext) => Response | Promise<Response>): unknown;
82
+ }
83
+ export interface MountA2AOptions<InputSchema extends StandardSchemaV1, OutputSchema extends StandardSchemaV1> {
84
+ readonly agent: AgentDefinition<InputSchema, OutputSchema>;
85
+ readonly card: A2ACardInfo;
86
+ /** JSON-RPC endpoint path. Default `/a2a`. */
87
+ readonly path?: string;
88
+ /** Agent card path. Default `/.well-known/agent-card.json`. */
89
+ readonly cardPath?: string;
90
+ /** Bounded run length passed to the runner. */
91
+ readonly maxTurns?: number;
92
+ /** Maximum request body size in bytes. Default 1_000_000. */
93
+ readonly maxBodyBytes?: number;
94
+ /** Prototype-poisoning policy for the framing lane. Default `"reject"`. */
95
+ readonly protoPoisoning?: ProtoPoisoning;
96
+ /**
97
+ * Build the ports for one request - the model, durable state store, approval transport,
98
+ * capabilities, and budgets. Receives the route context so the caller can scope every port to the
99
+ * request subject.
100
+ */
101
+ readonly ports: (c: A2ARouteContext) => AgentPorts | Promise<AgentPorts>;
102
+ }
103
+ /** Derive a spec-shaped agent card from the agent definition plus the caller's identity fields. */
104
+ export declare function agentCard<InputSchema extends StandardSchemaV1, OutputSchema extends StandardSchemaV1>(agent: AgentDefinition<InputSchema, OutputSchema>, info: A2ACardInfo): A2AAgentCard;
105
+ /** Mount an agent as an A2A server: the agent card on GET, the JSON-RPC binding on POST. */
106
+ export declare function mountA2A<InputSchema extends StandardSchemaV1, OutputSchema extends StandardSchemaV1>(app: A2AMountableApp, options: MountA2AOptions<InputSchema, OutputSchema>): void;
107
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,UAAU,EAQhB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAEL,KAAK,cAAc,EAIpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,eAAO,MAAM,oBAAoB,QAAQ,CAAA;AACzC,eAAO,MAAM,0BAA0B,iCAAiC,CAAA;AAQxE,eAAO,MAAM,eAAe;;;;;;;;CAQlB,CAAA;AAEV,MAAM,MAAM,YAAY,GACpB,sBAAsB,GACtB,oBAAoB,GACpB,2BAA2B,GAC3B,sBAAsB,GACtB,mBAAmB,GACnB,qBAAqB,CAAA;AAEzB,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;CACjC;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,YAAY,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAA;IACtD,QAAQ,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7C,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAA;IAC9C,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAA;CAC1C;AAED,wFAAwF;AACxF,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB;AAED,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAA;IACzF,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAA;CAC3F;AAED,MAAM,WAAW,eAAe,CAC9B,WAAW,SAAS,gBAAgB,EACpC,YAAY,SAAS,gBAAgB;IAErC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;IAC1D,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAA;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;CACzE;AAED,mGAAmG;AACnG,wBAAgB,SAAS,CACvB,WAAW,SAAS,gBAAgB,EACpC,YAAY,SAAS,gBAAgB,EACrC,KAAK,EAAE,eAAe,CAAC,WAAW,EAAE,YAAY,CAAC,EAAE,IAAI,EAAE,WAAW,GAAG,YAAY,CAiBpF;AAED,4FAA4F;AAC5F,wBAAgB,QAAQ,CACtB,WAAW,SAAS,gBAAgB,EACpC,YAAY,SAAS,gBAAgB,EACrC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,eAAe,CAAC,WAAW,EAAE,YAAY,CAAC,GAAG,IAAI,CAmBjF"}
package/dist/index.js ADDED
@@ -0,0 +1,369 @@
1
+ /**
2
+ * `@nifrajs/a2a` - mount a nifra agent as an Agent2Agent (A2A) protocol server.
3
+ *
4
+ * The package is a protocol bridge over the `@nifrajs/agent` runner, mirroring the `mountAgent`
5
+ * seam: HTTP in, the bounded runner in the middle, A2A JSON-RPC out. The request body is read
6
+ * through core's single bounded, proto-guarded framing lane (`@nifrajs/core/edge-kit`); the model,
7
+ * durable state store, and approval transport arrive through a per-request `ports` factory and never
8
+ * live in this package.
9
+ *
10
+ * Wire contract (A2A protocol version 1.0, JSON-RPC binding):
11
+ * - `GET {cardPath}` (default `/.well-known/agent-card.json`) -> the agent card.
12
+ * - `POST {path}` (default `/a2a`) -> a JSON-RPC 2.0 request:
13
+ * - `SendMessage` runs one turn and returns `{ task }` with a terminal or interrupted status.
14
+ * - `SendStreamingMessage` responds `text/event-stream`; each SSE `data:` field is a complete
15
+ * JSON-RPC response whose result is `{ statusUpdate }` per step-evidence item, then
16
+ * `{ artifactUpdate }` for a produced output, then a final `{ statusUpdate }` with
17
+ * `final: true`.
18
+ * - `GetTask` projects the stored turn state for a task id (requires `ports.state`).
19
+ * - Every other spec method returns `UnsupportedOperationError` (-32004): the seam is stateless
20
+ * per request, so cross-request cancellation and subscription registries belong to the caller.
21
+ * - A task is one agent turn: `taskId` == the runner's `turnId`. A suspended run surfaces as
22
+ * `TASK_STATE_INPUT_REQUIRED` whose status message carries the pending continuation in
23
+ * `metadata`. To resume, send the continuation back via `message.taskId` +
24
+ * `message.metadata.resume` (`{ continuation, approval? }`); the runtime keeps state token-only,
25
+ * so a suspended tool's input must be replayed in `continuation.input`.
26
+ * - Agent input is `message.metadata.input` when present, otherwise the first `text` part.
27
+ * Structured output is returned as a `text` part carrying JSON.
28
+ * - The seam performs no authentication or authorization. Wrap it with the app's own route guards,
29
+ * and scope the store/model returned by `ports` to the caller.
30
+ */
31
+ import { combineAgentTelemetry, createAgentState, resumeAgent, runAgent, } from "@nifrajs/agent";
32
+ import { createAgentEvidenceStream } from "@nifrajs/agent/events";
33
+ import { EMPTY_RESPONSE_CONTROLS, readBodyFramed, toResponse, } from "@nifrajs/core/edge-kit";
34
+ export const A2A_PROTOCOL_VERSION = "1.0";
35
+ export const AGENT_CARD_WELL_KNOWN_PATH = "/.well-known/agent-card.json";
36
+ const DEFAULT_PATH = "/a2a";
37
+ const DEFAULT_MAX_BODY_BYTES = 1_000_000;
38
+ const PENDING_KINDS = ["approval", "budget", "model", "cancelled"];
39
+ const TURN_ID_PATTERN = /^[a-zA-Z0-9._:-]{1,128}$/;
40
+ // JSON-RPC 2.0 standard codes plus the A2A-specific range.
41
+ export const A2A_ERROR_CODES = {
42
+ parseError: -32700,
43
+ invalidRequest: -32600,
44
+ methodNotFound: -32601,
45
+ invalidParams: -32602,
46
+ internalError: -32603,
47
+ taskNotFound: -32001,
48
+ unsupportedOperation: -32004,
49
+ };
50
+ /** Derive a spec-shaped agent card from the agent definition plus the caller's identity fields. */
51
+ export function agentCard(agent, info) {
52
+ return {
53
+ protocolVersion: A2A_PROTOCOL_VERSION,
54
+ name: info.name ?? agent.name,
55
+ description: info.description ?? agent.instruction,
56
+ url: info.url,
57
+ version: info.version,
58
+ capabilities: { streaming: true },
59
+ defaultInputModes: ["application/json", "text/plain"],
60
+ defaultOutputModes: ["application/json", "text/plain"],
61
+ skills: agent.tools.map((tool) => ({
62
+ id: tool.name,
63
+ name: tool.name,
64
+ description: tool.description,
65
+ tags: [tool.capability],
66
+ })),
67
+ };
68
+ }
69
+ /** Mount an agent as an A2A server: the agent card on GET, the JSON-RPC binding on POST. */
70
+ export function mountA2A(app, options) {
71
+ const path = options.path ?? DEFAULT_PATH;
72
+ const cardPath = options.cardPath ?? AGENT_CARD_WELL_KNOWN_PATH;
73
+ const maxBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
74
+ const proto = options.protoPoisoning ?? "reject";
75
+ const runOptions = options.maxTurns === undefined ? {} : { maxTurns: options.maxTurns };
76
+ const card = agentCard(options.agent, options.card);
77
+ app.get(cardPath, () => jsonResponse(200, card));
78
+ app.post(path, (c) => readBodyFramed(c.req, maxBytes, proto, (parsed) => dispatch(c, parsed, options, runOptions), (rejection) => render(rejection), () => rpcErrorResponse(null, A2A_ERROR_CODES.parseError, "parse_error")).catch(() => rpcErrorResponse(null, A2A_ERROR_CODES.internalError, "internal_error")));
79
+ }
80
+ async function dispatch(c, parsed, options, runOptions) {
81
+ const request = asRecord(parsed);
82
+ const id = request === undefined ? undefined : asRpcId(request.id);
83
+ if (request === undefined || request.jsonrpc !== "2.0" || id === undefined)
84
+ return rpcErrorResponse(null, A2A_ERROR_CODES.invalidRequest, "invalid_request");
85
+ const method = request.method;
86
+ if (typeof method !== "string")
87
+ return rpcErrorResponse(id, A2A_ERROR_CODES.invalidRequest, "invalid_request");
88
+ const params = asRecord(request.params) ?? {};
89
+ switch (method) {
90
+ case "SendMessage":
91
+ case "SendStreamingMessage": {
92
+ const turn = prepareTurn(params);
93
+ if ("error" in turn)
94
+ return rpcErrorResponse(id, turn.error.code, turn.error.message);
95
+ const basePorts = await options.ports(c);
96
+ const start = (telemetry) => {
97
+ // Compose rather than replace: the SSE evidence stream must not displace a telemetry
98
+ // port the caller injected through `ports`.
99
+ const combined = combineAgentTelemetry(basePorts.telemetry, telemetry);
100
+ const ports = combined === undefined ? basePorts : { ...basePorts, telemetry: combined };
101
+ if (turn.resume !== undefined) {
102
+ return resumeAgent(options.agent, turn.turnId, { value: turn.input, resume: turn.resume }, ports, runOptions);
103
+ }
104
+ return runAgent(options.agent, { value: turn.input }, ports, {
105
+ ...runOptions,
106
+ state: createAgentState(turn.turnId),
107
+ });
108
+ };
109
+ if (method === "SendMessage") {
110
+ try {
111
+ return rpcResultResponse(id, { task: projectTask(turn.turnId, await start()) });
112
+ }
113
+ catch {
114
+ return rpcErrorResponse(id, A2A_ERROR_CODES.taskNotFound, "task_not_resumable");
115
+ }
116
+ }
117
+ return streamingResponse(id, turn.turnId, start);
118
+ }
119
+ case "GetTask": {
120
+ const taskId = typeof params.id === "string" ? params.id : undefined;
121
+ if (taskId === undefined)
122
+ return rpcErrorResponse(id, A2A_ERROR_CODES.invalidParams, "invalid_params");
123
+ const basePorts = await options.ports(c);
124
+ const state = await basePorts.state?.load(taskId);
125
+ if (state === undefined)
126
+ return rpcErrorResponse(id, A2A_ERROR_CODES.taskNotFound, "task_not_found");
127
+ return rpcResultResponse(id, { task: projectStoredTask(state) });
128
+ }
129
+ case "ListTasks":
130
+ case "CancelTask":
131
+ case "SubscribeToTask":
132
+ case "CreateTaskPushNotificationConfig":
133
+ case "GetTaskPushNotificationConfig":
134
+ case "ListTaskPushNotificationConfigs":
135
+ case "DeleteTaskPushNotificationConfig":
136
+ case "GetExtendedAgentCard":
137
+ return rpcErrorResponse(id, A2A_ERROR_CODES.unsupportedOperation, "unsupported_operation");
138
+ default:
139
+ return rpcErrorResponse(id, A2A_ERROR_CODES.methodNotFound, "method_not_found");
140
+ }
141
+ }
142
+ function prepareTurn(params) {
143
+ const message = asRecord(params.message);
144
+ if (message === undefined)
145
+ return { error: { code: A2A_ERROR_CODES.invalidParams, message: "invalid_params" } };
146
+ const metadata = asRecord(message.metadata) ?? {};
147
+ const taskId = typeof message.taskId === "string" ? message.taskId : undefined;
148
+ const resume = parseResume(metadata.resume);
149
+ if (taskId !== undefined && !TURN_ID_PATTERN.test(taskId))
150
+ return { error: { code: A2A_ERROR_CODES.invalidParams, message: "invalid_task_id" } };
151
+ // A message that names a task must carry the continuation: the runtime keeps state token-only,
152
+ // so there is nothing to continue from without it.
153
+ if (taskId !== undefined && resume === undefined)
154
+ return { error: { code: A2A_ERROR_CODES.invalidParams, message: "resume_metadata_required" } };
155
+ if (resume !== undefined && taskId === undefined)
156
+ return { error: { code: A2A_ERROR_CODES.invalidParams, message: "task_id_required" } };
157
+ return {
158
+ turnId: taskId ?? crypto.randomUUID(),
159
+ input: Object.hasOwn(metadata, "input") ? metadata.input : firstTextPart(message.parts),
160
+ resume,
161
+ };
162
+ }
163
+ function firstTextPart(parts) {
164
+ if (!Array.isArray(parts))
165
+ return undefined;
166
+ for (const part of parts) {
167
+ const record = asRecord(part);
168
+ if (record !== undefined && typeof record.text === "string")
169
+ return record.text;
170
+ }
171
+ return undefined;
172
+ }
173
+ function streamingResponse(id, turnId, start) {
174
+ const stream = createAgentEvidenceStream();
175
+ const body = new ReadableStream({
176
+ async start(controller) {
177
+ const encoder = new TextEncoder();
178
+ const send = (payload) => {
179
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`));
180
+ };
181
+ const run = start(stream);
182
+ // The evidence stream must terminate whether the run resolves or throws, or the `for await`
183
+ // below would hang; the run result (or the error) is still reported in-band after it drains.
184
+ run
185
+ .then(() => { }, () => { })
186
+ .finally(() => stream.complete());
187
+ try {
188
+ for await (const evidence of stream) {
189
+ send(rpcResult(id, {
190
+ statusUpdate: {
191
+ taskId: turnId,
192
+ contextId: turnId,
193
+ status: { state: "TASK_STATE_WORKING" },
194
+ final: false,
195
+ metadata: { evidence },
196
+ },
197
+ }));
198
+ }
199
+ const result = await run;
200
+ if (result.status === "completed" && result.error === undefined) {
201
+ send(rpcResult(id, {
202
+ artifactUpdate: {
203
+ taskId: turnId,
204
+ contextId: turnId,
205
+ artifact: outputArtifact(turnId, result.output),
206
+ lastChunk: true,
207
+ },
208
+ }));
209
+ }
210
+ const task = projectTask(turnId, result);
211
+ send(rpcResult(id, {
212
+ statusUpdate: {
213
+ taskId: turnId,
214
+ contextId: turnId,
215
+ status: task.status,
216
+ final: true,
217
+ ...(task.metadata === undefined ? {} : { metadata: task.metadata }),
218
+ },
219
+ }));
220
+ }
221
+ catch {
222
+ send(rpcError(id, A2A_ERROR_CODES.internalError, "run_failed"));
223
+ }
224
+ finally {
225
+ controller.close();
226
+ }
227
+ },
228
+ });
229
+ return new Response(body, {
230
+ status: 200,
231
+ headers: {
232
+ "content-type": "text/event-stream; charset=utf-8",
233
+ "cache-control": "no-cache, no-transform",
234
+ connection: "keep-alive",
235
+ },
236
+ });
237
+ }
238
+ /** Project a run result onto the A2A task shape. Evidence is token-only by design, so it passes through. */
239
+ function projectTask(turnId, result) {
240
+ const base = { id: turnId, contextId: turnId };
241
+ if (result.status === "completed") {
242
+ if (result.error !== undefined) {
243
+ return {
244
+ ...base,
245
+ status: { state: "TASK_STATE_FAILED" },
246
+ metadata: { evidence: result.evidence, error: result.error },
247
+ };
248
+ }
249
+ return {
250
+ ...base,
251
+ status: { state: "TASK_STATE_COMPLETED" },
252
+ artifacts: [outputArtifact(turnId, result.output)],
253
+ metadata: { evidence: result.evidence },
254
+ };
255
+ }
256
+ if (result.status === "suspended") {
257
+ const state = result.reason === "cancelled" ? "TASK_STATE_CANCELED" : "TASK_STATE_INPUT_REQUIRED";
258
+ return {
259
+ ...base,
260
+ status: {
261
+ state,
262
+ message: {
263
+ messageId: `${turnId}:pending`,
264
+ taskId: turnId,
265
+ role: "ROLE_AGENT",
266
+ parts: [],
267
+ metadata: { reason: result.reason, continuation: result.pending },
268
+ },
269
+ },
270
+ metadata: { evidence: result.evidence },
271
+ };
272
+ }
273
+ // `continue` never escapes the run loop; project it defensively as still-working.
274
+ return {
275
+ ...base,
276
+ status: { state: "TASK_STATE_WORKING" },
277
+ metadata: { evidence: result.evidence },
278
+ };
279
+ }
280
+ /** Project stored turn state (GetTask). Stored state cannot distinguish a failed completion. */
281
+ function projectStoredTask(state) {
282
+ const taskState = state.status === "completed"
283
+ ? "TASK_STATE_COMPLETED"
284
+ : state.status === "suspended"
285
+ ? "TASK_STATE_INPUT_REQUIRED"
286
+ : "TASK_STATE_WORKING";
287
+ return {
288
+ id: state.turnId,
289
+ contextId: state.turnId,
290
+ status: { state: taskState },
291
+ metadata: {
292
+ evidence: state.evidence,
293
+ ...(state.pending === undefined ? {} : { continuation: state.pending }),
294
+ },
295
+ };
296
+ }
297
+ function outputArtifact(turnId, output) {
298
+ return {
299
+ artifactId: `${turnId}:output`,
300
+ name: "output",
301
+ parts: [typeof output === "string" ? { text: output } : { text: JSON.stringify(output) }],
302
+ };
303
+ }
304
+ function parseResume(raw) {
305
+ const record = asRecord(raw);
306
+ if (record === undefined)
307
+ return undefined;
308
+ const continuation = asRecord(record.continuation);
309
+ if (continuation === undefined)
310
+ return undefined;
311
+ const kind = continuation.kind;
312
+ if (typeof kind !== "string" || !PENDING_KINDS.includes(kind))
313
+ return undefined;
314
+ if (typeof continuation.effectId !== "string")
315
+ return undefined;
316
+ if (continuation.tool !== undefined && typeof continuation.tool !== "string")
317
+ return undefined;
318
+ const approval = asRecord(record.approval);
319
+ return {
320
+ continuation: {
321
+ kind: kind,
322
+ ...(continuation.tool === undefined ? {} : { tool: continuation.tool }),
323
+ ...(Object.hasOwn(continuation, "input") ? { input: continuation.input } : {}),
324
+ effectId: continuation.effectId,
325
+ },
326
+ ...(approval !== undefined && typeof approval.granted === "boolean"
327
+ ? {
328
+ approval: {
329
+ granted: approval.granted,
330
+ ...(typeof approval.reason === "string" ? { reason: approval.reason } : {}),
331
+ },
332
+ }
333
+ : {}),
334
+ };
335
+ }
336
+ function rpcResult(id, result) {
337
+ return { jsonrpc: "2.0", id, result };
338
+ }
339
+ function rpcError(id, code, message) {
340
+ return { jsonrpc: "2.0", id, error: { code, message } };
341
+ }
342
+ function rpcResultResponse(id, result) {
343
+ return jsonResponse(200, rpcResult(id, result));
344
+ }
345
+ function rpcErrorResponse(id, code, message) {
346
+ return jsonResponse(200, rpcError(id, code, message));
347
+ }
348
+ function asRpcId(value) {
349
+ if (typeof value === "string")
350
+ return value;
351
+ if (typeof value === "number" && Number.isFinite(value))
352
+ return value;
353
+ return undefined;
354
+ }
355
+ function asRecord(value) {
356
+ if (value === null || typeof value !== "object" || Array.isArray(value))
357
+ return undefined;
358
+ return value;
359
+ }
360
+ function jsonResponse(status, body) {
361
+ return new Response(JSON.stringify(body), {
362
+ status,
363
+ headers: { "content-type": "application/json; charset=utf-8" },
364
+ });
365
+ }
366
+ function render(response) {
367
+ return response instanceof Response ? response : toResponse(response, EMPTY_RESPONSE_CONTROLS);
368
+ }
369
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@nifrajs/a2a",
3
+ "version": "3.2.0",
4
+ "description": "Mount a nifra agent as an Agent2Agent (A2A) protocol server - the agent card on GET, the A2A 1.0 JSON-RPC binding on POST with SendMessage, streaming SendStreamingMessage over SSE, and GetTask projected from the runner's turn state. A protocol bridge over @nifrajs/agent; the model, state store, and approval transport stay injected per request.",
5
+ "keywords": [
6
+ "bun",
7
+ "typescript",
8
+ "nifra",
9
+ "a2a",
10
+ "agent2agent",
11
+ "agents",
12
+ "json-rpc",
13
+ "sse",
14
+ "interop"
15
+ ],
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "bun": "./src/index.ts",
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src"
30
+ ],
31
+ "sideEffects": false,
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
34
+ },
35
+ "peerDependencies": {
36
+ "@nifrajs/agent": "^3.2.0",
37
+ "@nifrajs/core": "^3.2.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@nifrajs/core": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "devDependencies": {
45
+ "@nifrajs/agent": "workspace:*",
46
+ "@nifrajs/core": "workspace:*",
47
+ "@nifrajs/schema": "workspace:*"
48
+ },
49
+ "license": "MIT",
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "git+https://github.com/nifrajs/nifra.git",
56
+ "directory": "packages/a2a"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/nifrajs/nifra/issues"
60
+ },
61
+ "homepage": "https://github.com/nifrajs/nifra#readme"
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,528 @@
1
+ /**
2
+ * `@nifrajs/a2a` - mount a nifra agent as an Agent2Agent (A2A) protocol server.
3
+ *
4
+ * The package is a protocol bridge over the `@nifrajs/agent` runner, mirroring the `mountAgent`
5
+ * seam: HTTP in, the bounded runner in the middle, A2A JSON-RPC out. The request body is read
6
+ * through core's single bounded, proto-guarded framing lane (`@nifrajs/core/edge-kit`); the model,
7
+ * durable state store, and approval transport arrive through a per-request `ports` factory and never
8
+ * live in this package.
9
+ *
10
+ * Wire contract (A2A protocol version 1.0, JSON-RPC binding):
11
+ * - `GET {cardPath}` (default `/.well-known/agent-card.json`) -> the agent card.
12
+ * - `POST {path}` (default `/a2a`) -> a JSON-RPC 2.0 request:
13
+ * - `SendMessage` runs one turn and returns `{ task }` with a terminal or interrupted status.
14
+ * - `SendStreamingMessage` responds `text/event-stream`; each SSE `data:` field is a complete
15
+ * JSON-RPC response whose result is `{ statusUpdate }` per step-evidence item, then
16
+ * `{ artifactUpdate }` for a produced output, then a final `{ statusUpdate }` with
17
+ * `final: true`.
18
+ * - `GetTask` projects the stored turn state for a task id (requires `ports.state`).
19
+ * - Every other spec method returns `UnsupportedOperationError` (-32004): the seam is stateless
20
+ * per request, so cross-request cancellation and subscription registries belong to the caller.
21
+ * - A task is one agent turn: `taskId` == the runner's `turnId`. A suspended run surfaces as
22
+ * `TASK_STATE_INPUT_REQUIRED` whose status message carries the pending continuation in
23
+ * `metadata`. To resume, send the continuation back via `message.taskId` +
24
+ * `message.metadata.resume` (`{ continuation, approval? }`); the runtime keeps state token-only,
25
+ * so a suspended tool's input must be replayed in `continuation.input`.
26
+ * - Agent input is `message.metadata.input` when present, otherwise the first `text` part.
27
+ * Structured output is returned as a `text` part carrying JSON.
28
+ * - The seam performs no authentication or authorization. Wrap it with the app's own route guards,
29
+ * and scope the store/model returned by `ports` to the caller.
30
+ */
31
+
32
+ import {
33
+ type AgentDefinition,
34
+ type AgentPendingKind,
35
+ type AgentPorts,
36
+ type AgentRunResult,
37
+ type AgentTurnInput,
38
+ type AgentTurnState,
39
+ combineAgentTelemetry,
40
+ createAgentState,
41
+ resumeAgent,
42
+ runAgent,
43
+ } from "@nifrajs/agent"
44
+ import { createAgentEvidenceStream } from "@nifrajs/agent/events"
45
+ import {
46
+ EMPTY_RESPONSE_CONTROLS,
47
+ type ProtoPoisoning,
48
+ type ResponseResult,
49
+ readBodyFramed,
50
+ toResponse,
51
+ } from "@nifrajs/core/edge-kit"
52
+ import type { StandardSchemaV1 } from "@nifrajs/core/schema"
53
+
54
+ export const A2A_PROTOCOL_VERSION = "1.0"
55
+ export const AGENT_CARD_WELL_KNOWN_PATH = "/.well-known/agent-card.json"
56
+
57
+ const DEFAULT_PATH = "/a2a"
58
+ const DEFAULT_MAX_BODY_BYTES = 1_000_000
59
+ const PENDING_KINDS: readonly AgentPendingKind[] = ["approval", "budget", "model", "cancelled"]
60
+ const TURN_ID_PATTERN = /^[a-zA-Z0-9._:-]{1,128}$/
61
+
62
+ // JSON-RPC 2.0 standard codes plus the A2A-specific range.
63
+ export const A2A_ERROR_CODES = {
64
+ parseError: -32700,
65
+ invalidRequest: -32600,
66
+ methodNotFound: -32601,
67
+ invalidParams: -32602,
68
+ internalError: -32603,
69
+ taskNotFound: -32001,
70
+ unsupportedOperation: -32004,
71
+ } as const
72
+
73
+ export type A2ATaskState =
74
+ | "TASK_STATE_SUBMITTED"
75
+ | "TASK_STATE_WORKING"
76
+ | "TASK_STATE_INPUT_REQUIRED"
77
+ | "TASK_STATE_COMPLETED"
78
+ | "TASK_STATE_FAILED"
79
+ | "TASK_STATE_CANCELED"
80
+
81
+ export interface A2AAgentSkill {
82
+ readonly id: string
83
+ readonly name: string
84
+ readonly description: string
85
+ readonly tags: readonly string[]
86
+ }
87
+
88
+ export interface A2AAgentCard {
89
+ readonly protocolVersion: string
90
+ readonly name: string
91
+ readonly description: string
92
+ readonly url: string
93
+ readonly version: string
94
+ readonly capabilities: { readonly streaming: boolean }
95
+ readonly defaultInputModes: readonly string[]
96
+ readonly defaultOutputModes: readonly string[]
97
+ readonly skills: readonly A2AAgentSkill[]
98
+ }
99
+
100
+ /** Caller-supplied card identity; everything else derives from the agent definition. */
101
+ export interface A2ACardInfo {
102
+ /** Public URL of the JSON-RPC endpoint, as advertised to A2A clients. */
103
+ readonly url: string
104
+ /** Version of the exposed agent (the deployment's, not the protocol's). */
105
+ readonly version: string
106
+ readonly name?: string
107
+ readonly description?: string
108
+ }
109
+
110
+ /** The structural slice of a route context the seam needs. */
111
+ export interface A2ARouteContext {
112
+ readonly req: Request
113
+ }
114
+
115
+ /** The structural slice of a nifra server `mountA2A` needs. */
116
+ export interface A2AMountableApp {
117
+ get(path: string, handler: (c: A2ARouteContext) => Response | Promise<Response>): unknown
118
+ post(path: string, handler: (c: A2ARouteContext) => Response | Promise<Response>): unknown
119
+ }
120
+
121
+ export interface MountA2AOptions<
122
+ InputSchema extends StandardSchemaV1,
123
+ OutputSchema extends StandardSchemaV1,
124
+ > {
125
+ readonly agent: AgentDefinition<InputSchema, OutputSchema>
126
+ readonly card: A2ACardInfo
127
+ /** JSON-RPC endpoint path. Default `/a2a`. */
128
+ readonly path?: string
129
+ /** Agent card path. Default `/.well-known/agent-card.json`. */
130
+ readonly cardPath?: string
131
+ /** Bounded run length passed to the runner. */
132
+ readonly maxTurns?: number
133
+ /** Maximum request body size in bytes. Default 1_000_000. */
134
+ readonly maxBodyBytes?: number
135
+ /** Prototype-poisoning policy for the framing lane. Default `"reject"`. */
136
+ readonly protoPoisoning?: ProtoPoisoning
137
+ /**
138
+ * Build the ports for one request - the model, durable state store, approval transport,
139
+ * capabilities, and budgets. Receives the route context so the caller can scope every port to the
140
+ * request subject.
141
+ */
142
+ readonly ports: (c: A2ARouteContext) => AgentPorts | Promise<AgentPorts>
143
+ }
144
+
145
+ /** Derive a spec-shaped agent card from the agent definition plus the caller's identity fields. */
146
+ export function agentCard<
147
+ InputSchema extends StandardSchemaV1,
148
+ OutputSchema extends StandardSchemaV1,
149
+ >(agent: AgentDefinition<InputSchema, OutputSchema>, info: A2ACardInfo): A2AAgentCard {
150
+ return {
151
+ protocolVersion: A2A_PROTOCOL_VERSION,
152
+ name: info.name ?? agent.name,
153
+ description: info.description ?? agent.instruction,
154
+ url: info.url,
155
+ version: info.version,
156
+ capabilities: { streaming: true },
157
+ defaultInputModes: ["application/json", "text/plain"],
158
+ defaultOutputModes: ["application/json", "text/plain"],
159
+ skills: agent.tools.map((tool) => ({
160
+ id: tool.name,
161
+ name: tool.name,
162
+ description: tool.description,
163
+ tags: [tool.capability],
164
+ })),
165
+ }
166
+ }
167
+
168
+ /** Mount an agent as an A2A server: the agent card on GET, the JSON-RPC binding on POST. */
169
+ export function mountA2A<
170
+ InputSchema extends StandardSchemaV1,
171
+ OutputSchema extends StandardSchemaV1,
172
+ >(app: A2AMountableApp, options: MountA2AOptions<InputSchema, OutputSchema>): void {
173
+ const path = options.path ?? DEFAULT_PATH
174
+ const cardPath = options.cardPath ?? AGENT_CARD_WELL_KNOWN_PATH
175
+ const maxBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
176
+ const proto = options.protoPoisoning ?? "reject"
177
+ const runOptions = options.maxTurns === undefined ? {} : { maxTurns: options.maxTurns }
178
+ const card = agentCard(options.agent, options.card)
179
+
180
+ app.get(cardPath, () => jsonResponse(200, card))
181
+ app.post(path, (c) =>
182
+ readBodyFramed<Response>(
183
+ c.req,
184
+ maxBytes,
185
+ proto,
186
+ (parsed) => dispatch(c, parsed, options, runOptions),
187
+ (rejection) => render(rejection),
188
+ () => rpcErrorResponse(null, A2A_ERROR_CODES.parseError, "parse_error"),
189
+ ).catch(() => rpcErrorResponse(null, A2A_ERROR_CODES.internalError, "internal_error")),
190
+ )
191
+ }
192
+
193
+ type RpcId = string | number
194
+ type RunStart = (telemetry?: AgentPorts["telemetry"]) => Promise<AgentRunResult<unknown>>
195
+
196
+ async function dispatch<
197
+ InputSchema extends StandardSchemaV1,
198
+ OutputSchema extends StandardSchemaV1,
199
+ >(
200
+ c: A2ARouteContext,
201
+ parsed: unknown,
202
+ options: MountA2AOptions<InputSchema, OutputSchema>,
203
+ runOptions: { readonly maxTurns?: number },
204
+ ): Promise<Response> {
205
+ const request = asRecord(parsed)
206
+ const id = request === undefined ? undefined : asRpcId(request.id)
207
+ if (request === undefined || request.jsonrpc !== "2.0" || id === undefined)
208
+ return rpcErrorResponse(null, A2A_ERROR_CODES.invalidRequest, "invalid_request")
209
+ const method = request.method
210
+ if (typeof method !== "string")
211
+ return rpcErrorResponse(id, A2A_ERROR_CODES.invalidRequest, "invalid_request")
212
+ const params = asRecord(request.params) ?? {}
213
+
214
+ switch (method) {
215
+ case "SendMessage":
216
+ case "SendStreamingMessage": {
217
+ const turn = prepareTurn(params)
218
+ if ("error" in turn) return rpcErrorResponse(id, turn.error.code, turn.error.message)
219
+ const basePorts = await options.ports(c)
220
+ const start: RunStart = (telemetry) => {
221
+ // Compose rather than replace: the SSE evidence stream must not displace a telemetry
222
+ // port the caller injected through `ports`.
223
+ const combined = combineAgentTelemetry(basePorts.telemetry, telemetry)
224
+ const ports = combined === undefined ? basePorts : { ...basePorts, telemetry: combined }
225
+ if (turn.resume !== undefined) {
226
+ return resumeAgent(
227
+ options.agent,
228
+ turn.turnId,
229
+ { value: turn.input, resume: turn.resume },
230
+ ports,
231
+ runOptions,
232
+ )
233
+ }
234
+ return runAgent(options.agent, { value: turn.input }, ports, {
235
+ ...runOptions,
236
+ state: createAgentState(turn.turnId),
237
+ })
238
+ }
239
+ if (method === "SendMessage") {
240
+ try {
241
+ return rpcResultResponse(id, { task: projectTask(turn.turnId, await start()) })
242
+ } catch {
243
+ return rpcErrorResponse(id, A2A_ERROR_CODES.taskNotFound, "task_not_resumable")
244
+ }
245
+ }
246
+ return streamingResponse(id, turn.turnId, start)
247
+ }
248
+ case "GetTask": {
249
+ const taskId = typeof params.id === "string" ? params.id : undefined
250
+ if (taskId === undefined)
251
+ return rpcErrorResponse(id, A2A_ERROR_CODES.invalidParams, "invalid_params")
252
+ const basePorts = await options.ports(c)
253
+ const state = await basePorts.state?.load(taskId)
254
+ if (state === undefined)
255
+ return rpcErrorResponse(id, A2A_ERROR_CODES.taskNotFound, "task_not_found")
256
+ return rpcResultResponse(id, { task: projectStoredTask(state) })
257
+ }
258
+ case "ListTasks":
259
+ case "CancelTask":
260
+ case "SubscribeToTask":
261
+ case "CreateTaskPushNotificationConfig":
262
+ case "GetTaskPushNotificationConfig":
263
+ case "ListTaskPushNotificationConfigs":
264
+ case "DeleteTaskPushNotificationConfig":
265
+ case "GetExtendedAgentCard":
266
+ return rpcErrorResponse(id, A2A_ERROR_CODES.unsupportedOperation, "unsupported_operation")
267
+ default:
268
+ return rpcErrorResponse(id, A2A_ERROR_CODES.methodNotFound, "method_not_found")
269
+ }
270
+ }
271
+
272
+ interface PreparedTurn {
273
+ readonly turnId: string
274
+ readonly input: unknown
275
+ readonly resume: AgentTurnInput["resume"]
276
+ }
277
+
278
+ function prepareTurn(
279
+ params: Record<string, unknown>,
280
+ ): PreparedTurn | { readonly error: { readonly code: number; readonly message: string } } {
281
+ const message = asRecord(params.message)
282
+ if (message === undefined)
283
+ return { error: { code: A2A_ERROR_CODES.invalidParams, message: "invalid_params" } }
284
+ const metadata = asRecord(message.metadata) ?? {}
285
+ const taskId = typeof message.taskId === "string" ? message.taskId : undefined
286
+ const resume = parseResume(metadata.resume)
287
+ if (taskId !== undefined && !TURN_ID_PATTERN.test(taskId))
288
+ return { error: { code: A2A_ERROR_CODES.invalidParams, message: "invalid_task_id" } }
289
+ // A message that names a task must carry the continuation: the runtime keeps state token-only,
290
+ // so there is nothing to continue from without it.
291
+ if (taskId !== undefined && resume === undefined)
292
+ return { error: { code: A2A_ERROR_CODES.invalidParams, message: "resume_metadata_required" } }
293
+ if (resume !== undefined && taskId === undefined)
294
+ return { error: { code: A2A_ERROR_CODES.invalidParams, message: "task_id_required" } }
295
+ return {
296
+ turnId: taskId ?? crypto.randomUUID(),
297
+ input: Object.hasOwn(metadata, "input") ? metadata.input : firstTextPart(message.parts),
298
+ resume,
299
+ }
300
+ }
301
+
302
+ function firstTextPart(parts: unknown): unknown {
303
+ if (!Array.isArray(parts)) return undefined
304
+ for (const part of parts) {
305
+ const record = asRecord(part)
306
+ if (record !== undefined && typeof record.text === "string") return record.text
307
+ }
308
+ return undefined
309
+ }
310
+
311
+ function streamingResponse(id: RpcId, turnId: string, start: RunStart): Response {
312
+ const stream = createAgentEvidenceStream()
313
+ const body = new ReadableStream<Uint8Array>({
314
+ async start(controller) {
315
+ const encoder = new TextEncoder()
316
+ const send = (payload: unknown): void => {
317
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`))
318
+ }
319
+ const run = start(stream)
320
+ // The evidence stream must terminate whether the run resolves or throws, or the `for await`
321
+ // below would hang; the run result (or the error) is still reported in-band after it drains.
322
+ run
323
+ .then(
324
+ () => {},
325
+ () => {},
326
+ )
327
+ .finally(() => stream.complete())
328
+ try {
329
+ for await (const evidence of stream) {
330
+ send(
331
+ rpcResult(id, {
332
+ statusUpdate: {
333
+ taskId: turnId,
334
+ contextId: turnId,
335
+ status: { state: "TASK_STATE_WORKING" satisfies A2ATaskState },
336
+ final: false,
337
+ metadata: { evidence },
338
+ },
339
+ }),
340
+ )
341
+ }
342
+ const result = await run
343
+ if (result.status === "completed" && result.error === undefined) {
344
+ send(
345
+ rpcResult(id, {
346
+ artifactUpdate: {
347
+ taskId: turnId,
348
+ contextId: turnId,
349
+ artifact: outputArtifact(turnId, result.output),
350
+ lastChunk: true,
351
+ },
352
+ }),
353
+ )
354
+ }
355
+ const task = projectTask(turnId, result)
356
+ send(
357
+ rpcResult(id, {
358
+ statusUpdate: {
359
+ taskId: turnId,
360
+ contextId: turnId,
361
+ status: task.status,
362
+ final: true,
363
+ ...(task.metadata === undefined ? {} : { metadata: task.metadata }),
364
+ },
365
+ }),
366
+ )
367
+ } catch {
368
+ send(rpcError(id, A2A_ERROR_CODES.internalError, "run_failed"))
369
+ } finally {
370
+ controller.close()
371
+ }
372
+ },
373
+ })
374
+ return new Response(body, {
375
+ status: 200,
376
+ headers: {
377
+ "content-type": "text/event-stream; charset=utf-8",
378
+ "cache-control": "no-cache, no-transform",
379
+ connection: "keep-alive",
380
+ },
381
+ })
382
+ }
383
+
384
+ interface ProjectedTask {
385
+ readonly id: string
386
+ readonly contextId: string
387
+ readonly status: { readonly state: A2ATaskState; readonly message?: unknown }
388
+ readonly artifacts?: readonly unknown[]
389
+ readonly metadata?: Record<string, unknown>
390
+ }
391
+
392
+ /** Project a run result onto the A2A task shape. Evidence is token-only by design, so it passes through. */
393
+ function projectTask(turnId: string, result: AgentRunResult<unknown>): ProjectedTask {
394
+ const base = { id: turnId, contextId: turnId }
395
+ if (result.status === "completed") {
396
+ if (result.error !== undefined) {
397
+ return {
398
+ ...base,
399
+ status: { state: "TASK_STATE_FAILED" },
400
+ metadata: { evidence: result.evidence, error: result.error },
401
+ }
402
+ }
403
+ return {
404
+ ...base,
405
+ status: { state: "TASK_STATE_COMPLETED" },
406
+ artifacts: [outputArtifact(turnId, result.output)],
407
+ metadata: { evidence: result.evidence },
408
+ }
409
+ }
410
+ if (result.status === "suspended") {
411
+ const state: A2ATaskState =
412
+ result.reason === "cancelled" ? "TASK_STATE_CANCELED" : "TASK_STATE_INPUT_REQUIRED"
413
+ return {
414
+ ...base,
415
+ status: {
416
+ state,
417
+ message: {
418
+ messageId: `${turnId}:pending`,
419
+ taskId: turnId,
420
+ role: "ROLE_AGENT",
421
+ parts: [],
422
+ metadata: { reason: result.reason, continuation: result.pending },
423
+ },
424
+ },
425
+ metadata: { evidence: result.evidence },
426
+ }
427
+ }
428
+ // `continue` never escapes the run loop; project it defensively as still-working.
429
+ return {
430
+ ...base,
431
+ status: { state: "TASK_STATE_WORKING" },
432
+ metadata: { evidence: result.evidence },
433
+ }
434
+ }
435
+
436
+ /** Project stored turn state (GetTask). Stored state cannot distinguish a failed completion. */
437
+ function projectStoredTask(state: AgentTurnState): ProjectedTask {
438
+ const taskState: A2ATaskState =
439
+ state.status === "completed"
440
+ ? "TASK_STATE_COMPLETED"
441
+ : state.status === "suspended"
442
+ ? "TASK_STATE_INPUT_REQUIRED"
443
+ : "TASK_STATE_WORKING"
444
+ return {
445
+ id: state.turnId,
446
+ contextId: state.turnId,
447
+ status: { state: taskState },
448
+ metadata: {
449
+ evidence: state.evidence,
450
+ ...(state.pending === undefined ? {} : { continuation: state.pending }),
451
+ },
452
+ }
453
+ }
454
+
455
+ function outputArtifact(turnId: string, output: unknown): Record<string, unknown> {
456
+ return {
457
+ artifactId: `${turnId}:output`,
458
+ name: "output",
459
+ parts: [typeof output === "string" ? { text: output } : { text: JSON.stringify(output) }],
460
+ }
461
+ }
462
+
463
+ function parseResume(raw: unknown): AgentTurnInput["resume"] | undefined {
464
+ const record = asRecord(raw)
465
+ if (record === undefined) return undefined
466
+ const continuation = asRecord(record.continuation)
467
+ if (continuation === undefined) return undefined
468
+ const kind = continuation.kind
469
+ if (typeof kind !== "string" || !PENDING_KINDS.includes(kind as AgentPendingKind))
470
+ return undefined
471
+ if (typeof continuation.effectId !== "string") return undefined
472
+ if (continuation.tool !== undefined && typeof continuation.tool !== "string") return undefined
473
+ const approval = asRecord(record.approval)
474
+ return {
475
+ continuation: {
476
+ kind: kind as AgentPendingKind,
477
+ ...(continuation.tool === undefined ? {} : { tool: continuation.tool as string }),
478
+ ...(Object.hasOwn(continuation, "input") ? { input: continuation.input } : {}),
479
+ effectId: continuation.effectId,
480
+ },
481
+ ...(approval !== undefined && typeof approval.granted === "boolean"
482
+ ? {
483
+ approval: {
484
+ granted: approval.granted,
485
+ ...(typeof approval.reason === "string" ? { reason: approval.reason } : {}),
486
+ },
487
+ }
488
+ : {}),
489
+ }
490
+ }
491
+
492
+ function rpcResult(id: RpcId, result: unknown): Record<string, unknown> {
493
+ return { jsonrpc: "2.0", id, result }
494
+ }
495
+
496
+ function rpcError(id: RpcId | null, code: number, message: string): Record<string, unknown> {
497
+ return { jsonrpc: "2.0", id, error: { code, message } }
498
+ }
499
+
500
+ function rpcResultResponse(id: RpcId, result: unknown): Response {
501
+ return jsonResponse(200, rpcResult(id, result))
502
+ }
503
+
504
+ function rpcErrorResponse(id: RpcId | null, code: number, message: string): Response {
505
+ return jsonResponse(200, rpcError(id, code, message))
506
+ }
507
+
508
+ function asRpcId(value: unknown): RpcId | undefined {
509
+ if (typeof value === "string") return value
510
+ if (typeof value === "number" && Number.isFinite(value)) return value
511
+ return undefined
512
+ }
513
+
514
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
515
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined
516
+ return value as Record<string, unknown>
517
+ }
518
+
519
+ function jsonResponse(status: number, body: unknown): Response {
520
+ return new Response(JSON.stringify(body), {
521
+ status,
522
+ headers: { "content-type": "application/json; charset=utf-8" },
523
+ })
524
+ }
525
+
526
+ function render(response: Response | ResponseResult): Response {
527
+ return response instanceof Response ? response : toResponse(response, EMPTY_RESPONSE_CONTROLS)
528
+ }