Skip to content

Commit c252cb5

Browse files
authored
feat: #713 Access tool call items in an output guardrail (#716)
1 parent 870cc20 commit c252cb5

File tree

4 files changed

+105
-2
lines changed

4 files changed

+105
-2
lines changed

.changeset/polite-onions-help.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: #713 Access tool call items in an output guardrail

packages/agents-core/src/guardrail.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { ModelItem } from './types/protocol';
22
import { Agent, AgentOutputType } from './agent';
33
import { RunContext } from './runContext';
4-
import { ResolvedAgentOutput, TextOutput, UnknownContext } from './types';
4+
import {
5+
AgentOutputItem,
6+
ResolvedAgentOutput,
7+
TextOutput,
8+
UnknownContext,
9+
} from './types';
510
import type { ModelResponse } from './model';
611

712
/**
@@ -166,6 +171,8 @@ export interface OutputGuardrailFunctionArgs<
166171
details?: {
167172
/** Model response associated with the output if available. */
168173
modelResponse?: ModelResponse;
174+
/** Model output items generated during the run (excluding approvals). */
175+
output?: AgentOutputItem[];
169176
};
170177
}
171178
/**

packages/agents-core/src/run.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1422,11 +1422,15 @@ export class Runner extends RunHooks<any, AgentOutputType<unknown>> {
14221422
);
14231423
if (guardrails.length > 0) {
14241424
const agentOutput = state._currentAgent.processFinalOutput(output);
1425+
const runOutput = getTurnInput([], state._generatedItems);
14251426
const guardrailArgs: OutputGuardrailFunctionArgs<unknown, TOutput> = {
14261427
agent: state._currentAgent,
14271428
agentOutput,
14281429
context: state._context,
1429-
details: { modelResponse: state._lastTurnResponse },
1430+
details: {
1431+
modelResponse: state._lastTurnResponse,
1432+
output: runOutput,
1433+
},
14301434
};
14311435
try {
14321436
const results = await Promise.all(

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
OutputGuardrailTripwireTriggered,
1818
Session,
1919
ModelInputData,
20+
type OutputGuardrailFunctionArgs,
2021
type AgentInputItem,
2122
run,
2223
Runner,
@@ -436,6 +437,92 @@ describe('Runner.run', () => {
436437
);
437438
});
438439

440+
it('passes run details to output guardrails', async () => {
441+
let receivedArgs: OutputGuardrailFunctionArgs | undefined;
442+
let guardrailValidatedTool = false;
443+
const guardrailFn = vi.fn(async (args: OutputGuardrailFunctionArgs) => {
444+
receivedArgs = args;
445+
const toolCall = args.details?.output?.find(
446+
(item): item is protocol.FunctionCallItem =>
447+
item.type === 'function_call' &&
448+
(item as protocol.FunctionCallItem).name === 'queryPerson',
449+
);
450+
expect(toolCall?.arguments).toContain('person-1');
451+
guardrailValidatedTool = true;
452+
return { tripwireTriggered: false, outputInfo: {} };
453+
});
454+
const runner = new Runner({
455+
outputGuardrails: [{ name: 'og', execute: guardrailFn }],
456+
});
457+
const queryPerson = tool({
458+
name: 'queryPerson',
459+
description: 'Look up a person by id',
460+
parameters: z.object({ personId: z.string() }),
461+
execute: async ({ personId }) => `${personId} result`,
462+
});
463+
const responses: ModelResponse[] = [
464+
{
465+
output: [
466+
{
467+
id: 'call-1',
468+
type: 'function_call',
469+
name: 'queryPerson',
470+
callId: 'call-1',
471+
status: 'completed',
472+
arguments: '{"personId":"person-1"}',
473+
},
474+
],
475+
usage: new Usage(),
476+
},
477+
{
478+
output: [fakeModelMessage('done')],
479+
usage: new Usage(),
480+
},
481+
];
482+
const agent = new Agent({
483+
name: 'Out',
484+
model: new FakeModel(responses),
485+
tools: [queryPerson],
486+
});
487+
488+
await runner.run(agent, 'input');
489+
490+
expect(guardrailFn).toHaveBeenCalledTimes(1);
491+
492+
const outputItems = receivedArgs?.details?.output ?? [];
493+
expect(outputItems.length).toBeGreaterThan(0);
494+
const toolCall = outputItems.find(
495+
(item): item is protocol.FunctionCallItem =>
496+
item.type === 'function_call' && (item as any).name === 'queryPerson',
497+
);
498+
expect(toolCall?.arguments).toContain('person-1');
499+
500+
const toolResult = outputItems.find(
501+
(item): item is protocol.FunctionCallResultItem =>
502+
item.type === 'function_call_result' &&
503+
(item as any).callId === 'call-1',
504+
);
505+
expect(toolResult).toBeDefined();
506+
const toolOutput = toolResult?.output;
507+
if (typeof toolOutput === 'string') {
508+
expect(toolOutput).toBe('person-1 result');
509+
} else if (Array.isArray(toolOutput)) {
510+
expect(
511+
toolOutput.some(
512+
(item) =>
513+
'text' in item &&
514+
typeof (item as { text?: unknown }).text === 'string' &&
515+
(item as { text: string }).text === 'person-1 result',
516+
),
517+
).toBe(true);
518+
} else {
519+
expect((toolOutput as { text?: string } | undefined)?.text).toBe(
520+
'person-1 result',
521+
);
522+
}
523+
expect(guardrailValidatedTool).toBe(true);
524+
});
525+
439526
it('executes tool calls and records output', async () => {
440527
const first: ModelResponse = {
441528
output: [

0 commit comments

Comments
 (0)