Strands AgentsのHuman in the Loopハンドラーを試してみた

Strands AgentsのHuman in the Loopハンドラーを試してみた

Strands AgentsのHumanInTheLoopハンドラーを使って、AIエージェントに人間の承認フローを組み込む方法を試してみました!
2026.07.22

はじめに

こんにちは、スーパーマーケットが大好きなコンサル部の神野(じんの)です。

前回の記事ではInterventionsのConfirmを使って承認フローを自作しましたが、ドキュメントを眺めていると、Human in the Loopという組み込みハンドラーを見つけたのでこちらも試してみました。
AIエージェントに作業を任せるうえで、人間の判断を挟みたい場面は多いので大事な機能かつ、実装も簡単になりそうです。

https://strandsagents.com/docs/user-guide/concepts/agents/interventions/human-in-the-loop/

Interventionsの基本(Deny / Guide / Confirm / Transformの型付きアクション)については前回の記事をご覧ください。

https://dev.classmethod.jp/articles/strands-agents-interventions

前提

今回の検証環境は下記の通りです。

項目 バージョン
OS macOS (Apple Silicon)
Python 3.12
strands-agents 1.46.0
bedrock-agentcore 1.18.0
モデル Claude Haiku 4.5 (Amazon Bedrock)

HumanInTheLoopハンドラーは追加パッケージなしで使えます。strands-agentsをインストールしておけばOKです。AgentCore Runtimeの節では bedrock-agentcore も使います。

セットアップ
uv add 'strands-agents==1.46.0'
uv add 'bedrock-agentcore==1.18.0'

サンプルコード一式はGitHubに置いています。
必要に応じてご参照ください!

https://github.com/yuu551/strands-hitl-samples

HumanInTheLoopハンドラー

公式ドキュメントを確認すると、下記のように記載があります。

The HumanInTheLoop intervention handler pauses agent execution before tool calls to request human approval. It provides a configurable, drop-in way to add human oversight without writing custom interrupt logic.

ツール実行前にエージェントを一時停止して人間の承認を求めるハンドラーで、カスタムのinterrupt処理を書かずに人間の監督を組み込める方式です。内部的にはInterventionsのConfirmアクションを使っていて、承認の判定フローは次のようになっています。

ツール呼び出しが発生すると、まず許可リスト(allowed_tools)にあるかチェックされ、あれば即実行します。なければTrust済みかチェックされ、Trust済みなら実行します。どちらでもなければ人間に承認を求め、承認されれば実行、拒否されればキャンセルという流れです。

公式ドキュメントのフローチャートを参考に日本語化すると、こんな感じです。

人間への聞き方は、 ask オプションで3つのモードから選べます。

モード askの指定 用途
interrupt/resume 指定なし(デフォルト) Web UIやSlackなど、応答を外部で収集する場合
stdio ask="stdio" CLIアプリ。標準入力でその場で確認
カスタムコールバック ask=関数 Slack DMやWebモーダルなど独自UI

ここからは、stdioモード・許可リスト・TrustモードをCLIで試したあと、フロントエンドと繋ぐ想定でinterrupt/resumeモードをAgentCore Runtimeに載せるイメージで実装してみます。

やってみた

stdioモードで最小構成の承認フロー

まずはCLIで一番手軽なstdioモードから試してみます。前回Confirmで自作したのと同じ「ファイル削除に承認を求める」フローを作ってみます。
承認フローの確認が目的なので、サンプルの delete_file は実際にはファイルを削除せず、削除した体のメッセージだけを返します。

hitl1_stdio.py
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.hitl import HumanInTheLoop

