@arnilo/prism-supervisor 0.0.96 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +117 -4
- package/README.md +2 -2
- package/dist/a2a-card.js +19 -7
- package/dist/a2a-client.js +345 -65
- package/dist/a2a-event-source.d.ts +42 -0
- package/dist/a2a-event-source.js +40 -0
- package/dist/a2a-parts.js +109 -19
- package/dist/a2a-push.js +10 -2
- package/dist/a2a-server.js +239 -88
- package/dist/a2a-types.d.ts +12 -1
- package/dist/errors.js +12 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/limits.js +4 -1
- package/dist/supervisor.js +180 -26
- package/dist/types.d.ts +23 -1
- package/package.json +2 -2
package/dist/a2a-client.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createA2AAgentCard } from "./a2a-card.js";
|
|
2
2
|
import { resolveA2ALimits } from "./a2a-parts.js";
|
|
3
|
+
import { A2A_PROTOCOL_VERSION, } from "./a2a-types.js";
|
|
3
4
|
import { A2AError } from "./errors.js";
|
|
4
|
-
import { A2A_PROTOCOL_VERSION } from "./a2a-types.js";
|
|
5
5
|
export function createA2AClient(options) {
|
|
6
6
|
const endpoint = requireAllowedHttpsUrl(options.endpoint, options.allowedOrigins);
|
|
7
7
|
const cardUrl = requireAllowedHttpsUrl(options.cardUrl ?? `${endpoint.origin}/.well-known/agent-card.json`, options.allowedOrigins);
|
|
@@ -24,7 +24,12 @@ export function createA2AClient(options) {
|
|
|
24
24
|
}
|
|
25
25
|
async function getCard(call = {}) {
|
|
26
26
|
return withRequest(call.signal, async (signal) => {
|
|
27
|
-
const response = await fetcher(cardUrl, {
|
|
27
|
+
const response = await fetcher(cardUrl, {
|
|
28
|
+
method: "GET",
|
|
29
|
+
signal,
|
|
30
|
+
redirect: "error",
|
|
31
|
+
headers: { accept: "application/a2a+json, application/json" },
|
|
32
|
+
});
|
|
28
33
|
if (!response.ok)
|
|
29
34
|
throw new A2AError("A2A card request failed", response.status, "ERR_PRISM_A2A_REMOTE");
|
|
30
35
|
const value = await readBoundedJson(response, limits.maxCardBytes, signal);
|
|
@@ -45,7 +50,18 @@ export function createA2AClient(options) {
|
|
|
45
50
|
if (new TextEncoder().encode(body).byteLength > limits.maxRequestBytes)
|
|
46
51
|
throw new A2AError("A2A request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
|
|
47
52
|
const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal }) ?? {}), signal);
|
|
48
|
-
const response = await fetcher(endpoint, {
|
|
53
|
+
const response = await fetcher(endpoint, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
signal,
|
|
56
|
+
redirect: "error",
|
|
57
|
+
headers: {
|
|
58
|
+
...headersObject(authHeaders),
|
|
59
|
+
"content-type": "application/a2a+json",
|
|
60
|
+
accept: "application/a2a+json",
|
|
61
|
+
"a2a-version": "1.0",
|
|
62
|
+
},
|
|
63
|
+
body,
|
|
64
|
+
});
|
|
49
65
|
if (!response.ok)
|
|
50
66
|
throw new A2AError("A2A remote request failed", response.status, "ERR_PRISM_A2A_REMOTE");
|
|
51
67
|
const rpc = parseRpcResponse(await readBoundedJson(response, limits.maxResponseBytes, signal), id);
|
|
@@ -54,25 +70,37 @@ export function createA2AClient(options) {
|
|
|
54
70
|
return taskResult(parseTaskResult(rpc.result), options);
|
|
55
71
|
});
|
|
56
72
|
}
|
|
57
|
-
async function*
|
|
73
|
+
async function* streamMessage(message, call = {}) {
|
|
58
74
|
if (active >= limits.maxConcurrentRequests)
|
|
59
75
|
throw new A2AError("A2A client concurrency exceeded", 429, "ERR_PRISM_A2A_CONCURRENCY");
|
|
60
76
|
active += 1;
|
|
61
77
|
const owned = ownedSignal(call.signal, limits.timeoutMs);
|
|
62
78
|
let reader;
|
|
63
79
|
try {
|
|
64
|
-
|
|
80
|
+
assertMessage(message, limits.maxRequestBytes);
|
|
65
81
|
await getCardWithin(owned.signal);
|
|
66
82
|
const id = ++requestId;
|
|
67
|
-
const body = JSON.stringify(
|
|
83
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id, method: "SendStreamingMessage", params: { message } });
|
|
68
84
|
if (new TextEncoder().encode(body).byteLength > limits.maxRequestBytes)
|
|
69
85
|
throw new A2AError("A2A request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
|
|
70
86
|
const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal: owned.signal }) ?? {}), owned.signal);
|
|
71
|
-
const response = await fetcher(endpoint, {
|
|
87
|
+
const response = await fetcher(endpoint, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
signal: owned.signal,
|
|
90
|
+
redirect: "error",
|
|
91
|
+
headers: {
|
|
92
|
+
...headersObject(authHeaders),
|
|
93
|
+
"content-type": "application/a2a+json",
|
|
94
|
+
accept: "text/event-stream",
|
|
95
|
+
"a2a-version": "1.0",
|
|
96
|
+
},
|
|
97
|
+
body,
|
|
98
|
+
});
|
|
72
99
|
if (!response.ok || !response.body || !response.headers.get("content-type")?.startsWith("text/event-stream"))
|
|
73
100
|
throw new A2AError("A2A stream request failed", response.status, "ERR_PRISM_A2A_REMOTE");
|
|
74
101
|
reader = response.body.getReader();
|
|
75
102
|
let terminal = false;
|
|
103
|
+
let count = 0;
|
|
76
104
|
for await (const data of readA2AStreamData(reader, limits, owned.signal)) {
|
|
77
105
|
if (terminal)
|
|
78
106
|
throw new A2AError("A2A stream continued after terminal task state", 502, "ERR_PRISM_A2A_REMOTE");
|
|
@@ -88,17 +116,17 @@ export function createA2AClient(options) {
|
|
|
88
116
|
const rpc = parseRpcResponse(parsed, id);
|
|
89
117
|
if (rpc.error)
|
|
90
118
|
throw new A2AError(safeRemote(rpc.error.message, options), 502, "ERR_PRISM_A2A_REMOTE");
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
const event = parseA2AStreamEvent(rpc.result, `stream-${++count}`);
|
|
120
|
+
const status = eventStatus(event);
|
|
121
|
+
if (status === "TASK_STATE_FAILED" ||
|
|
122
|
+
status === "TASK_STATE_CANCELED" ||
|
|
123
|
+
status === "TASK_STATE_REJECTED" ||
|
|
124
|
+
status === "TASK_STATE_INPUT_REQUIRED" ||
|
|
125
|
+
status === "TASK_STATE_AUTH_REQUIRED")
|
|
126
|
+
terminal = true;
|
|
127
|
+
if (status === "TASK_STATE_COMPLETED")
|
|
97
128
|
terminal = true;
|
|
98
|
-
|
|
99
|
-
for (const part of artifact.parts)
|
|
100
|
-
if (typeof part.text === "string")
|
|
101
|
-
yield options.redactor?.redact(part.text) ?? part.text;
|
|
129
|
+
yield event;
|
|
102
130
|
}
|
|
103
131
|
if (!terminal)
|
|
104
132
|
throw new A2AError("A2A stream ended before terminal task state", 502, "ERR_PRISM_A2A_REMOTE");
|
|
@@ -109,8 +137,25 @@ export function createA2AClient(options) {
|
|
|
109
137
|
active -= 1;
|
|
110
138
|
}
|
|
111
139
|
}
|
|
140
|
+
async function* stream(input, call = {}) {
|
|
141
|
+
assertInput(input, limits.maxRequestBytes);
|
|
142
|
+
for await (const event of streamMessage({ role: "user", messageId: "stream-input", parts: [{ text: input }] }, call)) {
|
|
143
|
+
const status = eventStatus(event);
|
|
144
|
+
if (status === "TASK_STATE_FAILED" || status === "TASK_STATE_CANCELED" || status === "TASK_STATE_REJECTED")
|
|
145
|
+
throw new A2AError("Remote A2A stream task failed", 502, "ERR_PRISM_A2A_REMOTE");
|
|
146
|
+
if (status === "TASK_STATE_INPUT_REQUIRED" || status === "TASK_STATE_AUTH_REQUIRED")
|
|
147
|
+
throw new A2AError(`Remote A2A task interrupted: ${status}`, 409, "ERR_PRISM_A2A_INTERRUPTED");
|
|
148
|
+
for (const text of streamEventText(event))
|
|
149
|
+
yield options.redactor?.redact(text) ?? text;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
112
152
|
async function getCardWithin(signal) {
|
|
113
|
-
const response = await fetcher(cardUrl, {
|
|
153
|
+
const response = await fetcher(cardUrl, {
|
|
154
|
+
method: "GET",
|
|
155
|
+
signal,
|
|
156
|
+
redirect: "error",
|
|
157
|
+
headers: { accept: "application/a2a+json, application/json" },
|
|
158
|
+
});
|
|
114
159
|
if (!response.ok)
|
|
115
160
|
throw new A2AError("A2A card request failed", response.status, "ERR_PRISM_A2A_REMOTE");
|
|
116
161
|
const card = parseCard(await readBoundedJson(response, limits.maxCardBytes, signal));
|
|
@@ -128,7 +173,18 @@ export function createA2AClient(options) {
|
|
|
128
173
|
if (Buffer.byteLength(body) > limits.maxRequestBytes)
|
|
129
174
|
throw new A2AError("A2A request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
|
|
130
175
|
const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal: owned }) ?? {}), owned);
|
|
131
|
-
const response = await fetcher(endpoint, {
|
|
176
|
+
const response = await fetcher(endpoint, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
signal: owned,
|
|
179
|
+
redirect: "error",
|
|
180
|
+
headers: {
|
|
181
|
+
...headersObject(authHeaders),
|
|
182
|
+
"content-type": "application/a2a+json",
|
|
183
|
+
accept: "application/a2a+json",
|
|
184
|
+
"a2a-version": "1.0",
|
|
185
|
+
},
|
|
186
|
+
body,
|
|
187
|
+
});
|
|
132
188
|
if (!response.ok)
|
|
133
189
|
throw new A2AError("A2A remote request failed", response.status, "ERR_PRISM_A2A_REMOTE");
|
|
134
190
|
const rpc = parseRpcResponse(await readBoundedJson(response, limits.maxResponseBytes, owned), id);
|
|
@@ -140,14 +196,22 @@ export function createA2AClient(options) {
|
|
|
140
196
|
async function sendMessage(message, call = {}) {
|
|
141
197
|
return parseTaskResult(await invoke("SendMessage", { message, configuration: { returnImmediately: call.returnImmediately ?? false } }, call.signal));
|
|
142
198
|
}
|
|
143
|
-
async function getTask(id, call = {}) {
|
|
199
|
+
async function getTask(id, call = {}) {
|
|
200
|
+
return parseTaskResult(await invoke("GetTask", { id, historyLength: call.historyLength ?? 0 }, call.signal));
|
|
201
|
+
}
|
|
144
202
|
async function listTasks(call = {}) {
|
|
145
203
|
const value = await invoke("ListTasks", { pageSize: call.pageSize ?? 50, pageToken: call.pageToken, contextId: call.contextId }, call.signal);
|
|
146
204
|
if (!isRecord(value) || !Array.isArray(value.tasks) || value.tasks.length > limits.maxPageSize)
|
|
147
205
|
throw new A2AError("Malformed A2A task page", 502, "ERR_PRISM_A2A_REMOTE");
|
|
148
|
-
return {
|
|
206
|
+
return {
|
|
207
|
+
tasks: value.tasks.map((task) => parseTaskResult(task)),
|
|
208
|
+
nextPageToken: typeof value.nextPageToken === "string" ? value.nextPageToken : undefined,
|
|
209
|
+
totalSize: typeof value.totalSize === "number" ? value.totalSize : undefined,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async function cancelTask(id, call = {}) {
|
|
213
|
+
return parseTaskResult(await invoke("CancelTask", { id }, call.signal));
|
|
149
214
|
}
|
|
150
|
-
async function cancelTask(id, call = {}) { return parseTaskResult(await invoke("CancelTask", { id }, call.signal)); }
|
|
151
215
|
async function* subscribeToTask(id, call = {}) {
|
|
152
216
|
if (active >= limits.maxConcurrentRequests)
|
|
153
217
|
throw new A2AError("A2A client concurrency exceeded", 429, "ERR_PRISM_A2A_CONCURRENCY");
|
|
@@ -158,19 +222,31 @@ export function createA2AClient(options) {
|
|
|
158
222
|
await getCardWithin(owned.signal);
|
|
159
223
|
const request = ++requestId;
|
|
160
224
|
const authHeaders = await abortable(Promise.resolve(options.authorize?.({ endpoint: endpoint.href, signal: owned.signal }) ?? {}), owned.signal);
|
|
161
|
-
const response = await fetcher(endpoint, {
|
|
225
|
+
const response = await fetcher(endpoint, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
signal: owned.signal,
|
|
228
|
+
redirect: "error",
|
|
229
|
+
headers: {
|
|
230
|
+
...headersObject(authHeaders),
|
|
231
|
+
"content-type": "application/a2a+json",
|
|
232
|
+
accept: "text/event-stream",
|
|
233
|
+
"a2a-version": "1.0",
|
|
234
|
+
},
|
|
235
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: request, method: "SubscribeToTask", params: { id, afterEventId: call.afterEventId } }),
|
|
236
|
+
});
|
|
162
237
|
if (!response.ok || !response.body || !response.headers.get("content-type")?.startsWith("text/event-stream"))
|
|
163
238
|
throw new A2AError("A2A subscribe request failed", response.status, "ERR_PRISM_A2A_REMOTE");
|
|
164
239
|
reader = response.body.getReader();
|
|
165
|
-
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
let count = 0;
|
|
166
242
|
for await (const data of readA2AStreamData(reader, limits, owned.signal)) {
|
|
167
243
|
const rpc = parseRpcResponse(JSON.parse(data), request);
|
|
168
244
|
if (rpc.error)
|
|
169
245
|
throw remoteProtocolError(rpc.error.code, rpc.error.message, options);
|
|
170
246
|
const event = parseTaskEvent(rpc.result);
|
|
171
|
-
if (event.eventId
|
|
247
|
+
if (seen.has(event.eventId))
|
|
172
248
|
continue;
|
|
173
|
-
|
|
249
|
+
seen.add(event.eventId);
|
|
174
250
|
if (++count > limits.maxReplayEvents)
|
|
175
251
|
throw new A2AError("A2A replay exceeds event limit", 507, "ERR_PRISM_A2A_STREAM_LIMIT");
|
|
176
252
|
yield event;
|
|
@@ -182,12 +258,39 @@ export function createA2AClient(options) {
|
|
|
182
258
|
active -= 1;
|
|
183
259
|
}
|
|
184
260
|
}
|
|
185
|
-
async function createPushConfig(config, call = {}) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
261
|
+
async function createPushConfig(config, call = {}) {
|
|
262
|
+
return parsePushConfig(await invoke("CreateTaskPushNotificationConfig", { ...config }, call.signal));
|
|
263
|
+
}
|
|
264
|
+
async function getPushConfig(taskId, id, call = {}) {
|
|
265
|
+
return parsePushConfig(await invoke("GetTaskPushNotificationConfig", { taskId, id }, call.signal));
|
|
266
|
+
}
|
|
267
|
+
async function listPushConfigs(taskId, call = {}) {
|
|
268
|
+
const value = await invoke("ListTaskPushNotificationConfigs", { taskId, pageSize: call.pageSize, pageToken: call.pageToken }, call.signal);
|
|
269
|
+
if (!isRecord(value) || !Array.isArray(value.configs))
|
|
270
|
+
throw new A2AError("Malformed A2A push config page", 502, "ERR_PRISM_A2A_REMOTE");
|
|
271
|
+
return {
|
|
272
|
+
configs: value.configs.map(parsePushConfig),
|
|
273
|
+
nextPageToken: typeof value.nextPageToken === "string" ? value.nextPageToken : undefined,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
async function deletePushConfig(taskId, id, call = {}) {
|
|
277
|
+
await invoke("DeleteTaskPushNotificationConfig", { taskId, id }, call.signal);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
getCard,
|
|
281
|
+
send,
|
|
282
|
+
sendMessage,
|
|
283
|
+
stream,
|
|
284
|
+
streamMessage,
|
|
285
|
+
getTask,
|
|
286
|
+
listTasks,
|
|
287
|
+
cancelTask,
|
|
288
|
+
subscribeToTask,
|
|
289
|
+
createPushConfig,
|
|
290
|
+
getPushConfig,
|
|
291
|
+
listPushConfigs,
|
|
292
|
+
deletePushConfig,
|
|
293
|
+
};
|
|
191
294
|
}
|
|
192
295
|
async function* readA2AStreamData(reader, limits, signal) {
|
|
193
296
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
@@ -295,12 +398,21 @@ function taskResult(task, options) {
|
|
|
295
398
|
throw new A2AError("A2A response task is not terminal", 502, "ERR_PRISM_A2A_REMOTE");
|
|
296
399
|
if (task.status.state === "TASK_STATE_INPUT_REQUIRED" || task.status.state === "TASK_STATE_AUTH_REQUIRED")
|
|
297
400
|
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("");
|
|
401
|
+
const text = (task.artifacts ?? []).flatMap((artifact) => artifact.parts.flatMap((part) => ("text" in part ? [part.text] : []))).join("");
|
|
299
402
|
const safeText = options.redactor?.redact(text) ?? text;
|
|
300
403
|
const status = task.status.state === "TASK_STATE_COMPLETED" ? "succeeded" : task.status.state === "TASK_STATE_CANCELED" ? "aborted" : "failed";
|
|
301
404
|
const content = safeText ? [{ type: "text", text: safeText }] : [];
|
|
302
405
|
const message = safeText ? { role: "assistant", content } : undefined;
|
|
303
|
-
return Object.freeze({
|
|
406
|
+
return Object.freeze({
|
|
407
|
+
sessionId: task.contextId,
|
|
408
|
+
runId: task.id,
|
|
409
|
+
status,
|
|
410
|
+
text: safeText,
|
|
411
|
+
content,
|
|
412
|
+
message,
|
|
413
|
+
error: status === "failed" ? { message: "Remote A2A task failed" } : undefined,
|
|
414
|
+
abortReason: status === "aborted" ? "Remote A2A task canceled" : undefined,
|
|
415
|
+
});
|
|
304
416
|
}
|
|
305
417
|
function parseTaskResult(value) {
|
|
306
418
|
if (!isRecord(value))
|
|
@@ -308,12 +420,37 @@ function parseTaskResult(value) {
|
|
|
308
420
|
const task = isRecord(value.task) ? value.task : value;
|
|
309
421
|
if (typeof task.id !== "string" || typeof task.contextId !== "string" || !isRecord(task.status) || typeof task.status.state !== "string")
|
|
310
422
|
throw new A2AError("Malformed A2A task", 502, "ERR_PRISM_A2A_REMOTE");
|
|
311
|
-
const states = new Set([
|
|
423
|
+
const states = new Set([
|
|
424
|
+
"TASK_STATE_SUBMITTED",
|
|
425
|
+
"TASK_STATE_WORKING",
|
|
426
|
+
"TASK_STATE_COMPLETED",
|
|
427
|
+
"TASK_STATE_FAILED",
|
|
428
|
+
"TASK_STATE_CANCELED",
|
|
429
|
+
"TASK_STATE_INPUT_REQUIRED",
|
|
430
|
+
"TASK_STATE_REJECTED",
|
|
431
|
+
"TASK_STATE_AUTH_REQUIRED",
|
|
432
|
+
]);
|
|
312
433
|
if (!states.has(task.status.state))
|
|
313
434
|
throw new A2AError("Unknown A2A task state", 502, "ERR_PRISM_A2A_REMOTE");
|
|
314
435
|
const artifacts = task.artifacts === undefined ? undefined : parseArtifacts(task.artifacts);
|
|
315
|
-
const history = task.history === undefined
|
|
316
|
-
|
|
436
|
+
const history = task.history === undefined
|
|
437
|
+
? undefined
|
|
438
|
+
: Array.isArray(task.history)
|
|
439
|
+
? task.history.map(parseRemoteMessage)
|
|
440
|
+
: (() => {
|
|
441
|
+
throw new A2AError("Malformed A2A task history", 502, "ERR_PRISM_A2A_REMOTE");
|
|
442
|
+
})();
|
|
443
|
+
return {
|
|
444
|
+
id: task.id,
|
|
445
|
+
contextId: task.contextId,
|
|
446
|
+
status: {
|
|
447
|
+
state: task.status.state,
|
|
448
|
+
timestamp: typeof task.status.timestamp === "string" ? task.status.timestamp : new Date(0).toISOString(),
|
|
449
|
+
...(task.status.message === undefined ? {} : { message: parseRemoteMessage(task.status.message) }),
|
|
450
|
+
},
|
|
451
|
+
artifacts,
|
|
452
|
+
history,
|
|
453
|
+
};
|
|
317
454
|
}
|
|
318
455
|
function parseArtifacts(value) {
|
|
319
456
|
if (!Array.isArray(value) || value.length > 32)
|
|
@@ -330,10 +467,15 @@ function parseRemotePart(value) {
|
|
|
330
467
|
const keys = ["text", "raw", "url", "data"].filter((key) => Object.hasOwn(value, key));
|
|
331
468
|
if (keys.length !== 1)
|
|
332
469
|
throw new A2AError("Malformed A2A part union", 502, "ERR_PRISM_A2A_REMOTE");
|
|
333
|
-
const base = {
|
|
470
|
+
const base = {
|
|
471
|
+
mediaType: typeof value.mediaType === "string" ? value.mediaType : undefined,
|
|
472
|
+
filename: typeof value.filename === "string" ? value.filename : undefined,
|
|
473
|
+
};
|
|
334
474
|
if (keys[0] === "text" && typeof value.text === "string")
|
|
335
475
|
return { ...base, text: value.text };
|
|
336
|
-
if (keys[0] === "raw" &&
|
|
476
|
+
if (keys[0] === "raw" &&
|
|
477
|
+
typeof value.raw === "string" &&
|
|
478
|
+
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value.raw))
|
|
337
479
|
return { ...base, raw: value.raw };
|
|
338
480
|
if (keys[0] === "url" && typeof value.url === "string") {
|
|
339
481
|
const url = new URL(value.url);
|
|
@@ -345,25 +487,99 @@ function parseRemotePart(value) {
|
|
|
345
487
|
return { ...base, data: structuredClone(value.data) };
|
|
346
488
|
throw new A2AError("Malformed A2A part", 502, "ERR_PRISM_A2A_REMOTE");
|
|
347
489
|
}
|
|
348
|
-
function parseRemoteMessage(value) {
|
|
349
|
-
|
|
490
|
+
function parseRemoteMessage(value) {
|
|
491
|
+
if (!isRecord(value) ||
|
|
492
|
+
typeof value.messageId !== "string" ||
|
|
493
|
+
!Array.isArray(value.parts) ||
|
|
494
|
+
(value.role !== "ROLE_USER" && value.role !== "ROLE_AGENT" && value.role !== "user" && value.role !== "agent"))
|
|
495
|
+
throw new A2AError("Malformed A2A message", 502, "ERR_PRISM_A2A_REMOTE");
|
|
496
|
+
return {
|
|
497
|
+
role: value.role,
|
|
498
|
+
messageId: value.messageId,
|
|
499
|
+
parts: value.parts.map(parseRemotePart),
|
|
500
|
+
contextId: typeof value.contextId === "string" ? value.contextId : undefined,
|
|
501
|
+
taskId: typeof value.taskId === "string" ? value.taskId : undefined,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function parseA2AStreamEvent(value, fallbackEventId) {
|
|
505
|
+
if (isRecord(value) && isRecord(value.message))
|
|
506
|
+
return { eventId: fallbackEventId, message: parseRemoteMessage(value.message) };
|
|
507
|
+
if (isRecord(value) && !Object.hasOwn(value, "eventId") && (isRecord(value.task) || typeof value.id === "string")) {
|
|
508
|
+
return { eventId: fallbackEventId, task: parseTaskResult(value) };
|
|
509
|
+
}
|
|
510
|
+
return parseTaskEvent(value);
|
|
511
|
+
}
|
|
512
|
+
function eventStatus(event) {
|
|
513
|
+
if ("task" in event)
|
|
514
|
+
return event.task.status.state;
|
|
515
|
+
if ("statusUpdate" in event)
|
|
516
|
+
return event.statusUpdate.status.state;
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
function streamEventText(event) {
|
|
520
|
+
const parts = "message" in event
|
|
521
|
+
? event.message.parts
|
|
522
|
+
: "task" in event
|
|
523
|
+
? (event.task.artifacts?.flatMap((artifact) => artifact.parts) ?? [])
|
|
524
|
+
: "artifactUpdate" in event
|
|
525
|
+
? event.artifactUpdate.artifact.parts
|
|
526
|
+
: [];
|
|
527
|
+
return parts.flatMap((part) => (typeof part.text === "string" ? [part.text] : []));
|
|
528
|
+
}
|
|
350
529
|
function parseTaskEvent(value) {
|
|
351
530
|
if (!isRecord(value) || typeof value.eventId !== "string" || !value.eventId)
|
|
352
531
|
throw new A2AError("Malformed A2A task event", 502, "ERR_PRISM_A2A_REMOTE");
|
|
353
532
|
if (isRecord(value.task))
|
|
354
533
|
return { eventId: value.eventId, task: parseTaskResult(value.task) };
|
|
355
|
-
if (isRecord(value.statusUpdate) &&
|
|
356
|
-
|
|
534
|
+
if (isRecord(value.statusUpdate) &&
|
|
535
|
+
typeof value.statusUpdate.taskId === "string" &&
|
|
536
|
+
typeof value.statusUpdate.contextId === "string" &&
|
|
537
|
+
isRecord(value.statusUpdate.status)) {
|
|
538
|
+
const parsed = parseTaskResult({
|
|
539
|
+
id: value.statusUpdate.taskId,
|
|
540
|
+
contextId: value.statusUpdate.contextId,
|
|
541
|
+
status: value.statusUpdate.status,
|
|
542
|
+
});
|
|
357
543
|
return { eventId: value.eventId, statusUpdate: { taskId: parsed.id, contextId: parsed.contextId, status: parsed.status } };
|
|
358
544
|
}
|
|
359
|
-
if (isRecord(value.artifactUpdate) &&
|
|
360
|
-
|
|
545
|
+
if (isRecord(value.artifactUpdate) &&
|
|
546
|
+
typeof value.artifactUpdate.taskId === "string" &&
|
|
547
|
+
typeof value.artifactUpdate.contextId === "string" &&
|
|
548
|
+
isRecord(value.artifactUpdate.artifact))
|
|
549
|
+
return {
|
|
550
|
+
eventId: value.eventId,
|
|
551
|
+
artifactUpdate: {
|
|
552
|
+
taskId: value.artifactUpdate.taskId,
|
|
553
|
+
contextId: value.artifactUpdate.contextId,
|
|
554
|
+
artifact: parseArtifacts([value.artifactUpdate.artifact])[0],
|
|
555
|
+
append: value.artifactUpdate.append === true,
|
|
556
|
+
lastChunk: value.artifactUpdate.lastChunk === true,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
361
559
|
throw new A2AError("Malformed A2A task event", 502, "ERR_PRISM_A2A_REMOTE");
|
|
362
560
|
}
|
|
363
|
-
function parsePushConfig(value) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
561
|
+
function parsePushConfig(value) {
|
|
562
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.taskId !== "string" || typeof value.url !== "string")
|
|
563
|
+
throw new A2AError("Malformed A2A push config", 502, "ERR_PRISM_A2A_REMOTE");
|
|
564
|
+
const url = new URL(value.url);
|
|
565
|
+
if (url.protocol !== "https:" || url.username || url.password || url.hash)
|
|
566
|
+
throw new A2AError("Unsafe A2A push URL", 502, "ERR_PRISM_A2A_REMOTE");
|
|
567
|
+
return {
|
|
568
|
+
id: value.id,
|
|
569
|
+
taskId: value.taskId,
|
|
570
|
+
url: url.href,
|
|
571
|
+
token: typeof value.token === "string" ? value.token : undefined,
|
|
572
|
+
authentication: isRecord(value.authentication) && typeof value.authentication.scheme === "string"
|
|
573
|
+
? {
|
|
574
|
+
scheme: value.authentication.scheme,
|
|
575
|
+
credentials: typeof value.authentication.credentials === "string" ? value.authentication.credentials : undefined,
|
|
576
|
+
}
|
|
577
|
+
: undefined,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function remoteProtocolError(code, message, options) {
|
|
581
|
+
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");
|
|
582
|
+
}
|
|
367
583
|
function parseRpcResponse(value, id) {
|
|
368
584
|
if (!isRecord(value) || value.jsonrpc !== "2.0" || value.id !== id)
|
|
369
585
|
throw new A2AError("Malformed A2A JSON-RPC response", 502, "ERR_PRISM_A2A_REMOTE");
|
|
@@ -373,7 +589,16 @@ function parseRpcResponse(value, id) {
|
|
|
373
589
|
return { jsonrpc: "2.0", id, result: value.result, error: error };
|
|
374
590
|
}
|
|
375
591
|
function parseCard(value) {
|
|
376
|
-
if (!isRecord(value) ||
|
|
592
|
+
if (!isRecord(value) ||
|
|
593
|
+
typeof value.name !== "string" ||
|
|
594
|
+
typeof value.description !== "string" ||
|
|
595
|
+
typeof value.version !== "string" ||
|
|
596
|
+
!Array.isArray(value.supportedInterfaces) ||
|
|
597
|
+
!Array.isArray(value.skills) ||
|
|
598
|
+
!stringArray(value.defaultInputModes) ||
|
|
599
|
+
!stringArray(value.defaultOutputModes) ||
|
|
600
|
+
!isRecord(value.capabilities) ||
|
|
601
|
+
typeof value.capabilities.streaming !== "boolean")
|
|
377
602
|
throw new A2AError("Malformed A2A agent card", 502, "ERR_PRISM_A2A_CARD");
|
|
378
603
|
const supportedInterfaces = value.supportedInterfaces.map((item) => {
|
|
379
604
|
if (!isRecord(item) || typeof item.url !== "string" || item.protocolBinding !== "JSONRPC" || item.protocolVersion !== "1.0")
|
|
@@ -381,21 +606,47 @@ function parseCard(value) {
|
|
|
381
606
|
return { url: item.url, protocolBinding: "JSONRPC", protocolVersion: "1.0" };
|
|
382
607
|
});
|
|
383
608
|
const skills = value.skills.map((skill) => {
|
|
384
|
-
if (!isRecord(skill) ||
|
|
609
|
+
if (!isRecord(skill) ||
|
|
610
|
+
typeof skill.id !== "string" ||
|
|
611
|
+
typeof skill.name !== "string" ||
|
|
612
|
+
typeof skill.description !== "string" ||
|
|
613
|
+
!stringArray(skill.tags))
|
|
385
614
|
throw new A2AError("Malformed A2A agent skill", 502, "ERR_PRISM_A2A_CARD");
|
|
386
|
-
return {
|
|
615
|
+
return {
|
|
616
|
+
id: skill.id,
|
|
617
|
+
name: skill.name,
|
|
618
|
+
description: skill.description,
|
|
619
|
+
tags: skill.tags,
|
|
620
|
+
examples: stringArray(skill.examples) ? skill.examples : undefined,
|
|
621
|
+
inputModes: stringArray(skill.inputModes) ? skill.inputModes : undefined,
|
|
622
|
+
outputModes: stringArray(skill.outputModes) ? skill.outputModes : undefined,
|
|
623
|
+
};
|
|
387
624
|
});
|
|
388
|
-
const signatures = value.signatures === undefined
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
625
|
+
const signatures = value.signatures === undefined
|
|
626
|
+
? undefined
|
|
627
|
+
: Array.isArray(value.signatures)
|
|
628
|
+
? value.signatures.map((signature) => {
|
|
629
|
+
if (!isRecord(signature) || typeof signature.protected !== "string" || typeof signature.signature !== "string")
|
|
630
|
+
throw new A2AError("Malformed A2A card signature", 502, "ERR_PRISM_A2A_CARD");
|
|
631
|
+
return {
|
|
632
|
+
protected: signature.protected,
|
|
633
|
+
signature: signature.signature,
|
|
634
|
+
header: isRecord(signature.header) ? signature.header : undefined,
|
|
635
|
+
};
|
|
636
|
+
})
|
|
637
|
+
: (() => {
|
|
638
|
+
throw new A2AError("Malformed A2A card signatures", 502, "ERR_PRISM_A2A_CARD");
|
|
639
|
+
})();
|
|
393
640
|
return createA2AAgentCard({
|
|
394
641
|
name: value.name,
|
|
395
642
|
description: value.description,
|
|
396
643
|
version: value.version,
|
|
397
644
|
supportedInterfaces,
|
|
398
|
-
capabilities: {
|
|
645
|
+
capabilities: {
|
|
646
|
+
streaming: value.capabilities.streaming,
|
|
647
|
+
pushNotifications: typeof value.capabilities.pushNotifications === "boolean" ? value.capabilities.pushNotifications : undefined,
|
|
648
|
+
extendedAgentCard: typeof value.capabilities.extendedAgentCard === "boolean" ? value.capabilities.extendedAgentCard : undefined,
|
|
649
|
+
},
|
|
399
650
|
defaultInputModes: value.defaultInputModes,
|
|
400
651
|
defaultOutputModes: value.defaultOutputModes,
|
|
401
652
|
skills,
|
|
@@ -455,7 +706,13 @@ function ownedSignal(parent, timeoutMs) {
|
|
|
455
706
|
else
|
|
456
707
|
parent?.addEventListener("abort", abort, { once: true });
|
|
457
708
|
const timer = setTimeout(() => controller.abort(new DOMException("A2A request timed out", "AbortError")), timeoutMs);
|
|
458
|
-
return {
|
|
709
|
+
return {
|
|
710
|
+
signal: controller.signal,
|
|
711
|
+
dispose: () => {
|
|
712
|
+
clearTimeout(timer);
|
|
713
|
+
parent?.removeEventListener("abort", abort);
|
|
714
|
+
},
|
|
715
|
+
};
|
|
459
716
|
}
|
|
460
717
|
function abortable(promise, signal) {
|
|
461
718
|
if (signal.aborted)
|
|
@@ -466,11 +723,32 @@ function abortable(promise, signal) {
|
|
|
466
723
|
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
467
724
|
});
|
|
468
725
|
}
|
|
469
|
-
function assertInput(input, maxBytes) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
function
|
|
726
|
+
function assertInput(input, maxBytes) {
|
|
727
|
+
if (new TextEncoder().encode(input).byteLength > maxBytes)
|
|
728
|
+
throw new A2AError("A2A input exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
|
|
729
|
+
}
|
|
730
|
+
function assertMessage(message, maxBytes) {
|
|
731
|
+
if (!message.messageId || !Array.isArray(message.parts) || message.parts.length === 0)
|
|
732
|
+
throw new A2AError("Invalid A2A outbound message", 400, "ERR_PRISM_A2A_MESSAGE");
|
|
733
|
+
let encoded;
|
|
734
|
+
try {
|
|
735
|
+
encoded = JSON.stringify(message);
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
throw new A2AError("Invalid A2A outbound message", 400, "ERR_PRISM_A2A_MESSAGE");
|
|
739
|
+
}
|
|
740
|
+
if (Buffer.byteLength(encoded, "utf8") > maxBytes)
|
|
741
|
+
throw new A2AError("A2A input exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
|
|
742
|
+
}
|
|
743
|
+
function headersObject(headers) {
|
|
744
|
+
return Object.fromEntries(new Headers(headers).entries());
|
|
745
|
+
}
|
|
746
|
+
function isRecord(value) {
|
|
747
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
748
|
+
}
|
|
749
|
+
function stringArray(value) {
|
|
750
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
751
|
+
}
|
|
474
752
|
function parseSecurity(value) {
|
|
475
753
|
if (value === undefined)
|
|
476
754
|
return undefined;
|
|
@@ -482,5 +760,7 @@ function parseSecurity(value) {
|
|
|
482
760
|
return entry;
|
|
483
761
|
});
|
|
484
762
|
}
|
|
485
|
-
function safeRemote(message, options) {
|
|
763
|
+
function safeRemote(message, options) {
|
|
764
|
+
return options.redactor?.redact(message.slice(0, 1024)) ?? message.slice(0, 1024);
|
|
765
|
+
}
|
|
486
766
|
//# sourceMappingURL=a2a-client.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AgentEventSource, AgentRunRef, DurableAgentEventRecord } from "@arnilo/prism";
|
|
2
|
+
import type { A2AAuthorization, A2ATask, A2ATaskEvent } from "./a2a-types.js";
|
|
3
|
+
export type A2ATaskEventPayload = {
|
|
4
|
+
readonly task: A2ATask;
|
|
5
|
+
} | {
|
|
6
|
+
readonly statusUpdate: Extract<A2ATaskEvent, {
|
|
7
|
+
readonly statusUpdate: unknown;
|
|
8
|
+
}>["statusUpdate"];
|
|
9
|
+
} | {
|
|
10
|
+
readonly artifactUpdate: Extract<A2ATaskEvent, {
|
|
11
|
+
readonly artifactUpdate: unknown;
|
|
12
|
+
}>["artifactUpdate"];
|
|
13
|
+
};
|
|
14
|
+
export interface A2AAgentEventTask {
|
|
15
|
+
readonly task: A2ATask;
|
|
16
|
+
readonly run: AgentRunRef;
|
|
17
|
+
}
|
|
18
|
+
export interface A2AAgentEventSourceOptions {
|
|
19
|
+
readonly source: AgentEventSource;
|
|
20
|
+
/** Resolves only host-owned task state and its exact Prism run. */
|
|
21
|
+
readonly resolveTask: (input: {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly authorization: A2AAuthorization;
|
|
24
|
+
readonly signal: AbortSignal;
|
|
25
|
+
}) => A2AAgentEventTask | undefined | Promise<A2AAgentEventTask | undefined>;
|
|
26
|
+
/** Maps at most one durable Prism record to one A2A update. Event IDs stay source-owned cursors. */
|
|
27
|
+
readonly map: (input: {
|
|
28
|
+
readonly record: DurableAgentEventRecord;
|
|
29
|
+
readonly task: A2ATask;
|
|
30
|
+
readonly authorization: A2AAuthorization;
|
|
31
|
+
}) => A2ATaskEventPayload | undefined | Promise<A2ATaskEventPayload | undefined>;
|
|
32
|
+
}
|
|
33
|
+
export interface A2AAgentEventSource {
|
|
34
|
+
subscribe(input: {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly afterEventId?: string;
|
|
37
|
+
readonly authorization: A2AAuthorization;
|
|
38
|
+
readonly signal: AbortSignal;
|
|
39
|
+
}): AsyncIterable<A2ATaskEvent>;
|
|
40
|
+
}
|
|
41
|
+
/** Host-selected durable task stream over AgentEventSource; owns no task database or worker. */
|
|
42
|
+
export declare function createA2AAgentEventSource(options: A2AAgentEventSourceOptions): A2AAgentEventSource;
|