• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

guglielmo-san / a2a-js / 19853305123

02 Dec 2025 09:15AM UTC coverage: 88.035% (-0.3%) from 88.294%
19853305123

Pull #7

github

web-flow
Merge 2fae786b2 into d08f72852
Pull Request #7: feat: add more lines not covered

481 of 577 branches covered (83.36%)

Branch coverage included in aggregate %.

8 of 15 new or added lines in 1 file covered. (53.33%)

1 existing line in 1 file now uncovered.

2955 of 3326 relevant lines covered (88.85%)

11.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.9
/src/server/request_handler/default_request_handler.ts
1
import { v4 as uuidv4 } from 'uuid'; // For generating unique IDs
1✔
2

1✔
3
import {
1✔
4
  Message,
1✔
5
  AgentCard,
1✔
6
  Task,
1✔
7
  MessageSendParams,
1✔
8
  TaskState,
1✔
9
  TaskStatusUpdateEvent,
1✔
10
  TaskArtifactUpdateEvent,
1✔
11
  TaskQueryParams,
1✔
12
  TaskIdParams,
1✔
13
  TaskPushNotificationConfig,
1✔
14
  DeleteTaskPushNotificationConfigParams,
1✔
15
  GetTaskPushNotificationConfigParams,
1✔
16
  ListTaskPushNotificationConfigParams,
1✔
17
} from '../../types.js';
1✔
18
import { AgentExecutor } from '../agent_execution/agent_executor.js';
1✔
19
import { RequestContext } from '../agent_execution/request_context.js';
1✔
20
import { A2AError } from '../error.js';
1✔
21
import {
1✔
22
  ExecutionEventBusManager,
1✔
23
  DefaultExecutionEventBusManager,
1✔
24
} from '../events/execution_event_bus_manager.js';
1✔
25
import { AgentExecutionEvent } from '../events/execution_event_bus.js';
1✔
26
import { ExecutionEventQueue } from '../events/execution_event_queue.js';
1✔
27
import { ResultManager } from '../result_manager.js';
1✔
28
import { TaskStore } from '../store.js';
1✔
29
import { A2ARequestHandler } from './a2a_request_handler.js';
1✔
30
import {
1✔
31
  InMemoryPushNotificationStore,
1✔
32
  PushNotificationStore,
1✔
33
} from '../push_notification/push_notification_store.js';
1✔
34
import { PushNotificationSender } from '../push_notification/push_notification_sender.js';
1✔
35
import { DefaultPushNotificationSender } from '../push_notification/default_push_notification_sender.js';
1✔
36
import { ServerCallContext } from '../context.js';
1✔
37

1✔
38
const terminalStates: TaskState[] = ['completed', 'failed', 'canceled', 'rejected'];
1✔
39

