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

guglielmo-san / a2a-js / 19833220453

01 Dec 2025 06:27PM UTC coverage: 88.256% (-0.02%) from 88.279%
19833220453

Pull #5

github

web-flow
Merge 24f962a98 into 873d99913
Pull Request #5: feat: add function with no coverage

483 of 576 branches covered (83.85%)

Branch coverage included in aggregate %.

13 of 15 new or added lines in 1 file covered. (86.67%)

2959 of 3324 relevant lines covered (89.02%)

11.74 hits per line

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

81.89
/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
    this.noCoverage();
4✔
75
    return this.agentCard;
4✔
76
  }
4✔
77

56✔
78
  noCoverage(): number {
56✔
79
    console.log('No coverage');
4✔
80
    const array = [1, 2, 3];
4✔
81
    let total: number = 0;
4✔
82
    array.forEach((element) => {
4✔
83
      console.log(element);
12✔
84
      total += element;
12✔
85
    });
4✔
86
    if (total % 5 == 0) {
4!
NEW
87
      total += 1;
×
NEW
88
    }
×
89
    return total;
4✔
90
  }
4✔
91

56✔
92
  async getAuthenticatedExtendedAgentCard(context?: ServerCallContext): Promise<AgentCard> {
56✔
93
    if (!this.agentCard.supportsAuthenticatedExtendedCard) {
4✔
94
      throw A2AError.unsupportedOperation('Agent does not support authenticated extended card.');
1✔
95
    }
1✔
96
    if (!this.extendedAgentCardProvider) {
4✔
97
      throw A2AError.authenticatedExtendedCardNotConfigured();
1✔
98
    }
1✔
99
    if (typeof this.extendedAgentCardProvider === 'function') {
4✔
100
      return this.extendedAgentCardProvider(context);
1✔
101
    }
1✔
102
    if (context?.user?.isAuthenticated()) {
4✔
103
      return this.extendedAgentCardProvider;
1✔
104
    }
1✔
105
    return this.agentCard;
4!
106
  }
4✔
107

56✔
108
  private async _createRequestContext(
56✔
109
    incomingMessage: Message,
35✔
110
    context?: ServerCallContext
35✔
111
  ): Promise<RequestContext> {
35✔
112
    let task: Task | undefined;
35✔
113
    let referenceTasks: Task[] | undefined;
35✔
114

35✔
115
    // incomingMessage would contain taskId, if a task already exists.
35✔
116
    if (incomingMessage.taskId) {
35✔
117
      task = await this.taskStore.load(incomingMessage.taskId);
11✔
118
      if (!task) {
11!
119
        throw A2AError.taskNotFound(incomingMessage.taskId);
×
120
      }
×
121
      if (terminalStates.includes(task.status.state)) {
11✔
122
        // Throw an error that conforms to the JSON-RPC Invalid Request error specification.
5✔
123
        throw A2AError.invalidRequest(
5✔
124
          `Task ${task.id} is in a terminal state (${task.status.state}) and cannot be modified.`
5✔
125
        );
5✔
126
      }
5✔
127
      // Add incomingMessage to history and save the task.
11✔
128
      task.history = [...(task.history || []), incomingMessage];
11✔
129
      await this.taskStore.save(task);
11✔
130
    }
11✔
131
    // Ensure taskId is present
35✔
132
    const taskId = incomingMessage.taskId || uuidv4();
35✔
133

35✔
134
    if (incomingMessage.referenceTaskIds && incomingMessage.referenceTaskIds.length > 0) {
35!
135
      referenceTasks = [];
×
136
      for (const refId of incomingMessage.referenceTaskIds) {
×
137
        const refTask = await this.taskStore.load(refId);
×
138
        if (refTask) {
×
139
          referenceTasks.push(refTask);
×
140
        } else {
×
141
          console.warn(`Reference task ${refId} not found.`);
×
142
          // Optionally, throw an error or handle as per specific requirements
×
143
        }
×
144
      }
×
145
    }
×
146
    // Ensure contextId is present
35✔
147
    const contextId = incomingMessage.contextId || task?.contextId || uuidv4();
35✔
148

35✔
149
    // Validate requested extensions against agent capabilities
35✔
150
    if (context?.requestedExtensions) {
35✔
151
      const agentCard = await this.getAgentCard();
3✔
152
      const exposedExtensions = new Set(
3✔
153
        agentCard.capabilities.extensions?.map((ext) => ext.uri) || []
3✔
154
      );
3✔
155
      const validExtensions = new Set(
3✔
156
        Array.from(context.requestedExtensions).filter((extension) =>
3✔
157
          exposedExtensions.has(extension)
2✔
158
        )
3✔
159
      );
3✔
160
      context = new ServerCallContext(validExtensions, context.user);
3✔
161
    }
3✔
162

35✔
163
    const messageForContext = {
35✔
164
      ...incomingMessage,
30✔
165
      contextId,
30✔
166
      taskId,
30✔
167
    };
30✔
168
    return new RequestContext(messageForContext, taskId, contextId, task, referenceTasks, context);
30✔
169
  }
