コンテンツにスキップ

Claude Agent SDK 用語集+学習メモ — 写経で詰まったところ

毎日使っている Claude Code の「ハーネス」(ループ・ツール・権限・サブエージェント)を、 Anthropic 公式の Claude Agent SDK で Python から自作して一周するフォルダ。 他のレクチャーが OpenAI SDK 中心なのに対し、ここだけ Claude Code 本体と同じ SDK を触る。

ロードマップ本体は ../../agent-design/STUDY_NOTES.md(STEP 2 の発展)、 サンプルの並びは README.md を参照。 検証環境: claude-agent-sdk==0.2.97uv.lock 実測)、Python >=3.11。


0. このフォルダの位置づけ — 「ハーネスを Python から組む」とは

LLM そのもの(モデル)は「テキストを1往復で返すだけ」の存在。普段の Claude Code が 「ファイルを読む・コマンドを実行する・許可を聞く・サブエージェントに振る」ように見えるのは、 モデルの周りを囲むハーネス(=ループ + ツール実行 + 権限判定 + コンテキスト管理)が やっている。Claude Agent SDK は、そのハーネスをそっくり Python から組み立てる SDK。

あなたのコード(query / ClaudeSDKClient)
        │  prompt を渡す
┌──────────────────────────────────────────────┐
│  Claude Agent SDK(= Claude Code のハーネス本体) │
│   1. モデルに投げる                              │
│   2. モデルが「ツール使う」と言ったら…             │
│        → hooks(ex04)で前検査                    │
│        → can_use_tool(ex05)で許可判定           │
│        → ツール(@tool / 組込 Bash 等)を実行      │
│        → 結果をモデルに戻す                       │
│   3. 完了するまで 1〜2 をループ(max_turns で上限) │
└──────────────────────────────────────────────┘
        │  メッセージのストリームを返す
   AssistantMessage / ResultMessage …(型付き)

重要: モデル呼び出しは Claude Code CLI が内部で行い、認証もそこに寄せるclaude login)。 だから他フォルダの .env.op + op run は不要で、ANTHROPIC_API_KEY も基本いらない。 これは「ハーネスとモデル呼び出しを分離し、認証はハーネス側へ」という設計の表れ。


全体像 — サンプル間の関係と学ぶ順序

query() という最小の入口から始め、ツール・状態・権限・分離と、ハーネスの構成要素を 1つずつ足していく。後半ほど「制御を細かく握る」方向に進む。

# ファイル 学ぶ概念 中心 API ひとことで
01 ex01_query_basics.py 使い切りのエージェント呼び出しと型付きメッセージ query() / AssistantMessage / ResultMessage ループ全体がメッセージのストリームで返る
02 ex02_custom_tools.py 自作ツールを「プロセス内 MCP」として与える @tool / create_sdk_mcp_server / allowed_tools 関数を書くだけで MCP サーバになる
03 ex03_client_multiturn.py 状態を保つマルチターン会話 ClaudeSDKClient / receive_response() 会話履歴を SDK が肩代わり
04 ex04_hooks.py ツール実行前後に決定論コードを差し込む HookMatcher / PreToolUse / permissionDecision settings.json の hooks の Python 版
05 ex05_permissions.py 引数を見て動的に許可判定 can_use_tool / permission_mode / PermissionResultAllow/Deny 名前ではなく中身で allow/deny
06 ex06_subagents.py コンテキスト分離のためのサブエージェント agents / AgentDefinition / "Agent" ツール 親に返るのは最終結果だけ

学ぶ順序の流れ(依存関係)

flowchart LR
    ex01["ex01 query()<br/>最小の入口"] --> ex02["ex02 @tool<br/>ツールを足す"]
    ex01 --> ex03["ex03 Client<br/>状態を足す"]
    ex02 --> ex04["ex04 hooks<br/>実行前に割り込む"]
    ex03 --> ex04
    ex04 --> ex05["ex05 can_use_tool<br/>引数で許可判定"]
    ex05 --> ex06["ex06 subagents<br/>分離"]

