【Claude Code 連載 第89回】SDK のサブエージェント(agent-sdk/subagents)

スポンサーリンク
【Claude Code 連載 第89回】SDK のサブエージェント(agent-sdk/subagents) 用語解説
【Claude Code 連載 第89回】SDK のサブエージェント(agent-sdk/subagents)
この記事は約50分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

スポンサーリンク

背景

この連載では、Claude Codeの公式ドキュメントを1ページずつ読み解いていきます。公式ドキュメントは情報が網羅されている分、「結局どの機能を、どんな場面で使えばいいのか」は自分で考える必要があり、読むのに意外と時間がかかります。そこで、私が実務で使うために読み込んだ内容を「使う場面→実例コード」の順に整理して残していくことにしました。専門家の解説というより、一次情報を読んだ記録です。推測や動作を確認していない部分には、その都度そう書きます。

公式ドキュメント:https://code.claude.com/docs/ja/agent-sdk/subagents

1. 一言でいうと

サブエージェントは、メインのエージェントが的を絞った作業を任せるために生み出す、別のエージェントです。Agent SDK では、コードの中で名前・説明・システムプロンプト・使えるツールなどを定義して渡すと、Claude が必要に応じてそのサブエージェントを呼び出します。

サブエージェントを使う理由は、作業ごとに文脈を分けられること、複数の作業を並行して進められること、専門的な指示をメインのプロンプトに混ぜずに持たせられること、使えるツールを絞れることです。第64回で「並列実行の比較」としてサブエージェント・エージェントチーム・ワークフローを並べましたが、今回はそのサブエージェントを SDK のコードから扱う回です。


2. どういう場面で役立つか

シーン1:コードレビューを観点ごとに並行して走らせる

スタイル、セキュリティ、テストの網羅率をそれぞれ担当するサブエージェントを用意し、順番にではなく同時に走らせます。独立した作業なら、全体の時間は「合計」ではなく「一番遅いもの」で決まります。

シーン2:大量のファイルを調べさせ、要約だけ受け取る

「API のエンドポイントを全部洗い出して」のように何十ものファイルを読む調査をサブエージェントに任せると、読んだファイルの中身はサブエージェントの中に留まり、メインのエージェントには要約だけが返ります。メインの会話のコンテキストが膨らみません(第70回の「6,100トークン読んで420トークン返す」の話)。

シーン3:触ってほしくない操作を担当ごとに封じる

ドキュメントを見直すサブエージェントには Read と Grep だけを渡し、ファイルを書き換えられないようにします。テストを走らせるサブエージェントにだけ Bash を渡す、という分け方もできます。

不要・向かないケース

  • 何十、何百ものエージェントを調整する大規模な作業:サブエージェントは1ターンあたり数個の委任に向いています。大規模なら後述のワークフロー(第67回)を使います。
  • エージェントどうしが対等に相談しながら進める作業:それはエージェントチーム(第66回)の領域です。
  • ファイル変更の取り消しが前提の作業:サブエージェントの編集はファイルチェックポイントに記録されません(第88回)。

3. コードと仕組みの解説

原文の fenced コードブロックは13本(対訳6組と、明示的な呼び出しのプロンプト例1本)です。どれも内容が違うので、全数を引用します。コードは原文から機械的に抜き出したもので、入れ子に由来する先頭インデントを外した以外は変えていません。

3-0. バージョン要件

機能ごとに分かれているので、表にまとめます。

挙動必要バージョン
tool_use ブロックでのツール名が "Agent" になる(それ以前は "Task")Claude Code v2.1.63 以降
サブエージェントを既定でバックグラウンドで動かす(それ以前は段階的な展開中で、同期的に動くこともあった)Claude Code v2.1.198 以降
サブエージェントの最終メッセージを、親が読む前に検査するClaude Code v2.1.210 以降
深さ・同時実行数・支出の上限(3-7)TypeScript SDK v0.3.219 以降/Python SDK v0.2.127 以降(Claude Code v2.1.219 以降を同梱)
maxTurns に達したとき、出力を「途中まで」と印を付けて返すClaude Code v2.1.246 以降
omitClaudeMdTypeScript SDK v0.3.271 以降(Python SDK には無い)
Workflow ツールTypeScript SDK v0.3.149 以降

3-1. 作り方は3通り

方法内容
プログラムで定義(推奨)query() のオプションの agents パラメーターで定義する。SDK のアプリにはこれが勧められている
ファイルで定義.claude/agents/ ディレクトリに Markdown ファイルとして置く(CLI のサブエージェントのページの範囲)
組み込みの汎用エージェント何も定義しなくても、Claude は Agent ツールで組み込みの general-purpose サブエージェントをいつでも呼べる

同じ名前なら、プログラムで定義したものがファイルで定義したものより優先されます。

組み込みの汎用エージェントについて補足です。Claude が subagent_type を指定せずに Agent ツールを呼ぶと、この general-purpose が使われます。環境変数 CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS=1 を設定するとこの既定が無くなり、subagent_type の無い呼び出しは subagent_type is required エラーで失敗します。自分で定義したサブエージェント以外を使わせたくない場合の設定です(使いどころの整理は筆者)。

3-2. プログラムで定義する

