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

guglielmo-san / a2a-js / 19852976584

02 Dec 2025 09:03AM UTC coverage: 88.161% (-0.1%) from 88.294%
19852976584

Pull #7

github

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

481 of 575 branches covered (83.65%)

Branch coverage included in aggregate %.

5 of 7 new or added lines in 1 file covered. (71.43%)

2 existing lines in 1 file now uncovered.

2952 of 3319 relevant lines covered (88.94%)

11.75 hits per line

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

81.41
/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
    let num1 = 2
27✔
218
    if (num1 % 2 == 1) {
27!
UNCOV
219
      console.log("Hello this is a test")
×
UNCOV
220
    }
×
221
    let num2 = 3
27✔
222
    if (num2 % 2 == 0) {
27!
NEW
223
      console.log("Hello this is a test")
×
NEW
224
    }
×
225

27✔
226

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

27✔
233
    const requestContext = await this._createRequestContext(incomingMessage, context);
27✔
234
    const taskId = requestContext.taskId;
27✔
235

23✔
236
    // Use the (potentially updated) contextId from requestContext
23✔
237
    const finalMessageForAgent = requestContext.userMessage;
23✔
238

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

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

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

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

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

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

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

8✔
334
    const requestContext = await this._createRequestContext(incomingMessage, context);
8✔
335
    const taskId = requestContext.taskId;
8✔
336
    const finalMessageForAgent = requestContext.userMessage;
7✔
337

7✔
338
    const eventBus = this.eventBusManager.createOrGetByTaskId(taskId);
7✔
339
    const eventQueue = new ExecutionEventQueue(eventBus);
7✔
340

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

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

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

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

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

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

3✔
417
    const eventBus = this.eventBusManager.getByTaskId(params.id);
3✔
418

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

×
442
      await this.taskStore.save(task);
×
443
    }
×
444

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

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

15✔
467
    const { taskId, pushNotificationConfig } = params;
15✔
468

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

15✔
474
    await this.pushNotificationStore?.save(taskId, pushNotificationConfig);
15✔
475

15✔
476
    return params;
15✔
477
  }
15✔
478

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

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

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

4✔
504
    const config = configs.find((c) => c.id === configId);
4✔
505

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

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

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

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

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

4✔
546
    const { id: taskId, pushNotificationConfigId } = params;
4✔
547

2✔
548
    await this.pushNotificationStore?.delete(taskId, pushNotificationConfigId);
4✔
549
  }
4✔
550

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

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

1✔
570
    // Yield the current task state first
1✔
571
    yield task;
1✔
572

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

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

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

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

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

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

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

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

71✔
637
    // Send push notification in the background.
71✔
638
    this.pushNotificationSender?.send(task);
71✔
639
  }
71✔
640

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

1✔
654
    // re-throw error for blocking case to catch
1✔
655
    if (!firstResultRejector) {
1!
656
      throw error;
×
657
    }
×
658

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

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

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