コンテンツにスキップ

人間の介入(HITL)

このガイドでは、SDK の承認ベースの Human in the loop (人間の介入) フローについて説明します。ツール呼び出しに承認が必要な場合、SDK は実行を一時停止して interruptions を返し、後で同じ RunState から再開できるようにします。

この承認の対象範囲は実行全体であり、現在のトップレベルエージェントだけに限定されません。同じパターンは、ツールが現在のエージェントに属する場合、ハンドオフ先のエージェントに属する場合、ネストされた agent.asTool() の実行に属する場合のいずれにも適用されます。ネストされた agent.asTool() の場合でも、中断は外側の実行に現れるため、外側の result.state で承認または拒否し、元のルート実行を再開します。

agent.asTool() では、2 つの異なるレイヤーで承認が発生する可能性があります。エージェントツール自体が asTool({ needsApproval }) による承認を必要とする場合と、ネストされた実行の開始後に、ネストされたエージェント内のツールが独自の承認を要求する場合です。どちらも、同じ外側の実行の中断フローを通じて処理されます。

このページでは、interruptions を使用する手動の承認フローに焦点を当てます。アプリがコード内で判断できる場合、一部のツールタイプではプログラムによる承認コールバックもサポートされており、実行を一時停止せずに続行できます。agent.asTool() 自体を設定する場合は、ツールを参照してください。このページでは、その実行階層内のいずれかのツールで承認が必要になった後の動作について説明します。

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. ツール呼び出しを実行する直前に、SDK はその承認ルール(needsApproval またはホスト型 MCP の同等機能)を評価します。
  2. 承認が必要で、まだ判断が保存されていない場合、ツール呼び出しは実行されません。代わりに、実行は RunToolApprovalItem を記録します。
  3. そのターンの終了時に実行は一時停止し、保留中のすべての承認をエージェントの実行結果interruptions 配列で返します。これには、ネストされた agent.asTool() の実行内で発生した承認も含まれます。
  4. 保留中の各アイテムを result.state.approve(interruption) または result.state.reject(interruption) で処理します。実行の残りの期間、同じツールを常に承認または拒否する場合は、{ alwaysApprove: true } または { alwaysReject: true } を渡します。拒否する場合は、{ message: '...' } を渡して、そのツール呼び出しについてモデルに返す拒否メッセージを指定することもできます。
  5. 更新された result.staterunner.run(agent, state) に渡して再開します。ここで、agent はその実行の元のトップレベルエージェントです。SDK は、ネストされたエージェントツールの実行も含め、中断された箇所から続行します。

デフォルトでは、関数ツールの入力ガードレールは承認後、ツールの実行直前にのみ実行されます。保留中の承認を表示する前に、同じ入力ガードレールでローカル関数ツールの呼び出しを検証するには、run() または RunnertoolExecution: { preApprovalInputGuardrails: true } を渡します。承認前のガードレールが拒否した場合、SDK は承認による中断を作成する代わりに、ガードレールのメッセージをツール出力としてモデルに返します。呼び出しが許可された場合でも、実行は承認のために一時停止します。また、待機中にツール呼び出しが安全でなくなる可能性に備え、承認後に入力ガードレールが再度実行されます。

needsApproval が関数の場合、SDK はツール引数が検査可能なオブジェクトとして解析された後にのみ、その関数を呼び出します。不正な JSON やオブジェクト以外の値では、安全側に倒して処理されます。SDK はコールバックの呼び出しやツールの実行を行わず、承認を要求します。その呼び出しを承認してもツールは実行されず、通常の引数解析エラーの処理に進みます。Realtime の関数ツールも同じルールに従い、無効な呼び出しに対して tool_approval_requested を発行します。

{ alwaysApprove: true } または { alwaysReject: true } で作成された継続的な判断は実行状態に保存されるため、後で同じ一時停止中の実行を再開するときも、toString() / fromString() を通じて保持されます。

GA モデルでは、コンピュータツールの中断が 1 つの computer_call に含まれる複数アクションを表す場合があります。SDK は実行前にアクションごとに needsApproval を評価するため、1 つの保留中の承認で、移動 + クリックのような一連の操作を対象にできます。UI を表示するために interruption.rawItem を検査する場合は、GA の actions 配列と従来の単一の action フィールドの両方を処理してください。

シリアライズされた RunState では、現在の computer ツール名と従来の computer_use_preview 名の両方についてコンピュータツールの承認が保持されるため、プレビューから GA への移行中でも、一時停止した実行を問題なく再開できます。

