
Strands Agents + Lambda + API Gateway で、認証・ストリーミング・セッション管理付きのAPI を作ってみた
はじめに
こんにちは、スーパーマーケットが好きなコンサル部の神野です。
みなさん Strands Agents で AI エージェント作っていますか?
Strands Agents で作ったエージェントのデプロイ先といえば AgentCore が定番ですが、公式ドキュメントを見ると Lambda も選択肢として挙げられています。
API Gateway も 2025 年 11 月にレスポンスストリーミングに対応しました。であれば Lambda + API Gateway でストリーミング対応のエージェント API もサクッと作れるのでは?と気になり、試してみました!
今回のコードは GitHub で公開しています。
リポジトリの v1 ブランチに記事本文のコードがあります。clone 後は pnpm install && pnpm deploy で一括デプロイできます。
git clone -b v1 https://github.com/yuu551/lambda-api-strands.git
cd lambda-api-strands
pnpm install
pnpm deploy
前提
執筆時に動作確認したバージョンは下記です。正確な依存関係はリポジトリの lockfile を参照してください。
| 項目 | バージョン・設定 |
|---|---|
| リージョン | us-east-1 |
| Node.js | 24.16.0 |
| Python | 3.13(AgentCore Memory 版のみ) |
| パッケージ管理 | pnpm 11.11.0 |
| aws-cdk-lib | 2.261.0 |
| @strands-agents/sdk | 1.10.0 |
| hono | 4.12.31 |
| zod | 4.4.3 |
| @aws-cdk/aws-lambda-python-alpha | 2.262.0-alpha.0(AgentCore Memory 版のみ) |
| モデル | Claude Haiku 4.5 |
アーキテクチャ
この記事で最終的に作るものは、全体としてはこうなります。