エージェントループの中で hooks / can_use_tool / ツールが動く順番(ex04・ex05 が刺さる場所)

sequenceDiagram
    participant You as あなたのコード
    participant SDK as Agent SDK(ハーネス)
    participant Model as Claude モデル
    participant Hook as PreToolUse hook (ex04)
    participant Perm as can_use_tool (ex05)
    participant Tool as ツール(@tool / Bash 等)

    You->>SDK: query(prompt, options)
    SDK->>Model: prompt を投げる
    Model-->>SDK: 「このツールを使いたい」(ToolUseBlock)
    SDK->>Hook: 実行前に検査(決定論コード)
    alt deny を返した
        Hook-->>SDK: permissionDecision: "deny"
        SDK->>Model: 「禁止されたので使えない」と伝える
    else 通過
        Hook-->>SDK: {} (許可)
        SDK->>Perm: tool_name + 引数で許可判定
        alt PermissionResultDeny
            Perm-->>SDK: deny(理由つき)
            SDK->>Model: 「許可されなかった」と伝える
        else PermissionResultAllow
            Perm-->>SDK: allow
            SDK->>Tool: 実行
            Tool-->>SDK: 結果
            SDK->>Model: ツール結果を戻す
        end
    end
    Model-->>SDK: 最終回答
    SDK-->>You: AssistantMessage … ResultMessage

ポイント: hook(ex04)→ can_use_tool(ex05)→ ツール実行 の順。 hook は「広く差し込む検査」、can_use_tool は「許可判定専用」で、層が違う(§用語集 §3 で詳述)。


サンプル別の要点(原理から)

ex01 — query():最小のエージェント呼び出し

何を学ぶか: OpenAI SDK の chat.completions.create() が「1 リクエスト = 1 レスポンス(1往復)」 なのに対し、query()エージェントループ全体を回し、その過程を「メッセージのストリーム」で返す。 返ってくるのは生の文字列ではなく型付きメッセージ

# ex01_query_basics.py:33-53
options = ClaudeAgentOptions(
    system_prompt="あなたは簡潔に答える学習アシスタントです。",
    max_turns=1,            # ① ループの暴走を止める上限
    allowed_tools=[],       # ① ツールを使わせない(純粋な応答だけ観察)
)

async for message in query(prompt="RAG とは何か3行で説明して。", options=options):  # ②
    if isinstance(message, AssistantMessage):       # ③ アシスタントの発話
        for block in message.content:               #    content は block のリスト
            if isinstance(block, TextBlock):         #    TextBlock / ToolUseBlock 等
                print("[assistant]", block.text)
    elif isinstance(message, ResultMessage):        # ④ ループ終了時に1つだけ来る
        cost = getattr(message, "total_cost_usd", None)  # コスト等の集計
やってること なぜそうする
max_turns / allowed_tools=[] ループ暴走を止め、ツールなしの素の応答を観察するため
query(...)async for で回す query は async ジェネレータ。途中経過が逐次流れる
AssistantMessage.content を block 単位で読む 1メッセージにテキストとツール使用が混在しうるため
ResultMessage で最終結果+コスト ループ終了の合図。集計はここに集まる

なぜ getattr で防御的に読むか: SDK のバージョンで属性名が変わることがあるため。 写経時はまず print(message) で生の型を眺め、どんなメッセージ型が流れるか体感するのが速い。

ex02 — @tool + create_sdk_mcp_server:プロセス内 MCP

何を学ぶか: MCP(連載19/20回)は普通別プロセスのサーバを立てて IPC で繋ぐが、Agent SDK では @tool で書いた Python 関数をプロセス内 MCP サーバにできる。サブプロセス管理も IPC も無く、 ただの関数として書ける(「in-process MCP」)。

# ex02_custom_tools.py:28-50
@tool("add", "2つの整数を足し算する", {"a": int, "b": int})   # ① name, description, schema
async def add(args: dict) -> dict:
    result = args["a"] + args["b"]
    return {"content": [{"type": "text", "text": f"答えは {result} です"}]}  # ② MCP のツール結果形式

