【Claude Code 連載 第77回】エージェントループの仕組み — ターン・メッセージ・コンテキストの実装面

スポンサーリンク
【Claude Code 連載 第77回】エージェントループの仕組み — ターン・メッセージ・コンテキストの実装面 用語解説
【Claude Code 連載 第77回】エージェントループの仕組み — ターン・メッセージ・コンテキストの実装面
この記事は約31分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

スポンサーリンク

背景

この連載では、Claude Codeの公式ドキュメントを1ページずつ読み解いていきます。公式ドキュメントは情報が網羅されている分、「結局どの機能を、どんな場面で使えばいいのか」は自分で考える必要があり、読むのに意外と時間がかかります。そこで、私が実務で使うために読み込んだ内容を「使う場面→実例コード」の順に整理して残していくことにしました。専門家の解説というより、一次情報を読んだ記録です。推測や動作を確認していない部分には、その都度そう書きます。

1. 一言でいうと何か

このページは、Agent SDK を使ったときにループの中で何が起きているかを、メッセージ単位まで降りて説明したものです。

第71回でも agentic ループを扱いましたが、あちらは概念図でした。今回は**「どのメッセージが、いつ、いくつ飛ぶか」**という実装の話です。原文も役割分担を明示しています。agentic ループのより広い概念的な図(SDK 固有ではない)については、Claude Code の仕組みを参照してください。

位置づけの再確認から入ります。Agent SDK を使用すると、Claude Code の自律型エージェントループを独自のアプリケーションに組み込むことができます。SDK はスタンドアロンパッケージで、ツール、権限、コスト制限、および出力をプログラムで制御できます。

導入で1つ実用的な事実があります。TypeScript と Python の両方の SDK には、ネイティブな Claude Code バイナリがバンドルされているため、ほとんどのインストールでは別途 Claude Code をインストールする必要がありません。

**SDK を入れれば Claude Code 本体も付いてくる。**前回(第76回)では触れられていなかった点です。

ループの実体は同じものです。エージェントを開始すると、SDK は Claude Code を支える実行ループと同じものを実行します。Claude はプロンプトを評価し、ツールを呼び出してアクションを実行し、結果を受け取り、タスクが完了するまで繰り返します。

2. どういう場面で役立つか

シーン1:どのメッセージを拾えばいいか決める

**処理するメッセージは、構築しているものによって異なります。**原文が3択で整理しています。**最終結果のみ/進捗更新/ライブストリーミング。**自分のアプリがどれを必要とするかで、書くコード量が変わります。

シーン2:暴走を止める

制限がない場合、ループは Claude が独自に終了するまで実行されます。これは適切にスコープされたタスクには問題ありませんが、オープンエンドのプロンプト(「improve this codebase」)では長時間実行される可能性があります。

そして助言。予算を設定することは、本番エージェントの良いデフォルトです。

シーン3:コンテキスト切れを避ける

**コンテキストウィンドウは、セッション中に Claude が利用できる情報の総量です。セッション内のターン間でリセットされません。すべてが蓄積されます。**長時間走らせるほど効いてきます。

シーン4:終わり方を判定する

**subtype フィールドは、終了状態をチェックする主な方法です。**成功したのか、上限で止まったのか、壊れたのか。5種類の終了があります。

不要・向かないケース

  • **概念だけ知りたい。**それは第71回の範囲です。
  • **エラー結果の後もそのまま処理を続けたい。**単発の query() では例外が飛びます。コードがそれを超えて続行する必要がある場合は、ループを try ブロックでラップしてください。
  • result フィールドを常に読みたい。できません。result フィールドは最終テキスト出力を保持し、success バリアントにのみ存在します。
  • **usage でサブエージェント込みの合計を取りたい。****usage フィールドはメインエージェントループのみをカバーします。**ツリー全体は model_usage / modelUsage。
  • **ルートで bypassPermissions を使いたい。**Unix でルートとして実行する場合は使用できません。
  • **初期プロンプトに恒久ルールを書きたい。**圧縮は古いメッセージを要約に置き換えるため、会話の早い段階からの特定の指示は保持されない可能性があります。永続的なルールは初期プロンプトではなく CLAUDE.md に属します。

