From 95d3dfafe939a198b84be689ac6f005ad42ecc00 Mon Sep 17 00:00:00 2001 From: Kumbham Ajay Goud Date: Tue, 26 Aug 2025 13:36:18 +0530 Subject: [PATCH 1/3] fix: emit agent_end lifecycle event for streaming agents - Fix streaming agents not calling agent_end lifecycle hook when completing execution - Add agent_end event emissions in streaming loop to match non-streaming behavior - Add tests to verify agent_end events are emitted for both streaming and non-streaming agents - Resolves issue where users could not collect usage information from streaming agent runs Fixes #371 --- .../fix-streaming-agent-end-lifecycle.md | 9 +++ packages/agents-core/src/run.ts | 11 ++++ packages/agents-core/test/run.stream.test.ts | 64 +++++++++++++++++++ packages/agents-core/test/run.test.ts | 29 +++++++++ 4 files changed, 113 insertions(+) create mode 100644 .changeset/fix-streaming-agent-end-lifecycle.md diff --git a/.changeset/fix-streaming-agent-end-lifecycle.md b/.changeset/fix-streaming-agent-end-lifecycle.md new file mode 100644 index 00000000..6756942d --- /dev/null +++ b/.changeset/fix-streaming-agent-end-lifecycle.md @@ -0,0 +1,9 @@ +--- +'@openai/agents-core': patch +--- + +Fix streaming agents not calling agent_end lifecycle hook + +Streaming agents were not emitting the `agent_end` lifecycle event when completing execution, while non-streaming agents were correctly emitting this event. This fix ensures that both the agent instance and the runner emit the `agent_end` event for streaming agents when they produce a final output, maintaining consistency with the non-streaming behavior. + +This resolves the issue where users could not collect usage information or perform cleanup tasks at the end of streaming agent runs using the `agent_end` event handler. \ No newline at end of file diff --git a/packages/agents-core/src/run.ts b/packages/agents-core/src/run.ts index 987fded3..3440be76 100644 --- a/packages/agents-core/src/run.ts +++ b/packages/agents-core/src/run.ts @@ -832,6 +832,17 @@ export class Runner extends RunHooks> { result.state, result.state._currentStep.output, ); + this.emit( + 'agent_end', + result.state._context, + currentAgent, + result.state._currentStep.output, + ); + currentAgent.emit( + 'agent_end', + result.state._context, + result.state._currentStep.output, + ); return; } else if ( result.state._currentStep.type === 'next_step_interruption' diff --git a/packages/agents-core/test/run.stream.test.ts b/packages/agents-core/test/run.stream.test.ts index f7b29f86..94d0186b 100644 --- a/packages/agents-core/test/run.stream.test.ts +++ b/packages/agents-core/test/run.stream.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { Agent, run, + Runner, setDefaultModelProvider, setTracingDisabled, Usage, @@ -115,4 +116,67 @@ describe('Runner.run (streaming)', () => { ); expect(update?.agent).toBe(agentB); }); + + it('emits agent_end lifecycle event for streaming agents', async () => { + class SimpleStreamingModel implements Model { + constructor(private resp: ModelResponse) {} + async getResponse(_req: ModelRequest): Promise { + return this.resp; + } + async *getStreamedResponse(): AsyncIterable { + yield { + type: 'response_done', + response: { + id: 'r', + usage: { + requests: 1, + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + }, + output: this.resp.output, + }, + } as any; + } + } + + const agent = new Agent({ + name: 'TestAgent', + model: new SimpleStreamingModel({ + output: [fakeModelMessage('Final output')], + usage: new Usage(), + }), + }); + + // Track agent_end events on both the agent and runner + const agentEndEvents: Array<{ context: any; output: string }> = []; + const runnerEndEvents: Array<{ context: any; agent: any; output: string }> = []; + + agent.on('agent_end', (context, output) => { + agentEndEvents.push({ context, output }); + }); + + // Create a runner instance to listen for events + const runner = new Runner(); + runner.on('agent_end', (context, agent, output) => { + runnerEndEvents.push({ context, agent, output }); + }); + + const result = await runner.run(agent, 'test input', { stream: true }); + + // Consume the stream + const events: RunStreamEvent[] = []; + for await (const e of result.toStream()) { + events.push(e); + } + await result.completed; + + // Verify agent_end was called on both agent and runner + expect(agentEndEvents).toHaveLength(1); + expect(agentEndEvents[0].output).toBe('Final output'); + + expect(runnerEndEvents).toHaveLength(1); + expect(runnerEndEvents[0].agent).toBe(agent); + expect(runnerEndEvents[0].output).toBe('Final output'); + }); }); diff --git a/packages/agents-core/test/run.test.ts b/packages/agents-core/test/run.test.ts index 7cdd1a54..5a5d90bd 100644 --- a/packages/agents-core/test/run.test.ts +++ b/packages/agents-core/test/run.test.ts @@ -140,6 +140,35 @@ describe('Runner.run', () => { await expect(run(agent, 'fail')).rejects.toThrow('No response found'); }); + + it('emits agent_end lifecycle event for non-streaming agents', async () => { + const agent = new Agent({ + name: 'TestAgent', + }); + + // Track agent_end events on both the agent and runner + const agentEndEvents: Array<{ context: any; output: string }> = []; + const runnerEndEvents: Array<{ context: any; agent: any; output: string }> = []; + + agent.on('agent_end', (context, output) => { + agentEndEvents.push({ context, output }); + }); + + const runner = new Runner(); + runner.on('agent_end', (context, agent, output) => { + runnerEndEvents.push({ context, agent, output }); + }); + + const result = await runner.run(agent, 'test input'); + + // Verify agent_end was called on both agent and runner + expect(agentEndEvents).toHaveLength(1); + expect(agentEndEvents[0].output).toBe('Hello World'); + + expect(runnerEndEvents).toHaveLength(1); + expect(runnerEndEvents[0].agent).toBe(agent); + expect(runnerEndEvents[0].output).toBe('Hello World'); + }); }); describe('additional scenarios', () => { From 59abedb104719fc8107029ff1897ea070426d02a Mon Sep 17 00:00:00 2001 From: Kumbham Ajay Goud Date: Tue, 26 Aug 2025 14:37:17 +0530 Subject: [PATCH 2/3] fix: resolve unused 'result' variable in lifecycle event test - Added assertion to verify result.finalOutput in agent_end lifecycle test - Resolves ESLint error: 'result' is assigned a value but never used - Makes test more comprehensive by validating both events and return value --- packages/agents-core/test/run.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/agents-core/test/run.test.ts b/packages/agents-core/test/run.test.ts index 5a5d90bd..ccba70fb 100644 --- a/packages/agents-core/test/run.test.ts +++ b/packages/agents-core/test/run.test.ts @@ -161,10 +161,13 @@ describe('Runner.run', () => { const result = await runner.run(agent, 'test input'); + // Verify the result has the expected output + expect(result.finalOutput).toBe('Hello World'); + // Verify agent_end was called on both agent and runner expect(agentEndEvents).toHaveLength(1); expect(agentEndEvents[0].output).toBe('Hello World'); - + expect(runnerEndEvents).toHaveLength(1); expect(runnerEndEvents[0].agent).toBe(agent); expect(runnerEndEvents[0].output).toBe('Hello World'); @@ -404,7 +407,7 @@ describe('Runner.run', () => { usage: new Usage(), }; class SimpleStreamingModel implements Model { - constructor(private resps: ModelResponse[]) {} + constructor(private resps: ModelResponse[]) { } async getResponse(_req: ModelRequest): Promise { const r = this.resps.shift(); if (!r) { From 58cc24138af25f6e2eb5367c7e9ee50225bcd002 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 26 Aug 2025 18:25:47 +0900 Subject: [PATCH 3/3] Update .changeset/fix-streaming-agent-end-lifecycle.md --- .changeset/fix-streaming-agent-end-lifecycle.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.changeset/fix-streaming-agent-end-lifecycle.md b/.changeset/fix-streaming-agent-end-lifecycle.md index 6756942d..5b268c86 100644 --- a/.changeset/fix-streaming-agent-end-lifecycle.md +++ b/.changeset/fix-streaming-agent-end-lifecycle.md @@ -2,8 +2,4 @@ '@openai/agents-core': patch --- -Fix streaming agents not calling agent_end lifecycle hook - -Streaming agents were not emitting the `agent_end` lifecycle event when completing execution, while non-streaming agents were correctly emitting this event. This fix ensures that both the agent instance and the runner emit the `agent_end` event for streaming agents when they produce a final output, maintaining consistency with the non-streaming behavior. - -This resolves the issue where users could not collect usage information or perform cleanup tasks at the end of streaming agent runs using the `agent_end` event handler. \ No newline at end of file +Fix #371 streaming agents not calling agent_end lifecycle hook \ No newline at end of file