server = create_sdk_mcp_server(name="mytools", version="1.0.0", tools=[add, word_count])  # ③
options = ClaudeAgentOptions(
    mcp_servers={"mytools": server},
    allowed_tools=["mcp__mytools__add", "mcp__mytools__word_count"],  # ④ mcp__<server>__<tool>
    max_turns=5,
)
やってること なぜそうする
@tool(name, description, schema) description/docstring がモデルの「いつ呼ぶか」判断材料
戻り値は {"content": [{"type":"text","text":...}]} 固定 MCP のツール結果フォーマット。崩すと結果が渡らない
create_sdk_mcp_server で複数ツールを1サーバに束ねる name がツール名のプレフィックスになる
allowed_toolsmcp__mytools__add 等を列挙 この命名規則のツール名で静的に自動承認する

観察: モデルは「枝豆の妖精」を word_count → 結果に 100 を add、と順にツールを呼んで繋ぐはずallowed_tools から add を外すと add 呼び出しで承認待ち/拒否になる。ツール名でゲートされる この入口が、ex05 の引数依存の権限制御へ繋がる。

ex03 — ClaudeSDKClient:状態を保つマルチターン

何を学ぶか: query() は1依頼を回し切る使い切り。会話を続けたい(前ターンを覚えたまま次を頼む)なら ClaudeSDKClientasync with でセッションを開き、query()receive_response() を反復する。 Claude Code の対話セッションそのものの最小形。

# ex03_client_multiturn.py:42-51
async with ClaudeSDKClient(options=options) as client:   # ② 閉じるまで会話状態が保たれる
    await client.query("私はいま RAG を勉強しています。recall@k を使っています。")
    await print_response(client)                          # receive_response() を読む
    await client.query("さっき言った評価指標、ほかに何と組み合わせるといい?")  # ③ 前ターンを指す
    await print_response(client)
やってること なぜそうする
① (print_response) client.receive_response()async for そのターン分のメッセージを流す async ジェネレータ
async with ClaudeSDKClient(...) セッションを開き、閉じるまで会話履歴を保持
2ターン目で1ターン目を指す質問 状態が効いていれば「recall@k」を覚えて MRR 等を提案するはず

判定基準(最重要): 状態(会話履歴)を保ちたいか? → Yes なら ClaudeSDKClient、No(単発依頼)なら query()query() を2回呼ぶと毎回新規セッションになり、Q2 は文脈を失う。 これは context_basics で自前で messages を積んでいた処理を、SDK が肩代わりしている形。

ex04 — hooks:実行前に決定論コードで割り込む

何を学ぶか: hooks は「ツール実行の前後」などのイベントで自分のコードを差し込む仕組み。 ~/.claude/settings.json の PreToolUse hook(gitleaks 等)のプログラム版。 LLM の判断とは独立に、コードで deny を返せるのが要点(=構造的防御。注入されても効く)。

# ex04_hooks.py:29-44
async def block_dangerous_bash(input_data: dict, tool_use_id: str, context) -> dict:  # ① 3引数
    if input_data.get("tool_name") != "Bash":
        return {}                                          # ② 空 dict = 通す
    command = input_data.get("tool_input", {}).get("command", "")
    for pattern in BLOCK_PATTERNS:                         # ["rm -rf", "curl", "> /etc/", ":(){"]
        if pattern in command:
            return {                                       # ③ この形を返すとブロック
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"禁止パターン: {pattern}",
                }
            }
    return {}

# ex04_hooks.py:55
hooks={"PreToolUse": [HookMatcher(matcher="Bash", hooks=[block_dangerous_bash])]}  # ④
やってること なぜそうする
hook 関数は (input_data, tool_use_id, context) input_datatool_name / tool_input が入る
空 dict を返すと「通す」 何も言わなければ素通り。明示的に止めるときだけ dict を返す
permissionDecision: "deny" で実行ブロック 理由(...Reason)がモデルに伝わり、別の手を考えさせる
HookMatcher(matcher="Bash", hooks=[...]) 対象ツールに hook を紐づける。"*" で全ツール対象

