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.97(uv.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_tools に mcp__mytools__add 等を列挙 |
この命名規則のツール名で静的に自動承認する |
観察: モデルは「枝豆の妖精」を
word_count→ 結果に 100 をadd、と順にツールを呼んで繋ぐはず。allowed_toolsからaddを外すと add 呼び出しで承認待ち/拒否になる。ツール名でゲートされる この入口が、ex05 の引数依存の権限制御へ繋がる。
ex03 — ClaudeSDKClient:状態を保つマルチターン¶
何を学ぶか: query() は1依頼を回し切る使い切り。会話を続けたい(前ターンを覚えたまま次を頼む)なら
ClaudeSDKClient。async 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_data に tool_name / tool_input が入る |
| ② | 空 dict を返すと「通す」 | 何も言わなければ素通り。明示的に止めるときだけ dict を返す |
| ③ | permissionDecision: "deny" で実行ブロック |
理由(...Reason)がモデルに伝わり、別の手を考えさせる |
| ④ | HookMatcher(matcher="Bash", hooks=[...]) |
対象ツールに hook を紐づける。"*" で全ツール対象 |
観察:
lsは hook を通過し、rm -rfは deny される。モデルは「削除できなかった」と報告するはず (guardrails ex03 と同じく拒否してもループは続く)。これがまさに~/.claudeのgitleaks-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_attributesを no-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_server → mcp_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 の許可リスト | ~/.claude の gitleaks-precommit 等 hooks |
guardrails ex03 の evaluate() |
判定基準: - 「このツールは使わせない/使わせる」だけ →
allowed_tools- 「実行前にログ取りたい・パターン検知したい・広く差し込みたい」 → hooks(イベント駆動・汎用) - 「許可するか否かを引数の中身で決めたい」 →can_use_tool(判定専用) hook と can_use_tool は排他ではなく重ねて使える(hook で広く検査 → can_use_tool で最終許可判定)。
4. permission_mode と can_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案)¶
- ex02 のツールに失敗系を足す:
addに文字列が来たら{"content":[...], "is_error": true}を返し、 モデルが「ツールが失敗した」をどう受けてリトライ/別経路に行くかを観察する。エラーの伝わり方を体感できる。 - ex04 の hook を PostToolUse / Stop に拡張:実行後に結果を検査する PostToolUse、応答完了時の Stop hook を足し、
~/.claude/settings.jsonの各イベントが SDK でどう書けるかを対応づける。 - ex05 の can_use_tool を「ask」相当に:deny でも allow でもなく、標準入力でユーザーに確認を取る
human-in-the-loop を挟み、
~/.claudepermissions のask層を再現する。 - ex06 にレビュアーと別の writer サブエージェントを追加し、親が「writer に書かせ → reviewer にレビューさせる」
2段委譲をするよう仕向ける。
multi_agent/の協調と、分離の subagents の違いを同じコードで比較する。 - ex01 で
print(message)を有効化して全メッセージ型をダンプし、ToolUseBlock/ToolResultBlock等 テキスト以外の block 型を一覧化する。型のカタログを自作するとデバッグが速くなる。 - 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