前半では ① の S3 SessionManager 版だけを作ります。構成はシンプルで、登場するリソースは Lambda 1 個と API Gateway、Cognito、S3 バケットだけです。後半で AgentCore Memory を使う 2 本目の Lambda を足しますが、まずはこの最小構成から始めます。
クライアントは事前に Cognito で認証して ID トークンを取得し、Authorization ヘッダーに付けて API を呼び出します。
API Gateway の Cognito オーソライザーがトークンを検証するので、Lambda 側に認証ロジックを書く必要はありません。
Hono を挟む理由
Lambda でストリーミングするだけなら awslambda.streamifyResponse を直接使っても書けます。
Hono を挟むとストリーミング処理を簡潔に書け、ルーティングも扱えます。個人的にも好きなフレームワークなので、今回は Hono を使いました!
Hono を使った類似の実装は、弊社ブログの次の記事でも紹介しています。必要に応じてご参照ください。
実装
今回は下記のような構成で実装しました!思ったよりもシンプルになりました。
lambda-api-strands/
├── bin/
│ └── app.ts # CDK エントリーポイント
├── lib/
│ └── agent-api-stack.ts # スタック定義
├── lambda/
│ └── handler.ts # Hono + Strands Agents(エージェント本体)
├── cdk.json
├── package.json
└── tsconfig.json
プロジェクトのセットアップ
まず pnpm で依存関係をインストールします。
pnpm init
pnpm add aws-cdk-lib constructs @strands-agents/sdk @aws-sdk/client-s3 hono zod @aws-cdk/aws-lambda-python-alpha
pnpm add -D aws-cdk typescript tsx esbuild @types/node
Lambda ハンドラー
Hono + Strands Agents + S3 SessionManager を組み合わせたハンドラーを実装していきます。
import { Hono } from 'hono'
import { streamHandle } from 'hono/aws-lambda'
import { streamText } from 'hono/streaming'
import { Agent, BedrockModel, SessionManager, tool } from '@strands-agents/sdk'
import { S3Storage } from '@strands-agents/sdk/storage'
import z from 'zod'
type Bindings = {
event: {
requestContext: {
authorizer: {
claims: { sub: string; email?: string }
}
}
}
}
const SYSTEM_PROMPT = `あなたは日本語で応答する親切なアシスタントです。
現在時刻が必要な質問には get_current_time ツールを使って正確に回答してください。
回答は簡潔にまとめてください。`
const getCurrentTime = tool({
name: 'get_current_time',
description: '日本時間(JST)の現在時刻を返します。',
inputSchema: z.object({}),
callback: () =>
new Date().toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }),
})
const app = new Hono<{ Bindings: Bindings }>()
app.post('/invoke', async (c) => {
const { prompt, sessionId } = await c.req.json<{ prompt: string; sessionId?: string }>()
if (!prompt) {
return c.json({ error: 'prompt is required' }, 400)
}
// Cognito の JWT claims からユーザー固有の sub を取得
const userSub = c.env.event.requestContext.authorizer.claims.sub
const sessionManager = new SessionManager({
sessionId: sessionId ?? 'default',
// ユーザーごとに S3 プレフィックスを分離
storage: new S3Storage(process.env.SESSION_BUCKET!, {
prefix: `sessions/${userSub}/`,
}),
})
const agent = new Agent({
model: new BedrockModel({ modelId: process.env.MODEL_ID }),
systemPrompt: SYSTEM_PROMPT,
tools: [getCurrentTime],
sessionManager,
printer: false,
})
return streamText(c, async (stream) => {
for await (const chunk of agent.stream(prompt)) {
if (
chunk.type === 'modelStreamUpdateEvent' &&
chunk.event.type === 'modelContentBlockDeltaEvent' &&
chunk.event.delta.type === 'textDelta'
) {
await stream.write(chunk.event.delta.text)
}
}
})
})
export const handler = streamHandle(app)
Cognito の ID トークンには、そのユーザーを一意に表す sub という値が入っています。API Gateway のオーソライザーはトークンを検証したあと、中身をデコードして Lambda のイベントに載せてくれます。つまり Lambda 側はトークンを自分で解析する必要がなく、c.env.event.requestContext.authorizer.claims.sub を読むだけでログイン中のユーザー情報を取得できます。
この sub を S3 のキーの先頭に埋め込むことで、保存先が sessions/{sub}/{sessionId}/... に分割し、誰かが他人と同じ sessionId を指定してきても、sub が違えば別のパスになるので、会話履歴が混ざらないようにしています。
ストリーミングは Hono の streamText() の中で 処理しています。
イベントは二重構造で、外側の modelStreamUpdateEvent の中に modelContentBlockDeltaEvent が入っており、delta.type が textDelta のものがテキストの増分です。これを stream.write() に渡せばクライアントへ逐次返却します。
セッション管理は SessionManager に S3Storage を設定して実現できます。リクエストボディの sessionId で会話を識別し、同じ sessionId なら前回の会話履歴を自動復元してくれます。DynamoDB のテーブル設計も自前のセッション管理コードも不要です。簡単に実装できて良いですね。
CDK スタック
本体のスタック定義はこうしました。
import * as cdk from 'aws-cdk-lib'
import * as apigateway from 'aws-cdk-lib/aws-apigateway'
import * as cognito from 'aws-cdk-lib/aws-cognito'
import * as iam from 'aws-cdk-lib/aws-iam'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'
import * as s3 from 'aws-cdk-lib/aws-s3'
import { Construct } from 'constructs'
const MODEL_ID = 'global.anthropic.claude-haiku-4-5-20251001-v1:0'
export class AgentApiStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
// Cognito User Pool
const userPool = new cognito.UserPool(this, 'AgentUserPool', {
selfSignUpEnabled: false,
signInAliases: { email: true },
removalPolicy: cdk.RemovalPolicy.DESTROY,
})
const userPoolClient = userPool.addClient('AgentApiClient', {
authFlows: { userPassword: true },
})
// セッション保存用 S3 バケット
const sessionBucket = new s3.Bucket(this, 'SessionBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
})
// Lambda(Hono + Strands Agents TypeScript SDK)
const agentFunction = new NodejsFunction(this, 'AgentFunction', {
entry: 'lambda/handler.ts',
handler: 'handler',
runtime: lambda.Runtime.NODEJS_24_X,
architecture: lambda.Architecture.ARM_64,
timeout: cdk.Duration.minutes(5),
memorySize: 512,
environment: {
MODEL_ID,
SESSION_BUCKET: sessionBucket.bucketName,
},
bundling: {
format: OutputFormat.ESM,
banner:
"import { createRequire } from 'module'; const require = createRequire(import.meta.url);",
},
})
sessionBucket.grantReadWrite(agentFunction)
agentFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
resources: ['*'],
}),
)
// API Gateway (REST API) + Cognito オーソライザー + ストリーミング
const api = new apigateway.RestApi(this, 'AgentApi', {
restApiName: 'strands-agent-api',
deployOptions: { stageName: 'v1' },
})
const authorizer = new apigateway.CognitoUserPoolsAuthorizer(this, 'AgentApiAuthorizer', {
cognitoUserPools: [userPool],
})
const methodOptions: apigateway.MethodOptions = {
authorizer,
authorizationType: apigateway.AuthorizationType.COGNITO,
}
// POST /invoke: ストリーミングで応答
api.root.addResource('invoke').addMethod(
'POST',
new apigateway.LambdaIntegration(agentFunction, {
responseTransferMode: apigateway.ResponseTransferMode.STREAM,
timeout: cdk.Duration.minutes(5),
}),
methodOptions,
)
new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url })
new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId })
new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId })
}
}
NodejsFunction を使っているので、handler.ts を esbuild で自動バンドルしてくれます。Strands SDK も Hono も zod も全部バンドルに含まれるため、Lambda レイヤーは不要です。
ストリーミングの有効化は LambdaIntegration の responseTransferMode に STREAM を設定します。
主な設定値は下記のようになります。
| 設定項目 | 設定値 | 説明 |
|---|---|---|
| runtime | Node.js 24 | Lambda ストリーミングに対応したマネージドランタイム |
| architecture | ARM64 | |
| responseTransferMode | STREAM | API Gateway のレスポンスストリーミングを有効化 |
| timeout | 5 分 | Lambda 関数のタイムアウト。長い応答に備えて長めに設定 |
| bundling.format | ESM | NodejsFunction のバンドル形式 |
デプロイ
実装できたらデプロイしてみます。
pnpm install
npx cdk deploy
✅ AgentApiStack
✨ Deployment time: 61.86s
Outputs:
AgentApiStack.ApiEndpoint = https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/v1/
AgentApiStack.UserPoolClientId = xxxxxxxxxxxxxxxxxxxxxxxxxx
AgentApiStack.UserPoolId = us-east-1_xxxxxxxxx
デプロイできました!動作確認してみます。
動作確認
実際に試してみます!
テストユーザーの作成
Cognito にテストユーザーを作成します。ここでは、デプロイ時に出力されたユーザープール ID を使用します。
USER_POOL_ID=us-east-1_xxxxxxxxx
aws cognito-idp admin-create-user \
--user-pool-id $USER_POOL_ID \
--username test@example.com \
--message-action SUPPRESS
aws cognito-idp admin-set-user-password \
--user-pool-id $USER_POOL_ID \
--username test@example.com \
--password 'YourPassword123!' \
--permanent
認証の確認
まずトークンなしで叩いてみます。
curl -X POST "$API_ENDPOINT/invoke" \
-H "Content-Type: application/json" \
-d '{"prompt": "こんにちは"}'
{"message":"Unauthorized"}
ちゃんと 401 で弾かれていますね!
ストリーミングの確認
Cognito からトークンを取得して、ストリーミングで呼んでみます。
CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
ID_TOKEN=$(aws cognito-idp initiate-auth \
--auth-flow USER_PASSWORD_AUTH \
--client-id $CLIENT_ID \
--auth-parameters USERNAME=test@example.com,PASSWORD='YourPassword123!' \
--query 'AuthenticationResult.IdToken' \
--output text)
curl -N -X POST "$API_ENDPOINT/invoke" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"AWSの好きなサービスを3つ挙げて","sessionId":"test-1"}'
ターミナル上で文字が逐次表示されました!本当にストリーミングされているか、チャンクの到着タイミングを計測してみると下記のようになっていました。
[+ 1.51s] chunk 1: 'AWSの好きなサービスを3つ'
[+ 1.54s] chunk 2: '挙げる'
[+ 1.55s] chunk 3: 'とす'
(中略)
[+ 2.95s] chunk 63: '的なサービ'
[+ 2.98s] chunk 66: 'スです。'
--- first: +1.51s, chunks: 74, done: +3.26s
リクエストから約 1.5 秒で最初のチャンクが届き、74 個のチャンクが逐次到着していますね!
マルチターン会話の確認
S3 SessionManager が機能しているか確認します。同じ sessionId で 2 回呼んでみます。
curl -N -X POST "$API_ENDPOINT/invoke" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"こんにちは、私は神野です。好きな食べ物はカレーです。覚えておいてね。","sessionId":"blog-test-1"}'
こんにちは、神野さん!はじめまして。
好きな食べ物がカレーだということですね。カレーはおいしいですね!
ご紹介いただきありがとうございます。この情報は今後のお話の中で参考にさせていただきます。
curl -N -X POST "$API_ENDPOINT/invoke" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"私の名前と好きな食べ物を覚えてる?","sessionId":"blog-test-1"}'
はい、覚えていますよ!
あなたのお名前:神野さん
好きな食べ物:カレー
ですね。今後のご質問やお話の中で、この情報を活用させていただきます!
お、ちゃんと前の会話を踏まえて回答してくれていますね!
S3 の中身を見てみると、Cognito の sub でユーザーごとにパスが分離されていました。
sessions/94085448-7071-7023-fd3e-d4866412213a/session/blog-test-1/scopes/agent/agent/snapshots/snapshot_latest.json
sessions/{sub}/{sessionId}/... という構造になっていますね!別のユーザーが同じ sessionId で呼んでもパスが変わるため、アプリケーション上はユーザーごとに履歴が分かれます。
Lambda はステートレスですが、S3 SessionManager のおかげでマルチターン会話が実現できています。
S3 版はこれで一通り動きました!!
AgentCore Memory 版も試してみる
AgentCore Memory の SessionManager を使うと、会話からユーザーの好みや事実情報を自動抽出して、別セッションでも参照できる長期記憶も簡単に連携できます。
ただし、Strands 向けの AgentCoreMemorySessionManager は執筆時点では Python 向けに提供されています。そのため Hono(TypeScript)とは別に、FastAPI + Lambda Web Adapter の Python Lambda を追加する構成にしました。ここだけ言語が変わりますが、Lambda でストリーミングしつつ AgentCore Memory も使いたいとなると、現状は Python を起点にする形になります。
AgentCore Memory リソースの作成
AgentCore Memory は CDK から作れるので、スタックに足してしまいます。3 つの長期記憶戦略を有効化しています。
import * as agentcore from 'aws-cdk-lib/aws-bedrockagentcore'
// AgentCore Memory(Python 側の RetrievalConfig のパスと合わせる)
const memory = new agentcore.Memory(this, 'AgentMemory', {
memoryName: 'lambda_api_strands_memory',
expirationDuration: cdk.Duration.days(7),
memoryStrategies: [
agentcore.MemoryStrategy.usingUserPreference({
strategyName: 'UserPreference',
namespaces: ['/preferences/{actorId}/'],
}),
agentcore.MemoryStrategy.usingSemantic({
strategyName: 'SemanticFacts',
namespaces: ['/facts/{actorId}/'],
}),
agentcore.MemoryStrategy.usingSummarization({
strategyName: 'SessionSummary',
namespaces: ['/summaries/{actorId}/{sessionId}/'],
}),
],
})
const cfnMemory = memory.node.findChild('Memory') as agentcore.CfnMemory
cfnMemory.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY)
namespace の {actorId} はリクエスト時に自動置換されるプレースホルダーです。この後 Python 側で actor_id に Cognito の sub を渡すので、ユーザーごとに記憶が分かれます。
Python Lambda ハンドラー
FastAPI + Strands Python SDK + AgentCore Memory SessionManager のハンドラーです。
import json
import os
from datetime import datetime, timedelta, timezone
from bedrock_agentcore.memory.integrations.strands.config import (
AgentCoreMemoryConfig,
RetrievalConfig,
)
from bedrock_agentcore.memory.integrations.strands.session_manager import (
AgentCoreMemorySessionManager,
)
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel
from strands import Agent, tool
SYSTEM_PROMPT = """あなたは日本語で応答する親切なアシスタントです。
現在時刻が必要な質問には get_current_time ツールを使って正確に回答してください。
回答は簡潔にまとめてください。
"""
app = FastAPI()
@tool
def get_current_time() -> str:
"""日本時間(JST)の現在時刻を返します。"""
jst = timezone(timedelta(hours=9))
return datetime.now(jst).strftime("%Y-%m-%d %H:%M:%S")
class InvokeRequest(BaseModel):
prompt: str
session_id: str | None = None
def _get_user_sub(request: Request) -> str | None:
"""Cognito の sub を返す。取得できない場合は None(呼び出し側で 401)。"""
# LWA (response streaming mode) は x-amzn-request-context に requestContext を渡す
raw = request.headers.get("x-amzn-request-context")
if not raw:
return None
try:
ctx = json.loads(raw)
sub = ctx.get("authorizer", {}).get("claims", {}).get("sub")
return sub if isinstance(sub, str) and sub else None
except (json.JSONDecodeError, TypeError, AttributeError):
return None
@app.post("/invoke-memory")
async def invoke_memory(req: InvokeRequest, request: Request):
# actor_id は Memory のユーザー分離境界なので、共有 ID にフォールバックしない
user_sub = _get_user_sub(request)
if user_sub is None:
return JSONResponse({"message": "Unauthorized"}, status_code=401)
session_id = req.session_id or "default"
config = AgentCoreMemoryConfig(
memory_id=os.environ["MEMORY_ID"],
session_id=session_id,
actor_id=user_sub,
retrieval_config={
"/preferences/{actorId}/": RetrievalConfig(top_k=5, relevance_score=0.5),
"/facts/{actorId}/": RetrievalConfig(top_k=10, relevance_score=0.3),
"/summaries/{actorId}/{sessionId}/": RetrievalConfig(
top_k=3, relevance_score=0.5
),
},
)
session_manager = AgentCoreMemorySessionManager(
config, region_name=os.environ.get("MEMORY_REGION", "us-east-1")
)
agent = Agent(
model=os.environ["MODEL_ID"],
system_prompt=SYSTEM_PROMPT,
tools=[get_current_time],
session_manager=session_manager,
callback_handler=None,
)
async def generate():
# 途中で例外やクライアント切断が起きても確実にフラッシュする
try:
async for event in agent.stream_async(req.prompt):
if "data" in event:
yield event["data"]
finally:
session_manager.close()
return StreamingResponse(generate(), media_type="text/plain; charset=utf-8")
Hono 版と構造は似ていますが、いくつか違いがあります。
ストリーミングは agent.stream_async() を使います。TypeScript 版の agent.stream() に相当する非同期ジェネレーターで、イベントの data キーにテキストの増分が入ってきます。
ユーザー分離は Cognito の sub を AgentCoreMemoryConfig の actor_id に渡すだけです。Hono 版では自分で S3 のプレフィックスに sub を埋め込みましたが、AgentCore Memory は namespace の {actorId} を自動置換してくれます。
JWT claims の取得方法は Hono 版と違うので注意してください。
Lambda Web Adapter のストリーミングモードでは、API Gateway の requestContext が x-amzn-request-context ヘッダーに JSON として渡されます。Hono だと c.env.event でイベントを取り出せましたが、LWA 経由の FastAPI ではリクエストヘッダーから取り出します。
CDK への追加
CDK スタックに Python Lambda を追加します。@aws-cdk/aws-lambda-python-alpha の PythonFunction を使うと、requirements.txt から Docker 経由で依存関係を自動バンドルしてくれます。TypeScript 側の NodejsFunction と同じ感覚です。
import に @aws-cdk/aws-lambda-python-alpha を追加しておきます。
import * as python from '@aws-cdk/aws-lambda-python-alpha'
CDK 追加分(agent-api-stack.ts に追記)
// Python Lambda(FastAPI + Strands Python SDK + AgentCore Memory)
const lwaLayer = lambda.LayerVersion.fromLayerVersionArn(
this,
'LwaLayer',
`arn:aws:lambda:${this.region}:753240598075:layer:LambdaAdapterLayerArm64:25`,
)
const memoryFunction = new python.PythonFunction(this, 'MemoryFunction', {
entry: 'lambda/memory',
index: 'main.py',
runtime: lambda.Runtime.PYTHON_3_13,
architecture: lambda.Architecture.ARM_64,
timeout: cdk.Duration.minutes(5),
memorySize: 512,
layers: [lwaLayer],
environment: {
MODEL_ID,
MEMORY_ID: memory.memoryId,
MEMORY_REGION: this.region,
AWS_LAMBDA_EXEC_WRAPPER: '/opt/bootstrap',
AWS_LWA_INVOKE_MODE: 'response_stream',
PORT: '8000',
},
})
// PythonFunction は handler を Python モジュール形式で生成するが、
// LWA は run.sh をエントリポイントとして使うので L1 でオーバーライドする
const cfnMemoryFunction = memoryFunction.node.defaultChild as lambda.CfnFunction
cfnMemoryFunction.addPropertyOverride('Handler', 'run.sh')
memoryFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
resources: ['*'],
}),
)
memory.grantFullAccess(memoryFunction)
// POST /invoke-memory エンドポイント追加
api.root.addResource('invoke-memory').addMethod(
'POST',
new apigateway.LambdaIntegration(memoryFunction, {
responseTransferMode: apigateway.ResponseTransferMode.STREAM,
timeout: cdk.Duration.minutes(5),
}),
methodOptions,
)
環境変数の MEMORY_ID には、CDK で作った Memory のリソース ID を設定します。
PythonFunction は entry で指定したディレクトリの requirements.txt を検出し、cdk synth 時に Docker コンテナ内で pip install を実行します。ARM64 向けのネイティブホイールも Docker が解決してくれます。
AgentCore Memory 版の動作確認
デプロイ後、/invoke-memory エンドポイントを叩いてみます。
curl -N -X POST "$API_ENDPOINT/invoke-memory" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"こんにちは、私は神野です。TypeScriptが好きで、趣味はスーパーマーケット巡りです。覚えてね。","session_id":"ltm-test-1"}'
こんにちは、神野さん!よろしくお願いします。
あなたについて覚えさせていただきます:
- 名前: 神野さん
- 好きなプログラミング言語: TypeScript
- 趣味: スーパーマーケット巡り
素晴らしい趣味ですね!今後の会話でお役に立てるよう、この情報を念頭に置いておきます。
同一セッションでの記憶確認は S3 版と同じように動きます。
長期記憶の抽出が終わるのを少し待ってから、別の sessionId で呼んでみます。
curl -N -X POST "$API_ENDPOINT/invoke-memory" \
-H "Content-Type: application/json" \
-H "Authorization: $ID_TOKEN" \
-d '{"prompt":"私について何か知ってることある?","session_id":"ltm-cross-session"}'
はい、神野さんについて知っていることがあります!
- 名前:神野さん
- 好きな言語:TypeScript
- 趣味:スーパーマーケット巡り
TypeScriptは確かに素晴らしい言語ですね。型安全性があって開発効率も良いです。
また、スーパーマーケット巡りは、各地の特色ある商品や地域の食文化を発見できる素敵な趣味だと思います!
初めて使うセッション ID ですが、前の会話で伝えた情報を覚えていますね!!
AgentCore Memory が裏側で会話からユーザーの好みを自動抽出していて、下記のようなレコードが保存されていました!
{"context":"ユーザーが自己紹介の中で、TypeScriptが好きだと明示的に述べた。",
"preference":"TypeScriptが好き",
"categories":["プログラミング","技術"]}
{"context":"ユーザーが自己紹介の中で、趣味はスーパーマーケット巡りだと明示的に述べた。",
"preference":"趣味はスーパーマーケット巡り",
"categories":["趣味","ショッピング"]}
AgentCore との使い分け
Strands の Lambda デプロイガイドでは、ガイド内の Lambda 例はストリーミングを実装していないと説明されていて、必要な場合の選択肢として Fargate が挙げられています。ただ今回試したように、API Gateway のストリーミング対応と Hono を組み合わせれば、Lambda でもストリーミング API は作れます。
既存の API 基盤に乗せたい、Cognito や WAF、使用量プランなど API Gateway の機能をフルに使いたい、リクエスト課金でコストを抑えたいという場合は Lambda + API Gateway が有利な気がしました。S3 セッションを使う最小版ならわりと簡単に実装できる気がします。API Gateway と AgentCoreを組み合わせることも可能ですが、ネイティブに結合はできず、プロキシするLambdaが必要になるのでそこが少しデメリットかもなと感じました。
一方、実行時間が 15 分を超える長時間タスクや、microVM によるセッション分離が必要になったら、AgentCore を検討したほうがよいです。AgentCore CLI を使えばエージェントの雛形からデプロイまで一気に通せるので、まず動くものを作りたいケースでもとっつきやすいと思います。ケースバイケースな気がしましたが、とにかくAIエージェントを作りたいんだ!試したい!と言ったケースはAgentCore RuntimeやHarnessで試すのも良い気がします。
おまけとしてチャット UI も作ってみた
せっかくなので比較用のチャット UI も作ってみました!
フルスタックで使える方が嬉しいですもんね、チャット UI は main ブランチに入っています。

