Strands Agentsでマルチエージェント(Graph)のイベントをストリーミング表示する

Strands Agentsでマルチエージェント(Graph)のイベントをストリーミング表示する

2026.01.31

はじめに

Strands Agentsのマルチエージェントパターンの一つであるGraphではマルチエージェントのイベントをストリーミングできます。

https://strandsagents.com/latest/documentation/docs/user-guide/concepts/multi-agent/graph/

https://strandsagents.com/latest/documentation/docs/user-guide/concepts/streaming/#multi-agent-events

本記事のソースコードは以下にありますので適宜参照してください。

https://github.com/shuntaka9576/graph-multi-agent-streaming

動作概要

Strands AgentsのGraph Multi-Agent Patternの構成は以下の通りです。今回は観点ごとに並列でAgentが回答を作成し、その後回答を集約するシンプルなパターンです。

動作している様子は以下の通りです。

各観点のAgentが並列に文章生成を開始します。
CleanShot 2026-01-31 at 13.13.23 3_compressed

最後にまとめ用のAgentが統合します。
CleanShot 2026-01-31 at 13.13.23_tail_compressed

すべての結果は以下のような形です。
CleanShot 2026-01-31 at 13.20.11@2x

構成

UIは表現力の高いReactで書きつつ、手軽にローカルデバッグとデプロイ[1]ができて、認証やWAFも対応可能な[2]構成にしました。Bedrock従量課金分を除けば費用感も安く抑えられると思います。ただ毎回オリジンからSPAを取得するのでバンドルサイズが重いとつらくなります。[3]

architecture.drawio

Lambdaソース(SPA及びServer)に修正が入っても、hotswapありで大体15秒程度でビルドとデプロイが完了します。

$ time bun run deploy:hotswap
$ turbo build --filter=!iac && bun --filter iac deploy:hotswap
╭──────────────────────────────────────────────────────────────────────────╮
│                                                                          │
│                     Update available v2.7.6 ≫ v2.8.0                     │
│    Changelog: https://github.com/vercel/turborepo/releases/tag/v2.8.0    │
│            Run "bunx @turbo/codemod@latest update" to update             │
│                                                                          │
│          Follow @turborepo for updates: https://x.com/turborepo          │
╰──────────────────────────────────────────────────────────────────────────╯
• turbo 2.7.6
• Packages in scope: agent, server, spa
• Running build in 3 packages
• Remote caching disabled, using shared worktree cache
spa:build: cache hit, replaying logs 5c70423ebfcc300a
spa:build: $ vite build
spa:build: vite v6.4.1 building for production...
spa:build: transforming...
spa:build: ✓ 277 modules transformed.
spa:build: rendering chunks...
spa:build: computing gzip size...
spa:build: ../server/dist/spa/index.html                  0.33 kB │ gzip:   0.24 kB
spa:build: ../server/dist/spa/assets/index-DMGb393i.js  404.39 kB │ gzip: 126.93 kB
spa:build: ✓ built in 665ms
server:build: cache hit, replaying logs c06fc42816fe9c76
server:build: $ bun build ./src/index.ts --compile --target=bun-linux-arm64 --outfile ./dist/bootstrap
server:build:  [113ms]  bundle  247 modules
server:build:    [5ms] compile  ./dist/bootstrap bun-linux-aarch64-v1.3.8

 Tasks:    2 successful, 2 total
Cached:    2 cached, 2 total
  Time:    172ms >>> FULL TURBO

iac deploy:hotswap $ cdk deploy --hotswap -c stageName=dev d-acp-main --require-approval never
iac deploy:hotswap $ cdk deploy --hotswap -c stageName=dev d-acp-main --require-approval never
iac deploy:hotswap $ cdk deploy --hotswap -c stageName=dev d-acp-main --require-approval never
iac deploy:hotswap $ cdk deploy --hotswap -c stageName=dev d-acp-main --require-approval never
iac deploy:hotswap $ cdk deploy --hotswap -c stageName=dev d-acp-main --require-approval never
iac deploy:hotswap $ cdk deploy --hotswap -c stageName=dev d-acp-main --require-approval never
iac deploy:hotswap $ cdk deploy --hotswap -c stageName=dev d-acp-main --require-approval never
iac deploy:hotswap $ cdk deploy --hotswap -c stageName=dev d-acp-main --require-approval never
│ [47 lines elided]
│                 opt-in/out. You can also view the telemetry we collect by
│                 logging it to a local file, by adding
│                 `--telemetry-file=my/local/file` to any `cdk` command.
│
│       Affected versions: cli: ^2.1100.0
│
│       More information at: https://github.com/aws/aws-cdk/issues/34892
│
│
│ If you don’t want to see a notice anymore, use "cdk acknowledge <id>". For example, "cdk acknowledge 34892".
└─ Done in 14.95 s
bun run deploy:hotswap  3.34s user 0.63s system 24% cpu 15.875 total

