Skip to content

Commit 9147a6a

Browse files
authored
feat: #460 Enable to customize the internal runner for an agent as too (#481)
1 parent 1d4984b commit 9147a6a

File tree

4 files changed

+101
-7
lines changed

4 files changed

+101
-7
lines changed

.changeset/warm-ties-sort.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openai/agents-core': patch
3+
---
4+
5+
feat: #460 Enable to customize the internal runner for an agent as tool

examples/agent-patterns/agents-as-tools.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ const orchestratorAgent = new Agent({
2929
spanishAgent.asTool({
3030
toolName: 'translate_to_spanish',
3131
toolDescription: "Translate the user's message to Spanish",
32+
// You can customize both runner init options and additional options for its execution
33+
runConfig: {
34+
model: 'gpt-5',
35+
modelSettings: {
36+
providerData: {
37+
reasoning: { effort: 'low' },
38+
text: { verbosity: 'low' },
39+
},
40+
},
41+
},
42+
runOptions: {
43+
maxTurns: 3,
44+
},
3245
}),
3346
frenchAgent.asTool({
3447
toolName: 'translate_to_french',

packages/agents-core/src/agent.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import type {
2626
} from './types';
2727
import type { RunResult } from './result';
2828
import type { Handoff } from './handoff';
29-
import { Runner } from './run';
29+
import { NonStreamRunOptions, RunConfig, Runner } from './run';
3030
import { toFunctionToolName } from './utils/tools';
3131
import { getOutputText } from './utils/messages';
3232
import { isAgentToolInput } from './utils/typeGuards';
@@ -511,9 +511,23 @@ export class Agent<
511511
needsApproval?:
512512
| boolean
513513
| ToolApprovalFunction<typeof AgentAsToolNeedApprovalSchame>;
514+
/**
515+
* Run configuration for initializing the internal agent runner.
516+
*/
517+
runConfig?: Partial<RunConfig>;
518+
/**
519+
* Additional run options for the agent (as tool) execution.
520+
*/
521+
runOptions?: NonStreamRunOptions<TContext>;
514522
}): FunctionTool<TContext, typeof AgentAsToolNeedApprovalSchame> {
515-
const { toolName, toolDescription, customOutputExtractor, needsApproval } =
516-
options;
523+
const {
524+
toolName,
525+
toolDescription,
526+
customOutputExtractor,
527+
needsApproval,
528+
runConfig,
529+
runOptions,
530+
} = options;
517531
return tool({
518532
name: toolName ?? toFunctionToolName(this.name),
519533
description: toolDescription ?? '',
@@ -524,9 +538,11 @@ export class Agent<
524538
if (!isAgentToolInput(data)) {
525539
throw new ModelBehaviorError('Agent tool called with invalid input');
526540
}
527-
528-
const runner = new Runner();
529-
const result = await runner.run(this, data.input, { context });
541+
const runner = new Runner(runConfig ?? {});
542+
const result = await runner.run(this, data.input, {
543+
context,
544+
...(runOptions ?? {}),
545+
});
530546
const outputText =
531547
typeof customOutputExtractor === 'function'
532548
? await customOutputExtractor(result as any)

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { describe, it, expect, vi } from 'vitest';
1+
import { describe, it, expect, vi, afterEach } from 'vitest';
22
import { Agent } from '../src/agent';
33
import { RunContext } from '../src/runContext';
44
import { Handoff, handoff } from '../src/handoff';
55
import { z } from 'zod';
66
import { JsonSchemaDefinition, setDefaultModelProvider } from '../src';
77
import { FakeModelProvider } from './stubs';
8+
import { Runner } from '../src/run';
89

910
describe('Agent', () => {
11+
afterEach(() => {
12+
vi.restoreAllMocks();
13+
});
14+
1015
it('should create an agent with default values', () => {
1116
const agent = new Agent({ name: 'TestAgent' });
1217

@@ -206,6 +211,61 @@ describe('Agent', () => {
206211
expect(decision).toBe(true);
207212
});
208213

214+
it('passes runConfig and runOptions to the runner when used as a tool', async () => {
215+
const agent = new Agent({
216+
name: 'Configurable Agent',
217+
instructions: 'You do tests.',
218+
});
219+
const mockResult = {} as any;
220+
const runSpy = vi
221+
.spyOn(Runner.prototype, 'run')
222+
.mockImplementation(async () => mockResult);
223+
224+
const runConfig = {
225+
model: 'gpt-5',
226+
modelSettings: {
227+
providerData: {
228+
reasoning: { effort: 'low' },
229+
},
230+
},
231+
};
232+
const runOptions = {
233+
maxTurns: 3,
234+
previousResponseId: 'prev-response',
235+
};
236+
const customOutputExtractor = vi.fn().mockReturnValue('custom output');
237+
238+
const tool = agent.asTool({
239+
toolDescription: 'You act as a tool.',
240+
runConfig,
241+
runOptions,
242+
customOutputExtractor,
243+
});
244+
245+
const runContext = new RunContext({ locale: 'en-US' });
246+
const inputPayload = { input: 'translate this' };
247+
const result = await tool.invoke(runContext, JSON.stringify(inputPayload));
248+
249+
expect(result).toBe('custom output');
250+
expect(customOutputExtractor).toHaveBeenCalledWith(mockResult);
251+
expect(runSpy).toHaveBeenCalledTimes(1);
252+
253+
const [calledAgent, calledInput, calledOptions] = runSpy.mock.calls[0];
254+
expect(calledAgent).toBe(agent);
255+
expect(calledInput).toBe(inputPayload.input);
256+
expect(calledOptions).toMatchObject({
257+
context: runContext,
258+
maxTurns: runOptions.maxTurns,
259+
previousResponseId: runOptions.previousResponseId,
260+
});
261+
262+
const runnerInstance = runSpy.mock.instances[0] as unknown as Runner;
263+
expect(runnerInstance.config.model).toBe(runConfig.model);
264+
expect(runnerInstance.config.modelSettings).toEqual(
265+
runConfig.modelSettings,
266+
);
267+
});
268+
209269
it('should process final output (text)', async () => {
210270
const agent = new Agent({
211271
name: 'Test Agent',

0 commit comments

Comments
 (0)