React + Vite 製の SPA を S3 + CloudFront で配信する構成です。

画面左上のエンドポイントセレクターで、同じ画面のまま S3 SessionManager 版と AgentCore Memory 版を切り替えられます。スクリーンショットは AgentCore Memory 版で「私のことを覚えていますか?」と聞いたら、過去の会話から自動抽出された関心事を答えてくれていますね!
「新しい会話」で sessionId を切り替えてから同じ質問をすると、S3 版は忘れて AgentCore Memory 版は覚えている、という本編の比較を UI 上でポチポチ試せます。
本編のハンドラーはテキストをそのまま返却しましたが、UI でツール呼び出しの過程も見せたかったので、レスポンスを SSE に変更しています。agent.stream() が返すのは SDK 内部のイベントオブジェクトなので、テキストだけ流すなら本編のままで十分です。
最終的なディレクトリ構成はこうなりました。
lambda-api-strands/
├── bin/app.ts
├── lib/
│ ├── agent-api-stack.ts # API GW + Lambda + Cognito + CloudFront
│ └── model-catalog.ts # Haiku / Sonnet の切り替え定義
├── lambda/
│ ├── handler.ts # Hono + S3 SessionManager(SSE)
│ ├── validate-request.ts
│ └── memory/main.py # FastAPI + AgentCore Memory(SSE)
├── frontend/ # React + Vite のチャット UI
│ ├── public/app-config.json # Cognito / エンドポイント / モデル定義
│ └── src/
│ ├── components/ # Header, ChatMessage, EndpointSelector ...
│ ├── hooks/useChat.ts # 送信とストリーム状態の管理
│ └── lib/sse.ts # SSE パーサー(TS 版・Python 版で共通)
└── package.json
セットアップ手順は README にまとめてあります。pnpm deploy でフロントエンドのビルドから CDK デプロイまで一気に流れるので、動かしてみたい方はそちらをどうぞ!
フィードバックがあれば、Issue で教えてください!
おわりに
Lambda でもデプロイしてみたいと思い、試してみました。AgentCore だけでなく、こういった構成を選ぶケースもありそうです。
本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございました!
補足
API Gateway レスポンスストリーミングの注意点
ストリーミング有効時はいくつか挙動が変わります。API Gateway は Lambda を InvokeWithResponseStream API で呼び出すため、Lambda 側は streamifyResponse 相当の形式で返す必要があります(Hono が内部で処理してくれる)。統合タイムアウトは上限が 15 分まで延ばせるようになり(デフォルトは 29 秒のまま)、レスポンスの最初の 10 MB は帯域制限なし、以降は 2 MB/s に制限されます。課金はレスポンスペイロード 10 MB 単位でカウントされます。
S3 SessionManager のセッション構造
セッションデータはこの構造で S3 に保存されます。JWT の sub をプレフィックスに入れているため、アプリケーション上はユーザーごとに履歴が分かれます。
sessions/<userSub>/session/<sessionId>/
└── scopes/
└── agent/
└── <agentId>/
└── snapshots/
└── snapshot_latest.json
SessionManager は invocation が完了するたびに snapshot_latest.json を上書き保存します。次回同じ sessionId でリクエストが来ると、snapshot を自動で復元して会話を継続してくれます。
削除方法
検証が終わったらスタックごと消してください。S3 バケットには autoDeleteObjects、AgentCore Memory には RemovalPolicy.DESTROY を設定しているため、いずれもスタック削除と同時に消えます。
npx cdk destroy









