fix(core): throw explicit error on dropped tool responses (#26668)

This commit is contained in:
Aishanee Shah
2026-05-08 14:36:39 -04:00
committed by GitHub
parent 01635ddb83
commit f86e0ee418
2 changed files with 87 additions and 12 deletions
@@ -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: {},
},
},
+15 -11
View File
@@ -1287,25 +1287,29 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
}
}
// 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 {