agents パラメーターでサブエージェントを定義します。Claude は Agent ツールを通じてサブエージェントを呼び出します。次の例は、読み取りだけができるコードレビュー担当と、コマンドを実行できるテスト担当の2つを作ります。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition


async def main():
    async for message in query(
        prompt="Review the authentication module for security issues",
        options=ClaudeAgentOptions(
            # Auto-approve these tools
            allowed_tools=["Read", "Grep", "Glob", "Agent"],
            agents={
                "code-reviewer": AgentDefinition(
                    # description tells Claude when to use this subagent
                    description="Expert code review specialist. Use for quality, security, and maintainability reviews.",
                    # prompt defines the subagent's behavior and expertise
                    prompt="""You are a code review specialist with expertise in security, performance, and best practices.

When reviewing code:
- Identify security vulnerabilities
- Check for performance issues
- Verify adherence to coding standards
- Suggest specific improvements

Be thorough but concise in your feedback.""",
                    # tools restricts what the subagent can do (read-only here)
                    tools=["Read", "Grep", "Glob"],
                    # model overrides the default model for this subagent
                    model="sonnet",
                ),
                "test-runner": AgentDefinition(
                    description="Runs and analyzes test suites. Use for test execution and coverage analysis.",
                    prompt="""You are a test execution specialist. Run tests and provide clear analysis of results.

Focus on:
- Running test commands
- Analyzing test output
- Identifying failing tests
- Suggesting fixes for failures""",
                    # Bash access lets this subagent run test commands
                    tools=["Bash", "Read", "Grep"],
                ),
            },
        ),
    ):
        if hasattr(message, "result"):
            print(message.result)


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Review the authentication module for security issues",
  options: {
    // Auto-approve these tools
    allowedTools: ["Read", "Grep", "Glob", "Agent"],
    agents: {
      "code-reviewer": {
        // description tells Claude when to use this subagent
        description:
          "Expert code review specialist. Use for quality, security, and maintainability reviews.",
        // prompt defines the subagent's behavior and expertise
        prompt: `You are a code review specialist with expertise in security, performance, and best practices.

When reviewing code:
- Identify security vulnerabilities
- Check for performance issues
- Verify adherence to coding standards
- Suggest specific improvements

Be thorough but concise in your feedback.`,
        // tools restricts what the subagent can do (read-only here)
        tools: ["Read", "Grep", "Glob"],
        // model overrides the default model for this subagent
        model: "sonnet"
      },
      "test-runner": {
        description:
          "Runs and analyzes test suites. Use for test execution and coverage analysis.",
        prompt: `You are a test execution specialist. Run tests and provide clear analysis of results.

Focus on:
- Running test commands
- Analyzing test output
- Identifying failing tests
- Suggesting fixes for failures`,
        // Bash access lets this subagent run test commands
        tools: ["Bash", "Read", "Grep"]
      }
    }
  }
})) {
  if ("result" in message) console.log(message.result);
}

各フィールドの役割はコメントのとおりです。description は Claude がこのサブエージェントを使う場面を判断する材料、prompt はサブエージェントの振る舞いと専門知識を決めるシステムプロンプト、tools はできることの制限、model は既定のモデルの上書きです。

この例について、読むときの注意が2つあります(いずれも筆者の指摘)。

  • 親の allowedTools に Agent が入っている。 サブエージェントを呼び出すツール自体を事前承認しています。第80回では Agent は「実行前に確認しないツール」として挙がっていたので、承認の面では必須ではないかもしれませんが、明示しておけば意図がはっきりします。
  • test-runner の Bash が実際に承認されるかは、別に確かめる必要がある。 tools に Bash を入れるのは「使える状態にする」ことで、実行の承認は権限の評価(第80回)で決まります。親の allowedTools は Read・Grep・Glob・Agent だけで、権限モードも指定されていません。第82回で見たとおりサブエージェントは親の権限ルールを引き継ぐので、この例のままでは test-runner の Bash が確認待ちになり、canUseTool が無ければ拒否される(第77回)可能性があります。実際に使うなら、テストのコマンドを allowedTools にスコープ付きで足すなどの設定を考える必要があります(第83回の「使える状態」と「権限」の2層の考え方の当てはめ)。

どのページの例もほとんどが最終結果だけを表示します。Claude がサブエージェントに任せたのか自分で答えたのかを確かめる方法は 3-5 にあります。

3-3. AgentDefinition の設定項目

原文の表を保持します。

