Agent SDK のエージェントループ:ターン・メッセージ・結果の扱い

Claude Agent SDK エンジニアリング 7分で読めます

Claude Agent SDK でエージェントを起動すると、Claude Code と同じ実行ループが動きます。Claude がプロンプトを評価し、ツールを呼んで結果を受け取り、作業が終わるまでそれを繰り返す仕組みです。ループの中で何が起きているかを知っておくと、エージェントの作り込みやデバッグ、コストの最適化がしやすくなります。

この記事では、公式ドキュメント「How the agent loop works」に沿って、ターンとメッセージの流れ、ツールの実行と権限、ループを制御するオプション、コンテキストの消費と自動コンパクション、ResultMessage による結果の判定を整理します。SDK の導入から始める場合は「Claude Agent SDK でエージェントを構築する」を先に読んでください。

ループの流れとターン

どのセッションも同じサイクルをたどります。

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

ターンの数え方の例

ターンは、Claude がツール呼び出しを含む出力を出し、SDK がそれを実行し、結果が自動で Claude に戻るまでの1往復です。この間、制御はあなたのコードに戻りません。公式の例「auth.ts の失敗しているテストを直して」では、次のように進みます。

ターンClaude の動き
1Bash で npm test を実行し、3件の失敗を受け取る
2Read で auth.ts と auth.test.ts を読む
3Edit で auth.ts を直し、Bash でテストを再実行して全件成功
最終ツールを呼ばないテキストだけの応答を返す

ツール呼び出しのある3ターンと、テキストだけの最終応答で計4ターンです。max_turns / maxTurns で上限を付けられますが、数えるのはツールを使うターンだけです。たとえば max_turns=2 なら、上の例は編集の前で止まります。上限を付けなければ Claude が自分で終えるまで回り続けるため、「このコードベースを改善して」のような範囲の広いプロンプトでは長引くことがあります。公式は、本番のエージェントでは予算を設定するのがよい既定だとしています。

5つのメッセージ型

ループが進むあいだ、SDK はメッセージのストリームを返します。中心になる型は次の5つです。

型内容
SystemMessageセッションのライフサイクルイベント。subtype で区別し、"init"(セッションのメタデータ)、"compact_boundary"(コンパクション後)、"informational"(ループからの状態表示)、"worker_shutting_down"(ホストの終了や Remote Control の切断)がある
AssistantMessageClaude の応答のコンテンツブロックごとに1つ。テキストやツール呼び出しを1ブロックずつ持ち、同じ応答から出たメッセージはメッセージ ID を共有する
UserMessageツール実行ごとに、Claude へ戻すツール結果を持って届く。ループ途中でストリーミング入力したユーザー入力でも届く
StreamEvent部分メッセージを有効にしたときだけ届く、API の生のストリーミングイベント(テキストの差分、ツール入力の断片)
ResultMessageループの終わり。最終テキスト、トークン使用量、コスト、セッション ID を持ち、subtype で成功か上限到達かを判定する

TypeScript では、"init" 以外のシステムメッセージは SDKSystemMessage のサブタイプではなく、SDKMessage ユニオンの独立した型です。また ResultMessage のあとにも prompt_suggestion などのシステムイベントが少し届くことがあるため、結果を受け取った時点で break せず、ストリームを最後まで回します。

何を拾うかは作るものによる

型の判定は、Python では claude_agent_sdk から import したクラスに対する isinstance()、TypeScript では type 文字列(例 message.type === "result")で行います。TypeScript の AssistantMessage と UserMessage は生の API メッセージを .message に包んでいるので、コンテンツブロックは message.content ではなく message.message.content にあります。

import asyncio
from claude_agent_sdk import query, AssistantMessage, ResultMessage, TextBlock, ToolUseBlock


async def main():
    try:
        async for message in query(prompt="Summarize this project"):
            if isinstance(message, AssistantMessage):
                # AssistantMessage は1つのコンテンツブロックを持つ
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(f"Claude: {block.text}")
                    elif isinstance(block, ToolUseBlock):
                        print(f"Tool call: {block.name}")
            if isinstance(message, ResultMessage):
                if message.subtype == "success":
                    print(message.result)
                else:
                    print(f"Stopped: {message.subtype}")
    except Exception as error:
        # 単発の query() はエラー結果を返したあとに例外を投げる
        print(f"Session ended with an error: {error}")


asyncio.run(main())

ツールの実行と権限

SDK には Claude Code と同じ組み込みツールが入っています。

分類ツールできること
ファイル操作Read、Edit、Writeファイルの読み取り・変更・作成
検索Glob、Grepパターンでファイルを探す、正規表現で中身を検索する
実行Bashシェルコマンド、スクリプト、git 操作
WebWebSearch、WebFetchWeb 検索、ページの取得と解析
探索ToolSearchすべてを先読みせず、必要なツールをその場で探して読み込む
統括Agent、Skill、AskUserQuestion、TaskCreate、TaskUpdateサブエージェントの起動、スキルの呼び出し、ユーザーへの質問、タスクの追跡

