@cogitator-ai/a2a 0.7.7 → 0.8.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/README.md +173 -151
- package/dist/adapters/express.d.ts +5 -0
- package/dist/adapters/express.d.ts.map +1 -1
- package/dist/adapters/express.js +53 -30
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/fastify.d.ts +5 -0
- package/dist/adapters/fastify.d.ts.map +1 -1
- package/dist/adapters/fastify.js +51 -35
- package/dist/adapters/fastify.js.map +1 -1
- package/dist/adapters/hono.d.ts +5 -0
- package/dist/adapters/hono.d.ts.map +1 -1
- package/dist/adapters/hono.js +41 -49
- package/dist/adapters/hono.js.map +1 -1
- package/dist/adapters/koa.d.ts +5 -0
- package/dist/adapters/koa.d.ts.map +1 -1
- package/dist/adapters/koa.js +39 -32
- package/dist/adapters/koa.js.map +1 -1
- package/dist/adapters/next.d.ts +13 -2
- package/dist/adapters/next.d.ts.map +1 -1
- package/dist/adapters/next.js +75 -51
- package/dist/adapters/next.js.map +1 -1
- package/dist/adapters/shared.d.ts +43 -10
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/shared.js +156 -23
- package/dist/adapters/shared.js.map +1 -1
- package/dist/agent-card.d.ts +22 -7
- package/dist/agent-card.d.ts.map +1 -1
- package/dist/agent-card.js +71 -34
- package/dist/agent-card.js.map +1 -1
- package/dist/approvals.d.ts +46 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +105 -0
- package/dist/approvals.js.map +1 -0
- package/dist/client.d.ts +59 -17
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +237 -142
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +24 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +90 -19
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/ownership.d.ts +7 -1
- package/dist/ownership.d.ts.map +1 -1
- package/dist/ownership.js +24 -3
- package/dist/ownership.js.map +1 -1
- package/dist/protocol.d.ts +70 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +187 -0
- package/dist/protocol.js.map +1 -0
- package/dist/push-notifications.d.ts +5 -2
- package/dist/push-notifications.d.ts.map +1 -1
- package/dist/push-notifications.js +33 -23
- package/dist/push-notifications.js.map +1 -1
- package/dist/redis-task-store.js +3 -3
- package/dist/redis-task-store.js.map +1 -1
- package/dist/server.d.ts +75 -12
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +324 -270
- package/dist/server.js.map +1 -1
- package/dist/task-manager.d.ts +57 -11
- package/dist/task-manager.d.ts.map +1 -1
- package/dist/task-manager.js +264 -116
- package/dist/task-manager.js.map +1 -1
- package/dist/task-store.js +4 -4
- package/dist/task-store.js.map +1 -1
- package/dist/types.d.ts +226 -85
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -3
- package/dist/types.js.map +1 -1
- package/dist/url.d.ts +7 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +12 -0
- package/dist/url.js.map +1 -0
- package/package.json +19 -9
- package/dist/adapters/sse-error-event.d.ts +0 -3
- package/dist/adapters/sse-error-event.d.ts.map +0 -1
- package/dist/adapters/sse-error-event.js +0 -12
- package/dist/adapters/sse-error-event.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MAX_RUN_TIMEOUT_MS, resolveSseHeartbeatMs } from '@cogitator-ai/server-shared';
|
|
2
|
+
import { parseJsonRpcRequest, createSuccessResponse, createErrorResponse } from './json-rpc.js';
|
|
2
3
|
import { TaskManager } from './task-manager.js';
|
|
3
4
|
import { generateAgentCard, signAgentCard } from './agent-card.js';
|
|
4
5
|
import { A2AError } from './errors.js';
|
|
@@ -7,37 +8,80 @@ import { InMemoryTaskStore } from './task-store.js';
|
|
|
7
8
|
import { InMemoryPushNotificationStore, PushNotificationSender, validateWebhookUrl, } from './push-notifications.js';
|
|
8
9
|
import { isStreamFinalState } from './types.js';
|
|
9
10
|
import { isTaskVisibleTo, publicTask } from './ownership.js';
|
|
11
|
+
import { parseDeletePushNotificationConfigParams, parseGetPushNotificationConfigParams, parseListTasksParams, parseMessageSendParams, parseTaskIdParams, parseTaskPushNotificationConfig, parseTaskQueryParams, } from './protocol.js';
|
|
12
|
+
import { trimTrailingSlashes } from './url.js';
|
|
10
13
|
const MAX_LIST_LIMIT = 1000;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
/** The run time limit a client may ask for when neither the agent nor the server sets one: the Cogitator run default. */
|
|
15
|
+
const DEFAULT_MAX_RUN_TIMEOUT_MS = 120_000;
|
|
16
|
+
/** JSON-RPC methods answered with a stream of events (A2A v0.3, sections 7.2 and 7.9). */
|
|
17
|
+
export const STREAMING_METHODS = ['message/stream', 'tasks/resubscribe'];
|
|
18
|
+
function requestIdOf(body) {
|
|
19
|
+
if (body && typeof body === 'object' && !Array.isArray(body)) {
|
|
20
|
+
const id = body.id;
|
|
21
|
+
if (typeof id === 'string' || typeof id === 'number')
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
19
25
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
/** The MIME type a part is delivered as, for `acceptedOutputModes`. */
|
|
27
|
+
function partMimeType(part) {
|
|
28
|
+
switch (part.kind) {
|
|
29
|
+
case 'text':
|
|
30
|
+
return 'text/plain';
|
|
31
|
+
case 'data':
|
|
32
|
+
return 'application/json';
|
|
33
|
+
case 'file':
|
|
34
|
+
return part.file.mimeType ?? 'application/octet-stream';
|
|
35
|
+
}
|
|
25
36
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
/** The events of one task, queued until the stream that relays them takes them. */
|
|
38
|
+
class TaskEventQueue {
|
|
39
|
+
manager;
|
|
40
|
+
taskId;
|
|
41
|
+
queue = [];
|
|
42
|
+
wake = null;
|
|
43
|
+
listener = (event) => {
|
|
44
|
+
if (this.taskId !== null && event.taskId === this.taskId) {
|
|
45
|
+
this.queue.push(event);
|
|
46
|
+
this.notify();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
constructor(manager, taskId) {
|
|
50
|
+
this.manager = manager;
|
|
51
|
+
this.taskId = taskId;
|
|
52
|
+
manager.on('event', this.listener);
|
|
53
|
+
}
|
|
54
|
+
notify() {
|
|
55
|
+
const wake = this.wake;
|
|
56
|
+
this.wake = null;
|
|
57
|
+
wake?.();
|
|
58
|
+
}
|
|
59
|
+
shift() {
|
|
60
|
+
return this.queue.shift();
|
|
61
|
+
}
|
|
62
|
+
wait() {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
this.wake = resolve;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
close() {
|
|
68
|
+
this.manager.removeListener('event', this.listener);
|
|
69
|
+
this.notify();
|
|
31
70
|
}
|
|
32
71
|
}
|
|
33
72
|
export class A2AServer {
|
|
34
73
|
agents;
|
|
74
|
+
defaultAgentName;
|
|
35
75
|
cogitator;
|
|
36
76
|
taskManager;
|
|
37
|
-
agentCards;
|
|
38
77
|
/** The path the framework adapters serve JSON-RPC on, and the cards advertise without `cardUrl`. */
|
|
39
78
|
basePath;
|
|
79
|
+
/** How often the framework adapters write a heartbeat on an open stream; `0` for never */
|
|
80
|
+
sseHeartbeatMs;
|
|
40
81
|
cardUrl;
|
|
82
|
+
agentVersion;
|
|
83
|
+
provider;
|
|
84
|
+
maxRunTimeoutMs;
|
|
41
85
|
pushNotificationStore;
|
|
42
86
|
pushSender;
|
|
43
87
|
cardSigning;
|
|
@@ -50,12 +94,23 @@ export class A2AServer {
|
|
|
50
94
|
throw new Error('A2AServer requires at least one agent');
|
|
51
95
|
}
|
|
52
96
|
this.agents = config.agents;
|
|
97
|
+
this.defaultAgentName = agentNames[0];
|
|
53
98
|
this.cogitator = config.cogitator;
|
|
54
99
|
this.basePath = config.basePath ?? '/a2a';
|
|
55
100
|
if (!this.basePath.startsWith('/')) {
|
|
56
101
|
throw new Error(`A2AServer basePath must be a path starting with "/", got "${this.basePath}"`);
|
|
57
102
|
}
|
|
58
|
-
|
|
103
|
+
const maxRunTimeoutMs = config.maxRunTimeoutMs ?? DEFAULT_MAX_RUN_TIMEOUT_MS;
|
|
104
|
+
if (!Number.isInteger(maxRunTimeoutMs) ||
|
|
105
|
+
maxRunTimeoutMs <= 0 ||
|
|
106
|
+
maxRunTimeoutMs > MAX_RUN_TIMEOUT_MS) {
|
|
107
|
+
throw new Error(`A2AServer maxRunTimeoutMs must be a positive integer of at most ${MAX_RUN_TIMEOUT_MS}, got ${maxRunTimeoutMs}`);
|
|
108
|
+
}
|
|
109
|
+
this.maxRunTimeoutMs = maxRunTimeoutMs;
|
|
110
|
+
this.sseHeartbeatMs = resolveSseHeartbeatMs(config.sseHeartbeatMs);
|
|
111
|
+
this.cardUrl = config.cardUrl ? trimTrailingSlashes(config.cardUrl) : undefined;
|
|
112
|
+
this.agentVersion = config.agentVersion;
|
|
113
|
+
this.provider = config.provider;
|
|
59
114
|
this.cardSigning = config.cardSigning;
|
|
60
115
|
this.extendedCardGenerator = config.extendedCardGenerator;
|
|
61
116
|
this.allowPrivateUrls = config.allowPrivateUrls ?? false;
|
|
@@ -67,37 +122,13 @@ export class A2AServer {
|
|
|
67
122
|
taskStore: config.taskStore ?? new InMemoryTaskStore(),
|
|
68
123
|
});
|
|
69
124
|
this.taskManager.on('event', (event) => {
|
|
70
|
-
if (event.
|
|
71
|
-
|
|
72
|
-
|
|
125
|
+
if (event.kind !== 'status-update')
|
|
126
|
+
return;
|
|
127
|
+
this.taskManager
|
|
128
|
+
.getTask(event.taskId)
|
|
129
|
+
.then((task) => this.pushSender.notify(publicTask(task)))
|
|
130
|
+
.catch(() => { });
|
|
73
131
|
});
|
|
74
|
-
const hasPushNotifications = !!config.pushNotificationStore;
|
|
75
|
-
const hasExtendedCard = !!config.extendedCardGenerator;
|
|
76
|
-
this.agentCards = new Map();
|
|
77
|
-
for (const [name, agent] of Object.entries(this.agents)) {
|
|
78
|
-
const card = generateAgentCard(agent, {
|
|
79
|
-
url: this.cardUrl || this.basePath,
|
|
80
|
-
capabilities: {
|
|
81
|
-
streaming: true,
|
|
82
|
-
pushNotifications: hasPushNotifications,
|
|
83
|
-
extendedAgentCard: hasExtendedCard,
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
if (this.auth) {
|
|
87
|
-
const schemeName = this.auth.type === 'bearer' ? 'bearer' : 'apiKey';
|
|
88
|
-
card.securitySchemes = {
|
|
89
|
-
[schemeName]: this.auth.type === 'bearer'
|
|
90
|
-
? { type: 'http', scheme: 'bearer' }
|
|
91
|
-
: {
|
|
92
|
-
type: 'apiKey',
|
|
93
|
-
location: 'header',
|
|
94
|
-
parameterName: this.auth.headerName ?? 'x-api-key',
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
card.security = [{ [schemeName]: [] }];
|
|
98
|
-
}
|
|
99
|
-
this.agentCards.set(name, card);
|
|
100
|
-
}
|
|
101
132
|
}
|
|
102
133
|
/**
|
|
103
134
|
* Extract the credential configured by `auth` from request headers:
|
|
@@ -115,43 +146,65 @@ export class A2AServer {
|
|
|
115
146
|
const key = getHeader(this.auth.headerName ?? 'x-api-key');
|
|
116
147
|
return key ? key.trim() || undefined : undefined;
|
|
117
148
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const found = this.agentCards.get(agentName);
|
|
122
|
-
if (!found)
|
|
123
|
-
throw new A2AError(errors.agentNotFound(agentName));
|
|
124
|
-
card = found;
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
card = this.agentCards.values().next().value;
|
|
128
|
-
}
|
|
129
|
-
if (this.cardSigning) {
|
|
130
|
-
return signAgentCard(card, this.cardSigning);
|
|
131
|
-
}
|
|
132
|
-
return card;
|
|
149
|
+
/** The `WWW-Authenticate` challenge of a 401 response, when the auth scheme has one. */
|
|
150
|
+
get authChallenge() {
|
|
151
|
+
return this.auth?.type === 'bearer' ? 'Bearer realm="a2a"' : undefined;
|
|
133
152
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
/** Whether the server hosts an agent under this name. */
|
|
154
|
+
hasAgent(agentName) {
|
|
155
|
+
return Object.hasOwn(this.agents, agentName);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The Agent Card of an agent (the first one by default). Its `url` is the agent's JSON-RPC
|
|
159
|
+
* endpoint: `cardUrl`, or `baseUrl` plus `basePath`, and `/<agent name>` after it for every
|
|
160
|
+
* agent but the first.
|
|
161
|
+
*/
|
|
162
|
+
getAgentCard(agentName, options) {
|
|
163
|
+
const name = agentName ?? this.defaultAgentName;
|
|
164
|
+
if (!this.hasAgent(name))
|
|
165
|
+
throw new A2AError(errors.agentNotFound(name));
|
|
166
|
+
const card = generateAgentCard(this.agents[name], {
|
|
167
|
+
url: this.agentEndpoint(name, options?.baseUrl),
|
|
168
|
+
version: this.agentVersion,
|
|
169
|
+
provider: this.provider,
|
|
170
|
+
capabilities: { streaming: true, pushNotifications: true },
|
|
171
|
+
supportsAuthenticatedExtendedCard: !!this.extendedCardGenerator,
|
|
172
|
+
});
|
|
173
|
+
if (this.auth) {
|
|
174
|
+
const schemeName = this.auth.type === 'bearer' ? 'bearer' : 'apiKey';
|
|
175
|
+
card.securitySchemes = {
|
|
176
|
+
[schemeName]: this.auth.type === 'bearer'
|
|
177
|
+
? { type: 'http', scheme: 'bearer' }
|
|
178
|
+
: { type: 'apiKey', in: 'header', name: this.auth.headerName ?? 'x-api-key' },
|
|
179
|
+
};
|
|
180
|
+
card.security = [{ [schemeName]: [] }];
|
|
138
181
|
}
|
|
139
|
-
return
|
|
182
|
+
return this.signed(card);
|
|
183
|
+
}
|
|
184
|
+
/** The cards of every agent, in the order they were registered. */
|
|
185
|
+
getAgentCards(options) {
|
|
186
|
+
return Object.keys(this.agents).map((name) => this.getAgentCard(name, options));
|
|
187
|
+
}
|
|
188
|
+
agentEndpoint(agentName, baseUrl) {
|
|
189
|
+
const shared = this.cardUrl ?? `${trimTrailingSlashes(baseUrl ?? '')}${this.basePath}`;
|
|
190
|
+
return agentName === this.defaultAgentName
|
|
191
|
+
? shared
|
|
192
|
+
: `${trimTrailingSlashes(shared)}/${encodeURIComponent(agentName)}`;
|
|
193
|
+
}
|
|
194
|
+
signed(card) {
|
|
195
|
+
return this.cardSigning ? signAgentCard(card, this.cardSigning) : card;
|
|
140
196
|
}
|
|
141
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Answer a JSON-RPC request that is not streamed. Returns null for a notification (a request
|
|
199
|
+
* without `id`).
|
|
200
|
+
*/
|
|
201
|
+
async handleJsonRpc(body, authToken, options) {
|
|
142
202
|
let request;
|
|
143
203
|
try {
|
|
144
|
-
|
|
145
|
-
if (Array.isArray(parsed)) {
|
|
146
|
-
return createErrorResponse(null, errors.invalidRequest('Batch requests are not supported'));
|
|
147
|
-
}
|
|
148
|
-
request = parsed;
|
|
204
|
+
request = this.parseRequest(body);
|
|
149
205
|
}
|
|
150
206
|
catch (e) {
|
|
151
|
-
|
|
152
|
-
return createErrorResponse(null, e.code === -32600 ? errors.invalidRequest(e.message) : errors.parseError(e.message));
|
|
153
|
-
}
|
|
154
|
-
return createErrorResponse(null, errors.clientJsonRpcError(e, 'JSON-RPC parse error'));
|
|
207
|
+
return createErrorResponse(requestIdOf(body), errors.clientJsonRpcError(e, 'parse'));
|
|
155
208
|
}
|
|
156
209
|
let caller;
|
|
157
210
|
try {
|
|
@@ -163,7 +216,7 @@ export class A2AServer {
|
|
|
163
216
|
return createErrorResponse(request.id, errors.clientJsonRpcError(e, 'Authentication error'));
|
|
164
217
|
}
|
|
165
218
|
try {
|
|
166
|
-
const result = await this.routeMethod(request.method, request.params, caller);
|
|
219
|
+
const result = await this.routeMethod(request.method, request.params, caller, options);
|
|
167
220
|
if (request.id === undefined)
|
|
168
221
|
return null;
|
|
169
222
|
return createSuccessResponse(request.id, result);
|
|
@@ -174,153 +227,116 @@ export class A2AServer {
|
|
|
174
227
|
return createErrorResponse(request.id, errors.clientJsonRpcError(e, `${request.method} failed`));
|
|
175
228
|
}
|
|
176
229
|
}
|
|
177
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Answer a JSON-RPC request with a stream of JSON-RPC responses, each the data of one SSE
|
|
232
|
+
* event: for `message/stream` and `tasks/resubscribe` the task, then its status and artifact
|
|
233
|
+
* updates until the `final` one; a failure ends the stream with an error response. Any other
|
|
234
|
+
* method yields its single response.
|
|
235
|
+
*/
|
|
236
|
+
async *handleJsonRpcStream(body, authToken, signal, options) {
|
|
178
237
|
let request;
|
|
179
238
|
try {
|
|
180
|
-
|
|
181
|
-
if (Array.isArray(parsed)) {
|
|
182
|
-
yield failedStatusEvent('Batch requests are not supported');
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
request = parsed;
|
|
239
|
+
request = this.parseRequest(body);
|
|
186
240
|
}
|
|
187
241
|
catch (e) {
|
|
188
|
-
yield
|
|
242
|
+
yield createErrorResponse(requestIdOf(body), errors.clientJsonRpcError(e, 'parse'));
|
|
189
243
|
return;
|
|
190
244
|
}
|
|
245
|
+
const id = request.id ?? null;
|
|
191
246
|
let caller;
|
|
192
247
|
try {
|
|
193
248
|
caller = await this.authenticate(authToken);
|
|
194
249
|
}
|
|
195
250
|
catch (e) {
|
|
196
|
-
yield
|
|
251
|
+
yield createErrorResponse(id, errors.clientJsonRpcError(e, 'Authentication error'));
|
|
197
252
|
return;
|
|
198
253
|
}
|
|
199
|
-
if (request.method
|
|
200
|
-
|
|
254
|
+
if (!STREAMING_METHODS.includes(request.method)) {
|
|
255
|
+
const response = await this.handleJsonRpc(body, authToken, options);
|
|
256
|
+
if (response)
|
|
257
|
+
yield response;
|
|
201
258
|
return;
|
|
202
259
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
260
|
+
try {
|
|
261
|
+
const events = request.method === 'message/stream'
|
|
262
|
+
? this.streamMessage(request.params, caller, options, signal)
|
|
263
|
+
: this.resubscribe(request.params, caller, signal);
|
|
264
|
+
for await (const event of events) {
|
|
265
|
+
yield createSuccessResponse(id, event);
|
|
266
|
+
}
|
|
207
267
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
yield failedStatusEvent(`Agent not found: ${agentName}`);
|
|
213
|
-
return;
|
|
268
|
+
catch (e) {
|
|
269
|
+
if (signal?.aborted)
|
|
270
|
+
return;
|
|
271
|
+
yield createErrorResponse(id, errors.clientJsonRpcError(e, `${request.method} failed`));
|
|
214
272
|
}
|
|
273
|
+
}
|
|
274
|
+
parseRequest(body) {
|
|
275
|
+
const parsed = parseJsonRpcRequest(body);
|
|
276
|
+
if (Array.isArray(parsed)) {
|
|
277
|
+
throw new A2AError(errors.invalidRequest('Batch requests are not supported'));
|
|
278
|
+
}
|
|
279
|
+
return parsed;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The events of `message/stream`: the task as it starts, then its updates until the final
|
|
283
|
+
* status update.
|
|
284
|
+
*/
|
|
285
|
+
async *streamMessage(rawParams, caller, options, signal) {
|
|
286
|
+
const params = parseMessageSendParams(rawParams);
|
|
287
|
+
const { agentName, agent } = this.resolveAgent(params, options);
|
|
288
|
+
const { message, configuration } = params;
|
|
289
|
+
this.validateInitialPushConfig(configuration);
|
|
215
290
|
if (signal?.aborted)
|
|
216
291
|
return;
|
|
217
|
-
const
|
|
218
|
-
let wake = null;
|
|
219
|
-
const notify = () => {
|
|
220
|
-
if (wake) {
|
|
221
|
-
wake();
|
|
222
|
-
wake = null;
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
let taskId = message.taskId ?? null;
|
|
226
|
-
const onEvent = (event) => {
|
|
227
|
-
if (taskId && event.taskId === taskId) {
|
|
228
|
-
eventQueue.push(event);
|
|
229
|
-
notify();
|
|
230
|
-
}
|
|
231
|
-
};
|
|
292
|
+
const queue = new TaskEventQueue(this.taskManager, message.taskId ?? null);
|
|
232
293
|
let executingTaskId = null;
|
|
294
|
+
let executionPromise = null;
|
|
233
295
|
const onAbort = () => {
|
|
234
296
|
if (executingTaskId)
|
|
235
297
|
this.taskManager.abortExecution(executingTaskId);
|
|
236
|
-
notify();
|
|
298
|
+
queue.notify();
|
|
237
299
|
};
|
|
238
|
-
this.taskManager.on('event', onEvent);
|
|
239
300
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
240
|
-
let executionPromise = null;
|
|
241
301
|
try {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
this.validateInitialPushConfig(params.configuration);
|
|
246
|
-
if (isContinued) {
|
|
247
|
-
await this.visibleTask(message.taskId, caller);
|
|
248
|
-
task = await this.taskManager.continueTask(message.taskId, message);
|
|
249
|
-
}
|
|
250
|
-
else {
|
|
251
|
-
await this.assertContextAvailable(message.contextId, caller);
|
|
252
|
-
task = await this.taskManager.createTask(message, message.contextId, caller?.userId);
|
|
253
|
-
taskId = task.id;
|
|
254
|
-
await this.registerInitialPushConfig(task.id, params.configuration);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
catch (error) {
|
|
258
|
-
yield failedStatusEvent(errors.clientErrorMessage(error, 'message/stream failed'), taskId ?? '');
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
const currentTaskId = task.id;
|
|
262
|
-
const onToken = (token) => {
|
|
263
|
-
const event = {
|
|
264
|
-
type: 'token',
|
|
265
|
-
taskId: currentTaskId,
|
|
266
|
-
token,
|
|
267
|
-
timestamp: new Date().toISOString(),
|
|
268
|
-
};
|
|
269
|
-
eventQueue.push(event);
|
|
270
|
-
notify();
|
|
271
|
-
};
|
|
302
|
+
const task = await this.startTask(params, caller);
|
|
303
|
+
queue.taskId = task.id;
|
|
272
304
|
let executionDone = false;
|
|
273
305
|
let executionError;
|
|
274
306
|
executingTaskId = task.id;
|
|
275
307
|
executionPromise = this.taskManager
|
|
276
308
|
.executeTask(task, this.cogitator, agent, message, {
|
|
277
|
-
|
|
278
|
-
timeout:
|
|
309
|
+
stream: true,
|
|
310
|
+
timeout: this.runTimeout(agent, configuration?.timeout),
|
|
279
311
|
...(caller && { userId: caller.userId }),
|
|
280
312
|
})
|
|
281
313
|
.then(() => {
|
|
282
314
|
executionDone = true;
|
|
283
|
-
notify();
|
|
315
|
+
queue.notify();
|
|
284
316
|
}, (error) => {
|
|
285
317
|
executionError = error;
|
|
286
318
|
executionDone = true;
|
|
287
|
-
notify();
|
|
319
|
+
queue.notify();
|
|
288
320
|
});
|
|
289
|
-
|
|
290
|
-
yield {
|
|
291
|
-
type: 'status-update',
|
|
292
|
-
taskId: task.id,
|
|
293
|
-
status: task.status,
|
|
294
|
-
timestamp: new Date().toISOString(),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
321
|
+
yield this.shapeTask(task, configuration);
|
|
297
322
|
while (!signal?.aborted) {
|
|
298
|
-
const event =
|
|
323
|
+
const event = queue.shift();
|
|
299
324
|
if (event) {
|
|
300
325
|
yield event;
|
|
301
|
-
if (event.
|
|
326
|
+
if (event.kind === 'status-update' && event.final)
|
|
302
327
|
return;
|
|
303
|
-
}
|
|
304
328
|
continue;
|
|
305
329
|
}
|
|
306
330
|
if (executionDone) {
|
|
307
|
-
if (executionError !== undefined)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
const finalTask = await this.taskManager.getTask(task.id);
|
|
312
|
-
if (!isStreamFinalState(finalTask.status.state)) {
|
|
313
|
-
yield failedStatusEvent('Execution ended without reaching a terminal state', task.id);
|
|
314
|
-
}
|
|
315
|
-
return;
|
|
331
|
+
if (executionError !== undefined)
|
|
332
|
+
throw executionError;
|
|
333
|
+
throw new A2AError(errors.internalError(`Task ${task.id} of ${agentName} ended without a final status`));
|
|
316
334
|
}
|
|
317
|
-
await
|
|
318
|
-
wake = r;
|
|
319
|
-
});
|
|
335
|
+
await queue.wait();
|
|
320
336
|
}
|
|
321
337
|
}
|
|
322
338
|
finally {
|
|
323
|
-
|
|
339
|
+
queue.close();
|
|
324
340
|
signal?.removeEventListener('abort', onAbort);
|
|
325
341
|
if (executingTaskId)
|
|
326
342
|
this.taskManager.abortExecution(executingTaskId);
|
|
@@ -328,6 +344,38 @@ export class A2AServer {
|
|
|
328
344
|
await executionPromise;
|
|
329
345
|
}
|
|
330
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* The events of `tasks/resubscribe`: the task as it is now, then, while it still runs in this
|
|
349
|
+
* process, its updates until the final status update.
|
|
350
|
+
*/
|
|
351
|
+
async *resubscribe(rawParams, caller, signal) {
|
|
352
|
+
const { id } = parseTaskIdParams(rawParams);
|
|
353
|
+
const queue = new TaskEventQueue(this.taskManager, id);
|
|
354
|
+
const onAbort = () => queue.notify();
|
|
355
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
356
|
+
try {
|
|
357
|
+
const task = await this.visibleTask(id, caller);
|
|
358
|
+
yield this.shapeTask(task);
|
|
359
|
+
if (isStreamFinalState(task.status.state))
|
|
360
|
+
return;
|
|
361
|
+
while (!signal?.aborted) {
|
|
362
|
+
const event = queue.shift();
|
|
363
|
+
if (event) {
|
|
364
|
+
yield event;
|
|
365
|
+
if (event.kind === 'status-update' && event.final)
|
|
366
|
+
return;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (!this.taskManager.isExecuting(id))
|
|
370
|
+
return;
|
|
371
|
+
await queue.wait();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
queue.close();
|
|
376
|
+
signal?.removeEventListener('abort', onAbort);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
331
379
|
/**
|
|
332
380
|
* The caller of a request: undefined when there is no auth or `validate`
|
|
333
381
|
* admitted it without a user. Throws `unauthorized` otherwise.
|
|
@@ -361,56 +409,76 @@ export class A2AServer {
|
|
|
361
409
|
throw new A2AError(errors.invalidParams(`Unknown contextId: ${contextId}`));
|
|
362
410
|
}
|
|
363
411
|
}
|
|
364
|
-
|
|
412
|
+
resolveAgent(params, options) {
|
|
413
|
+
const agentName = options?.agentName ?? params.agentName ?? this.defaultAgentName;
|
|
414
|
+
if (!this.hasAgent(agentName))
|
|
415
|
+
throw new A2AError(errors.agentNotFound(agentName));
|
|
416
|
+
return { agentName, agent: this.agents[agentName] };
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* The run time limit for a task: none of its own unless the client asks for one, and what the
|
|
420
|
+
* client asks for never beyond the agent's `timeout`, or `maxRunTimeoutMs` for an agent
|
|
421
|
+
* without one. A client can shorten a run, never lift or remove the operator's limit.
|
|
422
|
+
*/
|
|
423
|
+
runTimeout(agent, requested) {
|
|
424
|
+
if (requested === undefined)
|
|
425
|
+
return undefined;
|
|
426
|
+
const agentTimeout = agent.config?.timeout;
|
|
427
|
+
const ceiling = agentTimeout !== undefined && agentTimeout > 0
|
|
428
|
+
? Math.min(agentTimeout, MAX_RUN_TIMEOUT_MS)
|
|
429
|
+
: this.maxRunTimeoutMs;
|
|
430
|
+
return Math.min(requested, ceiling);
|
|
431
|
+
}
|
|
432
|
+
/** A new task for the message, or the task it continues, ready to execute. */
|
|
433
|
+
async startTask(params, caller) {
|
|
434
|
+
const { message, configuration } = params;
|
|
435
|
+
if (message.taskId) {
|
|
436
|
+
const existing = await this.visibleTask(message.taskId, caller);
|
|
437
|
+
if (message.contextId && message.contextId !== existing.contextId) {
|
|
438
|
+
throw new A2AError(errors.invalidParams(`contextId does not match the context of task ${existing.id}`));
|
|
439
|
+
}
|
|
440
|
+
return this.taskManager.continueTask(message.taskId, message);
|
|
441
|
+
}
|
|
442
|
+
await this.assertContextAvailable(message.contextId, caller);
|
|
443
|
+
const task = await this.taskManager.createTask(message, message.contextId, caller?.userId);
|
|
444
|
+
await this.registerInitialPushConfig(task.id, configuration);
|
|
445
|
+
return task;
|
|
446
|
+
}
|
|
447
|
+
async routeMethod(method, params, caller, options) {
|
|
365
448
|
switch (method) {
|
|
366
449
|
case 'message/send':
|
|
367
|
-
return this.handleSendMessage(params, caller);
|
|
450
|
+
return this.handleSendMessage(params, caller, options);
|
|
368
451
|
case 'message/stream':
|
|
369
|
-
|
|
452
|
+
case 'tasks/resubscribe':
|
|
453
|
+
throw new A2AError(errors.unsupportedOperation(`${method} streams: use handleJsonRpcStream`));
|
|
370
454
|
case 'tasks/get':
|
|
371
455
|
return this.handleGetTask(params, caller);
|
|
372
456
|
case 'tasks/cancel':
|
|
373
457
|
return this.handleCancelTask(params, caller);
|
|
374
458
|
case 'tasks/list':
|
|
375
459
|
return this.handleListTasks(params, caller);
|
|
376
|
-
case 'tasks/
|
|
377
|
-
return this.
|
|
378
|
-
case 'tasks/
|
|
379
|
-
return this.
|
|
380
|
-
case 'tasks/
|
|
381
|
-
return this.
|
|
382
|
-
case 'tasks/
|
|
383
|
-
return this.
|
|
384
|
-
case 'agent/
|
|
385
|
-
return this.handleExtendedCard(params);
|
|
460
|
+
case 'tasks/pushNotificationConfig/set':
|
|
461
|
+
return this.handleSetPushNotificationConfig(params, caller);
|
|
462
|
+
case 'tasks/pushNotificationConfig/get':
|
|
463
|
+
return this.handleGetPushNotificationConfig(params, caller);
|
|
464
|
+
case 'tasks/pushNotificationConfig/list':
|
|
465
|
+
return this.handleListPushNotificationConfigs(params, caller);
|
|
466
|
+
case 'tasks/pushNotificationConfig/delete':
|
|
467
|
+
return this.handleDeletePushNotificationConfig(params, caller);
|
|
468
|
+
case 'agent/getAuthenticatedExtendedCard':
|
|
469
|
+
return this.handleExtendedCard(params, options);
|
|
386
470
|
default:
|
|
387
471
|
throw new A2AError(errors.methodNotFound(method));
|
|
388
472
|
}
|
|
389
473
|
}
|
|
390
|
-
async handleSendMessage(
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
assertOptionalNonNegativeInt(configuration?.historyLength, 'configuration.historyLength');
|
|
396
|
-
assertOptionalNonNegativeInt(configuration?.timeout, 'configuration.timeout');
|
|
397
|
-
const resolvedAgentName = agentName ?? Object.keys(this.agents)[0];
|
|
398
|
-
const agent = this.agents[resolvedAgentName];
|
|
399
|
-
if (!agent)
|
|
400
|
-
throw new A2AError(errors.agentNotFound(resolvedAgentName));
|
|
474
|
+
async handleSendMessage(rawParams, caller, options) {
|
|
475
|
+
const params = parseMessageSendParams(rawParams);
|
|
476
|
+
const { agent } = this.resolveAgent(params, options);
|
|
477
|
+
const { message, configuration } = params;
|
|
401
478
|
this.validateInitialPushConfig(configuration);
|
|
402
|
-
|
|
403
|
-
if (message.taskId) {
|
|
404
|
-
await this.visibleTask(message.taskId, caller);
|
|
405
|
-
task = await this.taskManager.continueTask(message.taskId, message);
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
await this.assertContextAvailable(message.contextId, caller);
|
|
409
|
-
task = await this.taskManager.createTask(message, message.contextId, caller?.userId);
|
|
410
|
-
await this.registerInitialPushConfig(task.id, configuration);
|
|
411
|
-
}
|
|
479
|
+
const task = await this.startTask(params, caller);
|
|
412
480
|
const execution = this.taskManager.executeTask(task, this.cogitator, agent, message, {
|
|
413
|
-
timeout: configuration?.timeout,
|
|
481
|
+
timeout: this.runTimeout(agent, configuration?.timeout),
|
|
414
482
|
...(caller && { userId: caller.userId }),
|
|
415
483
|
});
|
|
416
484
|
if (configuration?.blocking === false) {
|
|
@@ -423,12 +491,8 @@ export class A2AServer {
|
|
|
423
491
|
}
|
|
424
492
|
validateInitialPushConfig(configuration) {
|
|
425
493
|
const pushConfig = configuration?.pushNotificationConfig;
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
if (!pushConfig.webhookUrl) {
|
|
429
|
-
throw new A2AError(errors.invalidParams('pushNotificationConfig.webhookUrl is required'));
|
|
430
|
-
}
|
|
431
|
-
this.assertWebhookAllowed(pushConfig.webhookUrl);
|
|
494
|
+
if (pushConfig)
|
|
495
|
+
this.assertWebhookAllowed(pushConfig.url);
|
|
432
496
|
}
|
|
433
497
|
async registerInitialPushConfig(taskId, configuration) {
|
|
434
498
|
const pushConfig = configuration?.pushNotificationConfig;
|
|
@@ -453,44 +517,36 @@ export class A2AServer {
|
|
|
453
517
|
}
|
|
454
518
|
}
|
|
455
519
|
/**
|
|
456
|
-
*
|
|
520
|
+
* The task as the client receives it, with `historyLength` and `acceptedOutputModes` applied.
|
|
457
521
|
*/
|
|
458
522
|
shapeTask(task, configuration) {
|
|
459
523
|
let shaped = publicTask(task);
|
|
460
524
|
const historyLength = configuration?.historyLength;
|
|
461
525
|
if (historyLength !== undefined) {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
history: historyLength === 0 ? [] : shaped.history.slice(-historyLength),
|
|
465
|
-
};
|
|
526
|
+
const history = shaped.history ?? [];
|
|
527
|
+
shaped = { ...shaped, history: historyLength === 0 ? [] : history.slice(-historyLength) };
|
|
466
528
|
}
|
|
467
529
|
const accepted = configuration?.acceptedOutputModes;
|
|
468
530
|
if (accepted && accepted.length > 0) {
|
|
469
531
|
shaped = {
|
|
470
532
|
...shaped,
|
|
471
|
-
artifacts: shaped.artifacts.filter((
|
|
533
|
+
artifacts: (shaped.artifacts ?? []).filter((artifact) => artifact.parts.some((part) => accepted.includes(partMimeType(part)))),
|
|
472
534
|
};
|
|
473
535
|
}
|
|
474
536
|
return shaped;
|
|
475
537
|
}
|
|
476
538
|
async handleGetTask(params, caller) {
|
|
477
|
-
const { id, historyLength } = (params
|
|
478
|
-
if (!id)
|
|
479
|
-
throw new A2AError(errors.invalidParams('id is required'));
|
|
480
|
-
assertOptionalNonNegativeInt(historyLength, 'historyLength');
|
|
539
|
+
const { id, historyLength } = parseTaskQueryParams(params);
|
|
481
540
|
return this.shapeTask(await this.visibleTask(id, caller), { historyLength });
|
|
482
541
|
}
|
|
483
542
|
async handleCancelTask(params, caller) {
|
|
484
|
-
const { id } = (params
|
|
485
|
-
if (!id)
|
|
486
|
-
throw new A2AError(errors.invalidParams('id is required'));
|
|
543
|
+
const { id } = parseTaskIdParams(params);
|
|
487
544
|
await this.visibleTask(id, caller);
|
|
488
545
|
return publicTask(await this.taskManager.cancelTask(id));
|
|
489
546
|
}
|
|
547
|
+
/** Cogitator extension `tasks/list`: the caller's tasks, newest first. */
|
|
490
548
|
async handleListTasks(params, caller) {
|
|
491
|
-
const raw = (params
|
|
492
|
-
assertOptionalNonNegativeInt(raw.limit, 'limit');
|
|
493
|
-
assertOptionalNonNegativeInt(raw.offset, 'offset');
|
|
549
|
+
const raw = parseListTasksParams(params);
|
|
494
550
|
const filter = {
|
|
495
551
|
contextId: raw.contextId,
|
|
496
552
|
state: raw.state,
|
|
@@ -501,49 +557,47 @@ export class A2AServer {
|
|
|
501
557
|
const tasks = await this.taskManager.listTasks(filter);
|
|
502
558
|
return { tasks: tasks.map(publicTask) };
|
|
503
559
|
}
|
|
504
|
-
async
|
|
505
|
-
const { taskId,
|
|
506
|
-
|
|
507
|
-
throw new A2AError(errors.invalidParams('taskId is required'));
|
|
508
|
-
if (!config?.webhookUrl)
|
|
509
|
-
throw new A2AError(errors.invalidParams('config.webhookUrl is required'));
|
|
510
|
-
this.assertWebhookAllowed(config.webhookUrl);
|
|
560
|
+
async handleSetPushNotificationConfig(params, caller) {
|
|
561
|
+
const { taskId, pushNotificationConfig } = parseTaskPushNotificationConfig(params);
|
|
562
|
+
this.assertWebhookAllowed(pushNotificationConfig.url);
|
|
511
563
|
await this.visibleTask(taskId, caller);
|
|
512
|
-
|
|
564
|
+
const stored = await this.pushNotificationStore.create(taskId, pushNotificationConfig);
|
|
565
|
+
return { taskId, pushNotificationConfig: stored };
|
|
513
566
|
}
|
|
514
|
-
async
|
|
515
|
-
const {
|
|
516
|
-
|
|
517
|
-
|
|
567
|
+
async handleGetPushNotificationConfig(params, caller) {
|
|
568
|
+
const { id, pushNotificationConfigId } = parseGetPushNotificationConfigParams(params);
|
|
569
|
+
await this.visibleTask(id, caller);
|
|
570
|
+
const config = pushNotificationConfigId
|
|
571
|
+
? await this.pushNotificationStore.get(id, pushNotificationConfigId)
|
|
572
|
+
: ((await this.pushNotificationStore.list(id))[0] ?? null);
|
|
573
|
+
if (!config) {
|
|
574
|
+
throw new A2AError(errors.pushNotificationConfigNotFound(id, pushNotificationConfigId ?? id));
|
|
518
575
|
}
|
|
519
|
-
|
|
520
|
-
return this.pushNotificationStore.get(taskId, configId);
|
|
576
|
+
return { taskId: id, pushNotificationConfig: config };
|
|
521
577
|
}
|
|
522
|
-
async
|
|
523
|
-
const {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
return this.pushNotificationStore.list(taskId);
|
|
578
|
+
async handleListPushNotificationConfigs(params, caller) {
|
|
579
|
+
const { id } = parseTaskIdParams(params);
|
|
580
|
+
await this.visibleTask(id, caller);
|
|
581
|
+
const configs = await this.pushNotificationStore.list(id);
|
|
582
|
+
return configs.map((pushNotificationConfig) => ({ taskId: id, pushNotificationConfig }));
|
|
528
583
|
}
|
|
529
|
-
async
|
|
530
|
-
const {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
await this.visibleTask(taskId, caller);
|
|
535
|
-
await this.pushNotificationStore.delete(taskId, configId);
|
|
536
|
-
return { success: true };
|
|
584
|
+
async handleDeletePushNotificationConfig(params, caller) {
|
|
585
|
+
const { id, pushNotificationConfigId } = parseDeletePushNotificationConfigParams(params);
|
|
586
|
+
await this.visibleTask(id, caller);
|
|
587
|
+
await this.pushNotificationStore.delete(id, pushNotificationConfigId);
|
|
588
|
+
return null;
|
|
537
589
|
}
|
|
538
|
-
async handleExtendedCard(params) {
|
|
590
|
+
async handleExtendedCard(params, options) {
|
|
539
591
|
if (!this.extendedCardGenerator) {
|
|
540
|
-
throw new A2AError(errors.
|
|
592
|
+
throw new A2AError(errors.authenticatedExtendedCardNotConfigured());
|
|
541
593
|
}
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
594
|
+
const requested = params && typeof params === 'object' && !Array.isArray(params)
|
|
595
|
+
? params.agentName
|
|
596
|
+
: undefined;
|
|
597
|
+
const name = options?.agentName ?? (typeof requested === 'string' ? requested : this.defaultAgentName);
|
|
598
|
+
if (!this.hasAgent(name))
|
|
545
599
|
throw new A2AError(errors.agentNotFound(name));
|
|
546
|
-
return this.extendedCardGenerator(name);
|
|
600
|
+
return this.signed(this.extendedCardGenerator(name));
|
|
547
601
|
}
|
|
548
602
|
}
|
|
549
603
|
//# sourceMappingURL=server.js.map
|