観察: ls は hook を通過し、rm -rf は deny される。モデルは「削除できなかった」と報告するはず (guardrails ex03 と同じく拒否してもループは続く)。これがまさに ~/.claudegitleaks-precommit / block-secret-file-reads と同じ仕組みで、settings.json の hooks の Python 版。

ex05 — can_use_tool:引数を見て動的に許可判定

何を学ぶか: allowed_tools(ex02)は「ツール名」での静的な許可。can_use_tool は 「ツール名 + 引数」を受けて毎回動的に allow/deny を返すコールバック。 パスや宛先の中身で判断したいときの、最も細かい権限制御。

# ex05_permissions.py:34-45
async def can_use_tool(tool_name: str, tool_input: dict, context):   # ① 3引数
    path = tool_input.get("file_path") or tool_input.get("path") or ""
    if tool_name in ("Write", "Edit"):
        if not path.startswith(ALLOWED_DIRS):                        # ② 中身(パス)で判定
            return _deny(f"許可外パスへの書き込み: {path}")            #    PermissionResultDeny
        # 許可ディレクトリ配下なら通す
    return _allow()                                                  # PermissionResultAllow

# ex05_permissions.py:59-65
options = ClaudeAgentOptions(
    allowed_tools=["Read", "Write", "Edit"],
    permission_mode="acceptEdits",   # ③ 全体の構え。can_use_tool が個別に上書き
    can_use_tool=can_use_tool,
)
やってること なぜそうする
can_use_tool(tool_name, tool_input, context) 引数(tool_input)の中身を見て判定できるのが allowed_tools との差
path.startswith(ALLOWED_DIRS) で許可域を限定 名前だけでは分からない「どこに書くか」で許可を決める
permission_mode は全体の構え、can_use_tool で上書き 粗い既定(acceptEdits)+ 細かい個別判定の二段構え

観察: /etc/ への書き込みは deny、/tmp/ への書き込みは通るはず。 SDK バージョンで PermissionResultAllow/Deny の import 経路が違うことがあるため、 無ければ {"behavior": "allow"} / {"behavior": "deny", "message": ...} の dict で代用している(ex05:24-27, 48-53)。

ex06 — subagents:コンテキスト分離のためのサブエージェント

何を学ぶか: サブエージェントの第一義は「並列化」ではなくコンテキスト分離。各サブエージェントは 真っ新な会話で走り、親に返るのは最終メッセージだけ。途中で読んだ大量のファイルやツール結果は 親のコンテキストを汚さない(context_basics の isolate 戦略)。

# ex06_subagents.py:31-49
options = ClaudeAgentOptions(
    allowed_tools=["Read", "Grep", "Glob", "Agent"],   # ① "Agent" が無いと委譲が承認待ち
    agents={
        "code-reviewer": AgentDefinition(
            description="コード品質・セキュリティ・保守性のレビュー専門家。レビュー依頼時に使う。",  # ② 委譲のトリガ
            prompt="あなたはコードレビュー専門家です。…",
            tools=["Read", "Grep", "Glob"],            # ③ read-only に絞る(最小権限)
            model="sonnet",                            # ④ サブエージェント個別のモデル
        ),
    },
)
やってること なぜそうする
allowed_tools"Agent" を入れる これが無いとサブエージェント呼び出しが承認待ちになる(親が委譲できない)
AgentDefinition(description=...) モデルが「いつ委譲するか」を description で判断する
tools=["Read","Grep","Glob"] レビュアーは書き換え不可(最小権限)。Read を外せば何も読めなくなる
model="sonnet" サブエージェント個別にモデルを変えられる(alias 指定)

観察: レビュアーが読んだファイルの中身は親のストリームにほぼ現れない。返るのは最終レビュー結果だけ = コンテキスト分離が効いている。サブエージェント内の発話は parent_tool_use_id を持つ(ex06:60)。 注意: サブエージェントはさらに子を産めない(その tools"Agent" を入れていない)。


ex07 — langfuse 計装:LangChain 非対応エージェントをトレースする