フィールド型必須説明
descriptionstringはいこのエージェントをいつ使うかを説明する自然言語の文
promptstringはいエージェントの役割と振る舞いを決めるシステムプロンプト
toolsstring[]いいえ使えるツール名の配列。省略すると、サブエージェントで使える全てのツールを引き継ぐ
disallowedToolsstring[]いいえツールの一覧から取り除くツール名の配列。MCP サーバー単位の書き方も使える:mcp__server か mcp__server__* でそのサーバーの全ツール、mcp__* で全サーバーの全 MCP ツールを取り除く
modelstringいいえこのエージェントのモデルの上書き。'fable'・'opus'・'sonnet'・'haiku'・'inherit' などの別名か、完全なモデル ID。'inherit' はメインのモデルを使う。省略するとサブエージェントのモデル選択の順序で決まる
skillsstring[]いいえ起動時にコンテキストに読み込んでおくスキル名。挙げていないスキルも Skill ツールで呼べる
memory'user' | 'project' | 'local'いいえこのエージェントのメモリの出どころ
mcpServers(string | object)[]いいえこのエージェントが使える MCP サーバー(名前か、その場での設定)
initialPromptstringいいえこのエージェントがメインのエージェントとして動くとき、最初のユーザーのターンとして自動で送られる。サブエージェントとして呼ばれたときは無視される
maxTurnsnumberいいえ止まるまでの最大ターン数。上限に達すると出力に「途中まで」の印を付けて返し、あとで再開して続けられる(印付けは v2.1.246 以降)
backgroundbooleanいいえ呼ばれたとき、待たずに進むバックグラウンドの作業として動かす
omitClaudeMdbooleanいいえサブエージェントとして動くとき、ユーザー・プロジェクト・ローカルの CLAUDE.md を読まずに動く。管理ポリシーのファイルは読む。メインのエージェントとして動くときは無視される。TypeScript SDK v0.3.271 以降で、Python SDK には無い
effort'low' | 'medium' | 'high' | 'xhigh' | 'max' | numberいいえこのエージェントの推論の努力の度合い
permissionModePermissionModeいいえこのエージェント内でのツール実行の権限モード。いつ効くかはサブエージェントの継承の規則で決まる

Python でのフィールド名の注意。 Python SDK でも、disallowedTools や mcpServers のような複数の単語からなるフィールド名は、Python の慣習の snake_case ではなく、通信の形式に合わせて camelCase のまま書きます。第79回以降、Python では allowed_tools のように snake_case でしたが、AgentDefinition の中だけは例外です。

permissionMode の効き方は第80回で見たとおりです。親が default・dontAsk・plan のときだけサブエージェント側の設定が使われ、それでも bypassPermissions という値は適用されません。

既定はバックグラウンド実行です。 Agent ツールの呼び出しで run_in_background を省略すると、サブエージェントはバックグラウンドで動き、Claude は結果を待たずに先へ進みます。Claude が結果を受け取ってから進む必要がある場合は、run_in_background: false を指定します。background: true をエージェントの定義に書くと、Claude が何を指定しても、そのエージェントは常にバックグラウンドで動きます。

表を見る限り、定義の側で「必ずフォアグラウンドで動かす」ための項目はありません。run_in_background は Claude が Agent ツールを呼ぶときの入力なので、結果を待ってほしい作業は、プロンプトでそう伝えることになりそうです(表の項目からの筆者の読み)。

サブエージェントは、自分のサブエージェントを生み出すこともできます。 入れ子の深さ、同時に動く数、費用に上限を設ける方法は 3-7 で扱います。

3-4. サブエージェントが受け継ぐもの

サブエージェントが fork(親の会話の分岐)でない限り、そのコンテキストウィンドウは新しく始まり、親の会話を持っていません。ただし空ではありません。

親からサブエージェントに渡される内容は、Agent ツールのプロンプトの文字列だけです。 サブエージェントが必要とするファイルのパス、エラーメッセージ、決まったことは、そのプロンプトに直接書き込む必要があります。

サブエージェントが受け取るもの受け取らないもの
自分のシステムプロンプト(AgentDefinition.prompt)と、Agent ツールのプロンプト親の会話履歴やツールの結果
プロジェクトの CLAUDE.md(設定ソース経由で読み込まれたもの。omitClaudeMd を設定していなければ)読み込み済みのスキルの内容(AgentDefinition.skills に挙げたものを除く)
ツールの定義(親から引き継ぐか、tools で絞ったもの。バックグラウンド実行用に絞り込まれる)親のシステムプロンプト

ほかに受け継ぐものが2つあります。

  • 拡張思考の設定:メインのセッションの設定を引き継ぎます。
  • 名前付きエージェントの一覧:SendMessage ツールを持つサブエージェントは、そのセッションで動いている他の名前付きエージェントの一覧を持った状態で始まり、どの名前にメッセージを送れるかが分かります。Claude Code がこの一覧をサブエージェントの最初のターンに自動で加えます。fork は親の会話を受け継ぐので、この一覧は付きません。

2つ目は、連載で残っていた疑問に関わります。第64回では「サブエージェントは生み出した会話にだけ報告する」と整理しましたが、第72回では「名前を付けて生み出したサブエージェントどうしはメッセージを送り合える」とあり、食い違っていました。今回のページで、SendMessage ツールを持つサブエージェントは、他の名前付きエージェントにメッセージを送れることがはっきりしました。一方、作業の結果が親に返るのは Agent ツールの結果としてで、第64回の整理はその「結果の返り方」の話として読めば矛盾しません(両回の記述の整理は筆者)。SendMessage の拒否がサブエージェントへの送信にも及ぶことは、第73回で見ています。

親が受け取るのは最終メッセージで、しかも要約されることがあります。 親はサブエージェントの最終メッセージを Agent ツールの結果として受け取りますが、自分の応答の中ではそれを要約することがあります。サブエージェントの出力をユーザー向けの応答にそのまま残したいなら、メインの query() のプロンプトか systemPrompt で、そうするよう指示します。