message を指定しない場合、SDK は設定済みの toolErrorFormatter があればそれを使用し、その後、デフォルトの拒否メッセージにフォールバックします。

すべての保留中の承認を一度に処理する必要はありません。一部のアイテムのみを承認または拒否して再実行すると、処理済みの呼び出しは続行でき、未処理の呼び出しは interruptions に残って、実行を再び一時停止します。

手動の interruptions は最も汎用的なパターンですが、唯一の方法ではありません。

  • ローカルの shellTool()applyPatchTool() では、onApproval を使用してコード内で即座に承認または拒否できます。
  • ホスト型 MCP ツールでは、requireApprovalonApproval を組み合わせて、同様にプログラムで判断できます。
  • 通常の関数ツールでは、このページで説明する手動の中断フローを使用します。

これらのコールバックが判断を返すと、人間の応答を待つために一時停止することなく実行が続行されます。Realtime セッション API については、リアルタイムエージェントの構築の承認フローを参照してください。

同じ中断フローは、ストリーミング実行でも機能します。ストリーミング実行が一時停止したら、stream.completed を待ち、stream.interruptions を読み取って処理します。再開後の出力でもストリーミングを継続する場合は、{ stream: true } を指定して run() を再度呼び出します。このパターンのストリーミング版については、ストリーミングを参照してください。

session も使用している場合は、RunState から再開するときも同じ session を渡し続けてください。これにより、入力を再準備することなく、再開したターンがセッションメモリに追加されます。セッションのライフサイクルの詳細については、セッションを参照してください。

以下は、ターミナルで承認を求め、状態を一時的にファイルへ保存する 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) {
// Store the current run state
await fs.writeFile(
'result.json',
JSON.stringify(result.state, null, 2),
'utf-8',
);
// At this point, another process could review the saved state
// Read the saved state later
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 from the restored state
result = await run(agent, state);
hasInterruptions = result.interruptions?.length > 0;
}
console.log(result.finalOutput);
}
main().catch((error) => {
console.dir(error, { depth: null });
});

実際に動作するエンドツーエンド版については、完全なサンプルスクリプトを参照してください。

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

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

RunState がシリアライズされると、SDK はハンドオフと Agent.asTool() のグラフについて、安定したエージェント識別情報を記録します。これにより、再開するプロセスが同じエージェントグラフを再構築する限り、異なるエージェントが同じ name を共有していても、一時停止した実行を再開できます。

RunState.fromString(agent, serializedState) に渡す agent は、再構築されたグラフのルートです。デシリアライズ中に、SDK はそのエージェントのハンドオフと Agent.asTool() の参照を走査し、状態内のシリアライズされたすべてのエージェント参照を、再構築されたグラフに対して解決します。これには、現在のエージェントのほか、生成済みアイテム、処理済みのモデル応答、キューに入った次のステップが保持するネストされた参照も含まれます。

置き換えたグラフで再開する必要がある場合、たとえば別のランタイムによってモデルやツールがラップされたエージェントを使用する場合は、元のグラフで状態をデシリアライズし、再度シリアライズしてから、その文字列を置き換え後のルートエージェントでデシリアライズします。state.setCurrentAgent(agent) の呼び出しで変更されるのはアクティブなエージェントのみであり、デシリアライズ中にすでに解決されたネストされた参照は書き換えられません。

再開するプロセスで新しいコンテキストオブジェクトを注入する必要がある場合は、代わりに RunState.fromStringWithContext(agent, serializedState, context, { contextStrategy }) を使用します。

  • contextStrategy: 'merge'(デフォルト)は、指定された RunContext を維持し、シリアライズされた承認状態をマージします。また、新しいコンテキストに toolInput がまだ定義されていない場合は、シリアライズされた toolInput を復元します。
  • contextStrategy: 'replace' は、指定された RunContext をそのまま使用して実行を再構築します。

シリアライズされた実行状態には、アプリのコンテキストに加えて、承認、使用量、ネストされた toolInput、保留中のネストされたエージェントツールの再開情報など、SDK が管理するランタイムメタデータが含まれます。シリアライズされた状態を保存または送信する予定がある場合は、runContext.context を永続化されるデータとして扱い、意図的に状態とともに移動させる場合を除き、そこにシークレットを保存しないでください。

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

これにより、シリアライズされた状態をデータベースやリクエストとともに保存できます。

保留中タスクのバージョン管理

Section titled “保留中タスクのバージョン管理”

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

実際には、独自のコードにバージョン番号を割り当て、それをシリアライズされた状態とともに保存し、デシリアライズ時に正しいバージョンのコードへ振り分けます。