3. ループの中身とオプションの解説

fenced ブロックは5本(Python 2・TypeScript 2・Markdown 1)。**すべて原文どおり引用します。**Python と TypeScript は同じ処理の対訳なので、両方を載せます。表は7つあり、いずれも参照用途なので保持します。

前提:予算上限がサブエージェントに及ぶ挙動は Claude Code v2.1.217 以降が必要です。

5ステップのサイクル

すべてのエージェントセッションは同じサイクルに従います。

  1. **プロンプトを受け取る。**Claude はプロンプト、システムプロンプト、ツール定義、および会話履歴とともにプロンプトを受け取ります。SDK はセッションメタデータを含むサブタイプ "init" の SystemMessage を生成します。
  2. **評価して応答する。**Claude は現在の状態を評価し、どのように進めるかを決定します。テキストで応答したり、1つ以上のツール呼び出しをリクエストしたり、その両方を行ったりできます。
  3. ツールを実行する。****SDK は要求された各ツールを実行し、結果を収集します。ツール結果の各セットは次の決定のために Claude にフィードバックされます。フックで実行前に傍受、変更、またはブロックできます。
  4. **繰り返す。**各完全なサイクルは1ターンです。Claude はツール呼び出しと結果の処理を続け、ツール呼び出しのない応答を生成するまで続きます。
  5. **結果を返す。**最終的な AssistantMessage(テキスト応答、ツール呼び出しなし)を生成し、その後に最終テキスト、トークン使用量、コスト、およびセッション ID を含む ResultMessage を生成します。

規模感も示されています。簡単な質問(「ここにはどのようなファイルがありますか?」)は、Glob を呼び出して結果で応答する1〜2ターンで済む場合があります。複雑なタスク(「認証モジュールをリファクタリングしてテストを更新する」)は、多くのターンにわたって数十のツール呼び出しをチェーンできます。

ターンの定義

ここは誤解しやすいので、原文の定義を押さえます。

ターンはループ内の1往復です。Claude はツール呼び出しを含む出力を生成し、SDK はそれらのツールを実行し、結果は自動的に Claude にフィードバックされます。これはコードに制御を戻さずに発生します。

**途中であなたのコードに戻ってこない。**ツールの実行も結果の返送も SDK 内で完結し、ツール呼び出しのない出力が出た時点で初めてループが終わります。

「Fix the failing tests in auth.ts」の実例が載っています。

  1. ターン1:Bash で npm test(3つ失敗)
  2. ターン2:Read で auth.ts と auth.test.ts
  3. ターン3:Edit で修正、Bash で再実行(全て成功)
  4. 最終ターン:ツール呼び出しのないテキスト応答

これは4ターンでした。3つはツール呼び出し、1つは最終テキストのみの応答です。

上限の数え方に注意があります。**max_turns / maxTurns でループをキャップできます。これはツール使用ターンのみをカウントします。**そして具体例——上記のループで max_turns=2 は編集ステップの前に停止していたでしょう。

5つのメッセージタイプ

タイプいつ出るか
SystemMessageセッションライフサイクルイベント。subtype で区別
AssistantMessageClaude の各応答のコンテンツブロックごとに生成(最終テキストのみの応答を含む)
UserMessage**各ツール実行後、Claude に送り返されるツール結果コンテンツとともに生成。**ループ中盤のユーザー入力でも生成
StreamEvent**部分メッセージが有効な場合のみ。**生の API ストリーミングイベント
ResultMessage**エージェントループの終了をマーク。**最終テキスト・トークン使用量・コスト・セッション ID

SystemMessage の subtype は4つ。"init"(セッションメタデータ)/"compact_boundary"(圧縮後に発火)/"informational"(プレーンテキストのステータスバナー)/"worker_shutting_down"(ホストが終了しているか Remote Control が切断)。