親が読む前に、最終メッセージが検査されます(v2.1.210 以降)。 サブエージェントの出力に、指示のように振る舞う文が混ざっていないかを調べ、3種類のパターンを別々に扱います。

パターン扱い
制御タグの模倣(<system-reminder> のような、ハーネスだけが出すタグ)その場で無害化する。開きの < の後にバックスラッシュを入れる。何も消さない
権限設定への言及(.claude/settings.json、bypassPermissions、--dangerously-skip-permissions など)書かれたとおりに残す
ターンの区切りの模倣(Human: や Assistant: で始まる行)コロンの前にバックスラッシュを付け、会話のターンの境目に見せかけられないようにする

制御タグか権限設定に一致した場合は、どのパターンに一致したかを示す [harness: ...] という行が先頭に付きます。ターンの区切りの一致では、この行は付きません。検査が行う変更はこれだけで、サブエージェントの文章を消したり言い換えたりはしません。

これは、サブエージェントが読んだファイルやウェブページに仕込まれた指示が、サブエージェントの出力を経由して親に「命令」として届くのを防ぐための仕組みと読めます。第66回の「リレーされた承認は信頼できない入力として扱う」、第74回の「他人のアーティファクト内の指示は実行しない」と同じ考え方です(この位置づけは筆者)。

API エラーで早く終わったサブエージェント(レート制限など)は、それが結果として親に届くことはありません。フォアグラウンドとバックグラウンドでの挙動の違いは、CLI のサブエージェントのページに回されています。

3-5. サブエージェントを呼び出す

呼び出し方は3つあります。

自動の呼び出し。 Claude は、作業の内容と各サブエージェントの description をもとに、いつ呼ぶかを自分で判断します。たとえば説明が「クエリのチューニングのためのパフォーマンス最適化の専門家」の performance-optimizer を定義しておけば、プロンプトでクエリの最適化に触れると Claude はそれを呼びます。正しく割り振ってもらうには、明確で具体的な説明を書きます。

明示的な呼び出し。 特定のサブエージェントを確実に使わせたいなら、プロンプトで名前を指定します。

"Use the code-reviewer agent to check the authentication module"

これで自動の割り振りを飛ばし、指定したサブエージェントを直接呼びます。

実行時に定義を作る。 エージェントの定義は、実行時の条件に応じて作れます。次の例は、厳しさの度合いが違うセキュリティのレビュー担当を作り、厳しいレビューにはより高性能なモデルを使います。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition


# AgentDefinition を返すファクトリ関数
# このパターンにより、実行時の条件に基づいてエージェントをカスタマイズできます
def create_security_agent(security_level: str) -> AgentDefinition:
    is_strict = security_level == "strict"
    return AgentDefinition(
        description="Security code reviewer",
        # 厳密性レベルに基づいてプロンプトをカスタマイズ
        prompt=f"You are a {'strict' if is_strict else 'balanced'} security reviewer...",
        tools=["Read", "Grep", "Glob"],
        # 重要な洞察:高リスクのレビューにはより高性能なモデルを使用
        model="opus" if is_strict else "sonnet",
    )


async def main():
    # エージェントはクエリ時に作成されるため、各リクエストで異なる設定を使用できます
    async for message in query(
        prompt="Review this PR for security issues",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Grep", "Glob", "Agent"],
            agents={
                # 目的の設定でファクトリを呼び出す
                "security-reviewer": create_security_agent("strict")
            },
        ),
    ):
        if hasattr(message, "result"):
            print(message.result)


asyncio.run(main())
import { query, type AgentDefinition } from "@anthropic-ai/claude-agent-sdk";

// AgentDefinition を返すファクトリ関数
// このパターンにより、実行時の条件に基づいてエージェントをカスタマイズできます
function createSecurityAgent(securityLevel: "basic" | "strict"): AgentDefinition {
  const isStrict = securityLevel === "strict";
  return {
    description: "Security code reviewer",
    // 厳密性レベルに基づいてプロンプトをカスタマイズ
    prompt: `You are a ${isStrict ? "strict" : "balanced"} security reviewer...`,
    tools: ["Read", "Grep", "Glob"],
    // 重要な洞察:高リスクのレビューにはより高性能なモデルを使用
    model: isStrict ? "opus" : "sonnet"
  };
}

// エージェントはクエリ時に作成されるため、各リクエストで異なる設定を使用できます
for await (const message of query({
  prompt: "Review this PR for security issues",
  options: {
    allowedTools: ["Read", "Grep", "Glob", "Agent"],
    agents: {
      // 目的の設定でファクトリを呼び出す
      "security-reviewer": createSecurityAgent("strict")
    }
  }
})) {
  if ("result" in message) console.log(message.result);
}

定義を返す関数(ファクトリ)を用意し、リクエストごとに違う設定のエージェントを渡しています。TypeScript は securityLevel の型を "basic" | "strict" に限定し、Python は文字列で受けて "strict" かどうかだけを見ています。

呼び出されたことを確かめる

Claude は Agent ツールを通じてサブエージェントを呼びます。呼び出しを見つけるには、name が "Agent" の tool_use ブロックを探します。サブエージェントの中から出たメッセージには、parent_tool_use_id フィールドが付きます。