何を学ぶか: 「LangChain を使わないエージェントでも langfuse でトレースできる」を、自動計装が 一切効かない SDK で示す。他レクチャー(langfuse_basics)は OpenAI の langfuse.openai ドロップインや LangChain の CallbackHandler で「自動でトレースが生える」が、Claude Agent SDK はどちらにも乗らない:

自動計装の経路 この SDK で効くか 理由
from langfuse.openai import OpenAI(ドロップイン) OpenAI SDK 専用。query() は OpenAI SDK ではない
LangChain CallbackHandler LangChain のコールバック機構に乗っていない
@observe()手動計装 関数を包むだけ。SDK の種類を問わない

だから計装は @observe() 手動計装でやる。これが ex07 の核心。

# ex07_langfuse_tracing.py(要点)
@observe()                                    # ① 関数を1トレースにする(自動経路の代わり)
async def ask(prompt, user_id, session_id):
    with propagate_attributes(                # ② 「誰の・どの会話か」を trace に刻む
        trace_name="claude_agent_ask",
        user_id=user_id, session_id=session_id,
        tags=["lecture", "claude-agent-sdk"],
    ):
        return await run_agent(prompt)        # ③ 本体は langfuse を一切知らない(分離)

# run_agent の中で、LLM ステップを generation として手で記録する:
with langfuse.start_as_current_observation(
    name="claude_agent_query", as_type="generation",
    input=prompt, model="claude-agent-sdk",
) as generation:
    generation.update(output=final_text)
    generation.update(usage_details=usage)        # ResultMessage.usage から手で詰める
    generation.update(cost_details={"total": cost})  # total_cost_usd も手で
やってること なぜそうする
@observe() ドロップインも CallbackHandler も使えないので、関数を包んでトレースを作る
propagate_attributes(...) trace に user_id / session_id を伝播。UI の Users / Sessions で引ける
本体 run_agent は langfuse 非依存 計装と本体を分離。@observe を剥がしても本体は無傷 = 既存 ex に薄く乗せられる
generation start_as_current_observation(as_type="generation") ドロップインが usage/cost を自動で詰めてくれない分、ResultMessage から手で update() する

env ガード: LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY が揃わなければ observe / propagate_attributesno-op に差し替える。キーが無い環境でも import エラーや送信失敗で落ちず、 計装コードがそのまま素通りする。ex01_query_basics.py 冒頭にも同じガード付きで @observe()2 行だけ足してある(「既存ハーネスに 2 行で計装が乗る」のデモ)。

実行: op run --env-file=.env.op -- uv run python ex07_langfuse_tracing.py.env.op は langfuse の 3 キーのみ。SDK 認証は claude login なので OPENAI/ANTHROPIC キーは不要)


用語集(最重要)— 写経で言葉が混乱するポイント

1. query()ClaudeSDKClient — エージェントを動かす2つの入口

query()(ex01・02・06) ClaudeSDKClient(ex03・04・05)
性質 使い切り。1依頼を回し切って終わり 会話セッション。閉じるまで状態を保つ
状態(履歴) 持たない(呼ぶたび新規) 保持する(前ターンを覚える)
async for m in query(prompt, options) async with ClaudeSDKClient(options) as c:c.query() / c.receive_response()
具体例 ex01 の3行説明、ex06 のレビュー委譲 ex03 の2ターン会話、ex04/05 の連続依頼

判定基準: 会話の状態(履歴)を保ちたいか? → Yes なら ClaudeSDKClient、単発なら query()。 迷ったら「2ターン目で1ターン目を指す質問をするか?」で考える。するなら Client。

2. ツールの与え方:in-process MCP と 外部 MCP サーバ と 組込ツール

種類 何か 具体例 与え方
組込ツール SDK が最初から持つ(Claude Code と同じ) Bash(ex04) / Read/Write/Edit(ex05) / Grep/Glob(ex06) allowed_tools に名前を入れるだけ
in-process MCP(プロセス内) @tool で書いたPython 関数を束ねた MCP サーバ ex02 の add / word_count create_sdk_mcp_servermcp_servers に登録
外部 MCP サーバ(別プロセス) 別プロセスで動く MCP サーバに IPC で接続 (本フォルダでは未使用。連載19/20回が該当) mcp_servers にコマンド/接続情報を渡す

