@arnilo/prism-supervisor 0.0.96 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { A2AError } from "./errors.js";
|
|
2
|
+
/** Host-selected durable task stream over AgentEventSource; owns no task database or worker. */
|
|
3
|
+
export function createA2AAgentEventSource(options) {
|
|
4
|
+
return {
|
|
5
|
+
subscribe(input) {
|
|
6
|
+
return {
|
|
7
|
+
async *[Symbol.asyncIterator]() {
|
|
8
|
+
input.signal.throwIfAborted();
|
|
9
|
+
const resolved = await options.resolveTask(input);
|
|
10
|
+
if (!resolved?.run.sessionId || resolved.task.id !== input.id) {
|
|
11
|
+
throw new A2AError("Task unavailable", 404, "ERR_PRISM_A2A_TASK");
|
|
12
|
+
}
|
|
13
|
+
let first = true;
|
|
14
|
+
for await (const item of options.source.subscribe({
|
|
15
|
+
ownership: input.authorization.ownership,
|
|
16
|
+
sessionId: resolved.run.sessionId,
|
|
17
|
+
runId: resolved.run.runId,
|
|
18
|
+
after: input.afterEventId,
|
|
19
|
+
signal: input.signal,
|
|
20
|
+
})) {
|
|
21
|
+
if (!item.record.redacted)
|
|
22
|
+
throw new A2AError("Task event unavailable", 500, "ERR_PRISM_A2A_TASK");
|
|
23
|
+
const payload = await options.map({ record: item.record, task: resolved.task, authorization: input.authorization });
|
|
24
|
+
if (!payload)
|
|
25
|
+
continue;
|
|
26
|
+
if (first && input.afterEventId === undefined && !("task" in payload)) {
|
|
27
|
+
throw new A2AError("Initial task event required", 500, "ERR_PRISM_A2A_TASK");
|
|
28
|
+
}
|
|
29
|
+
first = false;
|
|
30
|
+
yield { eventId: item.cursor, ...payload };
|
|
31
|
+
}
|
|
32
|
+
if (first && input.afterEventId === undefined) {
|
|
33
|
+
throw new A2AError("Initial task event required", 500, "ERR_PRISM_A2A_TASK");
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=a2a-event-source.js.map
|
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,5 +1,6 @@
|
|
|
1
|
+
import { assertIdentityActive, assertIdentityMatchesOwnership } from "@arnilo/prism";
|
|
1
2
|
import { createA2AAgentCard } from "./a2a-card.js";
|
|
2
|
-
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";
|
|
3
4
|
import { A2AError } from "./errors.js";
|
|
4
5
|
const JSON_HEADERS = { "content-type": "application/a2a+json; charset=utf-8", "a2a-version": "1.0" };
|
|
5
6
|
const SSE_HEADERS = { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-cache, no-transform", "a2a-version": "1.0" };
|
|
@@ -39,36 +40,72 @@ export function createA2AHandler(options) {
|
|
|
39
40
|
const authorization = await abortable(Promise.resolve(options.authorize({ request, method: rpc.method, signal: owned.signal })), owned.signal);
|
|
40
41
|
if (!authorization)
|
|
41
42
|
return errorResponse(403, "Forbidden", rpc.id);
|
|
43
|
+
if (authorization.identity) {
|
|
44
|
+
try {
|
|
45
|
+
assertIdentityActive(authorization.identity);
|
|
46
|
+
assertIdentityMatchesOwnership(authorization.identity, authorization.ownership);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return errorResponse(403, "Forbidden", rpc.id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
42
52
|
if (rpc.method === "GetExtendedAgentCard")
|
|
43
|
-
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");
|
|
44
56
|
if (rpc.method === "SendMessage" || rpc.method === "SendStreamingMessage") {
|
|
45
57
|
const message = await parseA2AMessage(rpc.params?.message, limits, options.parts);
|
|
46
58
|
if (options.tasks) {
|
|
47
|
-
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
|
+
});
|
|
48
65
|
await validateA2ATask(task, limits, options.parts);
|
|
49
66
|
if (rpc.method === "SendMessage")
|
|
50
67
|
return json(rpc.id, { task }, limits, options);
|
|
51
68
|
transferred = true;
|
|
52
|
-
const events = terminal(task.status.state)
|
|
53
|
-
|
|
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
|
+
});
|
|
54
75
|
}
|
|
55
76
|
if (message.parts.some((part) => !("text" in part)))
|
|
56
77
|
return rpcError(rpc.id, -32004, "Durable task lifecycle required for rich parts");
|
|
57
78
|
const input = message.parts.map((part) => part.text).join("\n");
|
|
58
79
|
const session = await abortable(Promise.resolve(options.exposure.sessionFactory(authorization)), owned.signal);
|
|
59
80
|
const taskId = `task-${crypto.randomUUID()}`, contextId = message.contextId ?? `context-${crypto.randomUUID()}`;
|
|
81
|
+
const runOptions = {
|
|
82
|
+
ownership: authorization.ownership,
|
|
83
|
+
identity: authorization.identity,
|
|
84
|
+
metadata: authorization.metadata,
|
|
85
|
+
signal: owned.signal,
|
|
86
|
+
redactor: options.redactor,
|
|
87
|
+
};
|
|
60
88
|
if (rpc.method === "SendMessage")
|
|
61
|
-
return json(rpc.id, { task: toTask(taskId, contextId, await abortable(session.run(input,
|
|
89
|
+
return json(rpc.id, { task: toTask(taskId, contextId, await abortable(session.run(input, runOptions), owned.signal), options) }, limits, options);
|
|
62
90
|
transferred = true;
|
|
63
|
-
const events = runEvents(taskId, contextId, () => session.run(input,
|
|
64
|
-
return streamResponse(rpc.id, events, owned, limits, options, () => {
|
|
91
|
+
const events = runEvents(taskId, contextId, () => session.run(input, runOptions), options);
|
|
92
|
+
return streamResponse(rpc.id, events, owned, limits, options, () => {
|
|
93
|
+
active -= 1;
|
|
94
|
+
});
|
|
65
95
|
}
|
|
66
96
|
if (["GetTask", "ListTasks", "CancelTask", "SubscribeToTask"].includes(rpc.method)) {
|
|
67
97
|
if (!options.tasks)
|
|
68
98
|
return rpcError(rpc.id, -32004, "Task lifecycle unavailable");
|
|
69
99
|
if (rpc.method === "GetTask") {
|
|
70
|
-
const task = await options.tasks.get({
|
|
71
|
-
|
|
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");
|
|
72
109
|
}
|
|
73
110
|
if (rpc.method === "ListTasks") {
|
|
74
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");
|
|
@@ -82,7 +119,9 @@ export function createA2AHandler(options) {
|
|
|
82
119
|
}
|
|
83
120
|
if (rpc.method === "CancelTask") {
|
|
84
121
|
const task = await options.tasks.cancel({ id: requireId(rpc.params?.id, limits), authorization, signal: owned.signal });
|
|
85
|
-
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");
|
|
86
125
|
}
|
|
87
126
|
const taskId = requireId(rpc.params?.id, limits), afterEventId = optionalCursor(rpc.params?.afterEventId, limits);
|
|
88
127
|
const current = await options.tasks.get({ id: taskId, historyLength: 0, authorization, signal: owned.signal });
|
|
@@ -91,7 +130,9 @@ export function createA2AHandler(options) {
|
|
|
91
130
|
if (finalTerminal(current.status.state))
|
|
92
131
|
return rpcError(rpc.id, -32004, "Terminal task cannot be subscribed");
|
|
93
132
|
transferred = true;
|
|
94
|
-
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
|
+
});
|
|
95
136
|
}
|
|
96
137
|
if (rpc.method.includes("TaskPushNotificationConfig"))
|
|
97
138
|
return await pushOperation(rpc, authorization, owned.signal, limits, options);
|
|
@@ -135,7 +176,9 @@ async function pushOperation(rpc, authorization, signal, limits, options) {
|
|
|
135
176
|
return json(rpc.id, { configs: page.configs.map(publicPush), nextPageToken: optionalCursor(page.nextPageToken, limits) }, limits, options);
|
|
136
177
|
}
|
|
137
178
|
if (rpc.method === "DeleteTaskPushNotificationConfig")
|
|
138
|
-
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");
|
|
139
182
|
return rpcError(rpc.id, -32601, "Method not found");
|
|
140
183
|
}
|
|
141
184
|
async function parsePush(value, limits, options) {
|
|
@@ -152,44 +195,77 @@ async function parsePush(value, limits, options) {
|
|
|
152
195
|
if (url.protocol !== "https:" || url.username || url.password || url.hash)
|
|
153
196
|
throw new A2AError("Push URL requires credential-free HTTPS", 403, "ERR_PRISM_A2A_ORIGIN");
|
|
154
197
|
await options.parts.validateUrl(url);
|
|
155
|
-
const authentication = record(value.authentication) && typeof value.authentication.scheme === "string"
|
|
156
|
-
|
|
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
|
+
};
|
|
157
221
|
}
|
|
158
|
-
function publicPush(value) { return { id: value.id, taskId: value.taskId, url: value.url, authentication: value.authentication ? { scheme: value.authentication.scheme } : undefined }; }
|
|
159
222
|
function streamResponse(id, source, owned, limits, options, release) {
|
|
160
223
|
const iterator = source[Symbol.asyncIterator]();
|
|
161
224
|
let events = 0, bytes = 0, released = false, previous = "";
|
|
162
|
-
const finish = () => {
|
|
163
|
-
released
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
225
|
+
const finish = () => {
|
|
226
|
+
if (!released) {
|
|
227
|
+
released = true;
|
|
228
|
+
owned.dispose();
|
|
229
|
+
release();
|
|
230
|
+
void iterator.return?.();
|
|
231
|
+
}
|
|
232
|
+
};
|
|
168
233
|
return new Response(new ReadableStream({
|
|
169
|
-
async pull(controller) {
|
|
170
|
-
|
|
171
|
-
|
|
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) {
|
|
172
261
|
finish();
|
|
173
|
-
controller.
|
|
174
|
-
return;
|
|
262
|
+
controller.error(error);
|
|
175
263
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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);
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
264
|
+
},
|
|
265
|
+
cancel(reason) {
|
|
266
|
+
owned.abort(reason);
|
|
189
267
|
finish();
|
|
190
|
-
|
|
191
|
-
} },
|
|
192
|
-
cancel(reason) { owned.abort(reason); finish(); },
|
|
268
|
+
},
|
|
193
269
|
}), { headers: SSE_HEADERS });
|
|
194
270
|
}
|
|
195
271
|
async function validateTaskEvent(event, limits, options) {
|
|
@@ -201,51 +277,126 @@ async function validateTaskEvent(event, limits, options) {
|
|
|
201
277
|
await validateA2ATask({ id: event.statusUpdate.taskId, contextId: event.statusUpdate.contextId, status: event.statusUpdate.status }, limits, options.parts);
|
|
202
278
|
return;
|
|
203
279
|
}
|
|
204
|
-
await validateA2ATask({
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
async function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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) };
|
|
293
|
+
}
|
|
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();
|
|
222
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");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
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;
|
|
223
401
|
}
|
|
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));
|
|
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; }
|
|
251
402
|
//# sourceMappingURL=a2a-server.js.map
|