TypeScript の型設計には注意点があります。TypeScript では、"init" 以外の各サブタイプは SDKSystemMessage のサブタイプではなく、SDKMessage ユニオン内の独自のタイプです。

そして**AssistantMessage の粒度**が、このページで最も見落としやすい仕様です。

各メッセージは、テキストやツール呼び出しなどの単一のコンテンツブロックを持ち、1つの応答からのメッセージは同じメッセージ ID を共有します。

**1応答=1メッセージではありません。**ブロックごとに分かれて届き、同じ ID を共有します。テキストとツール呼び出しを両方含む応答は、2つの AssistantMessage になります。

ResultMessage の扱いにも作法があります。prompt_suggestion などの少数の末尾システムイベントはその後に到着する可能性があるため、結果で中断するのではなく、ストリームを完了まで反復処理します。

言語ごとの書き分け

判定方法が違います。**Python:claude_agent_sdk からインポートされたクラスに対して isinstance() でメッセージタイプをチェックします。**TypeScript:type 文字列フィールドをチェックします。

TypeScript には落とし穴があります。AssistantMessage と UserMessage は生の API メッセージを .message フィールドでラップするため、コンテンツブロックは message.content ではなく 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):
                # Each AssistantMessage carries one content block
                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:
        # A single-shot query() raises after yielding an error result. If the
        # failure was an error result, the error subtype branches above have
        # already run; connection or process failures yield no result message.
        print(f"Session ended with an error: {error}")


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

try {
  for await (const message of query({ prompt: "Summarize this project" })) {
    if (message.type === "assistant") {
      // Each assistant message carries one content block
      for (const block of message.message.content) {
        if (block.type === "text") {
          console.log(`Claude: ${block.text}`);
        } else if (block.type === "tool_use") {
          console.log(`Tool call: ${block.name}`);
        }
      }
    }
    if (message.type === "result") {
      if (message.subtype === "success") {
        console.log(message.result);
      } else {
        console.log(`Stopped: ${message.subtype}`);
      }
    }
  }
} catch (error) {
  // A single-shot query() throws after yielding an error result. If the
  // failure was an error result, the error subtype branches above have
  // already run; connection or process failures yield no result message.
  console.log(`Session ended with an error: ${error}`);
}

組み込みツール

SDK には Claude Code を支えるのと同じツールが含まれています。

カテゴリツール機能
ファイル操作Read、Edit、Writeファイルを読み取り、変更、作成
検索Glob、Grepパターンでファイルを検索、正規表現でコンテンツを検索
実行Bashシェルコマンド、スクリプト、git 操作を実行
WebWebSearch、WebFetchWeb を検索、ページを取得して解析
検出ToolSearchすべてをプリロードする代わりに、オンデマンドでツールを動的に検索してロード
オーケストレーションAgent、Skill、AskUserQuestion、TaskCreate、TaskUpdateサブエージェントを生成、スキルを呼び出し、ユーザーに質問、タスクを追跡

条件付きのものが1つ。モデルがタスク追跡ツールを取得しない場合、Claude Code は TaskCreate と TaskUpdate をオプトインした場合にのみ提供します。

並列実行のルール

第62回のツールリファレンスでは触れられていなかった仕様です。

読み取り専用ツール(Read、Glob、Grep、読み取り専用としてマークされた MCP ツール)は同時に実行できます。状態を変更するツール(Edit、Write、Bash)は競合を避けるために順序に実行されます。

自作ツールは保守的な既定です。カスタムツールはデフォルトで順序実行されます。カスタムツールの並列実行を有効にするには、その注釈で readOnlyHint を設定します。

ターンと予算

オプション制御内容デフォルト
最大ターン(max_turns / maxTurns)最大ツール使用往復数制限なし
最大予算(max_budget_usd / maxBudgetUsd)停止前の最大コスト制限なし

どちらも既定は制限なし。到達すると対応するエラーサブタイプ(error_max_turns または error_max_budget_usd)を含む ResultMessage を返します。

