diff --git a/.changeset/stupid-pugs-deliver.md b/.changeset/stupid-pugs-deliver.md new file mode 100644 index 00000000..47262162 --- /dev/null +++ b/.changeset/stupid-pugs-deliver.md @@ -0,0 +1,5 @@ +--- +"@openai/agents-core": patch +--- + +fix: #753 Emit agent_tool_end event when function tools throw errors diff --git a/packages/agents-core/src/runImplementation.ts b/packages/agents-core/src/runImplementation.ts index 53c832ee..bd66582e 100644 --- a/packages/agents-core/src/runImplementation.ts +++ b/packages/agents-core/src/runImplementation.ts @@ -1357,6 +1357,16 @@ export async function executeFunctionToolCalls( error: String(error), }, }); + + // Emit agent_tool_end even on error to maintain consistent event lifecycle + const errorResult = String(error); + runner.emit('agent_tool_end', state._context, agent, toolRun.tool, errorResult, { + toolCall: toolRun.toolCall, + }); + agent.emit('agent_tool_end', state._context, toolRun.tool, errorResult, { + toolCall: toolRun.toolCall, + }); + throw error; } }, diff --git a/packages/agents-core/test/runImplementation.test.ts b/packages/agents-core/test/runImplementation.test.ts index 0e535104..68ffb987 100644 --- a/packages/agents-core/test/runImplementation.test.ts +++ b/packages/agents-core/test/runImplementation.test.ts @@ -1771,6 +1771,50 @@ describe('executeFunctionToolCalls', () => { expect(invokeSpy).toHaveBeenCalled(); }); + it('emits agent_tool_end even when function tool throws error', async () => { + const errorMessage = 'Tool execution failed'; + const t = tool({ + name: 'failing_tool', + description: 'A tool that throws an error', + parameters: z.object({}), + // Disable default error handler to force raw error propagation + errorFunction: null, + execute: vi.fn(async () => { + throw new Error(errorMessage); + }), + }) as any; + + const start = vi.fn(); + const end = vi.fn(); + runner.on('agent_tool_start', start); + runner.on('agent_tool_end', end); + + // Tool should throw because we disabled the error handler + await expect( + withTrace('test', () => + executeFunctionToolCalls( + state._currentAgent, + [{ toolCall, tool: t }], + runner, + state, + ), + ), + ).rejects.toThrow(); + + // Both start and end should be emitted, even though tool threw + expect(start).toHaveBeenCalledWith(state._context, state._currentAgent, t, { + toolCall, + }); + expect(end).toHaveBeenCalled(); + expect(end).toHaveBeenCalledWith( + state._context, + state._currentAgent, + t, + expect.stringContaining(errorMessage), + { toolCall }, + ); + }); + it('propagates nested run result interruptions when provided by agent tools', async () => { const t = makeTool(false); const nestedAgent = new Agent({ name: 'Nested' }) as Agent<