🚀 Devin専門の解説メディア「StartDevin」を運営中!
Devinの導入・使い方・最新アップデート・活用事例を、日本語でまとめています。
👉 StartDevin をチェックする(startdevin.jp)
この記事にぜひ いいね❤️ していただけると励みになります 🙌
はじめに
手元にこんな構想図がありました。SlackやメールやステータスをトリガーにClaude Code Agent(親)が起動し、必要なら人にモバイルで承認を取り、複数のサブエージェントに仕事を割り振って、Slack返信・メール・レポート・発注まで出す。土台には業務ルール・メモリ・スキル・ガイドラインを「このタスクが来たらこのエージェントが動く」として定義しておく、という業務自動化の絵です。
図の右上には「Claude Managed Agentでできる?」と書いてありました。そこで、この絵の一つひとつを Claude Managed Agents(CMA) の機能に当てはめて、どこまでそのまま組めるのかを確かめました。結論から言うと、親子エージェント・メモリ・スキル・承認・出力はCMAにそのまま乗ります。自分で作る必要があるのは、トリガーの受信部分と承認の画面だけでした。
先に正直に言っておくと、これはエンドツーエンドで実走まで通した記録ではありません。CMAはbetaで、各機能を公式ドキュメントで確認しながら「絵をアーキテクチャに翻訳した」設計マッピングです。コードは構成を示すための例なので、実装時は最新のリファレンスに合わせてください。
先に3行でまとめると
- 親+複数サブエージェントは multiagent(coordinator+roster)、メモリは Memory Stores、スキルは Skills、業務ルールは agent の system プロンプトにそのまま対応する
- 人の承認は permission policy
always_ask。発注などの副作用ツールでセッションが止まり、user.tool_confirmationで allow/deny を返す。待ち受けはストリームを張り続けなくても、webhook で拾える - CMAが直接やってくれないのは トリガーの受信 と 承認を押す画面。放置で回すなら、セッションに金額の上限を付けておける
この記事に出てくる用語の位置づけ
| 用語 | どこの言葉 | ざっくりの意味 |
|---|---|---|
| Claude Managed Agents(CMA) | Anthropic | Anthropicがエージェントループとサンドボックスをホストするマネージド型エージェント |
| Agent / Session | CMA | Agent=再利用する設定(model/system/tools/skills)。Session=Agentを参照して動く1回の実行 |
| multiagent(coordinator) | CMA | 親エージェントが複数のサブエージェントに委譲する構成。サブはthreadで並列に動く |
| Memory Stores | CMA | セッションをまたいで永続する記憶。コンテナにディレクトリとしてマウントされる |
| Skills | CMA | 必要なときに読み込まれるタスク別の指示書 |
| permission policy(always_ask) | CMA | ツール実行前に人の承認を挟む設定 |
| Vault | CMA | MCPやAPIの認証情報を安全に預ける入れ物。サンドボックスからは見えない |
| Deployment | CMA | cronで定期的にセッションを起動する仕組み |
| webhook | CMA | セッションの状態変化をあなたのエンドポイントに通知する仕組み |
| session budget | CMA | 1セッションの使用額に上限を付ける設定。超えると止まる |
| Outcome | CMA | 合格基準(rubric)を渡して、満たすまで直させる仕組み |
絵の各要素を、CMAの機能に当てはめる
まず全体像です。図の箱を、CMAがネイティブに持つもの(緑)、自分で作るつなぎ(オレンジ)、人の関与(青)に色分けして並べ直しました。
対応関係を表にするとこうです。
| 絵の要素 | CMAの機能 | 自作が要るか |
|---|---|---|
| 親エージェント | Agent(multiagent: coordinator) |
— |
| 複数のサブエージェント | roster のメンバー(各subagent=thread、並列委譲) | — |
| メモリ | Memory Stores(セッション横断で永続) | — |
| スキル | Skills(anthropic製 / 独自) | — |
| 業務ルール・ガイドライン | agentの system、roster の description で振り分け |
— |
| 人の承認(モバイル) | permission policy always_ask → user.tool_confirmation
|
通知・承認の画面 |
| Slack返信・メール・発注 | MCPサーバー+Vault(認証)、副作用は承認ゲート | — |
| レポート | セッション出力(/mnt/session/outputs/)→ Files API |
— |
| イベント(Slack/メール/電話/注文) | — | 受信して sessions.create() する層 |
| 定期トリガー | Deployment(cron) | — |
「このタスクが来たらこのエージェントが動く」という絵の下段は、CMAでは coordinator の system プロンプトと roster(各サブエージェントの name と description)で表現します。親は各メンバーの説明を読んで誰に振るかを決めるので、description は親に読ませる文章として書きます。
親と子を定義する(multiagent)
まず土台の親エージェントです。ルール(system)、スキル、外部接続(MCP)、そして委譲先の roster をここに定義します。エージェントは一度作って再利用する設定オブジェクトなので、作成はセットアップ時に一度だけです。リクエストのたびに agents.create() を呼ぶと、使われないエージェントが積み上がっていきます。
セッションを作るには環境(コンテナの設定テンプレート)のIDも要るので、これも一度だけ作って使い回します。
# セットアップ(一度だけ)。実際のフィールドは公式リファレンスに合わせる
import anthropic
client = anthropic.Anthropic()
# コンテナの設定テンプレート。エージェント間で使い回せる
env = client.beta.environments.create(
name="業務自動化",
config={"type": "cloud", "networking": {"type": "unrestricted"}},
)
env_id = env.id
# 調査系の安いサブエージェント(読み取り中心)
worker = client.beta.agents.create(
name="調査担当",
description="1件の調査を任せる。検索・読み取り・要約をして根拠つきで報告する",
model="claude-haiku-4-5",
system="与えられた問いにだけ答える。出典を添えて簡潔に報告する。",
tools=[{"type": "agent_toolset_20260401"}],
)
# 親(オーケストレーター)
lead = client.beta.agents.create(
name="業務オーケストレーター",
model="claude-opus-5",
system=(
"受け取ったイベントを仕分けし、独立した調査は調査担当に委譲する。"
"検証と最終判断、承認が要る操作は自分で行う。業務ルールに従うこと。"
),
skills=[{"type": "anthropic", "skill_id": "xlsx"}], # レポート生成など
mcp_servers=[
{"type": "url", "name": "slack", "url": "https://<slack-mcp>/mcp"},
],
tools=[
{"type": "agent_toolset_20260401"},
# mcp_servers に書いただけではツールは生えない。名前で参照して初めて使える
{"type": "mcp_toolset", "mcp_server_name": "slack"},
],
multiagent={"type": "coordinator", "agents": [worker.id, {"type": "self"}]},
)
mcp_servers と tools の mcp_toolset は対で書きます。サーバーを宣言しただけだと接続先が決まるだけで、エージェントの手元にツールは現れません。宣言したサーバーはすべて、どれかの toolset から名前で参照されている必要があります。
{"type": "self"} を roster に入れておくと、親は自分自身のコピーにも仕事を振れます。まずは調査担当と self の2枚から始めて、専門サブエージェントを増やしていくのが素直です。
委譲で見落としやすいのは、サブエージェントが親の会話履歴を持たないことです。スレッドはコンテナとファイルシステムを共有しますが、文脈は共有しません。「さっきの件を進めて」では通じないので、渡すタスクにパスや制約を全部書く必要があります。
メモリと業務ルールを持たせる
絵の「メモリ」は Memory Stores に対応します。セッションが終わっても残る記憶で、コンテナに /mnt/memory/<ストア名>/ としてマウントされ、エージェントは普通のファイル操作で読み書きします。専用のメモリツールを覚える必要はありません。
store = client.beta.memory_stores.create(
name="業務メモリ",
description="顧客ごとの文脈・過去の判断・定型手順を置く。作業前に必ず確認する。",
)
description はエージェントに渡されるので、人間向けの説明ではなくモデルに読ませる文章として書きます。セッションに付けるときは resources に入れますが、メモリストアはセッション作成時にしか付けられません。あとから resources.add() で足すことはできないので、ここだけ順番に注意が要ります。
業務ルールとガイドラインは、agentの system プロンプトに書くのが基本です。「クレームは即エスカレーション」「割引は10%まで」のような判断基準を、ここに集約しておきます。
なお、メモリストアに認証情報を書かせてはいけません。一度書かれた鍵は、以後そのストアを読むセッションすべてに再生されます。鍵の置き場所は後述のVaultです。
トリガーを受けてセッションを起こす(ここは自作)
いちばん誤解しやすいのがここです。CMAはSlackやメールや電話を自分で聞いているわけではありません。外部イベントを受け取って sessions.create() する薄い層は、自分で用意します。
# あなたのWebhookハンドラの中で
session = client.beta.sessions.create(
agent=lead.id,
environment_id=env_id,
vault_ids=[vault_id], # Slack等の認証はVaultに預ける
resources=[{"type": "memory_store", "memory_store_id": store.id}],
initial_events=[{
"type": "user.message",
"content": [{"type": "text", "text": f"Slackで次の依頼が来た: {text}"}],
}],
)
initial_events を渡すと、セッション作成と最初の指示送信が1回の呼び出しで済みます。このときセッションは idle を経由せずいきなり running で作られるので、「idle から running への遷移を待って着手を検知する」という書き方をすると永遠に待つことになります。作成レスポンスの status を見るのが正解です。
図の「電話」は、CMAに入れる前に音声からテキストへの変換が別途必要です。そこを通せば、あとは他のイベントと同じ扱いになります。
定期トリガーは Deployment で、自作不要
「毎朝9時にレポート」のような定期トリガーは、Deployment でネイティブに組めます。ここは受信層を書く必要がありません。
client.beta.deployments.create(
name="日次レポート",
agent=lead.id,
environment_id=env_id,
initial_events=[{"type": "user.message",
"content": [{"type": "text", "text": "日次レポートを作成して"}]}],
schedule={"type": "cron", "expression": "0 9 * * 1-5", "timezone": "Asia/Tokyo"},
)
作成レスポンスの schedule.upcoming_runs_at に次回以降の発火時刻が入るので、cron式の解釈がこちらの意図と合っているかをそこで確認できます。実行は負荷分散のためにずれることがあり、間隔の15%まで、最大9分遅れます。毎時実行なら9分遅れうるので、後続処理に厳しい締切を置く設計は避けたほうがいいです。
夏時間の扱いも素直ではありません。壁時計の時刻で一致を見るので、春の進む日に存在しない時刻は飛ばされ、秋の戻る日に2回ある時刻は2回発火します。午前1時から3時を避けるか、UTCで組むのが安全です。
発注の前で人に承認を取る
絵の「人がモバイルで承認」に当たるのが、permission policy の always_ask です。発注のような副作用のあるツールにこれを付けると、エージェントがそのツールを呼ぼうとした瞬間にセッションが idle になり、人の判断を待ちます。読み取り系は自動で通し、危険な操作だけ止める、という粒度で設定できます。
tools=[{
"type": "agent_toolset_20260401",
"default_config": {"enabled": True,
"permission_policy": {"type": "always_allow"}},
"configs": [
# bash だけは毎回確認する
{"name": "bash", "permission_policy": {"type": "always_ask"}},
],
}]
発注のように外部サービスを叩くツールはMCP経由になります。mcp_toolset にも同じ形の default_config と configs が通る、という報告はあるのですが、公式のAPIリファレンスには {type, mcp_server_name} の最小形しか載っていません。ここは手元で確認してから本番に入れたほうがいい箇所です。
承認を待つ側は、イベントストリームで agent.tool_use を見張り、evaluated_permission が "ask" なら人に通知して、返ってきた判断を送り返します。
for event in stream: # sessions.events.stream(...)
if event.type == "agent.tool_use" and event.evaluated_permission == "ask":
approved = notify_and_wait_on_mobile(event) # ここが自作の承認UI
client.beta.sessions.events.send(session.id, events=[{
"type": "user.tool_confirmation",
"tool_use_id": event.id,
"result": "allow" if approved else "deny",
}])
tool_use_id に渡すのは event.id(sevt_ で始まるID)で、モデルが返す toolu_ のIDではありません。ここは素直に間違えるところです。拒否するときは理由を添えられて、その文面はエージェントに渡るので、次の一手を変えさせられます。
つまり「モバイルで承認」の仕組みはCMAが持っていて、押すための画面だけ自分で作る、という切り分けです。
承認待ちは、ストリームを張り続けなくていい
ここは当初、SSEストリームを見張り続ける前提で考えていました。ただ、モバイル通知のような用途だと、承認が来るまで接続を保つのは筋が悪いです。CMAには webhook があり、session.status_idled を購読しておけば、止まったタイミングだけあなたのエンドポイントに飛んできます。
登録はConsoleから行い、受け取り側ではSDKの検証関数を通します。
@app.route("/webhook", methods=["POST"])
def webhook():
try:
event = client.beta.webhooks.unwrap( # 署名と時刻を検証する
request.get_data(as_text=True), # 生のボディを渡す
headers=dict(request.headers),
)
except Exception:
return "invalid signature", 400 # 検証に落ちたら処理しない
if event.id in seen: # 同じイベントが複数回届く
return "", 204
seen.add(event.id)
if event.data.type == "session.status_idled":
notify_approver(event.data.id) # セッションIDだけ届く
return "", 204
実装で気をつける点が3つあります。
まず、ペイロードが薄いことです。届くのはイベント種別とリソースIDだけで、なぜ止まったのかは入っていません。承認待ちなのか、ただ仕事が終わったのかを知るには、セッションのイベント一覧を引いて最新の session.status_idle の stop_reason を見る必要があります。
次に、フレームワークがJSONを再シリアライズすると署名が壊れます。request.get_json() ではなく生のボディを渡してください。
最後に、webhookは取りこぼします。配信は最大3回で、全部失敗したらそのイベントは捨てられ、落ちたことすら通知されません。公式も「durable log ではない」と明記しています。承認が来ないまま止まっているセッションを拾う、定期的な棚卸しは別に要ります。
出力:Slack返信・メール・レポート・発注
Slack返信・メール送信・発注は、MCPサーバー経由で外部サービスを叩きます。認証情報はagentには書かず、Vaultに預けてセッションに vault_ids で渡します。
ここの設計がよくできていると思ったのは、鍵がサンドボックスに入らない点です。リクエストがサンドボックスを出たあとに、Anthropic側のプロキシが認証情報を差し込みます。コンテナの中で動くコードは、エージェントが自分で書いたものも含めて、本物の鍵を読み出せません。プロンプトインジェクションを食らっても鍵は出ていかない、という作りです。
GitHubリポジトリをマウントしたときの git push も同じ経路を通ります。一方で、サンドボックスから見えるのはダミー値なので、鍵の形式をローカルで検証するクライアントは起動時に弾かれることがあります。その場合はこの仕組みの副作用を疑ってください。
レポートは、エージェントが /mnt/session/outputs/ に書き出したファイルを Files API で引き取ります。Excelやスライドは Skills に任せると体裁よく作れます。
files = client.beta.files.list(scope_id=session.id,
betas=["managed-agents-2026-04-01"])
for f in files.data:
client.beta.files.download(f.id).write_to_file(f.filename)
scope_id を使うときはベータヘッダが2つ要ります。Files API のヘッダはSDKが自動で付けますが、Managed Agents のヘッダは自分で渡す必要があります。それと、セッションが idle になってからファイルが一覧に出るまで1秒から3秒ほどの遅れがあるので、空なら数回リトライしてください。
放置で回すなら、金額の上限を付けておく
ここは当初の設計マッピングで抜けていた観点です。イベント駆動で勝手に動く自動化を本番に置くなら、1セッションがいくらまで使えるかを縛っておきたくなります。CMAにはセッション単位の予算があります。
session = client.beta.sessions.create(
agent=lead.id,
environment_id=env_id,
budget={"type": "limit",
"max_list_cost": {"amount": "2500", "currency": "USD"}}, # $25.00
)
金額は最小単位の整数文字列で、"2500" が25ドルです。小数は受け付けません。浮動小数の丸めを一切挟まないための仕様で、通貨は現状USDのみです。
挙動で大事なのは、上限に達したセッションが終了ではなく一時停止になることです。stop_reason が budget_reached の idle になり、履歴もサンドボックスも残ります。この状態では、進行中の作業を片付けるイベント(ツール承認やツール結果)だけを受け付け、新しい仕事を始める user.message は400で弾かれます。再開させたければ予算を引き上げるか外します。
ただし外すのは一方通行で、一度消した予算は二度と付け直せません。上限を変えたいだけなら、消すのではなく金額を変更します。作成時にしか付けられない点も合わせて、最初から付けておく前提で設計するのが無難です。
カウントされるのは定価ベースの金額で、モデルのトークン、ウェブ検索、セッションの稼働時間が積み上がります。契約の割引価格ではなく定価で見るので、請求額より先に上限へ当たります。判定はモデル呼び出しの直前に入るため、上限を1リクエスト分だけ超えることがあります。きっちり止まる線ではなく、新しい仕事を始めさせない線だと考えてください。
「できた」の基準を渡して、満たすまで直させる
レポート作成のような成果物タスクでは、Outcome が使えます。user.define_outcome に合格基準を渡すと、別の採点役が成果物を採点し、満たすまでエージェントに直させるループをCMA側が回します。
client.beta.sessions.events.send(session.id, events=[{
"type": "user.define_outcome",
"description": "今月の受注一覧を .xlsx で作る",
"rubric": {"type": "text", "content": RUBRIC_MD},
"max_iterations": 5, # 既定は3、最大20
}])
採点役は別のコンテキストで動くので、自分の仕事を自分で甘く採点する構図になりません。結果は span.outcome_evaluation_end の result に入り、satisfied で終了、needs_revision なら次の周回に進みます。
rubric は採点できる形で書く必要があります。「読みやすいレポート」ではループが安定せず、「price 列が数値型であること」「全SKUが1行ずつ存在すること」のように、1項目ずつ独立に判定できる粒度まで落とします。それと、user.define_outcome を送ったらエージェントはそこから動き出すので、user.message で別途けしかけてはいけません。
できること・自分で作ること(正直な線引き)
| 観点 | CMAがそのまま提供 | 自分で作る |
|---|---|---|
| 親子エージェント | multiagent | — |
| メモリ/スキル/ルール | Memory Stores / Skills / system | — |
| 人の承認の仕組み | always_ask + tool_confirmation | 通知・承認の画面 |
| 承認待ちの検知 | webhook(session.status_idled) |
取りこぼしを拾う棚卸し |
| 外部への出力・発注 | MCP+Vault、出力ファイル | MCPサーバー自体の用意(既製がなければ) |
| 定期トリガー | Deployment(cron) | — |
| 使用額の上限 | session budget | — |
| 合格基準での反復 | Outcome(rubric採点) | rubricを書く |
| イベントトリガー | — | Slack/メール/注文の受信→sessions.create()
|
| 電話 | — | 音声からテキストへの変換 |
絵のほとんどはCMAの語彙に翻訳できました。足りないのは「外の世界のイベントをCMAに橋渡しする入口」で、そこは薄いWebフックのコードです。逆に言えば、エージェントの中身(親子の分業・記憶・承認・安全な発注)という難しいところは、ほぼ作らずに済みます。
ひとつ補足しておくと、以前のバージョンのこの記事で「組織の設定で管理者が敷いた deny/ask ルールは個人の自動化では上書きできない」と書いていました。これはClaude Codeの管理設定の話で、CMAの仕様ではありません。CMAのpermission policyはエージェントとセッションに紐づくもので、組織レベルで上書き不能なルールを敷く仕組みはリファレンスに見当たりませんでした。安全側の保証としてそこに寄りかかるのは誤りなので、訂正します。
おわりに
この絵を描いた人が知りたかったのは、たぶん「これ、絵に描いた餅じゃないよね?」でした。当てはめてみた答えは、中核はほぼそのまま組める、ただしトリガーの入口と承認の画面という端は自分で作る、です。
書き直していて気づいたのは、自分が無意識にストリームを張り続ける前提で設計していたことでした。webhookと予算上限を知ってから見ると、放置して回す自動化としての現実味が一段上がります。マネージド型のエージェントは、いよいよ「業務のイベント駆動オーケストレーション」を素で書ける段階に来た、というのが今回の実感でした。
次は、この設計をVault・Memory・承認まで含めて実際に一本通し、発注の承認が本当に手元の通知に飛んでくるところまでを確かめてみるつもりです。