予算にはサブエージェントも含まれます。それらの支出は合計に計上されます。支出が上限に達すると、別のサブエージェントを生成することは Budget limit reached で失敗し、Claude Code は実行中のバックグラウンドサブエージェントを停止します。(v2.1.217 以降)

ストリーミング入力時の挙動も定義されています。ターンが最大ターン制限で終了するときにまだキューに入っているメッセージは、キューに入ったままになります。そしてメッセージの新しいターンを開始し、そのターンの最大ターン数がリセットされます。

**ターン上限はメッセージごとにリセットされるが、予算は累積し続ける。**この非対称が重要です。支出が maxBudgetUsd に達すると、同じ会話内の後続メッセージは error_max_budget_usd 結果で終了します。リセットしたければ/clear は予算をリセットします。

努力レベル

レベル動作適している用途
"low"最小限の推論、高速応答ファイル検索、ディレクトリのリスト
"medium"バランスの取れた推論ルーチン編集、標準タスク
"high"徹底的な分析リファクタリング、デバッグ
"xhigh"拡張推論深度サポートしているモデルでのコーディングと agentic coding タスク
"max"最大推論深度深い分析が必要な複数ステップの問題

effort を設定しない場合、Claude Code は努力レベルを自身で解決します。そしてすべてのモデルが努力パラメータをサポートしているわけではありません。

混同されやすい点が注記されています。effort は各応答内の推論深度のレイテンシとトークンコストをトレードオフします。Extended thinking は、出力に thinking ブロックを生成する別の機能であり、これらは独立しています。effort: "low" を extended thinking 有効で設定することも、effort: "max" を有効にしないで設定することもできます。

サブエージェント単位でも設定できます。トップレベルの query() オプションでセッション全体に effort を設定するか、AgentDefinition の effort フィールドでサブエージェントごとにセッションレベルをオーバーライドします。

権限モード(SDK 版)

モード動作の要点
"default"許可ルールでカバーされていないツール呼び出しは canUseTool コールバックをトリガーします。コールバックがない場合は拒否
"acceptEdits"**ファイル編集と一般的なファイルシステムコマンド(mkdir、touch、mv、cp など)を自動承認。**他の Bash はデフォルトルールに従う
"plan"**ソースファイルを編集せずに探索して計画を作成。**ファイル編集は canUseTool でプロンプト
"dontAsk"プロンプトしない。事前承認済みと default で承認不要な呼び出しは実行、それ以外は拒否。AskUserQuestion、組織が ask に設定したコネクタツール、requiresUserInteraction の MCP ツールは、許可していても拒否
"auto"モデル分類器を使用して権限プロンプトを承認または拒否
"bypassPermissions"**尋ねずにすべての許可されたツールを実行。**ただし ask ルール一致・組織が ask に設定したコネクタ・ユーザー操作が必要なツールは除く

"default" の挙動が CLI と違う点は押さえるべきです。コールバックがない場合は拒否——つまり canUseTool を実装していない SDK アプリは、既定モードだと何も実行できません。

bypassPermissions には SDK 固有の条件が2つ。**TypeScript SDK では、options で allowDangerouslySkipPermissions: true も必要です。****Unix でルートとして実行する場合は使用できません。**そして用途の限定——エージェントのアクションが気にするシステムに影響を与えられない隔離環境でのみ使用します。

使い分けの結論も書かれています。インタラクティブアプリケーションの場合は、ツール承認コールバックで "default"。開発マシン上の自律型エージェントの場合は "acceptEdits"。CI、コンテナ、またはその他の隔離環境に対して "bypassPermissions" を予約します。

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

ソースロード時期影響
システムプロンプトすべてのリクエスト小さい固定コスト、常に存在
CLAUDE.md ファイルセッション開始時、settingSources 経由すべてのリクエストで完全なコンテンツ(ただしプロンプトキャッシュされるため、最初のリクエストのみが完全なコストを支払う)
ツール定義すべてのリクエスト。MCP スキーマはデフォルトで遅延組み込みツールスキーマは毎回ロード
会話履歴ターン間で蓄積**各ターンで増加。**プロンプト、応答、ツール入力、ツール出力
スキル説明セッション開始時、設定ソース経由短い要約。完全なコンテンツは呼び出し時のみ