それぞれ詳しく構成を解説します。

Lambda

  • Hono
    • /chat へのリクエストはAgentCore Runtimeへルーティング
    • 前項以外のリクエスト(*)は、SPAへルーティング
    • デプロイするソースはSPAとサーバー(Hono)の両方をバンドル
  • Bun自身のランタイムを埋め込んでシングルバイナリ化し、AL 2023 Runtimeへデプロイ
  • Lambdaに依存せず、かつローカルで動作するように Lambda Web AdapterをLayerとして利用
    • Lambda Web Adapterを利用しているので、ストリーム処理がローカルで動作し(hono/aws-lambdastreamHandle や ランタイム組み込みの awslambda.HttpResponseStream() は ローカルで起動できないため)、Layerで追加しているのでコンテナビルドは必要なく高速にデプロイされます。手軽に検証したい場合に便利です。

/chat から AgentCore Runtimeへデータを流す箇所は以下の通りです。

https://github.com/shuntaka9576/graph-multi-agent-streaming/blob/d3ee3ad3a0d358661fe3bad0c87c9621acb242c2/apps/server/src/routes/chat.ts#L75-L133

以下のようなコマンドでBunを使ってHonoのサーバーをBunランタイムをEmbedした形でシングルバイナリにコンパイルします。
https://github.com/shuntaka9576/graph-multi-agent-streaming/blob/d3ee3ad3a0d358661fe3bad0c87c9621acb242c2/apps/server/package.json#L8

SPA側(apps/spa/)のViteからサーバー側(apps/server/dist)へバンドルしたソースを出力します。できればSPAもembeddedFilesでEmbedしたかったのですが、別の機会に挑戦します😭
https://github.com/shuntaka9576/graph-multi-agent-streaming/blob/d3ee3ad3a0d358661fe3bad0c87c9621acb242c2/apps/spa/vite.config.ts#L13-L16

コンパイルされたシングルバイナリは、Lambda環境では同じディレクトリ階層にSPAがあるため参照して配信します。

https://github.com/shuntaka9576/graph-multi-agent-streaming/blob/d3ee3ad3a0d358661fe3bad0c87c9621acb242c2/apps/server/src/index.ts#L42-L63

作成された apps/server/dist をCDKでLambda Web AdapterをLayerに設定したPROVIDED_AL2023ランタイムのLambdaへデプロイします。Lambda Web Adapterを利用していますが、コンテナLambdaではないので早くデプロイできます。

https://github.com/shuntaka9576/graph-multi-agent-streaming/blob/d3ee3ad3a0d358661fe3bad0c87c9621acb242c2/iac/lib/constructs/web-app-construct.ts#L70-L99

Amazon API Gateway

Amazon API Gatewayで15分間ストリーミングすることが最近できるようになったため、REST APIで構築しています。

ただAmazon API GatewayのREST APIはカスタムドメインでない場合、basePathをつける必要があります。この制約のためアプリ側のSPAのルーティングを変える必要があります。差分としては以下でサンプルリポジトリにはすでに設定が入っています。

