Skip to content

Commit 2a4a696

Browse files
authored
feat: #762 Add turnInput (optional) to agent_start event hooks (#765)
1 parent bdbc87d commit 2a4a696

File tree

6 files changed

+138
-7
lines changed

6 files changed

+138
-7
lines changed

.changeset/frank-maps-grab.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@openai/agents-realtime': patch
3+
'@openai/agents-core': patch
4+
---
5+
6+
feat: #762 Add turnInput (optional) to agent_start event hooks

packages/agents-core/src/lifecycle.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ import {
66
EventEmitter,
77
EventEmitterEvents,
88
} from '@openai/agents-core/_shims';
9-
import { TextOutput, UnknownContext } from './types';
9+
import { AgentInputItem, TextOutput, UnknownContext } from './types';
1010
import * as protocol from './types/protocol';
1111

1212
export abstract class EventEmitterDelegate<
1313
EventTypes extends EventEmitterEvents = Record<string, any[]>,
14-
> implements EventEmitter<EventTypes>
15-
{
14+
> implements EventEmitter<EventTypes> {
1615
protected abstract eventEmitter: EventEmitter<EventTypes>;
1716

1817
on<K extends keyof EventTypes>(
@@ -47,9 +46,20 @@ export type AgentHookEvents<
4746
> = {
4847
/**
4948
* @param context - The context of the run
49+
* @param agent - The agent that is starting
50+
* @param turnInput - The input items for the current turn
5051
*/
51-
agent_start: [context: RunContext<TContext>, agent: Agent<TContext, TOutput>];
52+
agent_start: [
53+
context: RunContext<TContext>,
54+
agent: Agent<TContext, TOutput>,
55+
turnInput?: AgentInputItem[],
56+
];
5257
/**
58+
* Note that the second argument is not consistent with the run hooks here.
59+
* Changing the list is a breaking change, so we don't make changes for it in the short term
60+
* If we revisit the argument data structure (e.g., migrating to a single object instead),
61+
* more properties could be easily added later on.
62+
*
5363
* @param context - The context of the run
5464
* @param output - The output of the agent
5565
*/
@@ -105,7 +115,11 @@ export type RunHookEvents<
105115
* @param context - The context of the run
106116
* @param agent - The agent that is starting
107117
*/
108-
agent_start: [context: RunContext<TContext>, agent: Agent<TContext, TOutput>];
118+
agent_start: [
119+
context: RunContext<TContext>,
120+
agent: Agent<TContext, TOutput>,
121+
turnInput?: AgentInputItem[],
122+
];
109123
/**
110124
* @param context - The context of the run
111125
* @param agent - The agent that is ending

packages/agents-core/src/run.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -816,8 +816,14 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
816816
'agent_start',
817817
state._context,
818818
state._currentAgent,
819+
turnInput,
820+
);
821+
this.emit(
822+
'agent_start',
823+
state._context,
824+
state._currentAgent,
825+
turnInput,
819826
);
820-
this.emit('agent_start', state._context, state._currentAgent);
821827
}
822828

823829
const preparedCall = await this.#prepareModelCall(
@@ -1128,8 +1134,14 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
11281134
'agent_start',
11291135
result.state._context,
11301136
currentAgent,
1137+
turnInput,
1138+
);
1139+
this.emit(
1140+
'agent_start',
1141+
result.state._context,
1142+
currentAgent,
1143+
turnInput,
11311144
);
1132-
this.emit('agent_start', result.state._context, currentAgent);
11331145
}
11341146

11351147
let finalResponse: ModelResponse | undefined = undefined;

packages/agents-core/test/run.stream.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,68 @@ describe('Runner.run (streaming)', () => {
210210
expect(runnerEndEvents[0].output).toBe('Final output');
211211
});
212212

213+
it('emits turn input on agent_start during streaming runs', async () => {
214+
class LifecycleStreamingModel implements Model {
215+
async getResponse(_req: ModelRequest): Promise<ModelResponse> {
216+
return {
217+
output: [fakeModelMessage('Final output')],
218+
usage: new Usage(),
219+
};
220+
}
221+
222+
async *getStreamedResponse(): AsyncIterable<StreamEvent> {
223+
yield {
224+
type: 'response_done',
225+
response: {
226+
id: 'r_lifecycle',
227+
usage: {
228+
requests: 1,
229+
inputTokens: 0,
230+
outputTokens: 0,
231+
totalTokens: 0,
232+
},
233+
output: [fakeModelMessage('Final output')],
234+
},
235+
} as any;
236+
}
237+
}
238+
239+
const agent = new Agent({
240+
name: 'StreamLifecycleAgent',
241+
model: new LifecycleStreamingModel(),
242+
});
243+
const runner = new Runner();
244+
245+
const agentInputs: AgentInputItem[][] = [];
246+
const runnerInputs: AgentInputItem[][] = [];
247+
248+
agent.on('agent_start', (_context, _agent, turnInput) => {
249+
agentInputs.push(turnInput ?? []);
250+
});
251+
runner.on('agent_start', (_context, _agent, turnInput) => {
252+
runnerInputs.push(turnInput ?? []);
253+
});
254+
255+
const result = await runner.run(agent, 'stream this input', {
256+
stream: true,
257+
});
258+
259+
// Drain the stream to ensure the run completes.
260+
for await (const _event of result.toStream()) {
261+
// no-op
262+
}
263+
await result.completed;
264+
265+
expect(agentInputs).toHaveLength(1);
266+
expect(runnerInputs).toHaveLength(1);
267+
expect(agentInputs[0].map(getFirstTextContent)).toEqual([
268+
'stream this input',
269+
]);
270+
expect(runnerInputs[0].map(getFirstTextContent)).toEqual([
271+
'stream this input',
272+
]);
273+
});
274+
213275
it('updates cumulative usage during streaming responses', async () => {
214276
const testTool = tool({
215277
name: 'calculator',

packages/agents-core/test/run.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,41 @@ describe('Runner.run', () => {
123123
]);
124124
});
125125

126+
it('emits turn input on agent_start lifecycle hooks', async () => {
127+
const model = new FakeModel([
128+
{
129+
output: [fakeModelMessage('Acknowledged')],
130+
usage: new Usage(),
131+
},
132+
]);
133+
const agent = new Agent({
134+
name: 'LifecycleInputAgent',
135+
model,
136+
});
137+
const runner = new Runner();
138+
139+
const agentInputs: AgentInputItem[][] = [];
140+
const runnerInputs: AgentInputItem[][] = [];
141+
142+
agent.on('agent_start', (_context, _agent, turnInput) => {
143+
agentInputs.push(turnInput ?? []);
144+
});
145+
runner.on('agent_start', (_context, _agent, turnInput) => {
146+
runnerInputs.push(turnInput ?? []);
147+
});
148+
149+
await runner.run(agent, 'capture this input for tracing');
150+
151+
expect(agentInputs).toHaveLength(1);
152+
expect(runnerInputs).toHaveLength(1);
153+
expect(agentInputs[0].map(getFirstTextContent)).toEqual([
154+
'capture this input for tracing',
155+
]);
156+
expect(runnerInputs[0].map(getFirstTextContent)).toEqual([
157+
'capture this input for tracing',
158+
]);
159+
});
160+
126161
it('sholuld handle structured output', async () => {
127162
const fakeModel = new FakeModel([
128163
{

packages/agents-realtime/src/realtimeSessionEvents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
OutputGuardrailTripwireTriggered,
44
RunContext,
55
RunToolApprovalItem,
6+
AgentInputItem,
67
} from '@openai/agents-core';
78
import { RealtimeGuardrailMetadata } from './guardrail';
89
import { RealtimeItem, RealtimeMcpCallItem } from './items';
@@ -39,6 +40,7 @@ export type RealtimeSessionEventTypes<TContext = unknown> = {
3940
agent_start: [
4041
context: RunContext<RealtimeContextData<TContext>>,
4142
agent: AgentWithOrWithoutHistory<TContext>,
43+
turnInput?: AgentInputItem[],
4244
];
4345

4446
/**

0 commit comments

Comments
 (0)