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

guglielmo-san / a2a-js / 19853151439

02 Dec 2025 09:09AM UTC coverage: 88.096% (-0.2%) from 88.294%
19853151439

Pull #7

github

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

481 of 576 branches covered (83.51%)

Branch coverage included in aggregate %.

6 of 11 new or added lines in 1 file covered. (54.55%)

1 existing line in 1 file now uncovered.

2953 of 3322 relevant lines covered (88.89%)

11.75 hits per line

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

81.14
/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');
×
UNCOV
228
    }
×
229

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

27✔
236
    const requestContext = await this._createRequestContext(incomingMessage, context);
27✔
237
    const taskId = requestContext.taskId;
27✔
238

23✔
239
    // Use the (potentially updated) contextId from requestContext
23✔
240
    const finalMessageForAgent = requestContext.userMessage;
23✔
241

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

27✔
250
    const eventBus = this.eventBusManager.createOrGetByTaskId(taskId);
27✔
251
    // EventQueue should be attached to the bus, before the agent execution begins.
23✔
252
    const eventQueue = new ExecutionEventQueue(eventBus);
23✔
253

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

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

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

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

8✔
333
    // Instantiate ResultManager before creating RequestContext
8✔
334
    const resultManager = new ResultManager(this.taskStore);
8✔
335
    resultManager.setContext(incomingMessage); // Set context for ResultManager
8✔
336

8✔
337
    const requestContext = await this._createRequestContext(incomingMessage, context);
8✔
338
    const taskId = requestContext.taskId;
8✔
339
    const finalMessageForAgent = requestContext.userMessage;
7✔
340

7✔
341
    const eventBus = this.eventBusManager.createOrGetByTaskId(taskId);
7✔
342
    const eventQueue = new ExecutionEventQueue(eventBus);
7✔
343

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

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

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

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

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

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

3✔
420
    const eventBus = this.eventBusManager.getByTaskId(params.id);
3✔
421

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

×
445
      await this.taskStore.save(task);
×
446
    }
×
447

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

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

15✔
470
    const { taskId, pushNotificationConfig } = params;
15✔
471

13✔
472
    // Default the config ID to the task ID if not provided for backward compatibility.
13✔
473
    if (!pushNotificationConfig.id) {
15✔
474
      pushNotificationConfig.id = taskId;
1✔
475
    }
1✔
476

15✔
477
    await this.pushNotificationStore?.save(taskId, pushNotificationConfig);
15✔
478

15✔
479
    return params;
15✔
480
  }
15✔
481

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

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

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

4✔
507
    const config = configs.find((c) => c.id === configId);
4✔
508

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

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

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

6✔
531
    return configs.map((config) => ({
6✔
532
      taskId: params.id,
4✔
533
      pushNotificationConfig: config,
4✔
534
    }));
6✔
535
  }
6✔
536

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

4✔
549
    const { id: taskId, pushNotificationConfigId } = params;
4✔
550

2✔
551
    await this.pushNotificationStore?.delete(taskId, pushNotificationConfigId);
4✔
552
  }
4✔
553

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

1✔
568
    const task = await this.taskStore.load(params.id);
1✔
569
    if (!task) {
1!
570
      throw A2AError.taskNotFound(params.id);
×
571
    }
×
572

1✔
573
    // Yield the current task state first
1✔
574
    yield task;
1✔
575

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

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

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

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

56✔
616
  private async _sendPushNotificationIfNeeded(event: AgentExecutionEvent): Promise<void> {
56✔
617
    if (!this.agentCard.capabilities.pushNotifications) {
71!
618
      return;
×
619
    }
×
620

71✔
621
    let taskId: string = '';
71✔
622
    if (event.kind == 'task') {
71✔
623
      const task = event as Task;
25✔
624
      taskId = task.id;
25✔
625
    } else {
71✔
626
      taskId = event.taskId;
46✔
627
    }
46✔
628

71✔
629
    if (!taskId) {
71✔
630
      console.error(`Task ID not found for event ${event.kind}.`);
2✔
631
      return;
2✔
632
    }
2✔
633

71✔
634
    const task = await this.taskStore.load(taskId);
71✔
635
    if (!task) {
71✔
636
      console.error(`Task ${taskId} not found.`);
1✔
637
      return;
1✔
638
    }
1✔
639

71✔
640
    // Send push notification in the background.
71✔
641
    this.pushNotificationSender?.send(task);
71✔
642
  }
71✔
643

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

1✔
657
    // re-throw error for blocking case to catch
1✔
658
    if (!firstResultRejector) {
1!
659
      throw error;
×
660
    }
×
661

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

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

1✔
698
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