ツール名の注意。 このツールは tool_use ブロックでは "Agent" と表示されますが、system:init のツール一覧では "Task" と表示されます。また v2.1.63 より前は tool_use ブロックでも "Task" でした。SDK のバージョンをまたいで確実に見つけるには、block.name を両方の値と照合します。

メッセージの構造は言語で違います。Python は message.content で直接中身に届き、TypeScript は SDKAssistantMessage が API のメッセージを包んでいるので message.message.content を通ります(第77回の二重構造)。

次の例は、流れてくるメッセージを順に見て、サブエージェントが呼ばれたときと、それ以降のメッセージがサブエージェントの中から来たときを記録します。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ToolUseBlock


async def main():
    async for message in query(
        prompt="Use the code-reviewer agent to review this codebase",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Glob", "Grep", "Agent"],
            agents={
                "code-reviewer": AgentDefinition(
                    description="Expert code reviewer.",
                    prompt="Analyze code quality and suggest improvements.",
                    tools=["Read", "Glob", "Grep"],
                )
            },
        ),
    ):
        # Check for subagent invocation. Match both names: older SDK
        # versions emitted "Task", current versions emit "Agent".
        if hasattr(message, "content") and message.content:
            for block in message.content:
                if isinstance(block, ToolUseBlock) and block.name in (
                    "Task",
                    "Agent",
                ):
                    print(f"Subagent invoked: {block.input.get('subagent_type')}")

        # Check if this message is from within a subagent's context
        if hasattr(message, "parent_tool_use_id") and message.parent_tool_use_id:
            print("  (running inside subagent)")

        if hasattr(message, "result"):
            print(message.result)


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Use the code-reviewer agent to review this codebase",
  options: {
    allowedTools: ["Read", "Glob", "Grep", "Agent"],
    agents: {
      "code-reviewer": {
        description: "Expert code reviewer.",
        prompt: "Analyze code quality and suggest improvements.",
        tools: ["Read", "Glob", "Grep"]
      }
    }
  }
})) {
  const msg = message as any;

  // Check for subagent invocation. Match both names: older SDK versions
  // emitted "Task", current versions emit "Agent".
  for (const block of msg.message?.content ?? []) {
    if (block.type === "tool_use" && (block.name === "Task" || block.name === "Agent")) {
      console.log(`Subagent invoked: ${block.input.subagent_type}`);
    }
  }

  // Check if this message is from within a subagent's context
  if (msg.parent_tool_use_id) {
    console.log("  (running inside subagent)");
  }

  if ("result" in message) {
    console.log(message.result);
  }
}

TypeScript 版は message as any で型を外して読んでいます。msg.message?.content ?? [] で、中身の無いメッセージにも対応しています。どのサブエージェントが呼ばれたかは、block.input の subagent_type で分かります。

3-6. サブエージェントを再開する

サブエージェントは、最初からやり直さずに、中断したところから再開できます。再開したサブエージェントは、それまでのツール呼び出し、結果、推論を含む会話の履歴を全て持っています。

maxTurns の上限で止まった場合、Agent ツールの結果の出力には「途中まで」の印が付き、Claude は実行が終わっていないことが分かります。

サブエージェントが終わると、Agent ツールの結果に agentId: <id> を含む文章が入ります。組み込みの Explore と Plan のエージェントは1回きりで、agentId を返しません。再開したい場合は、自分で定義したエージェントか general-purpose を使います。

プログラムから再開する手順は3段階です。

  1. セッション ID を記録する:最初の問い合わせのメッセージから session_id を取り出す
  2. エージェント ID を取り出す:Agent ツールの結果の文章から agentId を読み取る
  3. セッションを再開する:2回目の問い合わせのオプションで resume: sessionId を渡し、プロンプトにエージェント ID を入れる。各 query() 呼び出しは既定で新しいセッションを始めるので、サブエージェントのトランスクリプトに届くには、同じセッションを再開する必要がある

自分で定義したエージェントを使う場合は、2回の問い合わせの両方で同じ agents の定義を渡します。

次の例は、endpoint-finder というエージェントを定義し、1回目でそれを動かしてセッション ID とエージェント ID を記録し、2回目でセッションを再開して、最初の分析の文脈が必要な追加の質問をします。

import asyncio
import re
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ToolResultBlock

AGENTS = {
    "endpoint-finder": AgentDefinition(
        description="Locates and catalogs API endpoints in a codebase.",
        prompt="You find and document API endpoints. Report each endpoint's path, method, and handler.",
        tools=["Read", "Grep", "Glob"],
    )
}


def extract_agent_id(block: ToolResultBlock) -> str | None:
    """Extract agentId from an Agent tool result's text content."""
    parts = block.content if isinstance(block.content, list) else [{"text": block.content}]
    for part in parts:
        if match := re.search(r"agentId:\s*([\w-]+)", part.get("text") or ""):
            return match.group(1)
    return None