判定基準: 自分の Python 関数を1個ツールにしたいだけ → in-process(ex02)。 既存の外部 MCP サーバ(DB・検索など別言語/別プロセス)を繋ぐ → 外部 MCP。 ツール名の規則: in-process / 外部とも mcp__<サーバ名>__<ツール名>。組込ツールは Bash のように素の名前。

3. 制御の3層:allowed_tools と hooks と can_use_tool(一番混乱する)

3つとも「ツールを使わせるか」に関わるが、層と役割が違う

allowed_tools(静的・名前)   … そもそもどのツールを土俵に乗せるか(ex02)
   │  土俵に乗ったツールが呼ばれようとすると…
hooks: PreToolUse(イベント駆動・広く検査)  … 実行前に任意コードを差し込む(ex04)
   │  hook を通過したら…
can_use_tool(許可判定専用・引数依存)  … allow/deny を返すのが唯一の役割(ex05)
   │  allow なら…
ツール実行
allowed_tools hooks(PreToolUse) can_use_tool
粒度 ツール(静的) イベント(前後)に任意コード ツール名 + 引数(動的)
役割 土俵に乗せる/外す 検査・ログ・ブロックなど何でも 許可判定専用(allow/deny だけ)
返すもの (リストに在るか無いか) 空 dict=通す / permissionDecision:"deny"=止める PermissionResultAllow / PermissionResultDeny
具体例 ["mcp__mytools__add"](ex02), ["Bash"](ex04) block_dangerous_bash(ex04) パスで判定する can_use_tool(ex05)
対応する原理 guardrails ex03 の許可リスト ~/.claudegitleaks-precommit 等 hooks guardrails ex03 の evaluate()

判定基準: - 「このツールは使わせない/使わせる」だけ → allowed_tools - 「実行前にログ取りたい・パターン検知したい・広く差し込みたい」 → hooks(イベント駆動・汎用) - 「許可するか否かを引数の中身で決めたい」 → can_use_tool(判定専用) hook と can_use_tool は排他ではなく重ねて使える(hook で広く検査 → can_use_tool で最終許可判定)。

4. permission_modecan_use_tool — 全体の構え vs 個別判定

permission_mode can_use_tool
エージェント全体の既定の構え(文字列) 1ツール呼び出しごとの動的判定コールバック
例の値 "acceptEdits"(編集を自動承認)等 関数を渡す(ex05 の can_use_tool
関係 既定。can_use_tool が個別に上書きできる 既定より細かく、最終的に効く

~/.claude の permissions 4層(defaultMode / allow / ask / deny)で言えば、 permission_mode ≒ defaultMode(構え)、can_use_tool ≒ allow/deny を引数依存で動的に出す部分、に対応する。

5. サブエージェント — Agent SDK の subagents と multi_agent の Supervisor/Swarm

同じ「サブエージェント」でも狙いが違う。混同しやすいので対比する。

Agent SDK の subagents(ex06) multi_agent/ の Supervisor / Swarm
第一義 コンテキスト分離(親を汚さない) 協調・ハンドオフ(仕事の振り分け)
親に返るもの 最終メッセージだけ 設計次第(途中状態も共有しうる)
子の権限 AgentDefinition.tools で個別に絞る フレームワーク次第
子の子 産めない(tools"Agent" を入れない設計) 設計次第
学んだ場所 context_basics の isolate 戦略 multi_agent/(連載18/23/24回)

判定基準: 「大量の中間出力で親の文脈を汚したくない/トークンを節約したい」が動機なら subagents(分離)。 「複数の役割で協調・引き継ぎしたい」が動機なら Supervisor/Swarm(協調)。

6. メッセージの型(query/receive_response から流れてくるもの)

何を表すか 中身 出てくる場所
AssistantMessage アシスタントの1発話 .content = block のリスト(TextBlock / ToolUseBlock 等) 全 ex
TextBlock テキスト断片 .text 全 ex
ResultMessage ループ終了の合図 最終結果・コスト(total_cost_usd 等) ex01・ex06
parent_tool_use_id(属性) そのメッセージがサブエージェント内の発話か 値があれば子の発話 ex06

7. よくある誤解の訂正

誤解 正しくは
query() は OpenAI の create() と同じで1往復 query()ループ全体を回し、複数往復しうる。過程がメッセージで流れる
ツールを書くには別プロセスの MCP サーバが要る @tool + create_sdk_mcp_serverプロセス内に立てられる(IPC 不要)
ANTHROPIC_API_KEY が必須 基本は claude login のクレデンシャルで動く(API キー明示も可だが必須でない)
hooks と can_use_tool は同じもの hook はイベント駆動で汎用、can_use_tool は許可判定専用。層が違う
allowed_tools があれば中身まで安全 allowed_tools名前の静的許可のみ。中身(パス等)の判定は can_use_tool が要る
サブエージェントの主目的は並列化 第一義はコンテキスト分離。並列化は副次的
サブエージェントから孫を呼べる ex06 の設計では子の tools"Agent" が無いので孫は産めない

モデル指定について: ex06 の model="sonnet" は Claude Code と同じエイリアス指定opus/sonnet/haiku)。 具体的なモデル ID(claude-...-YYYYMMDD)はバージョンで変わるため、断定が要るときは このリポジトリ CLAUDE.md の「最新の Claude モデル」節や公式ドキュメントで裏取りすること。