このほか、MCP サーバーで外部サービスをつなぐ、カスタムツールを定義する、setting sources でプロジェクトのスキルを読み込む、といった拡張ができます。

何を実行させるかは3つのオプションで決める

"Bash(npm *)" のようなルールで、特定のコマンドだけ許可することもできます。拒否されたツール呼び出しは、Claude に拒否メッセージがツール結果として返り、Claude はたいてい別の方法を試すか、続けられなかったと報告します。詳しい評価順は「Agent SDK の権限設定」を参照してください。

並列実行

1ターンで複数のツール呼び出しが要求されたとき、Read、Glob、Grep、読み取り専用と印の付いた MCP ツールは同時に実行できます。状態を変える Edit、Write、Bash は衝突を避けるため順番に実行されます。カスタムツールは既定で順番に実行され、並列にしたい場合はアノテーションで readOnlyHint を設定します。

ループを制御するオプション

ターン数、コスト、推論の深さ、ツールの承認は、Python の ClaudeAgentOptions、TypeScript の Options のフィールドで指定します。

ターンと予算

オプション制御するもの既定
max_turns / maxTurnsツールを使う往復の最大数上限なし
max_budget_usd / maxBudgetUsdループを止める推定支出額(USD)上限なし

どちらかに達すると、error_max_turns または error_max_budget_usd のサブタイプを持つ ResultMessage が返ります。予算の上限にはサブエージェントの支出も含まれ、上限に達するとそれ以降のサブエージェントの起動は Budget limit reached で失敗し、動いているバックグラウンドのサブエージェントも止められます(この動作は Claude Code v2.1.217 以降)。

支出の比較は、モデルの応答が届いたあとに、API が返すトークン使用量から計算して行われます。そのため上限に達した応答自体は最後まで完了してコストに数えられ、支出はその1応答分(と、その時点で動いていたサブエージェントが止まるまでの分)だけ上限を超えることがあります。上限には余裕を持たせて設定します。

effort(推論の深さ)

値動き向いている作業
"low"最小限の推論で速く答えるファイルの検索、ディレクトリの一覧
"medium"バランスのとれた推論定型の編集、標準的な作業
"high"入念な分析リファクタリング、デバッグ
"xhigh"より深い推論対応モデルでのコーディング・エージェント作業
"max"最大の推論深度深い分析が要る複数手順の問題

effort に対応しないモデルもあります。effort を指定しない場合は Claude Code が自分で決めます。effort は拡張思考(thinking ブロックを出す機能)とは独立しており、effort: "low" で拡張思考を有効にすることも、effort: "max" で無効にすることもできます。セッション全体は query() のオプションで、サブエージェントごとには AgentDefinition の effort で上書きできます。

permissionMode(権限モード)

モード動き
"default"許可ルールにない承認待ちの呼び出しは canUseTool コールバックに回る。コールバックがなければ拒否
"acceptEdits"ファイル編集と mkdir・touch・mv・cp などの一般的なファイル操作コマンドを自動承認。ほかの Bash コマンドは通常どおり
"plan"ソースファイルを編集せず調査と計画だけを行う。ファイル編集は自動承認されず canUseTool に回る
"dontAsk"一切確認しない。権限ルールで事前承認されたものと、承認不要の呼び出しだけが動き、確認が必要になるものはすべて拒否
"auto"モデルの分類器がシェルコマンドやネットワーク要求などを審査し、1件ずつ許可かブロックを決める
"bypassPermissions"明示的な ask ルールなどの例外を除き、許可されたツールを確認なしで実行。TypeScript では allowDangerouslySkipPermissions: true も必要。Unix の root では使えない

公式の目安は、対話型のアプリなら "default" と承認コールバック、手元の開発機で自律的に動かすなら "acceptEdits"、"bypassPermissions" は CI やコンテナなど、エージェントの操作が大事なシステムに影響しない隔離環境に限る、というものです。使うモデルは model オプションで選びます。

コンテキストと自動コンパクション

コンテキストウィンドウは、セッション中に Claude が参照できる情報の総量で、ターンをまたいでもリセットされません。システムプロンプト、ツール定義、会話履歴、ツールの入出力がすべて積み上がります。ターン間で変わらない部分(システムプロンプト、ツール定義、CLAUDE.md)は自動でプロンプトキャッシュされ、繰り返し部分のコストとレイテンシが下がります。

