From 926f787ec05f420f4b4d21370e1de0eb6767ea69 Mon Sep 17 00:00:00 2001 From: Shinya Ishikawa Date: Mon, 9 Jun 2025 23:49:50 +0900 Subject: [PATCH 1/4] Continue agent execution when function calls are pending --- packages/agents-core/src/runImplementation.ts | 16 ++++++++++++++-- packages/agents-core/src/runState.ts | 13 ++++++++++++- .../agents-core/test/runImplementation.test.ts | 2 ++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/agents-core/src/runImplementation.ts b/packages/agents-core/src/runImplementation.ts index c41a6f47..e79887c2 100644 --- a/packages/agents-core/src/runImplementation.ts +++ b/packages/agents-core/src/runImplementation.ts @@ -53,6 +53,7 @@ export type ProcessedResponse = { functions: ToolRunFunction[]; computerActions: ToolRunComputer[]; toolsUsed: string[]; + hasToolsOrApprovalsToRun(): boolean; }; /** @@ -147,6 +148,13 @@ export function processModelResponse( functions: runFunctions, computerActions: runComputerActions, toolsUsed: toolsUsed, + hasToolsOrApprovalsToRun(): boolean { + return ( + runHandoffs.length > 0 || + runFunctions.length > 0 || + runComputerActions.length > 0 + ); + }, }; } @@ -414,7 +422,10 @@ export async function executeToolsAndSideEffects( ); } - if (agent.outputType === 'text') { + if ( + agent.outputType === 'text' && + !processedResponse.hasToolsOrApprovalsToRun() + ) { return new SingleStepResult( originalInput, newResponse, @@ -425,7 +436,8 @@ export async function executeToolsAndSideEffects( output: potentialFinalOutput, }, ); - } else { + } else if (agent.outputType !== 'text' && potentialFinalOutput) { + // Structured output schema => always leads to a final output if we have text const { parser } = getSchemaAndParserFromInputType( agent.outputType, 'final_output', diff --git a/packages/agents-core/src/runState.ts b/packages/agents-core/src/runState.ts index 9ab5d928..6acb049a 100644 --- a/packages/agents-core/src/runState.ts +++ b/packages/agents-core/src/runState.ts @@ -694,7 +694,7 @@ async function deserializeProcessedResponse( }), ); - return { + const result = { newItems: serializedProcessedResponse.newItems.map((item) => deserializeItem(item, agentMap), ), @@ -735,4 +735,15 @@ async function deserializeProcessedResponse( }, ), }; + + return { + ...result, + hasToolsOrApprovalsToRun(): boolean { + return ( + result.handoffs.length > 0 || + result.functions.length > 0 || + result.computerActions.length > 0 + ); + }, + }; } diff --git a/packages/agents-core/test/runImplementation.test.ts b/packages/agents-core/test/runImplementation.test.ts index fdcf8ac9..2a2f3d5c 100644 --- a/packages/agents-core/test/runImplementation.test.ts +++ b/packages/agents-core/test/runImplementation.test.ts @@ -78,6 +78,7 @@ describe('processModelResponse', () => { expect(result.newItems[1].rawItem).toEqual( TEST_MODEL_RESPONSE_WITH_FUNCTION.output[1], ); + expect(result.hasToolsOrApprovalsToRun()).toBe(true); }); }); @@ -438,6 +439,7 @@ describe('processModelResponse edge cases', () => { expect(result.computerActions[0]?.toolCall).toBe(compCall); expect(result.handoffs[0]?.toolCall).toBe(handCall); expect(result.toolsUsed).toEqual(['test', 'computer_use', h.toolName]); + expect(result.hasToolsOrApprovalsToRun()).toBe(true); expect(result.newItems[3]).toBeInstanceOf(MessageOutputItem); }); }); From 635bcdbf38396c01a75a5421b44808f480928f37 Mon Sep 17 00:00:00 2001 From: Shinya Ishikawa Date: Tue, 10 Jun 2025 08:00:54 +0900 Subject: [PATCH 2/4] Add tests --- .../test/runImplementation.test.ts | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/packages/agents-core/test/runImplementation.test.ts b/packages/agents-core/test/runImplementation.test.ts index 2a2f3d5c..96dc08d2 100644 --- a/packages/agents-core/test/runImplementation.test.ts +++ b/packages/agents-core/test/runImplementation.test.ts @@ -25,6 +25,7 @@ import { executeFunctionToolCalls, executeComputerActions, executeHandoffCalls, + executeToolsAndSideEffects, } from '../src/runImplementation'; import { FunctionTool, FunctionToolResult, tool } from '../src/tool'; import { handoff } from '../src/handoff'; @@ -792,3 +793,133 @@ describe('empty execution helpers', () => { expect(comp).toEqual([]); }); }); + +describe('hasToolsOrApprovalsToRun method', () => { + it('returns true when handoffs are pending', () => { + const target = new Agent({ name: 'Target' }); + const h = handoff(target); + const response: ModelResponse = { + output: [{ ...TEST_MODEL_FUNCTION_CALL, name: h.toolName }], + usage: new Usage(), + } as any; + + const result = processModelResponse(response, TEST_AGENT, [], [h]); + expect(result.hasToolsOrApprovalsToRun()).toBe(true); + }); + + it('returns true when function calls are pending', () => { + const result = processModelResponse( + TEST_MODEL_RESPONSE_WITH_FUNCTION, + TEST_AGENT, + [TEST_TOOL], + [], + ); + expect(result.hasToolsOrApprovalsToRun()).toBe(true); + }); + + it('returns true when computer actions are pending', () => { + const computer = computerTool({ + computer: { + environment: 'mac', + dimensions: [10, 10], + screenshot: vi.fn(async () => 'img'), + click: vi.fn(async () => {}), + doubleClick: vi.fn(async () => {}), + drag: vi.fn(async () => {}), + keypress: vi.fn(async () => {}), + move: vi.fn(async () => {}), + scroll: vi.fn(async () => {}), + type: vi.fn(async () => {}), + wait: vi.fn(async () => {}), + }, + }); + const compCall: protocol.ComputerUseCallItem = { + id: 'c1', + type: 'computer_call', + callId: 'c1', + status: 'completed', + action: { type: 'screenshot' }, + }; + const response: ModelResponse = { + output: [compCall], + usage: new Usage(), + } as any; + + const result = processModelResponse(response, TEST_AGENT, [computer], []); + expect(result.hasToolsOrApprovalsToRun()).toBe(true); + }); + + it('returns false when no tools or approvals are pending', () => { + const response: ModelResponse = { + output: [TEST_MODEL_MESSAGE], + usage: new Usage(), + } as any; + + const result = processModelResponse(response, TEST_AGENT, [], []); + expect(result.hasToolsOrApprovalsToRun()).toBe(false); + }); +}); + +describe('executeToolsAndSideEffects', () => { + let runner: Runner; + let state: RunState; + + beforeEach(() => { + runner = new Runner({ tracingDisabled: true }); + state = new RunState(new RunContext(), 'test input', TEST_AGENT, 1); + }); + + it('continues execution when text agent has tools pending', async () => { + const textAgent = new Agent({ name: 'TextAgent', outputType: 'text' }); + const processedResponse = processModelResponse( + TEST_MODEL_RESPONSE_WITH_FUNCTION, + textAgent, + [TEST_TOOL], + [], + ); + + expect(processedResponse.hasToolsOrApprovalsToRun()).toBe(true); + + const result = await withTrace('test', () => + executeToolsAndSideEffects( + textAgent, + 'test input', + [], + TEST_MODEL_RESPONSE_WITH_FUNCTION, + processedResponse, + runner, + state, + ), + ); + + expect(result.nextStep.type).toBe('next_step_run_again'); + }); + + it('returns final output when text agent has no tools pending', async () => { + const textAgent = new Agent({ name: 'TextAgent', outputType: 'text' }); + const response: ModelResponse = { + output: [TEST_MODEL_MESSAGE], + usage: new Usage(), + } as any; + const processedResponse = processModelResponse(response, textAgent, [], []); + + expect(processedResponse.hasToolsOrApprovalsToRun()).toBe(false); + + const result = await withTrace('test', () => + executeToolsAndSideEffects( + textAgent, + 'test input', + [], + response, + processedResponse, + runner, + state, + ), + ); + + expect(result.nextStep.type).toBe('next_step_final_output'); + if (result.nextStep.type === 'next_step_final_output') { + expect(result.nextStep.output).toBe('Hello World'); + } + }); +}); From cff060606412e6aecc665c3bdb3b40cbe396a290 Mon Sep 17 00:00:00 2001 From: Shinya Ishikawa Date: Tue, 10 Jun 2025 08:03:27 +0900 Subject: [PATCH 3/4] Add changeset for tool execution fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .changeset/wet-regions-fold.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wet-regions-fold.md diff --git a/.changeset/wet-regions-fold.md b/.changeset/wet-regions-fold.md new file mode 100644 index 00000000..3a35e5b0 --- /dev/null +++ b/.changeset/wet-regions-fold.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-core': patch +--- + +Continue agent execution when function calls are pending From b53cfd9319f7c2f6607fc3c46b01d783640be9b9 Mon Sep 17 00:00:00 2001 From: Shinya Ishikawa Date: Tue, 10 Jun 2025 08:39:39 +0900 Subject: [PATCH 4/4] Fix mock object --- packages/agents-core/test/runState.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/agents-core/test/runState.test.ts b/packages/agents-core/test/runState.test.ts index 609e9394..9984d100 100644 --- a/packages/agents-core/test/runState.test.ts +++ b/packages/agents-core/test/runState.test.ts @@ -253,6 +253,7 @@ describe('deserialize helpers', () => { handoffs: [], computerActions: [{ toolCall: call, computer: tool }], toolsUsed: [], + hasToolsOrApprovalsToRun: () => true, }; const restored = await RunState.fromString(agent, state.toString()); @@ -277,6 +278,7 @@ describe('deserialize helpers', () => { handoffs: [], computerActions: [{ toolCall: call, computer: tool }], toolsUsed: [], + hasToolsOrApprovalsToRun: () => true, }; state._currentStep = { type: 'next_step_handoff',