@cogitator-ai/a2a 0.7.8 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +173 -151
  2. package/dist/adapters/express.d.ts +5 -0
  3. package/dist/adapters/express.d.ts.map +1 -1
  4. package/dist/adapters/express.js +53 -30
  5. package/dist/adapters/express.js.map +1 -1
  6. package/dist/adapters/fastify.d.ts +5 -0
  7. package/dist/adapters/fastify.d.ts.map +1 -1
  8. package/dist/adapters/fastify.js +51 -35
  9. package/dist/adapters/fastify.js.map +1 -1
  10. package/dist/adapters/hono.d.ts +5 -0
  11. package/dist/adapters/hono.d.ts.map +1 -1
  12. package/dist/adapters/hono.js +41 -49
  13. package/dist/adapters/hono.js.map +1 -1
  14. package/dist/adapters/koa.d.ts +5 -0
  15. package/dist/adapters/koa.d.ts.map +1 -1
  16. package/dist/adapters/koa.js +39 -32
  17. package/dist/adapters/koa.js.map +1 -1
  18. package/dist/adapters/next.d.ts +13 -2
  19. package/dist/adapters/next.d.ts.map +1 -1
  20. package/dist/adapters/next.js +75 -51
  21. package/dist/adapters/next.js.map +1 -1
  22. package/dist/adapters/shared.d.ts +43 -10
  23. package/dist/adapters/shared.d.ts.map +1 -1
  24. package/dist/adapters/shared.js +156 -23
  25. package/dist/adapters/shared.js.map +1 -1
  26. package/dist/agent-card.d.ts +22 -7
  27. package/dist/agent-card.d.ts.map +1 -1
  28. package/dist/agent-card.js +71 -34
  29. package/dist/agent-card.js.map +1 -1
  30. package/dist/approvals.d.ts +46 -0
  31. package/dist/approvals.d.ts.map +1 -0
  32. package/dist/approvals.js +105 -0
  33. package/dist/approvals.js.map +1 -0
  34. package/dist/client.d.ts +59 -17
  35. package/dist/client.d.ts.map +1 -1
  36. package/dist/client.js +237 -142
  37. package/dist/client.js.map +1 -1
  38. package/dist/errors.d.ts +24 -1
  39. package/dist/errors.d.ts.map +1 -1
  40. package/dist/errors.js +90 -19
  41. package/dist/errors.js.map +1 -1
  42. package/dist/index.d.ts +11 -7
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +7 -5
  45. package/dist/index.js.map +1 -1
  46. package/dist/ownership.d.ts +7 -1
  47. package/dist/ownership.d.ts.map +1 -1
  48. package/dist/ownership.js +24 -3
  49. package/dist/ownership.js.map +1 -1
  50. package/dist/protocol.d.ts +70 -0
  51. package/dist/protocol.d.ts.map +1 -0
  52. package/dist/protocol.js +187 -0
  53. package/dist/protocol.js.map +1 -0
  54. package/dist/push-notifications.d.ts +5 -2
  55. package/dist/push-notifications.d.ts.map +1 -1
  56. package/dist/push-notifications.js +33 -23
  57. package/dist/push-notifications.js.map +1 -1
  58. package/dist/redis-task-store.js +3 -3
  59. package/dist/redis-task-store.js.map +1 -1
  60. package/dist/server.d.ts +75 -12
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +324 -270
  63. package/dist/server.js.map +1 -1
  64. package/dist/task-manager.d.ts +57 -11
  65. package/dist/task-manager.d.ts.map +1 -1
  66. package/dist/task-manager.js +264 -116
  67. package/dist/task-manager.js.map +1 -1
  68. package/dist/task-store.js +4 -4
  69. package/dist/task-store.js.map +1 -1
  70. package/dist/types.d.ts +226 -85
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js +10 -3
  73. package/dist/types.js.map +1 -1
  74. package/dist/url.d.ts +7 -0
  75. package/dist/url.d.ts.map +1 -0
  76. package/dist/url.js +12 -0
  77. package/dist/url.js.map +1 -0
  78. package/package.json +19 -9
  79. package/dist/adapters/sse-error-event.d.ts +0 -3
  80. package/dist/adapters/sse-error-event.d.ts.map +0 -1
  81. package/dist/adapters/sse-error-event.js +0 -12
  82. package/dist/adapters/sse-error-event.js.map +0 -1