第70回と同じ構図ですが、プロンプトキャッシュの言及が加わっています。ターン間で同じままのコンテンツ(システムプロンプト、ツール定義、CLAUDE.md)は自動的にプロンプトキャッシュされ、繰り返されるプリフィックスのコストとレイテンシが削減されます。

警告も明快です。大きなツール出力は大量のコンテキストを消費します。大きなファイルを読み取るか、詳細な出力を含むコマンドを実行すると、単一のターンで数千のトークンを使用できます。

自動圧縮のカスタマイズ

コンテキストウィンドウが制限に近づくと、SDK は会話を自動的に圧縮します。発生時にはtype: "system" と subtype: "compact_boundary" を含むメッセージが流れます。

カスタマイズは3通り。

**CLAUDE.md の要約指示。**圧縮機は他のコンテキストと同様に CLAUDE.md を読むため、要約時に保持する内容を指示するセクションを含めることができます。圧縮機は意図に基づいて一致するため、セクションヘッダーは自由形式です。

# 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

第71回で「CLAUDE.md に Compact Instructions セクションを追加する」とあったものの、具体的な書き方がここで初めて示されました。

**PreCompact フック。**圧縮が発生する前にカスタムロジックを実行します。たとえば、完全なトランスクリプトをアーカイブします。フックは trigger フィールド(manual または auto)を受け取ります。

手動圧縮。****/compact をプロンプト文字列として送信して、オンデマンドで圧縮をトリガーします。この方法で送信されるコマンドは SDK 入力です。

コンテキストを節約する4手

  • **サブタスク用にサブエージェントを使う。**各サブエージェントは新しい会話で開始されます(以前のメッセージ履歴はありませんが、独自のシステムプロンプトとプロジェクトレベルのコンテキスト(CLAUDE.md など)をロードします)。メインエージェントのコンテキストは完全なサブタスクトランスクリプトではなく、その要約で増加します。
  • ツールを選別する。****すべてのツール定義はコンテキストスペースを取ります。AgentDefinition の tools で最小セットに絞る。
  • MCP サーバーコストを監視する。ツール検索がオフだと各 MCP サーバーはすべてのツールスキーマをすべてのリクエストに追加するため、多くのツールを持つ少数のサーバーは、エージェントが何か作業を行う前に大量のコンテキストを消費できます。
  • ルーチンタスクに低い努力を使う。

セッション

ResultMessage.session_id からセッション ID をキャプチャして(両方の SDK で利用可能)、後で再開します。

取得方法に言語差があります。TypeScript SDK は init SystemMessage の直接フィールドとしても公開します。Python では、SystemMessage.data にネストされています。

ステートレス環境向けの仕組みもあります。ステートレスコンテナまたはサーバーレスホスト全体でセッションを再開するには、session_store / sessionStore アダプターを渡して、SDK がトランスクリプトを独自のバックエンドにミラーリングし、別のホストがそれらを再開できるようにします。ただしClaude Code サブプロセスは引き続きローカルディスクに最初に書き込みます。(デュアルライト)

終了状態の5分類

結果サブタイプ何が起こったかresult は利用可能か
successClaude は通常、タスクを完了しましたはい
error_max_turns完了前に maxTurns 制限に達しましたいいえ
error_max_budget_usd完了前に maxBudgetUsd 制限に達しましたいいえ
error_during_executionエラーがループを中断しました(たとえば、キャンセルされたリクエスト)いいえ
error_max_structured_output_retries設定された再試行制限内で有効な構造化出力が生成されませんでしたいいえ

result を読む前に必ずサブタイプを確認する——これが作法です。

救いとしてすべての結果サブタイプは total_cost_usd、usage、num_turns、および session_id を持つため、コストを追跡し、エラー後でも再開できます。

