@arnilo/prism-supervisor 0.0.15 → 0.0.17
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 +10 -0
- package/dist/a2a-card.js +19 -7
- package/dist/a2a-client.js +278 -50
- package/dist/a2a-parts.js +109 -19
- package/dist/a2a-push.js +10 -2
- package/dist/a2a-server.js +218 -84
- package/dist/errors.js +12 -3
- package/dist/limits.js +4 -1
- package/dist/supervisor.js +51 -25
- package/package.json +2 -2
package/dist/a2a-parts.js
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
import { A2AError } from "./errors.js";
|
|
2
|
-
export const A2A_DEFAULT_LIMITS = {
|
|
3
|
-
|
|
2
|
+
export const A2A_DEFAULT_LIMITS = {
|
|
3
|
+
maxRequestBytes: 64 * 1024,
|
|
4
|
+
maxResponseBytes: 1024 * 1024,
|
|
5
|
+
maxEventBytes: 64 * 1024,
|
|
6
|
+
maxStreamBytes: 10 * 1024 * 1024,
|
|
7
|
+
maxStreamEvents: 10_000,
|
|
8
|
+
maxConcurrentRequests: 16,
|
|
9
|
+
timeoutMs: 120_000,
|
|
10
|
+
maxCardBytes: 64 * 1024,
|
|
11
|
+
maxIdBytes: 256,
|
|
12
|
+
maxParts: 32,
|
|
13
|
+
maxPartBytes: 1024 * 1024,
|
|
14
|
+
maxRawBytes: 1024 * 1024,
|
|
15
|
+
maxDataBytes: 256 * 1024,
|
|
16
|
+
maxArtifacts: 32,
|
|
17
|
+
maxHistory: 100,
|
|
18
|
+
maxPageSize: 100,
|
|
19
|
+
maxCursorBytes: 4096,
|
|
20
|
+
maxReplayEvents: 1000,
|
|
21
|
+
maxPushConfigs: 10,
|
|
22
|
+
};
|
|
23
|
+
export const A2A_HARD_LIMITS = {
|
|
24
|
+
maxRequestBytes: 1024 * 1024,
|
|
25
|
+
maxResponseBytes: 8 * 1024 * 1024,
|
|
26
|
+
maxEventBytes: 1024 * 1024,
|
|
27
|
+
maxStreamBytes: 64 * 1024 * 1024,
|
|
28
|
+
maxStreamEvents: 100_000,
|
|
29
|
+
maxConcurrentRequests: 256,
|
|
30
|
+
timeoutMs: 30 * 60_000,
|
|
31
|
+
maxCardBytes: 1024 * 1024,
|
|
32
|
+
maxIdBytes: 4096,
|
|
33
|
+
maxParts: 256,
|
|
34
|
+
maxPartBytes: 8 * 1024 * 1024,
|
|
35
|
+
maxRawBytes: 8 * 1024 * 1024,
|
|
36
|
+
maxDataBytes: 4 * 1024 * 1024,
|
|
37
|
+
maxArtifacts: 256,
|
|
38
|
+
maxHistory: 1000,
|
|
39
|
+
maxPageSize: 1000,
|
|
40
|
+
maxCursorBytes: 16 * 1024,
|
|
41
|
+
maxReplayEvents: 10_000,
|
|
42
|
+
maxPushConfigs: 100,
|
|
43
|
+
};
|
|
4
44
|
export function resolveA2ALimits(input = {}) {
|
|
5
45
|
const output = {};
|
|
6
46
|
for (const key of Object.keys(A2A_DEFAULT_LIMITS)) {
|
|
@@ -12,12 +52,24 @@ export function resolveA2ALimits(input = {}) {
|
|
|
12
52
|
return output;
|
|
13
53
|
}
|
|
14
54
|
export async function parseA2AMessage(value, limits, policy = {}) {
|
|
15
|
-
if (!record(value) ||
|
|
55
|
+
if (!record(value) ||
|
|
56
|
+
(value.role !== "user" && value.role !== "ROLE_USER" && value.role !== "agent" && value.role !== "ROLE_AGENT") ||
|
|
57
|
+
!id(value.messageId, limits) ||
|
|
58
|
+
!Array.isArray(value.parts) ||
|
|
59
|
+
value.parts.length < 1 ||
|
|
60
|
+
value.parts.length > limits.maxParts)
|
|
16
61
|
throw new A2AError("Invalid A2A message", 400, "ERR_PRISM_A2A_MESSAGE");
|
|
17
62
|
const parts = [];
|
|
18
63
|
for (const part of value.parts)
|
|
19
64
|
parts.push(await parseA2APart(part, limits, policy));
|
|
20
|
-
const message = {
|
|
65
|
+
const message = {
|
|
66
|
+
role: value.role,
|
|
67
|
+
messageId: value.messageId,
|
|
68
|
+
parts,
|
|
69
|
+
contextId: optionalId(value.contextId, limits),
|
|
70
|
+
taskId: optionalId(value.taskId, limits),
|
|
71
|
+
metadata: record(value.metadata) ? value.metadata : undefined,
|
|
72
|
+
};
|
|
21
73
|
bounded(message, limits.maxRequestBytes, "A2A message");
|
|
22
74
|
return message;
|
|
23
75
|
}
|
|
@@ -29,11 +81,19 @@ export async function parseA2APart(value, limits, policy = {}) {
|
|
|
29
81
|
throw new A2AError("Unknown A2A part field", 400, "ERR_PRISM_A2A_PART");
|
|
30
82
|
if (variants.length !== 1)
|
|
31
83
|
throw new A2AError("A2A part requires exactly one content field", 400, "ERR_PRISM_A2A_PART");
|
|
32
|
-
const base = {
|
|
84
|
+
const base = {
|
|
85
|
+
mediaType: optionalString(value.mediaType, 256),
|
|
86
|
+
filename: optionalString(value.filename, 1024),
|
|
87
|
+
metadata: record(value.metadata) ? value.metadata : undefined,
|
|
88
|
+
};
|
|
33
89
|
let part;
|
|
34
90
|
if (variants[0] === "text" && typeof value.text === "string")
|
|
35
91
|
part = { ...base, text: value.text };
|
|
36
|
-
else if (variants[0] === "raw" &&
|
|
92
|
+
else if (variants[0] === "raw" &&
|
|
93
|
+
policy.allowRaw &&
|
|
94
|
+
typeof value.raw === "string" &&
|
|
95
|
+
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value.raw) &&
|
|
96
|
+
Buffer.from(value.raw, "base64").byteLength <= limits.maxRawBytes)
|
|
37
97
|
part = { ...base, raw: value.raw };
|
|
38
98
|
else if (variants[0] === "url" && policy.allowUrl && policy.validateUrl && typeof value.url === "string") {
|
|
39
99
|
let url;
|
|
@@ -58,7 +118,11 @@ export async function parseA2APart(value, limits, policy = {}) {
|
|
|
58
118
|
return part;
|
|
59
119
|
}
|
|
60
120
|
export async function validateA2ATask(task, limits, policy = { allowRaw: true, allowUrl: true, allowData: true }) {
|
|
61
|
-
if (!id(task.id, limits) ||
|
|
121
|
+
if (!id(task.id, limits) ||
|
|
122
|
+
!id(task.contextId, limits) ||
|
|
123
|
+
!task.status ||
|
|
124
|
+
!TASK_STATES.has(task.status.state) ||
|
|
125
|
+
!Number.isFinite(Date.parse(task.status.timestamp)))
|
|
62
126
|
throw new A2AError("Invalid A2A task", 500, "ERR_PRISM_A2A_TASK");
|
|
63
127
|
if ((task.artifacts?.length ?? 0) > limits.maxArtifacts || (task.history?.length ?? 0) > limits.maxHistory)
|
|
64
128
|
throw new A2AError("A2A task collection limit exceeded", 507, "ERR_PRISM_A2A_RESPONSE_LIMIT");
|
|
@@ -75,11 +139,18 @@ async function validateArtifact(value, limits, policy) {
|
|
|
75
139
|
for (const part of value.parts)
|
|
76
140
|
await parseA2APart(part, limits, policy);
|
|
77
141
|
}
|
|
78
|
-
export function requireId(value, limits, label = "task id") {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return
|
|
82
|
-
|
|
142
|
+
export function requireId(value, limits, label = "task id") {
|
|
143
|
+
if (!id(value, limits))
|
|
144
|
+
throw new A2AError(`Invalid A2A ${label}`, 400, "ERR_PRISM_A2A_REQUEST");
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
export function optionalCursor(value, limits) {
|
|
148
|
+
if (value === undefined || value === "")
|
|
149
|
+
return undefined;
|
|
150
|
+
if (typeof value !== "string" || Buffer.byteLength(value) > limits.maxCursorBytes)
|
|
151
|
+
throw new A2AError("Invalid A2A page/event cursor", 400, "ERR_PRISM_A2A_REQUEST");
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
83
154
|
export function bounded(value, maxBytes, label) {
|
|
84
155
|
let properties = 0;
|
|
85
156
|
const stack = [{ value, depth: 0 }];
|
|
@@ -111,11 +182,30 @@ export function bounded(value, maxBytes, label) {
|
|
|
111
182
|
throw new A2AError(`${label} exceeds max bytes`, 507, "ERR_PRISM_A2A_RESPONSE_LIMIT");
|
|
112
183
|
return value;
|
|
113
184
|
}
|
|
114
|
-
export function record(value) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
function
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
|
|
185
|
+
export function record(value) {
|
|
186
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
187
|
+
}
|
|
188
|
+
function id(value, limits) {
|
|
189
|
+
return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= limits.maxIdBytes;
|
|
190
|
+
}
|
|
191
|
+
function optionalId(value, limits) {
|
|
192
|
+
return value === undefined ? undefined : requireId(value, limits);
|
|
193
|
+
}
|
|
194
|
+
function optionalString(value, max) {
|
|
195
|
+
if (value === undefined)
|
|
196
|
+
return undefined;
|
|
197
|
+
if (typeof value !== "string" || Buffer.byteLength(value) > max)
|
|
198
|
+
throw new A2AError("Invalid A2A part metadata", 400, "ERR_PRISM_A2A_PART");
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
const TASK_STATES = new Set([
|
|
202
|
+
"TASK_STATE_SUBMITTED",
|
|
203
|
+
"TASK_STATE_WORKING",
|
|
204
|
+
"TASK_STATE_COMPLETED",
|
|
205
|
+
"TASK_STATE_FAILED",
|
|
206
|
+
"TASK_STATE_CANCELED",
|
|
207
|
+
"TASK_STATE_INPUT_REQUIRED",
|
|
208
|
+
"TASK_STATE_REJECTED",
|
|
209
|
+
"TASK_STATE_AUTH_REQUIRED",
|
|
210
|
+
]);
|
|
121
211
|
//# sourceMappingURL=a2a-parts.js.map
|
package/dist/a2a-push.js
CHANGED
|
@@ -4,7 +4,12 @@ import { A2AError } from "./errors.js";
|
|
|
4
4
|
export async function deliverA2APushEvent(delivery, config, event, options = {}) {
|
|
5
5
|
const limits = resolveA2ALimits(options.limits);
|
|
6
6
|
const maxAttempts = options.maxAttempts ?? 1, timeoutMs = options.timeoutMs ?? 10_000;
|
|
7
|
-
if (!Number.isSafeInteger(maxAttempts) ||
|
|
7
|
+
if (!Number.isSafeInteger(maxAttempts) ||
|
|
8
|
+
maxAttempts < 1 ||
|
|
9
|
+
maxAttempts > 3 ||
|
|
10
|
+
!Number.isSafeInteger(timeoutMs) ||
|
|
11
|
+
timeoutMs < 1 ||
|
|
12
|
+
timeoutMs > 60_000)
|
|
8
13
|
throw new A2AError("Invalid A2A push delivery limits", 400, "ERR_PRISM_A2A_CONFIG");
|
|
9
14
|
bounded(event, limits.maxEventBytes, "A2A push event");
|
|
10
15
|
if (!event.eventId || Buffer.byteLength(event.eventId) > limits.maxCursorBytes)
|
|
@@ -20,7 +25,10 @@ export async function deliverA2APushEvent(delivery, config, event, options = {})
|
|
|
20
25
|
const timer = setTimeout(() => controller.abort(new DOMException("A2A push delivery timed out", "AbortError")), timeoutMs);
|
|
21
26
|
try {
|
|
22
27
|
controller.signal.throwIfAborted();
|
|
23
|
-
await Promise.race([
|
|
28
|
+
await Promise.race([
|
|
29
|
+
delivery.deliver({ config, event, idempotencyKey: event.eventId, attempt, signal: controller.signal }),
|
|
30
|
+
new Promise((_resolve, reject) => controller.signal.addEventListener("abort", () => reject(controller.signal.reason), { once: true })),
|
|
31
|
+
]);
|
|
24
32
|
return { attempts: attempt };
|
|
25
33
|
}
|
|
26
34
|
catch (error) {
|
package/dist/a2a-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assertIdentityActive, assertIdentityMatchesOwnership } from "@arnilo/prism";
|
|
2
2
|
import { createA2AAgentCard } from "./a2a-card.js";
|
|
3
|
-
import { bounded, optionalCursor, parseA2AMessage, record, requireId, resolveA2ALimits, validateA2ATask } from "./a2a-parts.js";
|
|
3
|
+
import { bounded, optionalCursor, parseA2AMessage, record, requireId, resolveA2ALimits, validateA2ATask, } from "./a2a-parts.js";
|
|
4
4
|
import { A2AError } from "./errors.js";
|
|
5
5
|
const JSON_HEADERS = { "content-type": "application/a2a+json; charset=utf-8", "a2a-version": "1.0" };
|
|
6
6
|
const SSE_HEADERS = { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", "a2a-version": "1.0" };
|
|
@@ -50,17 +50,28 @@ export function createA2AHandler(options) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
if (rpc.method === "GetExtendedAgentCard")
|
|
53
|
-
return card.capabilities.extendedAgentCard
|
|
53
|
+
return card.capabilities.extendedAgentCard
|
|
54
|
+
? json(rpc.id, card, limits, options)
|
|
55
|
+
: rpcError(rpc.id, -32004, "Extended Agent Card unavailable");
|
|
54
56
|
if (rpc.method === "SendMessage" || rpc.method === "SendStreamingMessage") {
|
|
55
57
|
const message = await parseA2AMessage(rpc.params?.message, limits, options.parts);
|
|
56
58
|
if (options.tasks) {
|
|
57
|
-
const task = await options.tasks.start({
|
|
59
|
+
const task = await options.tasks.start({
|
|
60
|
+
message,
|
|
61
|
+
authorization,
|
|
62
|
+
signal: owned.signal,
|
|
63
|
+
returnImmediately: record(rpc.params?.configuration) && rpc.params.configuration.returnImmediately === true,
|
|
64
|
+
});
|
|
58
65
|
await validateA2ATask(task, limits, options.parts);
|
|
59
66
|
if (rpc.method === "SendMessage")
|
|
60
67
|
return json(rpc.id, { task }, limits, options);
|
|
61
68
|
transferred = true;
|
|
62
|
-
const events = terminal(task.status.state)
|
|
63
|
-
|
|
69
|
+
const events = terminal(task.status.state)
|
|
70
|
+
? oneEvent(task)
|
|
71
|
+
: options.tasks.subscribe({ id: task.id, authorization, signal: owned.signal });
|
|
72
|
+
return streamResponse(rpc.id, events, owned, limits, options, () => {
|
|
73
|
+
active -= 1;
|
|
74
|
+
});
|
|
64
75
|
}
|
|
65
76
|
if (message.parts.some((part) => !("text" in part)))
|
|
66
77
|
return rpcError(rpc.id, -32004, "Durable task lifecycle required for rich parts");
|
|
@@ -78,14 +89,23 @@ export function createA2AHandler(options) {
|
|
|
78
89
|
return json(rpc.id, { task: toTask(taskId, contextId, await abortable(session.run(input, runOptions), owned.signal), options) }, limits, options);
|
|
79
90
|
transferred = true;
|
|
80
91
|
const events = runEvents(taskId, contextId, () => session.run(input, runOptions), options);
|
|
81
|
-
return streamResponse(rpc.id, events, owned, limits, options, () => {
|
|
92
|
+
return streamResponse(rpc.id, events, owned, limits, options, () => {
|
|
93
|
+
active -= 1;
|
|
94
|
+
});
|
|
82
95
|
}
|
|
83
96
|
if (["GetTask", "ListTasks", "CancelTask", "SubscribeToTask"].includes(rpc.method)) {
|
|
84
97
|
if (!options.tasks)
|
|
85
98
|
return rpcError(rpc.id, -32004, "Task lifecycle unavailable");
|
|
86
99
|
if (rpc.method === "GetTask") {
|
|
87
|
-
const task = await options.tasks.get({
|
|
88
|
-
|
|
100
|
+
const task = await options.tasks.get({
|
|
101
|
+
id: requireId(rpc.params?.id, limits),
|
|
102
|
+
historyLength: integer(rpc.params?.historyLength, 0, limits.maxHistory),
|
|
103
|
+
authorization,
|
|
104
|
+
signal: owned.signal,
|
|
105
|
+
});
|
|
106
|
+
return task
|
|
107
|
+
? json(rpc.id, await validateA2ATask(task, limits, options.parts), limits, options)
|
|
108
|
+
: rpcError(rpc.id, -32001, "Task not found");
|
|
89
109
|
}
|
|
90
110
|
if (rpc.method === "ListTasks") {
|
|
91
111
|
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");
|
|
@@ -99,7 +119,9 @@ export function createA2AHandler(options) {
|
|
|
99
119
|
}
|
|
100
120
|
if (rpc.method === "CancelTask") {
|
|
101
121
|
const task = await options.tasks.cancel({ id: requireId(rpc.params?.id, limits), authorization, signal: owned.signal });
|
|
102
|
-
return task
|
|
122
|
+
return task
|
|
123
|
+
? json(rpc.id, await validateA2ATask(task, limits, options.parts), limits, options)
|
|
124
|
+
: rpcError(rpc.id, -32001, "Task not found");
|
|
103
125
|
}
|
|
104
126
|
const taskId = requireId(rpc.params?.id, limits), afterEventId = optionalCursor(rpc.params?.afterEventId, limits);
|
|
105
127
|
const current = await options.tasks.get({ id: taskId, historyLength: 0, authorization, signal: owned.signal });
|
|
@@ -108,7 +130,9 @@ export function createA2AHandler(options) {
|
|
|
108
130
|
if (finalTerminal(current.status.state))
|
|
109
131
|
return rpcError(rpc.id, -32004, "Terminal task cannot be subscribed");
|
|
110
132
|
transferred = true;
|
|
111
|
-
return streamResponse(rpc.id, options.tasks.subscribe({ id: taskId, afterEventId, authorization, signal: owned.signal }), owned, limits, options, () => {
|
|
133
|
+
return streamResponse(rpc.id, options.tasks.subscribe({ id: taskId, afterEventId, authorization, signal: owned.signal }), owned, limits, options, () => {
|
|
134
|
+
active -= 1;
|
|
135
|
+
});
|
|
112
136
|
}
|
|
113
137
|
if (rpc.method.includes("TaskPushNotificationConfig"))
|
|
114
138
|
return await pushOperation(rpc, authorization, owned.signal, limits, options);
|
|
@@ -152,7 +176,9 @@ async function pushOperation(rpc, authorization, signal, limits, options) {
|
|
|
152
176
|
return json(rpc.id, { configs: page.configs.map(publicPush), nextPageToken: optionalCursor(page.nextPageToken, limits) }, limits, options);
|
|
153
177
|
}
|
|
154
178
|
if (rpc.method === "DeleteTaskPushNotificationConfig")
|
|
155
|
-
return await options.push.delete({ taskId, id: id, authorization, signal })
|
|
179
|
+
return (await options.push.delete({ taskId, id: id, authorization, signal }))
|
|
180
|
+
? json(rpc.id, {}, limits, options)
|
|
181
|
+
: rpcError(rpc.id, -32001, "Task or push config not found");
|
|
156
182
|
return rpcError(rpc.id, -32601, "Method not found");
|
|
157
183
|
}
|
|
158
184
|
async function parsePush(value, limits, options) {
|
|
@@ -169,44 +195,77 @@ async function parsePush(value, limits, options) {
|
|
|
169
195
|
if (url.protocol !== "https:" || url.username || url.password || url.hash)
|
|
170
196
|
throw new A2AError("Push URL requires credential-free HTTPS", 403, "ERR_PRISM_A2A_ORIGIN");
|
|
171
197
|
await options.parts.validateUrl(url);
|
|
172
|
-
const authentication = record(value.authentication) && typeof value.authentication.scheme === "string"
|
|
173
|
-
|
|
198
|
+
const authentication = record(value.authentication) && typeof value.authentication.scheme === "string"
|
|
199
|
+
? {
|
|
200
|
+
scheme: value.authentication.scheme.slice(0, 64),
|
|
201
|
+
credentials: typeof value.authentication.credentials === "string"
|
|
202
|
+
? value.authentication.credentials.slice(0, limits.maxPartBytes)
|
|
203
|
+
: undefined,
|
|
204
|
+
}
|
|
205
|
+
: undefined;
|
|
206
|
+
return bounded({
|
|
207
|
+
id,
|
|
208
|
+
taskId,
|
|
209
|
+
url: url.href,
|
|
210
|
+
token: typeof value.token === "string" ? value.token.slice(0, limits.maxPartBytes) : undefined,
|
|
211
|
+
authentication,
|
|
212
|
+
}, limits.maxPartBytes, "Push config");
|
|
213
|
+
}
|
|
214
|
+
function publicPush(value) {
|
|
215
|
+
return {
|
|
216
|
+
id: value.id,
|
|
217
|
+
taskId: value.taskId,
|
|
218
|
+
url: value.url,
|
|
219
|
+
authentication: value.authentication ? { scheme: value.authentication.scheme } : undefined,
|
|
220
|
+
};
|
|
174
221
|
}
|
|
175
|
-
function publicPush(value) { return { id: value.id, taskId: value.taskId, url: value.url, authentication: value.authentication ? { scheme: value.authentication.scheme } : undefined }; }
|
|
176
222
|
function streamResponse(id, source, owned, limits, options, release) {
|
|
177
223
|
const iterator = source[Symbol.asyncIterator]();
|
|
178
224
|
let events = 0, bytes = 0, released = false, previous = "";
|
|
179
|
-
const finish = () => {
|
|
180
|
-
released
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
const finish = () => {
|
|
226
|
+
if (!released) {
|
|
227
|
+
released = true;
|
|
228
|
+
owned.dispose();
|
|
229
|
+
release();
|
|
230
|
+
void iterator.return?.();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
185
233
|
return new Response(new ReadableStream({
|
|
186
|
-
async pull(controller) {
|
|
187
|
-
|
|
188
|
-
|
|
234
|
+
async pull(controller) {
|
|
235
|
+
try {
|
|
236
|
+
const next = await iterator.next();
|
|
237
|
+
if (next.done) {
|
|
238
|
+
finish();
|
|
239
|
+
controller.close();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (!optionalCursor(next.value.eventId, limits) || next.value.eventId === previous)
|
|
243
|
+
throw new A2AError("Duplicate/invalid A2A event id", 500, "ERR_PRISM_A2A_STREAM_LIMIT");
|
|
244
|
+
await validateTaskEvent(next.value, limits, options);
|
|
245
|
+
previous = next.value.eventId;
|
|
246
|
+
const payload = options.redactor?.redact({ jsonrpc: "2.0", id, result: next.value }) ?? {
|
|
247
|
+
jsonrpc: "2.0",
|
|
248
|
+
id,
|
|
249
|
+
result: next.value,
|
|
250
|
+
};
|
|
251
|
+
const chunk = new TextEncoder().encode(`id: ${next.value.eventId}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
252
|
+
events++;
|
|
253
|
+
bytes += chunk.byteLength;
|
|
254
|
+
if (events > Math.min(limits.maxStreamEvents, limits.maxReplayEvents) ||
|
|
255
|
+
chunk.byteLength > limits.maxEventBytes ||
|
|
256
|
+
bytes > limits.maxStreamBytes)
|
|
257
|
+
throw new A2AError("A2A stream limit exceeded", 507, "ERR_PRISM_A2A_STREAM_LIMIT");
|
|
258
|
+
controller.enqueue(chunk);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
189
261
|
finish();
|
|
190
|
-
controller.
|
|
191
|
-
return;
|
|
262
|
+
controller.error(error);
|
|
192
263
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
previous = next.value.eventId;
|
|
197
|
-
const payload = options.redactor?.redact({ jsonrpc: "2.0", id, result: next.value }) ?? { jsonrpc: "2.0", id, result: next.value };
|
|
198
|
-
const chunk = new TextEncoder().encode(`id: ${next.value.eventId}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
199
|
-
events++;
|
|
200
|
-
bytes += chunk.byteLength;
|
|
201
|
-
if (events > Math.min(limits.maxStreamEvents, limits.maxReplayEvents) || chunk.byteLength > limits.maxEventBytes || bytes > limits.maxStreamBytes)
|
|
202
|
-
throw new A2AError("A2A stream limit exceeded", 507, "ERR_PRISM_A2A_STREAM_LIMIT");
|
|
203
|
-
controller.enqueue(chunk);
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
264
|
+
},
|
|
265
|
+
cancel(reason) {
|
|
266
|
+
owned.abort(reason);
|
|
206
267
|
finish();
|
|
207
|
-
|
|
208
|
-
} },
|
|
209
|
-
cancel(reason) { owned.abort(reason); finish(); },
|
|
268
|
+
},
|
|
210
269
|
}), { headers: SSE_HEADERS });
|
|
211
270
|
}
|
|
212
271
|
async function validateTaskEvent(event, limits, options) {
|
|
@@ -218,51 +277,126 @@ async function validateTaskEvent(event, limits, options) {
|
|
|
218
277
|
await validateA2ATask({ id: event.statusUpdate.taskId, contextId: event.statusUpdate.contextId, status: event.statusUpdate.status }, limits, options.parts);
|
|
219
278
|
return;
|
|
220
279
|
}
|
|
221
|
-
await validateA2ATask({
|
|
280
|
+
await validateA2ATask({
|
|
281
|
+
id: event.artifactUpdate.taskId,
|
|
282
|
+
contextId: event.artifactUpdate.contextId,
|
|
283
|
+
status: { state: "TASK_STATE_WORKING", timestamp: new Date().toISOString() },
|
|
284
|
+
artifacts: [event.artifactUpdate.artifact],
|
|
285
|
+
}, limits, options.parts);
|
|
286
|
+
}
|
|
287
|
+
async function* oneEvent(task) {
|
|
288
|
+
yield { eventId: `terminal-${task.id}`, task };
|
|
289
|
+
}
|
|
290
|
+
async function* runEvents(taskId, contextId, run, options) {
|
|
291
|
+
yield { eventId: "1", task: { id: taskId, contextId, status: { state: "TASK_STATE_WORKING", timestamp: new Date().toISOString() } } };
|
|
292
|
+
yield { eventId: "2", task: toTask(taskId, contextId, await run(), options) };
|
|
222
293
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
294
|
+
function toTask(taskId, contextId, result, options) {
|
|
295
|
+
const state = result.status === "succeeded" ? "TASK_STATE_COMPLETED" : result.status === "aborted" ? "TASK_STATE_CANCELED" : "TASK_STATE_FAILED";
|
|
296
|
+
const text = options.redactor?.redact(result.text) ?? result.text;
|
|
297
|
+
return {
|
|
298
|
+
id: taskId,
|
|
299
|
+
contextId,
|
|
300
|
+
status: { state, timestamp: new Date().toISOString() },
|
|
301
|
+
artifacts: text ? [{ artifactId: `${taskId}-result`, parts: [{ text }] }] : undefined,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function parseRpc(value) {
|
|
305
|
+
if (!record(value) ||
|
|
306
|
+
value.jsonrpc !== "2.0" ||
|
|
307
|
+
!(typeof value.id === "string" || typeof value.id === "number" || value.id === null) ||
|
|
308
|
+
typeof value.method !== "string" ||
|
|
309
|
+
(value.params !== undefined && !record(value.params)))
|
|
310
|
+
throw new A2AError("Invalid JSON-RPC request", 400, "ERR_PRISM_A2A_REQUEST");
|
|
311
|
+
return { jsonrpc: "2.0", id: value.id, method: value.method, params: value.params };
|
|
312
|
+
}
|
|
313
|
+
async function readJson(request, maxBytes, signal) {
|
|
314
|
+
if (!request.body)
|
|
315
|
+
throw new A2AError("Request body is required", 400, "ERR_PRISM_A2A_REQUEST");
|
|
316
|
+
const reader = request.body.getReader(), chunks = [];
|
|
317
|
+
let size = 0;
|
|
318
|
+
try {
|
|
319
|
+
while (true) {
|
|
320
|
+
signal.throwIfAborted();
|
|
321
|
+
const n = await reader.read();
|
|
322
|
+
if (n.done)
|
|
323
|
+
break;
|
|
324
|
+
size += n.value.byteLength;
|
|
325
|
+
if (size > maxBytes)
|
|
326
|
+
throw new A2AError("Request exceeds max bytes", 413, "ERR_PRISM_A2A_REQUEST_LIMIT");
|
|
327
|
+
chunks.push(n.value);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
reader.releaseLock();
|
|
332
|
+
}
|
|
333
|
+
const bytes = new Uint8Array(size);
|
|
334
|
+
let offset = 0;
|
|
335
|
+
for (const chunk of chunks) {
|
|
336
|
+
bytes.set(chunk, offset);
|
|
337
|
+
offset += chunk.byteLength;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
throw new A2AError("Invalid JSON", 400, "ERR_PRISM_A2A_REQUEST");
|
|
239
344
|
}
|
|
240
345
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
346
|
+
function json(id, result, limits, options) {
|
|
347
|
+
const body = JSON.stringify(options.redactor?.redact({ jsonrpc: "2.0", id, result }) ?? { jsonrpc: "2.0", id, result });
|
|
348
|
+
if (Buffer.byteLength(body) > limits.maxResponseBytes)
|
|
349
|
+
throw new A2AError("Response exceeds max bytes", 507, "ERR_PRISM_A2A_RESPONSE_LIMIT");
|
|
350
|
+
return new Response(body, { headers: JSON_HEADERS });
|
|
351
|
+
}
|
|
352
|
+
function rpcError(id, code, message) {
|
|
353
|
+
return new Response(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }), { headers: JSON_HEADERS });
|
|
354
|
+
}
|
|
355
|
+
function errorResponse(status, message, id) {
|
|
356
|
+
const body = { jsonrpc: "2.0", id, error: { code: status === 404 ? -32001 : -32000, message } };
|
|
357
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
358
|
+
}
|
|
359
|
+
function integer(value, fallback, max) {
|
|
360
|
+
if (value === undefined)
|
|
361
|
+
return fallback;
|
|
362
|
+
if (!Number.isSafeInteger(value) || Number(value) < 0 || Number(value) > max)
|
|
363
|
+
throw new A2AError("Invalid A2A integer limit", 400, "ERR_PRISM_A2A_REQUEST");
|
|
364
|
+
return Number(value);
|
|
365
|
+
}
|
|
366
|
+
function terminal(state) {
|
|
367
|
+
return finalTerminal(state) || state === "TASK_STATE_INPUT_REQUIRED" || state === "TASK_STATE_AUTH_REQUIRED";
|
|
368
|
+
}
|
|
369
|
+
function finalTerminal(state) {
|
|
370
|
+
return ["TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_REJECTED"].includes(state);
|
|
371
|
+
}
|
|
372
|
+
function ownedSignal(parent, timeoutMs) {
|
|
373
|
+
const controller = new AbortController();
|
|
374
|
+
const abort = () => controller.abort(parent.reason);
|
|
375
|
+
if (parent.aborted)
|
|
376
|
+
abort();
|
|
377
|
+
else
|
|
378
|
+
parent.addEventListener("abort", abort, { once: true });
|
|
379
|
+
const timer = setTimeout(() => controller.abort(new DOMException("A2A request timed out", "AbortError")), timeoutMs);
|
|
380
|
+
return {
|
|
381
|
+
signal: controller.signal,
|
|
382
|
+
abort: (reason) => controller.abort(reason),
|
|
383
|
+
dispose: () => {
|
|
384
|
+
clearTimeout(timer);
|
|
385
|
+
parent.removeEventListener("abort", abort);
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function abortable(promise, signal) {
|
|
390
|
+
if (signal.aborted)
|
|
391
|
+
return Promise.reject(signal.reason);
|
|
392
|
+
return new Promise((resolve, reject) => {
|
|
393
|
+
const abort = () => reject(signal.reason);
|
|
394
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
395
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function safeError(error, options) {
|
|
399
|
+
const message = (error instanceof Error ? error.message : "A2A request failed").slice(0, 1024);
|
|
400
|
+
return options.redactor?.redact(message) ?? message;
|
|
248
401
|
}
|
|
249
|
-
catch {
|
|
250
|
-
throw new A2AError("Invalid JSON", 400, "ERR_PRISM_A2A_REQUEST");
|
|
251
|
-
} }
|
|
252
|
-
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)
|
|
253
|
-
throw new A2AError("Response exceeds max bytes", 507, "ERR_PRISM_A2A_RESPONSE_LIMIT"); return new Response(body, { headers: JSON_HEADERS }); }
|
|
254
|
-
function rpcError(id, code, message) { return new Response(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }), { headers: JSON_HEADERS }); }
|
|
255
|
-
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 }); }
|
|
256
|
-
function integer(value, fallback, max) { if (value === undefined)
|
|
257
|
-
return fallback; if (!Number.isSafeInteger(value) || Number(value) < 0 || Number(value) > max)
|
|
258
|
-
throw new A2AError("Invalid A2A integer limit", 400, "ERR_PRISM_A2A_REQUEST"); return Number(value); }
|
|
259
|
-
function terminal(state) { return finalTerminal(state) || state === "TASK_STATE_INPUT_REQUIRED" || state === "TASK_STATE_AUTH_REQUIRED"; }
|
|
260
|
-
function finalTerminal(state) { return ["TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_REJECTED"].includes(state); }
|
|
261
|
-
function ownedSignal(parent, timeoutMs) { const controller = new AbortController(); const abort = () => controller.abort(parent.reason); if (parent.aborted)
|
|
262
|
-
abort();
|
|
263
|
-
else
|
|
264
|
-
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); } }; }
|
|
265
|
-
function abortable(promise, signal) { if (signal.aborted)
|
|
266
|
-
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)); }); }
|
|
267
|
-
function safeError(error, options) { const message = (error instanceof Error ? error.message : "A2A request failed").slice(0, 1024); return options.redactor?.redact(message) ?? message; }
|
|
268
402
|
//# sourceMappingURL=a2a-server.js.map
|
package/dist/errors.js
CHANGED
|
@@ -7,13 +7,22 @@ export class SupervisorError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
9
|
export class SupervisorValidationError extends SupervisorError {
|
|
10
|
-
constructor(message) {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message, "ERR_PRISM_SUPERVISOR_VALIDATION");
|
|
12
|
+
this.name = "SupervisorValidationError";
|
|
13
|
+
}
|
|
11
14
|
}
|
|
12
15
|
export class SupervisorLimitError extends SupervisorError {
|
|
13
|
-
constructor(message) {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message, "ERR_PRISM_SUPERVISOR_LIMIT");
|
|
18
|
+
this.name = "SupervisorLimitError";
|
|
19
|
+
}
|
|
14
20
|
}
|
|
15
21
|
export class SupervisorDeniedError extends SupervisorError {
|
|
16
|
-
constructor(message = "Delegation denied") {
|
|
22
|
+
constructor(message = "Delegation denied") {
|
|
23
|
+
super(message, "ERR_PRISM_SUPERVISOR_DENIED");
|
|
24
|
+
this.name = "SupervisorDeniedError";
|
|
25
|
+
}
|
|
17
26
|
}
|
|
18
27
|
export class A2AError extends SupervisorError {
|
|
19
28
|
status;
|
package/dist/limits.js
CHANGED
|
@@ -37,6 +37,9 @@ export function narrowSupervisorLimits(parent, input) {
|
|
|
37
37
|
if (!input)
|
|
38
38
|
return parent;
|
|
39
39
|
const requested = resolveSupervisorLimits({ ...parent, ...input });
|
|
40
|
-
return Object.fromEntries(Object.keys(SPECS).map((key) => [
|
|
40
|
+
return Object.fromEntries(Object.keys(SPECS).map((key) => [
|
|
41
|
+
key,
|
|
42
|
+
Math.min(parent[key], requested[key]),
|
|
43
|
+
]));
|
|
41
44
|
}
|
|
42
45
|
//# sourceMappingURL=limits.js.map
|