async def main():
    agent_id = None
    session_id = None

    # First invocation - run the endpoint-finder subagent
    try:
        async for message in query(
            prompt="Use the endpoint-finder agent to find all API endpoints in this codebase",
            options=ClaudeAgentOptions(allowed_tools=["Read", "Grep", "Glob", "Agent"], agents=AGENTS),
        ):
            # Capture session_id from ResultMessage (needed to resume this session)
            if hasattr(message, "session_id"):
                session_id = message.session_id
            # Search tool results for the agentId trailer
            for block in getattr(message, "content", None) or []:
                if isinstance(block, ToolResultBlock):
                    agent_id = extract_agent_id(block) or agent_id
            # Print the final result
            if hasattr(message, "result"):
                print(message.result)
    except Exception as error:
        # A single-shot query() raises after yielding an error result,
        # so session_id and agent_id have already been captured by the loop above.
        print(f"Session ended with an error: {error}")

    # Second invocation - resume and ask follow-up
    if agent_id and session_id:
        async for message in query(
            prompt=f"Resume agent {agent_id} and list the top 3 most complex endpoints",
            options=ClaudeAgentOptions(
                allowed_tools=["Read", "Grep", "Glob", "Agent"], agents=AGENTS, resume=session_id
            ),
        ):
            if hasattr(message, "result"):
                print(message.result)
    else:
        print("No agentId found in the first query, so there is no subagent to resume.")


asyncio.run(main())
import { query, type SDKMessage } from "@anthropic-ai/claude-agent-sdk";

const agents = {
  "endpoint-finder": {
    description: "Locates and catalogs API endpoints in a codebase.",
    prompt: "You find and document API endpoints. Report each endpoint's path, method, and handler.",
    tools: ["Read", "Grep", "Glob"]
  }
};

// Stringify content to search for agentId without traversing nested block types
function extractAgentId(message: SDKMessage): string | undefined {
  if (message.type !== "assistant" && message.type !== "user") return undefined;
  const content = JSON.stringify(message.message.content);
  const match = content.match(/agentId:\s*([\w-]+)/);
  return match?.[1];
}

let agentId: string | undefined;
let sessionId: string | undefined;

// First invocation - run the endpoint-finder subagent
try {
  for await (const message of query({
    prompt: "Use the endpoint-finder agent to find all API endpoints in this codebase",
    options: { allowedTools: ["Read", "Grep", "Glob", "Agent"], agents }
  })) {
    // Capture session_id from ResultMessage (needed to resume this session)
    if ("session_id" in message) sessionId = message.session_id;
    // Search message content for the agentId (appears in Agent tool results)
    const extractedId = extractAgentId(message);
    if (extractedId) agentId = extractedId;
    // Print the final result
    if ("result" in message) console.log(message.result);
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result,
  // so sessionId and agentId have already been captured by the loop above.
  console.error(`Session ended with an error: ${error}`);
}

// Second invocation - resume and ask follow-up
if (agentId && sessionId) {
  for await (const message of query({
    prompt: `Resume agent ${agentId} and list the top 3 most complex endpoints`,
    options: { allowedTools: ["Read", "Grep", "Glob", "Agent"], agents, resume: sessionId }
  })) {
    if ("result" in message) console.log(message.result);
  }
} else {
  console.log("No agentId found in the first query, so there is no subagent to resume.");
}

エージェント ID の取り出し方は、言語で工夫が違います。Python は ToolResultBlock を見つけて、その中身(文字列か、部品のリスト)を正規表現 agentId:\s*([\w-]+) で調べます。TypeScript は、入れ子のブロックの型をたどる代わりに、メッセージの中身を丸ごと JSON.stringify で文字列にしてから同じ正規表現で探しています。

どちらも、ツールの結果の文章から正規表現で ID を拾う方法です。文章の形式が変われば拾えなくなるので、拾えなかったときの処理(例の else の分岐)を必ず用意しておくのが安全です(筆者の指摘)。

サブエージェントのトランスクリプトは別のファイルに保存され、メインの会話とは独立して残ります。圧縮の挙動と、cleanupPeriodDays による掃除の期間は、CLI のサブエージェントのページに回されています(cleanupPeriodDays は第66・68回で見た設定です)。

3-7. ツールを制限する

tools フィールドで、サブエージェントができることを制限します。

  • tools を省略:サブエージェントで使える全てのツールを受け取る
  • ツールを列挙:列挙したツールだけを受け取る。たとえばファイルを編集してはいけないコードレビュー担当には ["Read", "Grep", "Glob"]

省いたツールは、サブエージェントのセッションにそもそも存在しません。 Claude はそのツール無しで作業し、権限の確認もエラーも出ません。第80回で見た「ツール名だけの拒否ルールはツールをコンテキストから消す」と同じく、確認で止めるのではなく最初から見せない方式です。

次の例は、コードを調べられるが、ファイルを変えることもコマンドを動かすこともできない、読み取り専用の分析エージェントを作ります。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition


async def main():
    async for message in query(
        prompt="Analyze the architecture of this codebase",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Grep", "Glob", "Agent"],
            agents={
                "code-analyzer": AgentDefinition(
                    description="Static code analysis and architecture review",
                    prompt="""You are a code architecture analyst. Analyze code structure,
identify patterns, and suggest improvements without making changes.""",
                    # Read-only tools: no Edit, Write, or Bash access
                    tools=["Read", "Grep", "Glob"],
                )
            },
        ),
    ):
        if hasattr(message, "result"):
            print(message.result)


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "Analyze the architecture of this codebase",
  options: {
    allowedTools: ["Read", "Grep", "Glob", "Agent"],
    agents: {
      "code-analyzer": {
        description: "Static code analysis and architecture review",
        prompt: `You are a code architecture analyst. Analyze code structure,
identify patterns, and suggest improvements without making changes.`,
        // Read-only tools: no Edit, Write, or Bash access
        tools: ["Read", "Grep", "Glob"]
      }
    }
  }
})) {
  if ("result" in message) console.log(message.result);
}