@tool
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する"""
    print(f"\n[delete_file] {path} を削除しました")
    return f"{path} を削除しました"

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

agent = Agent(
    model=model,
    tools=[delete_file],
    interventions=[HumanInTheLoop(ask="stdio")],
    system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)

result = agent("old_report.pdf を削除してください")

前回はConfirmを返すハンドラークラスと、stop_reasonを見てinterruptに回答するwhileループを書きましたが、今回は HumanInTheLoop(ask="stdio") を渡すだけです。
承認プロンプトの表示も入力待ちも全部ハンドラーがやってくれます。

実行コマンド
uv run hitl1_stdio.py
実行結果(yで承認)
了解いたしました。old_report.pdf を削除します。
Tool #1: delete_file
Tool "delete_file" requires human approval. Input: {"path": "old_report.pdf"} (y/n): y

[delete_file] old_report.pdf を削除しました
old_report.pdf の削除が完了いたしました。
実行結果(nで拒否)
ファイル old_report.pdf を削除します。
Tool #1: delete_file
Tool "delete_file" requires human approval. Input: {"path": "old_report.pdf"} (y/n): n
申し訳ございませんが、セキュリティ上の理由からファイル削除操作には人間による承認が必要です。old_report.pdf を削除してもよろしいでしょうか?確認をお願いします。

ツール名と入力パラメータ入りの承認プロンプトが自動で表示されます。yなら実行、nならツールはキャンセルされてモデルが状況を説明してくれます。
シンプルな実装で良いですね!

許可リストで読み取り系ツールの承認をスキップ

全ツールで毎回承認を求められるとさすがに面倒かなと思うので、安全な読み取り系ツールは許可リストに入れて挙動を確認してみます。

hitl2_allowed.py
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.hitl import HumanInTheLoop

@tool
def read_file(path: str) -> str:
    """指定パスのファイルを読み取る"""
    print(f"\n[read_file] {path} を読み取りました")
    return "log_retention: 7days\nlog_dir: /tmp/old-logs"

@tool
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する"""
    print(f"\n[delete_file] {path} を削除しました")
    return f"{path} を削除しました"

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

agent = Agent(
    model=model,
    tools=[read_file, delete_file],
    interventions=[
        HumanInTheLoop(
            ask="stdio",
            allowed_tools=["read_file"],
        )
    ],
    system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)

result = agent("config.yaml を読んでから /tmp/old-logs を削除してください")

allowed_tools にはツール名のほか、全許可の "*" や否定の "!tool_name" も書けます。 ["*", "!delete_file"] とすれば delete_file 以外すべてOK、という書き方ができるので、危険なツールだけ承認対象にする運用も実現できます!

実行コマンド
uv run python hitl2_allowed.py
実行結果
Tool #1: read_file

[read_file] config.yaml を読み取りました
config.yaml を確認しました。次に /tmp/old-logs を削除します。
Tool #2: delete_file
Tool "delete_file" requires human approval. Input: {"path": "/tmp/old-logs"} (y/n): y

[delete_file] /tmp/old-logs を削除しました
完了しました。

read_file はそのまま実行され、 delete_file だけで承認を求められました。読み取りのたびに操作を止めずに済むので、CLIでも使いやすくなります。

Trustモードなら一度信頼したツールは以降スキップ

同じツールを何度も呼ぶタスクだと、毎回yを打つのは面倒ですよね。皆さんもコーディングエージェントとかお使いなら馴染みがある場面じゃないでしょうか。そういった際は enable_trust を有効にすると、承認時にtと答えることで「今回のセッション中、このツールはもう聞かなくていい」という指示ができます。

hitl3_trust.py
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.hitl import HumanInTheLoop

@tool
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する"""
    print(f"\n[delete_file] {path} を削除しました")
    return f"{path} を削除しました"

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

agent = Agent(
    model=model,
    tools=[delete_file],
    interventions=[
        HumanInTheLoop(
            ask="stdio",
            enable_trust=True,
        )
    ],
    system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)

result = agent("temp1.log を削除して、成功したら続けて temp2.log と temp3.log も1つずつ削除してください")

3ファイルを順番に削除させて、1回目の承認でtと答えてみます。

実行コマンド
uv run python hitl3_trust.py
実行結果
temp1.log を削除します。
Tool #1: delete_file
Tool "delete_file" requires human approval. Input: {"path": "temp1.log"} (y/n/t): t

[delete_file] temp1.log を削除しました
temp1.log の削除が成功しました。次に temp2.log を削除します。
Tool #2: delete_file

[delete_file] temp2.log を削除しました
temp2.log の削除が成功しました。最後に temp3.log を削除します。
Tool #3: delete_file

[delete_file] temp3.log を削除しました
完了しました!

お、1回目だけ承認を求められて、2回目と3回目はプロンプトなしで実行されています。Claude Codeの「今後確認しない」と同じ体験が作れましたね!面白い!

Trust状態は agent.state に保存されるのでセッション内のターンをまたいで有効ですが、エージェントを作り直すとリセットされます。なお、 "!tool_name" で否定指定したツールはTrustできず毎回必ず確認される仕様なので、絶対に確認したい操作はそちらで守れます。

Trust状態をAgentCore Memoryで永続化する

Trust状態は agent.state に保存されると書きましたが、 agent.state はJSON serializableなkey-valueストレージで、Session Managerを設定すると外部に永続化できます。ここではAmazon Bedrock AgentCore Memoryを使って、プロセスを再起動してもTrust状態が復元されることを確認してみます。

AgentCore Memoryのリソースは事前に作成しておきます。

Memoryリソースの作成(1回だけ)
from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")
memory = client.create_memory(
    name="hitlTrustDemo",
    description="HumanInTheLoop Trust persistence demo",
)
print(f"Memory ID: {memory['id']}")

リソースがACTIVEになったら、 AgentCoreMemorySessionManager をAgentに渡します。

hitl_trust_persist.py
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import (
    AgentCoreMemorySessionManager,
)
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.hitl import HumanInTheLoop

MEMORY_ID = "your-memory-id"
SESSION_ID = "user-session-001"

@tool
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する"""
    print(f"\n[delete_file] {path} を削除しました")
    return f"{path} を削除しました"

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

config = AgentCoreMemoryConfig(
    memory_id=MEMORY_ID,
    session_id=SESSION_ID,
    actor_id="demo-user",
)

with AgentCoreMemorySessionManager(
    agentcore_memory_config=config, region_name="us-east-1"
) as session_manager:
    agent = Agent(
        model=model,
        tools=[delete_file],
        interventions=[HumanInTheLoop(ask="stdio", enable_trust=True)],
        session_manager=session_manager,
    )

    agent("temp.log を削除してください")

初回実行でtを入力してTrustします。

初回実行
temp.log を削除します。
Tool #1: delete_file
Tool "delete_file" requires human approval. Input: {"path": "temp.log"} (y/n/t): t

[delete_file] temp.log を削除しました
temp.log ファイルの削除が完了しました。

この時点で agent.state にはTrust状態が入っています。

agent.stateの中身
{
  "hitl:trusted_tools": [
    "delete_file"
  ]
}

AgentCoreMemorySessionManager がこのstateをAgentCore Memoryに永続化してくれるので、プロセスを終了しても消えません。ここでプロセスを落として、同じ SESSION_ID で新しいAgentを作り直してみます。

復元の確認
# プロセス再起動後、同じSESSION_IDで新しいAgentを作成
config = AgentCoreMemoryConfig(
    memory_id=MEMORY_ID,
    session_id=SESSION_ID,  # 同じセッションID
    actor_id="demo-user",
)

with AgentCoreMemorySessionManager(
    agentcore_memory_config=config, region_name="us-east-1"
) as session_manager:
    agent = Agent(
        model=model,
        tools=[delete_file],
        interventions=[HumanInTheLoop(ask="stdio", enable_trust=True)],
        session_manager=session_manager,
    )

    # stateが復元されているか確認
    print(agent.state.get())
    # => {'hitl:trusted_tools': ['delete_file']}

    agent("another.log を削除してください")
2回目の実行結果(プロセス再起動後)
{'hitl:trusted_tools': ['delete_file']}
another.log を削除します。
Tool #1: delete_file
another.log ファイルの削除が完了しました。

agent.state.get()hitl:trusted_tools が復元されていることが確認でき、 delete_file は承認プロンプトなしで実行されました。AgentCore Memoryの短期記憶(STM)として会話履歴とstateが保存されるため、microVMが停止してもTrust状態を引き継げます。

AgentCore Memory以外にも FileSessionManager(ローカル)や S3SessionManager(S3)が使えるので、環境に合わせて選べます。

なお、Trustを永続化すると、同じ actor_idsession_id からstateを復元できる間は承認がスキップされます。本番では認証ユーザーに紐づけ、期限や取り消し方法を設けておくのが安全です。破壊的なツールは永続Trustの対象外にする判断も必要になります。

interrupt/resumeモードでフロントエンドと繋ぐ

ここまでのstdioモードはCLI前提でしたが、実際のプロダクトだとチャットUIを持つWebアプリに組み込むケースが多いと思います。その場合は、 ask を指定しないデフォルトのinterrupt/resumeモードを使います。エージェントが stop_reason == "interrupt" で停止するので、interruptの内容をフロントエンドに返して承認ダイアログを出し、ユーザーの回答を渡して再開する、という流れになります。

今回はデプロイ先としてそのまま使えるAmazon Bedrock AgentCore Runtimeを想定して作ります。 BedrockAgentCoreApp のHTTPエントリポイントは /invocations の1つだけですが、interruptへの回答も結局は agent(...) に渡す次の入力なので、ペイロードに responses があれば承認応答、なければ通常メッセージとして扱えばよさそうです。

エントリポイントはAgentCore SDKのBedrockAgentCoreAppで書きます。

hitl6_agentcore.py
from bedrock_agentcore.runtime import BedrockAgentCoreApp

from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.hitl import HumanInTheLoop

@tool
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する"""
    print(f"[delete_file] {path} を削除しました")
    return f"{path} を削除しました"

model = BedrockModel(model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0")

agent = Agent(
    model=model,
    tools=[delete_file],
    interventions=[HumanInTheLoop()],
    system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
)

app = BedrockAgentCoreApp()

@app.entrypoint
def invoke(payload):
    if payload.get("responses"):
        result = agent(
            [
                {
                    "interruptResponse": {
                        "interruptId": r["interrupt_id"],
                        "response": r.get("response"),
                    }
                }
                for r in payload["responses"]
            ]
        )
    else:
        result = agent(payload.get("message"))

    if result.stop_reason == "interrupt":
        return {
            "status": "pending_approval",
            "interrupts": [
                {
                    "interrupt_id": interrupt.id,
                    "prompt": interrupt.reason,
                }
                for interrupt in result.interrupts
            ],
        }
    return {"status": "done", "message": str(result)}

if __name__ == "__main__":
    app.run()

ペイロードに responses があればinterruptResponseとしてエージェントを再開し、なければ通常のメッセージとして渡します。result.interrupts はリストなので、モデルが複数のツールを同時に呼ぶ場合に備えて全件をクライアントへ返却し、resume時も全件分の応答を受け取ります。エージェント側のコードは相変わらず HumanInTheLoop() の1行だけで、承認プロトコルの部分はただのJSONのやり取りです。

BedrockAgentCoreAppはローカル実行するとポート8080で /invocations を受け付けるので、起動して、フロントエンドの代わりにcurlで一連の流れを追ってみます。

起動
uv run python hitl6_agentcore.py

まずはチャットでファイル削除を依頼します。

実行コマンド
curl -s -X POST http://localhost:8080/invocations \
  -H "Content-Type: application/json" \
  -d '{"message": "old_report.pdf を削除してください"}'
レスポンス
{
    "status": "pending_approval",
    "interrupts": [
        {
            "interrupt_id": "v1:before_tool_call:tooluse_bM0fXqg8P1ZC1AkXqQEF1v:b9e6f0f7-048e-5f43-a7b0-2b41b64a57ec",
            "prompt": "Tool \"delete_file\" requires human approval. Input: {\"path\": \"old_report.pdf\"}"
        }
    ]
}

ツール実行前に停止し、承認待ちのレスポンスが返りました。 interrupts 配列の各要素に prompt が含まれるため、フロントエンドはこれをダイアログへ表示できます。返ってきた interrupt_id を付けて、同じ /invocations に承認を送ります。

実行コマンド
curl -s -X POST http://localhost:8080/invocations \
  -H "Content-Type: application/json" \
  -d '{"responses": [{"interrupt_id": "v1:before_tool_call:tooluse_bM0fXqg8P1ZC1AkXqQEF1v:b9e6f0f7-048e-5f43-a7b0-2b41b64a57ec", "response": "yes"}]}'
レスポンス
{
    "status": "done",
    "message": "完了しました。ファイル「old_report.pdf」を削除しました。\n"
}

承認からのツール実行、完了メッセージまで単一のエントリポイントで受付できました。拒否した場合も見ておきます。 response"no" を渡すと、ツールは実行されずにエージェントが応答を返してきます。

responseをnoにした場合のレスポンス
{
    "status": "done",
    "message": "申し訳ありませんが、ファイルの削除には人的確認が必要です。backup.zip を本当に削除してよろしいですか?削除すると復元できない可能性があります。確認をお願いします。\n"
}

デプロイ後もJSONペイロードの形は変わりません。ただし、通常メッセージと承認応答の InvokeAgentRuntime 呼び出しには、必ず同じ runtimeSessionId を指定します。AgentCore RuntimeはこのIDで同一セッションのmicroVMへルーティングするため、異なるIDを使うと別の環境に届いてresumeできません。サーバー側で最初の呼び出しに使った runtimeSessionId を保持し、承認応答でも再利用します。

フロントエンド側は、このAPIに対して下記のような処理を書くイメージです。 status"pending_approval" だったら各interruptに対して確認ダイアログを出して、全件分の応答を同じ入り口に送り返します。

フロントエンド側のイメージ
// invokeAgent はサーバー経由でInvokeAgentRuntimeを呼ぶ関数の想定
let res = await invokeAgent({ message: userInput });

while (res.status === "pending_approval") {
  const responses = res.interrupts.map((intr) => ({
    interrupt_id: intr.interrupt_id,
    response: window.confirm(intr.prompt) ? "yes" : "no",
  }));
  res = await invokeAgent({ responses });
}

showMessage(res.message);

whileループにしているのは、1ターンの中で承認が複数回発生するケース(複数の危険ツールを順に呼ぶ場合など)に対応するためです。

デモUI

React(Vite)でフロントエンドを作って、ローカルのAgentCoreサーバーに繋いでみました。ファイル削除を依頼すると、ツール実行前に承認ダイアログが表示されます。

承認ダイアログ。ツール名と入力パラメータがJSON表示され、承認/拒否ボタンが並ぶ

承認するとツールが実行され、完了メッセージが返ります。

承認後の画面。承認ログとエージェントの完了メッセージが表示される

拒否するとツールは実行されず、エージェントが状況を説明してくれます。

拒否後の画面。拒否ログとエージェントの確認メッセージが表示される

実際にデモを試したい場合は、リポジトリをcloneしてご利用ください!

カスタムUIについては、 ask に関数を渡す3つ目のモードもあります。

カスタムコールバックの例
async def ask(prompt: str) -> str:
    # Slack DMやWebモーダルなど、好きなUIで質問して回答を返す
    return await ask_user_via_slack(prompt)

agent = Agent(
    tools=[delete_file],
    interventions=[HumanInTheLoop(ask=ask)],
)

WebSocketやSlackのように、承認の問いかけをこちらからプッシュできるチャネルであれば、上記のように書けます。逆に今回のようなリクエスト・レスポンス型のWeb APIでは、人間が回答するまでリクエストを保持し続ける設計はタイムアウトや切断を考えると扱いづらいため、interrupt/resumeモードで一度クライアントにボールを返す設計が向いているように感じました。

補足 interrupt状態の永続化

記事中のセッション補足で「承認が長時間放置される場合はSession Managementで状態を永続化する設計が必要」と書きましたが、実際にできるのか確認しました。

AgentCore Memoryをsession managerに設定したagentでinterruptを発生させ、withブロックを抜けてagentを破棄します。その後、同じ session_id で新しいagentを作り、控えておいた interrupt_id でresumeしてみます。

検証結果
Step 1: interruptで停止
interrupt_id: v1:before_tool_call:tooluse_T1BF...
messages count: 2

Step 2: agentを破棄(microVM停止を模擬)

Step 3: 同じsession_idで復元してresume
復元後のmessages count: 2

[delete_file] important.doc を削除しました
✅ AgentCore Memoryでinterrupt状態を復元してresumeできた

Session Managerは会話履歴に加え、Agentの内部状態にあるinterrupt stateも永続化します。そのため、同じ session_idagent_id でAgentを再構築すると中断時点の状態が復元され、控えておいた interrupt_id でresumeできました。今回のコードでは agent_id を省略しているため、どちらもデフォルトの "default" になっています。

検証に使ったスクリプト
test_interrupt_agentcore_memory.py
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import (
    AgentCoreMemorySessionManager,
)
from strands import Agent, tool
from strands.models import BedrockModel
from strands.vended_interventions.hitl import HumanInTheLoop

REGION = "us-east-1"
MEMORY_ID = "your-memory-id"
SESSION_ID = "interrupt-persist-test-001"
ACTOR_ID = "test-user"

@tool
def delete_file(path: str) -> str:
    """指定パスのファイルを削除する"""
    print(f"\n[delete_file] {path} を削除しました")
    return f"{path} を削除しました"

model = BedrockModel(
    model_id="us.anthropic.claude-haiku-4-5-20251001-v1:0",
    region_name=REGION,
)

# Step 1: interruptで停止
config1 = AgentCoreMemoryConfig(
    memory_id=MEMORY_ID, session_id=SESSION_ID, actor_id=ACTOR_ID,
)

with AgentCoreMemorySessionManager(
    agentcore_memory_config=config1, region_name=REGION
) as sm1:
    agent1 = Agent(
        model=model,
        tools=[delete_file],
        interventions=[HumanInTheLoop()],
        session_manager=sm1,
        system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
    )
    result1 = agent1("important.doc を削除してください")
    interrupt_id = result1.interrupts[0].id
    print(f"interrupt_id: {interrupt_id}")
    print(f"messages count: {len(agent1.messages)}")

# withブロックを抜けてagent破棄(microVM停止を模擬)

# Step 2: 同じsession_idで復元してresume
config2 = AgentCoreMemoryConfig(
    memory_id=MEMORY_ID, session_id=SESSION_ID, actor_id=ACTOR_ID,
)

with AgentCoreMemorySessionManager(
    agentcore_memory_config=config2, region_name=REGION
) as sm2:
    agent2 = Agent(
        model=model,
        tools=[delete_file],
        interventions=[HumanInTheLoop()],
        session_manager=sm2,
        system_prompt="ユーザーに確認を求めず、自律的に判断してタスクを完了してください。",
    )
    print(f"復元後のmessages count: {len(agent2.messages)}")

    result2 = agent2(
        [{"interruptResponse": {"interruptId": interrupt_id, "response": "yes"}}]
    )
    print(f"stop_reason: {result2.stop_reason}")
    print(f"結果: {result2}")

設定オプションまとめ

今回試したものを含め、設定できるパラメータは次のとおりです。

パラメータ デフォルト 説明
ask なし(interrupt/resume) "stdio"、コールバック関数、または省略
allowed_tools なし 承認をスキップするツール。"*""!tool_name" が使える
enable_trust False tでセッション中の承認スキップを許可
evaluate y / yes / True を承認 承認判定のカスタムロジック
evaluate_trust t / trust を受理 Trust判定のカスタムロジック

evaluate を差し替えると、たとえば「confirmと完全一致で入力しないと承認しない」といった厳しめの運用もできます。

evaluateのカスタマイズ例
HumanInTheLoop(
    ask="stdio",
    evaluate=lambda response: isinstance(response, str) and response.lower() == "confirm",
)

うっかりyを押しての事故を防ぎたい、破壊的な操作向けの設定ですね。

Interruptsとの使い分け

ここまでHumanInTheLoopハンドラーの話をしてきましたが、Strands Agentsには下位レイヤーとしてInterruptsという汎用の一時停止メカニズムがあります。

https://strandsagents.com/docs/user-guide/concepts/interrupts/

HumanInTheLoopは内部的にこのInterruptsの上に構築されているので、両者の関係を整理しておきます。

Interruptsは、Hookのコールバックやツール定義の中から event.interrupt() / context.interrupt() を呼び出してエージェントを一時停止する仕組みです。interruptに名前と任意のJSON payloadを付けられるので、「承認を求める」だけでなく、「追加パラメータを聞く」「ユーザーに選択肢を提示して選ばせる」といった自由な対話を差し込めます。その代わり、 stop_reason の判定、 result.interrupts のループ、 interruptResponse の組み立てと再開といった一連のライフサイクル管理を自分で書く必要があります。前回の記事ではInterventionsの Confirm を使いましたが、呼び出し側のresumeループは自前で書いていました。

一方でHumanInTheLoopは、そのInterruptsの上にInterventionsのConfirmアクションを載せ、さらに許可リスト・Trust・収集モードを加えたものです。ツール実行前に承認・拒否を聞き、Confirmの生成、承認判定、許可リスト、Trust、回答の収集方法をまとめて提供します。

使い分けの基準は公式ドキュメントでも次のように書かれています。

  • ツール実行前の承認・拒否で済むなら、HumanInTheLoopを使う
  • 承認時に追加の入力を受け取りたい、interruptの名前やreasonの形式を設計したい、ツール定義の途中で複数ステップの対話を挟みたいなど、単純な承認・拒否に収まらないフローが必要ならInterruptsを使う

体感として、ツール承認という用途に限ればHumanInTheLoopを使うのが良さそうです。シンプルですもんね。

おわりに

今回紹介したTrustモードは、Claude Codeのようなエージェント系ツールで日常的に触っている体験そのものが数行で再現できて、かなりコーディングAIエージェントを意識した機能が増えてきたなと感じました。人間の承認を求めるケースではこのハンドラーを使っていきたいですね。

本記事が少しでも役立ちましたら幸いです!最後までご覧いただきありがとうございましたー!!

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事