消費元読み込まれる時点影響
システムプロンプト毎リクエスト小さな固定コスト
CLAUDE.mdセッション開始時(settingSources 経由)毎リクエストに全文が入るが、キャッシュされるので全額がかかるのは最初だけ
ツール定義Bash・Read・Edit などの中核ツールは毎リクエスト。MCP ツールなどはツール検索で必要時にツール検索が無効な構成ではすべてのスキーマが先に読み込まれる
会話履歴ターンごとに蓄積プロンプト、応答、ツールの入出力で増え続ける
スキルの説明セッション開始時短い要約だけ。本文は呼び出したときに読み込まれる

大きなファイルの読み取りや出力の多いコマンドは、1ターンで数千トークンを使うことがあります。

自動コンパクション

コンテキストが上限に近づくと、SDK は古い履歴を要約して空きを作ります。直近のやり取りと重要な決定は残ります。このときストリームに type: "system"、subtype: "compact_boundary" のメッセージが流れます(Python では SystemMessage、TypeScript では独立した SDKCompactBoundaryMessage 型)。

要約で置き換えられるため、会話の初めに出した指示が残らないことがあります。守らせ続けたいルールは最初のプロンプトではなく CLAUDE.md に書きます。CLAUDE.md は毎リクエストに入るからです。コンパクションの調整方法は次の3つです。

# Summary instructions

When summarizing this conversation, always preserve:
- The current task objective and acceptance criteria
- File paths that have been read or modified
- Test results and error messages
- Decisions made and the reasoning behind them

コンテキストを節約するには

結果の扱い

ループが終わると、ResultMessage の subtype で何が起きたかを判定します。

subtype意味result の有無
success正常に完了したあり
error_max_turns完了前に maxTurns に達したなし
error_max_budget_usd完了前に maxBudgetUsd に達したなし
error_during_executionリクエストの取り消しなど、エラーでループが中断したなし
error_max_structured_output_retries設定した再試行回数内に有効な構造化出力が得られなかったなし

result は success のときだけあるので、必ず先に subtype を確認します。一方、total_cost_usd、usage、num_turns、session_id はどのサブタイプにもあり、エラー後でもコストを記録したりセッションを再開したりできます。Python ではこれらのうち total_cost_usd、usage、model_usage が Optional 型なので、None でないことを確かめてから読みます。usage はメインのループ分だけなので、サブエージェントを含めた集計には modelUsage(Python は model_usage)を使います(「Agent SDK でコストと使用量を追う」も参照)。

エラー結果のあとの動き

stop_reason には最終ターンでモデルが生成を止めた理由が入り、代表的な値は end_turn(正常終了)、max_tokens(出力トークン上限)、refusal(モデルが依頼を断った)です。拒否を検出するには stop_reason == "refusal" を確認します。クラッシュ後に合成された結果では null になります。セッション ID は ResultMessage.session_id から取り、あとで再開できます(「Agent SDK のセッション」を参照)。

まとめの例

ツールを自動承認し、プロジェクト設定を読み込み、ターン上限と effort を付けて失敗しているテストを直させる公式の例です。結果のサブタイプごとに処理を分け、コストを表示します。

import { query } from "@anthropic-ai/claude-agent-sdk";

let sessionId: string | undefined;

try {
  for await (const message of query({
    prompt: "Find and fix the bug causing test failures in the auth module",
    options: {
      allowedTools: ["Read", "Edit", "Bash", "Glob", "Grep"], // 列挙したツールは確認なしで実行
      settingSources: ["project"], // カレントディレクトリの CLAUDE.md・スキル・フックを読み込む
      maxTurns: 30, // 暴走を防ぐ
      effort: "high" // 複雑なデバッグなので入念に推論させる
    }
  })) {
    // あとで再開できるようにセッション ID を保存
    if (message.type === "system" && message.subtype === "init") {
      sessionId = message.session_id;
    }

    if (message.type === "result") {
      if (message.subtype === "success") {
        console.log(`Done: ${message.result}`);
      } else if (message.subtype === "error_max_turns") {
        // ターンを使い切った。上限を上げて再開する
        console.log(`Hit turn limit. Resume session ${sessionId} to continue.`);
      } else if (message.subtype === "error_max_budget_usd") {
        console.log("Hit budget limit.");
      } else {
        console.log(`Stopped: ${message.subtype}`);
      }
      console.log(`Cost: $${message.total_cost_usd.toFixed(4)}`);
    }
  }
} catch (error) {
  // 単発の query() はエラー結果を返したあとに例外を投げる
  console.log(`Session ended with an error: ${error}`);
}

本ページは How the agent loop works の内容に基づく非公式の日本語解説で、オプション名と ResultMessage のサブタイプは Agent SDK reference - Python の ClaudeAgentOptions と ResultMessage の定義と照合しています(確認日 2026-10-23)。ツール名・フィールド名は公式表記のまま記載しています。最新の仕様は公式ドキュメントを確認してください。