よく使う組み合わせは次のとおりです(原文の表)。

用途ツール説明
読み取り専用の分析Read、Grep、Globコードを調べられるが、変更も実行もできない
テストの実行Bash、Read、Grepコマンドを実行し、出力を分析できる
コードの変更Read、Edit、Write、Grep、Globコマンドの実行なしで、読み書きが全てできる
全てのツール全てのツールサブエージェントで使えるツールを引き継ぐ(tools を省略)

3-8. 深さ・同時実行数・支出に上限を設ける

この節は TypeScript SDK v0.3.219 以降と Python SDK v0.2.127 以降(Claude Code v2.1.219 以降を同梱)を前提にしています。それより前の版では一部の上限が無いか、既定値が違うので、上限に頼る前に更新します。

Claude は、サブエージェントをいつ、いくつ生み出すかを自分で決めます。 各サブエージェントはそれぞれ API にリクエストを送り、その費用は問い合わせの total_cost_usd に加算されます。さらにサブエージェントが自分のサブエージェントを生み出せるので、1つのプロンプトがエージェントの木に育つことがあります。

この広がりには3つの方法で上限を設けられます。深さと同時実行数は env オプションで環境変数として、支出は問い合わせのオプションとして設定します。

上限設定既定値上限に達したときの動き
深さCLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTHメインの下に3層。1 にするとサブエージェントは自分のサブエージェントを生み出せない最下層のサブエージェントは生み出せなくなり、任された作業を自分で行う
同時実行数CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS20個。Claude が Agent ツールで生み出す全てのサブエージェントを数えるそれ以上の生成を断り、Concurrent subagent limit reached を返す。実行中の数が上限を下回るまで待つ。ultracode が有効なセッションは断られない
支出TypeScript は maxBudgetUsd、Python は max_budget_usd上限なし。呼び出し自体の支出を数え、サブエージェントのリクエストも含む3つの方法で止める:それ以上の生成を断って Budget limit reached を返す/実行中のバックグラウンドのサブエージェントを止める/error_max_budget_usd の結果で問い合わせを終える

env の扱いは、これまでと同じく言語で違います。TypeScript は子プロセスの環境を置き換えるので process.env を展開して PATH などを残し、Python は引き継いだ環境に重ねます(第79・85回)。次の例は、入れ子を無効にし、同時に最大5個まで許し、推定の支出が5ドルに達したら問い合わせを止めます。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
    try:
        async for message in query(
            prompt="Audit every service in this repo for unhandled promise rejections",
            options=ClaudeAgentOptions(
                allowed_tools=["Read", "Grep", "Glob", "Agent"],
                # env is merged on top of the inherited environment
                env={
                    "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH": "1",
                    "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS": "5",
                },
                max_budget_usd=5.0,
            ),
        ):
            if isinstance(message, ResultMessage):
                print(f"{message.subtype}: ${message.total_cost_usd}")
    except Exception as error:
        # A single-shot query() raises after yielding an error result,
        # so the budget-capped result has already been printed above.
        print(f"Session ended with an error: {error}")


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

