@sema-agent/core 5.2.0 → 5.3.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.
@@ -0,0 +1,651 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Type } from "typebox";
3
+ import { A2A_TASK_STATES } from "./a2a-task-state.js";
4
+ import { describeHttpTransportFailure, resolveProtocolHttpHeaders } from "./mcp.js";
5
+ import { mintNamespacePrefix, mintNamespacedToolName } from "./protocol-naming.js";
6
+ import { A2A_NAMESPACE } from "./protocol-table.js";
7
+ import { delimitUntrusted, inlineUntrusted } from "./untrusted-text.js";
8
+ const A2A_REQUEST_TIMEOUT_MS = 30_000;
9
+ const A2A_CARD_TIMEOUT_MS = 10_000;
10
+ const A2A_CALL_TOTAL_TIMEOUT_MS = 10 * 60_000;
11
+ const A2A_STATE_IDLE_TIMEOUT_MS = 120_000;
12
+ const A2A_POLL_FIRST_DELAY_MS = 500;
13
+ const A2A_POLL_MAX_DELAY_MS = 5_000;
14
+ const A2A_POLL_BACKOFF_FACTOR = 2;
15
+ const A2A_RESPONSE_MAX_BYTES = 8 * 1024 * 1024;
16
+ const A2A_SKILL_NAME_MAX_CHARS = 200;
17
+ const A2A_SKILL_TEXT_MAX_CHARS = 1_200;
18
+ const A2A_SKILL_TAGS_MAX_CHARS = 240;
19
+ const A2A_SKILL_EXAMPLES_MAX_CHARS = 320;
20
+ export const A2A_SKILL_DESCRIPTION_MAX_CHARS = 2_048;
21
+ const A2A_CARD_NAME_MAX_CHARS = 160;
22
+ const A2A_CARD_DESCRIPTION_MAX_CHARS = 240;
23
+ const A2A_ID_MAX_CHARS = 160;
24
+ const A2A_RESULT_BODY_MAX_CHARS = 100_000;
25
+ const A2A_ERROR_TEXT_MAX_CHARS = 240;
26
+ const A2A_RESULT_FENCE_REASON = "the peer is an autonomous agent, so its output is worker text, not tool data";
27
+ const A2A_CARD_PATHS = ["/.well-known/agent-card.json", "/.well-known/agent.json"];
28
+ const A2A_PROTOCOL_VERSION = "1.0";
29
+ const A2A_JSONRPC_TRANSPORT = "JSONRPC";
30
+ const A2A_ERROR_CODE_NAMES = new Map([
31
+ [-32700, "ParseError"],
32
+ [-32600, "InvalidRequest"],
33
+ [-32601, "MethodNotFound"],
34
+ [-32602, "InvalidParams"],
35
+ [-32603, "InternalError"],
36
+ [-32001, "TaskNotFound"],
37
+ [-32002, "TaskNotCancelable"],
38
+ [-32003, "PushNotificationNotSupported"],
39
+ [-32004, "UnsupportedOperation"],
40
+ [-32005, "ContentTypeNotSupported"],
41
+ [-32006, "InvalidAgentResponse"],
42
+ ]);
43
+ const A2A_TERMINAL_STATES = new Set(["completed", "canceled", "failed", "rejected"]);
44
+ const A2A_CALLER_TURN_STATES = new Set(["input-required", "auth-required"]);
45
+ const A2A_STATE_SET = new Set(A2A_TASK_STATES);
46
+ export class A2aRpcError extends Error {
47
+ code;
48
+ constructor(peer, method, code, message) {
49
+ const named = A2A_ERROR_CODE_NAMES.get(code);
50
+ super(`a2a: peer "${inlineUntrusted(peer, A2A_ID_MAX_CHARS)}" refused ${method} with ${code}${named !== undefined ? ` (${named})` : ""}: ${inlineUntrusted(message, A2A_ERROR_TEXT_MAX_CHARS)}`);
51
+ this.name = "A2aRpcError";
52
+ this.code = code;
53
+ }
54
+ }
55
+ function asRecord(v) {
56
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined;
57
+ }
58
+ function asString(v) {
59
+ return typeof v === "string" ? v : undefined;
60
+ }
61
+ function asArray(v) {
62
+ if (!Array.isArray(v))
63
+ return undefined;
64
+ const items = v;
65
+ return items;
66
+ }
67
+ function discriminantOf(v) {
68
+ return asString(v["kind"]) ?? asString(v["type"]);
69
+ }
70
+ function safeId(v) {
71
+ const s = asString(v);
72
+ if (s === undefined || s === "")
73
+ return undefined;
74
+ return inlineUntrusted(s, A2A_ID_MAX_CHARS);
75
+ }
76
+ function withRequestDeadline(outer, timeoutMs) {
77
+ const controller = new AbortController();
78
+ let timedOut = false;
79
+ const timer = setTimeout(() => {
80
+ timedOut = true;
81
+ controller.abort();
82
+ }, timeoutMs);
83
+ const onOuterAbort = () => controller.abort();
84
+ if (outer !== undefined) {
85
+ if (outer.aborted)
86
+ controller.abort();
87
+ else
88
+ outer.addEventListener("abort", onOuterAbort, { once: true });
89
+ }
90
+ return {
91
+ signal: controller.signal,
92
+ timedOut: () => timedOut,
93
+ dispose: () => {
94
+ clearTimeout(timer);
95
+ outer?.removeEventListener("abort", onOuterAbort);
96
+ },
97
+ };
98
+ }
99
+ function resolveFetch() {
100
+ const f = globalThis.fetch;
101
+ if (typeof f !== "function") {
102
+ throw new Error("a2a: this runtime has no global fetch — the A2A client speaks HTTP over the built-in fetch (Node 20+)");
103
+ }
104
+ return f;
105
+ }
106
+ async function readBoundedText(res, what) {
107
+ const stream = res.body;
108
+ if (stream === null || typeof stream.getReader !== "function") {
109
+ const text = await res.text();
110
+ if (text.length > A2A_RESPONSE_MAX_BYTES) {
111
+ throw new Error(`a2a: ${what} exceeded the ${A2A_RESPONSE_MAX_BYTES}-byte response limit`);
112
+ }
113
+ return text;
114
+ }
115
+ const reader = stream.getReader();
116
+ const decoder = new TextDecoder();
117
+ const parts = [];
118
+ let received = 0;
119
+ try {
120
+ for (;;) {
121
+ const { done, value } = await reader.read();
122
+ if (done)
123
+ break;
124
+ if (!value || value.length === 0)
125
+ continue;
126
+ received += value.length;
127
+ if (received > A2A_RESPONSE_MAX_BYTES) {
128
+ await reader.cancel().catch(() => undefined);
129
+ throw new Error(`a2a: ${what} exceeded the ${A2A_RESPONSE_MAX_BYTES}-byte response limit — the transfer was stopped`);
130
+ }
131
+ parts.push(decoder.decode(value, { stream: true }));
132
+ }
133
+ }
134
+ finally {
135
+ reader.releaseLock();
136
+ }
137
+ parts.push(decoder.decode());
138
+ return parts.join("");
139
+ }
140
+ async function requestJson(t, what, init) {
141
+ const doFetch = resolveFetch();
142
+ const deadline = withRequestDeadline(init.signal, init.timeoutMs);
143
+ try {
144
+ const res = await doFetch(init.url, {
145
+ method: init.method,
146
+ signal: deadline.signal,
147
+ headers: {
148
+ accept: "application/json",
149
+ "a2a-version": A2A_PROTOCOL_VERSION,
150
+ ...(init.body !== undefined ? { "content-type": "application/json" } : {}),
151
+ ...t.headers,
152
+ },
153
+ ...(init.body !== undefined ? { body: init.body } : {}),
154
+ });
155
+ const text = await readBoundedText(res, `${what} from A2A peer "${inlineUntrusted(t.peer, A2A_ID_MAX_CHARS)}"`);
156
+ return { status: res.status, text };
157
+ }
158
+ catch (err) {
159
+ if (init.signal?.aborted === true)
160
+ throw err;
161
+ const peerLabel = inlineUntrusted(t.peer, A2A_ID_MAX_CHARS);
162
+ if (deadline.timedOut()) {
163
+ throw new Error(`a2a: ${what} on peer "${peerLabel}" got no answer within ${init.timeoutMs}ms. The client stopped waiting — the peer may still be acting on the request, so treat the outcome as UNKNOWN and verify before retrying.`, { cause: err });
164
+ }
165
+ const classified = describeHttpTransportFailure(err);
166
+ if (classified !== undefined) {
167
+ throw new Error(`a2a: ${what} on peer "${peerLabel}" failed — ${classified.condition}. ${classified.delivered === "no" ? "The request was NOT delivered." : "It is not known whether the peer received the request; treat the outcome as UNKNOWN."}`, { cause: err });
168
+ }
169
+ throw err;
170
+ }
171
+ finally {
172
+ deadline.dispose();
173
+ }
174
+ }
175
+ async function jsonRpc(t, method, params, signal, timeoutMs = A2A_REQUEST_TIMEOUT_MS) {
176
+ const peerLabel = inlineUntrusted(t.peer, A2A_ID_MAX_CHARS);
177
+ const { status, text } = await requestJson(t, method, {
178
+ method: "POST",
179
+ url: t.endpoint,
180
+ body: JSON.stringify({ jsonrpc: "2.0", id: randomUUID(), method, params }),
181
+ timeoutMs,
182
+ ...(signal !== undefined ? { signal } : {}),
183
+ });
184
+ if (status < 200 || status >= 300) {
185
+ throw new Error(`a2a: peer "${peerLabel}" answered ${status} to ${method} instead of a JSON-RPC response`);
186
+ }
187
+ let parsed;
188
+ try {
189
+ parsed = JSON.parse(text);
190
+ }
191
+ catch (err) {
192
+ throw new Error(`a2a: peer "${peerLabel}" answered ${method} with a body that is not JSON`, { cause: err });
193
+ }
194
+ const envelope = asRecord(parsed);
195
+ if (envelope === undefined) {
196
+ throw new Error(`a2a: peer "${peerLabel}" answered ${method} with a JSON value that is not a JSON-RPC envelope`);
197
+ }
198
+ const errorMember = asRecord(envelope["error"]);
199
+ if (errorMember !== undefined) {
200
+ const code = errorMember["code"];
201
+ throw new A2aRpcError(t.peer, method, typeof code === "number" ? code : -32603, asString(errorMember["message"]) ?? "(no message)");
202
+ }
203
+ if (!("result" in envelope)) {
204
+ throw new Error(`a2a: peer "${peerLabel}" answered ${method} with neither a result nor an error member`);
205
+ }
206
+ return envelope["result"];
207
+ }
208
+ async function fetchCardAt(t, url, signal) {
209
+ const { status, text } = await requestJson(t, "agent-card discovery", { method: "GET", url, timeoutMs: A2A_CARD_TIMEOUT_MS, ...(signal !== undefined ? { signal } : {}) });
210
+ if (status === 404 || status === 410)
211
+ return undefined;
212
+ const peerLabel = inlineUntrusted(t.peer, A2A_ID_MAX_CHARS);
213
+ if (status < 200 || status >= 300) {
214
+ throw new Error(`a2a: peer "${peerLabel}" answered ${status} for its agent card at ${url}`);
215
+ }
216
+ try {
217
+ return JSON.parse(text);
218
+ }
219
+ catch (err) {
220
+ throw new Error(`a2a: peer "${peerLabel}" served an agent card at ${url} that is not JSON`, { cause: err });
221
+ }
222
+ }
223
+ async function discoverCardBody(spec, t, signal) {
224
+ const peerLabel = inlineUntrusted(spec.name, A2A_ID_MAX_CHARS);
225
+ if (spec.cardUrl !== undefined) {
226
+ const body = await fetchCardAt(t, spec.cardUrl, signal);
227
+ if (body === undefined)
228
+ throw new Error(`a2a: peer "${peerLabel}" has no agent card at its declared cardUrl (${spec.cardUrl} answered 404)`);
229
+ return body;
230
+ }
231
+ const tried = [];
232
+ for (const path of A2A_CARD_PATHS) {
233
+ const url = new URL(path, spec.url).toString();
234
+ tried.push(url);
235
+ const body = await fetchCardAt(t, url, signal);
236
+ if (body !== undefined)
237
+ return body;
238
+ }
239
+ throw new Error(`a2a: peer "${peerLabel}" published no agent card — none of ${tried.join(", ")} answered`);
240
+ }
241
+ function negotiateEndpoint(peer, card, fallbackUrl) {
242
+ const offered = [];
243
+ const primaryUrl = asString(card["url"]) ?? fallbackUrl;
244
+ offered.push({ url: primaryUrl, transport: asString(card["preferredTransport"]) ?? A2A_JSONRPC_TRANSPORT });
245
+ for (const entry of asArray(card["additionalInterfaces"]) ?? []) {
246
+ const rec = asRecord(entry);
247
+ if (rec === undefined)
248
+ continue;
249
+ const url = asString(rec["url"]);
250
+ const transport = asString(rec["transport"]);
251
+ if (url !== undefined && transport !== undefined)
252
+ offered.push({ url, transport });
253
+ }
254
+ for (const candidate of offered) {
255
+ if (candidate.transport.toUpperCase() !== A2A_JSONRPC_TRANSPORT)
256
+ continue;
257
+ let absolute;
258
+ try {
259
+ absolute = new URL(candidate.url, fallbackUrl);
260
+ }
261
+ catch {
262
+ continue;
263
+ }
264
+ if (absolute.protocol !== "http:" && absolute.protocol !== "https:")
265
+ continue;
266
+ return absolute.toString();
267
+ }
268
+ const names = offered.map((o) => inlineUntrusted(o.transport, 40)).join(", ");
269
+ throw new Error(`a2a: peer "${inlineUntrusted(peer, A2A_ID_MAX_CHARS)}" advertises no ${A2A_JSONRPC_TRANSPORT} interface (offered: ${names || "none"}) — this client speaks ${A2A_JSONRPC_TRANSPORT} only`);
270
+ }
271
+ function readCard(peer, body, fallbackUrl) {
272
+ const card = asRecord(body);
273
+ if (card === undefined) {
274
+ throw new Error(`a2a: peer "${inlineUntrusted(peer, A2A_ID_MAX_CHARS)}" served an agent card that is not a JSON object`);
275
+ }
276
+ const skills = [];
277
+ for (const entry of asArray(card["skills"]) ?? []) {
278
+ const rec = asRecord(entry);
279
+ if (rec === undefined)
280
+ continue;
281
+ const id = asString(rec["id"]);
282
+ if (id === undefined || id === "")
283
+ continue;
284
+ const tags = (asArray(rec["tags"]) ?? []).filter((v) => typeof v === "string");
285
+ const examples = (asArray(rec["examples"]) ?? []).filter((v) => typeof v === "string");
286
+ skills.push({
287
+ id,
288
+ ...(asString(rec["name"]) !== undefined ? { name: asString(rec["name"]) } : {}),
289
+ ...(asString(rec["description"]) !== undefined ? { description: asString(rec["description"]) } : {}),
290
+ ...(tags.length > 0 ? { tags } : {}),
291
+ ...(examples.length > 0 ? { examples } : {}),
292
+ });
293
+ }
294
+ return {
295
+ ...(asString(card["name"]) !== undefined ? { name: inlineUntrusted(asString(card["name"]), A2A_CARD_NAME_MAX_CHARS) } : {}),
296
+ ...(asString(card["description"]) !== undefined ? { description: inlineUntrusted(asString(card["description"]), A2A_CARD_DESCRIPTION_MAX_CHARS) } : {}),
297
+ endpoint: negotiateEndpoint(peer, card, fallbackUrl),
298
+ skills,
299
+ };
300
+ }
301
+ function a2aToolParameters() {
302
+ return Type.Object({
303
+ message: Type.String({ description: "What to ask this agent to do, in natural language." }),
304
+ taskId: Type.Optional(Type.String({ description: "Continue an existing task on this agent — pass the taskId a previous call returned (e.g. when it asked for more input)." })),
305
+ contextId: Type.Optional(Type.String({ description: "Keep this call in an existing conversation — pass the contextId a previous call returned." })),
306
+ });
307
+ }
308
+ function skillDescription(peerLabel, skill) {
309
+ const lines = [];
310
+ const title = skill.name !== undefined ? inlineUntrusted(skill.name, A2A_SKILL_NAME_MAX_CHARS) : skill.id;
311
+ lines.push(`Ask the remote agent "${peerLabel}" to perform its "${title}" skill.`);
312
+ if (skill.description !== undefined)
313
+ lines.push(inlineUntrusted(skill.description, A2A_SKILL_TEXT_MAX_CHARS));
314
+ if (skill.tags !== undefined)
315
+ lines.push(`Tags: ${inlineUntrusted(skill.tags.join(", "), A2A_SKILL_TAGS_MAX_CHARS)}`);
316
+ if (skill.examples !== undefined)
317
+ lines.push(`Examples: ${inlineUntrusted(skill.examples.join(" | "), A2A_SKILL_EXAMPLES_MAX_CHARS)}`);
318
+ lines.push("The agent acts on its own side; its reply is returned as untrusted external data.");
319
+ return lines.join("\n");
320
+ }
321
+ function a2aAxisFor(name, override) {
322
+ const axis = { name, egress: true, effect: "write" };
323
+ if (override === undefined)
324
+ return axis;
325
+ if (override.effect !== undefined)
326
+ axis.effect = override.effect;
327
+ if (override.egress === false)
328
+ delete axis.egress;
329
+ if (override.irreversibility === "always")
330
+ axis.irreversibility = "always";
331
+ return axis;
332
+ }
333
+ function renderParts(parts) {
334
+ const lines = [];
335
+ for (const raw of parts) {
336
+ const part = asRecord(raw);
337
+ if (part === undefined)
338
+ continue;
339
+ const kind = discriminantOf(part);
340
+ if (kind === "text") {
341
+ const text = asString(part["text"]);
342
+ if (text !== undefined)
343
+ lines.push(text);
344
+ continue;
345
+ }
346
+ if (kind === "file") {
347
+ const file = asRecord(part["file"]) ?? {};
348
+ const name = asString(file["name"]) ?? "(unnamed)";
349
+ const mime = asString(file["mimeType"]) ?? "unknown type";
350
+ const uri = asString(file["uri"]);
351
+ if (uri !== undefined)
352
+ lines.push(`[file part: ${name} (${mime}) at ${uri} — NOT downloaded by this client]`);
353
+ else if (typeof file["bytes"] === "string")
354
+ lines.push(`[file part: ${name} (${mime}), inline bytes not decoded by this client]`);
355
+ else
356
+ lines.push(`[file part: ${name} (${mime}), no content]`);
357
+ continue;
358
+ }
359
+ if (kind === "data") {
360
+ lines.push(`[data part] ${JSON.stringify(part["data"]) ?? "undefined"}`);
361
+ continue;
362
+ }
363
+ lines.push(`[part of unsupported kind ${JSON.stringify(kind ?? null)}]`);
364
+ }
365
+ return lines;
366
+ }
367
+ function renderMessage(message) {
368
+ const parts = asArray(message["parts"]);
369
+ return parts === undefined ? [] : renderParts(parts);
370
+ }
371
+ function renderTask(task) {
372
+ const lines = [];
373
+ const status = asRecord(task["status"]);
374
+ const statusMessage = status !== undefined ? asRecord(status["message"]) : undefined;
375
+ if (statusMessage !== undefined)
376
+ lines.push(...renderMessage(statusMessage));
377
+ for (const entry of asArray(task["artifacts"]) ?? []) {
378
+ const artifact = asRecord(entry);
379
+ if (artifact === undefined)
380
+ continue;
381
+ const name = asString(artifact["name"]) ?? asString(artifact["artifactId"]) ?? "(unnamed artifact)";
382
+ lines.push(`--- artifact: ${name} ---`);
383
+ const parts = asArray(artifact["parts"]);
384
+ if (parts !== undefined)
385
+ lines.push(...renderParts(parts));
386
+ }
387
+ return lines;
388
+ }
389
+ function taskState(task) {
390
+ const status = asRecord(task["status"]);
391
+ const raw = status !== undefined ? asString(status["state"]) : undefined;
392
+ if (raw === undefined)
393
+ return { unrecognized: "(absent)" };
394
+ if (!A2A_STATE_SET.has(raw))
395
+ return { unrecognized: raw };
396
+ return { state: raw };
397
+ }
398
+ function fenceResult(peerLabel, skillId, body) {
399
+ return delimitUntrusted(`a2a ${peerLabel} ${inlineUntrusted(skillId, A2A_ID_MAX_CHARS)} — ${A2A_RESULT_FENCE_REASON}`, body, A2A_RESULT_BODY_MAX_CHARS);
400
+ }
401
+ function continuationLine(taskId, contextId) {
402
+ const keys = [];
403
+ if (taskId !== undefined)
404
+ keys.push(`taskId ${taskId}`);
405
+ if (contextId !== undefined)
406
+ keys.push(`contextId ${contextId}`);
407
+ return keys.length > 0 ? ` Continue it by calling this tool again with ${keys.join(" and ")}.` : "";
408
+ }
409
+ function delay(ms, signal) {
410
+ return new Promise((resolve) => {
411
+ const timer = setTimeout(() => {
412
+ signal?.removeEventListener("abort", onAbort);
413
+ resolve();
414
+ }, ms);
415
+ function onAbort() {
416
+ clearTimeout(timer);
417
+ resolve();
418
+ }
419
+ if (signal !== undefined) {
420
+ if (signal.aborted) {
421
+ clearTimeout(timer);
422
+ resolve();
423
+ return;
424
+ }
425
+ signal.addEventListener("abort", onAbort, { once: true });
426
+ }
427
+ });
428
+ }
429
+ async function awaitTaskOutcome(t, peerLabel, skillId, firstTask, signal) {
430
+ const startedAt = Date.now();
431
+ let task = firstTask;
432
+ let observed = taskState(task);
433
+ let idleSince = startedAt;
434
+ let pollDelayMs = A2A_POLL_FIRST_DELAY_MS;
435
+ for (;;) {
436
+ if ("unrecognized" in observed) {
437
+ throw new Error(`a2a: peer "${peerLabel}" reported task state ${JSON.stringify(inlineUntrusted(observed.unrecognized, A2A_ID_MAX_CHARS))} for skill "${inlineUntrusted(skillId, A2A_ID_MAX_CHARS)}", which is not a state this client knows. The outcome is UNKNOWN.${continuationLine(safeId(task["id"]), safeId(task["contextId"]))}`);
438
+ }
439
+ const state = observed.state;
440
+ const taskId = safeId(task["id"]);
441
+ const contextId = safeId(task["contextId"]);
442
+ if (A2A_CALLER_TURN_STATES.has(state)) {
443
+ const body = renderTask(task).join("\n").trim();
444
+ return {
445
+ content: [
446
+ {
447
+ type: "text",
448
+ text: `The remote agent needs more from you before it can continue (state: ${state}).${continuationLine(taskId, contextId)}\n` +
449
+ fenceResult(peerLabel, skillId, body || "(the agent sent no message with its request)"),
450
+ },
451
+ ],
452
+ details: { type: "a2a", state, ...(taskId !== undefined ? { taskId } : {}), ...(contextId !== undefined ? { contextId } : {}) },
453
+ terminate: false,
454
+ };
455
+ }
456
+ if (A2A_TERMINAL_STATES.has(state)) {
457
+ const body = renderTask(task).join("\n").trim();
458
+ if (state === "completed") {
459
+ return {
460
+ content: [{ type: "text", text: fenceResult(peerLabel, skillId, body || "(the agent completed the task and returned no content)") }],
461
+ details: { type: "a2a", state, ...(taskId !== undefined ? { taskId } : {}), ...(contextId !== undefined ? { contextId } : {}) },
462
+ terminate: false,
463
+ };
464
+ }
465
+ throw new Error(`a2a: the remote agent "${peerLabel}" ended task state ${state} for skill "${inlineUntrusted(skillId, A2A_ID_MAX_CHARS)}".${continuationLine(taskId, contextId)}\n` +
466
+ fenceResult(peerLabel, skillId, body || "(the agent gave no reason)"));
467
+ }
468
+ const now = Date.now();
469
+ if (now - startedAt >= A2A_CALL_TOTAL_TIMEOUT_MS) {
470
+ throw new Error(`a2a: the remote agent "${peerLabel}" was still ${state} after ${Math.round((now - startedAt) / 1000)}s (ceiling ${A2A_CALL_TOTAL_TIMEOUT_MS}ms). The client stopped waiting; the agent is still acting, so treat the outcome as UNKNOWN.${continuationLine(taskId, contextId)}`);
471
+ }
472
+ if (now - idleSince >= A2A_STATE_IDLE_TIMEOUT_MS) {
473
+ throw new Error(`a2a: the remote agent "${peerLabel}" answered every poll but never left state ${state} for ${A2A_STATE_IDLE_TIMEOUT_MS}ms. The client stopped waiting; treat the outcome as UNKNOWN.${continuationLine(taskId, contextId)}`);
474
+ }
475
+ await delay(pollDelayMs, signal);
476
+ if (signal?.aborted === true)
477
+ throw new Error(`a2a: the call to "${peerLabel}" was aborted while waiting for the remote agent.${continuationLine(taskId, contextId)}`);
478
+ pollDelayMs = Math.min(pollDelayMs * A2A_POLL_BACKOFF_FACTOR, A2A_POLL_MAX_DELAY_MS);
479
+ const rawId = asString(task["id"]);
480
+ if (rawId === undefined) {
481
+ throw new Error(`a2a: the remote agent "${peerLabel}" returned a non-terminal task with no id, so it cannot be polled. The outcome is UNKNOWN.`);
482
+ }
483
+ const polled = asRecord(await jsonRpc(t, "tasks/get", { id: rawId }, signal));
484
+ if (polled === undefined) {
485
+ throw new Error(`a2a: peer "${peerLabel}" answered tasks/get with something that is not a task object`);
486
+ }
487
+ task = polled;
488
+ const next = taskState(task);
489
+ if ("state" in next && "state" in observed && next.state !== observed.state)
490
+ idleSince = Date.now();
491
+ observed = next;
492
+ }
493
+ }
494
+ function buildSkillTool(spec, t, skill, lifecycle) {
495
+ const peerLabel = inlineUntrusted(spec.name, A2A_ID_MAX_CHARS);
496
+ const name = mintNamespacedToolName(A2A_NAMESPACE, spec.name, skill.id);
497
+ return {
498
+ name,
499
+ label: `${spec.name}:${skill.id}`,
500
+ description: skillDescription(peerLabel, skill),
501
+ parameters: a2aToolParameters(),
502
+ execute: async (_toolCallId, params, signal) => {
503
+ if (lifecycle.disposed) {
504
+ throw new Error(`a2a: the task that mounted peer "${peerLabel}" has ended — this tool no longer sends anything to the remote agent.`);
505
+ }
506
+ const args = asRecord(params) ?? {};
507
+ const message = asString(args["message"]);
508
+ if (message === undefined || message.trim() === "") {
509
+ throw new Error(`a2a: "message" is required — say what the remote agent "${peerLabel}" should do.`);
510
+ }
511
+ const taskId = asString(args["taskId"]);
512
+ const contextId = asString(args["contextId"]);
513
+ const result = await jsonRpc(t, "message/send", {
514
+ message: {
515
+ role: "user",
516
+ kind: "message",
517
+ messageId: randomUUID(),
518
+ parts: [{ kind: "text", text: message }],
519
+ ...(taskId !== undefined && taskId !== "" ? { taskId } : {}),
520
+ ...(contextId !== undefined && contextId !== "" ? { contextId } : {}),
521
+ },
522
+ configuration: { blocking: true },
523
+ }, signal);
524
+ const answer = asRecord(result);
525
+ if (answer === undefined) {
526
+ throw new Error(`a2a: peer "${peerLabel}" answered message/send with something that is not a protocol object`);
527
+ }
528
+ const kind = discriminantOf(answer);
529
+ if (kind === "message" || (kind === undefined && answer["parts"] !== undefined)) {
530
+ const body = renderMessage(answer).join("\n").trim();
531
+ return {
532
+ content: [{ type: "text", text: fenceResult(peerLabel, skill.id, body || "(the agent replied with no content)") }],
533
+ details: { type: "a2a", kind: "message", ...(safeId(answer["contextId"]) !== undefined ? { contextId: safeId(answer["contextId"]) } : {}) },
534
+ terminate: false,
535
+ };
536
+ }
537
+ if (kind === "task" || (kind === undefined && answer["status"] !== undefined)) {
538
+ return await awaitTaskOutcome(t, peerLabel, skill.id, answer, signal);
539
+ }
540
+ throw new Error(`a2a: peer "${peerLabel}" answered message/send with ${JSON.stringify(inlineUntrusted(kind ?? "(no kind)", A2A_ID_MAX_CHARS))}, which is neither a message nor a task`);
541
+ },
542
+ };
543
+ }
544
+ async function materializePeer(spec, principal, lifecycle, signal) {
545
+ const headers = resolveProtocolHttpHeaders(A2A_NAMESPACE.id, spec, principal);
546
+ const discovery = { peer: spec.name, endpoint: spec.url, headers };
547
+ const card = readCard(spec.name, await discoverCardBody(spec, discovery, signal), spec.url);
548
+ const transport = { peer: spec.name, endpoint: card.endpoint, headers };
549
+ const tools = [];
550
+ const axes = [];
551
+ for (const skill of card.skills) {
552
+ if (spec.allowSkills !== undefined && !spec.allowSkills.includes(skill.id))
553
+ continue;
554
+ const tool = buildSkillTool(spec, transport, skill, lifecycle);
555
+ tools.push(tool);
556
+ axes.push(a2aAxisFor(tool.name, spec.toolAxes?.[skill.id]));
557
+ }
558
+ return {
559
+ tools,
560
+ axes,
561
+ status: {
562
+ name: spec.name,
563
+ status: "ready",
564
+ ...(card.name !== undefined ? { agentName: card.name } : {}),
565
+ ...(card.description !== undefined ? { agentDescription: card.description } : {}),
566
+ endpoint: card.endpoint,
567
+ toolNames: tools.map((tool) => tool.name),
568
+ },
569
+ };
570
+ }
571
+ function asPeerWarning(spec, err) {
572
+ const detail = err instanceof Error ? err.message : String(err);
573
+ const warning = new Error(`a2a: peer "${spec.name}" could not be materialized — skipped (${detail})`, { cause: err });
574
+ warning.code = "a2a.peer_unavailable";
575
+ return warning;
576
+ }
577
+ export async function materializeA2aTools(specs, principal, signal) {
578
+ const lifecycle = { disposed: false };
579
+ const tools = [];
580
+ const toolAxes = [];
581
+ const warnings = [];
582
+ const statuses = [];
583
+ const settled = await Promise.allSettled(specs.map((spec) => materializePeer(spec, principal, lifecycle, signal)));
584
+ for (let i = 0; i < specs.length; i++) {
585
+ const spec = specs[i];
586
+ const outcome = settled[i];
587
+ if (outcome.status === "fulfilled") {
588
+ tools.push(...outcome.value.tools);
589
+ toolAxes.push(...outcome.value.axes);
590
+ statuses.push(outcome.value.status);
591
+ }
592
+ else {
593
+ warnings.push(asPeerWarning(spec, outcome.reason));
594
+ statuses.push({ name: spec.name, status: "failed", error: inlineUntrusted(outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason), A2A_ERROR_TEXT_MAX_CHARS) });
595
+ }
596
+ }
597
+ return {
598
+ tools,
599
+ toolAxes,
600
+ warnings,
601
+ statuses,
602
+ refresh: async (peer) => {
603
+ const prefixOf = (name) => mintNamespacePrefix(A2A_NAMESPACE, name);
604
+ const targets = peer !== undefined ? specs.filter((s) => s.name === peer) : specs;
605
+ if (peer !== undefined && targets.length === 0) {
606
+ return [{ peer, prefix: prefixOf(peer), status: "failed", toolCount: 0, added: [], removed: [], error: "no peer of that name is declared for this task" }];
607
+ }
608
+ const results = [];
609
+ for (const spec of targets) {
610
+ const prefix = prefixOf(spec.name);
611
+ if (lifecycle.disposed) {
612
+ results.push({ peer: spec.name, prefix, status: "disposed", toolCount: 0, added: [], removed: [] });
613
+ continue;
614
+ }
615
+ const previous = statuses.find((s) => s.name === spec.name)?.toolNames ?? [];
616
+ try {
617
+ const fresh = await materializePeer(spec, principal, lifecycle, signal);
618
+ const names = fresh.tools.map((t) => t.name);
619
+ results.push({
620
+ peer: spec.name,
621
+ prefix,
622
+ status: "refreshed",
623
+ toolCount: names.length,
624
+ added: names.filter((n) => !previous.includes(n)),
625
+ removed: previous.filter((n) => !names.includes(n)),
626
+ tools: fresh.tools,
627
+ axes: fresh.axes,
628
+ });
629
+ const at = statuses.findIndex((s) => s.name === spec.name);
630
+ if (at >= 0)
631
+ statuses[at] = fresh.status;
632
+ }
633
+ catch (err) {
634
+ results.push({
635
+ peer: spec.name,
636
+ prefix,
637
+ status: "failed",
638
+ toolCount: previous.length,
639
+ added: [],
640
+ removed: [],
641
+ error: inlineUntrusted(err instanceof Error ? err.message : String(err), A2A_ERROR_TEXT_MAX_CHARS),
642
+ });
643
+ }
644
+ }
645
+ return results;
646
+ },
647
+ dispose: async () => {
648
+ lifecycle.disposed = true;
649
+ },
650
+ };
651
+ }
@@ -240,7 +240,12 @@ export interface CheckpointSummary {
240
240
  export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
241
241
  export declare class CheckpointError extends Error {
242
242
  readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch";
243
- constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch", message: string);
243
+ readonly detail?: {
244
+ field?: "boundCallId" | "boundInputHash";
245
+ } | undefined;
246
+ constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found" | "checkpoint.gate_mismatch" | "checkpoint.resume_aborted" | "checkpoint.invalid_outcome" | "checkpoint.unsupported_version" | "checkpoint.reopen_revote" | "checkpoint.reopened_concurrently" | "checkpoint.reopen_failed" | "steering.invalid_content" | "wake.gate_pending" | "wake.nothing_to_deliver" | "resume.parent_constraint_missing" | "resume.parent_constraint_mismatch", message: string, detail?: {
247
+ field?: "boundCallId" | "boundInputHash";
248
+ } | undefined);
244
249
  }
245
250
  export interface CheckpointStore {
246
251
  put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
@@ -204,9 +204,11 @@ export function summarizeCheckpoint(cp) {
204
204
  }
205
205
  export class CheckpointError extends Error {
206
206
  code;
207
- constructor(code, message) {
207
+ detail;
208
+ constructor(code, message, detail) {
208
209
  super(message);
209
210
  this.code = code;
211
+ this.detail = detail;
210
212
  this.name = "CheckpointError";
211
213
  }
212
214
  }