From dc82d97b09ead3a71631896c428807a413ddb0ef Mon Sep 17 00:00:00 2001 From: Keith Schaab Date: Mon, 4 May 2026 21:23:21 +0000 Subject: [PATCH 1/8] Fixing race condition in task updates --- .../src/agent/task-event-driven.test.ts | 54 +++++++-- packages/a2a-server/src/agent/task.test.ts | 106 ++++++++++++++++++ packages/a2a-server/src/agent/task.ts | 11 +- packages/a2a-server/src/http/app.test.ts | 26 ++++- 4 files changed, 181 insertions(+), 16 deletions(-) diff --git a/packages/a2a-server/src/agent/task-event-driven.test.ts b/packages/a2a-server/src/agent/task-event-driven.test.ts index 86436fa811e..5fc548a8f49 100644 --- a/packages/a2a-server/src/agent/task-event-driven.test.ts +++ b/packages/a2a-server/src/agent/task-event-driven.test.ts @@ -66,6 +66,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall], + schedulerId: 'task-id', }); expect(mockEventBus.publish).toHaveBeenCalledWith( @@ -106,6 +107,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall], + schedulerId: 'task-id', }); // Simulate A2A client confirmation @@ -148,7 +150,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); // Simulate Rejection (Cancel) const handled = await ( @@ -174,7 +180,11 @@ describe('Task Event-Driven Scheduler', () => { correlationId: 'corr-2', confirmationDetails: { type: 'info', title: 'test', prompt: 'test' }, }; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall2] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall2], + schedulerId: 'task-id', + }); // Simulate ModifyWithEditor const handled2 = await ( @@ -215,7 +225,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); // Simulate ProceedOnce for MCP const handled = await ( @@ -255,7 +269,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); const handled = await ( task as unknown as { @@ -294,7 +312,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); const handled = await ( task as unknown as { @@ -333,7 +355,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); const handled = await ( task as unknown as { @@ -376,7 +402,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (yoloMessageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); // Should NOT auto-publish ProceedOnce anymore, because PolicyEngine handles it directly expect(yoloMessageBus.publish).not.toHaveBeenCalledWith( @@ -419,6 +449,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall], + schedulerId: 'task-id', }); // Should publish artifact update for output @@ -453,7 +484,11 @@ describe('Task Event-Driven Scheduler', () => { const handler = (messageBus.subscribe as Mock).mock.calls.find( (call: unknown[]) => call[0] === MessageBusType.TOOL_CALLS_UPDATE, )?.[1]; - handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall] }); + handler({ + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [toolCall], + schedulerId: 'task-id', + }); // The tool should be complete and registered appropriately, eventually // triggering the toolCompletionPromise resolution when all clear. @@ -533,6 +568,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall1, toolCall2], + schedulerId: 'task-id', }); // Confirm first tool call @@ -600,6 +636,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall1, toolCall2], + schedulerId: 'task-id', }); // Should NOT transition to input-required yet @@ -621,6 +658,7 @@ describe('Task Event-Driven Scheduler', () => { handler({ type: MessageBusType.TOOL_CALLS_UPDATE, toolCalls: [toolCall1Complete, toolCall2], + schedulerId: 'task-id', }); // Now it should transition diff --git a/packages/a2a-server/src/agent/task.test.ts b/packages/a2a-server/src/agent/task.test.ts index 26039ae3aa3..27e6c28fed3 100644 --- a/packages/a2a-server/src/agent/task.test.ts +++ b/packages/a2a-server/src/agent/task.test.ts @@ -460,4 +460,110 @@ describe('Task', () => { expect(task.currentPromptId).toBe(expectedPromptId2); }); }); + + describe('Race Condition Fix', () => { + const mockConfig = createMockConfig(); + const mockEventBus: ExecutionEventBus = { + publish: vi.fn(), + on: vi.fn(), + off: vi.fn(), + once: vi.fn(), + removeAllListeners: vi.fn(), + finished: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should NOT transition to input-required if a tool is still validating', async () => { + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + // Manually register two tool calls + task['_registerToolCall']('tool-1', 'awaiting_approval'); + task['_registerToolCall']('tool-2', 'validating'); + + // Call checkInputRequiredState (private) + task['checkInputRequiredState'](); + + // Verify task state did NOT change to input-required + expect(task.taskState).not.toBe('input-required'); + expect(mockEventBus.publish).not.toHaveBeenCalledWith( + expect.objectContaining({ + status: expect.objectContaining({ state: 'input-required' }), + }), + ); + }); + + it('should transition to input-required if all active tools are awaiting approval', async () => { + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + // Transition from submitted to working first to simulate normal flow + task.taskState = 'working'; + + // Manually register tool calls + task['_registerToolCall']('tool-1', 'awaiting_approval'); + + // Call checkInputRequiredState + task['checkInputRequiredState'](); + + // Verify task state changed to input-required + expect(task.taskState).toBe('input-required'); + expect(mockEventBus.publish).toHaveBeenCalledWith( + expect.objectContaining({ + status: expect.objectContaining({ state: 'input-required' }), + }), + ); + }); + + it('handleEventDrivenToolCallsUpdate should ignore events for other schedulers', async () => { + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + const handleEventDrivenToolCallSpy = vi.spyOn( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + task as any, + 'handleEventDrivenToolCall', + ); + + const otherEvent = { + type: 'tool-calls-update', + toolCalls: [{ request: { callId: '1' }, status: 'executing' }], + schedulerId: 'other-task-id', + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + task['handleEventDrivenToolCallsUpdate'](otherEvent as any); + + expect(handleEventDrivenToolCallSpy).not.toHaveBeenCalled(); + + const ownEvent = { + type: 'tool-calls-update', + toolCalls: [{ request: { callId: '1' }, status: 'executing' }], + schedulerId: 'task-id', + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + task['handleEventDrivenToolCallsUpdate'](ownEvent as any); + + expect(handleEventDrivenToolCallSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index a76054263fa..6e87a033ddf 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -413,7 +413,10 @@ export class Task { private handleEventDrivenToolCallsUpdate( event: ToolCallsUpdateMessage, ): void { - if (event.type !== MessageBusType.TOOL_CALLS_UPDATE) { + if ( + event.type !== MessageBusType.TOOL_CALLS_UPDATE || + event.schedulerId !== this.id + ) { return; } @@ -508,7 +511,11 @@ export class Task { let isExecuting = false; for (const [callId, status] of this.pendingToolCalls.entries()) { - if (status === 'executing' || status === 'scheduled') { + if ( + status === 'executing' || + status === 'scheduled' || + status === 'validating' + ) { isExecuting = true; } else if ( status === 'awaiting_approval' && diff --git a/packages/a2a-server/src/http/app.test.ts b/packages/a2a-server/src/http/app.test.ts index 4a883992b52..ac4568ba3d5 100644 --- a/packages/a2a-server/src/http/app.test.ts +++ b/packages/a2a-server/src/http/app.test.ts @@ -381,11 +381,11 @@ describe('E2E Tests', () => { ]); // 6. Tool 1 is awaiting approval. - const toolCallAwaitEvent = events[5].result as TaskStatusUpdateEvent; - expect(toolCallAwaitEvent.metadata?.['coderAgent']).toMatchObject({ + const toolCallAwaitEvent1 = events[5].result as TaskStatusUpdateEvent; + expect(toolCallAwaitEvent1.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-confirmation', }); - expect(toolCallAwaitEvent.status.message?.parts).toMatchObject([ + expect(toolCallAwaitEvent1.status.message?.parts).toMatchObject([ { data: { request: { callId: 'test-call-id-1' }, @@ -394,14 +394,28 @@ describe('E2E Tests', () => { }, ]); - // 7. The final event is "input-required". - const finalEvent = events[6].result as TaskStatusUpdateEvent; + // 7. Tool 2 is awaiting approval. + const toolCallAwaitEvent2 = events[6].result as TaskStatusUpdateEvent; + expect(toolCallAwaitEvent2.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-confirmation', + }); + expect(toolCallAwaitEvent2.status.message?.parts).toMatchObject([ + { + data: { + request: { callId: 'test-call-id-2' }, + status: 'awaiting_approval', + }, + }, + ]); + + // 8. The final event is "input-required". + const finalEvent = events[7].result as TaskStatusUpdateEvent; expect(finalEvent.final).toBe(true); expect(finalEvent.status.state).toBe('input-required'); // The scheduler now waits for approval, so no more events are sent. assertUniqueFinalEventIsLast(events); - expect(events.length).toBe(7); + expect(events.length).toBe(8); }); it('should handle multiple tool calls sequentially in YOLO mode', async () => { From ebd9d83c212a4a4a3b3031df411f73ffa254cb73 Mon Sep 17 00:00:00 2001 From: Keith Schaab Date: Tue, 5 May 2026 00:40:38 +0000 Subject: [PATCH 2/8] fix(a2a-server): improve tool call state tracking and client status reporting - Enhanced Tool Call Tracking: Added pendingOutcomes to track tool call outcomes alongside statuses, ensuring more reliable change detection. - Improved Waiting Logic: Refined _isWaitingForToolExecutionOrApproval to better handle 'validating' states and tools already confirmed by the agent. - Client-Side Consistency: Mapped the internal 'validating' tool call status to 'scheduled' when serializing for clients, ensuring external state reporting is consistent. - Test Updates: Adjusted E2E tests to align with the new 'scheduled' status reporting. --- packages/a2a-server/src/agent/task.test.ts | 127 +++++++++++++++++++-- packages/a2a-server/src/agent/task.ts | 30 +++-- packages/a2a-server/src/http/app.test.ts | 90 ++++++++++----- 3 files changed, 196 insertions(+), 51 deletions(-) diff --git a/packages/a2a-server/src/agent/task.test.ts b/packages/a2a-server/src/agent/task.test.ts index 27e6c28fed3..92b7788cca9 100644 --- a/packages/a2a-server/src/agent/task.test.ts +++ b/packages/a2a-server/src/agent/task.test.ts @@ -12,6 +12,9 @@ import { type ToolCallRequestInfo, type GitService, type CompletedToolCall, + type ToolCall, + type ToolCallsUpdateMessage, + MessageBusType, } from '@google/gemini-cli-core'; import { createMockConfig } from '../utils/testing_utils.js'; import type { ExecutionEventBus, RequestContext } from '@a2a-js/sdk/server'; @@ -538,32 +541,132 @@ describe('Task', () => { ); const handleEventDrivenToolCallSpy = vi.spyOn( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - task as any, + task as unknown as { + handleEventDrivenToolCall: Task['handleEventDrivenToolCall']; + }, 'handleEventDrivenToolCall', ); - const otherEvent = { - type: 'tool-calls-update', - toolCalls: [{ request: { callId: '1' }, status: 'executing' }], + const otherEvent: ToolCallsUpdateMessage = { + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [ + { request: { callId: '1' }, status: 'executing' } as ToolCall, + ], schedulerId: 'other-task-id', }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - task['handleEventDrivenToolCallsUpdate'](otherEvent as any); + // @ts-expect-error - Accessing private method + task['handleEventDrivenToolCallsUpdate'](otherEvent); expect(handleEventDrivenToolCallSpy).not.toHaveBeenCalled(); - const ownEvent = { - type: 'tool-calls-update', - toolCalls: [{ request: { callId: '1' }, status: 'executing' }], + const ownEvent: ToolCallsUpdateMessage = { + type: MessageBusType.TOOL_CALLS_UPDATE, + toolCalls: [ + { request: { callId: '1' }, status: 'executing' } as ToolCall, + ], schedulerId: 'task-id', }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - task['handleEventDrivenToolCallsUpdate'](ownEvent as any); + // @ts-expect-error - Accessing private method + task['handleEventDrivenToolCallsUpdate'](ownEvent); expect(handleEventDrivenToolCallSpy).toHaveBeenCalled(); }); }); + + describe('Serialization and Mapping', () => { + it('should map internal "validating" status to "scheduled" for the client and include outcome', async () => { + const mockConfig = createMockConfig(); + const mockEventBus: ExecutionEventBus = { + publish: vi.fn(), + on: vi.fn(), + off: vi.fn(), + once: vi.fn(), + removeAllListeners: vi.fn(), + finished: vi.fn(), + }; + + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + const mockToolCall = { + request: { callId: 'tool-1' }, + status: 'validating', + outcome: 'accepted', + tool: { name: 'test-tool' }, + }; + + // @ts-expect-error - Accessing private method + const message = task['toolStatusMessage']( + mockToolCall as unknown as ToolCall, + 'task-id', + 'context-id', + ); + const serialized = ( + message.parts![0] as { + data: { status: string; outcome: string }; + } + ).data; + + expect(serialized.status).toBe('scheduled'); + expect(serialized.outcome).toBe('accepted'); + }); + + it('should correctly detect changes when status or outcome changes', async () => { + const mockConfig = createMockConfig(); + const mockEventBus: ExecutionEventBus = { + publish: vi.fn(), + on: vi.fn(), + off: vi.fn(), + once: vi.fn(), + removeAllListeners: vi.fn(), + finished: vi.fn(), + }; + + // @ts-expect-error - Calling private constructor + const task = new Task( + 'task-id', + 'context-id', + mockConfig as Config, + mockEventBus, + ); + + const toolCall1 = { + request: { callId: 'tool-1' }, + status: 'awaiting_approval', + }; + + // First update - should trigger change + // @ts-expect-error - Accessing private method + const changed1 = task['handleEventDrivenToolCall']( + toolCall1 as unknown as ToolCall, + ); + expect(changed1).toBe(true); + + // Second update with same status - should NOT trigger change + // @ts-expect-error - Accessing private method + const changed2 = task['handleEventDrivenToolCall']( + toolCall1 as unknown as ToolCall, + ); + expect(changed2).toBe(false); + + // Update with new outcome - SHOULD trigger change + const toolCall2 = { + request: { callId: 'tool-1' }, + status: 'awaiting_approval', + outcome: 'accepted', + }; + // @ts-expect-error - Accessing private method + const changed3 = task['handleEventDrivenToolCall']( + toolCall2 as unknown as ToolCall, + ); + expect(changed3).toBe(true); + }); + }); }); diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index 6e87a033ddf..a8963c78af5 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -11,6 +11,7 @@ import { GeminiEventType, ToolConfirmationOutcome, ApprovalMode, + CoreToolCallStatus, getAllMCPServerStatuses, MCPServerStatus, isNodeError, @@ -95,6 +96,7 @@ export class Task { // For tool waiting logic private pendingToolCalls: Map = new Map(); //toolCallId --> status + private pendingOutcomes: Map = new Map(); // toolCallId --> outcome private toolsAlreadyConfirmed: Set = new Set(); private toolCompletionPromise?: Promise; private toolCompletionNotifier?: { @@ -429,7 +431,7 @@ export class Task { this.checkInputRequiredState(); } - private handleEventDrivenToolCall(tc: ToolCall): void { + private handleEventDrivenToolCall(tc: ToolCall): boolean { const callId = tc.request.callId; // Do not process events for tools that have already been finalized. @@ -439,11 +441,16 @@ export class Task { this.processedToolCallIds.has(callId) || this.completedToolCalls.some((c) => c.request.callId === callId) ) { - return; + return false; } const previousStatus = this.pendingToolCalls.get(callId); - const hasChanged = previousStatus !== tc.status; + const previousOutcome = this.pendingOutcomes.get(callId); + const hasChanged = + previousStatus !== tc.status || previousOutcome !== tc.outcome; + + // Update outcome tracking + this.pendingOutcomes.set(callId, tc.outcome); // 1. Handle Output if (tc.status === 'executing' && tc.liveOutput) { @@ -457,6 +464,7 @@ export class Task { tc.status === 'cancelled' ) { this.toolsAlreadyConfirmed.delete(callId); + this.pendingOutcomes.delete(callId); if (hasChanged) { logger.info( `[Task] Tool call ${callId} completed with status: ${tc.status}`, @@ -499,6 +507,8 @@ export class Task { ); this.eventBus?.publish(statusUpdate); } + + return hasChanged; } private checkInputRequiredState(): void { @@ -514,13 +524,11 @@ export class Task { if ( status === 'executing' || status === 'scheduled' || - status === 'validating' + status === 'validating' || + this.toolsAlreadyConfirmed.has(callId) ) { isExecuting = true; - } else if ( - status === 'awaiting_approval' && - !this.toolsAlreadyConfirmed.has(callId) - ) { + } else if (status === 'awaiting_approval') { isAwaitingApproval = true; } } @@ -581,8 +589,14 @@ export class Task { 'confirmationDetails', 'liveOutput', 'response', + 'outcome', ); + // Map internal 'validating' status to 'scheduled' for the client + if (serializableToolCall.status === 'validating') { + serializableToolCall.status = CoreToolCallStatus.Scheduled; + } + if (tc.tool) { const toolFields = this._pickFields( tc.tool, diff --git a/packages/a2a-server/src/http/app.test.ts b/packages/a2a-server/src/http/app.test.ts index ac4568ba3d5..7534ad66a8f 100644 --- a/packages/a2a-server/src/http/app.test.ts +++ b/packages/a2a-server/src/http/app.test.ts @@ -228,7 +228,7 @@ describe('E2E Tests', () => { expect(toolCallUpdateEvent.status.message?.parts).toMatchObject([ { data: { - status: 'validating', + status: 'scheduled', request: { callId: 'test-call-id' }, }, }, @@ -330,7 +330,7 @@ describe('E2E Tests', () => { expect(toolCallValidateEvent1.status.message?.parts).toMatchObject([ { data: { - status: 'validating', + status: 'scheduled', request: { callId: 'test-call-id-1' }, }, }, @@ -352,7 +352,7 @@ describe('E2E Tests', () => { kind: 'state-change', }); - // 4. Tool 1 is validating. + // 4. Tool 1 is scheduled. const toolCallUpdate1 = events[3].result as TaskStatusUpdateEvent; expect(toolCallUpdate1.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', @@ -361,12 +361,12 @@ describe('E2E Tests', () => { { data: { request: { callId: 'test-call-id-1' }, - status: 'validating', + status: 'scheduled', }, }, ]); - // 5. Tool 2 is validating. + // 5. Tool 2 is scheduled. const toolCallUpdate2 = events[4].result as TaskStatusUpdateEvent; expect(toolCallUpdate2.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', @@ -375,7 +375,7 @@ describe('E2E Tests', () => { { data: { request: { callId: 'test-call-id-2' }, - status: 'validating', + status: 'scheduled', }, }, ]); @@ -513,7 +513,7 @@ describe('E2E Tests', () => { // Tool 1 Lifecycle { kind: 'tool-call-update', - status: 'validating', + status: 'scheduled', callId: 'test-call-id-1', }, { @@ -534,7 +534,7 @@ describe('E2E Tests', () => { // Tool 2 Lifecycle { kind: 'tool-call-update', - status: 'validating', + status: 'scheduled', callId: 'test-call-id-2', }, { @@ -617,26 +617,40 @@ describe('E2E Tests', () => { expect(workingEvent2.kind).toBe('status-update'); expect(workingEvent2.status.state).toBe('working'); - // Status update: tool-call-update (validating) - const validatingEvent = events[3].result as TaskStatusUpdateEvent; - expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({ + // Status update: tool-call-update (scheduled) + const scheduledEvent1 = events[3].result as TaskStatusUpdateEvent; + expect(scheduledEvent1.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); - expect(validatingEvent.status.message?.parts).toMatchObject([ + expect(scheduledEvent1.status.message?.parts).toMatchObject([ { data: { - status: 'validating', + status: 'scheduled', request: { callId: 'test-call-id-no-approval' }, }, }, ]); // Status update: tool-call-update (scheduled) - const scheduledEvent = events[4].result as TaskStatusUpdateEvent; - expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({ + const scheduledEvent2 = events[4].result as TaskStatusUpdateEvent; + expect(scheduledEvent2.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); - expect(scheduledEvent.status.message?.parts).toMatchObject([ + expect(scheduledEvent2.status.message?.parts).toMatchObject([ + { + data: { + status: 'scheduled', + request: { callId: 'test-call-id-no-approval' }, + }, + }, + ]); + + // Status update: tool-call-update (scheduled) + const scheduledEvent3 = events[5].result as TaskStatusUpdateEvent; + expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(scheduledEvent3.status.message?.parts).toMatchObject([ { data: { status: 'scheduled', @@ -646,7 +660,7 @@ describe('E2E Tests', () => { ]); // Status update: tool-call-update (executing) - const executingEvent = events[5].result as TaskStatusUpdateEvent; + const executingEvent = events[6].result as TaskStatusUpdateEvent; expect(executingEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); @@ -660,7 +674,7 @@ describe('E2E Tests', () => { ]); // Status update: tool-call-update (success) - const successEvent = events[6].result as TaskStatusUpdateEvent; + const successEvent = events[7].result as TaskStatusUpdateEvent; expect(successEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); @@ -674,12 +688,12 @@ describe('E2E Tests', () => { ]); // Status update: working (before sending tool result to LLM) - const workingEvent3 = events[7].result as TaskStatusUpdateEvent; + const workingEvent3 = events[8].result as TaskStatusUpdateEvent; expect(workingEvent3.kind).toBe('status-update'); expect(workingEvent3.status.state).toBe('working'); // Status update: text-content (final LLM response) - const textContentEvent = events[8].result as TaskStatusUpdateEvent; + const textContentEvent = events[9].result as TaskStatusUpdateEvent; expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'text-content', }); @@ -688,7 +702,7 @@ describe('E2E Tests', () => { ]); assertUniqueFinalEventIsLast(events); - expect(events.length).toBe(10); + expect(events.length).toBe(11); }); it('should bypass tool approval in YOLO mode', async () => { @@ -748,15 +762,15 @@ describe('E2E Tests', () => { expect(workingEvent2.kind).toBe('status-update'); expect(workingEvent2.status.state).toBe('working'); - // Status update: tool-call-update (validating) - const validatingEvent = events[3].result as TaskStatusUpdateEvent; - expect(validatingEvent.metadata?.['coderAgent']).toMatchObject({ + // Status update: tool-call-update (scheduled) + const scheduledEvent = events[3].result as TaskStatusUpdateEvent; + expect(scheduledEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); - expect(validatingEvent.status.message?.parts).toMatchObject([ + expect(scheduledEvent.status.message?.parts).toMatchObject([ { data: { - status: 'validating', + status: 'scheduled', request: { callId: 'test-call-id-yolo' }, }, }, @@ -776,8 +790,22 @@ describe('E2E Tests', () => { }, ]); + // Status update: tool-call-update (scheduled) + const scheduledEvent3 = events[5].result as TaskStatusUpdateEvent; + expect(scheduledEvent3.metadata?.['coderAgent']).toMatchObject({ + kind: 'tool-call-update', + }); + expect(scheduledEvent3.status.message?.parts).toMatchObject([ + { + data: { + status: 'scheduled', + request: { callId: 'test-call-id-yolo' }, + }, + }, + ]); + // Status update: tool-call-update (executing) - const executingEvent = events[5].result as TaskStatusUpdateEvent; + const executingEvent = events[6].result as TaskStatusUpdateEvent; expect(executingEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); @@ -791,7 +819,7 @@ describe('E2E Tests', () => { ]); // Status update: tool-call-update (success) - const successEvent = events[6].result as TaskStatusUpdateEvent; + const successEvent = events[7].result as TaskStatusUpdateEvent; expect(successEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'tool-call-update', }); @@ -805,12 +833,12 @@ describe('E2E Tests', () => { ]); // Status update: working (before sending tool result to LLM) - const workingEvent3 = events[7].result as TaskStatusUpdateEvent; + const workingEvent3 = events[8].result as TaskStatusUpdateEvent; expect(workingEvent3.kind).toBe('status-update'); expect(workingEvent3.status.state).toBe('working'); // Status update: text-content (final LLM response) - const textContentEvent = events[8].result as TaskStatusUpdateEvent; + const textContentEvent = events[9].result as TaskStatusUpdateEvent; expect(textContentEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'text-content', }); @@ -819,7 +847,7 @@ describe('E2E Tests', () => { ]); assertUniqueFinalEventIsLast(events); - expect(events.length).toBe(10); + expect(events.length).toBe(11); }); it('should include traceId in status updates when available', async () => { From 5eaae482d8c2da708f57865be6a90665f73d606a Mon Sep 17 00:00:00 2001 From: Keith Schaab Date: Mon, 4 May 2026 18:51:11 -0700 Subject: [PATCH 3/8] Update packages/a2a-server/src/agent/task.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/a2a-server/src/agent/task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index a8963c78af5..df23180ac47 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -96,7 +96,7 @@ export class Task { // For tool waiting logic private pendingToolCalls: Map = new Map(); //toolCallId --> status - private pendingOutcomes: Map = new Map(); // toolCallId --> outcome + private pendingOutcomes: Map = new Map(); // toolCallId --> outcome private toolsAlreadyConfirmed: Set = new Set(); private toolCompletionPromise?: Promise; private toolCompletionNotifier?: { From c97c705017be5bbee5dbd337d4612b8db30fddaa Mon Sep 17 00:00:00 2001 From: Keith Schaab Date: Mon, 4 May 2026 18:51:21 -0700 Subject: [PATCH 4/8] Update packages/a2a-server/src/agent/task.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/a2a-server/src/agent/task.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index df23180ac47..f962821daf7 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -522,9 +522,9 @@ export class Task { for (const [callId, status] of this.pendingToolCalls.entries()) { if ( - status === 'executing' || - status === 'scheduled' || - status === 'validating' || + status === CoreToolCallStatus.Executing || + status === CoreToolCallStatus.Scheduled || + status === CoreToolCallStatus.Validating || this.toolsAlreadyConfirmed.has(callId) ) { isExecuting = true; From 24685a55885a1ddb19432095f1f47e060f8231d7 Mon Sep 17 00:00:00 2001 From: Keith Schaab Date: Mon, 4 May 2026 18:51:30 -0700 Subject: [PATCH 5/8] Update packages/a2a-server/src/agent/task.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/a2a-server/src/agent/task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index f962821daf7..b2539670c31 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -528,7 +528,7 @@ export class Task { this.toolsAlreadyConfirmed.has(callId) ) { isExecuting = true; - } else if (status === 'awaiting_approval') { + } else if (status === CoreToolCallStatus.AwaitingApproval) { isAwaitingApproval = true; } } From 7e7f52d928d17e927f8e35a6f9c8b53ddf6b7c40 Mon Sep 17 00:00:00 2001 From: Keith Schaab Date: Mon, 4 May 2026 18:51:39 -0700 Subject: [PATCH 6/8] Update packages/a2a-server/src/agent/task.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/a2a-server/src/agent/task.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index b2539670c31..8d9e19944b1 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -593,7 +593,7 @@ export class Task { ); // Map internal 'validating' status to 'scheduled' for the client - if (serializableToolCall.status === 'validating') { + if (serializableToolCall.status === CoreToolCallStatus.Validating) { serializableToolCall.status = CoreToolCallStatus.Scheduled; } From 73dd07a0e9c1bd1e0b370913141ae78289d75d8f Mon Sep 17 00:00:00 2001 From: Keith Schaab Date: Tue, 5 May 2026 02:23:31 +0000 Subject: [PATCH 7/8] Fixing unused linter warning --- packages/a2a-server/src/agent/task.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/a2a-server/src/agent/task.test.ts b/packages/a2a-server/src/agent/task.test.ts index 92b7788cca9..2a3a20ebf43 100644 --- a/packages/a2a-server/src/agent/task.test.ts +++ b/packages/a2a-server/src/agent/task.test.ts @@ -555,7 +555,6 @@ describe('Task', () => { schedulerId: 'other-task-id', }; - // @ts-expect-error - Accessing private method task['handleEventDrivenToolCallsUpdate'](otherEvent); expect(handleEventDrivenToolCallSpy).not.toHaveBeenCalled(); @@ -568,7 +567,6 @@ describe('Task', () => { schedulerId: 'task-id', }; - // @ts-expect-error - Accessing private method task['handleEventDrivenToolCallsUpdate'](ownEvent); expect(handleEventDrivenToolCallSpy).toHaveBeenCalled(); @@ -602,7 +600,6 @@ describe('Task', () => { tool: { name: 'test-tool' }, }; - // @ts-expect-error - Accessing private method const message = task['toolStatusMessage']( mockToolCall as unknown as ToolCall, 'task-id', @@ -643,14 +640,12 @@ describe('Task', () => { }; // First update - should trigger change - // @ts-expect-error - Accessing private method const changed1 = task['handleEventDrivenToolCall']( toolCall1 as unknown as ToolCall, ); expect(changed1).toBe(true); // Second update with same status - should NOT trigger change - // @ts-expect-error - Accessing private method const changed2 = task['handleEventDrivenToolCall']( toolCall1 as unknown as ToolCall, ); @@ -662,7 +657,6 @@ describe('Task', () => { status: 'awaiting_approval', outcome: 'accepted', }; - // @ts-expect-error - Accessing private method const changed3 = task['handleEventDrivenToolCall']( toolCall2 as unknown as ToolCall, ); From 36c9bdc10320aeeb2ca889e21b63b8d047c4350f Mon Sep 17 00:00:00 2001 From: Keith Schaab Date: Tue, 5 May 2026 02:31:56 +0000 Subject: [PATCH 8/8] Fixing prettier error --- packages/a2a-server/src/agent/task.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index 8d9e19944b1..3fcb5c3ef50 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -96,7 +96,8 @@ export class Task { // For tool waiting logic private pendingToolCalls: Map = new Map(); //toolCallId --> status - private pendingOutcomes: Map = new Map(); // toolCallId --> outcome + private pendingOutcomes: Map = + new Map(); // toolCallId --> outcome private toolsAlreadyConfirmed: Set = new Set(); private toolCompletionPromise?: Promise; private toolCompletionNotifier?: {