try {
  for await (const message of query({
    prompt: "Audit every service in this repo for unhandled promise rejections",
    options: {
      allowedTools: ["Read", "Grep", "Glob", "Agent"],
      // env replaces the subprocess environment, so spread process.env to keep PATH
      env: {
        ...process.env,
        CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH: "1",
        CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS: "5",
      },
      maxBudgetUsd: 5,
    },
  })) {
    if (message.type === "result") {
      console.log(`${message.subtype}: $${message.total_cost_usd}`);
    }
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result,
  // so the budget-capped result has already been logged above.
  console.error(`Session ended with an error: ${error}`);
}

表示される内容は、どの上限に達したかで変わります。

  • 支出の上限より下:success と推定の費用が表示される
  • 支出の上限に達した:error_max_budget_usd と5ドル以上の費用が表示され、その後で例外の処理が動く
  • 同時実行数の上限に達した:メッセージの流れに Concurrent subagent limit reached を含む tool_result ブロックが現れる。Claude も Agent ツールの結果として同じものを受け取る

支出の上限は「5ドル以上」で止まる点に注意が要ります。上限に達したことが分かった時点で止めるので、実際の費用は上限を少し超えることがあります。上限は「ちょうどその額で止まる」ものではなく「超えたら止める」ものと考えておくのが安全です(原文の「$5 以上のコストで表示」からの筆者の読み)。

Opus 5 でサブエージェントを使うとき

Claude Opus 5 は、以前のモデルよりサブエージェントへの委任に積極的です。そのため、上の3つの上限は Opus 5 を使う問い合わせで最も重要になります。Opus 5 のプロンプトの手引きには、どんなプロンプトにも足せる委任の指示が載っています。Claude Code が自分で指示を足すかどうかは、システムプロンプトの選び方(第86回)で変わります。

  • claude_code プリセット:モデルが Opus 5 のとき、Claude Code はシステムプロンプトに1行足し、頼まれない限り Agent ツールを呼ばないよう指示する。Agent ツール自体は使えるまま。
  • カスタムプロンプト、または systemPrompt なし:Claude Code はシステムプロンプトを組み立てないので、その1行は入らない。手引きの委任の指示を自分のプロンプトに足す。

どちらの指示も Claude を方向づけるだけなので、上限も設定します。 上限は、Claude がどう委任するかに関係なく Claude Code が守らせます。第72回の「指示はリクエストであって保証ではない」がそのまま当てはまる場面です。

3-9. もっと大きな作業にはワークフロー

サブエージェントは、1ターンあたり数個の作業を任せるのに向いています。何十、何百ものエージェントを調整する実行には、Workflow ツールを使います。これは、調整の仕事を会話のコンテキストの外、実行時が動かすスクリプトに移すものです(第67回で扱った動的ワークフロー)。

Workflow ツールは TypeScript Agent SDK v0.3.149 以降で使えます。allowedTools に Workflow を入れると、ワークフローの実行が事前承認されます。入力と出力の形は TypeScript のリファレンスにあります。

3-10. よくある問題

Claude がサブエージェントに任せない。 Claude が任せずに自分で片付けてしまう場合は、次を試します。

  • プロンプトでサブエージェントを名前で指定する(例:「code-reviewer エージェントを使って認証モジュールを確認して」)
  • いつ使うべきかを正確に書いた、明確な説明にする

ファイルで定義したエージェントが読み込まれない。 Claude Code は ~/.claude/agents/ と .claude/agents/ を見張っていて、新しいファイルや編集は数秒で反映され、再起動は要りません。表示されない場合の原因は次のとおりです。

原因内容
新しい agents ディレクトリ見張りはセッション開始時にあったディレクトリだけが対象。新しく作ったディレクトリの最初のファイルは、セッションの再起動が必要。最もよくある原因
frontmatter の誤りや name の重複ファイルの YAML を確認し、同じ name のエージェントが無いかを確かめる
--disable-slash-commandsこのフラグで始めたセッションはディレクトリを見張らないので、新しいファイルには常に再起動が必要
追加したディレクトリの下のファイルadd_dirs(Python)/additionalDirectories(TypeScript)や CLI の --add-dir・/add-dir で追加したディレクトリの .claude/agents/ は読み込むが見張らないので、新規・編集には再起動が必要
同じ名前のプログラム定義query() に渡した agents が、同じ名前のファイルのエージェントを上書きする

ファイルの書き方は CLI のサブエージェントのページに回されています。


4. まとめ

  • サブエージェントは、メインのエージェントが作業を任せる別のエージェント。利点は文脈の分離・並行実行・専門の指示・ツールの制限。SDK では agents パラメーターでのプログラム定義が推奨で、同名ならファイル定義より優先される。
  • AgentDefinition の必須は description(いつ使うか)と prompt(振る舞い)。tools・model・maxTurns・permissionMode などで細かく決める。Python でも中の複数語のフィールド名は camelCase。omitClaudeMd は TypeScript のみ。
  • 既定はバックグラウンド実行。background: true で常にバックグラウンドにできる。
  • サブエージェントは親の会話を持たず、渡るのは Agent ツールのプロンプトだけ。必要な情報はそこに書く。受け取るのは自分のプロンプト、プロジェクトの CLAUDE.md、ツールの定義、拡張思考の設定。
  • SendMessage を持つサブエージェントは、他の名前付きエージェントにメッセージを送れる(第64回と第72回の食い違いの答え)。
  • 最終メッセージは親が読む前に検査され、制御タグや会話の区切りの模倣が無害化される(v2.1.210 以降)。親は出力を要約しうる。
  • 呼び出しは自動(説明で判断)・明示(名前で指定)・実行時の定義。検出は tool_use の名前 "Agent" と "Task" の両方を見る。
  • 再開は、セッション ID と agentId を取り、同じセッションを resume して ID をプロンプトに入れる。Explore と Plan は再開できない。
  • 省いたツールは存在しないものとして扱われ、確認もエラーも出ない。
  • 上限は深さ(既定3層)・同時実行数(既定20)・支出(既定なし)。Opus 5 は委任に積極的なので上限が特に重要。指示だけでなく上限で守らせる。
  • 数十〜数百のエージェントを扱うなら Workflow ツール。

次回予告

次回は 「スキル」(agent-sdk/skills) を取り上げる予定です。

今回、サブエージェントの定義には skills という項目があり、挙げたスキルを起動時に読み込んでおけると見ました。第86回でも、スキルはシステムプロンプトの外で振る舞いを形づくるものとして名前だけ登場しています。次回は、そのスキルを SDK のエージェントでどう読み込み、どう使わせるのかを見ていきます。

※テーマは変更になる場合があります。


よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました