diff --git a/.changeset/modern-bananas-greet.md b/.changeset/modern-bananas-greet.md new file mode 100644 index 00000000..f6cc8236 --- /dev/null +++ b/.changeset/modern-bananas-greet.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-core': patch +--- + +Prevent premature agent termination when models return both text and tool calls diff --git a/packages/agents-core/src/runImplementation.ts b/packages/agents-core/src/runImplementation.ts index c41a6f47..fe60474c 100644 --- a/packages/agents-core/src/runImplementation.ts +++ b/packages/agents-core/src/runImplementation.ts @@ -404,7 +404,13 @@ export async function executeToolsAndSideEffects( : undefined; // if there is no output we just run again - if (!potentialFinalOutput) { + // Some models return both text and tool calls, + // So If there //are tool results, we should continue the agent loop + if ( + !potentialFinalOutput || + functionResults.length > 0 || + computerResults?.length > 0 + ) { return new SingleStepResult( originalInput, newResponse, diff --git a/packages/agents-core/test/runImplementation.test.ts b/packages/agents-core/test/runImplementation.test.ts index fdcf8ac9..5d67c0a2 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'; @@ -40,6 +41,7 @@ import { TEST_MODEL_RESPONSE_WITH_FUNCTION, TEST_TOOL, FakeModelProvider, + fakeModelMessage, } from './stubs'; import { computerTool } from '../src/tool'; import * as protocol from '../src/types/protocol'; @@ -790,3 +792,39 @@ describe('empty execution helpers', () => { expect(comp).toEqual([]); }); }); + +describe('executeToolsAndSideEffects with text and tool calls', () => { + it('continues agent loop when model returns both text and tool calls', async () => { + const runner = new Runner({ tracingDisabled: true }); + const state = new RunState(new RunContext(), '', TEST_AGENT, 1); + + const responseWithBothTextAndTool: ModelResponse = { + output: [ + TEST_MODEL_FUNCTION_CALL, + fakeModelMessage('Some text response'), + ], + usage: new Usage(), + }; + + const processedResponse = processModelResponse( + responseWithBothTextAndTool, + TEST_AGENT, + [TEST_TOOL], + [], + ); + + const result = await withTrace('test', () => + executeToolsAndSideEffects( + TEST_AGENT, + '', + [], + responseWithBothTextAndTool, + processedResponse, + runner, + state, + ), + ); + + expect(result.nextStep.type).toBe('next_step_run_again'); + }); +});