コンテンツにスキップ

人間の介入(HITL)

このガイドでは、SDK に組み込まれた Human in the loop (人間の介入) サポートを使用して、人の介入に基づいてエージェントの実行を一時停止および再開する方法を示します。

現時点での主なユースケースは、機密性の高いツール実行に対する承認を求めることです。

needsApproval オプションを true、または真偽値を返す非同期関数に設定することで、承認が必要なツールを定義できます。

ツール承認の定義
import { tool } from '@openai/agents';
import z from 'zod';
const sensitiveTool = tool({
name: 'cancelOrder',
description: 'Cancel order',
parameters: z.object({
orderId: z.number(),
}),
// always requires approval
needsApproval: true,
execute: async ({ orderId }, args) => {
// prepare order return
},
});
const sendEmail = tool({
name: 'sendEmail',
description: 'Send an email',
parameters: z.object({
to: z.string(),
subject: z.string(),
body: z.string(),
}),
needsApproval: async (_context, { subject }) => {
// check if the email is spam
return subject.includes('spam');
},
execute: async ({ to, subject, body }, args) => {
// send email
},
});
  1. エージェントがツール(複数可)を呼び出すと判断した場合、needsApproval を評価してそのツールに承認が必要か確認します
  2. 承認が必要な場合、エージェントは承認がすでに許可または拒否されているかを確認します
    • 承認が許可も拒否もされていない場合、ツールはツール呼び出しを実行できないという固定メッセージをエージェントに返します
    • 承認 / 拒否が未設定の場合、ツール承認リクエストがトリガーされます
  3. エージェントはすべてのツール承認リクエストを収集し、実行を中断します
  4. 中断がある場合、エージェントの実行結果 に、保留中のステップを示す interruptions 配列が含まれます。ツール呼び出しに確認が必要なときは、type: "tool_approval_item" を持つ ToolApprovalItem が表示されます
  5. ツール呼び出しを承認または拒否するには、result.state.approve(interruption) または result.state.reject(interruption) を呼び出します。同じツールをその実行の残りの間ずっと承認または拒否状態にしたい場合は、{ alwaysApprove: true } または { alwaysReject: true } を渡します
  6. すべての中断を処理したら、result.staterunner.run(agent, state) に渡して実行を再開できます。ここで agent は全体の実行をトリガーした元のエージェントです
  7. フローは手順 1 から再開されます

以下は、ターミナルで承認を促し、状態を一時的にファイルに保存する Human in the loop (人間の介入) フローの、より完全な例です。

Human in the loop (人間の介入)
import { z } from 'zod';
import readline from 'node:readline/promises';
import fs from 'node:fs/promises';
import { Agent, run, tool, RunState, RunResult } from '@openai/agents';
const getWeatherTool = tool({
name: 'get_weather',
description: 'Get the weather for a given city',
parameters: z.object({
location: z.string(),
}),
needsApproval: async (_context, { location }) => {
// forces approval to look up the weather in San Francisco
return location === 'San Francisco';
},
execute: async ({ location }) => {
return `The weather in ${location} is sunny`;
},
});
const dataAgentTwo = new Agent({
name: 'Data agent',
instructions: 'You are a data agent',
handoffDescription: 'You know everything about the weather',
tools: [getWeatherTool],
});
const agent = new Agent({
name: 'Basic test agent',
instructions: 'You are a basic agent',
handoffs: [dataAgentTwo],
});
async function confirm(question: string) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await rl.question(`${question} (y/n): `);
const normalizedAnswer = answer.toLowerCase();
rl.close();
return normalizedAnswer === 'y' || normalizedAnswer === 'yes';
}
async function main() {
let result: RunResult<unknown, Agent<unknown, any>> = await run(
agent,
'What is the weather in Oakland and San Francisco?',
);
let hasInterruptions = result.interruptions?.length > 0;
while (hasInterruptions) {
// storing
await fs.writeFile(
'result.json',
JSON.stringify(result.state, null, 2),
'utf-8',
);
// from here on you could run things on a different thread/process
// reading later on
const storedState = await fs.readFile('result.json', 'utf-8');
const state = await RunState.fromString(agent, storedState);
for (const interruption of result.interruptions) {
const confirmed = await confirm(
`Agent ${interruption.agent.name} would like to use the tool ${interruption.name} with "${interruption.arguments}". Do you approve?`,
);
if (confirmed) {
state.approve(interruption);
} else {
state.reject(interruption);
}
}
// resume execution of the current state
result = await run(agent, state);
hasInterruptions = result.interruptions?.length > 0;
}
console.log(result.finalOutput);
}
main().catch((error) => {
console.dir(error, { depth: null });
});

エンドツーエンドで動作するバージョンは、the full example script を参照してください。

Human in the loop (人間の介入) フローは、サーバーを稼働させ続けることなく長時間中断可能なように設計されています。リクエストを一旦終了して後で続行する必要がある場合、状態をシリアライズして後で再開できます。

result.state.toString()(または JSON.stringify(result.state))で状態をシリアライズし、シリアライズ済み状態を RunState.fromString(agent, serializedState) に渡すことで後から再開できます。ここで agent は全体の実行をトリガーしたエージェントのインスタンスです。

再開時に新しいコンテキストオブジェクトを挿入する必要がある場合は、代わりに RunState.fromStringWithContext(agent, serializedState, context, { contextStrategy }) を使用してください。

  • contextStrategy: 'merge'(デフォルト)は、シリアライズされた承認状態を保持し、提供された RunContext にマージします
  • contextStrategy: 'replace' は、提供された RunContext をそのまま使用して実行を再構築します

デフォルトでは、トレーシング API キーはシリアライズされた状態から省かれるため、機密情報を誤って永続化しないようになっています。状態と一緒にトレーシングの認証情報を移動する必要がある場合にのみ、result.state.toString({ includeTracingApiKey: true }) を渡してください。

この方法により、シリアライズ済み状態をデータベースやリクエストと一緒に保存できます。

承認リクエストに長時間かかり、エージェント定義を意味のある形でバージョン管理したい、あるいは Agents SDK のバージョンを上げたい場合は、現時点ではパッケージエイリアスを使用して 2 つのバージョンの Agents SDK を並行インストールし、独自の分岐ロジックを実装することをおすすめします。

実務上は、自分のコードにバージョン番号を割り当て、それをシリアライズ済み状態と一緒に保存し、デシリアライズを正しいコードのバージョンに誘導することを意味します。