@arnilo/prism-supervisor 0.0.7 → 0.0.8

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/CHANGELOG.md CHANGED
@@ -1,4 +1,13 @@
1
1
  # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ ## [0.0.8] - 2026-07-20
6
+
7
+ - Added host-owned durable A2A task start/get/list/cancel/subscribe with bounded cursor replay, interrupted states, ordered rich task events, and non-disclosing task errors.
8
+ - Added opt-in bounded text/raw/URL/data parts; URL policy validates without dereferencing.
9
+ - Added capability-gated push config CRUD/client APIs and explicit bounded `deliverA2APushEvent()` retry/timeout/idempotency-key wrapper; webhook transport/credentials remain host-owned and secrets are omitted from responses.
10
+
2
11
  ## [0.0.7] - 2026-07-19
3
12
 
4
13
  - Released with the exact 0.0.7 first-party package graph.
package/README.md CHANGED
@@ -26,6 +26,6 @@ const supervisor = createSupervisor({
26
26
  console.log((await supervisor.delegate({ childId: "research", input: "Check sources" })).text);
27
27
  ```
28
28
 
29
- Also exports A2A 1.0 `createA2AAgentCard`, `signA2AAgentCard`, `verifyA2AAgentCard`, `createA2AHandler`, and `createA2AClient`. Streaming uses one fatal UTF-8 decoder, accepts LF/CRLF/mixed SSE separators and multiline `data:`, rejects truncated/post-terminal frames, and retains existing finite byte/event/time limits. Only text parts and JSON-RPC `SendMessage`, `SendStreamingMessage`, and `GetExtendedAgentCard` are supported. Hosts own authentication, TLS, endpoint allow-lists, child credential resolution, and memory construction from package-derived resource/thread IDs.
29
+ Also exports bounded A2A 1.0 cards, handler/client, rich one-of parts, host-owned `A2ATaskLifecycle`, reconnect subscriptions, and push-config CRUD. Direct text invocation remains compatible; durable get/list/cancel/subscribe and rich raw/data/URL parts require explicit adapters/policy. URL parts are validated but never fetched. Push persistence/network/credentials and exact-owner checks remain host-owned; explicit `deliverA2APushEvent()` only bounds attempts/time and forwards stable event IDs for host idempotency. Returned configs omit secrets. JSON-RPC/HTTPS is the only binding.
30
30
 
31
31
  See [Supervisors](../../docs/supervisors.md) and [A2A interoperability](../../docs/a2a.md).
package/dist/a2a-card.js CHANGED
@@ -60,6 +60,15 @@ function canonicalCard(card) {
60
60
  return canonicalJson(unsigned);
61
61
  }
62
62
  function validateCard(card) {
63
+ let serialized;
64
+ try {
65
+ serialized = JSON.stringify(card);
66
+ }
67
+ catch {
68
+ throw new A2AError("Agent card must be JSON", 400, "ERR_PRISM_A2A_CARD");
69
+ }
70
+ if (Buffer.byteLength(serialized) > 1024 * 1024 || card.supportedInterfaces.length > 16 || card.skills.length > 256)
71
+ throw new A2AError("Agent card exceeds collection/byte limits", 400, "ERR_PRISM_A2A_CARD");
63
72
  if (!card.name?.trim() || !card.description?.trim() || !card.version?.trim())
64
73
  throw new A2AError("Agent card identity is incomplete", 400, "ERR_PRISM_A2A_CARD");
65
74
  if (!card.supportedInterfaces.length || !card.supportedInterfaces.every((item) => item.protocolBinding === "JSONRPC" && item.protocolVersion === "1.0" && isHttpsUrl(item.url)))
@@ -68,7 +77,7 @@ function validateCard(card) {
68
77
  throw new A2AError("Agent card must support text/plain", 400, "ERR_PRISM_A2A_CARD");
69
78
  const ids = new Set();
70
79
  for (const skill of card.skills) {
71
- if (!skill.id.trim() || !skill.name.trim() || !skill.description.trim() || ids.has(skill.id))
80
+ if (!skill.id.trim() || !skill.name.trim() || !skill.description.trim() || ids.has(skill.id) || skill.tags.length > 64 || [skill.id, skill.name, skill.description, ...skill.tags].some((value) => Buffer.byteLength(value) > 16 * 1024))
72
81
  throw new A2AError("Agent card skill is invalid", 400, "ERR_PRISM_A2A_CARD");
73
82
  ids.add(skill.id);
74
83
  }
@@ -1,13 +1,12 @@
1
1
  import { createA2AAgentCard } from "./a2a-card.js";
2
+ import { resolveA2ALimits } from "./a2a-parts.js";
2
3
  import { A2AError } from "./errors.js";
3
4
  import { A2A_PROTOCOL_VERSION } from "./a2a-types.js";
4
- const DEFAULTS = { maxRequestBytes: 64 * 1024, maxResponseBytes: 1024 * 1024, maxEventBytes: 64 * 1024, maxStreamBytes: 10 * 1024 * 1024, maxStreamEvents: 10_000, maxConcurrentRequests: 16, timeoutMs: 120_000, maxCardBytes: 64 * 1024 };
5
- const HARD = { maxRequestBytes: 1024 * 1024, maxResponseBytes: 8 * 1024 * 1024, maxEventBytes: 1024 * 1024, maxStreamBytes: 64 * 1024 * 1024, maxStreamEvents: 100_000, maxConcurrentRequests: 256, timeoutMs: 30 * 60_000, maxCardBytes: 1024 * 1024 };
6
5
  export function createA2AClient(options) {
7
6
  const endpoint = requireAllowedHttpsUrl(options.endpoint, options.allowedOrigins);
8
7
  const cardUrl = requireAllowedHttpsUrl(options.cardUrl ?? `${endpoint.origin}/.well-known/agent-card.json`, options.allowedOrigins);
9
8
  const fetcher = options.fetch ?? globalThis.fetch;
10
- const limits = clientLimits(options.limits);
9
+ const limits = resolveA2ALimits(options.limits);
11
10
  let active = 0;
12
11
  let requestId = 0;
13
12
  async function withRequest(signal, operation) {
@@ -46,7 +45,7 @@ export function createA2AClient(options) {
46
45
  if (new TextEncoder().encode(body).byteLength > limits.maxRequestBytes)
47
46
  throw new A2AError("A2A request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
48
47
  const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal }) ?? {}), signal);
49
- const response = await fetcher(endpoint, { method: "POST", signal, redirect: "error", headers: { ...headersObject(authHeaders), "content-type": "application/a2a+json", accept: "application/a2a+json" }, body });
48
+ const response = await fetcher(endpoint, { method: "POST", signal, redirect: "error", headers: { ...headersObject(authHeaders), "content-type": "application/a2a+json", accept: "application/a2a+json", "a2a-version": "1.0" }, body });
50
49
  if (!response.ok)
51
50
  throw new A2AError("A2A remote request failed", response.status, "ERR_PRISM_A2A_REMOTE");
52
51
  const rpc = parseRpcResponse(await readBoundedJson(response, limits.maxResponseBytes, signal), id);
@@ -69,7 +68,7 @@ export function createA2AClient(options) {
69
68
  if (new TextEncoder().encode(body).byteLength > limits.maxRequestBytes)
70
69
  throw new A2AError("A2A request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
71
70
  const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal: owned.signal }) ?? {}), owned.signal);
72
- const response = await fetcher(endpoint, { method: "POST", signal: owned.signal, redirect: "error", headers: { ...headersObject(authHeaders), "content-type": "application/a2a+json", accept: "text/event-stream" }, body });
71
+ const response = await fetcher(endpoint, { method: "POST", signal: owned.signal, redirect: "error", headers: { ...headersObject(authHeaders), "content-type": "application/a2a+json", accept: "text/event-stream", "a2a-version": "1.0" }, body });
73
72
  if (!response.ok || !response.body || !response.headers.get("content-type")?.startsWith("text/event-stream"))
74
73
  throw new A2AError("A2A stream request failed", response.status, "ERR_PRISM_A2A_REMOTE");
75
74
  reader = response.body.getReader();
@@ -90,13 +89,16 @@ export function createA2AClient(options) {
90
89
  if (rpc.error)
91
90
  throw new A2AError(safeRemote(rpc.error.message, options), 502, "ERR_PRISM_A2A_REMOTE");
92
91
  const task = parseTaskResult(rpc.result);
93
- if (task.status.state === "TASK_STATE_FAILED" || task.status.state === "TASK_STATE_CANCELED")
92
+ if (task.status.state === "TASK_STATE_FAILED" || task.status.state === "TASK_STATE_CANCELED" || task.status.state === "TASK_STATE_REJECTED")
94
93
  throw new A2AError("Remote A2A stream task failed", 502, "ERR_PRISM_A2A_REMOTE");
94
+ if (task.status.state === "TASK_STATE_INPUT_REQUIRED" || task.status.state === "TASK_STATE_AUTH_REQUIRED")
95
+ throw new A2AError(`Remote A2A task interrupted: ${task.status.state}`, 409, "ERR_PRISM_A2A_INTERRUPTED");
95
96
  if (task.status.state === "TASK_STATE_COMPLETED")
96
97
  terminal = true;
97
98
  for (const artifact of task.artifacts ?? [])
98
99
  for (const part of artifact.parts)
99
- yield options.redactor?.redact(part.text) ?? part.text;
100
+ if (typeof part.text === "string")
101
+ yield options.redactor?.redact(part.text) ?? part.text;
100
102
  }
101
103
  if (!terminal)
102
104
  throw new A2AError("A2A stream ended before terminal task state", 502, "ERR_PRISM_A2A_REMOTE");
@@ -118,7 +120,74 @@ export function createA2AClient(options) {
118
120
  await abortable(Promise.resolve(options.verifyCard(card)), signal);
119
121
  return card;
120
122
  }
121
- return { getCard, send, stream };
123
+ async function invoke(method, params, signal) {
124
+ return withRequest(signal, async (owned) => {
125
+ await getCardWithin(owned);
126
+ const id = ++requestId;
127
+ const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
128
+ if (Buffer.byteLength(body) > limits.maxRequestBytes)
129
+ throw new A2AError("A2A request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
130
+ const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal: owned }) ?? {}), owned);
131
+ const response = await fetcher(endpoint, { method: "POST", signal: owned, redirect: "error", headers: { ...headersObject(authHeaders), "content-type": "application/a2a+json", accept: "application/a2a+json", "a2a-version": "1.0" }, body });
132
+ if (!response.ok)
133
+ throw new A2AError("A2A remote request failed", response.status, "ERR_PRISM_A2A_REMOTE");
134
+ const rpc = parseRpcResponse(await readBoundedJson(response, limits.maxResponseBytes, owned), id);
135
+ if (rpc.error)
136
+ throw remoteProtocolError(rpc.error.code, rpc.error.message, options);
137
+ return rpc.result;
138
+ });
139
+ }
140
+ async function sendMessage(message, call = {}) {
141
+ return parseTaskResult(await invoke("SendMessage", { message, configuration: { returnImmediately: call.returnImmediately ?? false } }, call.signal));
142
+ }
143
+ async function getTask(id, call = {}) { return parseTaskResult(await invoke("GetTask", { id, historyLength: call.historyLength ?? 0 }, call.signal)); }
144
+ async function listTasks(call = {}) {
145
+ const value = await invoke("ListTasks", { pageSize: call.pageSize ?? 50, pageToken: call.pageToken, contextId: call.contextId }, call.signal);
146
+ if (!isRecord(value) || !Array.isArray(value.tasks) || value.tasks.length > limits.maxPageSize)
147
+ throw new A2AError("Malformed A2A task page", 502, "ERR_PRISM_A2A_REMOTE");
148
+ return { tasks: value.tasks.map((task) => parseTaskResult(task)), nextPageToken: typeof value.nextPageToken === "string" ? value.nextPageToken : undefined, totalSize: typeof value.totalSize === "number" ? value.totalSize : undefined };
149
+ }
150
+ async function cancelTask(id, call = {}) { return parseTaskResult(await invoke("CancelTask", { id }, call.signal)); }
151
+ async function* subscribeToTask(id, call = {}) {
152
+ if (active >= limits.maxConcurrentRequests)
153
+ throw new A2AError("A2A client concurrency exceeded", 429, "ERR_PRISM_A2A_CONCURRENCY");
154
+ active += 1;
155
+ const owned = ownedSignal(call.signal, limits.timeoutMs);
156
+ let reader;
157
+ try {
158
+ await getCardWithin(owned.signal);
159
+ const request = ++requestId;
160
+ const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal: owned.signal }) ?? {}), owned.signal);
161
+ const response = await fetcher(endpoint, { method: "POST", signal: owned.signal, redirect: "error", headers: { ...headersObject(authHeaders), "content-type": "application/a2a+json", accept: "text/event-stream", "a2a-version": "1.0" }, body: JSON.stringify({ jsonrpc: "2.0", id: request, method: "SubscribeToTask", params: { id, afterEventId: call.afterEventId } }) });
162
+ if (!response.ok || !response.body || !response.headers.get("content-type")?.startsWith("text/event-stream"))
163
+ throw new A2AError("A2A subscribe request failed", response.status, "ERR_PRISM_A2A_REMOTE");
164
+ reader = response.body.getReader();
165
+ let previous = "", count = 0;
166
+ for await (const data of readA2AStreamData(reader, limits, owned.signal)) {
167
+ const rpc = parseRpcResponse(JSON.parse(data), request);
168
+ if (rpc.error)
169
+ throw remoteProtocolError(rpc.error.code, rpc.error.message, options);
170
+ const event = parseTaskEvent(rpc.result);
171
+ if (event.eventId === previous)
172
+ continue;
173
+ previous = event.eventId;
174
+ if (++count > limits.maxReplayEvents)
175
+ throw new A2AError("A2A replay exceeds event limit", 507, "ERR_PRISM_A2A_STREAM_LIMIT");
176
+ yield event;
177
+ }
178
+ }
179
+ finally {
180
+ await reader?.cancel().catch(() => undefined);
181
+ owned.dispose();
182
+ active -= 1;
183
+ }
184
+ }
185
+ async function createPushConfig(config, call = {}) { return parsePushConfig(await invoke("CreateTaskPushNotificationConfig", { ...config }, call.signal)); }
186
+ async function getPushConfig(taskId, id, call = {}) { return parsePushConfig(await invoke("GetTaskPushNotificationConfig", { taskId, id }, call.signal)); }
187
+ async function listPushConfigs(taskId, call = {}) { const value = await invoke("ListTaskPushNotificationConfigs", { taskId, pageSize: call.pageSize, pageToken: call.pageToken }, call.signal); if (!isRecord(value) || !Array.isArray(value.configs))
188
+ throw new A2AError("Malformed A2A push config page", 502, "ERR_PRISM_A2A_REMOTE"); return { configs: value.configs.map(parsePushConfig), nextPageToken: typeof value.nextPageToken === "string" ? value.nextPageToken : undefined }; }
189
+ async function deletePushConfig(taskId, id, call = {}) { await invoke("DeleteTaskPushNotificationConfig", { taskId, id }, call.signal); }
190
+ return { getCard, send, sendMessage, stream, getTask, listTasks, cancelTask, subscribeToTask, createPushConfig, getPushConfig, listPushConfigs, deletePushConfig };
122
191
  }
123
192
  async function* readA2AStreamData(reader, limits, signal) {
124
193
  const decoder = new TextDecoder("utf-8", { fatal: true });
@@ -224,7 +293,9 @@ function requestBody(id, method, input) {
224
293
  function taskResult(task, options) {
225
294
  if (task.status.state === "TASK_STATE_SUBMITTED" || task.status.state === "TASK_STATE_WORKING")
226
295
  throw new A2AError("A2A response task is not terminal", 502, "ERR_PRISM_A2A_REMOTE");
227
- const text = (task.artifacts ?? []).flatMap((artifact) => artifact.parts.map((part) => part.text)).join("");
296
+ if (task.status.state === "TASK_STATE_INPUT_REQUIRED" || task.status.state === "TASK_STATE_AUTH_REQUIRED")
297
+ throw new A2AError(`Remote A2A task interrupted: ${task.status.state}`, 409, "ERR_PRISM_A2A_INTERRUPTED");
298
+ const text = (task.artifacts ?? []).flatMap((artifact) => artifact.parts.flatMap((part) => "text" in part ? [part.text] : [])).join("");
228
299
  const safeText = options.redactor?.redact(text) ?? text;
229
300
  const status = task.status.state === "TASK_STATE_COMPLETED" ? "succeeded" : task.status.state === "TASK_STATE_CANCELED" ? "aborted" : "failed";
230
301
  const content = safeText ? [{ type: "text", text: safeText }] : [];
@@ -232,16 +303,17 @@ function taskResult(task, options) {
232
303
  return Object.freeze({ sessionId: task.contextId, runId: task.id, status, text: safeText, content, message, error: status === "failed" ? { message: "Remote A2A task failed" } : undefined, abortReason: status === "aborted" ? "Remote A2A task canceled" : undefined });
233
304
  }
234
305
  function parseTaskResult(value) {
235
- if (!isRecord(value) || !isRecord(value.task))
306
+ if (!isRecord(value))
236
307
  throw new A2AError("Malformed A2A task result", 502, "ERR_PRISM_A2A_REMOTE");
237
- const task = value.task;
308
+ const task = isRecord(value.task) ? value.task : value;
238
309
  if (typeof task.id !== "string" || typeof task.contextId !== "string" || !isRecord(task.status) || typeof task.status.state !== "string")
239
310
  throw new A2AError("Malformed A2A task", 502, "ERR_PRISM_A2A_REMOTE");
240
- const states = new Set(["TASK_STATE_SUBMITTED", "TASK_STATE_WORKING", "TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED"]);
311
+ const states = new Set(["TASK_STATE_SUBMITTED", "TASK_STATE_WORKING", "TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_INPUT_REQUIRED", "TASK_STATE_REJECTED", "TASK_STATE_AUTH_REQUIRED"]);
241
312
  if (!states.has(task.status.state))
242
313
  throw new A2AError("Unknown A2A task state", 502, "ERR_PRISM_A2A_REMOTE");
243
314
  const artifacts = task.artifacts === undefined ? undefined : parseArtifacts(task.artifacts);
244
- return { id: task.id, contextId: task.contextId, status: { state: task.status.state, timestamp: typeof task.status.timestamp === "string" ? task.status.timestamp : new Date(0).toISOString() }, artifacts };
315
+ const history = task.history === undefined ? undefined : Array.isArray(task.history) ? task.history.map(parseRemoteMessage) : (() => { throw new A2AError("Malformed A2A task history", 502, "ERR_PRISM_A2A_REMOTE"); })();
316
+ return { id: task.id, contextId: task.contextId, status: { state: task.status.state, timestamp: typeof task.status.timestamp === "string" ? task.status.timestamp : new Date(0).toISOString() }, artifacts, history };
245
317
  }
246
318
  function parseArtifacts(value) {
247
319
  if (!Array.isArray(value) || value.length > 32)
@@ -249,13 +321,49 @@ function parseArtifacts(value) {
249
321
  return value.map((artifact) => {
250
322
  if (!isRecord(artifact) || typeof artifact.artifactId !== "string" || !Array.isArray(artifact.parts) || artifact.parts.length > 32)
251
323
  throw new A2AError("Malformed A2A artifact", 502, "ERR_PRISM_A2A_REMOTE");
252
- return { artifactId: artifact.artifactId, parts: artifact.parts.map((part) => {
253
- if (!isRecord(part) || typeof part.text !== "string")
254
- throw new A2AError("Unsupported A2A artifact part", 502, "ERR_PRISM_A2A_REMOTE");
255
- return { text: part.text };
256
- }) };
324
+ return { artifactId: artifact.artifactId, parts: artifact.parts.map(parseRemotePart) };
257
325
  });
258
326
  }
327
+ function parseRemotePart(value) {
328
+ if (!isRecord(value))
329
+ throw new A2AError("Malformed A2A part", 502, "ERR_PRISM_A2A_REMOTE");
330
+ const keys = ["text", "raw", "url", "data"].filter((key) => Object.hasOwn(value, key));
331
+ if (keys.length !== 1)
332
+ throw new A2AError("Malformed A2A part union", 502, "ERR_PRISM_A2A_REMOTE");
333
+ const base = { mediaType: typeof value.mediaType === "string" ? value.mediaType : undefined, filename: typeof value.filename === "string" ? value.filename : undefined };
334
+ if (keys[0] === "text" && typeof value.text === "string")
335
+ return { ...base, text: value.text };
336
+ if (keys[0] === "raw" && typeof value.raw === "string" && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value.raw))
337
+ return { ...base, raw: value.raw };
338
+ if (keys[0] === "url" && typeof value.url === "string") {
339
+ const url = new URL(value.url);
340
+ if (url.protocol !== "https:" || url.username || url.password || url.hash)
341
+ throw new A2AError("Unsafe remote A2A URL part", 502, "ERR_PRISM_A2A_REMOTE");
342
+ return { ...base, url: url.href };
343
+ }
344
+ if (keys[0] === "data")
345
+ return { ...base, data: structuredClone(value.data) };
346
+ throw new A2AError("Malformed A2A part", 502, "ERR_PRISM_A2A_REMOTE");
347
+ }
348
+ function parseRemoteMessage(value) { if (!isRecord(value) || typeof value.messageId !== "string" || !Array.isArray(value.parts) || (value.role !== "ROLE_USER" && value.role !== "ROLE_AGENT" && value.role !== "user" && value.role !== "agent"))
349
+ throw new A2AError("Malformed A2A message", 502, "ERR_PRISM_A2A_REMOTE"); return { role: value.role, messageId: value.messageId, parts: value.parts.map(parseRemotePart), contextId: typeof value.contextId === "string" ? value.contextId : undefined, taskId: typeof value.taskId === "string" ? value.taskId : undefined }; }
350
+ function parseTaskEvent(value) {
351
+ if (!isRecord(value) || typeof value.eventId !== "string" || !value.eventId)
352
+ throw new A2AError("Malformed A2A task event", 502, "ERR_PRISM_A2A_REMOTE");
353
+ if (isRecord(value.task))
354
+ return { eventId: value.eventId, task: parseTaskResult(value.task) };
355
+ if (isRecord(value.statusUpdate) && typeof value.statusUpdate.taskId === "string" && typeof value.statusUpdate.contextId === "string" && isRecord(value.statusUpdate.status)) {
356
+ const parsed = parseTaskResult({ id: value.statusUpdate.taskId, contextId: value.statusUpdate.contextId, status: value.statusUpdate.status });
357
+ return { eventId: value.eventId, statusUpdate: { taskId: parsed.id, contextId: parsed.contextId, status: parsed.status } };
358
+ }
359
+ if (isRecord(value.artifactUpdate) && typeof value.artifactUpdate.taskId === "string" && typeof value.artifactUpdate.contextId === "string" && isRecord(value.artifactUpdate.artifact))
360
+ return { eventId: value.eventId, artifactUpdate: { taskId: value.artifactUpdate.taskId, contextId: value.artifactUpdate.contextId, artifact: parseArtifacts([value.artifactUpdate.artifact])[0], append: value.artifactUpdate.append === true, lastChunk: value.artifactUpdate.lastChunk === true } };
361
+ throw new A2AError("Malformed A2A task event", 502, "ERR_PRISM_A2A_REMOTE");
362
+ }
363
+ function parsePushConfig(value) { if (!isRecord(value) || typeof value.id !== "string" || typeof value.taskId !== "string" || typeof value.url !== "string")
364
+ throw new A2AError("Malformed A2A push config", 502, "ERR_PRISM_A2A_REMOTE"); const url = new URL(value.url); if (url.protocol !== "https:" || url.username || url.password || url.hash)
365
+ throw new A2AError("Unsafe A2A push URL", 502, "ERR_PRISM_A2A_REMOTE"); return { id: value.id, taskId: value.taskId, url: url.href, token: typeof value.token === "string" ? value.token : undefined, authentication: isRecord(value.authentication) && typeof value.authentication.scheme === "string" ? { scheme: value.authentication.scheme, credentials: typeof value.authentication.credentials === "string" ? value.authentication.credentials : undefined } : undefined }; }
366
+ function remoteProtocolError(code, message, options) { return new A2AError(safeRemote(message, options), code === -32001 ? 404 : code === -32004 ? 501 : 502, code === -32001 ? "ERR_PRISM_A2A_TASK_NOT_FOUND" : code === -32004 ? "ERR_PRISM_A2A_UNSUPPORTED" : "ERR_PRISM_A2A_REMOTE"); }
259
367
  function parseRpcResponse(value, id) {
260
368
  if (!isRecord(value) || value.jsonrpc !== "2.0" || value.id !== id)
261
369
  throw new A2AError("Malformed A2A JSON-RPC response", 502, "ERR_PRISM_A2A_REMOTE");
@@ -339,16 +447,6 @@ function requireAllowedHttpsUrl(value, origins) {
339
447
  throw new A2AError("A2A endpoint origin is not allow-listed HTTPS", 403, "ERR_PRISM_A2A_ORIGIN");
340
448
  return url;
341
449
  }
342
- function clientLimits(input = {}) {
343
- const output = {};
344
- for (const key of Object.keys(DEFAULTS)) {
345
- const value = input[key] ?? DEFAULTS[key];
346
- if (!Number.isSafeInteger(value) || value < 1 || value > HARD[key])
347
- throw new A2AError(`${key} is invalid`, 400, "ERR_PRISM_A2A_CONFIG");
348
- output[key] = value;
349
- }
350
- return output;
351
- }
352
450
  function ownedSignal(parent, timeoutMs) {
353
451
  const controller = new AbortController();
354
452
  const abort = () => controller.abort(parent?.reason);
@@ -0,0 +1,54 @@
1
+ import type { A2ALimits, A2AMessage, A2APart, A2APartPolicy, A2ATask } from "./a2a-types.js";
2
+ export declare const A2A_DEFAULT_LIMITS: {
3
+ readonly maxRequestBytes: number;
4
+ readonly maxResponseBytes: number;
5
+ readonly maxEventBytes: number;
6
+ readonly maxStreamBytes: number;
7
+ readonly maxStreamEvents: 10000;
8
+ readonly maxConcurrentRequests: 16;
9
+ readonly timeoutMs: 120000;
10
+ readonly maxCardBytes: number;
11
+ readonly maxIdBytes: 256;
12
+ readonly maxParts: 32;
13
+ readonly maxPartBytes: number;
14
+ readonly maxRawBytes: number;
15
+ readonly maxDataBytes: number;
16
+ readonly maxArtifacts: 32;
17
+ readonly maxHistory: 100;
18
+ readonly maxPageSize: 100;
19
+ readonly maxCursorBytes: 4096;
20
+ readonly maxReplayEvents: 1000;
21
+ readonly maxPushConfigs: 10;
22
+ };
23
+ export declare const A2A_HARD_LIMITS: {
24
+ readonly maxRequestBytes: number;
25
+ readonly maxResponseBytes: number;
26
+ readonly maxEventBytes: number;
27
+ readonly maxStreamBytes: number;
28
+ readonly maxStreamEvents: 100000;
29
+ readonly maxConcurrentRequests: 256;
30
+ readonly timeoutMs: number;
31
+ readonly maxCardBytes: number;
32
+ readonly maxIdBytes: 4096;
33
+ readonly maxParts: 256;
34
+ readonly maxPartBytes: number;
35
+ readonly maxRawBytes: number;
36
+ readonly maxDataBytes: number;
37
+ readonly maxArtifacts: 256;
38
+ readonly maxHistory: 1000;
39
+ readonly maxPageSize: 1000;
40
+ readonly maxCursorBytes: number;
41
+ readonly maxReplayEvents: 10000;
42
+ readonly maxPushConfigs: 100;
43
+ };
44
+ export type ResolvedA2ALimits = {
45
+ readonly [K in keyof typeof A2A_DEFAULT_LIMITS]: number;
46
+ };
47
+ export declare function resolveA2ALimits(input?: A2ALimits): ResolvedA2ALimits;
48
+ export declare function parseA2AMessage(value: unknown, limits: ResolvedA2ALimits, policy?: A2APartPolicy): Promise<A2AMessage>;
49
+ export declare function parseA2APart(value: unknown, limits: ResolvedA2ALimits, policy?: A2APartPolicy): Promise<A2APart>;
50
+ export declare function validateA2ATask(task: A2ATask, limits: ResolvedA2ALimits, policy?: A2APartPolicy): Promise<A2ATask>;
51
+ export declare function requireId(value: unknown, limits: ResolvedA2ALimits, label?: string): string;
52
+ export declare function optionalCursor(value: unknown, limits: ResolvedA2ALimits): string | undefined;
53
+ export declare function bounded<T>(value: T, maxBytes: number, label: string): T;
54
+ export declare function record(value: unknown): value is Record<string, unknown>;
@@ -0,0 +1,121 @@
1
+ import { A2AError } from "./errors.js";
2
+ export const A2A_DEFAULT_LIMITS = { maxRequestBytes: 64 * 1024, maxResponseBytes: 1024 * 1024, maxEventBytes: 64 * 1024, maxStreamBytes: 10 * 1024 * 1024, maxStreamEvents: 10_000, maxConcurrentRequests: 16, timeoutMs: 120_000, maxCardBytes: 64 * 1024, maxIdBytes: 256, maxParts: 32, maxPartBytes: 1024 * 1024, maxRawBytes: 1024 * 1024, maxDataBytes: 256 * 1024, maxArtifacts: 32, maxHistory: 100, maxPageSize: 100, maxCursorBytes: 4096, maxReplayEvents: 1000, maxPushConfigs: 10 };
3
+ export const A2A_HARD_LIMITS = { maxRequestBytes: 1024 * 1024, maxResponseBytes: 8 * 1024 * 1024, maxEventBytes: 1024 * 1024, maxStreamBytes: 64 * 1024 * 1024, maxStreamEvents: 100_000, maxConcurrentRequests: 256, timeoutMs: 30 * 60_000, maxCardBytes: 1024 * 1024, maxIdBytes: 4096, maxParts: 256, maxPartBytes: 8 * 1024 * 1024, maxRawBytes: 8 * 1024 * 1024, maxDataBytes: 4 * 1024 * 1024, maxArtifacts: 256, maxHistory: 1000, maxPageSize: 1000, maxCursorBytes: 16 * 1024, maxReplayEvents: 10_000, maxPushConfigs: 100 };
4
+ export function resolveA2ALimits(input = {}) {
5
+ const output = {};
6
+ for (const key of Object.keys(A2A_DEFAULT_LIMITS)) {
7
+ const value = input[key] ?? A2A_DEFAULT_LIMITS[key];
8
+ if (!Number.isSafeInteger(value) || value < 1 || value > A2A_HARD_LIMITS[key])
9
+ throw new A2AError(`${key} is invalid`, 400, "ERR_PRISM_A2A_CONFIG");
10
+ output[key] = value;
11
+ }
12
+ return output;
13
+ }
14
+ export async function parseA2AMessage(value, limits, policy = {}) {
15
+ if (!record(value) || (value.role !== "user" && value.role !== "ROLE_USER" && value.role !== "agent" && value.role !== "ROLE_AGENT") || !id(value.messageId, limits) || !Array.isArray(value.parts) || value.parts.length < 1 || value.parts.length > limits.maxParts)
16
+ throw new A2AError("Invalid A2A message", 400, "ERR_PRISM_A2A_MESSAGE");
17
+ const parts = [];
18
+ for (const part of value.parts)
19
+ parts.push(await parseA2APart(part, limits, policy));
20
+ const message = { role: value.role, messageId: value.messageId, parts, contextId: optionalId(value.contextId, limits), taskId: optionalId(value.taskId, limits), metadata: record(value.metadata) ? value.metadata : undefined };
21
+ bounded(message, limits.maxRequestBytes, "A2A message");
22
+ return message;
23
+ }
24
+ export async function parseA2APart(value, limits, policy = {}) {
25
+ if (!record(value))
26
+ throw new A2AError("Invalid A2A part", 400, "ERR_PRISM_A2A_PART");
27
+ const variants = ["text", "raw", "url", "data"].filter((key) => Object.hasOwn(value, key));
28
+ if (Object.keys(value).some((key) => !["text", "raw", "url", "data", "mediaType", "filename", "metadata"].includes(key)))
29
+ throw new A2AError("Unknown A2A part field", 400, "ERR_PRISM_A2A_PART");
30
+ if (variants.length !== 1)
31
+ throw new A2AError("A2A part requires exactly one content field", 400, "ERR_PRISM_A2A_PART");
32
+ const base = { mediaType: optionalString(value.mediaType, 256), filename: optionalString(value.filename, 1024), metadata: record(value.metadata) ? value.metadata : undefined };
33
+ let part;
34
+ if (variants[0] === "text" && typeof value.text === "string")
35
+ part = { ...base, text: value.text };
36
+ else if (variants[0] === "raw" && policy.allowRaw && typeof value.raw === "string" && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value.raw) && Buffer.from(value.raw, "base64").byteLength <= limits.maxRawBytes)
37
+ part = { ...base, raw: value.raw };
38
+ else if (variants[0] === "url" && policy.allowUrl && policy.validateUrl && typeof value.url === "string") {
39
+ let url;
40
+ try {
41
+ url = new URL(value.url);
42
+ }
43
+ catch {
44
+ throw new A2AError("Invalid A2A file URL", 400, "ERR_PRISM_A2A_PART");
45
+ }
46
+ if (url.protocol !== "https:" || url.username || url.password || url.hash)
47
+ throw new A2AError("A2A file URL requires credential-free HTTPS", 403, "ERR_PRISM_A2A_ORIGIN");
48
+ await policy.validateUrl?.(url); // Validation only; never dereference.
49
+ part = { ...base, url: url.href };
50
+ }
51
+ else if (variants[0] === "data" && policy.allowData) {
52
+ bounded(value.data, limits.maxDataBytes, "A2A data part");
53
+ part = { ...base, data: structuredClone(value.data) };
54
+ }
55
+ else
56
+ throw new A2AError("Unsupported A2A part", 400, "ERR_PRISM_A2A_PART");
57
+ bounded(part, limits.maxPartBytes, "A2A part");
58
+ return part;
59
+ }
60
+ export async function validateA2ATask(task, limits, policy = { allowRaw: true, allowUrl: true, allowData: true }) {
61
+ if (!id(task.id, limits) || !id(task.contextId, limits) || !task.status || !TASK_STATES.has(task.status.state) || !Number.isFinite(Date.parse(task.status.timestamp)))
62
+ throw new A2AError("Invalid A2A task", 500, "ERR_PRISM_A2A_TASK");
63
+ if ((task.artifacts?.length ?? 0) > limits.maxArtifacts || (task.history?.length ?? 0) > limits.maxHistory)
64
+ throw new A2AError("A2A task collection limit exceeded", 507, "ERR_PRISM_A2A_RESPONSE_LIMIT");
65
+ for (const artifact of task.artifacts ?? [])
66
+ await validateArtifact(artifact, limits, policy);
67
+ for (const message of task.history ?? [])
68
+ await parseA2AMessage(message, limits, policy);
69
+ bounded(task, limits.maxResponseBytes, "A2A task");
70
+ return task;
71
+ }
72
+ async function validateArtifact(value, limits, policy) {
73
+ if (!id(value.artifactId, limits) || !value.parts.length || value.parts.length > limits.maxParts)
74
+ throw new A2AError("Invalid A2A artifact", 500, "ERR_PRISM_A2A_TASK");
75
+ for (const part of value.parts)
76
+ await parseA2APart(part, limits, policy);
77
+ }
78
+ export function requireId(value, limits, label = "task id") { if (!id(value, limits))
79
+ throw new A2AError(`Invalid A2A ${label}`, 400, "ERR_PRISM_A2A_REQUEST"); return value; }
80
+ export function optionalCursor(value, limits) { if (value === undefined || value === "")
81
+ return undefined; if (typeof value !== "string" || Buffer.byteLength(value) > limits.maxCursorBytes)
82
+ throw new A2AError("Invalid A2A page/event cursor", 400, "ERR_PRISM_A2A_REQUEST"); return value; }
83
+ export function bounded(value, maxBytes, label) {
84
+ let properties = 0;
85
+ const stack = [{ value, depth: 0 }];
86
+ const seen = new Set();
87
+ while (stack.length) {
88
+ const item = stack.pop();
89
+ if (typeof item.value === "number" && !Number.isFinite(item.value))
90
+ throw new A2AError(`${label} contains non-finite number`, 400, "ERR_PRISM_A2A_REQUEST");
91
+ if (!item.value || typeof item.value !== "object")
92
+ continue;
93
+ if (item.depth > 64 || seen.has(item.value))
94
+ throw new A2AError(`${label} exceeds JSON depth or is cyclic`, 400, "ERR_PRISM_A2A_REQUEST");
95
+ seen.add(item.value);
96
+ const values = Array.isArray(item.value) ? item.value : Object.values(item.value);
97
+ properties += values.length;
98
+ if (properties > 10_000)
99
+ throw new A2AError(`${label} exceeds JSON property limit`, 400, "ERR_PRISM_A2A_REQUEST");
100
+ for (const child of values)
101
+ stack.push({ value: child, depth: item.depth + 1 });
102
+ }
103
+ let json;
104
+ try {
105
+ json = JSON.stringify(value);
106
+ }
107
+ catch {
108
+ throw new A2AError(`${label} is not JSON`, 400, "ERR_PRISM_A2A_REQUEST");
109
+ }
110
+ if (Buffer.byteLength(json) > maxBytes)
111
+ throw new A2AError(`${label} exceeds max bytes`, 507, "ERR_PRISM_A2A_RESPONSE_LIMIT");
112
+ return value;
113
+ }
114
+ export function record(value) { return !!value && typeof value === "object" && !Array.isArray(value); }
115
+ function id(value, limits) { return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= limits.maxIdBytes; }
116
+ function optionalId(value, limits) { return value === undefined ? undefined : requireId(value, limits); }
117
+ function optionalString(value, max) { if (value === undefined)
118
+ return undefined; if (typeof value !== "string" || Buffer.byteLength(value) > max)
119
+ throw new A2AError("Invalid A2A part metadata", 400, "ERR_PRISM_A2A_PART"); return value; }
120
+ const TASK_STATES = new Set(["TASK_STATE_SUBMITTED", "TASK_STATE_WORKING", "TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_INPUT_REQUIRED", "TASK_STATE_REJECTED", "TASK_STATE_AUTH_REQUIRED"]);
121
+ //# sourceMappingURL=a2a-parts.js.map
@@ -0,0 +1,20 @@
1
+ import type { A2ALimits, A2APushConfig, A2ATaskEvent } from "./a2a-types.js";
2
+ export interface A2APushDelivery {
3
+ deliver(input: {
4
+ readonly config: A2APushConfig;
5
+ readonly event: A2ATaskEvent;
6
+ readonly idempotencyKey: string;
7
+ readonly attempt: number;
8
+ readonly signal: AbortSignal;
9
+ }): Promise<void>;
10
+ }
11
+ export interface DeliverA2APushEventOptions {
12
+ readonly signal?: AbortSignal;
13
+ readonly maxAttempts?: number;
14
+ readonly timeoutMs?: number;
15
+ readonly limits?: A2ALimits;
16
+ }
17
+ /** Explicit host call; stores no timer/config/event and performs no network I/O itself. */
18
+ export declare function deliverA2APushEvent(delivery: A2APushDelivery, config: A2APushConfig, event: A2ATaskEvent, options?: DeliverA2APushEventOptions): Promise<{
19
+ readonly attempts: number;
20
+ }>;
@@ -0,0 +1,38 @@
1
+ import { bounded, resolveA2ALimits } from "./a2a-parts.js";
2
+ import { A2AError } from "./errors.js";
3
+ /** Explicit host call; stores no timer/config/event and performs no network I/O itself. */
4
+ export async function deliverA2APushEvent(delivery, config, event, options = {}) {
5
+ const limits = resolveA2ALimits(options.limits);
6
+ const maxAttempts = options.maxAttempts ?? 1, timeoutMs = options.timeoutMs ?? 10_000;
7
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 3 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000)
8
+ throw new A2AError("Invalid A2A push delivery limits", 400, "ERR_PRISM_A2A_CONFIG");
9
+ bounded(event, limits.maxEventBytes, "A2A push event");
10
+ if (!event.eventId || Buffer.byteLength(event.eventId) > limits.maxCursorBytes)
11
+ throw new A2AError("Invalid A2A push event id", 400, "ERR_PRISM_A2A_PUSH");
12
+ let last;
13
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
14
+ const controller = new AbortController();
15
+ const abort = () => controller.abort(options.signal?.reason);
16
+ if (options.signal?.aborted)
17
+ abort();
18
+ else
19
+ options.signal?.addEventListener("abort", abort, { once: true });
20
+ const timer = setTimeout(() => controller.abort(new DOMException("A2A push delivery timed out", "AbortError")), timeoutMs);
21
+ try {
22
+ controller.signal.throwIfAborted();
23
+ await Promise.race([delivery.deliver({ config, event, idempotencyKey: event.eventId, attempt, signal: controller.signal }), new Promise((_resolve, reject) => controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true }))]);
24
+ return { attempts: attempt };
25
+ }
26
+ catch (error) {
27
+ last = error;
28
+ if (controller.signal.aborted || attempt === maxAttempts)
29
+ break;
30
+ }
31
+ finally {
32
+ clearTimeout(timer);
33
+ options.signal?.removeEventListener("abort", abort);
34
+ }
35
+ }
36
+ throw new A2AError(last instanceof DOMException && last.name === "AbortError" ? "A2A push delivery timed out" : "A2A push delivery failed", 502, "ERR_PRISM_A2A_PUSH");
37
+ }
38
+ //# sourceMappingURL=a2a-push.js.map
@@ -1,29 +1,31 @@
1
1
  import { createA2AAgentCard } from "./a2a-card.js";
2
+ import { bounded, optionalCursor, parseA2AMessage, record, requireId, resolveA2ALimits, validateA2ATask } from "./a2a-parts.js";
2
3
  import { A2AError } from "./errors.js";
3
- const JSON_HEADERS = { "content-type": "application/a2a+json; charset=utf-8" };
4
- const SSE_HEADERS = { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform" };
5
- const DEFAULTS = { maxRequestBytes: 64 * 1024, maxResponseBytes: 1024 * 1024, maxEventBytes: 64 * 1024, maxStreamBytes: 10 * 1024 * 1024, maxStreamEvents: 10_000, maxConcurrentRequests: 16, timeoutMs: 120_000, maxCardBytes: 64 * 1024 };
6
- const HARD = { maxRequestBytes: 1024 * 1024, maxResponseBytes: 8 * 1024 * 1024, maxEventBytes: 1024 * 1024, maxStreamBytes: 64 * 1024 * 1024, maxStreamEvents: 100_000, maxConcurrentRequests: 256, timeoutMs: 30 * 60_000, maxCardBytes: 1024 * 1024 };
4
+ const JSON_HEADERS = { "content-type": "application/a2a+json; charset=utf-8", "a2a-version": "1.0" };
5
+ const SSE_HEADERS = { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", "a2a-version": "1.0" };
7
6
  export function createA2AHandler(options) {
8
7
  const limits = resolveA2ALimits(options.limits);
9
8
  const card = createA2AAgentCard(options.card);
9
+ if (Boolean(card.capabilities.pushNotifications) !== Boolean(options.push))
10
+ throw new A2AError("Agent card push capability must match push adapter", 400, "ERR_PRISM_A2A_CONFIG");
10
11
  const endpointPath = options.endpointPath ?? new URL(card.supportedInterfaces[0].url).pathname;
11
12
  if (!endpointPath.startsWith("/"))
12
13
  throw new A2AError("endpointPath must be absolute", 400, "ERR_PRISM_A2A_CONFIG");
13
14
  const cardJson = JSON.stringify(card);
14
- if (new TextEncoder().encode(cardJson).byteLength > limits.maxCardBytes)
15
+ if (Buffer.byteLength(cardJson) > limits.maxCardBytes)
15
16
  throw new A2AError("Agent card exceeds max bytes", 400, "ERR_PRISM_A2A_CARD");
16
17
  let active = 0;
17
- let sequence = 0;
18
18
  return async (request) => {
19
- let acquired = false;
20
- let transferred = false;
19
+ let acquired = false, transferred = false;
21
20
  try {
22
21
  const path = new URL(request.url).pathname;
23
22
  if (request.method === "GET" && path === "/.well-known/agent-card.json")
24
- return new Response(cardJson, { status: 200, headers: JSON_HEADERS });
23
+ return new Response(cardJson, { headers: JSON_HEADERS });
25
24
  if (request.method !== "POST" || path !== endpointPath)
26
25
  return errorResponse(404, "Not found", null);
26
+ const version = request.headers.get("a2a-version");
27
+ if (version && version !== "1.0")
28
+ return rpcError(null, -32009, "Unsupported A2A version");
27
29
  if (active >= limits.maxConcurrentRequests)
28
30
  return errorResponse(429, "Too many requests", null);
29
31
  active += 1;
@@ -33,31 +35,67 @@ export function createA2AHandler(options) {
33
35
  const contentType = request.headers.get("content-type")?.split(";", 1)[0]?.trim();
34
36
  if (contentType !== "application/json" && contentType !== "application/a2a+json")
35
37
  return errorResponse(415, "Unsupported media type", null);
36
- const body = await readJson(request, limits.maxRequestBytes, owned.signal);
37
- const rpc = parseRpc(body);
38
- const authorized = await abortable(Promise.resolve(options.authorize({ request, method: rpc.method, signal: owned.signal })), owned.signal);
39
- if (!authorized)
38
+ const rpc = parseRpc(await readJson(request, limits.maxRequestBytes, owned.signal));
39
+ const authorization = await abortable(Promise.resolve(options.authorize({ request, method: rpc.method, signal: owned.signal })), owned.signal);
40
+ if (!authorization)
40
41
  return errorResponse(403, "Forbidden", rpc.id);
41
42
  if (rpc.method === "GetExtendedAgentCard")
42
- return boundedJson({ jsonrpc: "2.0", id: rpc.id, result: card }, limits.maxResponseBytes, options);
43
- if (rpc.method !== "SendMessage" && rpc.method !== "SendStreamingMessage")
44
- return boundedJson({ jsonrpc: "2.0", id: rpc.id, error: { code: -32601, message: "Method not found" } }, limits.maxResponseBytes, options);
45
- const message = parseMessage(rpc.params?.message, limits.maxRequestBytes);
46
- const input = message.parts.map((part) => part.text).join("\n");
47
- sequence += 1;
48
- const taskId = `task-${crypto.randomUUID()}`;
49
- const contextId = message.contextId ?? `context-${sequence}-${crypto.randomUUID()}`;
50
- const session = await abortable(Promise.resolve(options.exposure.sessionFactory(authorized)), owned.signal);
51
- if (rpc.method === "SendMessage") {
52
- const result = await abortable(session.run(input, { ownership: authorized.ownership, metadata: authorized.metadata, signal: owned.signal, redactor: options.redactor }), owned.signal);
53
- return boundedJson({ jsonrpc: "2.0", id: rpc.id, result: { task: toTask(taskId, contextId, result, options) } }, limits.maxResponseBytes, options);
43
+ return card.capabilities.extendedAgentCard ? json(rpc.id, card, limits, options) : rpcError(rpc.id, -32004, "Extended Agent Card unavailable");
44
+ if (rpc.method === "SendMessage" || rpc.method === "SendStreamingMessage") {
45
+ const message = await parseA2AMessage(rpc.params?.message, limits, options.parts);
46
+ if (options.tasks) {
47
+ const task = await options.tasks.start({ message, authorization, signal: owned.signal, returnImmediately: record(rpc.params?.configuration) && rpc.params.configuration.returnImmediately === true });
48
+ await validateA2ATask(task, limits, options.parts);
49
+ if (rpc.method === "SendMessage")
50
+ return json(rpc.id, { task }, limits, options);
51
+ transferred = true;
52
+ const events = terminal(task.status.state) ? oneEvent(task) : options.tasks.subscribe({ id: task.id, authorization, signal: owned.signal });
53
+ return streamResponse(rpc.id, events, owned, limits, options, () => { active -= 1; });
54
+ }
55
+ if (message.parts.some((part) => !("text" in part)))
56
+ return rpcError(rpc.id, -32004, "Durable task lifecycle required for rich parts");
57
+ const input = message.parts.map((part) => part.text).join("\n");
58
+ const session = await abortable(Promise.resolve(options.exposure.sessionFactory(authorization)), owned.signal);
59
+ const taskId = `task-${crypto.randomUUID()}`, contextId = message.contextId ?? `context-${crypto.randomUUID()}`;
60
+ if (rpc.method === "SendMessage")
61
+ return json(rpc.id, { task: toTask(taskId, contextId, await abortable(session.run(input, { ownership: authorization.ownership, metadata: authorization.metadata, signal: owned.signal, redactor: options.redactor }), owned.signal), options) }, limits, options);
62
+ transferred = true;
63
+ const events = runEvents(taskId, contextId, () => session.run(input, { ownership: authorization.ownership, metadata: authorization.metadata, signal: owned.signal, redactor: options.redactor }), options);
64
+ return streamResponse(rpc.id, events, owned, limits, options, () => { active -= 1; });
54
65
  }
55
- if (rpc.method === "SendStreamingMessage") {
66
+ if (["GetTask", "ListTasks", "CancelTask", "SubscribeToTask"].includes(rpc.method)) {
67
+ if (!options.tasks)
68
+ return rpcError(rpc.id, -32004, "Task lifecycle unavailable");
69
+ if (rpc.method === "GetTask") {
70
+ const task = await options.tasks.get({ id: requireId(rpc.params?.id, limits), historyLength: integer(rpc.params?.historyLength, 0, limits.maxHistory), authorization, signal: owned.signal });
71
+ return task ? json(rpc.id, await validateA2ATask(task, limits, options.parts), limits, options) : rpcError(rpc.id, -32001, "Task not found");
72
+ }
73
+ if (rpc.method === "ListTasks") {
74
+ const pageSize = integer(rpc.params?.pageSize, 50, limits.maxPageSize), pageToken = optionalCursor(rpc.params?.pageToken, limits), contextId = rpc.params?.contextId === undefined ? undefined : requireId(rpc.params.contextId, limits, "context id");
75
+ const page = await options.tasks.list({ pageSize, pageToken, contextId, authorization, signal: owned.signal });
76
+ if (page.tasks.length > pageSize || page.tasks.length > limits.maxPageSize)
77
+ throw new A2AError("Task page exceeds limit", 507, "ERR_PRISM_A2A_RESPONSE_LIMIT");
78
+ for (const task of page.tasks)
79
+ await validateA2ATask(task, limits, options.parts);
80
+ optionalCursor(page.nextPageToken, limits);
81
+ return json(rpc.id, page, limits, options);
82
+ }
83
+ if (rpc.method === "CancelTask") {
84
+ const task = await options.tasks.cancel({ id: requireId(rpc.params?.id, limits), authorization, signal: owned.signal });
85
+ return task ? json(rpc.id, await validateA2ATask(task, limits, options.parts), limits, options) : rpcError(rpc.id, -32001, "Task not found");
86
+ }
87
+ const taskId = requireId(rpc.params?.id, limits), afterEventId = optionalCursor(rpc.params?.afterEventId, limits);
88
+ const current = await options.tasks.get({ id: taskId, historyLength: 0, authorization, signal: owned.signal });
89
+ if (!current)
90
+ return rpcError(rpc.id, -32001, "Task not found");
91
+ if (finalTerminal(current.status.state))
92
+ return rpcError(rpc.id, -32004, "Terminal task cannot be subscribed");
56
93
  transferred = true;
57
- const stream = taskStream(rpc.id, taskId, contextId, () => session.run(input, { ownership: authorized.ownership, metadata: authorized.metadata, signal: owned.signal, redactor: options.redactor }), owned, limits, options, () => { active -= 1; });
58
- return new Response(stream, { status: 200, headers: SSE_HEADERS });
94
+ return streamResponse(rpc.id, options.tasks.subscribe({ id: taskId, afterEventId, authorization, signal: owned.signal }), owned, limits, options, () => { active -= 1; });
59
95
  }
60
- throw new A2AError("Method not found", 400, "ERR_PRISM_A2A_METHOD");
96
+ if (rpc.method.includes("TaskPushNotificationConfig"))
97
+ return await pushOperation(rpc, authorization, owned.signal, limits, options);
98
+ return rpcError(rpc.id, -32601, "Method not found");
61
99
  }
62
100
  finally {
63
101
  if (!transferred)
@@ -74,157 +112,140 @@ export function createA2AHandler(options) {
74
112
  }
75
113
  };
76
114
  }
77
- function taskStream(id, taskId, contextId, run, owned, limits, options, release) {
78
- const iterator = (async function* () {
79
- yield { jsonrpc: "2.0", id, result: { task: { id: taskId, contextId, status: { state: "TASK_STATE_WORKING", timestamp: new Date().toISOString() } } } };
80
- try {
81
- const result = await run();
82
- yield { jsonrpc: "2.0", id, result: { task: toTask(taskId, contextId, result, options) } };
83
- }
84
- catch (error) {
85
- yield { jsonrpc: "2.0", id, error: { code: -32000, message: safeError(error, options) } };
86
- }
87
- })()[Symbol.asyncIterator]();
88
- let events = 0;
89
- let bytes = 0;
90
- let released = false;
115
+ async function pushOperation(rpc, authorization, signal, limits, options) {
116
+ if (!options.push)
117
+ return rpcError(rpc.id, -32004, "Push notifications unavailable");
118
+ const taskId = requireId(rpc.params?.taskId, limits), id = rpc.params?.id === undefined ? undefined : requireId(rpc.params.id, limits, "push config id");
119
+ if (rpc.method === "CreateTaskPushNotificationConfig") {
120
+ const supplied = record(rpc.params?.config) ? rpc.params.config : rpc.params;
121
+ if (!record(supplied))
122
+ throw new A2AError("Invalid push config", 400, "ERR_PRISM_A2A_PUSH");
123
+ const config = await parsePush({ ...supplied, taskId }, limits, options);
124
+ return json(rpc.id, publicPush(await options.push.create({ config, authorization, signal })), limits, options);
125
+ }
126
+ if (rpc.method === "GetTaskPushNotificationConfig") {
127
+ const value = await options.push.get({ taskId, id: id, authorization, signal });
128
+ return value ? json(rpc.id, publicPush(value), limits, options) : rpcError(rpc.id, -32001, "Task or push config not found");
129
+ }
130
+ if (rpc.method === "ListTaskPushNotificationConfigs") {
131
+ const pageSize = integer(rpc.params?.pageSize, limits.maxPushConfigs, limits.maxPushConfigs), pageToken = optionalCursor(rpc.params?.pageToken, limits);
132
+ const page = await options.push.list({ taskId, pageSize, pageToken, authorization, signal });
133
+ if (page.configs.length > pageSize)
134
+ throw new A2AError("Push config page exceeds limit", 507, "ERR_PRISM_A2A_RESPONSE_LIMIT");
135
+ return json(rpc.id, { configs: page.configs.map(publicPush), nextPageToken: optionalCursor(page.nextPageToken, limits) }, limits, options);
136
+ }
137
+ if (rpc.method === "DeleteTaskPushNotificationConfig")
138
+ return await options.push.delete({ taskId, id: id, authorization, signal }) ? json(rpc.id, {}, limits, options) : rpcError(rpc.id, -32001, "Task or push config not found");
139
+ return rpcError(rpc.id, -32601, "Method not found");
140
+ }
141
+ async function parsePush(value, limits, options) {
142
+ const id = requireId(value.id, limits, "push config id"), taskId = requireId(value.taskId, limits);
143
+ if (typeof value.url !== "string" || !options.parts?.validateUrl)
144
+ throw new A2AError("Push URL policy required", 403, "ERR_PRISM_A2A_ORIGIN");
145
+ let url;
146
+ try {
147
+ url = new URL(value.url);
148
+ }
149
+ catch {
150
+ throw new A2AError("Invalid push URL", 400, "ERR_PRISM_A2A_PUSH");
151
+ }
152
+ if (url.protocol !== "https:" || url.username || url.password || url.hash)
153
+ throw new A2AError("Push URL requires credential-free HTTPS", 403, "ERR_PRISM_A2A_ORIGIN");
154
+ await options.parts.validateUrl(url);
155
+ const authentication = record(value.authentication) && typeof value.authentication.scheme === "string" ? { scheme: value.authentication.scheme.slice(0, 64), credentials: typeof value.authentication.credentials === "string" ? value.authentication.credentials.slice(0, limits.maxPartBytes) : undefined } : undefined;
156
+ return bounded({ id, taskId, url: url.href, token: typeof value.token === "string" ? value.token.slice(0, limits.maxPartBytes) : undefined, authentication }, limits.maxPartBytes, "Push config");
157
+ }
158
+ function publicPush(value) { return { id: value.id, taskId: value.taskId, url: value.url, authentication: value.authentication ? { scheme: value.authentication.scheme } : undefined }; }
159
+ function streamResponse(id, source, owned, limits, options, release) {
160
+ const iterator = source[Symbol.asyncIterator]();
161
+ let events = 0, bytes = 0, released = false, previous = "";
91
162
  const finish = () => { if (!released) {
92
163
  released = true;
93
164
  owned.dispose();
94
165
  release();
166
+ void iterator.return?.();
95
167
  } };
96
- return new ReadableStream({
97
- async pull(controller) {
98
- try {
99
- const next = await iterator.next();
100
- if (next.done) {
101
- finish();
102
- controller.close();
103
- return;
104
- }
105
- const payload = options.redactor?.redact(next.value) ?? next.value;
106
- const chunk = new TextEncoder().encode(`data: ${JSON.stringify(payload)}\n\n`);
107
- events += 1;
108
- bytes += chunk.byteLength;
109
- if (chunk.byteLength > limits.maxEventBytes || events > limits.maxStreamEvents || bytes > limits.maxStreamBytes)
110
- throw new A2AError("A2A stream limit exceeded", 507, "ERR_PRISM_A2A_STREAM_LIMIT");
111
- controller.enqueue(chunk);
112
- }
113
- catch (error) {
168
+ return new Response(new ReadableStream({
169
+ async pull(controller) { try {
170
+ const next = await iterator.next();
171
+ if (next.done) {
114
172
  finish();
115
- controller.error(error);
173
+ controller.close();
174
+ return;
116
175
  }
117
- },
118
- cancel(reason) { owned.abort(reason); finish(); void iterator.return?.(); },
119
- });
120
- }
121
- function toTask(taskId, contextId, result, options) {
122
- const state = result.status === "succeeded" ? "TASK_STATE_COMPLETED" : result.status === "aborted" ? "TASK_STATE_CANCELED" : "TASK_STATE_FAILED";
123
- const text = options.redactor?.redact(result.text) ?? result.text;
124
- return Object.freeze({
125
- id: taskId,
126
- contextId,
127
- status: { state, timestamp: new Date().toISOString() },
128
- artifacts: text ? [{ artifactId: `${taskId}-result`, parts: [{ text }] }] : undefined,
129
- });
130
- }
131
- function parseRpc(value) {
132
- if (!isRecord(value) || value.jsonrpc !== "2.0" || !(typeof value.id === "string" || typeof value.id === "number" || value.id === null) || typeof value.method !== "string")
133
- throw new A2AError("Invalid JSON-RPC request", 400, "ERR_PRISM_A2A_REQUEST");
134
- if (value.params !== undefined && !isRecord(value.params))
135
- throw new A2AError("Invalid JSON-RPC params", 400, "ERR_PRISM_A2A_REQUEST");
136
- return { jsonrpc: "2.0", id: value.id, method: value.method, params: value.params };
137
- }
138
- function parseMessage(value, maxBytes) {
139
- if (!isRecord(value) || (value.role !== "user" && value.role !== "ROLE_USER") || typeof value.messageId !== "string" || !value.messageId || !Array.isArray(value.parts) || value.parts.length < 1 || value.parts.length > 32)
140
- throw new A2AError("Invalid A2A message", 400, "ERR_PRISM_A2A_MESSAGE");
141
- const parts = value.parts.map((part) => {
142
- if (!isRecord(part) || typeof part.text !== "string" || Object.keys(part).some((key) => key !== "text" && key !== "metadata"))
143
- throw new A2AError("Only text A2A parts are supported", 400, "ERR_PRISM_A2A_MESSAGE");
144
- return { text: part.text, metadata: isRecord(part.metadata) ? part.metadata : undefined };
145
- });
146
- const message = { role: value.role, messageId: value.messageId, parts, contextId: typeof value.contextId === "string" ? value.contextId : undefined };
147
- if (encode(message).byteLength > maxBytes)
148
- throw new A2AError("A2A message exceeds max bytes", 413, "ERR_PRISM_A2A_MESSAGE_LIMIT");
149
- return message;
150
- }
151
- function resolveA2ALimits(input = {}) {
152
- const output = {};
153
- for (const key of Object.keys(DEFAULTS)) {
154
- const value = input[key] ?? DEFAULTS[key];
155
- if (!Number.isSafeInteger(value) || value < 1 || value > HARD[key])
156
- throw new A2AError(`${key} is invalid`, 400, "ERR_PRISM_A2A_CONFIG");
157
- output[key] = value;
158
- }
159
- return output;
160
- }
161
- async function readJson(request, maxBytes, signal) {
162
- if (!request.body)
163
- throw new A2AError("Request body is required", 400, "ERR_PRISM_A2A_REQUEST");
164
- const reader = request.body.getReader();
165
- const chunks = [];
166
- let size = 0;
167
- try {
168
- while (true) {
169
- signal.throwIfAborted();
170
- const next = await reader.read();
171
- if (next.done)
172
- break;
173
- size += next.value.byteLength;
174
- if (size > maxBytes)
175
- throw new A2AError("Request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
176
- chunks.push(next.value);
176
+ if (!optionalCursor(next.value.eventId, limits) || next.value.eventId === previous)
177
+ throw new A2AError("Duplicate/invalid A2A event id", 500, "ERR_PRISM_A2A_STREAM_LIMIT");
178
+ await validateTaskEvent(next.value, limits, options);
179
+ previous = next.value.eventId;
180
+ const payload = options.redactor?.redact({ jsonrpc: "2.0", id, result: next.value }) ?? { jsonrpc: "2.0", id, result: next.value };
181
+ const chunk = new TextEncoder().encode(`id: ${next.value.eventId}\ndata: ${JSON.stringify(payload)}\n\n`);
182
+ events++;
183
+ bytes += chunk.byteLength;
184
+ if (events > Math.min(limits.maxStreamEvents, limits.maxReplayEvents) || chunk.byteLength > limits.maxEventBytes || bytes > limits.maxStreamBytes)
185
+ throw new A2AError("A2A stream limit exceeded", 507, "ERR_PRISM_A2A_STREAM_LIMIT");
186
+ controller.enqueue(chunk);
177
187
  }
188
+ catch (error) {
189
+ finish();
190
+ controller.error(error);
191
+ } },
192
+ cancel(reason) { owned.abort(reason); finish(); },
193
+ }), { headers: SSE_HEADERS });
194
+ }
195
+ async function validateTaskEvent(event, limits, options) {
196
+ if ("task" in event) {
197
+ await validateA2ATask(event.task, limits, options.parts);
198
+ return;
178
199
  }
179
- finally {
180
- reader.releaseLock();
181
- }
182
- const bytes = new Uint8Array(size);
183
- let offset = 0;
184
- for (const chunk of chunks) {
185
- bytes.set(chunk, offset);
186
- offset += chunk.byteLength;
187
- }
188
- try {
189
- return JSON.parse(new TextDecoder().decode(bytes));
190
- }
191
- catch {
192
- throw new A2AError("Invalid JSON", 400, "ERR_PRISM_A2A_REQUEST");
200
+ if ("statusUpdate" in event) {
201
+ await validateA2ATask({ id: event.statusUpdate.taskId, contextId: event.statusUpdate.contextId, status: event.statusUpdate.status }, limits, options.parts);
202
+ return;
193
203
  }
204
+ await validateA2ATask({ id: event.artifactUpdate.taskId, contextId: event.artifactUpdate.contextId, status: { state: "TASK_STATE_WORKING", timestamp: new Date().toISOString() }, artifacts: [event.artifactUpdate.artifact] }, limits, options.parts);
194
205
  }
195
- function boundedJson(value, maxBytes, options) {
196
- const body = JSON.stringify(options.redactor?.redact(value) ?? value);
197
- if (new TextEncoder().encode(body).byteLength > maxBytes)
198
- throw new A2AError("Response exceeds max bytes", 507, "ERR_PRISM_A2A_RESPONSE_LIMIT");
199
- return new Response(body, { status: 200, headers: JSON_HEADERS });
200
- }
201
- function errorResponse(status, message, id) {
202
- const body = { jsonrpc: "2.0", id, error: { code: status === 404 ? -32601 : -32000, message } };
203
- return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
204
- }
205
- function ownedSignal(parent, timeoutMs) {
206
- const controller = new AbortController();
207
- const abort = () => controller.abort(parent.reason);
208
- if (parent.aborted)
209
- abort();
210
- else
211
- parent.addEventListener("abort", abort, { once: true });
212
- const timer = setTimeout(() => controller.abort(new DOMException("A2A request timed out", "AbortError")), timeoutMs);
213
- return { signal: controller.signal, abort: (reason) => controller.abort(reason), dispose: () => { clearTimeout(timer); parent.removeEventListener("abort", abort); } };
214
- }
215
- function abortable(promise, signal) {
216
- if (signal.aborted)
217
- return Promise.reject(signal.reason);
218
- return new Promise((resolve, reject) => {
219
- const abort = () => reject(signal.reason);
220
- signal.addEventListener("abort", abort, { once: true });
221
- promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
222
- });
206
+ async function* oneEvent(task) { yield { eventId: `terminal-${task.id}`, task }; }
207
+ async function* runEvents(taskId, contextId, run, options) { yield { eventId: "1", task: { id: taskId, contextId, status: { state: "TASK_STATE_WORKING", timestamp: new Date().toISOString() } } }; yield { eventId: "2", task: toTask(taskId, contextId, await run(), options) }; }
208
+ function toTask(taskId, contextId, result, options) { const state = result.status === "succeeded" ? "TASK_STATE_COMPLETED" : result.status === "aborted" ? "TASK_STATE_CANCELED" : "TASK_STATE_FAILED"; const text = options.redactor?.redact(result.text) ?? result.text; return { id: taskId, contextId, status: { state, timestamp: new Date().toISOString() }, artifacts: text ? [{ artifactId: `${taskId}-result`, parts: [{ text }] }] : undefined }; }
209
+ function parseRpc(value) { if (!record(value) || value.jsonrpc !== "2.0" || !(typeof value.id === "string" || typeof value.id === "number" || value.id === null) || typeof value.method !== "string" || (value.params !== undefined && !record(value.params)))
210
+ throw new A2AError("Invalid JSON-RPC request", 400, "ERR_PRISM_A2A_REQUEST"); return { jsonrpc: "2.0", id: value.id, method: value.method, params: value.params }; }
211
+ async function readJson(request, maxBytes, signal) { if (!request.body)
212
+ throw new A2AError("Request body is required", 400, "ERR_PRISM_A2A_REQUEST"); const reader = request.body.getReader(), chunks = []; let size = 0; try {
213
+ while (true) {
214
+ signal.throwIfAborted();
215
+ const n = await reader.read();
216
+ if (n.done)
217
+ break;
218
+ size += n.value.byteLength;
219
+ if (size > maxBytes)
220
+ throw new A2AError("Request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
221
+ chunks.push(n.value);
222
+ }
223
223
  }
224
- function encode(value) { return new TextEncoder().encode(JSON.stringify(value)); }
225
- function isRecord(value) { return !!value && typeof value === "object" && !Array.isArray(value); }
226
- function safeError(error, options) {
227
- const message = (error instanceof Error ? error.message : "A2A request failed").slice(0, 1024);
228
- return options.redactor?.redact(message) ?? message;
224
+ finally {
225
+ reader.releaseLock();
226
+ } const bytes = new Uint8Array(size); let offset = 0; for (const chunk of chunks) {
227
+ bytes.set(chunk, offset);
228
+ offset += chunk.byteLength;
229
+ } try {
230
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
229
231
  }
232
+ catch {
233
+ throw new A2AError("Invalid JSON", 400, "ERR_PRISM_A2A_REQUEST");
234
+ } }
235
+ function json(id, result, limits, options) { const body = JSON.stringify(options.redactor?.redact({ jsonrpc: "2.0", id, result }) ?? { jsonrpc: "2.0", id, result }); if (Buffer.byteLength(body) > limits.maxResponseBytes)
236
+ throw new A2AError("Response exceeds max bytes", 507, "ERR_PRISM_A2A_RESPONSE_LIMIT"); return new Response(body, { headers: JSON_HEADERS }); }
237
+ function rpcError(id, code, message) { return new Response(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }), { headers: JSON_HEADERS }); }
238
+ function errorResponse(status, message, id) { const body = { jsonrpc: "2.0", id, error: { code: status === 404 ? -32001 : -32000, message } }; return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS }); }
239
+ function integer(value, fallback, max) { if (value === undefined)
240
+ return fallback; if (!Number.isSafeInteger(value) || Number(value) < 0 || Number(value) > max)
241
+ throw new A2AError("Invalid A2A integer limit", 400, "ERR_PRISM_A2A_REQUEST"); return Number(value); }
242
+ function terminal(state) { return finalTerminal(state) || state === "TASK_STATE_INPUT_REQUIRED" || state === "TASK_STATE_AUTH_REQUIRED"; }
243
+ function finalTerminal(state) { return ["TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_REJECTED"].includes(state); }
244
+ function ownedSignal(parent, timeoutMs) { const controller = new AbortController(); const abort = () => controller.abort(parent.reason); if (parent.aborted)
245
+ abort();
246
+ else
247
+ parent.addEventListener("abort", abort, { once: true }); const timer = setTimeout(() => controller.abort(new DOMException("A2A request timed out", "AbortError")), timeoutMs); return { signal: controller.signal, abort: (reason) => controller.abort(reason), dispose: () => { clearTimeout(timer); parent.removeEventListener("abort", abort); } }; }
248
+ function abortable(promise, signal) { if (signal.aborted)
249
+ return Promise.reject(signal.reason); return new Promise((resolve, reject) => { const abort = () => reject(signal.reason); signal.addEventListener("abort", abort, { once: true }); promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)); }); }
250
+ function safeError(error, options) { const message = (error instanceof Error ? error.message : "A2A request failed").slice(0, 1024); return options.redactor?.redact(message) ?? message; }
230
251
  //# sourceMappingURL=a2a-server.js.map
@@ -36,19 +36,51 @@ export interface A2AAgentCard {
36
36
  readonly security?: readonly Readonly<Record<string, readonly string[]>>[];
37
37
  readonly signatures?: readonly A2AAgentCardSignature[];
38
38
  }
39
- export interface A2ATextPart {
40
- readonly text: string;
39
+ interface A2APartBase {
40
+ readonly mediaType?: string;
41
+ readonly filename?: string;
41
42
  readonly metadata?: Readonly<Record<string, unknown>>;
42
43
  }
44
+ export type A2APart = (A2APartBase & {
45
+ readonly text: string;
46
+ readonly raw?: never;
47
+ readonly url?: never;
48
+ readonly data?: never;
49
+ }) | (A2APartBase & {
50
+ readonly raw: string;
51
+ readonly text?: never;
52
+ readonly url?: never;
53
+ readonly data?: never;
54
+ }) | (A2APartBase & {
55
+ readonly url: string;
56
+ readonly text?: never;
57
+ readonly raw?: never;
58
+ readonly data?: never;
59
+ }) | (A2APartBase & {
60
+ readonly data: unknown;
61
+ readonly text?: never;
62
+ readonly raw?: never;
63
+ readonly url?: never;
64
+ });
65
+ export type A2ATextPart = Extract<A2APart, {
66
+ readonly text: string;
67
+ }>;
43
68
  export interface A2AMessage {
44
69
  readonly role: "user" | "agent" | "ROLE_USER" | "ROLE_AGENT";
45
- readonly parts: readonly A2ATextPart[];
70
+ readonly parts: readonly A2APart[];
46
71
  readonly messageId: string;
47
72
  readonly contextId?: string;
48
73
  readonly taskId?: string;
49
74
  readonly metadata?: Readonly<Record<string, unknown>>;
50
75
  }
51
- export type A2ATaskState = "TASK_STATE_SUBMITTED" | "TASK_STATE_WORKING" | "TASK_STATE_COMPLETED" | "TASK_STATE_FAILED" | "TASK_STATE_CANCELED";
76
+ export interface A2AArtifact {
77
+ readonly artifactId: string;
78
+ readonly parts: readonly A2APart[];
79
+ readonly name?: string;
80
+ readonly description?: string;
81
+ readonly metadata?: Readonly<Record<string, unknown>>;
82
+ }
83
+ export type A2ATaskState = "TASK_STATE_SUBMITTED" | "TASK_STATE_WORKING" | "TASK_STATE_COMPLETED" | "TASK_STATE_FAILED" | "TASK_STATE_CANCELED" | "TASK_STATE_INPUT_REQUIRED" | "TASK_STATE_REJECTED" | "TASK_STATE_AUTH_REQUIRED";
52
84
  export interface A2ATask {
53
85
  readonly id: string;
54
86
  readonly contextId: string;
@@ -57,16 +89,35 @@ export interface A2ATask {
57
89
  readonly timestamp: string;
58
90
  readonly message?: A2AMessage;
59
91
  };
60
- readonly artifacts?: readonly {
61
- readonly artifactId: string;
62
- readonly parts: readonly A2ATextPart[];
63
- }[];
92
+ readonly artifacts?: readonly A2AArtifact[];
93
+ readonly history?: readonly A2AMessage[];
94
+ readonly metadata?: Readonly<Record<string, unknown>>;
64
95
  }
96
+ export type A2ATaskEvent = {
97
+ readonly eventId: string;
98
+ readonly task: A2ATask;
99
+ } | {
100
+ readonly eventId: string;
101
+ readonly statusUpdate: {
102
+ readonly taskId: string;
103
+ readonly contextId: string;
104
+ readonly status: A2ATask["status"];
105
+ };
106
+ } | {
107
+ readonly eventId: string;
108
+ readonly artifactUpdate: {
109
+ readonly taskId: string;
110
+ readonly contextId: string;
111
+ readonly artifact: A2AArtifact;
112
+ readonly append?: boolean;
113
+ readonly lastChunk?: boolean;
114
+ };
115
+ };
65
116
  export type A2ARequestId = string | number | null;
66
117
  export interface A2AJsonRpcRequest {
67
118
  readonly jsonrpc: "2.0";
68
119
  readonly id: A2ARequestId;
69
- readonly method: "SendMessage" | "SendStreamingMessage" | "GetExtendedAgentCard" | string;
120
+ readonly method: string;
70
121
  readonly params?: Readonly<Record<string, unknown>>;
71
122
  }
72
123
  export interface A2AJsonRpcResponse {
@@ -91,6 +142,88 @@ export type A2AAuthorizer = (input: {
91
142
  export interface A2AAgentExposure {
92
143
  readonly sessionFactory: (authorization: A2AAuthorization) => AgentSession | Promise<AgentSession>;
93
144
  }
145
+ export interface A2ATaskPage {
146
+ readonly tasks: readonly A2ATask[];
147
+ readonly nextPageToken?: string;
148
+ readonly totalSize?: number;
149
+ }
150
+ export interface A2ATaskLifecycle {
151
+ start(input: {
152
+ readonly message: A2AMessage;
153
+ readonly authorization: A2AAuthorization;
154
+ readonly signal: AbortSignal;
155
+ readonly returnImmediately?: boolean;
156
+ }): Promise<A2ATask>;
157
+ get(input: {
158
+ readonly id: string;
159
+ readonly historyLength: number;
160
+ readonly authorization: A2AAuthorization;
161
+ readonly signal: AbortSignal;
162
+ }): Promise<A2ATask | undefined>;
163
+ list(input: {
164
+ readonly pageSize: number;
165
+ readonly pageToken?: string;
166
+ readonly contextId?: string;
167
+ readonly authorization: A2AAuthorization;
168
+ readonly signal: AbortSignal;
169
+ }): Promise<A2ATaskPage>;
170
+ cancel(input: {
171
+ readonly id: string;
172
+ readonly authorization: A2AAuthorization;
173
+ readonly signal: AbortSignal;
174
+ }): Promise<A2ATask | undefined>;
175
+ subscribe(input: {
176
+ readonly id: string;
177
+ readonly afterEventId?: string;
178
+ readonly authorization: A2AAuthorization;
179
+ readonly signal: AbortSignal;
180
+ }): AsyncIterable<A2ATaskEvent>;
181
+ }
182
+ export interface A2APushConfig {
183
+ readonly id: string;
184
+ readonly taskId: string;
185
+ readonly url: string;
186
+ readonly token?: string;
187
+ readonly authentication?: {
188
+ readonly scheme: string;
189
+ readonly credentials?: string;
190
+ };
191
+ }
192
+ export interface A2APushProvider {
193
+ create(input: {
194
+ readonly config: A2APushConfig;
195
+ readonly authorization: A2AAuthorization;
196
+ readonly signal: AbortSignal;
197
+ }): Promise<A2APushConfig>;
198
+ get(input: {
199
+ readonly taskId: string;
200
+ readonly id: string;
201
+ readonly authorization: A2AAuthorization;
202
+ readonly signal: AbortSignal;
203
+ }): Promise<A2APushConfig | undefined>;
204
+ list(input: {
205
+ readonly taskId: string;
206
+ readonly pageSize: number;
207
+ readonly pageToken?: string;
208
+ readonly authorization: A2AAuthorization;
209
+ readonly signal: AbortSignal;
210
+ }): Promise<{
211
+ readonly configs: readonly A2APushConfig[];
212
+ readonly nextPageToken?: string;
213
+ }>;
214
+ delete(input: {
215
+ readonly taskId: string;
216
+ readonly id: string;
217
+ readonly authorization: A2AAuthorization;
218
+ readonly signal: AbortSignal;
219
+ }): Promise<boolean>;
220
+ }
221
+ export interface A2APartPolicy {
222
+ readonly allowRaw?: boolean;
223
+ readonly allowUrl?: boolean;
224
+ readonly allowData?: boolean;
225
+ readonly validateUrl?: (url: URL) => void | Promise<void>;
226
+ }
94
227
  export interface A2ALimits {
95
228
  readonly maxRequestBytes?: number;
96
229
  readonly maxResponseBytes?: number;
@@ -100,11 +233,25 @@ export interface A2ALimits {
100
233
  readonly maxConcurrentRequests?: number;
101
234
  readonly timeoutMs?: number;
102
235
  readonly maxCardBytes?: number;
236
+ readonly maxIdBytes?: number;
237
+ readonly maxParts?: number;
238
+ readonly maxPartBytes?: number;
239
+ readonly maxRawBytes?: number;
240
+ readonly maxDataBytes?: number;
241
+ readonly maxArtifacts?: number;
242
+ readonly maxHistory?: number;
243
+ readonly maxPageSize?: number;
244
+ readonly maxCursorBytes?: number;
245
+ readonly maxReplayEvents?: number;
246
+ readonly maxPushConfigs?: number;
103
247
  }
104
248
  export interface CreateA2AHandlerOptions {
105
249
  readonly card: A2AAgentCard;
106
250
  readonly exposure: A2AAgentExposure;
107
251
  readonly authorize: A2AAuthorizer;
252
+ readonly tasks?: A2ATaskLifecycle;
253
+ readonly push?: A2APushProvider;
254
+ readonly parts?: A2APartPolicy;
108
255
  readonly endpointPath?: string;
109
256
  readonly redactor?: SecretRedactor;
110
257
  readonly limits?: A2ALimits;
@@ -121,6 +268,7 @@ export interface A2AClientOptions {
121
268
  readonly cardUrl?: string;
122
269
  readonly limits?: A2ALimits;
123
270
  readonly redactor?: SecretRedactor;
271
+ readonly parts?: A2APartPolicy;
124
272
  }
125
273
  export interface A2AClient {
126
274
  getCard(options?: {
@@ -129,7 +277,46 @@ export interface A2AClient {
129
277
  send(input: string, options?: {
130
278
  readonly signal?: AbortSignal;
131
279
  }): Promise<AgentRunResult>;
280
+ sendMessage(message: A2AMessage, options?: {
281
+ readonly signal?: AbortSignal;
282
+ readonly returnImmediately?: boolean;
283
+ }): Promise<A2ATask>;
132
284
  stream(input: string, options?: {
133
285
  readonly signal?: AbortSignal;
134
286
  }): AsyncIterable<string>;
287
+ getTask(id: string, options?: {
288
+ readonly signal?: AbortSignal;
289
+ readonly historyLength?: number;
290
+ }): Promise<A2ATask>;
291
+ listTasks(options?: {
292
+ readonly signal?: AbortSignal;
293
+ readonly pageSize?: number;
294
+ readonly pageToken?: string;
295
+ readonly contextId?: string;
296
+ }): Promise<A2ATaskPage>;
297
+ cancelTask(id: string, options?: {
298
+ readonly signal?: AbortSignal;
299
+ }): Promise<A2ATask>;
300
+ subscribeToTask(id: string, options?: {
301
+ readonly signal?: AbortSignal;
302
+ readonly afterEventId?: string;
303
+ }): AsyncIterable<A2ATaskEvent>;
304
+ createPushConfig(config: A2APushConfig, options?: {
305
+ readonly signal?: AbortSignal;
306
+ }): Promise<A2APushConfig>;
307
+ getPushConfig(taskId: string, id: string, options?: {
308
+ readonly signal?: AbortSignal;
309
+ }): Promise<A2APushConfig>;
310
+ listPushConfigs(taskId: string, options?: {
311
+ readonly signal?: AbortSignal;
312
+ readonly pageSize?: number;
313
+ readonly pageToken?: string;
314
+ }): Promise<{
315
+ readonly configs: readonly A2APushConfig[];
316
+ readonly nextPageToken?: string;
317
+ }>;
318
+ deletePushConfig(taskId: string, id: string, options?: {
319
+ readonly signal?: AbortSignal;
320
+ }): Promise<void>;
135
321
  }
322
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./a2a-card.js";
2
2
  export * from "./a2a-client.js";
3
+ export * from "./a2a-parts.js";
4
+ export * from "./a2a-push.js";
3
5
  export * from "./a2a-server.js";
4
6
  export type * from "./a2a-types.js";
5
7
  export * from "./errors.js";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./a2a-card.js";
2
2
  export * from "./a2a-client.js";
3
+ export * from "./a2a-parts.js";
4
+ export * from "./a2a-push.js";
3
5
  export * from "./a2a-server.js";
4
6
  export * from "./errors.js";
5
7
  export * from "./limits.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arnilo/prism-supervisor",
3
- "version": "0.0.7",
4
- "description": "Optional bounded local supervisor delegation and A2A 1.0 interoperability.",
3
+ "version": "0.0.8",
4
+ "description": "Bounded supervisor delegation and A2A 1.0 durable task, rich-part, reconnect, and push interoperability.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
25
25
  "pack:dry-run": "npm pack --dry-run"
26
26
  },
27
27
  "peerDependencies": {
28
- "@arnilo/prism": "0.0.7"
28
+ "@arnilo/prism": "0.0.8"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@arnilo/prism": "file:../.."