agent-runtime-hooks について
`execAgent` を使う開発者向けのライフサイクルフックで、ステップ、ツール呼び出し、圧縮、人間による介入などを扱います。ローカル処理、ツール結果のモック、Webhook による判定に対応します。
主な機能
- 0116 種類のライフサイクルイベント
- 02登録順でのフック実行
- 03ローカルハンドラーによる結果モック
- 04Webhook によるイベント配信
- 05ツール呼び出しの許可・拒否
活用シーン
- 01Agent のステップとツール呼び出しの記録
- 02テストでのツール結果のモック
- 03Webhook を介したツール実行の審査
- 04人間の承認とサブエージェントイベントの追跡
MCP 対応
MCP ツールは他のツールと同じイベント経路を使い、identifier、apiName、toolSource で絞り込めます。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ lobehub/ lobehub --skill agent-runtime-hooksインストール後、Claude Code で/agent-runtime-hooksを入力して読み込まれたか確認します。
詳細
エージェントランタイムのフック
execAgent({ hooks }) を通じてライフサイクルフックを登録します。HookDispatcher は操作ごとにフックを保存し、登録順にディスパッチします。
フックの種類
5つのカテゴリにわたる16種類のフック:
execAgent({ hooks })
│
├─ beforeStep ──────────── Before each step executes
│ │
│ ├─ [call_llm] LLM inference
│ │
│ ├─ [call_tool]
│ │ ├─ beforeToolCall ── Before tool executes (supports mocking)
│ │ ├─ (tool execution)
│ │ ├─ afterToolCall ─── After tool completes (optional result control)
│ │ └─ onToolCallError ─ Tool threw an exception
│ │
│ ├─ [request_human_approve]
│ │ ├─ beforeHumanIntervention ── Before agent pauses
│ │ ├─ afterHumanIntervention ─── After approve/reject + resume
│ │ └─ onStopByHumanIntervention ── User rejected, agent halted
│ │
│ ├─ [compress_context]
│ │ ├─ beforeCompact ──── Before compression starts
│ │ ├─ afterCompact ───── After compression completes
│ │ └─ onCompactError ─── Compression failed
│ │
│ ├─ [callAgent] (via execSubAgentTask)
│ │ ├─ beforeCallAgent ── Before sub-agent starts
│ │ ├─ afterCallAgent ─── After creation/start returns
│ │ └─ onCallAgentError ── Sub-agent failed
│ │
│ └─ afterStep ──────────── After step completes
│
├─ (next step...)
│
├─ onComplete ───────────── Operation reaches terminal state
└─ onError ──────────────── Error during execution
主要なファイル
| ファイル | 役割 |
|---|---|
packages/agent-runtime/src/types/hooks.ts | イベントの種類と必須/任意フィールド |
apps/server/src/services/agentRuntime/hooks/types.ts | 登録とWebhookの種類 |
apps/server/src/services/agentRuntime/hooks/HookDispatcher.ts | 登録、ディスパッチ、ローカルモック、HTTP/QStash配信 |
apps/server/src/services/agentRuntime/hooks/webhookPayload.ts | イベントの投影とメール情報の付加 |
apps/server/src/modules/AgentRuntime/adapters/toolCallHookContext.ts | 共有ツールイベントコンテキストビルダー |
apps/server/src/modules/AgentRuntime/adapters/ServerToolTransport.ts | ツール通知と実行結果 |
apps/server/src/modules/AgentRuntime/RuntimeExecutors.ts | ツール、圧縮、人による介入の実行 |
apps/server/src/services/agentRuntime/AgentRuntimeService/ (executeStep/, intervention.ts) | ステップイベントと人による介入後の続行 |
apps/server/src/services/agentRuntime/CompletionLifecycle/index.ts | 終端イベント |
apps/server/src/services/aiAgent/subAgentRuns.ts | サブエージェントイベント |
登録
const hooks: AgentHook[] = [
{
id: 'observe-step',
type: 'afterStep',
handler: async (event) => {
console.log(event.operationId, event.stepIndex);
},
},
];
await aiAgentService.execAgent({ agentId, prompt, hooks });
完了時に、hookDispatcher.unregister(operationId) を通じて登録をクリーンアップします。
Webhook フック
Webhook のみのフックは、ローカルモードとキューモードの両方で配信されます。handler と webhook の両方を持つフックの場合、ローカルディスパッチではハンドラーが呼び出され、キューディスパッチではシリアライズされた Webhook が使用されます。永続化されるのは Webhook の設定のみのため、プロセスの再起動後は関数を利用できません。
const hook: AgentHook = {
id: 'tool-notification',
type: 'afterToolCall',
matcher: '^fs/readFile$',
webhook: {
url: 'https://example.com/hooks',
delivery: 'fetch',
timeout: 5, // seconds; default 30
headers: { Authorization: 'Bearer ${HOOK_TOKEN}' },
allowedEnvVars: ['HOOK_TOKEN'],
},
};
matcherは${identifier}/${apiName}に対する正規表現で、ツールイベントでのみサポートされます。省略、空、または*の場合は、すべてのツールに一致します。- ヘッダーの環境変数テンプレートは送信時にのみ
allowedEnvVarsから解決されます。解決前のテンプレートを永続化し、解決済みのシークレットは決して永続化しないでください。 - 通知ではレスポンスの内容は無視され、HTTP 204 が返される場合があります。
responseHandling: 'toolCall'を指定したbeforeToolCallは、ツールの実行前に任意の HTTP 判定を待ちます。介入するには、{ "decision": "allow" }または{ "decision": "deny", "reason": "禁止执行该操作" }を含む HTTP 2xx の JSON を返してください。型は{ decision: 'allow' } | { decision: 'deny'; reason?: string }です。deny で reason が省略または空の場合は、Blocked by beforeToolCall hook.が使用されます。拒否理由は分類hook_deniedとともにツール結果/カードに保持されます。afterToolCallでもresponseHandling: 'toolCall'を使用できます。アーカイブおよびモデルによる処理の前に、リクエスト/結果全体を同期的に確認します。拒否すると、元の結果はレシーバーからの任意の理由で置き換えられ、コンテンツとブロック状態の両方に反映されます。deny で reason が省略または空の場合は、beforeToolCallと同じBlocked by ${type} hook.のルールに従い、Blocked by afterToolCall hook.が使用されます。beforeToolCallと同様に、理由はモデルコンテキストとツールカードに含まれます。ツールの副作用、再試行、または課金を取り消すものではありません。遅延完了とランタイムの境界については、結果制御の仕様を参照してください。- 環境変数によるレスポンスモードは
ignore(通知のデフォルト)とtoolCall(before と after の両方で任意の判定に対応)です。有効になるのは、サブスクライブされたイベントのみです。削除されたtoolResult/toolCallAndResultの値はtoolCallに置き換えてください。after のみの確認を選択するには、イベントリストを使用します。有効な判定を含む既存の after レスポンスは、現在、結果を制御します。 - HTTP 204、空の成功レスポンス本文、無効な JSON、トップレベルの decision を含まない JSON(配列、スカラー値、null、旧形式のネストされた
hookSpecificOutputのみの場合を含む)は、onError: 'block'であっても通知レスポンスです。後続のフックも引き続き確認してください。通知を、後続のポリシーをスキップする許可として扱わないでください。明示的な無効判定、2xx 以外のステータス、サイズ超過の本文、無効な UTF-8、ネットワーク障害、タイムアウトには、onError: 'continue' | 'block'(デフォルトはcontinue)が適用されます。ブロック時には、before と after の両方のフックで、hook_control_errorがツール結果のコンテンツおよび理由として使用されます。キャンセルによって許可が与えられることはありません。 - 追加のレスポンスフィールドは許可されますが、破棄されます。使用されるのは
decisionと、deny の場合の任意の文字列reasonのみです。updatedInput/additionalContextによって、ツールの引数や会話コンテキストが変わることはありません。 - 設定:
packages/types/src/agentHook.ts、レスポンス解析:packages/types/src/agentHookResponse.ts、HTTP 配信:apps/server/src/services/agentRuntime/hooks/httpWebhook.ts。
フック設定の有効期間
- 環境フック(
AGENT_HOOK_WEBHOOK_*)は、人間による承認後や遅延完了後も、常に実行中のワーカーの現在の環境から取得します。設定を永続化したり、以前の環境のフックを復元したりしないでください。 - コードから指定されたWebhookフック(
execAgent({ hooks }))は、既存の操作ごとのシリアライズと永続化の仕組みを維持します。呼び出し元のフックは、ランタイムスナップショットまたは永続化された操作レコードから復元します。環境変数のテンプレートは、シークレットを展開してストレージに保存することなく維持してください。 - 復元した呼び出し元のフックを各操作について順番に評価し、その後、最終ラウンドで現在の環境フックを一度評価します。操作間で呼び出し元のフックIDを重複排除しないでください。環境フックのみの場合、操作を永続ストレージに保存する必要はありません。環境フックを削除すると、それ以降の結果への制御は停止します。一致する結果制御が残っていない場合は、結果を許可します。承認や再起動をまたいで以前の環境ポリシーを固定しないでください。
- 遅延完了は空のプレースホルダーに一度だけ公開します。重複したコールバックでは、拒否を含め、最初の最終結果を保持します。呼び出し元ポリシーのストレージエラーは、読み取りの失敗を空のフックリストとして扱わず、そのまま伝播させてください。設定済みフックへの配信に失敗した場合に、デフォルトで許可してはいけません。これらの失敗は
onErrorに従います。 - 回帰テストでは、承認後およびスナップショットの有効期限切れ後の環境フックの削除・追加、レビュー用マーカーなしでの公開、永続化された呼び出し元フックによる制御の継続を検証してください。
結果フック後に公開する
- アーカイブ、メッセージ内容・状態の書き込み、ストリーミング、モデルによる利用の前に、結果制御の完了を待ちます。許可された結果またはサニタイズ済みの拒否を直接永続化してください。
toolResultControlの保留中・許可・ブロックのメタデータを追加したり、履歴の投影を通じて生の出力を隠したりしないでください。 - 人間による承認の発生元は決定を確定し、回答を非公開の解決データ/送信待ちデータに保持します。
MessageModel.resolveHumanApprovalはデフォルトで確定のみを行います。結果制御後にpublishResult: trueを指定して呼び出すのは、ランタイムだけです。停止/拒否の受領通知は、実行済みツールの出力を含まないため、直接公開できます。 - 既存の介入操作IDまたは遅延コールバックの親操作を通じて、元の呼び出し元フックを復元します。メモリー上のフック判定は、ツールが所有する状態と分離してください。ツールが
type: 'blocked'を返しても、それはフックによる拒否ではありません。 - 起動に失敗したメンバーを含め、すべての遅延バックフィルで、同じく書き込みより先にフックを実行します。重複した最終書き込みの調停には既存の空のプレースホルダーを使い、代替となるレビューステートマシンを導入しないでください。
- スレッドにサブエージェントまたはグループメンバーの完了ブリッジがある場合、そのブリッジだけがツール結果を公開します。スレッドのライフサイクルフックはスレッドのメタデータのみを更新し、承認後の復旧時も含め、制御を経ていない要約をツールメッセージに事前入力しないでください。
- 回帰テストでは、フック実行中に回答が保留されること、公開とロールバックの原子性、ポリシー削除後の重複コールバック、メンバーの起動が一部失敗した場合を検証してください。履歴の読み取り側に結果レビューのロジックが不要な状態にしてください。
イベント
| イベント | タイミングとペイロード |
|---|---|
beforeStep | ステップの前;AgentHookEvent |
afterStep | ステップの後;コンテンツ、ツール呼び出し/結果、使用量の合計 |
onComplete | 終了状態;done、error、interrupted、max_steps、cost_limit などの理由 |
onError | 操作エラー;errorMessage、errorDetail、利用可能なエラーメタデータ |
beforeToolCall | ツール実行の前;共有ツールコンテキストとローカルの mock() コールバック |
afterToolCall | ツール実行の後;構造化された result と mocked |
onToolCallError | ツール実行で例外が発生;共有ツールコンテキストと error |
beforeHumanIntervention | 承認の前;pendingTools、operationId、stepIndex |
afterHumanIntervention | 承認の判断と続行;action、省略可能な toolCallId、rejectionReason |
onStopByHumanIntervention | 人間による拒否で実行を停止;省略可能な toolCallId と rejectionReason |
beforeCompact | 圧縮の前;messageCount、tokenCount、stepIndex |
afterCompact | 圧縮の後;groupId、messagesBefore、messagesAfter、summary |
onCompactError | 圧縮エラー;error、tokenCount、stepIndex |
beforeCallAgent | サブエージェント作成の前;agentId、instruction |
afterCallAgent | 作成/開始の戻り値;agentId、subOperationId、success、省略可能な threadId |
onCallAgentError | サブエージェント呼び出しの失敗;agentId、error |
CallAgent イベントは、parentOperationId で識別される親操作でディスパッチされます。分離された子は threadId を提供し、共有グループのメンバーは共有の会話を使用します。子の完了は、その子の onComplete イベントで通知されます。
ツールコンテキスト
3 つのツールイベントには buildToolCallHookContext() を使用します。
- 必須フィールド:ネイティブの
toolCallId、assistantMessageId、identifier、apiName、有効なargs、callIndex、stepIndex、operationId、executor。 - 省略可能な関連情報は、実行コンテキストから取得されます:エージェント、トピック、セッション、スレッド、グループ、タスク、ワークスペース、ドキュメント、ソース/ツールメッセージ、親操作の ID。
userIdにはruntime.userId ?? origin.userIdが使用されます。toolSourceはツールの由来を示し、executorはサーバー/クライアントのディスパッチ経路を示します。executionTargetとactiveDeviceIdは、実行計画とデバイスの選択を表します。afterToolCall.resultにはcontent、success、省略可能なexecutionTime、およびstate.type: 'blocked'などのエラー/状態データが含まれます。mockedは、ローカルフックから提供された結果であることを示します。
Sandbox ツールと MCP ツールは、同じイベント経路を使用します。identifier、apiName、toolSource でフィルタリングします。
ローカルモック
dispatchBeforeToolCall() は event.mock(result) を公開します。最初に受理されたモックが優先され、残りのモックハンドラーはスキップされます。モックの場合、このメソッドは { isMocked: true, result } を返し、実行を続行する場合は null を返します。
event.mock({ content: '{"items":[]}', success: true });
ツール監視のペイロードには BeforeToolCallObservationEvent を使用します。コールバックはインメモリのハンドラーイベントに属します。dispatchBeforeToolCall() は Webhook を配信しません。
Webhook ペイロード
createWebhookPayloadBuilder() は eventFields を選択し、hookId/hookType を追加してから、webhook.body をマージします。フィールドが重複する場合、最終的な値はボディによって決まります。ツール結果通知のペイロードでは redactResultForEvents() を使って生の skill Work データをトリミングします。コントロールには結果全体が渡されます。Work の登録では、プロセス内の結果全体が保持されます。ローカルハンドラーでは finalState を利用できます。シリアライズされたペイロードには、イベントのデータフィールドが含まれます。
メールが選択されている場合、ビルダーは最終的に有効な userId から userEmail を解決し、指定されたメールアドレスの値を置き換えます。メールのみのプロジェクションではイベント ID を使用します。明示された ID が無効な場合、メールアドレスがない場合、または検索エラーが発生した場合、メールアドレスは省略されます。
メールのクエリでは、ディスパッチャーごとに最大 1,000 ユーザーを 5 分間保持するキャッシュを共有します。クエリのタイムアウトはデータベース層で処理されます。ビルダーのオプションの第 4 引数 { signal } を使うと、待機中の呼び出し元が個別にキャンセルできます。キャンセルされた場合は undefined が返されるため、呼び出し元は配信を中止できます。Fetch と QStash では、エンリッチされたペイロードを使用します。
配信エラー
ディスパッチでは、各ハンドラーまたは Webhook の処理完了を待ちます。通常のエラーはログに記録され、ディスパッチは続行されます。fallback: 'none' を指定した Webhook で発生したエラーは CriticalHookDeliveryError として蓄積され、同じ階層にある他のフックの実行後にスローされます。QStash の配信では、デフォルトで Fetch へのフォールバックが使用されます。QStash 署名付きエンドポイントでは fallback: 'none' を使用してください。