8. クイック早見表(困ったらここ)

困りごと 見る/使うもの
とりあえず1回 LLM に投げたい query(prompt, options)(ex01)
会話を続けて前ターンを覚えさせたい ClaudeSDKClient + receive_response()(ex03)
自分の Python 関数をツールにしたい @tool + create_sdk_mcp_server + allowed_tools(ex02)
危険コマンドを実行前に止めたい・ログ取りたい PreToolUse hook + HookMatcher(ex04)
書き込み先パスなど引数の中身で許可を決めたい can_use_tool(ex05)
全体の許可の構えをまとめて決めたい permission_mode(ex05)
大量の中間出力で親の文脈を汚したくない agents + AgentDefinition(ex06 / 分離)
サブエージェントの権限を絞りたい AgentDefinition.tools(read-only 等)(ex06)
コスト/トークンを知りたい ResultMessage.total_cost_usd 等(ex01)
query を2回呼んだら文脈が消えた 状態を保つなら ClaudeSDKClient に替える
ツール名が mcp__...__... で長い mcp__<サーバ名>__<ツール名> の規則。allowed_tools に同じ名前で入れる
サブエージェント呼び出しが承認待ちになる 親の allowed_tools"Agent" を入れる
import で PermissionResultAllow が無い SDK バージョン差。dict({"behavior":"allow"})で代用(ex05 がやっている)
エージェントの実行を langfuse でトレースしたい @observe() 手動計装(ex07)。ドロップイン / CallbackHandler は使えない

9. LangChain 非対応エージェントの計装 = @observe() 手動計装(CallbackHandler は使えない)

langfuse の「自動でトレースが生える」経路は 2 つとも前提を持つ:

自動経路 前提 Claude Agent SDK では
langfuse.openai(ドロップイン) 呼び出しが OpenAI SDK であること 効かない(query() は OpenAI SDK ではない)
CallbackHandler LangChain のコールバック機構に乗っていること 効かない(LangChain 非対応の別系統)

判定基準: 計装したいコードは OpenAI SDK か LangChain か? どちらでもないなら、自動経路は諦めて @observe() 手動計装にする。@observe() は関数を包むだけなので SDK の種類を問わない。 LLM ステップは start_as_current_observation(as_type="generation") で generation にして、 usage / cost は ResultMessage から手で update() する(ドロップインが自動で詰めてくれた部分を肩代わり)。 user_id / session_id は propagate_attributes で trace に伝播(LangChain なら CallbackHandler の metadata でやることを、非対応 SDK では自分で with を開く)。具体は ex07。


