はじめに
前回の記事では、Strands harness で Security Hub CSPM の検出結果をトリアージするエージェントを作りました。
ただ、このエージェントはローカルPC(Mac) で動かしていただけです。
実際に使うなら、AWS 上で動かしたくなります。
そこで今回は、Strands harness で作ったエージェントを Amazon Bedrock AgentCore Runtime(以下、AgentCore Runtime)にデプロイし、harness の機能が AWS 上でどう動くのかを 1 つずつ確かめてみました。
やってみると、ローカルでは問題なく動いていたのに、デプロイすると動かない場面がありました。
この記事では、その落とし穴と対処も含めて、ハンズオンしてみます。
:::note info 本記事の内容は執筆時点(strands-harness 0.2.0 / AgentCore CLI 0.31.1)の情報です。 :::
この記事でわかること
- AgentCore CLI で、Strands harness のエージェントをデプロイする手順
- デプロイ先の AgentCore Runtime で起きた落とし穴と、その対処
- ターミナルがない環境で、ツール実行の承認をどう返すか
- セッションを止めると、エージェントは会話を忘れるのか(セッションストレージの有無で比較)
この記事で扱わないこと
Strands harness には、ツール結果のオフロード、サブエージェントへの委譲、長期メモリなどの機能もありますが、今回は試していません。
今回は「デプロイすると何が変わるか」に絞ってます。
全体像
今回の構成です。
| 登場人物 | 役割 |
|---|---|
| ローカルPC | AgentCore CLI でデプロイし、Python スクリプトでエージェントを呼び出す |
| AgentCore Runtime | エージェントを動かす実行環境。セッションごとに専用の microVM が割り当てられる |
| Amazon Bedrock | エージェントが使うモデル(Claude Opus 5) |
| Amazon CloudWatch | ログとトレースの出力先 |
ローカルPCからは、次の 2 種類の方法で AgentCore Runtime にアクセスします。
| 方法 | API | 用途 |
|---|---|---|
| ① エージェントを呼び出す | InvokeAgentRuntime |
依頼を送る。承認の返事もこれで送る |
| ② セッションを直接操作する |
InvokeAgentRuntimeCommand / StopRuntimeSession
|
microVM の中でコマンドを実行する(エージェントを通さない)、セッションを止める |
②の InvokeAgentRuntimeCommand を使うと、エージェント(LLM)を通さずに microVM の中を直接のぞけます。
harness が裏で作ったファイルを確認するのに何度も使いました。
Strands harness と「AgentCore の harness」は別物
名前が似ていて紛らわしいので、先に整理しておきます。
| Strands harness | AgentCore の harness | |
|---|---|---|
| 提供元 | Strands Agents(OSS) | Amazon Bedrock AgentCore |
| 作り方 | Python / TypeScript のコードで create_harness() を呼ぶ |
設定ファイル(harness.json)で定義する |
| 動かす場所 | どこでも(ローカル、AWS、他クラウド) | AgentCore |
今回使うのは Strands harness です。
後述しますが、AgentCore CLI でプロジェクトを作るときに「Harness」を選ぶと AgentCore の harness になってしまうので注意してください。
事前準備
次のものを用意します。
| 必要なもの | 確認コマンド | 備考 |
|---|---|---|
| Node.js 20 以上 | node --version |
AgentCore CLI が npm パッケージのため |
| Python 3.10 以上 | python3 --version |
エージェントのコード用 |
| uv | uv --version |
依存パッケージの追加に使う |
| AWS の認証情報 | aws sts get-caller-identity |
AgentCore と CDK のデプロイ権限が必要 |
AgentCore CLI をインストールします。
npm install -g @aws/agentcoreagentcore --version
バージョン番号が表示されれば OK です。
デプロイ先のリージョンは東京にします。
export AWS_REGION=ap-northeast-1
1. プロジェクトを作る
agentcore create
作業用のフォルダで、プロジェクトを作成します。
agentcore create --project-name HarnessHandsOn --name HarnessAgent \
--language Python --framework Strands --model-provider Bedrock \
--memory none --build CodeZip
cd HarnessHandsOn
| オプション | 指定した値 | 意味 |
|---|---|---|
--framework |
Strands |
Strands Agents のエージェントを作る |
--model-provider |
Bedrock |
モデルは Amazon Bedrock を使う |
--memory |
none |
AgentCore Memory は使わない |
--build |
CodeZip |
コードを zip にしてデプロイする(Docker 不要) |
オプションを付けずに agentcore create を実行すると、対話形式のウィザードになります。
その場合、最初の質問で 「Harness」ではなく「Agent」 を選んでください。
「Harness」を選ぶと、Strands harness ではなく AgentCore の harness(設定ファイルで定義するもの)のプロジェクトになります。
次のような構成のプロジェクトができます。
HarnessHandsOn/
├── agentcore/
│ ├── agentcore.json ← プロジェクトの設定(あとでセッションストレージを追加する)
│ └── aws-targets.json ← デプロイ先のアカウント・リージョン
└── app/
└── HarnessAgent/
├── main.py ← エージェントのコード(これを置き換える)
└── pyproject.toml
Strands harness を追加する
生成されたプロジェクトには Strands Agents の SDK は入っていますが、Strands harness は入っていません。
uv add で追加します。
cd app/HarnessAgentuv add strands-harnesscd ../..
main.py を置き換える
生成された main.py は、Strands Agents の Agent() でエージェントを作るサンプルになっています。
これを、create_harness() でエージェントを作るコードに置き換えます。
元のファイルは比較用に残しておきます。
cp app/HarnessAgent/main.py app/HarnessAgent/main.py.orig
置き換え後の main.py の全体です。この後、ブロックごとに解説します。
"""Strands harness を AgentCore Runtime で動かすエントリポイント
agentcore create で「Agent → Strands Agents」を選んで生成された
app/<エージェント名>/main.py を、このファイルで置き換えて使う。
呼び出し方(payload の "prompt")
- 普通の依頼 : "〇〇してください"
- 承認待ちへの返事 : "/approve <interrupt_id> yes" または "/approve <interrupt_id> no"
"""
import json
import os
from collections import OrderedDict
from pathlib import Path
import boto3
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from botocore.config import Config
from strands.vended_interventions.hitl import HumanInTheLoop
from strands_harness import create_harness
app = BedrockAgentCoreApp()
# ------------------------------------------------------------
# 作業フォルダと保存先
# AgentCore ではコードが /var/task に展開され、そこは読み取り専用。
# そのまま動かすと、harness が会話を保存する .agent フォルダを作れずに失敗する。
# そこで、書き込める場所を選んで作業フォルダごと移動する。
# 1. セッションストレージ(/mnt/workspace)があればそこ … 止めて再開しても残る
# 2. カレントディレクトリに書き込めればそこ … ローカルの agentcore dev
# 3. どちらもだめなら /tmp/workspace … 書けるが、セッション終了で消える
# ------------------------------------------------------------
SESSION_STORAGE = Path(os.environ.get("WORKSPACE_DIR", "/mnt/workspace"))
if SESSION_STORAGE.is_dir():
WORK_DIR = SESSION_STORAGE
elif os.access(".", os.W_OK):
WORK_DIR = Path.cwd()
else:
WORK_DIR = Path("/tmp/workspace")
WORK_DIR.mkdir(parents=True, exist_ok=True)
# shell / write ツールが相対パスで書くファイルも、ここに置かれるようにする
os.chdir(WORK_DIR)
STATE_DIR = WORK_DIR / ".agent"
MODEL_ID = os.environ.get("MODEL_ID", "bedrock/global.anthropic.claude-opus-5")
REGION = os.environ.get("AWS_REGION", "ap-northeast-1")
# エージェントはセッション ID ごとに 1 回だけ作り、使い回す。
# (生成コードと同じく、最大 128 セッションまで保持し、古いものから捨てる)
_agents: OrderedDict = OrderedDict()
def get_agent(session_id: str):
if session_id in _agents:
_agents.move_to_end(session_id)
return _agents[session_id]
if len(_agents) >= 128:
_agents.popitem(last=False)
agent = create_harness(
model=MODEL_ID,
# 同じ ID で作り直すと、保存済みの会話が復元される
session={"id": session_id, "dir": str(STATE_DIR / "sessions")},
# 長期メモリは今回は使わない。
# セッションストレージはセッションごとに分かれているため、
# 「セッションをまたいで覚える」長期メモリの置き場所には向かない。
memory=False,
# ask を指定しない → ターミナルで y/n を聞かず、「承認待ち」を返して処理を終える
interventions=HumanInTheLoop(
allowed_tools=["read", "todo_write", "retrieve_offloaded_content"],
classifier=True, # それ以外のツールは、組み込みの LLM 審査でリスクを判定する
),
)
# 前回の記事でハマった Bedrock の読み取りタイムアウト(既定 120 秒)を延ばす
agent.model.client = boto3.Session().client(
"bedrock-runtime",
region_name=REGION,
config=Config(
read_timeout=600,
retries={"max_attempts": 5, "mode": "adaptive"},
user_agent_extra="strands-agents",
),
)
_agents[session_id] = agent
return agent
def to_response(result) -> dict:
"""エージェントの結果を、呼び出し元に返す形に整える"""
if result.stop_reason == "interrupt":
return {
"status": "approval_required",
"interrupts": [
{"id": i.id, "reason": json.loads(json.dumps(i.reason, ensure_ascii=False, default=str))}
for i in result.interrupts
],
"how_to_answer": "同じセッション ID で '/approve <id> yes' または '/approve <id> no' を送ってください",
}
return {"status": "completed", "work_dir": str(WORK_DIR), "message": str(result)}
# async にしておくと、エージェントの処理中もヘルスチェック(/ping)に応答できる。
# 同期関数で処理をふさぐと、15 分でアイドル扱いされて止められることがある。
@app.entrypoint
async def invoke(payload, context):
session_id = getattr(context, "session_id", None) or "local-session"
agent = get_agent(session_id)
prompt = payload.get("prompt", "").strip()
# 承認待ちへの返事
if prompt.startswith("/approve"):
parts = prompt.split()
if len(parts) != 3:
return {"status": "error", "message": "形式: /approve <interrupt_id> yes|no"}
_, interrupt_id, answer = parts
result = await agent.invoke_async(
[{"interruptResponse": {"interruptId": interrupt_id, "response": answer}}]
)
return to_response(result)
# 普通の依頼
if not prompt:
return {"status": "error", "message": "prompt が空です"}
result = await agent.invoke_async(prompt)
return to_response(result)
if __name__ == "__main__":
app.run()
① 作業フォルダと保存先
SESSION_STORAGE = Path(os.environ.get("WORKSPACE_DIR", "/mnt/workspace"))
if SESSION_STORAGE.is_dir():
WORK_DIR = SESSION_STORAGE
elif os.access(".", os.W_OK):
WORK_DIR = Path.cwd()
else:
WORK_DIR = Path("/tmp/workspace")
エージェントがファイルを読み書きする場所と、harness が会話を保存する場所を決めています。
| 優先順 | 場所 | 使われる場面 | セッションを止めると |
|---|---|---|---|
| 1 | /mnt/workspace |
セッションストレージを設定したとき | 残る |
| 2 | カレントディレクトリ | ローカルの agentcore dev
|
(ローカルなので残る) |
| 3 | /tmp/workspace |
デプロイ先で、セッションストレージがないとき | 消える |
このブロックは最初は入れておらず、デプロイしてみてハマったので追加したものです(後述)。
② create_harness でエージェントを作る
agent = create_harness(
model=MODEL_ID,
session={"id": session_id, "dir": str(STATE_DIR / "sessions")},
memory=False,
interventions=HumanInTheLoop(
allowed_tools=["read", "todo_write", "retrieve_offloaded_content"],
classifier=True,
),
)
| 引数 | 内容 |
|---|---|
model |
使うモデル。Bedrock のグローバル推論プロファイル経由で Claude Opus 5 を呼ぶ |
session |
会話の保存先。同じ id でエージェントを作り直すと、保存済みの会話が復元される
|
memory |
長期メモリ。今回は使わないので False
|
interventions |
ツール呼び出しの審査(後述) |
session の id には、AgentCore Runtime のセッション ID をそのまま渡しています。
これで「AgentCore のセッション」と「harness の会話」が 1 対 1 で対応します。
長期メモリを無効にしているのは、今回の検証の対象外だからです。
加えて、この後で使うセッションストレージはセッションごとに分かれているため、「セッションをまたいで覚える」長期メモリの置き場所には向きません。
③ 承認の仕組み(HumanInTheLoop)
前回の記事では、HumanInTheLoop に ask="stdio" を指定して、ターミナルで y/n を聞いていました。
しかし、AgentCore Runtime 上で動くエージェントには、答えるためのターミナルがありません。
そこで今回は ask を指定していません。
ask を省略すると、承認が必要なツール呼び出しが来たとき、エージェントは処理をいったん終えて「承認待ち」を返します。
| ローカル(前回) | AgentCore(今回) | |
|---|---|---|
ask |
"stdio" |
指定しない |
| 承認が必要になったら | プログラムが止まったまま y/n を待つ |
「承認待ち」を返して処理を終える |
| 人の返事の送り方 | ターミナルで y を打つ |
同じセッション ID でもう一度呼び出す |
allowed_tools に入れたツールは審査なしで実行され、それ以外(shell や write など)は classifier=True で組み込みの LLM が審査します。
④ Bedrock クライアントの差し替え
agent.model.client = boto3.Session().client(
"bedrock-runtime",
region_name=REGION,
config=Config(read_timeout=600, ...),
)
前回の記事でハマった、Bedrock の読み取りタイムアウト(既定 120 秒)を延ばす設定です。
⑤ 結果を返す形に整える(to_response)
エージェントの結果は、次の 2 種類のどちらかで返します。
status |
意味 | 一緒に返すもの |
|---|---|---|
completed |
処理が終わった | エージェントの回答、作業フォルダ |
approval_required |
承認待ちで止まった | 承認待ちの ID と理由 |
result.stop_reason が "interrupt" なら承認待ちです。
work_dir を返しているのは、エージェントがどこで動いたかを毎回確認できるようにするためです。
⑥ エントリポイント
@app.entrypoint
async def invoke(payload, context):
session_id = getattr(context, "session_id", None) or "local-session"
...
if prompt.startswith("/approve"):
...
result = await agent.invoke_async(
[{"interruptResponse": {"interruptId": interrupt_id, "response": answer}}]
)
AgentCore Runtime から呼ばれる関数です。
セッション ID は context.session_id で受け取れます。
/approve <ID> yes で始まる依頼は、承認待ちへの返事として扱い、interruptResponse の形でエージェントに渡します。
これで、止まっていた処理が再開します。
async にしているのは、生成されたコードに合わせるためです。
エージェントが処理している間も、AgentCore Runtime からのヘルスチェック(/ping)に応答できるようにしています。
2. ローカルで動かす
デプロイする前に、手元で動かして確認します。
agentcore dev
ブラウザでチャット画面(agent inspector)が開きます。

