From f86e0ee418e34927058dce3798d1bb9fc50fc628 Mon Sep 17 00:00:00 2001 From: Aishanee Shah Date: Fri, 8 May 2026 14:36:39 -0400 Subject: [PATCH] fix(core): throw explicit error on dropped tool responses (#26668) --- .../core/src/agents/local-executor.test.ts | 73 ++++++++++++++++++- packages/core/src/agents/local-executor.ts | 26 ++++--- 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/packages/core/src/agents/local-executor.test.ts b/packages/core/src/agents/local-executor.test.ts index cb328e1125..a35dc580b7 100644 --- a/packages/core/src/agents/local-executor.test.ts +++ b/packages/core/src/agents/local-executor.test.ts @@ -2227,6 +2227,69 @@ describe('LocalAgentExecutor', () => { // Agent should terminate with ABORTED status expect(output.terminate_reason).toBe(AgentTerminateMode.ABORTED); }); + + it('should throw a critical error when a tool response is dropped by the scheduler', async () => { + const definition = createTestDefinition([LS_TOOL_NAME]); + const executor = await LocalAgentExecutor.create( + definition, + mockConfig, + onActivity, + ); + + // Turn 1: Model calls two tools + mockModelResponse([ + { name: LS_TOOL_NAME, args: { path: 'dir1' }, id: 'call1' }, + { name: LS_TOOL_NAME, args: { path: 'dir2' }, id: 'call2' }, + ]); + + // Simulate scheduler returning only ONE result for TWO calls (dropped response) + mockScheduleAgentTools.mockResolvedValueOnce([ + { + status: 'success', + request: { callId: 'call1', name: LS_TOOL_NAME }, + response: { + responseParts: [ + { + functionResponse: { + name: LS_TOOL_NAME, + id: 'call1', + response: { ok: true }, + }, + }, + ], + }, + }, + ]); + + await expect( + executor.run({ goal: 'Protocol test' }, signal), + ).rejects.toThrow( + 'Critical System Failure: Tool execution result was lost/dropped by the scheduler', + ); + }); + + it('should throw a critical error when all scheduler results are missing/dropped', async () => { + const definition = createTestDefinition([LS_TOOL_NAME]); + const executor = await LocalAgentExecutor.create( + definition, + mockConfig, + onActivity, + ); + + // Turn 1: Model calls one tool + mockModelResponse([ + { name: LS_TOOL_NAME, args: { path: 'dir1' }, id: 'call1' }, + ]); + + // Simulate scheduler returning NO results (dropped response) + mockScheduleAgentTools.mockResolvedValueOnce([]); + + await expect( + executor.run({ goal: 'Protocol test 2' }, signal), + ).rejects.toThrow( + 'Critical System Failure: Tool execution result was lost/dropped by the scheduler', + ); + }); }); describe('Model Routing', () => { @@ -2334,7 +2397,15 @@ describe('LocalAgentExecutor', () => { }, response: { resultDisplay: 'ls result', - responseParts: [], + responseParts: [ + { + functionResponse: { + name: LS_TOOL_NAME, + id: 'call1', + response: { ok: true }, + }, + }, + ], data: {}, }, }, diff --git a/packages/core/src/agents/local-executor.ts b/packages/core/src/agents/local-executor.ts index b05a80f0b7..8780325ab8 100644 --- a/packages/core/src/agents/local-executor.ts +++ b/packages/core/src/agents/local-executor.ts @@ -1287,25 +1287,29 @@ export class LocalAgentExecutor { } } - // Reconstruct toolResponseParts in the original order + // Ensure exactly one response per function call to satisfy the Gemini API protocol. const toolResponseParts: Part[] = []; for (const [index, functionCall] of functionCalls.entries()) { const callId = functionCall.id ?? `${promptId}-${index}`; const part = syncResults.get(callId); + if (part) { toolResponseParts.push(part); + continue; } - } - // If all authorized tool calls failed (and task isn't complete), provide a generic error. - if ( - functionCalls.length > 0 && - toolResponseParts.length === 0 && - !taskCompleted - ) { - toolResponseParts.push({ - text: 'All tool calls failed or were unauthorized. Please analyze the errors and try an alternative approach.', - }); + const isAborted = signal.aborted; + const isTaskComplete = + functionCall.name === COMPLETE_TASK_TOOL_NAME && taskCompleted; + + // Safely skip missing responses if the run was interrupted or the turn won't be sent back. + if (isAborted || isTaskComplete) { + continue; + } + + throw new Error( + `[LocalAgentExecutor] Critical System Failure: Tool execution result was lost/dropped by the scheduler for callId ${callId} (${functionCall.name}). This indicates an internal race condition or scheduler bug.`, + ); } return {