Agent SDKのエージェントループを理解する — ターン・メッセージ・コンテキストウィンドウ
Agent SDKはClaude Codeと同じ実行ループで動きます。ターンとメッセージの数え方、上限オプションの置きどころ、コンテキストの消費源と自動圧縮の挙動を解説します。
Agent SDKが動かしているのはClaude Codeと同じループ
Agent SDKで query() を呼ぶと、内部ではClaude Codeを動かしているのと同じ実行ループが回ります。Claudeがプロンプトを評価し、ツールを呼び、結果を受け取り、ツール呼び出しのない応答が出るまで繰り返す仕組みです。TypeScript版・Python版のどちらもClaude Codeのネイティブバイナリを同梱しています。そのため多くの導入形態では、Claude Code本体を別途インストールしなくても動きます(一部の環境では別途インストールが必要です)。
上限をどこにかけるか、何が ResultMessage の subtype に出るか、コンテキストが何で埋まるか。この3点を押さえると、max_turns や max_budget_usd の置きどころが判断できます。
ループは5段階で進む
1回のセッションの流れ
- 1
プロンプトを受け取る
システムプロンプト・ツール定義・会話履歴とともにプロンプトが渡されます。SDKはセッションメタデータを持つ
SystemMessage(subtype"init")を返します。 - 2
評価して応答する
Claudeが現在の状態を評価し、テキストで応答するか、ツール呼び出しを要求するか、その両方を行います。SDKは
AssistantMessageを返します。 - 3
ツールを実行する
SDKが要求されたツールを実行し、結果をClaudeに返します。フックでツール呼び出しを横取り・改変・ブロックできるのはこの段階です。
- 4
繰り返す
ステップ2と3が1サイクル(=1ターン)です。Claudeがツール呼び出しを含まない応答を出すまで続きます。
- 5
結果を返す
最終の
AssistantMessageの後に、テキスト結果・トークン使用量・コスト・セッションIDを持つResultMessageが返ります。
ターンはどこまでを数えるのか
ターンは、ループの中の1往復です。Claudeがツール呼び出しを含む出力を生成し、SDKがそのツールを実行し、結果が自動的にClaudeへ戻ります。この間、ユーザーのコードに制御は戻りません。
「auth.tsの失敗しているテストを直して」というプロンプトなら、次の4ターンで終わる流れが公式の例です。
| ターン | Claudeの行動 | SDKが返すメッセージ |
|---|---|---|
| ターン1 | Claudeの行動Bash で npm test を実行し、3件の失敗を確認 | SDKが返すメッセージAssistantMessage(ツール呼び出し)→ UserMessage(実行結果) |
| ターン2 | Claudeの行動Read で auth.ts と auth.test.ts を読む | SDKが返すメッセージ呼び出しごとに AssistantMessage |
| ターン3 | Claudeの行動Edit で修正し、Bash で再テスト。3件とも成功 | SDKが返すメッセージ呼び出しごとに AssistantMessage |
| 最終ターン | Claudeの行動ツール呼び出しなしのテキスト応答で完了報告 | SDKが返すメッセージAssistantMessage → ResultMessage |
数えるのはツール呼び出しを伴うターンだけで、最終のテキスト応答は含まれません。上の例で max_turns=2 を指定すると、Edit に入る前でループが止まります。max_budget_usd は支出額を基準にした同種の上限です。どちらも既定は無制限です。max_budget_usd にはサブエージェントの支出も含まれます。上限到達後のサブエージェント起動は Budget limit reached で失敗します(v2.1.217以降)。上限に達すると ResultMessage の subtype が error_max_turns か error_max_budget_usd になります。両方を設定すれば先に達したほうでループが止まり、subtype を見れば原因を区別できます。
「ターン」は軽い質問なら1〜2回、リファクタリングのような作業なら数十回のツール呼び出しにまたがることもあります。
メッセージの5つの型と、1メッセージ1ブロックの注意点
ループが進むあいだ、SDKは型付きのメッセージをストリームで返します。
-
SystemMessage: セッションのライフサイクルイベント。主なsubtypeは次の4つです"init": 開始時のメタデータ"compact_boundary": 圧縮の後に届く"informational": 状態バナー"worker_shutting_down": ホストの終了やRemote Controlの切断
フックやバックグラウンドタスクなどのイベントも、別のsubtypeで流れます。起動時に
SessionStartやSetupのフックが走ると、そのライフサイクルメッセージはinitより先に届きます -
AssistantMessage: Claudeの応答に含まれるコンテンツブロック1つにつき1件発生します。最終のテキストのみの応答も同じです。1回の応答から出たメッセージは同じメッセージIDを共有します -
UserMessage: ツール実行のたびに、その結果を載せて発生します。ループの途中で入力をストリーミングしたときも、この型で返ります -
StreamEvent: 部分メッセージを有効にしたときだけ発生する、生のAPIストリーミングイベントです -
ResultMessage: ループの終了を示し、subtypeでタスクが成功したか上限に達したかを判定します
「1ブロック1件」は件数の数え方に効きます。1つの応答がテキストとツール呼び出し2件を含めば、AssistantMessage は3件流れます。「ターン数=AssistantMessage の件数」と数えると合いません。ターン数の目安は ResultMessage の num_turns で読みます。max_turns は大きめの値で試し、num_turns を見てから絞ると、想定より早い停止を避けられます。思考中の進捗を出したいときは、thinking_tokensメッセージの使い方が参考になります。
もう1つ、ResultMessage が最後とは限りません。prompt_suggestion のような少数のシステムイベントが後ろに届くことがあるため、結果を見た時点で break せず、ストリームを最後まで読み切る必要があります。最終結果だけでよければ ResultMessage、進捗表示なら AssistantMessage、逐次表示なら部分メッセージを有効にして StreamEvent を処理します。ストリーミング出力の詳細は別記事にあります。
1ターンで複数のツールが呼ばれたとき
同じターンの中での実行順
並行して動く
Read / Glob / Grep、および読み取り専用と指定されたMCPツールです。カスタムツールも、アノテーションに readOnlyHint を設定すれば仲間に入れられます。
逐次で動く
Edit / Write / Bash です。競合を避けるための扱いで、カスタムツールも既定ではこちらです。
ビルトインツールはClaude Code本体と共通で、ファイル操作・検索・実行・Web・サブエージェントやSkillの起動などが揃っています。ToolSearch もビルトインの1つで、ツールを先読みせず必要になった時点で探して読み込みます。
どのツールが実際に走るかは3つの設定で決まります。allowed_tools は列挙したツールを自動承認するだけで、リストに無いツールが使えなくなるわけではありません。リスト外のツールは、承認が要る呼び出しがパーミッションモードと canUseTool に回ります。使わせたくないツールは disallowed_tools で指定します。こちらは他の設定にかかわらずブロックされます。ツールが拒否されると、Claudeは拒否メッセージをツール結果として受け取り、別の方法を試すか、進められないと報告します。評価順などの詳細は権限の解説にあります。
permission_mode は、承認を人間にどこまで求めるかを決めます。
| モード | 挙動の要点 |
|---|---|
default | 挙動の要点承認が要る呼び出しは canUseTool に回り、コールバックが無ければ拒否 |
acceptEdits | 挙動の要点ファイル編集と mkdir / touch / mv / cp などを自動承認。ほかの Bash は通常のルールのまま |
plan | 挙動の要点編集せずに計画だけ立てる。ファイル編集は自動承認されない |
dontAsk | 挙動の要点一切プロンプトを出さない。許可ルールで承認済みの呼び出しと、default でも承認不要な呼び出し(作業ディレクトリ内のファイル読み取りなど)だけが走り、プロンプトが要るものはすべて拒否 |
auto | 挙動の要点モデルの分類器がシェルコマンドやネットワーク要求を審査して許可・ブロック |
bypassPermissions | 挙動の要点許可済みツールを確認なしで実行。ただし ask ルールに一致するもの等は例外。TypeScriptでは allowDangerouslySkipPermissions: true も必要で、Unixのrootでは使えない |
bypassPermissions はCI・コンテナ・隔離環境向けと位置づけられています。TypeScriptで permissionMode を省略すると、autoモードで始まる場合があります。
effort は、1回の応答でClaudeが使う推論量を決めます。値は low / medium / high / xhigh / max で、モデルによっては対応していません。未指定ならClaude Code側が決めます。query() のオプションならセッション全体に、AgentDefinition の effort ならサブエージェント単位に効きます。max_turns や max_budget_usd が上限で止める仕組みであるのに対し、effort は各応答の推論の深さと、レイテンシ・トークンコストのバランスを変える設定です。似た名前の task_budget はさらに別物です。強制停止ではなく、モデル自身への助言的な予算感覚です(task_budgetは強制上限でなくadvisoryという性質)。
SDKの制御オプションはCLIでどこまで再現できるか
SDKはClaude Codeのバイナリを動かしているので、制御オプションの多くはCLIのフラグにも対応します。v2.1.287の claude --help には、ループ制御に関わる次の行があります。
claude --version
2.1.287 (Claude Code)
claude --help # 抜粋
--autocompact <auto|tokens> Auto-compact window size (auto, or 100k–1M tokens)
--effort <level> Effort level for the current session (low, medium, high, xhigh, max)
--max-budget-usd <amount> Maximum dollar amount to spend on API calls (only works with --print)
--permission-mode <mode> choices: "acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"
--fork-session When resuming, create a new session ID instead of reusing the original
--no-session-persistence Disable session persistence ... cannot be resumed (only works with --print)
--setting-sources <sources> Comma-separated list of setting sources to load (user, project, local).読み取れる点が3つあります。
--max-budget-usdは--print(非対話)のときだけ効きます。対話セッションにはこの上限が掛かりません--max-turnsは--helpの一覧には出ませんが、CLIリファレンスには--print専用のフラグとして載っています。SDKではmaxTurns/max_turnsが対応します--autocompactには100k〜1Mトークンの範囲指定があり、CLIでは圧縮が起きる窓の大きさを変えられます
--help の --permission-mode には default の代わりに manual が出ますが、manual は default の別名で、どちらの値も使えます。
コンテキストウィンドウは何で埋まるか
コンテキストウィンドウは、セッション中にClaudeが使える情報量の総量です。ターンをまたいでリセットされず、システムプロンプト・ツール定義・会話履歴・ツールの入出力がすべて積み上がります。この蓄積はセッションが終わると失われます。セッションをまたいで持ち越すには、Managed Agentsのメモリストアのような別の仕組みが要ります。
| 消費源 | 読み込まれるタイミング | 影響 |
|---|---|---|
| システムプロンプト | 読み込まれるタイミング毎リクエスト | 影響固定の小さなコスト |
| CLAUDE.mdファイル | 読み込まれるタイミングセッション開始時(settingSources 経由) | 影響毎リクエストで全文が乗るが、プロンプトキャッシュにより初回以降は低コスト |
| ツール定義 | 読み込まれるタイミング毎リクエスト。MCPのスキーマは既定で遅延読み込み | 影響ビルトインのスキーマは毎回乗る。ツール検索がMCPのスキーマ読み込みを後回しにする。未対応モデルや一部プラットフォームでは先読みに戻る |
| 会話履歴 | 読み込まれるタイミングターンをまたいで蓄積 | 影響プロンプト・応答・ツール入出力が積み上がる |
| Skillの説明文 | 読み込まれるタイミングセッション開始時(settingSources 経由) | 影響短い要約のみ。全文は呼び出し時に読み込まれる |
大きなツール出力は効きます。巨大なファイルを読んだり、冗長なログを出すコマンドを走らせたりすると、1ターンで数千トークンを使うこともあります。ターンをまたいで変わらない部分は自動的にプロンプトキャッシュの対象になり、繰り返し送る分のコストと待ち時間を抑えます。
節約の手段
サブタスクをサブエージェントに任せると、メインのコンテキストはサブタスクの全文でなく、返ってきた要約の分だけ増えます。サブエージェントは会話履歴を持たない状態で始まり(CLAUDE.mdなどプロジェクトレベルのコンテキストは自分で読み込む)、親のターンは見えません。AgentDefinition の tools で使えるツールを絞れば、ツール定義のぶんも減らせます。Managed Agentsにはターン途中で助言だけをもらうadvisorもあります。ただしこれはコンテキストの節約ではなく、判断の質を上げる手段です。
MCPサーバーを複数つなぐと、ツール検索が効かない構成ではサーバーごとの全スキーマが毎リクエストに乗ります。作業を始める前から、コンテキストをかなり使う場合があります。
自動圧縮で何が残り、何が消えるか
コンテキストが上限に近づくと、SDKは古い履歴を要約して空きを作り、直近のやり取りと重要な決定事項は保持して進みます。圧縮の後、ストリームに type: "system"、subtype: "compact_boundary" のメッセージが流れます。TypeScriptではこれが SDKCompactBoundaryMessage という独立した型で、Pythonでは SystemMessage です。
圧縮をまたいだときの扱い
失われうるもの
セッションの早い段階でプロンプトとして与えた細かい指示です。古いメッセージが要約に置き換わるため、細部は保存されない場合があります。
影響を受けないもの
CLAUDE.mdの内容です。毎リクエストで入れ直されるので、圧縮の対象外として扱えます。
恒常的に守らせたいルールは、プロンプトでなくCLAUDE.mdに置くことになります。圧縮の挙動は3通りで調整できます。
- CLAUDE.mdに要約時の保持項目を書く: 圧縮処理はCLAUDE.mdを他のコンテキストと同じように読みます。そこに「現在のタスクの目的」「読んだ・変更したファイルのパス」「テスト結果とエラー」のような保持対象を書いておけます。見出しの名前は自由です
PreCompactフック: 圧縮の前に全文をアーカイブするなどの処理を挟みます。フックにはtriggerフィールド(manualかauto)が渡されます。詳しくはAgent SDKのHooksにあります/compactをプロンプトとして送る: 通常のSDK入力として扱われ、その場で圧縮が起きます
フックはアプリケーション側のプロセスで動き、エージェントのコンテキストを消費しません。PreToolUse フックがツール呼び出しを拒否すると、実行されずにClaudeへ拒否メッセージが返ります。
セッションの再開とフォーク
ResultMessage.session_id を保存しておけば、後から再開できます。再開すると、それまでに読んだファイル・行った分析・実行した操作を含む全コンテキストが復元されます。元のセッションを変えずに別の方法を試したいときは、フォークで分岐できます。Pythonの ClaudeSDKClient は、複数回の呼び出しをまたぐセッションIDを自動で扱います。
ステートレスなコンテナやサーバーレス環境では、session_store / sessionStore のアダプターでトランスクリプトを自前のバックエンドへ写せます。ただしClaude Codeのサブプロセスは、まずローカルのディスクに書き込みます。
CLI側にも対応するフラグがあります。上のv2.1.287の出力では、--fork-session が再開時に新しいセッションIDを発行します。--no-session-persistence は保存を止め、再開もできなくします(--print のときのみ)。
ループ終了後の結果を扱う
ResultMessage の subtype が、終了状態を判定する主な手がかりです。
| subtype | 意味 | result フィールド |
|---|---|---|
success | 意味ループが完了(最終のモデル呼び出しが失敗した場合は is_error がtrue) | result フィールドあり |
error_max_turns | 意味max_turns の上限に到達 | result フィールドなし |
error_max_budget_usd | 意味max_budget_usd の上限に到達 | result フィールドなし |
error_during_execution | 意味リクエストのキャンセルなどのエラーでループが中断 | result フィールドなし |
error_max_structured_output_retries | 意味構造化出力が、リトライ上限内に有効な形にならなかった。全試行が検証で落ちた場合と、モデルのフォールバックで完成済みの出力が撤回されて再試行も成功しなかった場合がある | result フィールドなし |
result は success のときだけ存在するので、読む前に subtype を確認します。最終のAPI呼び出しが失敗しても、subtype は success のままです。原因は terminal_reason に api_error などで入るので、is_error と合わせて見ます。どの subtype にも total_cost_usd ・usage ・num_turns ・session_id が付くため、エラーで終わってもコストの追跡と再開ができます。ただし注意点が3つあります。
- セッションがクラッシュすると、最終結果は
error_during_executionになり、コスト関連のフィールドがゼロで、stop_reasonがnullのことがあります。出力後にプロセスは終了します - Pythonでは
total_cost_usd・usage・model_usageがオプショナル型なので、Noneを確認してから読みます usageはメインのループ分だけです。サブエージェントを含む全体の集計にはmodelUsage(Pythonはmodel_usage)を使います。集計の見方はコスト追跡の解説にあります
単発の query() は、エラー結果を返した後に例外を送出します(Reached maximum number of turns のような文言が付きます)。この送出は仕様で、基盤のClaude Codeプロセスも非ゼロで終了します。処理を続けたいときは、ループをtry節で囲みます。ストリーミング入力のセッションなら生きたままで、メッセージを送り続けられます(クラッシュ時を除く)。
ResultMessage には stop_reason もあります。end_turn(正常終了)、max_tokens(出力トークン上限)、refusal(モデルが依頼を断った)が代表的な値です。拒否の検出は stop_reason が refusal かどうかで行います。
上限の置きどころは何で決めるか
範囲が明確な修正タスクなら、max_turns で往復回数を絞るほうが予測しやすくなります。「このコードベースを改善して」のような曖昧なタスクでは、ターン数より支出で歯止めをかける max_budget_usd のほうが実害を抑えやすい構成です。Managed Agentsのセッション予算も同じくドル建ての上限です。違いは、上限に達してもセッションを終了せず idle で止まり、予算の変更や解除で自動的に再開する点です。
SDKの max_budget_usd は、止まった後に subtype で区別でき、session_id を使って再開もできます。ストリーミング入力で max_turns に達したときは、キューに残ったメッセージが次の新しいターンで処理され、ターン数のカウントは0から数え直されます。予算は累積します。
よくあるつまずき
dontAskにしたのに質問ツールが動かない:AskUserQuestionや、requiresUserInteractionを付けたMCPツールは、許可していても拒否されます- 単発の
query()で上限到達後にスクリプトが落ちる: 例外の送出は仕様なので、ループをtry節で囲みます - セッションIDの取り出し方が言語で違う: TypeScriptはinitの
SystemMessageの直接フィールド、PythonはSystemMessage.dataの入れ子から読みます
まとめ
止める仕組みは5つあり、基準も止まった後の姿も違います。
| 仕組み | 止める基準 | 止まった後 |
|---|---|---|
max_turns | 止める基準ツール呼び出しを伴うターンの回数 | 止まった後subtype が error_max_turns |
max_budget_usd | 止める基準支出額(ドル) | 止まった後subtype が error_max_budget_usd。session_id で再開できる |
effort | 止める基準止めない。応答ごとの推論の深さを変える | 止まった後変化なし |
task_budget | 止める基準止めない。モデルへの助言的な予算感覚 | 止まった後変化なし |
| Managed Agentsのセッション予算 | 止める基準支出額(ドル) | 止まった後セッションは終了せず idle。予算の変更や解除で再開 |
読者が選ぶのは、往復回数で絞るか、ドルで絞るかの1点です。範囲の決まった修正なら前者、曖昧な依頼なら後者という分担になります。Claude Codeと同じループなので、圧縮をまたぐ予算はtask_budgetのremainingで予算を引き継ぐ方法も参考になります。