Amazon API GatewayのREST APIにおけるデフォルトbasePathのケア対応
diff --git a/apps/spa/src/components/Chat.tsx b/apps/spa/src/components/Chat.tsx
--- a/apps/spa/src/components/Chat.tsx
+++ b/apps/spa/src/components/Chat.tsx
@@ -56,7 +56,7 @@ export function Chat() {
     try {
       abortControllerRef.current = new AbortController();

-      const response = await fetch('/api/chat', {
+      const response = await fetch('./api/chat', {
         method: 'POST',
         headers: { 'Content-Type': 'application/json' },
         body: JSON.stringify({
diff --git a/apps/spa/src/main.tsx b/apps/spa/src/main.tsx
--- a/apps/spa/src/main.tsx
+++ b/apps/spa/src/main.tsx
@@ -4,7 +4,17 @@ import { createRoot } from 'react-dom/client';

 import { routeTree } from './routes/__root';

-const router = createRouter({ routeTree });
+// API Gateway stage prefix detection (e.g., /prod/, /dev/)
+const getBasePath = () => {
+  const path = window.location.pathname;
+  const match = path.match(/^\/[^/]+\//);
+  return match ? match[0].slice(0, -1) : '';
+};
+
+const router = createRouter({
+  routeTree,
+  basepath: getBasePath(),
+});

 declare module '@tanstack/react-router' {
   interface Register {
diff --git a/apps/spa/vite.config.ts b/apps/spa/vite.config.ts
--- a/apps/spa/vite.config.ts
+++ b/apps/spa/vite.config.ts
@@ -4,6 +4,7 @@ import { defineConfig } from 'vite';

 export default defineConfig({
   plugins: [react()],
+  base: './',
   resolve: {
     alias: {
       '@': path.resolve(import.meta.dirname, './src'),

Amazon API Gatewayのストリーミング設定部分は以下の箇所です。気づいたらL2でシンプルに書けるようになっていますね 😆

https://github.com/shuntaka9576/graph-multi-agent-streaming/blob/d3ee3ad3a0d358661fe3bad0c87c9621acb242c2/iac/lib/constructs/web-app-construct.ts#L113

AgentCore Runtime(Strands Agents)

冒頭でも説明したように今回はストリーミング部分が肝なので簡易な構成にします。

ソースで該当する部分は以下の通りです。

https://github.com/shuntaka9576/graph-multi-agent-streaming/blob/d3ee3ad3a0d358661fe3bad0c87c9621acb242c2/apps/agent/src/agent/main.py#L35-L55

ビルド&デプロイ

ソースコードには以下のような設定が入っています。

bun run deploy

動作

ストリーミングの様子は冒頭の動画をご確認いただくとして、ここではcurlで取得したレスポンスストリームの内容を確認します。レスポンスの構造を見ると、マルチエージェントシステムが並列でストリーミングしていることがわかります。

ストリーミングのイベントの種類は以下の通りです。

https://strandsagents.com/latest/documentation/docs/user-guide/concepts/streaming/#multi-agent-events

イベント名 説明 フィールド
multiagent_node_start ノードが実行を開始したとき type: "multiagent_node_start"
node_id: ノードの一意識別子
node_type: ノードの種類 ("agent", "swarm", "graph")
multiagent_node_stream エージェント/マルチエージェントからのイベントをノードコンテキスト付きで転送 type: "multiagent_node_stream"
node_id: イベントを生成したノードの識別子
event: 元のエージェントイベント(ネスト)
multiagent_node_stop ノードが実行を完了したとき type: "multiagent_node_stop"
node_id: ノードの一意識別子
node_result: 実行詳細、メトリクス、ステータスを含む完全なNodeResult
multiagent_handoff エージェント間の制御移譲(Swarm)またはバッチ遷移(Graph)時 type: "multiagent_handoff"
from_node_ids: 実行を完了したノードIDのリスト
to_node_ids: 実行を開始するノードIDのリスト
message: オプションのハンドオフメッセージ(主にSwarmで使用)
multiagent_result マルチエージェントの最終結果 type: "multiagent_result"
result: 最終的なGraphResultまたはSwarmResult

こちらをAgentCore側で判定して、変換してLambda → Amazon API Gatewayへ返却します。

https://github.com/shuntaka9576/graph-multi-agent-streaming/blob/d3ee3ad3a0d358661fe3bad0c87c9621acb242c2/apps/agent/src/agent/main.py#L66-L99

    1. 3つの専門ノードが並列起動
    • ethics - 倫理的観点
    • tech - 技術的観点
    • social - 社会的観点
    1. 各ノードがストリーミングで回答を生成
    • node_stream イベントでテキストが断片的に送られてくる
    • Unicode エスケープ(\u30e4\u30f3 -> ヤン)はJSON形式の正常な表現
    1. 最後に writer ノードが統合
    • 3つの視点を統合した最終回答を生成
    1. 完了
    • complete イベントで Status.COMPLETED
Amazon API Gatewayからのストリームレスポンス概要
$ curl -X POST "https://2kiom891rc.execute-api.ap-northeast-1.amazonaws.com/prod/api/chat" \
    -H "Content-Type: application/json" \
    -d '{"message": "ヤン・ルカンとデミス・ハサビスは、AGI実現へのアプローチの違いを説明してください", "sessionId": "550e8400-e29b-41d4-a716-446655440000"}'

# 1. 3つの専門ノードが並列起動
data: "{\"event\": \"node_start\", \"node_id\": \"ethics\"}"
data: "{\"event\": \"node_start\", \"node_id\": \"tech\"}"
data: "{\"event\": \"node_start\", \"node_id\": \"social\"}"

# 2. 各ノードが並列でストリーミング(テキストはUnicodeエスケープ形式)
data: "{\"event\": \"node_stream\", \"node_id\": \"ethics\", \"text\": \"\\u30e4\\u30f3\\u30fb\\u30eb\\u30ab\"}"
data: "{\"event\": \"node_stream\", \"node_id\": \"tech\", \"text\": \"\\u30e4\\u30f3\\u30fb\\u30eb\\u30ab\"}"
data: "{\"event\": \"node_stream\", \"node_id\": \"social\", \"text\": \"##\"}"
data: "{\"event\": \"node_stream\", \"node_id\": \"ethics\", \"text\": \"\\u30f3\\u3068\\u30c7\\u30df\\u30b9\\u30fb\\u30cf\"}"
data: "{\"event\": \"node_stream\", \"node_id\": \"tech\", \"text\": \"\\u30f3\\u3068\\u30c7\\u30df\\u30b9\\u30fb\\u30cf\"}"
data: "{\"event\": \"node_stream\", \"node_id\": \"social\", \"text\": \" \\u30e4\\u30f3\\u30fb\\u30eb\\u30ab\"}"

# ... 各ノードのストリーミングが並列で継続(約1700行省略)...

# 3. 各ノードが完了(node_stopにはcontentフィールドで生成結果全体が含まれる)
data: "{\"event\": \"node_stop\", \"node_id\": \"tech\", \"content\": \"\\u30e4\\u30f3\\u30fb\\u30eb\\u30ab\\u30f3\\u3068...(省略)\"}"
data: "{\"event\": \"node_stop\", \"node_id\": \"social\", \"content\": \"## \\u30e4\\u30f3\\u30fb\\u30eb\\u30ab\\u30f3\\u3068...(省略)\"}"
data: "{\"event\": \"node_stop\", \"node_id\": \"ethics\", \"content\": \"\\u30e4\\u30f3\\u30fb\\u30eb\\u30ab\\u30f3\\u3068...(省略)\"}"

# 4. writerノードが統合処理を開始
data: "{\"event\": \"node_start\", \"node_id\": \"writer\"}"

# 5. writerノードがストリーミング
data: "{\"event\": \"node_stream\", \"node_id\": \"writer\", \"text\": \"#\"}"
data: "{\"event\": \"node_stream\", \"node_id\": \"writer\", \"text\": \" \"}"
data: "{\"event\": \"node_stream\", \"node_id\": \"writer\", \"text\": \"\\u30e4\\u30f3\\u30fb\\u30eb\\u30ab\\u30f3\"}"
data: "{\"event\": \"node_stream\", \"node_id\": \"writer\", \"text\": \"\\u3068\\u30c7\\u30df\\u30b9\\u30fb\\u30cf\\u30b5\"}"

# ... writerノードのストリーミングが継続(約700行省略)...

# 6. writerノードが完了
data: "{\"event\": \"node_stop\", \"node_id\": \"writer\", \"content\": \"# \\u30e4\\u30f3\\u30fb\\u30eb\\u30ab\\u30f3\\u3068\\u30c7\\u30df\\u30b9...(省略)\"}"

# 7. 処理完了
data: "{\"event\": \"complete\", \"status\": \"Status.COMPLETED\"}"

さいごに

マルチエージェントで各エージェントの動きをストリーミングで閲覧できるのは、説明可能性や透明性、制御性の観点で良いアプローチといえます。またTTFT(Time to First Token)が短くなるため、UI/UXの観点でも有効です。今回のインフラ構成は非常に高速にデプロイできるため、Claude CodeとClaude in Chrome、AWS CLIを握らせてアプリ改善のAIフィードバックループを回すのにも良い構成と思います!

余談ですが、AIハッカソン的なものが社内で最近あり、Strandsのドキュメントを作業リポジトリに突っ込んでいたらClaude Codeがこの機能を見つけて勝手に実装していたと知ったといういい話があります。

脚注
  1. デプロイのしやすさの観点では、CloudFrontのプロビジョニングやCDKのs3deployment、OACでPOSTする場合のLambda@Edgeのデプロイが律速になるのを避けられます。 ↩︎

  2. Amazon API Gatewayを挟むことでWAFや認証も拡張しやすい ↩︎

  3. StreamdownのMermaidを入れたら一気に重くなったので消したりしました 🥺 ↩︎

この記事をシェアする

FacebookHatena blogX

関連記事