はじめに
本記事は、2026年3月15日時点、AWS公式アナウンスが行われる前に執筆しています。
Document historyにも記載されていない、Developer Guideに追記された情報を頼りに作っているため、今後のブログ記事等で疑問や課題がキャッチアップされる可能性があることをご了承ください。
公式アナウンスされていました。
https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-bedrock-agentcore-runtime-ag-ui-protocol/
2025年3月13日頃、AWSマネジメントコンソールやDeveloper Guideに、Amazon Bedrock AgentCore RuntimeにAG-UIプロトコルが追加されていた。
AG-UIは、以下の特徴を持つオープンプロトコルで、ざっくり言えば「複雑なエージェントの動作を分かりやすく人間にインタフェースする仕組み」と考えられる。
AIエージェントがユーザー向けアプリケーションに接続する方法を標準化する、オープンで軽量なイベントベースのプロトコルです。シンプルさと柔軟性を重視して設計されており、AIエージェント、リアルタイムのユーザーコンテキスト、およびユーザーインターフェース間のシームレスな統合を可能にします。
様々な言語、フレームワークとのインテグレーションも用意されていて、現在、AIエージェントのインタフェースを作るにうえで注目されているオープン仕様と言える。
今回は、そのAG-UIのフレームワークとして有名なCopilotKitでフロントエンドを実装して、追加されたAmazon Bedrock AgentCore AG-UIプロトコルのランタイムに接続することを実践する。名前が紛らわしいが、MicrosoftのCopilotは関係ないようだ。
Amazon Bedrock AgentCore RuntimeのIaC(Terraform)
まず、最新のAWS Providerが無いと動かないので、バージョン指定してinitをしよう。
terraform {
required_version = "~> 1.14.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.37.0"
}
auth0 = {
source = "auth0/auth0"
version = "~> 1.32.0"
}
}
}
認証関連(Auth0)
今回はIAM認証ではなくてJWTの認証を設定する。
また、アプリを最小構成にするため、画面経由での認証は行わず、お手軽にDevice Codeで一度手動でブラウザ認証してトークンを発行する方法を採用する。
※トークンをアプリの環境変数として渡すようにする。
resource "auth0_client" "example" {
name = "Amazon Bedrock AgentCore Runtime AGUI API"
description = "API for Amazon Bedrock AgentCore Runtime AGUI authorization"
app_type = "native"
custom_login_page_on = false
is_first_party = true
is_token_endpoint_ip_header_trusted = false
oidc_conformant = true
require_proof_of_possession = false
grant_types = [
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
]
jwt_configuration {
alg = "RS256"
lifetime_in_seconds = 3600
secret_encoded = false
}
refresh_token {
leeway = 0
token_lifetime = 31557600
rotation_type = "non-rotating"
expiration_type = "non-expiring"
}
}
resource "auth0_client_credentials" "example" {
client_id = auth0_client.example.id
authentication_method = "none"
}
resource "auth0_resource_server" "example" {
name = "Amazon Bedrock AgentCore Runtime AGUI Resource Server"
identifier = "urn:${local.auth0_resource_server_id}:api"
signing_alg = "RS256"
allow_offline_access = true
token_lifetime = 3600
skip_consent_for_verifiable_first_party_clients = true
enforce_policies = true
token_dialect = "access_token"
}
いちいちコマンドを作るのは面倒なので、以下のようにTerraformのOutputに半分任せる。
output "auth0_token_command_1" {
value = <<-EOT
curl --request POST \
--url https://${var.auth0_domain}/oauth/device/code \
--header "content-type: application/x-www-form-urlencoded" \
--data-urlencode "client_id=${auth0_client.amazon_bedrock_agentcore_runtime_auth.client_id}" \
--data-urlencode "scope=openid profile email offline_access" \
--data-urlencode "audience=${auth0_resource_server.amazon_bedrock_agentcore_runtime_auth.identifier}"
EOT
}
output "auth0_token_command_2" {
value = <<-EOT
curl -s --request POST \
--url https://${var.auth0_domain}/oauth/token \
--header "content-type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
--data-urlencode "client_id=${auth0_client.amazon_bedrock_agentcore_runtime_auth.client_id}" \
--data-urlencode "device_code=" | jq .access_token
EOT
}
なお、auth0.auto.tfvarsに情報を書いておくことで、いちいちvarを入力する必要がなくなるし、環境変数を設定する必要もなくなるので作っておくと楽に運用ができる。
auth0_domain = "Auth0の自分のドメイン"
auth0_api_token = "Auth0でAPIトークンを発行して書いておく"
Amazon ECR
Amazon ECRのリポジトリは特に特筆することはない。普通に作っておこう。
resource "aws_ecr_repository" "example" {
name = local.ecr_repository_name
image_tag_mutability = "MUTABLE"
}
IAM
IAMは色々お作法があるが、
- 信頼関係ポリシーは公式ドキュメントに従ってconditionを設定
- Amazon ECRからのPullと認証情報の取得
- Amazon CloudWatch Logsのロググループ・ストリームまわりの権限
- Amazon Bedrock AgentCoreのJWTまわりの認証権限
- Amazon Bedrockのモデル実行の権限(今回はストリーミングレスポンスを使うため
bedrock:InvokeModelWithResponseStreamを付与)
あたりを意識しておけばよい。
resource "aws_iam_role" "example" {
name = local.iam_role_name
assume_role_policy = data.aws_iam_policy_document.assume.json
}
data "aws_iam_policy_document" "assume" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = [
"bedrock-agentcore.amazonaws.com",
]
}
actions = [
"sts:AssumeRole",
]
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values = [
data.aws_caller_identity.self.id,
]
}
condition {
test = "ArnLike"
variable = "AWS:SourceArn"
values = [
"arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:*",
]
}
}
}
resource "aws_iam_role_policy" "example" {
name = local.iam_policy_name
role = aws_iam_role.example.id
policy = data.aws_iam_policy_document.example.json
}
data "aws_iam_policy_document" "example" {
statement {
effect = "Allow"
actions = [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
]
resources = [
"arn:aws:ecr:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:repository/*",
]
}
statement {
effect = "Allow"
actions = [
"ecr:GetAuthorizationToken",
]
resources = [
"*",
]
}
statement {
effect = "Allow"
actions = [
"logs:DescribeLogGroups",
]
resources = [
"arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:log-group:*",
]
}
statement {
effect = "Allow"
actions = [
"logs:DescribeLogStreams",
"logs:CreateLogGroup",
]
resources = [
"arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:log-group:/aws/bedrock-agentcore/runtimes/*",
]
}
statement {
effect = "Allow"
actions = [
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = [
"arn:aws:logs:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:log-group:/aws/bedrock-agentcore/runtimes/*:log-stream:*",
]
}
statement {
effect = "Allow"
actions = [
"bedrock-agentcore:GetWorkloadAccessTokenForJWT",
]
resources = [
"arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:workload-identity-directory/default",
"arn:aws:bedrock-agentcore:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:workload-identity-directory/default/workload-identity/*",
]
}
statement {
effect = "Allow"
actions = [
"bedrock:InvokeModelWithResponseStream",
]
resources = [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:bedrock:${data.aws_region.current.region}:${data.aws_caller_identity.self.id}:*",
]
}
}
Amazon Bedrock AgentCore Runtime
追加されたserver_protocol = "AGUI"でランタイムを作成する。
認証設定のJWT関連の設定はお手軽にやるのであればauthorizer_configurationで実装するのが楽なのでここでやっておく。
allowed_audienceは、さきほどauth0_resource_serverで設定したものと合わせる必要がある。
環境変数AWS_REGIONはこの後アプリケーション側で使うので設定しておく。
resource "aws_bedrockagentcore_agent_runtime" "example" {
agent_runtime_name = local.bac_runtime_name
role_arn = aws_iam_role.example.arn
agent_runtime_artifact {
container_configuration {
container_uri = "${aws_ecr_repository.example.repository_url}:latest"
}
}
authorizer_configuration {
custom_jwt_authorizer {
discovery_url = "https://${data.auth0_tenant.amazon_bedrock_agentcore_runtime_auth.domain}/.well-known/openid-configuration"
allowed_audience = [auth0_resource_server.amazon_bedrock_agentcore_runtime_auth.identifier]
}
}
network_configuration {
network_mode = "PUBLIC"
}
protocol_configuration {
server_protocol = "AGUI"
}
request_header_configuration {
request_header_allowlist = [
"Authorization",
]
}
environment_variables = {
AWS_REGION = data.aws_region.current.region
}
}
なお、ランタイムのARNが後で必要になるため、以下のOutput定義もしておく。
output "export_bedrock_agentcore_runtime_arn" {
value = "export BEDROCK_AGENTCORE_RUNTIME_ARN=${aws_bedrockagentcore_agent_runtime.example.agent_runtime_arn}"
}
ランタイムの書き換え
とてもイケていないが、Terraform AWS Providerが対応されるまでは、CLIで設定を上書きする。
CLIはピンポイントでprotocol-configurationを修正できず、指定していない部分をクリアするように上書きしてしまうので、現行の設定に合わせて出力する必要があるため、local_fileでファイル出力したものを、--cli-input-jsonで指定することにする。
output "cli_update_agent_runtime_command" {
value = "aws bedrock-agentcore-control update-agent-runtime --region ${data.aws_region.current.region} --cli-input-json file://${local_file.update_agent_runtime_input_json.filename}"
}
resource "local_file" "update_agent_runtime_input_json" {
filename = "${path.module}/terraform_tmp/update_agent_runtime_input.json"
content = jsonencode({
agentRuntimeId = aws_bedrockagentcore_agent_runtime.example.agent_runtime_id
agentRuntimeArtifact = {
containerConfiguration = {
containerUri = "${aws_ecr_repository.example.repository_url}:latest"
}
}
roleArn = aws_iam_role.example.arn
authorizerConfiguration = {
customJWTAuthorizer = {
discoveryUrl = "https://${data.auth0_tenant.amazon_bedrock_agentcore_runtime_auth.domain}/.well-known/openid-configuration"
allowedAudience = [
auth0_resource_server.amazon_bedrock_agentcore_runtime_auth.identifier
]
}
}
networkConfiguration = {
networkMode = "PUBLIC"
}
protocolConfiguration = {
serverProtocol = "AGUI"
}
requestHeaderConfiguration = {
requestHeaderAllowlist = [
"Authorization"
]
}
environmentVariables = {
AWS_REGION = data.aws_region.current.region
}
})
}
バックエンドのアプリケーション
バックエンドのアプリケーションは、基本的にDeveloper GuideのStep1に書いてある通りに作ればよい。
pyproject
特に気にするところはない。Developer Guideにある3つのライブラリを追加しておこう。
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "bedrock_agentcore_runtime_agui"
version = "0.1.0"
description = "Amazon Bedrock AgentCore Runtime AG-UI Example"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.135.1",
"uvicorn>=0.41.0",
"ag-ui-strands>=0.1.1",
]
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
[tool.ruff]
line-length = 120
[tool.ruff.lint]
extend-select = ["I"]
[tool.ty.rules]
invalid-method-override = "ignore"
ソースコード
ソースは以下の1つだけでよい。
ポイントとなるのは、ヘルスチェックの書き方がHTTPとAG-UIで異なる。
- HTTP
{
"status": "<status_value>"
}
- AG-UI
{
"status": "Healthy"
}
以下のように /ping の応答を実装しよう。
import os
import uvicorn
from ag_ui.core import RunAgentInput
from ag_ui.encoder import EventEncoder
from ag_ui_strands import StrandsAgent
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, StreamingResponse
from strands import Agent
from strands.models.bedrock import BedrockModel
# Create a simple Strands agent
model = BedrockModel(
model_id="jp.anthropic.claude-sonnet-4-6",
region_name=os.environ.get("AWS_REGION", "ap-northeast-1"),
)
agent = Agent(
model=model,
system_prompt="日本語でお喋りしましょう。",
)
agui_agent = StrandsAgent(
agent=agent,
name="agui_example",
description="Example chatbot for AG-UI",
)
app = FastAPI()
@app.post("/invocations")
async def invocations(input_data: dict, request: Request):
"""Main AGUI endpoint that returns event streams."""
accept_header = request.headers.get("accept", "")
encoder = EventEncoder(accept=accept_header)
async def event_generator():
run_input = RunAgentInput(**input_data)
async for event in agui_agent.run(run_input):
yield encoder.encode(event)
return StreamingResponse(event_generator(), media_type=encoder.get_content_type())
@app.get("/ping")
async def ping():
return JSONResponse({"status": "Healthy"})
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8080)
Dockerfile
上記で作ったアプリケーションを、Dockerfileで構築する。
FROM ghcr.io/astral-sh/uv:python3.12-trixie-slim
WORKDIR /app
ENV UV_LINK_MODE=copy
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen ${INSTALL_GROUPS} --no-editable --no-install-project
COPY src ./src
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen ${INSTALL_GROUPS} --no-editable
RUN useradd -m -u 1000 bedrock_agentcore
USER bedrock_agentcore
ENV DOCKER_CONTAINER=1
ENV VIRTUAL_ENV=/app/.venv
ENV PATH="/app/.venv/bin:$PATH"
ENV OTEL_PYTHON_CONFIGURATOR=
EXPOSE 8080
CMD ["python", "src/agent.py"]
デプロイ時の注意点
Amazon Bedrock AgentCore RuntimeのTerraformのコードの中に、Amazon ECRへの参照がある。
空のリポジトリへのアクセスはエラーになるため、ECRまで作ったらコンテナイメージをPushしておこう。
Terraformで自動でやるなら以下で可能だ。
Terraformのaws_ecr_authorization_tokenは以前はデータソースだったが、トークンがtfstateに保存されてしまう(セキュリティ上良くない上に、一時情報をtfstateに入れるのは良くない)ため、ephemeralを使うようにしよう。
ephemeral "aws_ecr_authorization_token" "token" {}
resource "terraform_data" "image_push" {
provisioner "local-exec" {
command = <<-EOF
cd ../back;
docker login -u AWS -p ${ephemeral.aws_ecr_authorization_token.token.password} ${ephemeral.aws_ecr_authorization_token.token.proxy_endpoint}
docker buildx build --platform linux/arm64 --push -t ${aws_ecr_repository.example.repository_url}:latest .
EOF
}
}
フロントエンドのアプリケーション
フロントエンドのアプリケーションは、React + CopilotKit + Viteで作成する。
まずは、
$ npm create vite@latest front_copilotkit -- --template react-ts
でソースのテンプレートを持ってこよう。
CopilotKitは、AG-UIのプロトコルに更に一部情報のやり取りを拡張していて、/api をベースに以下の流れでエンドポイントを実行する。
※チャットインタフェースの場合。他のケースについては未検証。
/api/info (GET) ※エージェントの一覧を返却
↓
/api/agent/[エージェント名]/connect (POST)
↓
/api/agent/[エージェント名]/run (POST)
↓
/api/agent/[エージェント名]/stop/[runId] (POST)
この内、Amazon Bedrock AgentCore RuntimeのAG-UIプロトコルでの責務はrunの部分になるようだ。
※ここは明確ではないが、そもそもAmazon Bedrock AgetCore Runtimeの /invocations エンドポイントはGETメソッドに対応していないし、Developer Guideのサンプルについても、connectの処理のハンドリングをしていない。
なので、今回は、run以外はViteのMiddlewareでプロキシ機能を実装して折り返しを行い、runのみをAmazon Bedrock AgetCore Runtimeのエンドポイントに向けるようにする。
画面の実装
以下のように実装していく。画面が寂しければ、CSSはコーディングエージェントにでも作ってもらおう。
今回、できるだけシンプルな画面にしたかったので、CopilotChatのmodalHeaderTitle, welcomeMessageText, chatInputPlaceholder, chatDisclaimerText属性で調整している。
なお、useSingleEndpointは、Trueにすると上記フローを全部同じエンドポイントに送るようになるため、すべての処理をAmazon Bedrock AgentCore Runtime側で実装することが可能になるが、固定値を返却するのにいちいちオーバーヘッドがかかるのも微妙なので、FalseにしてMiddlewareを実装することにした。
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import "@copilotkit/react-ui/v2/styles.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
import { CopilotChat, CopilotKit } from "@copilotkit/react-core/v2";
export default function App() {
return (
<div className="app-shell">
<header className="app-header">
<h1>Amazon Bedrock AgentCore AG-UI Runtime + Copilotkit</h1>
</header>
<main className="chat-container">
<CopilotKit
runtimeUrl="/api"
useSingleEndpoint={false}
agent="bedrock-agentcore"
>
<CopilotChat
agentId="bedrock-agentcore"
labels={{
modalHeaderTitle: "AgentCore Chat",
welcomeMessageText: "",
chatInputPlaceholder: "",
chatDisclaimerText: "",
}}
/>
</CopilotKit>
</main>
</div>
);
}
Middlewareの実装
Middlewareはvite.config.tsで実装する。
上記の通り、runのエンドポイントが指定されたときのみ、Amazon Bedrock AgentCore Runtimeのエンドポイント(upstreamUrl)に転送する。
CopilotKitの仕様なのか、なぜかconnectが複数回実行されて一部の接続がAbortされるので、try~catchのcatch部分で無視するようにしている。ここは、正しいハンドリングを知っておきたいところ……。
import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
function sendJson(
res: import("node:http").ServerResponse,
statusCode: number,
body: unknown
) {
res.statusCode = statusCode;
res.setHeader("content-type", "application/json; charset=utf-8");
res.end(JSON.stringify(body));
}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
const bearerToken = env.BEARER_TOKEN;
if (!bearerToken) {
throw new Error("BEARER_TOKEN is not set");
}
const runtimeArn = env.BEDROCK_AGENTCORE_RUNTIME_ARN;
if (!runtimeArn) {
throw new Error("BEDROCK_AGENTCORE_RUNTIME_ARN is not set");
}
const upstreamUrl = `https://bedrock-agentcore.ap-northeast-1.amazonaws.com/runtimes/${encodeURIComponent(runtimeArn)}/invocations?qualifier=DEFAULT`;
return {
plugins: [
react(),
{
name: "bedrock-agentcore-dev-middleware",
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
try {
const url = new URL(req.url ?? "/", "http://localhost");
const pathname = url.pathname;
const method = req.method ?? "GET";
if (!pathname.startsWith("/api/")) {
next();
return;
}
if (req.url === "/api/info") {
if (method !== "GET") {
sendJson(res, 405, { error: "Method Not Allowed" });
return;
}
sendJson(res, 200, {
agents: {
"bedrock-agentcore": {
name: "bedrock-agentcore",
description: "Bedrock AgentCore Runtime AG-UI",
},
}
});
return;
}
if (req.url === "/api/agent/bedrock-agentcore/connect") {
if (method !== "POST") {
sendJson(res, 405, { error: "Method Not Allowed" });
return;
}
res.statusCode = 200;
res.setHeader("content-type", "text/event-stream; charset=utf-8");
res.end()
return;
}
if (req.url === "/api/agent/bedrock-agentcore/run") {
if (method !== "POST") {
sendJson(res, 405, { error: "Method Not Allowed" });
return;
}
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const body = Buffer.concat(chunks);
const upstreamResponse = await fetch(upstreamUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${bearerToken}`,
"Content-Type": req.headers["content-type"] || "application/json",
Accept: req.headers["accept"] || "application/json, text/event-stream",
},
body,
});
if (!upstreamResponse.ok) {
const text = await upstreamResponse.text();
res.statusCode = upstreamResponse.status;
res.setHeader("content-type", "application/json");
res.end(
JSON.stringify({
error: "Upstream error",
status: upstreamResponse.status,
body: text,
}),
);
return;
}
res.statusCode = upstreamResponse.status;
upstreamResponse.headers.forEach((value, key) => {
if (key.toLowerCase() === "content-length") return;
if (key.toLowerCase() === "content-encoding") return;
res.setHeader(key, value);
});
if (!upstreamResponse.body) {
res.end();
return;
}
const reader = upstreamResponse.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(Buffer.from(value));
}
res.end();
return;
}
if (req.url?.startsWith("/api/agent/bedrock-agentcore/stop/")) {
sendJson(res, 200, { ok: "true" });
return;
}
sendJson(res, 404, { error: "Not Found" });
return;
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (msg.includes("aborted") || msg.includes("abort")) {
if (!res.writableEnded) res.end();
return;
}
console.warn("[middleware]", msg);
if (!res.headersSent) {
sendJson(res, 500, { error: "Proxy error", message: msg });
} else if (!res.writableEnded) {
res.end();
}
}
});
},
},
],
};
});
いざ、動かす!
環境変数の設定
BEARERトークン
以下の手順でトークンを取得して環境変数に設定する
-
terraform output auth0_token_command_1で出力されたcurlのコマンドを実行し、ブラウザでverification_uri_completeを開いて認証する -
terraform output auth0_token_command_1のcurlの結果で出力されたdevice_codeを、terraform output auth0_token_command_2で出力されたコマンドラインのdevice_codeに設定して実行する -
terraform output auth0_token_command_2のcurlの結果で出力されたコードを、export BEARER_TOKEN=[トークン]で設定する
Amazon Bedrock AgentCore RuntimeのARN
以下の手順でARNを取得して環境変数に設定する
-
terraform output export_bedrock_agentcore_runtime_arnで出力されたコマンドを実行する
実行
npm run devでローカルサーバが起動するので、表示されたURLにアクセスして、チャットウィンドウに書き込んでみよう。
しっかり応答が返ってきた!これで、AG-UIのサーバレスな実行環境が動かせるようになった!
今回のチャットウィンドウ程度だと、実はHTTPのままでも同じような結果になる。
AG-UIの真価はもっと他にあるようだが、それを引き出すアプリケーションが手元にないので、今後の記事で紹介していこうと思う。
