diff --git a/.changeset/cuddly-poets-refuse.md b/.changeset/cuddly-poets-refuse.md new file mode 100644 index 00000000..161da955 --- /dev/null +++ b/.changeset/cuddly-poets-refuse.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-core': patch +--- + +Add `returnRunResult` option to `agent.asTool()` to expose full RunResult, including interruptions such as approvals. diff --git a/docs/src/content/docs/guides/tools.mdx b/docs/src/content/docs/guides/tools.mdx index 2fd0d5d8..4902b0e6 100644 --- a/docs/src/content/docs/guides/tools.mdx +++ b/docs/src/content/docs/guides/tools.mdx @@ -85,6 +85,7 @@ Under the hood the SDK: - Creates a function tool with a single `input` parameter. - Runs the sub‑agent with that input when the tool is called. - Returns either the last message or the output extracted by `customOutputExtractor`. +- If `returnRunResult` is set, returns the full `RunResult` so nested interruptions can be inspected. --- diff --git a/examples/basic/tool-use-behavior.ts b/examples/basic/tool-use-behavior.ts index e60b6b4a..8e68c0a0 100644 --- a/examples/basic/tool-use-behavior.ts +++ b/examples/basic/tool-use-behavior.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { Agent, run, tool } from '@openai/agents'; +import { Agent, run, tool, RunContext, RunResult } from '@openai/agents'; const Weather = z.object({ city: z.string(), @@ -46,6 +46,19 @@ const agent3 = new Agent({ tools: [getWeatherTool, saySomethingTool], }); +const agentWithRunResult = new Agent({ + name: 'Tool agent with RunResult', + instructions, + toolUseBehavior: { stopAtToolNames: ['get_weather'] }, + outputType: Weather, + tools: [getWeatherTool, saySomethingTool], +}); + +const weatherToolWithRunResult = agentWithRunResult.asTool({ + toolName: 'weather_summary', + returnRunResult: true, +}); + async function main() { const input = 'What is the weather in San Francisco?'; const result = await run(agent, input); @@ -65,6 +78,12 @@ async function main() { const finalOutput3 = result3.finalOutput; // The weather in San Francisco is sunny. Thanks for asking! console.log(finalOutput3); + + const result4 = (await weatherToolWithRunResult.invoke( + new RunContext(), + JSON.stringify({ input }), + )) as RunResult>; + console.log('agentWithRunResult.newItems:', result4.newItems); } main(); diff --git a/examples/docs/tools/agentsAsTools.ts b/examples/docs/tools/agentsAsTools.ts index 9e857992..9a392387 100644 --- a/examples/docs/tools/agentsAsTools.ts +++ b/examples/docs/tools/agentsAsTools.ts @@ -10,7 +10,17 @@ const summarizerTool = summarizer.asTool({ toolDescription: 'Generate a concise summary of the supplied text.', }); +const echoAgent = new Agent({ + name: 'Echo', + instructions: 'Repeat whatever the user says.', +}); + +const echoTool = echoAgent.asTool({ + toolName: 'echo_text', + returnRunResult: true, +}); + const mainAgent = new Agent({ name: 'Research assistant', - tools: [summarizerTool], + tools: [summarizerTool, echoTool], }); diff --git a/packages/agents-core/src/agent.ts b/packages/agents-core/src/agent.ts index 114ec884..2c922f21 100644 --- a/packages/agents-core/src/agent.ts +++ b/packages/agents-core/src/agent.ts @@ -444,8 +444,22 @@ export class Agent< customOutputExtractor?: ( output: RunResult>, ) => string | Promise; - }): FunctionTool { - const { toolName, toolDescription, customOutputExtractor } = options; + /** + * If true, the invoked tool will return the {@link RunResult} of the agent + * run instead of just the extracted output text. + */ + returnRunResult?: boolean; + }): FunctionTool< + TContext, + any, + string | RunResult> + > { + const { + toolName, + toolDescription, + customOutputExtractor, + returnRunResult, + } = options; return tool({ name: toolName ?? toFunctionToolName(this.name), description: toolDescription ?? '', @@ -469,6 +483,9 @@ export class Agent< const result = await runner.run(this, data.input, { context: context?.context, }); + if (returnRunResult) { + return result as RunResult>; + } if (typeof customOutputExtractor === 'function') { return customOutputExtractor(result as any); } diff --git a/packages/agents-core/test/agent.test.ts b/packages/agents-core/test/agent.test.ts index 183f9ab8..e06c6ebd 100644 --- a/packages/agents-core/test/agent.test.ts +++ b/packages/agents-core/test/agent.test.ts @@ -3,7 +3,11 @@ import { Agent } from '../src/agent'; import { RunContext } from '../src/runContext'; import { Handoff, handoff } from '../src/handoff'; import { z } from 'zod/v3'; -import { JsonSchemaDefinition, setDefaultModelProvider } from '../src'; +import { + JsonSchemaDefinition, + setDefaultModelProvider, + RunResult, +} from '../src'; import { FakeModelProvider } from './stubs'; describe('Agent', () => { @@ -198,6 +202,67 @@ describe('Agent', () => { const result1 = agent.processFinalOutput('{"message": "Hi, how are you?"}'); expect(result1).toEqual({ message: 'Hi, how are you?' }); }); + + it('should return a RunResult when returnRunResult option is true', async () => { + const agent = new Agent({ + name: 'Test Agent', + instructions: 'You do tests.', + }); + const tool = agent.asTool({ returnRunResult: true }); + setDefaultModelProvider(new FakeModelProvider()); + const result = await tool.invoke({} as any, '{"input":"hello"}'); + expect(result).toBeInstanceOf(RunResult); + expect((result as RunResult>).finalOutput).toBe( + 'Hello World', + ); + }); + + it('should expose finalOutput with stopAtToolNames using returnRunResult', async () => { + setDefaultModelProvider(new FakeModelProvider()); + + const subAgent = new Agent({ + name: 'Echo', + instructions: 'Repeat what the user says.', + }); + + const queryTool = subAgent.asTool({ + toolName: 'query_action_logs', + toolDescription: 'Echo tool that represents a long-running tool', + }); + + const outerAgent = new Agent({ + name: 'Parent', + instructions: 'Use the tool then stop.', + tools: [queryTool], + toolUseBehavior: { stopAtToolNames: ['query_action_logs'] }, + }); + + const outerToolDefault = outerAgent.asTool({ + toolName: 'get_action_logs', + }); + + const outerToolWithRunResult = outerAgent.asTool({ + toolName: 'get_action_logs_rich', + returnRunResult: true, + }); + + const input = JSON.stringify({ input: 'hello' }); + + const resultDefault = await outerToolDefault.invoke( + new RunContext({}), + input, + ); + expect(resultDefault).toBe('Hello World'); + + const resultWithRunResult = await outerToolWithRunResult.invoke( + new RunContext({}), + input, + ); + expect( + (resultWithRunResult as RunResult>).finalOutput, + ).toBe('Hello World'); + }); + it('should process final output (json schema)', async () => { const agent = new Agent({ name: 'Test Agent',