ここで、次のように依頼してみます。
Python で 1〜50 の素数を列挙するスクリプトを primes.py に書いて、実行結果も見せて
{
"status": "completed",
"message": "`primes.py` を作成し、実行して動作を確認しました。\n\n## スクリプト(`primes.py`)\n...(省略)...\n\n## 実行結果\n\n$ python3 primes.py\n1〜50 の素数 (15 個):\n2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47\n..."
}
エージェントが自分で write ツールでファイルを作り、shell ツールで実行して、結果まで確認しています。
Agent() を create_harness() に置き換えただけで、ファイル操作とシェル実行ができるエージェントになっています。
harness が作るフォルダを見てみる
別のターミナルで、プロジェクトの中を確認します。
find . -name "primes.py" -not -path "*/.venv/*"
find . -path "*/.agent/*" -not -path "*/.venv/*" | head -20
./app/HarnessAgent/primes.py
./app/HarnessAgent/.agent/sessions
./app/HarnessAgent/.agent/sessions/context
./app/HarnessAgent/.agent/sessions/context/<セッションID>/scopes/agent/default/tooluse_xxxx_0
...
./app/HarnessAgent/.agent/sessions/session
./app/HarnessAgent/.agent/sessions/session/<セッションID>/scopes/agent/default/snapshots/snapshot_latest.json
main.py で指定した .agent/sessions の下に、2 種類のフォルダができていました。
| フォルダ | 中身 |
|---|---|
session/ |
会話の保存(スナップショット)。同じセッション ID でエージェントを作り直すと、ここから復元される |
context/ |
オフロードされたツール結果。大きなツール結果は会話から外され、ここに保存される |
公式ドキュメントにも「セッションが有効な場合、オフロードしたツール結果はセッションのフォルダに保存される」と書かれており、そのとおりの構成です。
この .agent フォルダが、デプロイ先のどこにでき、いつ消えるのかが、この後の話の中心になります。
承認待ちを起こしてみる
続けて、同じチャット画面で削除を依頼してみます。
さっき作った primes.py を削除して
今度は、エージェントが処理を止めて「承認待ち」を返してきました。
{
"status": "approval_required",
"interrupts": [
{
"id": "v1:before_tool_call:tooluse_xxxx:xxxx",
"reason": "Approve \"shell\" — Deletes a source file irreversibly via rm?\n Input: {\"command\": \"cd .../app/HarnessAgent && ls -l primes.py && rm primes.py && ...\"}"
}
],
"how_to_answer": "同じセッション ID で '/approve <id> yes' または '/approve <id> no' を送ってください"
}
reason には、審査役の LLM の判断理由(ソースファイルを取り消せない形で削除する)と、実行しようとしたコマンドがそのまま入っています。
人はこれを見て、承認するかを判断できます。
ちなみに、1 回目の「スクリプトを作って実行して」では承認待ちになりませんでした。
審査は「ファイルを書いたら止める」のような機械的なルールではなく、中身を見てリスクを判断していることがわかります。
ここでは、あえて拒否してみます。
/approve v1:before_tool_call:tooluse_xxxx:xxxx no
拒否されたあと、エージェントは別の手段で承認を回避しようとはしないと明言しました。
これは harness に組み込まれたシステムプロンプトの方針どおりの動きです(中身は後述)。
プロンプトキャッシュが効いている
チャット画面の下には、トークンの使用量が表示されます。
例)7.5k in / 352 out / 6.6k cached
入力 7,500 トークンのうち、6,600 トークンがキャッシュから読まれています。
harness では、Bedrock を使う場合はプロンプトキャッシュがデフォルトで有効になっています。設定は何もしていません。
デプロイ前に掃除する
agentcore dev を Ctrl + C で止めたら、デプロイの前に、ローカルで作られたファイルを消しておきます。
rm -rf app/HarnessAgent/.agent
rm -f app/HarnessAgent/primes.py
CodeZip は app/HarnessAgent フォルダのコードを zip にまとめてアップロードします。
消し忘れると、ローカルの会話データ(.agent)まで一緒に AWS へ送られる可能性があります。
3. デプロイする
agentcore deployを叩いてデプロイする
agentcore deploy
コードが zip にまとめられ、裏で AWS CDK が動いて AgentCore Runtime が作られます。
初回は CDK の準備(bootstrap)があるため、数分かかります。
終わったら、エージェントの ARN を確認します。
agentcore status
arn:aws:bedrock-agentcore:ap-northeast-1:<アカウントID>:runtime/HarnessHandsOn_HarnessAgent-xxxxxxxxxx
呼び出し用のスクリプト
デプロイしたエージェントは agentcore invoke でも呼び出せますが、今回は承認の返事を同じセッション ID で送り直す必要があります。
セッション ID を確実に指定できるよう、boto3 で呼び出すスクリプトを用意しました。
"""デプロイしたエージェントを、セッション ID を指定して呼び出す
使い方:
python invoke.py <セッション名> "<依頼文>"
python invoke.py <セッション名> "/approve <interrupt_id> yes"
"""
import json
import os
import sys
import boto3
AGENT_ARN = os.environ["AGENT_ARN"] # agentcore status で表示される ARN
REGION = os.environ.get("AWS_REGION", "ap-northeast-1")
def session_id_from(name: str) -> str:
# runtimeSessionId は 33 文字以上が必要なので、短い名前を伸ばして使う
return f"harness-handson-session-{name}".ljust(33, "0")
def main():
if len(sys.argv) != 3:
print(__doc__)
sys.exit(1)
name, prompt = sys.argv[1], sys.argv[2]
client = boto3.client("bedrock-agentcore", region_name=REGION)
response = client.invoke_agent_runtime(
agentRuntimeArn=AGENT_ARN,
runtimeSessionId=session_id_from(name),
payload=json.dumps({"prompt": prompt}).encode(),
qualifier="DEFAULT",
)
body = b"".join(response["response"]).decode("utf-8")
print(json.dumps(json.loads(body), ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
ポイントはセッション ID です。
InvokeAgentRuntime の runtimeSessionId は 33 文字以上が必要なので、demo01 のような短い名前から決まった ID を作るようにしています。
同じ名前を指定すれば、同じセッション(同じ microVM)に届きます。
落とし穴:デプロイ先では .agent フォルダが作れない
ローカルと同じ依頼を送ってみます。
export AGENT_ARN=<agentcore status で確認した ARN>
python invoke.py demo01 "Python で 1〜50 の素数を列挙するスクリプトを primes.py に書いて、実行結果も見せて"
botocore.errorfactory.RuntimeClientError: An error occurred (RuntimeClientError) when calling the InvokeAgentRuntime operation: Received error (500) from runtime. Please check your CloudWatch logs for more information.
ローカルでは動いていたのに、エラーになりました。
agentcore logs でログを確認してAIに食わせてみます。
agentcore logs --since 30m --level error
PermissionError: [Errno 13] Permission denied: '.agent'
strands.types.exceptions.StorageError: Failed to write 'session/harness-handson-session-demo01000/scopes/agent/default/snapshots/snapshot_latest.json'
スタックトレースを追うと、次のことがわかりました。
- AgentCore Runtime では、コードが
/var/taskに展開され、そこから実行される(ログ中の/var/task/main.py) -
/var/taskは読み取り専用 - harness は会話を保存するため、最初のメッセージの時点で
.agent/sessions/...を作りに行く - 実行中のフォルダ(
/var/task)に.agentを作れず、失敗する
エラーが出るまでの時間は 0.3 秒ほどでした。モデルを呼ぶ前、会話を保存しようとした時点で落ちています。
ローカルの agentcore dev では、実行中のフォルダがローカルPC のプロジェクトフォルダなので、問題なく書き込めていました。
デプロイして初めて表に出る落とし穴です。
対処:書き込める場所に移動する
main.py の「① 作業フォルダと保存先」のブロックが、この対処です。
SESSION_STORAGE = Path(os.environ.get("WORKSPACE_DIR", "/mnt/workspace"))
if SESSION_STORAGE.is_dir():
WORK_DIR = SESSION_STORAGE
elif os.access(".", os.W_OK):
WORK_DIR = Path.cwd()
else:
WORK_DIR = Path("/tmp/workspace")
WORK_DIR.mkdir(parents=True, exist_ok=True)
os.chdir(WORK_DIR)
STATE_DIR = WORK_DIR / ".agent"
ポイントは 2 つです。
-
会話の保存先(
STATE_DIR)を、書き込める場所にする os.chdirで作業フォルダごと移動する
2 つ目も必要です。
保存先だけ直しても、エージェントが primes.py のようなファイルを相対パスで書こうとすると、今度は /var/task への書き込みで失敗します。
作業フォルダごと移動しておけば、shell や write ツールが作るファイルも書き込める場所に置かれます。
再デプロイして、もう一度呼び出します。
agentcore deploy
python invoke.py demo01 "Python で 1〜50 の素数を列挙するスクリプトを primes.py に書いて、実行結果も見せて"
{
"status": "completed",
"work_dir": "/tmp/workspace",
"message": "`/tmp/workspace/primes.py` を作成し、実行して動作を確認しました。\n..."
}
今度は動きました。
work_dir を見ると、セッションストレージがないため /tmp/workspace で動いていることがわかります。
ログから harness の中身が見える
最初に失敗したときのログには、エラーだけでなく harness が組み立てたエージェントの中身も記録されていました。
AgentCore Runtime が OpenTelemetry で、エージェントの実行をトレースとして記録しているため見れるようです。
まず、組み込みツールの一覧です(gen_ai.agent.tools)。
shell, read, write, edit, web_fetch, programmatic_tool_caller,
subagent, todo_write, retrieve_context, strands_manage_background_task
create_harness() を呼んだだけで、これだけのツールが揃っています。
次に、harness が組み込んでいるシステムプロンプトの全文です(gen_ai.system.message)。
一部を抜粋します。
# Tools
- A denied or failed tool call is information: adjust your approach, do not retry it verbatim.
# Safety
- For actions that are hard to reverse or that reach outside the local environment, confirm first unless you have been explicitly told to proceed. Approval in one context does not carry to the next.
「拒否・失敗したツール呼び出しは情報として受け取り、やり方を変える。そのまま再試行しない」と書かれています。
2 章で削除を拒否したとき、エージェントが「別の手段で回避しない」と答えたのは、この方針どおりの動きでした。
ドキュメントを読まなくても、トレースを見れば harness が何を組み立てているかを確認できるのは便利です。
4. セッションを止めると忘れる
ここからは、harness の「セッション(会話の保存と復元)」が AgentCore Runtime 上でどう振る舞うかを確かめます。
まずは、今の状態(保存先が /tmp/workspace)で試します。
microVM の中をのぞくスクリプト
エージェントを通さずに microVM の中を確認したり、セッションを止めたりするスクリプトを用意しました。
"""AgentCore のセッション(microVM)を外から操作する
使い方:
# セッションの中でシェルコマンドを実行する(エージェント=LLM は通さない)
python session_tools.py run <セッション名> "<コマンド>"
# セッションを止める(microVM が終了する)
python session_tools.py stop <セッション名>
例:
python session_tools.py run demo01 "find /tmp/workspace -maxdepth 4 | head -30"
python session_tools.py stop demo01
セッション名は invoke.py と同じものを使う。
"""
import os
import sys
import boto3
from invoke import session_id_from # invoke.py と同じ規則でセッション ID を作る
AGENT_ARN = os.environ["AGENT_ARN"]
REGION = os.environ.get("AWS_REGION", "ap-northeast-1")
client = boto3.client("bedrock-agentcore", region_name=REGION)
def run(name: str, command: str) -> None:
response = client.invoke_agent_runtime_command(
agentRuntimeArn=AGENT_ARN,
runtimeSessionId=session_id_from(name),
body={"command": f"/bin/bash -c {command!r}", "timeout": 60},
)
for event in response["stream"]:
chunk = event.get("chunk", {})
if "contentDelta" in chunk:
delta = chunk["contentDelta"]
if delta.get("stdout"):
print(delta["stdout"], end="")
if delta.get("stderr"):
print(delta["stderr"], end="", file=sys.stderr)
if "contentStop" in chunk:
stop = chunk["contentStop"]
print(f"\n[exit code: {stop.get('exitCode')}, status: {stop.get('status')}]")
def stop(name: str) -> None:
client.stop_runtime_session(
agentRuntimeArn=AGENT_ARN,
runtimeSessionId=session_id_from(name),
qualifier="DEFAULT",
)
print(f"セッション {name} を停止しました")
if __name__ == "__main__":
if len(sys.argv) == 4 and sys.argv[1] == "run":
run(sys.argv[2], sys.argv[3])
elif len(sys.argv) == 3 and sys.argv[1] == "stop":
stop(sys.argv[2])
else:
print(__doc__)
sys.exit(1)
| サブコマンド | 使う API | 内容 |
|---|---|---|
run |
InvokeAgentRuntimeCommand |
セッションの microVM の中で、シェルコマンドを直接実行する |
stop |
StopRuntimeSession |
セッションを止める(microVM が終了する) |
run はエージェント(LLM)を通さないので、エージェントの答えではなく、microVM の中の事実を確認できます。
セッション ID の作り方は invoke.py から読み込んでいるので、同じセッション名を指定すれば同じ microVM に届きます。
① microVM の中をのぞく
3 章で primes.py を作らせたセッション(demo01)の中を見てみます。
python session_tools.py run demo01 "find /tmp/workspace -maxdepth 4 | head -30"
/tmp/workspace
/tmp/workspace/primes.py
/tmp/workspace/.agent
/tmp/workspace/.agent/sessions
/tmp/workspace/.agent/sessions/context
/tmp/workspace/.agent/sessions/context/harness-handson-session-demo01000
/tmp/workspace/.agent/sessions/session
/tmp/workspace/.agent/sessions/session/harness-handson-session-demo01000
[exit code: 0, status: COMPLETED]
ローカルのときと同じく、primes.py と harness の保存フォルダ(.agent/sessions)ができています。
フォルダ名には、AgentCore のセッション ID がそのまま使われています。
② 同じセッションで聞いてみる
python invoke.py demo01 "さっき作ったファイルの名前と、見つかった素数の個数を教えて"
{
"status": "completed",
"work_dir": "/tmp/workspace",
"message": "- ファイル名: `primes.py`(フルパス: `/tmp/workspace/primes.py`)\n- 見つかった素数の個数: **15 個**(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47)\n"
}
当然、覚えています。
③ セッションを止める
python session_tools.py stop demo01
AgentCore Runtime では、セッションごとに専用の microVM が割り当てられています。
セッションを止めると、この microVM が終了します。
④ もう一度のぞいて、聞いてみる
同じセッション名で、もう一度中をのぞきます。
python session_tools.py run demo01 "ls -la /tmp/workspace"
ls: cannot access '/tmp/workspace': No such file or directory
[exit code: 2, status: COMPLETED]
/tmp/workspace ごと、なくなっていました。
同じセッション ID でアクセスしても、届くのは新しく起動した microVM で、前の microVM のファイルは残っていません。
この状態で、エージェントにもう一度聞いてみます。
python invoke.py demo01 "さっき作ったファイルの名前と、見つかった素数の個数を教えて"
{
"status": "approval_required",
"interrupts": [
{
"id": "v1:before_tool_call:tooluse_xxxx:xxxx",
"reason": "Approve \"shell\" — Reads shell history; may expose secrets?\n Input: {\"command\": \"ls -la /tmp; echo \\\"=== any py/txt/csv/json anywhere user-writable ===\\\"; find / -xdev \\\\( -name '*prime*' -o -name '*\\u7d20\\u6570*' \\\\) ...; echo \\\"=== bash history ===\\\"; cat /tmp/workspace/.bash_history ~/.bash_history 2>/dev/null | head\"}"
}
],
...
}
答えは返ってこず、承認待ちになりました。
harness の会話の保存(.agent/sessions)も一緒に消えたため、エージェントは「さっき」が何のことかわかりません。
手がかりを探すため、ファイルシステム全体から prime や「素数」を含むファイルを検索し、さらにシェルの履歴(.bash_history)まで読もうとしました。
これを、審査役の LLM が「シェルの履歴を読む。秘密情報が漏れるおそれがある」と判定して止めています。
記憶を失ったエージェントが手当たり次第に探し回り、それを審査が止めた、という場面です。
もちろん、ここは拒否しておきます。
python invoke.py demo01 "/approve v1:before_tool_call:tooluse_xxxx:xxxx no"
ここまでのまとめ
| 止める前 | 止めた後 | |
|---|---|---|
/tmp/workspace |
primes.py と .agent がある |
フォルダごと存在しない |
| エージェントの答え | 「primes.py、15 個」 |
覚えておらず、手がかりを探し始めた |
harness のセッション機能は「同じ ID でエージェントを作り直すと、保存済みの会話を復元する」ものです。
しかし、保存先が microVM と一緒に消える場所にあると、復元する元がありません。
AgentCore Runtime のセッションは、アイドル状態が 15 分続いたときや、最大 8 時間に達したときにも終了します。
つまり、/tmp に保存している限り、少し放っておくだけで会話は消えるということです。
5. セッションストレージで「止めても覚えている」を実現する
4 章の問題は、保存先(/tmp/workspace)が microVM と一緒に消えることでした。
そこで、AgentCore Runtime のマネージドセッションストレージ(執筆時点でプレビュー)を使います。
マネージドセッションストレージとは
AgentCore Runtime のセッションごとに、microVM が終了しても消えない保存領域をマウントしてくれる機能です。
| 項目 | 内容 |
|---|---|
| マウント先 |
/mnt の直下に 1 階層(例:/mnt/workspace) |
| 分かれ方 | セッションごと(別のセッションからは見えない) |
| 残る期間 | 同じセッション ID で再開すれば残る。14 日間使わないと削除される |
| 注意点 | エージェントを更新(再デプロイ)すると、同じセッション ID でも中身はリセットされる |
| 必要な準備 | VPC や追加の IAM 権限は不要 |
同じセッション ID でアクセスすると、新しい microVM に同じ保存領域がマウントされます。
agentcore.json に 3 行足す
AgentCore CLI で作ったプロジェクトでは、agentcore/agentcore.json の runtimes に filesystemConfigurations を追加します。
"runtimes": [
{
"name": "HarnessAgent",
"build": "CodeZip",
"entrypoint": "main.py",
"codeLocation": "app/HarnessAgent/",
"runtimeVersion": "PYTHON_3_14",
"networkMode": "PUBLIC",
"protocol": "HTTP",
"filesystemConfigurations": [
{ "sessionStorage": { "mountPath": "/mnt/workspace" } }
]
}
],
マウント先は、main.py が最優先で探している /mnt/workspace に合わせています。
"protocol": "HTTP" の行末に , を足すのを忘れないようにしてください。
agentcore add agent --help を見ると、エージェントを追加するときに --session-storage-mount-path オプションでも指定できます。
今回は作成済みのエージェントに追加するので、設定ファイルを直接編集しました。
書き方は、agentcore.json の先頭にある $schema のスキーマ定義で確認できます。
設定を検証してから、デプロイします。
agentcore validate
agentcore deploy
main.py は一切変更していません。/mnt/workspace がマウントされたので、「① 作業フォルダと保存先」のブロックが自動的にそちらを選ぶようになります。
同じ手順で試す
4 章と同じ手順を、新しいセッション(demo03)で行います。
① 依頼する
python invoke.py demo03 "Python で 1〜50 の素数を列挙するスクリプトを primes.py に書いて、実行結果も見せて"
{
"status": "completed",
"work_dir": "/mnt/workspace",
"message": "`/mnt/workspace/primes.py` を作成し、実行して確認しました。\n..."
}
work_dir が /mnt/workspace に変わりました。セッションストレージが使われています。
② 中をのぞく
python session_tools.py run demo03 "find /mnt/workspace -maxdepth 4 | head -30"
/mnt/workspace
/mnt/workspace/.agent
/mnt/workspace/.agent/sessions
/mnt/workspace/.agent/sessions/context
/mnt/workspace/.agent/sessions/context/harness-handson-session-demo03000
/mnt/workspace/.agent/sessions/session
/mnt/workspace/.agent/sessions/session/harness-handson-session-demo03000
/mnt/workspace/primes.py
[exit code: 0, status: COMPLETED]
③ セッションを止める
python session_tools.py stop demo03
④ もう一度のぞいて、聞いてみる
python session_tools.py run demo03 "ls -la /mnt/workspace"
total 5
drwxr-xr-x 3 root root 0 Oct 14 14:34 .
drwxr-xr-x 1 root root 4096 Oct 14 14:35 ..
drwxr-xr-x 3 root root 0 Oct 14 14:33 .agent
-rw-r--r-- 1 root root 701 Oct 14 14:34 primes.py
[exit code: 0, status: COMPLETED]
セッションを止めた後も、primes.py と .agent が残っています。
python invoke.py demo03 "さっき作ったファイルの名前と、見つかった素数の個数を教えて"
{
"status": "completed",
"work_dir": "/mnt/workspace",
"message": "- **ファイル名**: `primes.py`(フルパス: `/mnt/workspace/primes.py`)\n- **見つかった素数の個数**: 15 個 — 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47\n"
}
今度は、探し回ることなく即答しました。
何が起きたのか
4 章(/tmp/workspace) |
5 章(/mnt/workspace) |
|
|---|---|---|
| 止めた後のファイル | フォルダごと消えた | そのまま残った |
| 止めた後の質問 | 覚えておらず、探し回った | 「primes.py、15 個」と即答 |
| 変えたもの | ― |
agentcore.json に 3 行 |
セッションを止めた後の呼び出しでは、次のことが起きています。
- 同じセッション ID なので、AgentCore Runtime が新しい microVM を起動し、同じセッションストレージを
/mnt/workspaceにマウントする -
main.pyが、同じセッション ID でcreate_harness()を呼ぶ - harness が
/mnt/workspace/.agent/sessionsから保存済みの会話を復元する
harness の「同じ ID で作り直せば会話を復元する」仕組みと、AgentCore の「同じ ID なら同じストレージをマウントする」仕組みが噛み合って、止めても覚えているエージェントになりました。
なお、執筆時点でプレビューのセッションストレージですが、東京リージョンで問題なく使えました。
やってみてわかったこと
1. 「ローカルで動く」は「デプロイ先で動く」ではない
harness はデフォルトで、実行中のフォルダの下に .agent を作って会話を保存します。
ローカルでは当たり前に書き込める場所ですが、AgentCore Runtime ではコードの展開先 /var/task が読み取り専用のため、最初のメッセージで落ちました。
agentcore dev でローカル確認ができても、ファイルを書く場所だけはデプロイ先で改めて確認が必要です。
2. 保存先の寿命が、エージェントの記憶の寿命
harness のセッション機能は「同じ ID で作り直せば、保存済みの会話を復元する」ものです。
裏を返すと、保存先が消えれば、記憶も消えます。
| 保存先 | セッションを止めると | エージェントの記憶 |
|---|---|---|
/tmp/workspace |
microVM と一緒に消える | 失う |
/mnt/workspace(セッションストレージ) |
残る | 残る |
AgentCore Runtime のセッションは、止めなくても、アイドル状態が続けば終了します。
harness をデプロイするなら、会話の保存先をどこに置くかは最初に決めておくべきだと感じました。
3. ターミナルがなくても、承認の仕組みは使える
HumanInTheLoop の ask を指定しなければ、エージェントは「承認待ち」を返して処理を終えます。
同じセッション ID で返事を送れば再開できるので、AgentCore Runtime のようなターミナルのない環境でも、人の承認を挟めました。
一方で、承認待ちのまま放置してセッションが終了した場合に、続きから再開できるかは今回試していません。
実際に運用するなら、ここは確認が必要なポイントになりそうです。
おわりに
Strands harness を AgentCore Runtime にデプロイして、機能を 1 つずつ確かめてみました。
Agent() を create_harness() に置き換えるだけで、ファイル操作・シェル実行・承認・会話の保存を備えたエージェントが AWS 上で動きます。
ただし、ローカルでは意識しなかった「ファイルを書く場所」が、デプロイ先では落とし穴にも、記憶を残す鍵にもなりました。
また、ここまで触ってみて、Strands harness は 「エージェントを手軽に使う」には非常に優れている と感じました。
一方で、AWS 上でエージェントを組み込んだサービスを作るとなると、話が変わってきます。
今回も、会話をどこに保存するか、承認をどう返すか、といった部分は結局自分で設計しました。
サービスとして作り込むほど、エージェントそのものを自分のアプリケーションの中で設計・制御したくなるはずです。
そう考えると、harness で手軽に動かすことと並行して、Strands Agents や LangChain といったエージェントフレームワークを使い、エージェントの構成要素(モデル、ツール、会話管理など)や設計パターンを理解しておく価値は大きくなってくるのではと思いました。
後片付け
検証が終わったら、作成したリソースを削除します。
HarnessHandsOn フォルダで、次のコマンドを実行します。
agentcore remove all
agentcore deploy
agentcore remove all で設定を空にし、続く agentcore deploy で、AWS 上のリソースが削除されます。
- セッションストレージのデータは、14 日間使われないと自動で削除されます。
- 初回デプロイ時に作られた CDK の準備用スタック(
CDKToolkit)は、上のコマンドでは削除されません。ほかで CDK を使っていなければ、CloudFormation のコンソールから削除できます。
参考
- Strands harness
- Strands harness - Persist sessions
- Strands harness - Gate tool calls with interventions
- Strands harness - Manage context and caching
- Strands harness - Take Strands harness to production
- Strands Agents - Human in the loop
- Get started with the AgentCore CLI
- How AgentCore Runtime works
- File system configurations for AgentCore Runtime
- Configure Amazon Bedrock AgentCore lifecycle settings
- Persist session state with filesystem configuration and execute shell commands(AWS Blog)