ただし例外が2つ。セッションクラッシュ後、最終結果は error_during_execution であり、そのコストフィールドはゼロになる可能性があり、その stop_reason は null です。そしてPython では、total_cost_usd、usage、および model_usage はオプションとして型付けされているため、読み取る前に None でないことを確認してください。

集計範囲の違いも重要です。usage フィールドはメインエージェントループのみをカバーします。ツリー全体のトークンとコスト会計には、model_usage を使用してください。

エラー時の挙動が SDK の呼び方で変わります。単一ショットの query() 呼び出しは最終結果メッセージを生成し、その後エラーを発生させます。発生は意図的です。一方ストリーミング入力セッションは生きたままで、メッセージを送信し続けることができます。ただし、セッションクラッシュ後は除きます。

stop_reason も用意されています。**一般的な値は end_turn(モデルが通常終了)、max_tokens(出力トークン制限に達した)、および refusal(モデルがリクエストを拒否)です。**拒否を検出するには、stop_reason === "refusal" をチェックしてください。

フック

フック発火時期一般的な用途
PreToolUseツール実行前入力を検証、危険なコマンドをブロック
PostToolUseツール戻り後出力を監査、副作用をトリガー
UserPromptSubmitプロンプト送信時プロンプトに追加コンテキストを注入
Stopエージェント終了時結果を検証、セッション状態を保存
SubagentStart / SubagentStopサブエージェント生成/完了時並列タスク結果を追跡して集約
PreCompactコンテキスト圧縮前要約前に完全なトランスクリプトをアーカイブ

性質が2つ明記されています。フックはエージェントのコンテキストウィンドウ内ではなく、アプリケーションプロセスで実行されるため、コンテキストを消費しません。そしてフックはループをショートサーキットすることもできます。ツール呼び出しを拒否する PreToolUse フックはそれが実行されるのを防ぎ、Claude は代わりに拒否メッセージを受け取ります。

SDK 間の差もあります。TypeScript SDK には、Python がまだサポートしていない追加のイベントが含まれています。

全部入りの例

許可されたツール(自動承認されるため、エージェントが自律的に実行される)、プロジェクト設定、およびターンと推論努力の安全制限でエージェントを構成します。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def run_agent():
    session_id = None

    try:
        async for message in query(
            prompt="Find and fix the bug causing test failures in the auth module",
            options=ClaudeAgentOptions(
                allowed_tools=[
                    "Read",
                    "Edit",
                    "Bash",
                    "Glob",
                    "Grep",
                ],  # Listing tools here auto-approves them (no prompting)
                setting_sources=[
                    "project"
                ],  # Load CLAUDE.md, skills, hooks from current directory
                max_turns=30,  # Prevent runaway sessions
                effort="high",  # Thorough reasoning for complex debugging
            ),
        ):
            # Handle the final result
            if isinstance(message, ResultMessage):
                session_id = message.session_id  # Save for potential resumption

                if message.subtype == "success":
                    print(f"Done: {message.result}")
                elif message.subtype == "error_max_turns":
                    # Agent ran out of turns. Resume with a higher limit.
                    print(f"Hit turn limit. Resume session {session_id} to continue.")
                elif message.subtype == "error_max_budget_usd":
                    print("Hit budget limit.")
                else:
                    print(f"Stopped: {message.subtype}")
                if message.total_cost_usd is not None:
                    print(f"Cost: ${message.total_cost_usd:.4f}")
    except Exception as error:
        # A single-shot query() raises after yielding an error result. If the
        # failure was an error result, the error subtype branches above have
        # already run; connection or process failures yield no result message.
        print(f"Session ended with an error: {error}")


asyncio.run(run_agent())
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"], // Listing tools here auto-approves them (no prompting)
      settingSources: ["project"], // Load CLAUDE.md, skills, hooks from current directory
      maxTurns: 30, // Prevent runaway sessions
      effort: "high" // Thorough reasoning for complex debugging
    }
  })) {
    // Save the session ID to resume later if needed
    if (message.type === "system" && message.subtype === "init") {
      sessionId = message.session_id;
    }

    // Handle the final result
    if (message.type === "result") {
      if (message.subtype === "success") {
        console.log(`Done: ${message.result}`);
      } else if (message.subtype === "error_max_turns") {
        // Agent ran out of turns. Resume with a higher limit.
        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) {
  // A single-shot query() throws after yielding an error result. If the
  // failure was an error result, the error subtype branches above have
  // already run; connection or process failures yield no result message.
  console.log(`Session ended with an error: ${error}`);
}

エージェントが正常に完了すると、この例は Done: 行にエージェントの修正の概要を出力し、その後 Cost: $0.0312 のような行を出力します。

2つの実装を見比べると、セッション ID の取り方が違うのが分かります。Python は ResultMessage.session_id、TypeScript は init の SystemMessage から先に拾っています。前述の言語差が、そのままコードに出ています。

4. まとめ + 次回予告

  • SDK はClaude Code と同じ実行ループを回す。ネイティブバイナリがバンドルされているので、多くの場合 Claude Code を別途入れる必要はない。
  • サイクルは5段階——プロンプト受領 → 評価・応答 → ツール実行 → 繰り返し → 結果返却。
  • ターンは1往復。ツール実行と結果返送はあなたのコードに制御を戻さずに進み、ツール呼び出しのない出力で終わる。
  • max_turns はツール使用ターンのみを数える。
  • メッセージは5種——SystemMessage / AssistantMessage / UserMessage / StreamEvent / ResultMessage。
  • **AssistantMessage はコンテンツブロックごとに1つ。**1応答=1メッセージではなく、同じ ID を共有する複数が届く。
  • **TypeScript では message.message.content。**生 API メッセージが .message でラップされている。
  • **結果が来てもストリームを止めない。**末尾システムイベントが後から届きうる。
  • ツールは**読み取り専用が並列、状態変更が直列。**カスタムツールは既定で直列、readOnlyHint で並列化。
  • **ターン上限も予算も既定は無制限。**本番なら予算を入れる。予算はサブエージェントの支出も含む(v2.1.217 以降)。
  • ストリーミング入力ではターン上限はメッセージごとにリセットされるが、予算は累積し続ける。****/clear で予算がリセット。
  • 努力は5段階(low / medium / high / xhigh / max)。**未設定なら Claude Code が自分で決める。**Extended thinking とは独立。
  • 権限モードは6種。"default" は canUseTool が無いと拒否。****bypassPermissions は TS で allowDangerouslySkipPermissions が別途必要、Unix の root では使えない。
  • コンテキストはターンをまたいでリセットされない。同じ前置きはプロンプトキャッシュが効く。
  • 圧縮のカスタマイズは3通り——CLAUDE.md の要約指示セクション(ヘッダー名は自由)/PreCompact フック//compact をプロンプト文字列として送る。
  • **恒久ルールは初期プロンプトではなく CLAUDE.md へ。**圧縮で消える。
  • セッション ID は**Python が ResultMessage.session_id、TypeScript は init の SystemMessage からも直接。**Python では SystemMessage.data にネスト。
  • ステートレス環境は session_store / sessionStore アダプターでトランスクリプトを自前バックエンドにミラーできる(デュアルライト)。
  • 終了は5分類。result フィールドは success のときだけ存在する。全分類でコストとセッション ID は取れるが、クラッシュ時はコストがゼロ・stop_reason が null になりうる。
  • **usage はメインループのみ。**ツリー全体は model_usage / modelUsage。
  • 拒否の検出は stop_reason == "refusal"。
  • フックはアプリケーションプロセスで走るのでコンテキストを消費しない。PreToolUse の拒否はループをショートサーキットする。

次回予告(暫定):ここまでで概要とループを押さえたので、次は**Agent SDK のクイックスタート(agent-sdk/quickstart)**を取り上げ、インストール・API キー設定・最初のエージェント構築を扱う予定です。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました