From 8379320d082328690fb2236280c3143e0be1c40a Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 2 Dec 2025 18:56:49 +0900 Subject: [PATCH] feat: #713 Access tool call items in an output guardrail --- .changeset/polite-onions-help.md | 5 ++ packages/agents-core/src/guardrail.ts | 9 ++- packages/agents-core/src/run.ts | 6 +- packages/agents-core/test/run.test.ts | 87 +++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 .changeset/polite-onions-help.md diff --git a/.changeset/polite-onions-help.md b/.changeset/polite-onions-help.md new file mode 100644 index 00000000..3d0c3354 --- /dev/null +++ b/.changeset/polite-onions-help.md @@ -0,0 +1,5 @@ +--- +'@openai/agents-core': patch +--- + +feat: #713 Access tool call items in an output guardrail diff --git a/packages/agents-core/src/guardrail.ts b/packages/agents-core/src/guardrail.ts index 1b27a115..b85564f8 100644 --- a/packages/agents-core/src/guardrail.ts +++ b/packages/agents-core/src/guardrail.ts @@ -1,7 +1,12 @@ import type { ModelItem } from './types/protocol'; import { Agent, AgentOutputType } from './agent'; import { RunContext } from './runContext'; -import { ResolvedAgentOutput, TextOutput, UnknownContext } from './types'; +import { + AgentOutputItem, + ResolvedAgentOutput, + TextOutput, + UnknownContext, +} from './types'; import type { ModelResponse } from './model'; /** @@ -166,6 +171,8 @@ export interface OutputGuardrailFunctionArgs< details?: { /** Model response associated with the output if available. */ modelResponse?: ModelResponse; + /** Model output items generated during the run (excluding approvals). */ + output?: AgentOutputItem[]; }; } /** diff --git a/packages/agents-core/src/run.ts b/packages/agents-core/src/run.ts index 91cc25b3..474f94c6 100644 --- a/packages/agents-core/src/run.ts +++ b/packages/agents-core/src/run.ts @@ -1422,11 +1422,15 @@ export class Runner extends RunHooks> { ); if (guardrails.length > 0) { const agentOutput = state._currentAgent.processFinalOutput(output); + const runOutput = getTurnInput([], state._generatedItems); const guardrailArgs: OutputGuardrailFunctionArgs = { agent: state._currentAgent, agentOutput, context: state._context, - details: { modelResponse: state._lastTurnResponse }, + details: { + modelResponse: state._lastTurnResponse, + output: runOutput, + }, }; try { const results = await Promise.all( diff --git a/packages/agents-core/test/run.test.ts b/packages/agents-core/test/run.test.ts index 8b976cbd..e1befe9b 100644 --- a/packages/agents-core/test/run.test.ts +++ b/packages/agents-core/test/run.test.ts @@ -17,6 +17,7 @@ import { OutputGuardrailTripwireTriggered, Session, ModelInputData, + type OutputGuardrailFunctionArgs, type AgentInputItem, run, Runner, @@ -436,6 +437,92 @@ describe('Runner.run', () => { ); }); + it('passes run details to output guardrails', async () => { + let receivedArgs: OutputGuardrailFunctionArgs | undefined; + let guardrailValidatedTool = false; + const guardrailFn = vi.fn(async (args: OutputGuardrailFunctionArgs) => { + receivedArgs = args; + const toolCall = args.details?.output?.find( + (item): item is protocol.FunctionCallItem => + item.type === 'function_call' && + (item as protocol.FunctionCallItem).name === 'queryPerson', + ); + expect(toolCall?.arguments).toContain('person-1'); + guardrailValidatedTool = true; + return { tripwireTriggered: false, outputInfo: {} }; + }); + const runner = new Runner({ + outputGuardrails: [{ name: 'og', execute: guardrailFn }], + }); + const queryPerson = tool({ + name: 'queryPerson', + description: 'Look up a person by id', + parameters: z.object({ personId: z.string() }), + execute: async ({ personId }) => `${personId} result`, + }); + const responses: ModelResponse[] = [ + { + output: [ + { + id: 'call-1', + type: 'function_call', + name: 'queryPerson', + callId: 'call-1', + status: 'completed', + arguments: '{"personId":"person-1"}', + }, + ], + usage: new Usage(), + }, + { + output: [fakeModelMessage('done')], + usage: new Usage(), + }, + ]; + const agent = new Agent({ + name: 'Out', + model: new FakeModel(responses), + tools: [queryPerson], + }); + + await runner.run(agent, 'input'); + + expect(guardrailFn).toHaveBeenCalledTimes(1); + + const outputItems = receivedArgs?.details?.output ?? []; + expect(outputItems.length).toBeGreaterThan(0); + const toolCall = outputItems.find( + (item): item is protocol.FunctionCallItem => + item.type === 'function_call' && (item as any).name === 'queryPerson', + ); + expect(toolCall?.arguments).toContain('person-1'); + + const toolResult = outputItems.find( + (item): item is protocol.FunctionCallResultItem => + item.type === 'function_call_result' && + (item as any).callId === 'call-1', + ); + expect(toolResult).toBeDefined(); + const toolOutput = toolResult?.output; + if (typeof toolOutput === 'string') { + expect(toolOutput).toBe('person-1 result'); + } else if (Array.isArray(toolOutput)) { + expect( + toolOutput.some( + (item) => + 'text' in item && + typeof (item as { text?: unknown }).text === 'string' && + (item as { text: string }).text === 'person-1 result', + ), + ).toBe(true); + } else { + expect((toolOutput as { text?: string } | undefined)?.text).toBe( + 'person-1 result', + ); + } + expect(guardrailValidatedTool).toBe(true); + }); + it('executes tool calls and records output', async () => { const first: ModelResponse = { output: [