$ cat ./blog/claude-managed-agents-intro.md
Claude Managed Agents(beta)— サーバー管理型エージェントの実践入門
Anthropic から Managed Agents が beta 公開されました。Claude がエージェントループを自走し、Anthropic 側でコンテナを管理する新しい API 面です。Agent / Session / Environment / Container の 3 レイヤー構造、setup と runtime の分離、イベントストリームの扱いまで、自作ループから移行するときに押さえるべき点を実装込みで整理します。
- author
- r43
- date
- (2026年4月23日)
- reading
- 4 min
- commit
- 7c70b02
Anthropic から Managed Agents(beta、managed-agents-2026-04-01)が公開されました。従来の「Claude API + tool use で agent ループを自作」路線に加えて、Claude 側のオーケストレーション層がエージェントループを走らせ、Anthropic 側がセッション毎にコンテナを立てて tool 実行まで面倒を見るという新しい API 面です。本記事では、仕組み・API 構造・自作ループからの移行ポイントを、実装込みで整理します。
到達点
- Agent(永続・バージョン管理)と Session(毎回)の 2 段構造を理解する
- Agent は 1 度だけ作って ID を保存、Session はリクエスト毎というパターンを守る
- SSE イベントストリームから idle / terminated を安全に読む
- どんなときに Claude API + tool use ではなく Managed Agents を選ぶか
検証環境: 2026-04 時点、SDK は @anthropic-ai/sdk v0.90、beta header managed-agents-2026-04-01(SDK が自動付与)。
3 レイヤーの頭の中
Managed Agents は 4 種類のオブジェクトの重ね合わせです。
- Agent — 永続・バージョン管理されたエージェント定義。モデル / system prompt / tools / MCP サーバー / skills を保持。
POST /v1/agents で作り、ID を永続保存する
- Session — その Agent を使った1 回の実行。初期メッセージ + resources + vault と共に起動すると、Anthropic 側で container が立ってエージェントループが走る
- Environment — コンテナの設定テンプレ(ネットワーキング・パッケージ等)。Agent とは別管理で使いまわし
- Container — Session ごとにプロビジョニングされる sandbox。tool(bash / file / code 実行)はここで動く
┌─ Anthropic orchestration layer ─┐
Agent (config) ──▶│ エージェントループ (Claude + tools) │
└──────────┬───────────────────────┘
│ tool calls
▼
Environment (template) ──▶ Container (tool 実行場所 = /workspace)
│
Session ─┤
├── Resources (files / github_repository)
├── Vault IDs (MCP 認証)
└── Event stream (SSE)
「エージェントの配線は Anthropic 側に委ね、自分のコードはクライアントとして events を読み書きするだけ」に近い姿になります。
Setup と Runtime の分離(最重要)
Managed Agents で最大のハマりポイントが、agents.create() をリクエストごとに呼んでしまうアンチパターンです。
- Setup(1 回) —
environments.create() と agents.create() を実行して返ってきた ID を設定ファイル/env に保存する
- Runtime(毎回) — 保存された Agent ID / Environment ID を読み、
sessions.create({ agent, environment_id }) で 1 セッション起動する
新規 Agent をリクエスト経路で毎回作ると、孤児 Agent が累積し、create 分のレイテンシを毎回支払うことになります。**「Agent は 1 度、ID を保存、Session は毎回」**が絶対のルール。
Setup(TypeScript、1 度だけ)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
async function setup() {
// 1) Environment: コンテナ設定テンプレ(ネットワーキング等)
const env = await client.beta.environments.create({
name: "r43lab-coder-env",
config: { type: "cloud", networking: { type: "unrestricted" } },
});
// 2) Agent: モデル / system / tools / skills はこっちに乗せる
const agent = await client.beta.agents.create({
name: "r43lab-coder",
model: "claude-opus-4-7",
system: "You are a senior TypeScript reviewer working in the mounted repo.",
tools: [
// 組込 agent toolset(bash / read / write / edit / glob / grep / web_fetch / web_search)
{ type: "agent_toolset_20260401", default_config: { enabled: true } },
],
// 必要に応じて skills や mcp_servers を追加
// skills: [{ type: "anthropic", skill_id: "xlsx" }],
});
console.log("ENV_ID=", env.id);
console.log("AGENT_ID=", agent.id);
console.log("AGENT_VERSION=", agent.version);
// → この 3 つを .env か secrets に保存して runtime で読む
}
setup().catch(console.error);
Runtime(毎回)
Setup で保存した ID だけ読み、sessions.create() を呼びます。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const AGENT_ID = process.env.AGENT_ID!;
const ENV_ID = process.env.ENV_ID!;
async function run(userText: string) {
// Session を 1 つ起動(agent と environment を参照するだけ)
const session = await client.beta.sessions.create({
agent: AGENT_ID, // 文字列なら常に最新 version
environment_id: ENV_ID,
title: `run ${new Date().toISOString()}`,
});
// ストリームを先に開いてから user.message を送る
const stream = await client.beta.sessions.events.stream(session.id);
await client.beta.sessions.events.send(session.id, {
events: [{ type: "user.message", content: [{ type: "text", text: userText }] }],
});
// 5: status_idle は "一時停止" と "終わり" の両方で出るので stop_reason を見る
for await (const event of stream) {
if (event.type === "agent.message") {
for (const b of event.content) {
if (b.type === "text") process.stdout.write(b.text);
}
}
if (event.type === "session.status_terminated") break;
if (event.type === "session.status_idle") {
// requires_action は tool 確認待ち等。break しない。
if (event.stop_reason.type === "requires_action") continue;
break;
}
}
}
重要: 「session.status_idle だけで break する」と、tool 確認待ちの中断まで終了扱いになる事故が起きます。stop_reason.type !== "requires_action" のときだけ break するのが正しい gate です。
Agent に permission_policy: { type: "always_ask" } を指定したツールは、agent.tool_use が来ると session が idle になって user.tool_confirmation を待ちます。
for await (const event of stream) {
if (event.type === "agent.tool_use" && event.evaluated_permission === "ask") {
// UI で承認を取った想定
await client.beta.sessions.events.send(session.id, {
events: [{
type: "user.tool_confirmation",
tool_use_id: event.id, // ← event.id(toolu_ ではない)
result: "allow", // "allow" | "deny"
// deny_message: "Please read .env.example instead", // deny 時の理由を伝える
}],
});
}
}
tool_use_id は event.id(sevt_...)を渡すのがキモ。Claude API 素の toolu_... と混同しやすい点です。
| こういう時 | 選ぶ surface |
|---|
| 分類 / 要約 / 抽出 / 単発 Q&A | Claude API(単発リクエスト) |
| 自作 tools を叩くワークフロー、ループの細部を自分で制御したい | Claude API + tool use |
| bash / file / code 実行を Anthropic 側のサンドボックスで走らせたい | Managed Agents |
| セッションごとにファイルマウント / GitHub リポマウントが要る | Managed Agents |
| 永続・バージョン管理されたエージェント設定が要る | Managed Agents |
| 長時間走る multi-turn 作業を SSE で UI に流したい | Managed Agents |
ポイントは「Anthropic 側にコンテナを持たせたい / 長期セッションが要る」かどうか。自社側で tool 実行環境を持っていて制御したいなら従来の Claude API + tool use が合理的です。
知っておくべき制約
- 1P 限定: Bedrock / Vertex AI / Microsoft Foundry では Managed Agents は未提供。マルチクラウド前提のシステムでは Claude API + tool use を使う
- Archive は永続: Agent / Environment / Session / Vault / Credential いずれも archive は read-only 化の終点で、unarchive は無い。本番の agent を routine cleanup で archive しないこと
- SSE に replay なし: ストリームが切れた瞬間に配信されたイベントは失われる。再接続時は
events.list() で履歴を取ってevent.id で dedupeしてから再開する
- file mount の
file_id はコピー: sessions.create() 時に元ファイルの session-scoped コピーが作られ、session.resources[0].file_id !== uploaded.id。セッション終了後は GC されるので、元ファイルは自分で消す
落とし穴
agents.create() をリクエスト毎に呼ぶ — 最頻の間違い。setup スクリプトに隔離して ID を保存する運用へ
- Session に
model / system / tools を書く — これらはすべて Agent 側に乗る。session に入れても受け付けられない
session.status_idle だけで break する — requires_action を終了扱いにしてしまう。stop_reason.type で分岐する
- stream-after-send —
events.send() を先に呼んで events.stream() を後で開くと、最初のイベントを取り逃す。stream 先 → send 後
- archive を routine にする — Agent / Environment は永続リソース、archive は終点。本番に対しては人間承認つきでのみ
- MCP 認証をインラインに書く — Agent の
mcp_servers は {type, name, url} のみ。credential は vault に入れ、session に vault_ids で紐付ける
まとめ
Managed Agents は「エージェントループの配線と tool 実行サンドボックスを Anthropic に委ねる代わり、クライアントはイベントストリームの応対に集中できる」サーフェスです。既存の Claude API + tool use が消えるわけではないので、セッション永続とコンテナ実行が要る用途で Managed Agents を選ぶという切り分けで十分です。
次回以降、release notes タグで続報(Skills / Memory / Context Editing 等)を追いかけていきます。
参考
$ tree ./repo/
記事で登場したコードをひとつの最小リポジトリとしてまとめました。ファイルツリーから切り替えて、全体の構造をそのまま確認できます。
┌─claude-managed-agents-intro──────────files08──────────────────────●●●─┐$cat./package.jsonjson
{
"envId": "env_abc123example",
"agentId": "agent_abc123example",
"agentVersion": 1
}
# .env にコピーして値を入れる (.env は gitignore 対象)
# https://console.anthropic.com/settings/keys
ANTHROPIC_API_KEY=
# setup 後に src/setup.ts が .agent-ids.json を生成する。
# 別の方法で保存したいならここに入れても良い。
# AGENT_ID=
# ENV_ID=
# managed-agents-starter
Claude **Managed Agents**(beta `managed-agents-2026-04-01`)の最小スターター。
[記事本文](https://r43lab.com/blog/claude-managed-agents-intro) と対になる実動コードです。
## Quick start
```bash
bun install
cp .env.example .env # ANTHROPIC_API_KEY を入れる
# 1) Setup を 1 度だけ走らせて Agent / Environment を作る
bun run src/setup.ts # → .agent-ids.json に ID が書き出される
# 2) Runtime — user message を投げて stream を読む
bun run src/run.ts "Claude Code の最小構成を 300 字で教えて"
# 3) tool 確認ゲートつきのパターン
bun run src/tool-confirmation.ts "cat ./README.md"
```
## ファイル構成
- `src/setup.ts` — **1 度だけ** 実行。`environments.create` + `agents.create` を行い、返ってきた ID を `.agent-ids.json` に保存する
- `src/run.ts` — **毎回** 実行。保存された ID を読んで session を起動、イベントストリームを読み終える
- `src/tool-confirmation.ts` — `permission_policy: "always_ask"` を bash だけ効かせる例。`user.tool_confirmation` で承認する
- `.env.example` — ANTHROPIC_API_KEY のテンプレ
- `.agent-ids.example.json` — setup 後に作られる `.agent-ids.json` の形
## 守るべきパターン
- **Agent は 1 度作って ID を保存**。runtime で `agents.create()` を呼ばない
- `sessions.create({ agent: AGENT_ID, environment_id: ENV_ID })` だけで毎回のセッションが起動
- `session.status_idle` は一時停止でも発火。**終了判定は `session.status_terminated` または `idle && stop_reason.type !== "requires_action"`**
- **stream 先 → send 後**(逆にすると最初のイベントを取りこぼす)
## 依存バージョン
`package.json` は **2026-04 時点のスナップショット** です。clone 後は最新化を推奨。
```bash
bun outdated
bun update
```
## Stack
`@anthropic-ai/sdk` v0.90 · Managed Agents beta `managed-agents-2026-04-01`
{
"name": "managed-agents-starter",
"private": true,
"type": "module",
"scripts": {
"setup": "bun src/setup.ts",
"run": "bun src/run.ts",
"tool": "bun src/tool-confirmation.ts"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.90.0"
},
"devDependencies": {
"@types/bun": "^1.3.0",
"typescript": "^6.0.3"
}
}
/**
* run.ts — 保存済みの AGENT_ID / ENV_ID で Session を 1 つ起動する例
*
* CLI: bun run src/run.ts "質問文"
*
* 守るべきポイント:
* - stream 先, send 後
* - status_idle 単独で break しない(stop_reason を見る)
*/
import Anthropic from "@anthropic-ai/sdk";
import { readFileSync } from "node:fs";
const client = new Anthropic();
function loadIds() {
const raw = readFileSync(".agent-ids.json", "utf8");
const { agentId, envId } = JSON.parse(raw) as {
agentId: string;
envId: string;
};
return { agentId, envId };
}
async function main() {
const userText = process.argv.slice(2).join(" ") || "Hello, who are you?";
const { agentId, envId } = loadIds();
// 1) Session を起動
const session = await client.beta.sessions.create({
agent: agentId, // 文字列で渡すと常に latest version
environment_id: envId,
title: `run ${new Date().toISOString()}`,
});
console.log(`[session] ${session.id} (status=${session.status})`);
// 2) stream を **先に** 開いてから send する
const stream = await client.beta.sessions.events.stream(session.id);
await client.beta.sessions.events.send(session.id, {
events: [
{ type: "user.message", content: [{ type: "text", text: userText }] },
],
});
// 3) イベントを読み進める
for await (const event of stream) {
if (event.type === "agent.message") {
for (const b of event.content) {
if (b.type === "text") process.stdout.write(b.text);
}
}
if (event.type === "session.status_terminated") {
console.log("\n[terminated]");
break;
}
if (event.type === "session.status_idle") {
// requires_action → tool 確認や custom_tool_result を待っている
// end_turn / retries_exhausted → 終了と見做す
if (event.stop_reason.type === "requires_action") continue;
console.log(`\n[idle: ${event.stop_reason.type}]`);
break;
}
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
/**
* setup.ts — Managed Agents の 1 回だけの初期化
*
* 実行後、.agent-ids.json に ID が書き出される。
* 以降の run.ts / tool-confirmation.ts はこのファイルを読む。
*
* **毎回 setup を走らせると孤児 Agent が累積する**ので、リクエスト経路には
* 絶対入れないこと。CI / 初回デプロイ時のみ走る運用が正解。
*/
import Anthropic from "@anthropic-ai/sdk";
import { writeFileSync } from "node:fs";
const client = new Anthropic();
async function main() {
// 1) Environment
const env = await client.beta.environments.create({
name: `r43lab-starter-${Date.now().toString(36)}`,
config: { type: "cloud", networking: { type: "unrestricted" } },
});
// 2) Agent: model / system / tools はここに乗せる(session に書かないこと)
const agent = await client.beta.agents.create({
name: "r43lab-starter-agent",
model: "claude-opus-4-7",
system: [
"You are a concise assistant. Keep answers short (<=300 chars) unless asked otherwise.",
"When using tools, prefer `read` over `bash` for file contents.",
].join("\n"),
tools: [
{
type: "agent_toolset_20260401",
default_config: { enabled: true },
},
],
});
const out = {
envId: env.id,
agentId: agent.id,
agentVersion: agent.version,
};
writeFileSync(".agent-ids.json", JSON.stringify(out, null, 2));
console.log("wrote .agent-ids.json\n", out);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
/**
* tool-confirmation.ts — permission_policy: "always_ask" を bash だけに効かせる例
*
* この構成を使うには setup 側で tools 設定を以下のように変える必要がある:
*
* tools: [
* {
* type: "agent_toolset_20260401",
* default_config: {
* enabled: true,
* permission_policy: { type: "always_allow" },
* },
* configs: [
* { name: "bash", permission_policy: { type: "always_ask" } },
* ],
* },
* ],
*
* 本ファイルは「その Agent を使って bash 呼び出しに承認を差し挟む」側の実装。
*/
import Anthropic from "@anthropic-ai/sdk";
import { readFileSync } from "node:fs";
import { createInterface } from "node:readline/promises";
const client = new Anthropic();
function loadIds() {
const raw = readFileSync(".agent-ids.json", "utf8");
const { agentId, envId } = JSON.parse(raw) as {
agentId: string;
envId: string;
};
return { agentId, envId };
}
async function confirm(cmd: string): Promise<boolean> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
const ans = (await rl.question(`\n> allow bash \`${cmd}\`? [y/N] `)).trim().toLowerCase();
rl.close();
return ans === "y" || ans === "yes";
}
async function main() {
const userText = process.argv.slice(2).join(" ") || "List files in the working directory.";
const { agentId, envId } = loadIds();
const session = await client.beta.sessions.create({
agent: agentId,
environment_id: envId,
title: `gated ${new Date().toISOString()}`,
});
console.log(`[session] ${session.id}`);
const stream = await client.beta.sessions.events.stream(session.id);
await client.beta.sessions.events.send(session.id, {
events: [{ type: "user.message", content: [{ type: "text", text: userText }] }],
});
for await (const event of stream) {
if (event.type === "agent.message") {
for (const b of event.content) if (b.type === "text") process.stdout.write(b.text);
}
// tool gate
if (event.type === "agent.tool_use" && event.evaluated_permission === "ask") {
const input = JSON.stringify(event.input).slice(0, 200);
console.log(`\n[ask] ${event.tool_name} ${input}`);
const ok = await confirm(event.tool_name);
await client.beta.sessions.events.send(session.id, {
events: [{
type: "user.tool_confirmation",
tool_use_id: event.id, // ← event.id を渡す (toolu_ ではない)
result: ok ? "allow" : "deny",
...(ok ? {} : { deny_message: "User denied this call. Try a safer approach." }),
}],
});
}
if (event.type === "session.status_terminated") break;
if (event.type === "session.status_idle") {
if (event.stop_reason.type === "requires_action") continue;
break;
}
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"types": ["bun"],
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
},
"include": ["src/**/*"]
}