学んだこと(要点)

  • モデル ≠ エージェント。エージェントに見えるのは周りのハーネス(ループ + ツール + 権限 + 分離)のおかげ。 Claude Agent SDK は、その普段使いの Claude Code のハーネスを Python から自作する SDK。
  • query()ループ全体をメッセージのストリームで返す。OpenAI SDK の「1往復」とは別物。 返るのは型付きメッセージ(AssistantMessage / ResultMessage …)で、isinstance で分岐して読む。
  • ツールは @tool + create_sdk_mcp_serverプロセス内 MCP にできる。サブプロセスも IPC も不要。
  • 状態を保ちたいかで query()(使い切り)と ClaudeSDKClient(会話継続)を選ぶ。
  • 権限・制御は3層allowed_tools(名前・静的)/ hooks(イベント駆動・汎用検査)/ can_use_tool(引数依存・許可判定専用)。 hook と can_use_tool は重ねて使える。これは ~/.claude の hooks + permissions 4層と同じ構図。
  • 構造的防御 > 確率的防御:hook / can_use_tool はコードで決定論的に止めるので、プロンプト注入されても効く。
  • サブエージェントの第一義はコンテキスト分離multi_agent/ の Supervisor/Swarm(協調)とは狙いが違う。
  • 認証は claude login(CLI のクレデンシャル)。ハーネスとモデル呼び出しを分け、認証をハーネス側に寄せた設計。

拡張アイデア(最低3案)

  1. ex02 のツールに失敗系を足すadd に文字列が来たら {"content":[...], "is_error": true} を返し、 モデルが「ツールが失敗した」をどう受けてリトライ/別経路に行くかを観察する。エラーの伝わり方を体感できる。
  2. ex04 の hook を PostToolUse / Stop に拡張:実行に結果を検査する PostToolUse、応答完了時の Stop hook を足し、 ~/.claude/settings.json の各イベントが SDK でどう書けるかを対応づける。
  3. ex05 の can_use_tool を「ask」相当に:deny でも allow でもなく、標準入力でユーザーに確認を取る human-in-the-loop を挟み、~/.claude permissions の ask 層を再現する。
  4. ex06 にレビュアーと別の writer サブエージェントを追加し、親が「writer に書かせ → reviewer にレビューさせる」 2段委譲をするよう仕向ける。multi_agent/ の協調と、分離の subagents の違いを同じコードで比較する。
  5. ex01 で print(message) を有効化して全メッセージ型をダンプし、ToolUseBlock / ToolResultBlock 等 テキスト以外の block 型を一覧化する。型のカタログを自作するとデバッグが速くなる。
  6. ex02 の in-process MCP を外部 MCP サーバに置き換え、同じツールを別プロセスで立てて mcp_servers に接続。 in-process と外部で allowed_tools の書き方が同じ(mcp__...)であることを確認する(連載19/20回との接続)。

既知の注意点

  • import 経路のバージョン差: PermissionResultAllow / PermissionResultDeny は SDK のバージョンで import 経路が変わることがある(ex05 は try/except で dict 代用にフォールバックしている)。 同様に ResultMessage の属性名(コスト等)も getattr で防御的に読むのが安全(ex01)。
  • モデル ID の断定回避: model="sonnet" 等のエイリアスは安定だが、実モデル ID(claude-...-YYYYMMDD)は 時期で変わる。ID を書くときは公式ドキュメント / CLAUDE.md で裏取りする。
  • 認証: claude login 未実施だと動かない。.env.op は不要(このフォルダだけ認証方式が違う)。

記事参照

  • 連載対応なし(Anthropic 公式 OSS。連載25〜33回が DSPy/LangChain v1 へ進む流れとは別軸)。
  • 公式: Claude Agent SDK(Python) https://platform.claude.com/docs/en/agent-sdk/python
  • サブエージェント: https://code.claude.com/docs/en/agent-sdk/subagents
  • ロードマップ: ../../agent-design/STUDY_NOTES.md(STEP 2 の発展)
  • 関連レクチャー: guardrails_basics/(hook/権限), context_basics/(isolate), multi_agent/(協調との対比), software-design/19・20(MCP)

作成: 2026-06-12 / 最終更新: 2026-06-12