@shinyaz

Bedrock AgentCore RuntimeのInvokeAgentRuntimeCommand APIでエージェントセッション内からシェルコマンドを実行する

目次

はじめに

2026年3月17日、AWSはAmazon Bedrock AgentCore RuntimeにInvokeAgentRuntimeCommand APIを追加した。これまでエージェントセッション内でシェルコマンドを実行するには、コンテナ内に独自のプロセス管理ロジックを組み込む必要があった。新APIはこの課題をプラットフォームレベルで解決し、テスト実行、git操作、依存インストールといった決定論的な操作をエージェントのLLM推論から分離して実行できる。

本記事では、コード構成(S3 ZIP)で最小限のランタイムを構築し、7つの観点からAPIの挙動を検証した結果を共有する。公式ドキュメントはExecute shell commands in AgentCore Runtimeを参照。

なぜシェルコマンド分離が必要か

AIコーディングエージェントの典型的なワークフローでは、LLMによるコード生成と、テスト・ビルド・lint・git操作といった決定論的な処理が交互に発生する。従来のAgentCore Runtimeでは、これらすべてをInvokeAgentRuntimeの中でエージェントコードが管理する必要があった。

この設計には3つの問題がある。

  1. 関心の混在 — LLM推論ループの中にプロセス管理、タイムアウト制御、出力キャプチャのロジックが入り込む
  2. ブロッキング — 長時間のビルドがエージェント全体をブロックする
  3. 出力のリアルタイム性 — コマンド出力をストリーミングするには独自のHTTPハンドリングが必要

InvokeAgentRuntimeCommandはこれらの課題を一発で解決する。

APIの設計

APIは以下のパラメータを受け取り、HTTP/2上のEventStreamでレスポンスを返す。

パラメータ説明
agentRuntimeArnstringデプロイ済みランタイムのARN
runtimeSessionIdstringセッションID(最小33文字)
qualifierstring通常は"DEFAULT"
body.commandstring実行するシェルコマンド(1B〜64KB)
body.timeoutintegerタイムアウト秒数(1〜3600)

レスポンスは3種類のイベントで構成される。

  • contentStart — コマンド実行開始の確認
  • contentDeltastdoutstderrのストリーミング出力
  • contentStopexitCodestatusCOMPLETEDまたはTIMED_OUT

各コマンドは新しいbashプロセスとして実行される「ワンショット実行」モデルだ。ドキュメントには「Stateless between commands」とあり、シェルヒストリーや環境変数はコマンド間で引き継がれない。一方で「Commands execute within the same container, filesystem」とも記載されており、後述の検証ではファイルシステムがセッション内で共有されることを確認した。シェル状態の引き継ぎが必要な場合のみ&&チェーンを使えばよい。

検証環境の構築

検証にはコード構成(S3 ZIP)を使い、最小限のPythonエージェントをデプロイした。以下の手順で再現できる。

前提条件:

  • AWS CLIセットアップ済み(bedrock-agentcore:*iam:*s3:*の操作権限)
  • boto3 1.42.70(検証時使用バージョン。invoke_agent_runtime_commandをサポート)
  • 呼び出し元IAMプリンシパルにbedrock-agentcore:InvokeAgentRuntimeCommand権限

エージェントコードの準備

最小限のエージェントコードmain.pyを作成してZIP化し、S3にアップロードする。InvokeAgentRuntimeCommandはエージェントのコードとは独立して動作するため、エージェント自体は最小実装で十分だ。

# main.py
import json
import sys
 
def handle_invoke(event):
    user_input = event.get("input", {}).get("text", "")
    return {
        "output": {
            "text": f"Received: {user_input}. This is a minimal test agent."
        }
    }
 
def main():
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        try:
            request = json.loads(line)
            response = handle_invoke(request)
            print(json.dumps(response), flush=True)
        except json.JSONDecodeError:
            print(json.dumps({"error": "Invalid JSON"}), flush=True)
 
if __name__ == "__main__":
    main()

デプロイ

IAMロール作成、S3アップロード、ランタイム+エンドポイント作成までを一連のコマンドで実行する。検証結果だけ読みたい場合は検証結果まで飛ばしてよい。

# 変数設定
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
BUCKET_NAME="agentcore-test-shell-cmd-${ACCOUNT_ID}"
REGION="us-west-2"
 
# S3バケット作成とコードアップロード
zip agent.zip main.py
aws s3 mb "s3://${BUCKET_NAME}" --region "$REGION"
aws s3 cp agent.zip "s3://${BUCKET_NAME}/agent.zip"
 
# IAMロール作成(AgentCoreサービスへの信頼ポリシー)
aws iam create-role \
  --role-name AgentCoreRuntimeTestRole \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {"Service": "bedrock-agentcore.amazonaws.com"},
      "Action": "sts:AssumeRole"
    }]
  }'
 
# 必要なポリシーをアタッチ
aws iam attach-role-policy \
  --role-name AgentCoreRuntimeTestRole \
  --policy-arn arn:aws:iam::aws:policy/AmazonBedrockFullAccess
 
aws iam put-role-policy \
  --role-name AgentCoreRuntimeTestRole \
  --policy-name S3Access \
  --policy-document "{
    \"Version\": \"2012-10-17\",
    \"Statement\": [{
      \"Effect\": \"Allow\",
      \"Action\": [\"s3:GetObject\", \"s3:ListBucket\"],
      \"Resource\": [
        \"arn:aws:s3:::${BUCKET_NAME}\",
        \"arn:aws:s3:::${BUCKET_NAME}/*\"
      ]
    }]
  }"
 
# ランタイム作成
aws bedrock-agentcore-control create-agent-runtime \
  --region "$REGION" \
  --agent-runtime-name shell_cmd_test_agent \
  --role-arn "arn:aws:iam::${ACCOUNT_ID}:role/AgentCoreRuntimeTestRole" \
  --agent-runtime-artifact "{
    \"codeConfiguration\": {
      \"code\": {\"s3\": {\"bucket\": \"${BUCKET_NAME}\", \"prefix\": \"agent.zip\"}},
      \"runtime\": \"PYTHON_3_13\",
      \"entryPoint\": [\"main.py\"]
    }
  }" \
  --network-configuration '{"networkMode": "PUBLIC"}'
# → レスポンスの agentRuntimeId を控える
 
RUNTIME_ID="shell_cmd_test_agent-XXXXXXXXXX"  # 実際のIDに置き換え
 
# エンドポイント作成+READYまでポーリング
aws bedrock-agentcore-control create-agent-runtime-endpoint \
  --region "$REGION" \
  --agent-runtime-id "$RUNTIME_ID" \
  --name shell_cmd_test_endpoint
 
while true; do
  STATUS=$(aws bedrock-agentcore-control get-agent-runtime-endpoint \
    --region "$REGION" \
    --agent-runtime-id "$RUNTIME_ID" \
    --endpoint-name shell_cmd_test_endpoint \
    --query 'status' --output text)
  echo "Endpoint status: $STATUS"
  [ "$STATUS" = "READY" ] && break
  sleep 10
done

ランタイム作成からエンドポイントのREADYまで、最初のポーリング(10秒以内)で完了した。コード構成の場合、コンテナイメージのビルドが不要だ。

検証結果

以下の7つの観点からAPIの挙動を検証した。

  1. 基本的なコマンド実行 — EventStreamの3段階イベント
  2. コンテナ環境 — プリインストール済みツールの確認
  3. セッション内ファイル永続化 — 別APIコールからのファイル参照
  4. セッション間隔離 — 異なるセッションIDでのファイル不可視
  5. エラーハンドリング — 非ゼロexit codeの返却
  6. タイムアウト — TIMED_OUTステータスと部分出力
  7. 並行実行 — 同一セッション内の非ブロッキング実行

以降のテストはすべて以下のPythonコードをベースにしている。セッションIDは33文字以上が必要なため、UUIDを結合して生成する。

import boto3, sys, uuid
 
client = boto3.client('bedrock-agentcore', region_name='us-west-2')
RUNTIME_ARN = "arn:aws:bedrock-agentcore:us-west-2:ACCOUNT_ID:runtime/RUNTIME_ID"
SESSION_ID = str(uuid.uuid4()) + "-" + str(uuid.uuid4())[:8]  # 45文字
 
def run_command(command, timeout=30, session_id=None):
    """コマンドを実行し、EventStreamを処理するヘルパー関数。"""
    response = client.invoke_agent_runtime_command(
        agentRuntimeArn=RUNTIME_ARN,
        runtimeSessionId=session_id or SESSION_ID,
        qualifier='DEFAULT',
        contentType='application/json',
        accept='application/vnd.amazon.eventstream',
        body={'command': command, 'timeout': timeout}
    )
    for event in response.get('stream', []):
        if 'chunk' in event:
            chunk = event['chunk']
            if 'contentStart' in chunk:
                print("[contentStart] Command execution started")
            if 'contentDelta' in chunk:
                delta = chunk['contentDelta']
                if delta.get('stdout'):
                    print(f"[stdout] {delta['stdout']}", end='')
                if delta.get('stderr'):
                    print(f"[stderr] {delta['stderr']}", end='', file=sys.stderr)
            if 'contentStop' in chunk:
                stop = chunk['contentStop']
                print(f"[contentStop] Exit code: {stop.get('exitCode')}, "
                      f"Status: {stop.get('status')}")

1. 基本的なコマンド実行とストリーミング

run_command('/bin/bash -c "echo Hello from AgentCore Runtime"')
[contentStart] Command execution started
[stdout] Hello from AgentCore Runtime
[contentStop] Exit code: 0, Status: COMPLETED

contentStartcontentDelta(stdout/stderr) → contentStopの3段階イベントが順序通り返される。ストリーミングなので長時間コマンドでも途中経過をリアルタイムに取得できる。

2. コンテナ環境の実態

uname -apython3 --versionwhoami、各ツールの--versionを実行して環境を調査した。

Linux localhost 6.1.158-15.288.amzn2023.aarch64 (Amazon Linux 2023ベース)
Python 3.13.9
User: root
Working directory: /
ツール利用可否
git2.50.1(プリインストール済み)
curl8.17.0(プリインストール済み)
python33.13.9(ランタイム指定に依存)
node未インストール
pip未インストール
aws CLI未インストール

コード構成の場合、git/curlは最初から使えるが、nodeやpip、AWS CLIは含まれない。 コンテナ構成を使えばDockerfileで自由にツールを追加できるが、コード構成では利用可能なツールが限定される点は設計時に考慮が必要だ。

3. セッション内のファイルシステム永続化

同一セッション内で作成したファイルが後続コマンドから参照できるかを検証した。以下は2回の別々のAPIコール(同一セッションID)で送信したコマンドだ。

# APIコール1: ファイル作成
run_command('/bin/bash -c "mkdir -p /tmp/test_dir && echo \'file content\' > /tmp/test_dir/test.txt"')
 
# APIコール2(同一セッション): ファイル確認
run_command('/bin/bash -c "cat /tmp/test_dir/test.txt"')
# → [stdout] file content

ドキュメントでは「各コマンドは新しいbashプロセス」かつ「Stateless between commands」と記載されているが、同じドキュメントに「Commands execute within the same container, filesystem, and environment as agent sessions」ともある。今回の検証では/tmp/パスでファイルの永続化を確認でき、少なくとも同一セッション内ではファイルシステムが共有されている。 &&チェーンを使わなくても中間成果物をファイル経由で受け渡せる。

4. セッション間のmicroVM隔離

一方、異なるセッションIDでは完全にファイルシステムが隔離されることを確認した。

new_session = str(uuid.uuid4()) + "-" + str(uuid.uuid4())[:8]
run_command(
    '/bin/bash -c "cat /tmp/test_dir/test.txt 2>&1 || echo File NOT found"',
    session_id=new_session
)
# → [stdout] File NOT found

セッションごとに独立したmicroVMが割り当てられるため、他のセッション(他の利用者含む)のデータには一切アクセスできない。

5. エラーハンドリングとexit code

存在しないパスへのlsを実行した結果:

[stderr] ls: cannot access '/nonexistent_path': No such file or directory
[contentStop] Exit code: 2, Status: COMPLETED

コマンド失敗はAPIエラーではなく、contentStopイベントのexitCodeで非ゼロ値として返される。statusはCOMPLETEDのままだ。これにより、テスト失敗やビルドエラーを通常のフローの中で処理できる。

6. タイムアウト挙動

1秒のタイムアウトで5秒のsleepを実行した結果:

run_command('/bin/bash -c "echo start && sleep 5 && echo end"', timeout=1)
[stdout] start
[contentStop] Exit code: -1, Status: TIMED_OUT

タイムアウト時の挙動が明確だ。exitCode-1statusTIMED_OUTになる。タイムアウト前に出力されたstartは正しくストリーミングされた上で、タイムアウトが発生する。endは出力されない。

7. 並行実行の実証

同一セッション内で3つの2秒コマンドをconcurrent.futuresで並行実行した結果:

cmd_A: exit=0, time=3.51s, output=A_done
cmd_B: exit=0, time=3.51s, output=B_done
cmd_C: exit=0, time=3.51s, output=C_done
Total wall time: 3.51s

各コマンドの実行時間は約3.5秒(sleep 2秒 + APIオーバーヘッド)だが、3つのコマンドの合計wall timeも3.5秒で完了した。直列実行なら約10.5秒かかるところだ。APIの呼び出し自体は非ブロッキングで、同一セッションへの複数コマンドが実際に並行実行される。テストスイートの並列実行や、ビルドとlintの同時実行といったパターンが自然に実現できる。

ユースケースと設計パターン

検証結果から見えてきた実践的なパターンは以下の通りだ。

コーディングエージェントのツール実行分離 — LLM推論のInvokeAgentRuntimeと決定論的操作のInvokeAgentRuntimeCommandを分離する。テスト実行、ビルド、gitコミットをエージェントの推論ループから独立して管理できる。

ファイルベースのステップ間連携 — セッション内のファイルシステム共有を活かし、エージェントがコードを生成→ファイルに書き出し→テスト実行→結果を読み取り→次の推論ステップにフィードバック、という流れを自然に構築できる。

並行実行によるフィードバック高速化 — テスト、lint、型チェックを並行に投げることで、エージェントの1ターンあたりの待ち時間を短縮できる。ただしコード構成ではnodeやpipがプリインストールされないため、本格的な開発エージェントにはコンテナ構成でDockerfileをカスタマイズすることを推奨する。

公式ベストプラクティスへの補足

ドキュメントのベストプラクティスでは、&&チェーンでの状態エンコードやストリーミング出力の逐次処理が推奨されている。検証結果を踏まえた補足は以下の通りだ。

  • &&チェーンはシェル状態の引き継ぎにのみ必要 — ベストプラクティスではcd /workspace && export NODE_ENV=test && npm testのような&&チェーンが推奨されている。これは正しいが、環境変数やカレントディレクトリの引き継ぎが目的の場合に限る。ファイル経由の受け渡しであれば、検証で確認した通り&&チェーンなしで別APIコールから参照できる。
  • タイムアウト設定は「短めに設定して部分出力で判断」が有効 — ベストプラクティスではテストスイートに5分、git pushに30秒が例示されている。検証でタイムアウト前の部分出力がストリーミングされることを確認したため、短めのタイムアウトを設定し、出力を逐次処理して早期に失敗を検出する戦略が実際に機能する。
  • exitCodeのチェックは必須、ただし値の意味に注意 — ベストプラクティスではexitCodeの確認が推奨されている。検証で判明した具体的な値として、コマンド失敗時はコマンド固有のexit code(例: lsの失敗は2)、タイムアウト時は-1が返される。この区別をエラーハンドリングに組み込むべきだ。

まとめ

検証を通じて、ドキュメントだけでは読み取れない実挙動が明らかになった。

  • 「ステートレス」の実態はシェルプロセス状態のみ — ドキュメントには「Stateless between commands」とある一方で「same container, filesystem」でコマンドが実行されるとも書かれている。検証では/tmp/パスでファイルの永続化を確認した。引き継がれないのはシェルヒストリーや環境変数だ。
  • タイムアウトはexit code -1で明示的に区別される — コマンド失敗(非ゼロexit code + COMPLETED)とタイムアウト(exit code -1 + TIMED_OUT)が二軸で分離されており、部分出力も正しくストリーミングされる。
  • 並行実行は実測で確認済み — ドキュメントには「コマンド実行がエージェント呼び出しをブロックしない」とあるが、検証では同一セッションへの複数コマンドも並行実行されることを確認した。3コマンドで合計wall timeが1コマンド分に収まった。
  • コード構成のコンテナではnode・pip・AWS CLIが使えない — git(2.50.1)とcurl(8.17.0)はプリインストールされているが、node、pip、AWS CLIは含まれなかった。ツールチェーンが必要な場合はコンテナ構成を選択すべきだ。

共有する

田原 慎也

田原 慎也

ソリューションアーキテクト @ AWS

AWS ソリューションアーキテクトとして金融業界のお客様を中心に技術支援を行っています。クラウドアーキテクチャや AI/ML に関する学びをこのサイトで発信しています。このサイトの内容は個人の見解であり、所属企業の公式な意見や見解を代表するものではありません。

関連記事