package/dist/server.js CHANGED
@@ -1,4 +1,5 @@
1
- import { parseJsonRpcRequest, createSuccessResponse, createErrorResponse, JsonRpcParseError, } from './json-rpc.js';
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
- function failedStatusEvent(message, taskId = '') {
12
- const timestamp = new Date().toISOString();
13
- return {
14
- type: 'status-update',
15
- taskId,
16
- status: { state: 'failed', timestamp, message },
17
- timestamp,
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
- function isValidMessage(message) {
21
- if (!message || typeof message !== 'object')
22
- return false;
23
- const m = message;
24
- return (m.role === 'user' || m.role === 'agent') && Array.isArray(m.parts);
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
- function assertOptionalNonNegativeInt(value, name) {
27
- if (value === undefined)
28
- return;
29
- if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
30
- throw new A2AError(errors.invalidParams(`${name} must be a non-negative integer`));
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
- this.cardUrl = config.cardUrl ?? '';
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.type === 'status-update' || event.type === 'artifact-update') {
71
- this.pushSender.notify(event.taskId, event).catch(() => { });
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
- getAgentCard(agentName) {
119
- let card;
120
- if (agentName) {
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
- getAgentCards() {
135
- const cards = Array.from(this.agentCards.values());
136
- if (this.cardSigning) {
137
- return cards.map((c) => signAgentCard(c, this.cardSigning));
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 cards;
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
- async handleJsonRpc(body, authToken) {
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
- const parsed = parseJsonRpcRequest(body);
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
- if (e instanceof JsonRpcParseError) {
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
- async *handleJsonRpcStream(body, authToken, signal) {
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
- const parsed = parseJsonRpcRequest(body);
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 failedStatusEvent(errors.clientErrorMessage(e, 'JSON-RPC parse error'));
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 failedStatusEvent(errors.clientErrorMessage(e, 'Authentication error'));
251
+ yield createErrorResponse(id, errors.clientJsonRpcError(e, 'Authentication error'));
197
252
  return;
198
253
  }
199
- if (request.method !== 'message/stream') {
200
- yield failedStatusEvent(`Unsupported method for streaming: ${request.method}`);
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
- const params = request.params;
204
- if (!params || !isValidMessage(params.message)) {
205
- yield failedStatusEvent('Missing required parameter: message with role and parts');
206
- return;
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
- const message = params.message;
209
- const agentName = params.agentName ?? Object.keys(this.agents)[0];
210
- const agent = this.agents[agentName];
211
- if (!agent) {
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 eventQueue = [];
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
- let task;
243
- const isContinued = !!message.taskId;
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
- onToken,
278
- timeout: params.configuration?.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
- if (!isContinued) {
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 = eventQueue.shift();
323
+ const event = queue.shift();
299
324
  if (event) {
300
325
  yield event;
301
- if (event.type === 'status-update' && isStreamFinalState(event.status.state)) {
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
- yield failedStatusEvent(errors.clientErrorMessage(executionError, `Task ${task.id} failed`), task.id);
309
- return;
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 new Promise((r) => {
318
- wake = r;
319
- });
335
+ await queue.wait();
320
336
  }
321
337
  }
322
338
  finally {
323
- this.taskManager.removeListener('event', onEvent);
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
- async routeMethod(method, params, caller) {
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
- throw new A2AError(errors.unsupportedOperation('Use handleJsonRpcStream for streaming'));
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/pushNotification/create':
377
- return this.handleCreatePushNotification(params, caller);
378
- case 'tasks/pushNotification/get':
379
- return this.handleGetPushNotification(params, caller);
380
- case 'tasks/pushNotification/list':
381
- return this.handleListPushNotifications(params, caller);
382
- case 'tasks/pushNotification/delete':
383
- return this.handleDeletePushNotification(params, caller);
384
- case 'agent/extendedCard':
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(params, caller) {
391
- const { message, agentName, configuration } = (params ?? {});
392
- if (!isValidMessage(message)) {
393
- throw new A2AError(errors.invalidParams('message is required with role and parts'));
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
- let task;
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 (!pushConfig)
427
- return;
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
- * Apply historyLength / acceptedOutputModes from the request configuration
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
- shaped = {
463
- ...shaped,
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((a) => !a.mimeType || accepted.includes(a.mimeType)),
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 handleCreatePushNotification(params, caller) {
505
- const { taskId, config } = (params ?? {});
506
- if (!taskId)
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
- return this.pushNotificationStore.create(taskId, config);
564
+ const stored = await this.pushNotificationStore.create(taskId, pushNotificationConfig);
565
+ return { taskId, pushNotificationConfig: stored };
513
566
  }
514
- async handleGetPushNotification(params, caller) {
515
- const { taskId, configId } = (params ?? {});
516
- if (!taskId || !configId) {
517
- throw new A2AError(errors.invalidParams('taskId and configId are required'));
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
- await this.visibleTask(taskId, caller);
520
- return this.pushNotificationStore.get(taskId, configId);
576
+ return { taskId: id, pushNotificationConfig: config };
521
577
  }
522
- async handleListPushNotifications(params, caller) {
523
- const { taskId } = (params ?? {});
524
- if (!taskId)
525
- throw new A2AError(errors.invalidParams('taskId is required'));
526
- await this.visibleTask(taskId, caller);
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 handleDeletePushNotification(params, caller) {
530
- const { taskId, configId } = (params ?? {});
531
- if (!taskId || !configId) {
532
- throw new A2AError(errors.invalidParams('taskId and configId are required'));
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.unsupportedOperation('Extended agent card is not configured'));
592
+ throw new A2AError(errors.authenticatedExtendedCardNotConfigured());
541
593
  }
542
- const { agentName } = (params ?? {});
543
- const name = agentName ?? Object.keys(this.agents)[0];
544
- if (!this.agents[name])
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