Skip to content

Commit 4ea61dc

Browse files
committed
Support both onApproval and HITL
1 parent a02c285 commit 4ea61dc

File tree

12 files changed

+306
-87
lines changed

12 files changed

+306
-87
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as readline from 'readline/promises';
2+
import { stdin, stdout } from 'node:process';
3+
import { Agent, run, hostedMcpTool, RunToolApprovalItem } from '@openai/agents';
4+
5+
async function promptApproval(item: RunToolApprovalItem): Promise<boolean> {
6+
const rl = readline.createInterface({ input: stdin, output: stdout });
7+
const name = item.rawItem.name;
8+
const params = JSON.parse(item.rawItem.providerData?.arguments || '{}');
9+
const answer = await rl.question(
10+
`Approve running tool (mcp: ${name}, params: ${JSON.stringify(params)})? (y/n) `,
11+
);
12+
rl.close();
13+
return answer.toLowerCase().trim() === 'y';
14+
}
15+
16+
async function main(verbose: boolean, stream: boolean): Promise<void> {
17+
// 'always' |
18+
// 'never' |
19+
// { never?: { toolNames: string[] }; always?: { toolNames: string[] } }
20+
const requireApproval = {
21+
never: { toolNames: ['search_codex_code', 'fetch_codex_documentation'] },
22+
always: { toolNames: ['fetch_generic_url_content'] },
23+
};
24+
const agent = new Agent({
25+
name: 'MCP Assistant',
26+
instructions: 'You must always use the MCP tools to answer questions.',
27+
tools: [
28+
hostedMcpTool({
29+
serverLabel: 'gitmcp',
30+
serverUrl: 'https://gitmcp.io/openai/codex',
31+
requireApproval,
32+
// when you don't pass onApproval, the agent loop will handle the approval process
33+
}),
34+
],
35+
});
36+
37+
const input = 'Which language is this repo written in?';
38+
39+
if (stream) {
40+
// Streaming
41+
const result = await run(agent, input, { stream: true, maxTurns: 100 });
42+
for await (const event of result) {
43+
if (verbose) {
44+
console.log(JSON.stringify(event, null, 2));
45+
} else {
46+
if (
47+
event.type === 'raw_model_stream_event' &&
48+
event.data.type === 'model'
49+
) {
50+
console.log(event.data.event.type);
51+
}
52+
}
53+
}
54+
console.log(`Done streaming; final result: ${result.finalOutput}`);
55+
} else {
56+
// Non-streaming
57+
let result = await run(agent, input, { maxTurns: 100 });
58+
while (result.interruptions && result.interruptions.length) {
59+
for (const interruption of result.interruptions) {
60+
const approval = await promptApproval(interruption);
61+
if (approval) {
62+
result.state.approve(interruption);
63+
} else {
64+
result.state.reject(interruption);
65+
}
66+
}
67+
result = await run(agent, result.state, { maxTurns: 100 });
68+
}
69+
console.log(result.finalOutput);
70+
71+
if (verbose) {
72+
console.log('----------------------------------------------------------');
73+
console.log(JSON.stringify(result.newItems, null, 2));
74+
console.log('----------------------------------------------------------');
75+
}
76+
}
77+
}
78+
79+
const args = process.argv.slice(2);
80+
const verbose = args.includes('--verbose');
81+
const stream = args.includes('--stream');
82+
83+
main(verbose, stream).catch((err) => {
84+
console.error(err);
85+
process.exit(1);
86+
});

examples/mcp/hosted-mcp-approvals.ts renamed to examples/mcp/hosted-mcp-on-approval.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,28 @@ async function promptApproval(item: RunToolApprovalItem): Promise<boolean> {
1414
}
1515

1616
async function main(verbose: boolean, stream: boolean): Promise<void> {
17+
// 'always' |
18+
// 'never' |
19+
// { never?: { toolNames: string[] }; always?: { toolNames: string[] } }
20+
const requireApproval = {
21+
never: {
22+
toolNames: ['fetch_codex_documentation', 'fetch_generic_url_content'],
23+
},
24+
always: {
25+
toolNames: ['search_codex_code'],
26+
},
27+
};
1728
const agent = new Agent({
1829
name: 'MCP Assistant',
1930
instructions: 'You must always use the MCP tools to answer questions.',
2031
tools: [
2132
hostedMcpTool({
2233
serverLabel: 'gitmcp',
2334
serverUrl: 'https://gitmcp.io/openai/codex',
24-
requireApproval: 'always',
25-
onApproval: async (_, data) => {
26-
return { approve: await promptApproval(data) };
35+
requireApproval,
36+
onApproval: async (_context, item) => {
37+
const approval = await promptApproval(item);
38+
return { approve: approval, reason: undefined };
2739
},
2840
}),
2941
],

examples/mcp/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"scripts": {
1010
"build-check": "tsc --noEmit",
1111
"start:stdio": "tsx filesystem-example.ts",
12-
"start:hosted-mcp-approvals": "tsx hosted-mcp-approvals.ts",
12+
"start:hosted-mcp-on-approval": "tsx hosted-mcp-on-approval.ts",
13+
"start:hosted-mcp-human-in-the-loop": "tsx hosted-mcp-human-in-the-loop.ts",
1314
"start:hosted-mcp-simple": "tsx hosted-mcp-simple.ts"
1415
}
1516
}

packages/agents-core/src/items.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export class RunToolApprovalItem extends RunItemBase {
142142
public readonly type = 'tool_approval_item' as const;
143143

144144
constructor(
145-
public rawItem: protocol.FunctionCallItem,
145+
public rawItem: protocol.FunctionCallItem | protocol.HostedToolCallItem,
146146
public agent: Agent<any, any>,
147147
) {
148148
super();

packages/agents-core/src/runContext.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,12 @@ export class RunContext<TContext = UnknownContext> {
117117
rejected: [],
118118
};
119119
if (Array.isArray(approvalEntry.approved)) {
120-
approvalEntry.approved.push(approvalItem.rawItem.callId);
120+
// function tool has call_id, hosted tool call has id
121+
const callId =
122+
'callId' in approvalItem.rawItem
123+
? approvalItem.rawItem.callId // function tools
124+
: approvalItem.rawItem.id!; // hosted tools
125+
approvalEntry.approved.push(callId);
121126
}
122127
this.#approvals.set(toolName, approvalEntry);
123128
}
@@ -146,7 +151,12 @@ export class RunContext<TContext = UnknownContext> {
146151
};
147152

148153
if (Array.isArray(approvalEntry.rejected)) {
149-
approvalEntry.rejected.push(approvalItem.rawItem.callId);
154+
// function tool has call_id, hosted tool call has id
155+
const callId =
156+
'callId' in approvalItem.rawItem
157+
? approvalItem.rawItem.callId // function tools
158+
: approvalItem.rawItem.id!; // hosted tools
159+
approvalEntry.rejected.push(callId);
150160
}
151161
this.#approvals.set(toolName, approvalEntry);
152162
}

0 commit comments

Comments
 (0)