35✔
170

56✔
171
  private async _processEvents(
56✔
172
    taskId: string,
25✔
173
    resultManager: ResultManager,
25✔
174
    eventQueue: ExecutionEventQueue,
25✔
175
    options?: {
25✔
176
      firstResultResolver?: (value: Message | Task | PromiseLike<Message | Task>) => void;
25✔
177
      firstResultRejector?: (reason?: unknown) => void;
25✔
178
    }
25✔
179
  ): Promise<void> {
25✔
180
    let firstResultSent = false;
25✔
181
    try {
25✔
182
      for await (const event of eventQueue.events()) {
25✔
183
        await resultManager.processEvent(event);
52✔
184

52✔
185
        try {
52✔
186
          await this._sendPushNotificationIfNeeded(event);
51✔
187
        } catch (error) {
52!
188
          console.error(`Error sending push notification: ${error}`);
×
189
        }
×
190

52✔
191
        if (options?.firstResultResolver && !firstResultSent) {
52✔
192
          let firstResult: Message | Task | undefined;
4✔
193
          if (event.kind === 'message') {
4!
194
            firstResult = event;
×
195
          } else {
4✔
196
            firstResult = resultManager.getCurrentTask();
4✔
197
          }
4✔
198
          if (firstResult) {
4✔
199
            options.firstResultResolver(firstResult);
4✔
200
            firstResultSent = true;
4✔
201
          }
4✔
202
        }
4✔
203
      }
52✔
204
      if (options?.firstResultRejector && !firstResultSent) {
25!
205
        options.firstResultRejector(
×
206
          A2AError.internalError('Execution finished before a message or task was produced.')
×
207
        );
×
208
      }
×
209
    } catch (error) {
25✔
210
      console.error(`Event processing loop failed for task ${taskId}:`, error);
1✔
211
      this._handleProcessingError(
1✔
212
        error,
1✔
213
        resultManager,
1✔
214
        firstResultSent,
1✔
215
        taskId,
1✔
216
        options?.firstResultRejector
1✔
217
      );
1✔
218
    } finally {
25✔
219
      this.eventBusManager.cleanupByTaskId(taskId);
25✔
220
    }
25✔
221
  }
25✔
222

56✔
223
  async sendMessage(
56✔
224
    params: MessageSendParams,
27✔
225
    context?: ServerCallContext
27✔
226
  ): Promise<Message | Task> {
27✔
227
    const incomingMessage = params.message;
27✔
228
    if (!incomingMessage.messageId) {
27!
229
      throw A2AError.invalidParams('message.messageId is required.');
×
230
    }
×
231

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3✔
422
    const eventBus = this.eventBusManager.getByTaskId(params.id);
3✔
423

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

×
447
      await this.taskStore.save(task);
×
448
    }
×
449

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

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

15✔
472
    const { taskId, pushNotificationConfig } = params;
15✔
473

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

15✔
479
    await this.pushNotificationStore?.save(taskId, pushNotificationConfig);
15✔
480

15✔
481
    return params;
15✔
482
  }
15✔
483

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

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

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

4✔
509
    const config = configs.find((c) => c.id === configId);
4✔
510

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

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

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

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

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

4✔
551
    const { id: taskId, pushNotificationConfigId } = params;
4✔
552

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

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

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

1✔
575
    // Yield the current task state first
1✔
576
    yield task;
1✔
577

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

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

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

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

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

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

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

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

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

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

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

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

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

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