コンテンツにスキップ

人間の介入(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 に残って再び実行を一時停止します。

alwaysApprove: true または alwaysReject: true で作成された固定判断は、同じツールへの後続の呼び出しに対するデフォルトになります。1 つの呼び出し ID に対する個別の判断は、その固定されたデフォルトより優先されます。他の呼び出しのデフォルトを変更することなく、常時承認の設定下で 1 つの呼び出しを拒否したり、常時拒否の設定下で 1 つの呼び出しを承認したりできます。後からその個別の判断を置き換えた場合、新しい個別判断がその呼び出しに適用され、固定されたデフォルトは残りの呼び出しに引き続き使用できます。

実行が一時停止している間に新しいユーザー入力が届き、次に再開されるモデル呼び出しの前に取り込む必要がある場合は、RunState.addInput() を使用します。承認を解決して同じ状態を Runner.run() に戻す前に、入力を追加します。準備された入力はシリアライズされた状態の一部となるため、未解決の承認やローカルツールの処理によってそのモデル呼び出しが遅延しても、toString() / fromString() 後に維持されます。

準備された項目の複製されたスナップショットを確認するには state.pendingInput を読み取り、再開前にすべて削除するには state.clearPendingInput() を呼び出します。addInput() は、文字列または入力項目の配列を受け取ります。終端状態、残りのターンがない状態、またはツールの実行結果によって実行が終了する可能性がある割り込みなど、状態が安全に次のモデル呼び出しへ到達できない場合は、UserError が発生します。

取り込まれると、準備された各入力は実行の newItems 内で RunInputItem になり、その元の入力項目が history に含まれます。ローカルの session を使用する場合、SDK はモデルリクエストを開始する前に、取り込まれた入力を一度だけ永続化します。conversationId または previousResponseId を使用する場合、サーバーがレスポンスを受け入れるまで入力は保留状態のままになるため、受け入れ前に発生したことが明らかな失敗では、入力を失わずに再開できます。リクエストがすでに受け入れられた可能性があるとプロバイダーから報告された場合、SDK はその入力をチェックポイントに記録し、暗黙に再実行する代わりに安全側で失敗させます。独立した明示的な安全でない再実行のオーバーライドについては、モデルの再試行を参照してください。

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

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

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

同じ割り込みフローは、ストリーミング実行でも機能します。ストリーミング実行が一時停止したら、stream.completed を待ち、stream.interruptions を読み取り、それらを解決します。再開後の出力でもストリーミングを続けるには、{ stream: true } を指定して run() を再度呼び出します。このパターンのストリーミング版については、ストリーミング中の Human in the loop (人間の介入)を参照してください。

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 が確実に判定できる場合、現在のレスポンスで生成された項目の正確な所有関係を記録します。そのため、対応している出力を伴う部分承認のチェックポイントは、シリアライズされた状態との間で往復変換できます。SDK は、以降のモデル、ツール、またはセッションの副作用が発生する前に、その所有関係を検証します。所有関係が欠落している、無効である、または曖昧である古いスナップショットやチェックポイントでは、引き続き安全側で失敗し、UserError が発生します。この場合は、安全な入力から新しい実行を開始してください。同じエージェントグラフを再構築し、元の toolUseBehavior を維持してください。元の項目を再実行して所有関係エラーを回避しないでください。

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 を並行してインストールし、独自の分岐ロジックを実装することを推奨します。

具体的には、独自のコードにバージョン番号を割り当て、シリアライズされた状態とともに保存し、デシリアライズ時に適切なバージョンのコードが使用されるようにします。