Claude Media
Agent SDKのエージェントループを理解する — ターン・メッセージ・コンテキストウィンドウ

Agent SDKのエージェントループを理解する — ターン・メッセージ・コンテキストウィンドウ

Agent SDKはClaude Codeと同じ実行ループで動きます。ターンとメッセージの単位、コンテキストウィンドウの消費源、自動圧縮の仕組みまで内部モデルを整理しました。

Agent SDKが動かしている実行ループの正体

Agent SDKで query() を呼ぶと、内部ではClaude Codeを動かしているのと同じ実行ループが回ります。Claudeがプロンプトを評価し、ツールを呼び、結果を受け取り、これを繰り返して初めてタスクが完了する仕組みです。TypeScript版・Python版のどちらもClaude Codeのネイティブバイナリを同梱しており、多くの導入形態では別途Claude Code本体をインストールしなくても動きます(一部の環境では別途インストールが必要です)。

このループが吐き出すメッセージの種類、1回の往復である「ターン」の数え方、そしてコンテキストウィンドウが何によって埋まっていくかを把握しておくと、max_turnsmax_budget_usd のような制御オプションをどこに設定すべきかが判断できるようになります。

ループの5ステップ

エージェントセッションは毎回同じサイクルをたどります。

  1. プロンプトを受け取る: システムプロンプト・ツール定義・会話履歴とともにプロンプトが渡され、SDKはセッションメタデータを持つ SystemMessage(subtype "init")を返す
  2. 評価して応答する: Claudeが現在の状態を評価し、テキストで応答するか、1つ以上のツール呼び出しを要求するか、あるいはその両方を行う。SDKは AssistantMessage を返す
  3. ツールを実行する: SDKが要求されたツールを実行し、結果をまとめてClaudeに返す。フックでツール呼び出しを横取り・改変・ブロックできる
  4. 繰り返す: ステップ2と3が1サイクル(=1ターン)として繰り返される。Claudeがツール呼び出しを含まない応答を出すまで続く
  5. 結果を返す: 最終的な AssistantMessage の後、テキスト結果・トークン使用量・コスト・セッションIDを含む ResultMessage が返る

「このディレクトリに何があるか」のような軽い質問なら Glob を1回呼んで答えるだけの1〜2ターンで終わります。「認証モジュールをリファクタリングしてテストを更新する」のような複雑なタスクは、ファイルの読み込み・編集・テスト実行を繰り返しながら、数十回のツール呼び出しにまたがることもあります。

ターンとメッセージの数え方

ターンとは、ループの中の1往復のことです。Claudeがツール呼び出しを含む出力を生成し、SDKがそのツールを実行し、結果が自動的にClaudeへフィードバックされる、この一連がユーザーのコードに制御を戻さないまま完結します。ツール呼び出しを含まない出力が出た時点でループが終わり、最終結果が返ります。

「auth.tsの失敗しているテストを直して」というプロンプトを例にすると、次の4ターンで完結する流れになります。

ターンClaudeの行動返るメッセージ
ターン1Claudeの行動Bashnpm test を実行し、3件の失敗を確認返るメッセージAssistantMessageUserMessage(実行結果)
ターン2Claudeの行動Readauth.tsauth.test.ts を読む返るメッセージAssistantMessage
ターン3Claudeの行動Edit で修正し、Bash で再テスト。3件とも成功返るメッセージAssistantMessage
最終ターンClaudeの行動ツール呼び出しなしのテキスト応答で完了報告返るメッセージAssistantMessageResultMessage

max_turns はツール呼び出しを伴うターンだけを数えます。上の例で max_turns=2 を指定すると、Edit(修正)に入る前でループが止まります。max_budget_usd は支出額を基準に同様の上限を設ける仕組みで、両方とも上限に達すると、ResultMessageerror_max_turns または error_max_budget_usd というsubtypeで返ります。上限を指定しない場合、ループはClaudeが自力で終えるまで動き続けます。

メッセージの5つの型

ループが進むあいだ、SDKは型付きのメッセージをストリームで返し続けます。中核となるのは次の5種類です。

  • SystemMessage: セッションのライフサイクルイベント。"init"(セッション開始時のメタデータ)、"compact_boundary"(自動圧縮の発生)、"informational"(状態バナー)、"worker_shutting_down"(現在のターンを最後にループが終わる)の4つのsubtypeを持つ
  • AssistantMessage: Claudeが応答するたびに発生し、最終のテキストのみの応答も含む。テキストとツール呼び出しの両方を含みうる
  • UserMessage: ツール実行のたびに、その結果とともに発生する。ループの途中でユーザー入力をストリーミングした場合もこの型で返る
  • StreamEvent: 部分メッセージを有効にしたときだけ発生する、生のAPIストリーミングイベント
  • ResultMessage: ループの終了を示す。最終テキスト・トークン使用量・コスト・セッションIDを持ち、subtype でタスクが成功したか上限に達したかを判定する

何を扱うかは用途で決まります。最終結果だけでよいなら ResultMessage を、進捗を逐次見せたいなら AssistantMessage を、リアルタイムのストリーミング表示が要るなら部分メッセージを有効にして StreamEvent を処理します。

ツール実行と並列化

ツールはClaudeに「行動する力」を与えます。ツールが無ければClaudeはテキストで応答するだけです。ビルトインツールはClaude Code本体と共通で、ファイル操作(Read / Edit / Write)・検索(Glob / Grep)・実行(Bash)・Web(WebSearch / WebFetch)・サブエージェントやSkillの起動などが揃っています。

1ターンで複数のツール呼び出しが要求されたとき、読み取り専用のツール(Read / Glob / Grep や読み取り専用指定のMCPツール)は並行実行され、状態を変更するツール(Edit / Write / Bash)は競合を避けるため逐次実行されます。カスタムツールは既定で逐次実行ですが、アノテーションに readOnlyHint を設定すると並列実行の対象にできます。

どのツールが実際に走るかは、allowed_tools で自動承認するツールを指定し、disallowed_tools で問答無用でブロックするツールを指定し、permission_mode でどこまで人間の承認を挟むかを決める、この3つの組み合わせで決まります。ツールが拒否されると、Claudeは拒否メッセージをツール結果として受け取り、別のアプローチを試みるか、進められないと報告します。

ループ全体の制御という観点では effort も見逃せないオプションです。これは1回の応答でClaudeが費やす推論量(トークン数)を調整するオプションで、低いeffortは1ターンあたりのトークン消費を減らしコストを下げます(モデルによっては対応していません)。max_turnsmax_budget_usd が上限到達時にループを強制停止させる仕組みであるのに対し、effort は各応答の考える深さそのものを決める仕組みという違いがあります。

コンテキストウィンドウを消費するもの

コンテキストウィンドウは、セッション中にClaudeが使える情報量の総量です。ターンをまたいでリセットされることはなく、システムプロンプト・ツール定義・会話履歴・ツールの入出力がすべて積み上がっていきます。

消費源読み込まれるタイミング影響
システムプロンプト読み込まれるタイミング毎リクエスト影響固定の小さなコスト
CLAUDE.mdファイル読み込まれるタイミングセッション開始時(settingSources 経由)影響毎リクエストで全文が乗るが、プロンプトキャッシュにより初回以降は低コスト
ツール定義読み込まれるタイミング毎リクエスト。MCPのスキーマは既定で遅延読み込み影響ビルトインツールのスキーマは毎回乗る。ツール検索を使うとMCPツールのスキーマ読み込みを後回しにできる(未対応モデルや一部プラットフォームでは、この先読み省略が効かず従来どおりの一括読み込みに戻る)
会話履歴読み込まれるタイミングターンをまたいで蓄積影響ターンが増えるほど、プロンプト・応答・ツール入出力が積み上がる
Skillの説明文読み込まれるタイミングセッション開始時(settingSources 経由)影響短い要約のみ。全文は呼び出し時に初めて読み込まれる

大きなツール出力はコンテキストを大きく消費します。巨大なファイルを読んだり、詳細なログを出すコマンドを実行したりすると、1ターンだけで数千トークンを使うこともあります。システムプロンプトやツール定義、CLAUDE.mdのようにターンをまたいで変わらない内容は自動的にプロンプトキャッシュの対象になり、繰り返し送る部分のコストと待ち時間を抑えます。

コンテキストが上限に近づいたときの自動圧縮

コンテキストウィンドウが上限に近づくと、SDKは自動的に会話を圧縮します。古い履歴を要約してスペースを空け、直近のやり取りと重要な決定事項は保持したまま進みます。この圧縮が起きたタイミングでは、type: "system"subtype: "compact_boundary" のメッセージがストリームに流れます。

圧縮の挙動はいくつかの方法でカスタマイズできます。CLAUDE.mdに要約時に保持してほしい項目を書いておくと、圧縮処理はそれを他のコンテキストと同様に読み込んで反映します。PreCompact フックを使えば、圧縮が起きる前に全文をアーカイブするといったカスタム処理を挟めます。手動で /compact をプロンプトとして送ることもでき、これは通常のSDK入力として扱われます。

長時間動かすエージェントでコンテキストを節約する手段としては、サブタスクをサブエージェントに任せる方法が中心になります。サブエージェントは会話履歴を持たない状態(ただしCLAUDE.mdなどプロジェクトレベルのコンテキストは読み込む)で始まり、親のターンは見えません。最終応答だけがツール結果として親に返るため、メインエージェントのコンテキストはサブタスクの全文ではなく要約の分だけ増えます。

セッションの再開とフォーク

Agent SDKとのやり取りはそれぞれセッションを作るか、既存のセッションを継続します。ResultMessage.session_id からセッションIDを取得しておけば、後から再開できます。再開すると、それまでのターンで読んだファイルや実行した分析、行った操作を含む全コンテキストが復元されます。元のセッションを変更せずに別のアプローチを試したい場合は、セッションをフォークして分岐させることもできます。

ループ終了後の結果を扱う

ループが終わると ResultMessage が何が起きたかを教えてくれます。判定の軸になるのは subtype フィールドです。

subtype意味result フィールド
success意味タスクが正常に完了result フィールドあり
error_max_turns意味max_turns の上限に到達result フィールドなし
error_max_budget_usd意味max_budget_usd の上限に到達result フィールドなし
error_during_execution意味API障害やリクエストのキャンセルなどでループが中断result フィールドなし
error_max_structured_output_retries意味構造化出力が既定のリトライ回数内で有効な形式にならなかったresult フィールドなし

result フィールドは success のときだけ存在するため、読む前に必ずsubtypeを確認します。すべてのsubtypeには total_cost_usdusagenum_turnssession_id が含まれており、エラー終了時でもコストの追跡とセッションの再開が可能です。ただしセッションがクラッシュした場合、最終結果は error_during_execution になり、コスト関連のフィールドがゼロになっていることがあります。

単発の query() 呼び出しは、エラー結果を返した後に例外を送出する仕様です。ループを継続させたい場合はtry節で囲みます。

max_turnsとmax_budget_usdは長時間エージェントのコストをどう変えるか

ここまで見た3つの仕組みは、実はすべて「長時間動くエージェントをどこで止め、何を失わずに済ませるか」という1つの問題に集約されます。

上限そのものの置きどころは、タスクの性質で決めるのが妥当です。範囲が明確な修正タスクなら max_turns で往復回数を絞り込むほうが予測しやすく、逆に「このコードベースを改善して」のような曖昧なタスクではターン数よりも支出で歯止めをかける max_budget_usd のほうが実害を抑えやすくなります。両方を同時に設定し、どちらか先に達した方でループを止めるという運用も可能です。

上限に引っかかったループを再開・分岐させたいときや、逆に上限を設けず長時間走らせたいときに効いてくるのが自動圧縮です。圧縮は会話の古い部分を要約に置き換えるため、セッションの早い段階でプロンプトとして与えた細かい指示は、要約の過程で失われることがあります。一方で、CLAUDE.mdの内容は毎リクエストで再注入される仕組みのため圧縮の影響を受けません。恒常的に守ってほしいルールをその都度のプロンプトに書くのではなくCLAUDE.mdへ退避しておくべきなのは、この非対称性が理由です。

まとめると、長時間エージェントのコスト設計は次の3点に集約できます。①上限(max_turns / max_budget_usd)はタスクの明確さで使い分ける、②圧縮で失われるのはプロンプトの細部であってCLAUDE.mdの内容ではない、③恒常ルールはプロンプトでなくCLAUDE.mdに置く。この3点を押さえておくと、上限設定そのものよりも「何を圧縮に委ねてよいか」の判断のほうが、長時間タスクの成否を左右することが見えてきます。

よくある質問

max_turnsmax_budget_usd を両方設定した場合、どちらが優先されますか

先に到達したほうでループが止まります。ResultMessagesubtype を見れば、error_max_turnserror_max_budget_usd かで判別できます。

自動圧縮が起きたことはどうやって検知できますか

ストリームに type: "system"subtype: "compact_boundary" のメッセージが流れてきた時点で検知できます。PreCompact フックを使えば、圧縮の直前に全文をアーカイブするような処理も挟めます。

effort を上げると必ず精度が上がりますか

effort は1回の応答あたりの推論量(トークン消費)を調整するオプションで、往復回数やターン数を直接増減させる仕組みではありません。値を上げるほど1ターンごとのトークン消費が増えてコストが上がるため、タスクの複雑さに見合っているかを見ながら調整するのが基本です(モデルによっては対応していません)。

サブエージェントを使うとコンテキストはどれだけ節約できますか

サブエージェントは親の会話履歴を持たない状態から始まり、最終応答だけが親に返るため、メインエージェントのコンテキストはサブタスクの全文ではなく要約の分だけ増えます。節約量はサブタスクの内容次第ですが、大きなファイルを読み込むような作業ほど効果が出やすくなります。

よくあるつまずき

  • max_turns を小さく設定しすぎて途中で止まる: ツール呼び出しを伴うターンだけがカウント対象なので、想定より早く上限に達することがあります。まずは大きめの値で試し、実際のターン数を見てから絞り込む方が安全です。
  • error_during_execution でコストがゼロに見える: セッションがクラッシュした場合、コスト関連のフィールドがゼロになっていることがあります。実際のコストはAPI側のログと突き合わせて確認します。

まとめ

Agent SDKのエージェントループは、Claude Codeと同じ「評価 → ツール実行 → 結果反映」のサイクルで動きます。ターンはツール呼び出しを伴う1往復として数え、max_turnsmax_budget_usd はこの単位で上限をかけます。コンテキストウィンドウはシステムプロンプト・CLAUDE.md・ツール定義・会話履歴の積み上げで消費され、上限が近づくと自動圧縮が働きます。長時間タスクを設計するときは、サブエージェントへの分割とプロンプトキャッシュの活用が、コンテキストとコストの両方を抑える基本線になります。

この記事を共有:XはてブLinkedIn