1✔
40
export class DefaultRequestHandler implements A2ARequestHandler {
1✔
41
  private readonly agentCard: AgentCard;
56✔
42
  private readonly taskStore: TaskStore;
56✔
43
  private readonly agentExecutor: AgentExecutor;
56✔
44
  private readonly eventBusManager: ExecutionEventBusManager;
56✔
45
  private readonly pushNotificationStore?: PushNotificationStore;
56✔
46
  private readonly pushNotificationSender?: PushNotificationSender;
56✔
47
  private readonly extendedAgentCardProvider?: AgentCard | ExtendedAgentCardProvider;
56✔
48

56✔
49
  constructor(
56✔
50
    agentCard: AgentCard,
56✔
51
    taskStore: TaskStore,
56✔
52
    agentExecutor: AgentExecutor,
56✔
53
    eventBusManager: ExecutionEventBusManager = new DefaultExecutionEventBusManager(),
56✔
54
    pushNotificationStore?: PushNotificationStore,
56✔
55
    pushNotificationSender?: PushNotificationSender,
56✔
56
    extendedAgentCardProvider?: AgentCard | ExtendedAgentCardProvider
56✔
57
  ) {
56✔
58
    this.agentCard = agentCard;
56✔
59
    this.taskStore = taskStore;
56✔
60
    this.agentExecutor = agentExecutor;
56✔
61
    this.eventBusManager = eventBusManager;
56✔
62
    this.extendedAgentCardProvider = extendedAgentCardProvider;
56✔
63

56✔
64
    // If push notifications are supported, use the provided store and sender.
56✔
65
    // Otherwise, use the default in-memory store and sender.
56✔
66
    if (agentCard.capabilities.pushNotifications) {
56✔
67
      this.pushNotificationStore = pushNotificationStore || new InMemoryPushNotificationStore();
55✔
68
      this.pushNotificationSender =
55✔
69
        pushNotificationSender || new DefaultPushNotificationSender(this.pushNotificationStore);
55✔
70
    }
55✔
71
  }
56✔
72

56✔
73
  async getAgentCard(): Promise<AgentCard> {
56✔
74
    return this.agentCard;
4✔
75
  }
4✔
76

56✔
77
  async getAuthenticatedExtendedAgentCard(context?: ServerCallContext): Promise<AgentCard> {
56✔
78
    if (!this.agentCard.supportsAuthenticatedExtendedCard) {
4✔
79
      throw A2AError.unsupportedOperation('Agent does not support authenticated extended card.');
1✔
80
    }
1✔
81
    if (!this.extendedAgentCardProvider) {
4✔
82
      throw A2AError.authenticatedExtendedCardNotConfigured();
1✔
83
    }
1✔
84
    if (typeof this.extendedAgentCardProvider === 'function') {
4✔
85
      return this.extendedAgentCardProvider(context);
1✔
86
    }
1✔
87
    if (context?.user?.isAuthenticated()) {
4✔
88
      return this.extendedAgentCardProvider;
1✔
89
    }
1✔
90
    return this.agentCard;
4!
91
  }
4✔
92

56✔
93
  private async _createRequestContext(
56✔
94
    incomingMessage: Message,
35✔
95
    context?: ServerCallContext
35✔
96
  ): Promise<RequestContext> {
35✔
97
    let task: Task | undefined;
35✔
98
    let referenceTasks: Task[] | undefined;
35✔
99

35✔
100
    // incomingMessage would contain taskId, if a task already exists.
35✔
101
    if (incomingMessage.taskId) {
35✔
102
      task = await this.taskStore.load(incomingMessage.taskId);
11✔
103
      if (!task) {
11!
104
        throw A2AError.taskNotFound(incomingMessage.taskId);
×
105
      }
×
106
      if (terminalStates.includes(task.status.state)) {
11✔
107
        // Throw an error that conforms to the JSON-RPC Invalid Request error specification.
5✔
108
        throw A2AError.invalidRequest(
5✔
109
          `Task ${task.id} is in a terminal state (${task.status.state}) and cannot be modified.`
5✔
110
        );
5✔
111
      }
5✔
112
      // Add incomingMessage to history and save the task.
11✔
113
      task.history = [...(task.history || []), incomingMessage];
11✔
114
      await this.taskStore.save(task);
11✔
115
    }
11✔
116
    // Ensure taskId is present
35✔
117
    const taskId = incomingMessage.taskId || uuidv4();
35✔
118

35✔
119
    if (incomingMessage.referenceTaskIds && incomingMessage.referenceTaskIds.length > 0) {
35!
120
      referenceTasks = [];
×
121
      for (const refId of incomingMessage.referenceTaskIds) {
×
122
        const refTask = await this.taskStore.load(refId);
×
123
        if (refTask) {
×
124
          referenceTasks.push(refTask);
×
125
        } else {
×
126
          console.warn(`Reference task ${refId} not found.`);
×
127
          // Optionally, throw an error or handle as per specific requirements
×
128
        }
×
129
      }
×
130
    }
×
131
    // Ensure contextId is present
35✔
132
    const contextId = incomingMessage.contextId || task?.contextId || uuidv4();
35✔
133

35✔
134
    // Validate requested extensions against agent capabilities
35✔
135
    if (context?.requestedExtensions) {
35✔
136
      const agentCard = await this.getAgentCard();
3✔
137
      const exposedExtensions = new Set(
3✔
138
        agentCard.capabilities.extensions?.map((ext) => ext.uri) || []
3✔
139
      );
3✔
140
      const validExtensions = new Set(
3✔
141
        Array.from(context.requestedExtensions).filter((extension) =>
3✔
142
          exposedExtensions.has(extension)
2✔
143
        )
3✔
144
      );
3✔
145
      context = new ServerCallContext(validExtensions, context.user);
3✔
146
    }
3✔
147

35✔
148
    const messageForContext = {
35✔
149
      ...incomingMessage,
30✔
150
      contextId,
30✔
151
      taskId,
30✔
152
    };
30✔
153
    return new RequestContext(messageForContext, taskId, contextId, task, referenceTasks, context);
30✔
154
  }
35✔
155

56✔
156
  private async _processEvents(
56✔
157
    taskId: string,
25✔
158
    resultManager: ResultManager,
25✔
159
    eventQueue: ExecutionEventQueue,
25✔
160
    options?: {
25✔
161
      firstResultResolver?: (value: Message | Task | PromiseLike<Message | Task>) => void;
25✔
162
      firstResultRejector?: (reason?: unknown) => void;
25✔
163
    }
25✔
164
  ): Promise<void> {
25✔
165
    let firstResultSent = false;
25✔
166
    try {
25✔
167
      for await (const event of eventQueue.events()) {
25✔
168
        await resultManager.processEvent(event);
52✔
169

52✔
170
        try {
52✔
171
          await this._sendPushNotificationIfNeeded(event);
51✔
172
        } catch (error) {
52!
173
          console.error(`Error sending push notification: ${error}`);
×
174
        }
×
175

52✔
176
        if (options?.firstResultResolver && !firstResultSent) {
52✔
177
          let firstResult: Message | Task | undefined;
4✔
178
          if (event.kind === 'message') {
4!
179
            firstResult = event;
×
180
          } else {
4✔
181
            firstResult = resultManager.getCurrentTask();
4✔
182
          }
4✔
183
          if (firstResult) {
4✔
184
            options.firstResultResolver(firstResult);
4✔
185
            firstResultSent = true;
4✔
186
          }
4✔
187
        }
4✔
188
      }
52✔
189
      if (options?.firstResultRejector && !firstResultSent) {
25!
190
        options.firstResultRejector(
×
191
          A2AError.internalError('Execution finished before a message or task was produced.')
×
192
        );
×
193
      }
×
194
    } catch (error) {
25✔
195
      console.error(`Event processing loop failed for task ${taskId}:`, error);
1✔
196
      this._handleProcessingError(
1✔
197
        error,
1✔
198
        resultManager,
1✔
199
        firstResultSent,
1✔
200
        taskId,
1✔
201
        options?.firstResultRejector
1✔
202
      );
1✔
203
    } finally {
25✔
204
      this.eventBusManager.cleanupByTaskId(taskId);
25✔
205
    }
25✔
206
  }
25✔
207

56✔
208
  async sendMessage(
56✔
209
    params: MessageSendParams,
27✔
210
    context?: ServerCallContext
27✔
211
  ): Promise<Message | Task> {
27✔
212
    const incomingMessage = params.message;
27✔
213
    if (!incomingMessage.messageId) {
27!
214
      throw A2AError.invalidParams('message.messageId is required.');
×
215
    }
×
216

27✔
217
    const num1 = 2;
27✔
218
    if (num1 % 2 == 1) {
27!
NEW
219
      console.log('Hello this is a test');
×
NEW
220
    }
×
221
    let num2 = 3;
27✔
222
    if (num2 % 2 == 0) {
27!
NEW
223
      console.log('Hello this is a test');
×
NEW
224
    }
×
225
    num2 = 4;
27✔
226
    if (num2 % 2 == 1) {
27!
NEW
227
      console.log('Hello this is a test');
×
NEW
228
    }
×
229
    num2 = 5;
27✔
230
    if (num2 % 2 == 0) {
27!
NEW
231
      console.log('Hello this is a test');
×
UNCOV
232
    }
×
233

27✔
234
    // Default to blocking behavior if 'blocking' is not explicitly false.
27✔
235
    const isBlocking = params.configuration?.blocking !== false;
27✔
236
    // Instantiate ResultManager before creating RequestContext
27✔
237
    const resultManager = new ResultManager(this.taskStore);
27✔
238
    resultManager.setContext(incomingMessage); // Set context for ResultManager
27✔
239

27✔
240
    const requestContext = await this._createRequestContext(incomingMessage, context);
27✔
241
    const taskId = requestContext.taskId;
27✔
242

23✔
243
    // Use the (potentially updated) contextId from requestContext
23✔
244
    const finalMessageForAgent = requestContext.userMessage;
23✔
245

23✔
246
    // If push notification config is provided, save it to the store.
23✔
247
    if (
23✔
248
      params.configuration?.pushNotificationConfig &&
27✔
249
      this.agentCard.capabilities.pushNotifications
6✔
250
    ) {
27✔
251
      await this.pushNotificationStore?.save(taskId, params.configuration.pushNotificationConfig);
6✔
252
    }
6✔
253

27✔
254
    const eventBus = this.eventBusManager.createOrGetByTaskId(taskId);
27✔
255
    // EventQueue should be attached to the bus, before the agent execution begins.
23✔
256
    const eventQueue = new ExecutionEventQueue(eventBus);
23✔
257

23✔
258
    // Start agent execution (non-blocking).
23✔
259
    // It runs in the background and publishes events to the eventBus.
23✔
260
    this.agentExecutor.execute(requestContext, eventBus).catch((err) => {
23✔
261
      console.error(`Agent execution failed for message ${finalMessageForAgent.messageId}:`, err);
2✔
262
      // Publish a synthetic error event, which will be handled by the ResultManager
2✔
263
      // and will also settle the firstResultPromise for non-blocking calls.
2✔
264
      const errorTask: Task = {
2✔
265
        id: requestContext.task?.id || uuidv4(), // Use existing task ID or generate new
2!
266
        contextId: finalMessageForAgent.contextId!,
2✔
267
        status: {
2✔
268
          state: 'failed',
2✔
269
          message: {
2✔
270
            kind: 'message',
2✔
271
            role: 'agent',
2✔
272
            messageId: uuidv4(),
2✔
273
            parts: [{ kind: 'text', text: `Agent execution error: ${err.message}` }],
2✔
274
            taskId: requestContext.task?.id,
2!
275
            contextId: finalMessageForAgent.contextId!,
2✔
276
          },
2✔
277
          timestamp: new Date().toISOString(),
2✔
278
        },
2✔
279
        history: requestContext.task?.history ? [...requestContext.task.history] : [],
2!
280
        kind: 'task',
2✔
281
      };
2✔
282
      if (finalMessageForAgent) {
2✔
283
        // Add incoming message to history
2✔
284
        if (!errorTask.history?.find((m) => m.messageId === finalMessageForAgent.messageId)) {
2✔
285
          errorTask.history?.push(finalMessageForAgent);
2✔
286
        }
2✔
287
      }
2✔
288
      eventBus.publish(errorTask);
2✔
289
      eventBus.publish({
2✔
290
        // And publish a final status update
2✔
291
        kind: 'status-update',
2✔
292
        taskId: errorTask.id,
2✔
293
        contextId: errorTask.contextId,
2✔
294
        status: errorTask.status,
2✔
295
        final: true,
2✔
296
      } as TaskStatusUpdateEvent);
2✔
297
      eventBus.finished();
2✔
298
    });
23✔
299

23✔
300
    if (isBlocking) {
27✔
301
      // In blocking mode, wait for the full processing to complete.
19✔
302
      await this._processEvents(taskId, resultManager, eventQueue);
19✔
303
      const finalResult = resultManager.getFinalResult();
19✔
304
      if (!finalResult) {
19!
305
        throw A2AError.internalError(
×
306
          'Agent execution finished without a result, and no task context found.'
×
307
        );
×
308
      }
×
309

19✔
310
      return finalResult;
19✔
311
    } else {
27✔
312
      // In non-blocking mode, return a promise that will be settled by fullProcessing.
4✔
313
      return new Promise<Message | Task>((resolve, reject) => {
4✔
314
        this._processEvents(taskId, resultManager, eventQueue, {
4✔
315
          firstResultResolver: resolve,
4✔
316
          firstResultRejector: reject,
4✔
317
        });
4✔
318
      });
4✔
319
    }
4✔
320
  }
27✔
321

56✔
322
  async *sendMessageStream(
56✔
323
    params: MessageSendParams,
8✔
324
    context?: ServerCallContext
8✔
325
  ): AsyncGenerator<
8✔
326
    Message | Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent,
8✔
327
    void,
8✔
328
    undefined
8✔
329
  > {
8✔
330
    const incomingMessage = params.message;
8✔
331
    if (!incomingMessage.messageId) {
8!
332
      // For streams, messageId might be set by client, or server can generate if not present.
×
333
      // Let's assume client provides it or throw for now.
×
334
      throw A2AError.invalidParams('message.messageId is required for streaming.');
×
335
    }
×
336

8✔
337
    // Instantiate ResultManager before creating RequestContext
8✔
338
    const resultManager = new ResultManager(this.taskStore);
8✔
339
    resultManager.setContext(incomingMessage); // Set context for ResultManager
8✔
340

8✔
341
    const requestContext = await this._createRequestContext(incomingMessage, context);
8✔
342
    const taskId = requestContext.taskId;
8✔
343
    const finalMessageForAgent = requestContext.userMessage;
7✔
344

7✔
345
    const eventBus = this.eventBusManager.createOrGetByTaskId(taskId);
7✔
346
    const eventQueue = new ExecutionEventQueue(eventBus);
7✔
347

7✔
348
    // If push notification config is provided, save it to the store.
7✔
349
    if (
7✔
350
      params.configuration?.pushNotificationConfig &&
8✔
351
      this.agentCard.capabilities.pushNotifications
1✔
352
    ) {
8✔
353
      await this.pushNotificationStore?.save(taskId, params.configuration.pushNotificationConfig);
1✔
354
    }
1✔
355

8✔
356
    // Start agent execution (non-blocking)
8✔
357
    this.agentExecutor.execute(requestContext, eventBus).catch((err) => {
8✔
358
      console.error(
×
359
        `Agent execution failed for stream message ${finalMessageForAgent.messageId}:`,
×
360
        err
×
361
      );
×
362
      // Publish a synthetic error event if needed
×
363
      const errorTaskStatus: TaskStatusUpdateEvent = {
×
364
        kind: 'status-update',
×
365
        taskId: requestContext.task?.id || uuidv4(), // Use existing or a placeholder
×
366
        contextId: finalMessageForAgent.contextId!,
×
367
        status: {
×
368
          state: 'failed',
×
369
          message: {
×
370
            kind: 'message',
×
371
            role: 'agent',
×
372
            messageId: uuidv4(),
×
373
            parts: [{ kind: 'text', text: `Agent execution error: ${err.message}` }],
×
374
            taskId: requestContext.task?.id,
×
375
            contextId: finalMessageForAgent.contextId!,
×
376
          },
×
377
          timestamp: new Date().toISOString(),
×
378
        },
×
379
        final: true, // This will terminate the stream for the client
×
380
      };
×
381
      eventBus.publish(errorTaskStatus);
×
382
    });
7✔
383

7✔
384
    try {
7✔
385
      for await (const event of eventQueue.events()) {
8✔
386
        await resultManager.processEvent(event); // Update store in background
20✔
387
        await this._sendPushNotificationIfNeeded(event);
20✔
388
        yield event; // Stream the event to the client
20✔
389
      }
20✔
390
    } finally {
8✔
391
      // Cleanup when the stream is fully consumed or breaks
7✔
392
      this.eventBusManager.cleanupByTaskId(taskId);
7✔
393
    }
7✔
394
  }
8✔
395

56✔
396
  async getTask(params: TaskQueryParams, _context?: ServerCallContext): Promise<Task> {
56✔
397
    const task = await this.taskStore.load(params.id);
2✔
398
    if (!task) {
2!
399
      throw A2AError.taskNotFound(params.id);
×
400
    }
×
401
    if (params.historyLength !== undefined && params.historyLength >= 0) {
2!
402
      if (task.history) {
×
403
        task.history = task.history.slice(-params.historyLength);
×
404
      }
×
405
    } else {
2✔
406
      // Negative or invalid historyLength means no history
2✔
407
      task.history = [];
2✔
408
    }
2✔
409
    return task;
2✔
410
  }
2✔
411

56✔
412
  async cancelTask(params: TaskIdParams, _context?: ServerCallContext): Promise<Task> {
56✔
413
    const task = await this.taskStore.load(params.id);
3✔
414
    if (!task) {
3!
415
      throw A2AError.taskNotFound(params.id);
×
416
    }
×
417

3✔
418
    // Check if task is in a cancelable state
3✔
419
    const nonCancelableStates = ['completed', 'failed', 'canceled', 'rejected'];
3✔
420
    if (nonCancelableStates.includes(task.status.state)) {
3✔
421
      throw A2AError.taskNotCancelable(params.id);
1✔
422
    }
1✔
423

3✔
424
    const eventBus = this.eventBusManager.getByTaskId(params.id);
3✔
425

2✔
426
    if (eventBus) {
2✔
427
      const eventQueue = new ExecutionEventQueue(eventBus);
2✔
428
      await this.agentExecutor.cancelTask(params.id, eventBus);
2✔
429
      // Consume all the events until the task reaches a terminal state.
2✔
430
      await this._processEvents(params.id, new ResultManager(this.taskStore), eventQueue);
2✔
431
    } else {
3!
432
      // Here we are marking task as cancelled. We are not waiting for the executor to actually cancel processing.
×
433
      task.status = {
×
434
        state: 'canceled',
×
435
        message: {
×
436
          // Optional: Add a system message indicating cancellation
×
437
          kind: 'message',
×
438
          role: 'agent',
×
439
          messageId: uuidv4(),
×
440
          parts: [{ kind: 'text', text: 'Task cancellation requested by user.' }],
×
441
          taskId: task.id,
×
442
          contextId: task.contextId,
×
443
        },
×
444
        timestamp: new Date().toISOString(),
×
445
      };
×
446
      // Add cancellation message to history
×
447
      task.history = [...(task.history || []), task.status.message];
×
448

×
449
      await this.taskStore.save(task);
×
450
    }
×
451

3✔
452
    const latestTask = await this.taskStore.load(params.id);
3✔
453
    if (!latestTask) {
3!
454
      throw A2AError.internalError(`Task ${params.id} not found after cancellation.`);
×
455
    }
×
456
    if (latestTask.status.state != 'canceled') {
3✔
457
      throw A2AError.taskNotCancelable(params.id);
1✔
458
    }
1✔
459
    return latestTask;
1✔
460
  }
3✔
461

56✔
462
  async setTaskPushNotificationConfig(
56✔
463
    params: TaskPushNotificationConfig,
15✔
464
    _context?: ServerCallContext
15✔
465
  ): Promise<TaskPushNotificationConfig> {
15✔
466
    if (!this.agentCard.capabilities.pushNotifications) {
15✔
467
      throw A2AError.pushNotificationNotSupported();
1✔
468
    }
1✔
469
    const task = await this.taskStore.load(params.taskId);
15✔
470
    if (!task) {
15✔
471
      throw A2AError.taskNotFound(params.taskId);
1✔
472
    }
1✔
473

15✔
474
    const { taskId, pushNotificationConfig } = params;
15✔
475

13✔
476
    // Default the config ID to the task ID if not provided for backward compatibility.
13✔
477
    if (!pushNotificationConfig.id) {
15✔
478
      pushNotificationConfig.id = taskId;
1✔
479
    }
1✔
480

15✔
481
    await this.pushNotificationStore?.save(taskId, pushNotificationConfig);
15✔
482

15✔
483
    return params;
15✔
484
  }
15✔
485

56✔
486
  async getTaskPushNotificationConfig(
56✔
487
    params: TaskIdParams | GetTaskPushNotificationConfigParams,
4✔
488
    _context?: ServerCallContext
4✔
489
  ): Promise<TaskPushNotificationConfig> {
4✔
490
    if (!this.agentCard.capabilities.pushNotifications) {
4✔
491
      throw A2AError.pushNotificationNotSupported();
1✔
492
    }
1✔
493
    const task = await this.taskStore.load(params.id);
4✔
494
    if (!task) {
4✔
495
      throw A2AError.taskNotFound(params.id);
1✔
496
    }
1✔
497

4✔
498
    const configs = (await this.pushNotificationStore?.load(params.id)) || [];
4!
499
    if (configs.length === 0) {
4!
500
      throw A2AError.internalError(`Push notification config not found for task ${params.id}.`);
×
501
    }
×
502

4✔
503
    let configId: string;
4✔
504
    if ('pushNotificationConfigId' in params && params.pushNotificationConfigId) {
4✔
505
      configId = params.pushNotificationConfigId;
1✔
506
    } else {
1✔
507
      // For backward compatibility, if no config ID is given, assume it's the task ID.
1✔
508
      configId = params.id;
1✔
509
    }
1✔
510

4✔
511
    const config = configs.find((c) => c.id === configId);
4✔
512

2✔
513
    if (!config) {
4!
514
      throw A2AError.internalError(
×
515
        `Push notification config with id '${configId}' not found for task ${params.id}.`
×
516
      );
×
517
    }
×
518
    return { taskId: params.id, pushNotificationConfig: config };
4✔
519
  }
4✔
520

56✔
521
  async listTaskPushNotificationConfigs(
56✔
522
    params: ListTaskPushNotificationConfigParams,
6✔
523
    _context?: ServerCallContext
6✔
524
  ): Promise<TaskPushNotificationConfig[]> {
6✔
525
    if (!this.agentCard.capabilities.pushNotifications) {
6✔
526
      throw A2AError.pushNotificationNotSupported();
1✔
527
    }
1✔
528
    const task = await this.taskStore.load(params.id);
6✔
529
    if (!task) {
6✔
530
      throw A2AError.taskNotFound(params.id);
1✔
531
    }
1✔
532

6✔
533
    const configs = (await this.pushNotificationStore?.load(params.id)) || [];
6!
534

6✔
535
    return configs.map((config) => ({
6✔
536
      taskId: params.id,
4✔
537
      pushNotificationConfig: config,
4✔
538
    }));
6✔
539
  }
6✔
540

56✔
541
  async deleteTaskPushNotificationConfig(
56✔
542
    params: DeleteTaskPushNotificationConfigParams,
4✔
543
    _context?: ServerCallContext
4✔
544
  ): Promise<void> {
4✔
545
    if (!this.agentCard.capabilities.pushNotifications) {
4✔
546
      throw A2AError.pushNotificationNotSupported();
1✔
547
    }
1✔
548
    const task = await this.taskStore.load(params.id);
4✔
549
    if (!task) {
4✔
550
      throw A2AError.taskNotFound(params.id);
1✔
551
    }
1✔
552

4✔
553
    const { id: taskId, pushNotificationConfigId } = params;
4✔
554

2✔
555
    await this.pushNotificationStore?.delete(taskId, pushNotificationConfigId);
4✔
556
  }
4✔
557

56✔
558
  async *resubscribe(
56✔
559
    params: TaskIdParams,
1✔
560
    _context?: ServerCallContext
1✔
561
  ): AsyncGenerator<
1✔
562
    | Task // Initial task state
1✔
563
    | TaskStatusUpdateEvent
1✔
564
    | TaskArtifactUpdateEvent,
1✔
565
    void,
1✔
566
    undefined
1✔
567
  > {
1✔
568
    if (!this.agentCard.capabilities.streaming) {
1!
569
      throw A2AError.unsupportedOperation('Streaming (and thus resubscription) is not supported.');
×
570
    }
×
571

1✔
572
    const task = await this.taskStore.load(params.id);
1✔
573
    if (!task) {
1!
574
      throw A2AError.taskNotFound(params.id);
×
575
    }
×
576

1✔
577
    // Yield the current task state first
1✔
578
    yield task;
1✔
579

1✔
580
    // If task is already in a final state, no more events will come.
1✔
581
    const finalStates = ['completed', 'failed', 'canceled', 'rejected'];
1✔
582
    if (finalStates.includes(task.status.state)) {
1!
583
      return;
×
584
    }
×
585

1✔
586
    const eventBus = this.eventBusManager.getByTaskId(params.id);
1✔
587
    if (!eventBus) {
1!
588
      // No active execution for this task, so no live events.
×
589
      console.warn(`Resubscribe: No active event bus for task ${params.id}.`);
×
590
      return;
×
591
    }
×
592

1✔
593
    // Attach a new queue to the existing bus for this resubscription
1✔
594
    const eventQueue = new ExecutionEventQueue(eventBus);
1✔
595
    // Note: The ResultManager part is already handled by the original execution flow.
1✔
596
    // Resubscribe just listens for new events.
1✔
597

1✔
598
    try {
1✔
599
      for await (const event of eventQueue.events()) {
1✔
600
        // We only care about updates related to *this* task.
1✔
601
        // The event bus might be shared if messageId was reused, though
1✔
602
        // ExecutionEventBusManager tries to give one bus per original message.
1✔
603
        if (event.kind === 'status-update' && event.taskId === params.id) {
1✔
604
          yield event as TaskStatusUpdateEvent;
1✔
605
        } else if (event.kind === 'artifact-update' && event.taskId === params.id) {
1!
606
          yield event as TaskArtifactUpdateEvent;
×
607
        } else if (event.kind === 'task' && event.id === params.id) {
×
608
          // This implies the task was re-emitted, yield it.
×
609
          yield event as Task;
×
610
        }
×
611
        // We don't yield 'message' events on resubscribe typically,
1✔
612
        // as those signal the end of an interaction for the *original* request.
1✔
613
        // If a 'message' event for the original request terminates the bus, this loop will also end.
1✔
614
      }
1✔
615
    } finally {
1✔
616
      eventQueue.stop();
1✔
617
    }
1✔
618
  }
1✔
619

56✔
620
  private async _sendPushNotificationIfNeeded(event: AgentExecutionEvent): Promise<void> {
56✔
621
    if (!this.agentCard.capabilities.pushNotifications) {
71!
622
      return;
×
623
    }
×
624

71✔
625
    let taskId: string = '';
71✔
626
    if (event.kind == 'task') {
71✔
627
      const task = event as Task;
25✔
628
      taskId = task.id;
25✔
629
    } else {
71✔
630
      taskId = event.taskId;
46✔
631
    }
46✔
632

71✔
633
    if (!taskId) {
71✔
634
      console.error(`Task ID not found for event ${event.kind}.`);
2✔
635
      return;
2✔
636
    }
2✔
637

71✔
638
    const task = await this.taskStore.load(taskId);
71✔
639
    if (!task) {
71✔
640
      console.error(`Task ${taskId} not found.`);
1✔
641
      return;
1✔
642
    }
1✔
643

71✔
644
    // Send push notification in the background.
71✔
645
    this.pushNotificationSender?.send(task);
71✔
646
  }
71✔
647

56✔
648
  private async _handleProcessingError(
56✔
649
    error: unknown,
1✔
650
    resultManager: ResultManager,
1✔
651
    firstResultSent: boolean,
1✔
652
    taskId: string,
1✔
653
    firstResultRejector?: (reason: unknown) => void
1✔
654
  ): Promise<void> {
1✔
655
    // Non-blocking case with with first result not sent
1✔
656
    if (firstResultRejector && !firstResultSent) {
1!
657
      firstResultRejector(error);
×
658
      return;
×
659
    }
×
660

1✔
661
    // re-throw error for blocking case to catch
1✔
662
    if (!firstResultRejector) {
1!
663
      throw error;
×
664
    }
×
665

1✔
666
    // Non-blocking case with first result already sent
1✔
667
    const currentTask = resultManager.getCurrentTask();
1✔
668
    const errorMessage = (error instanceof Error && error.message) || 'Unknown error';
1!
669
    if (currentTask) {
1✔
670
      const statusUpdateFailed: TaskStatusUpdateEvent = {
1✔
671
        taskId: currentTask.id,
1✔
672
        contextId: currentTask.contextId,
1✔
673
        status: {
1✔
674
          state: 'failed',
1✔
675
          message: {
1✔
676
            kind: 'message',
1✔
677
            role: 'agent',
1✔
678
            messageId: uuidv4(),
1✔
679
            parts: [{ kind: 'text', text: `Event processing loop failed: ${errorMessage}` }],
1✔
680
            taskId: currentTask.id,
1✔
681
            contextId: currentTask.contextId,
1✔
682
          },
1✔
683
          timestamp: new Date().toISOString(),
1✔
684
        },
1✔
685
        kind: 'status-update',
1✔
686
        final: true,
1✔
687
      };
1✔
688

1✔
689
      try {
1✔
690
        await resultManager.processEvent(statusUpdateFailed);
1✔
691
      } catch (error) {
1!
692
        console.error(
×
693
          `Event processing loop failed for task ${taskId}: ${(error instanceof Error && error.message) || 'Unknown error'}`
×
694
        );
×
695
      }
×
696
    } else {
1!
697
      console.error(`Event processing loop failed for task ${taskId}: ${errorMessage}`);
×
698
    }
×
699
  }
1✔
700
}
56✔
701

1✔
702
export type ExtendedAgentCardProvider = (context?: ServerCallContext) => Promise<AgentCard>;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc