Agent SDKのthinking_tokensで思考中の進捗を表示する
SDKThinkingTokensMessageは思考ブロックの生成中に流れる推定トークン数です。進捗バーに使う側と、確定値を読む側の切り分け、サブエージェント分の扱いを実装例つきで示します。
Agent SDKで長く考えるターンを走らせると、画面は数十秒止まったままになります。この空白を埋めるのが type: "system"、subtype: "thinking_tokens" のメッセージです。思考ブロックを生成している間、推定トークン数が流れてきます。
この記事では、受け取りのコード、推定値と確定値の役割分担、サブエージェント分が抜ける点、メッセージの紐づけまでを順に扱います。
SDKThinkingTokensMessageとは何か
SDKThinkingTokensMessage は、Claudeが思考ブロックを生成している間に出るシステムメッセージです。リダクトされた(中身が隠れた)思考ブロックでも出ます。TypeScript SDKのリファレンスにある型は次のとおりです。
type SDKThinkingTokensMessage = {
type: "system";
subtype: "thinking_tokens";
estimated_tokens: number;
estimated_tokens_delta: number;
user_message_uuid?: string;
uuid: UUID;
session_id: string;
};要件はClaude Code v2.1.153以降です。SDKMessage のユニオンにも含まれるため、query() のループにそのまま流れてきます。
2つの数値の意味
estimated_tokens
現在の思考ブロックで、ここまでに生成された思考トークンの推定値です。累計に近い値で、ブロックが進むほど増えます。
estimated_tokens_delta
このフレームが運ぶ増分です。バーを少しずつ伸ばすときに使います。
どちらも進捗表示のための推定値です。請求や集計の根拠にはしません。
受け取って進捗を出すコード
query() のループに分岐を1つ足すだけです。次の例は、思考中の推定トークン数を同じ行に上書き表示します。
import { query } from "@anthropic-ai/claude-agent-sdk";
let thinkingTotal = 0; // ブロックをまたいだ推定の合計
const prompt = "この設計の問題点を洗い出して";
for await (const message of query({ prompt })) {
if (message.type === "system" && message.subtype === "thinking_tokens") {
thinkingTotal += message.estimated_tokens_delta;
process.stdout.write(
`\r考え中… このブロック ${message.estimated_tokens} / 合計 ${thinkingTotal}`
);
}
if (message.type === "result") {
process.stdout.write("\n");
console.log(message.usage.output_tokens_details?.thinking_tokens);
}
}estimated_tokens は「現在のブロック」の値なので、ブロックをまたいだ合計が欲しいなら estimated_tokens_delta を足し込む形が書きやすくなります。この合計はアプリ側の工夫で、SDKが合計を渡してくれるわけではありません。
総量が分からない処理なので、パーセント表示のバーにはしにくい点に注意してください。「考え中」のラベルと数字の増加だけでも、固まっていないことは伝わります。
推定値と確定値の切り分け
思考トークンには、役割の違う2系統の数字があります。
| 項目 | 取れる場所 | 取れるタイミング | 用途 |
|---|---|---|---|
estimated_tokens / estimated_tokens_delta | 取れる場所thinking_tokens メッセージ | 取れるタイミング思考の生成中 | 用途進捗表示 |
usage.output_tokens_details.thinking_tokens | 取れる場所結果メッセージ | 取れるタイミングターンの終了時 | 用途確定値の記録 |
モデルまたはプロバイダーが内訳を返すとき、トップレベルのエージェントループの確定値は、結果メッセージの usage.output_tokens_details.thinking_tokens です。ターンが終わるまで確定しないため、途中経過は推定値に頼ることになります。
結果メッセージの thinking_tokens には、読み方の注意が4つあります。
- 課金には使わず、観測用に読みます。
output_tokensが正で、output_tokens - thinking_tokensが非思考の出力のおおよその量です - 内訳が返らないモデルやプロバイダーでは、結果メッセージ上で
0になります - 数える対象は、モデルが生成した生の推論です。思考ブロックの区切りも含み、レスポンス本文に返る思考テキストより長いことがあります
- 生の推論を再トークナイズして数えるため、モデルが実際に生成した数とは数トークンずれることがあります。1トークン単位の一致を前提にした検算は避けます
output_tokens_details はTypeScript SDK v0.3.228以降(Claude Code v2.1.228を同梱)の項目です。古いSDKでは存在しないため、?. で守って読むのが安全です。
サブエージェントの思考は確定値に入らない
結果メッセージの usage.output_tokens_details.thinking_tokens は、トップレベルのエージェントループだけを数えます。サブエージェントの分は含まれません。コスト追跡のページにある、結果メッセージの3項目の違いが手がかりになります。
| フィールド | サブエージェント分 |
|---|---|
usage | サブエージェント分含まれない(トップレベルのみ) |
total_cost_usd | サブエージェント分含まれる |
modelUsage | サブエージェント分含まれる(モデル別) |
サブエージェントを多用するアプリで「思考にいくら使ったか」を出したい場合、usage の thinking_tokens だけでは過小になります。サブエージェント込みの量は、modelUsage のモデル別エントリにある thinkingTokens を足し合わせて取ります。total_cost_usd は金額の全体像を見る用で、思考だけの内訳はありません。
| 取り方 | サブエージェント分 | 思考トークンの有無 |
|---|---|---|
usage.output_tokens_details.thinking_tokens | サブエージェント分含まれない | 思考トークンの有無あり(内訳が返るとき) |
modelUsage[model].thinkingTokens | サブエージェント分含まれる | 思考トークンの有無あり(記録するバージョン以降) |
total_cost_usd | サブエージェント分含まれる | 思考トークンの有無なし(金額のみ) |
ModelUsage.thinkingTokens は、そのモデルが生成した思考トークン数です。outputTokens にすでに含まれているので、2つを足してはいけません。値は省略されることがあるため ?? 0 で読みます。同じエントリの costUSD はクライアント側で計算した推定額なので、請求額の根拠にはせず、思考トークン数と並べて目安に使う程度にとどめます。
if (message.type === "result") {
const thinkingAll = Object.values(message.modelUsage).reduce(
(sum, u) => sum + (u.thinkingTokens ?? 0),
0
);
console.log(`思考トークン(サブエージェント込み): ${thinkingAll}`);
}この項目は、記録するバージョンで始まっていないセッションを再開したときに部分値になります。再開をまたぐ集計で数字が小さく出たら、まずセッションを始めたバージョンを疑います。古いセッションの再開をまたぐ集計では、過小に出る可能性があります。要件はAgent SDK v0.3.257以降です。
サブエージェントの思考そのものを画面に出したいなら、forwardSubagentText オプションがあります。有効にすると、サブエージェントのテキストと思考ブロックが、parent_tool_use_id つきのassistant / userメッセージとして転送されます。無効のときは、フォアグラウンドで動くサブエージェントのテキストと思考ブロックが省かれます。
実装としては、ターンが終わった時点で推定値の表示を確定値に差し替える形が素直です。上のコードの result 側で、トップレベルの確定値を「思考(確定)」として出し、サブエージェント込みの合計を別の行に添えます。生成中は「推定」、終了後は「確定」と表示の名前を変えておくと、数字が途中で食い違って見えても読み手が戸惑いません。
送ったメッセージと進捗を結びつける
複数のメッセージを続けて送るアプリでは、どの入力に対する思考中なのかが問題になります。thinking_tokens フレームには、ターンが応答しているメッセージの uuid が user_message_uuid として載ります。リファレンスは、ターンの最初の返信を待たずに思考の進捗を送信メッセージへ結びつけられる、と説明しています。この反映にはAgent SDK v0.3.260以降が必要です。
const pending = new Map<string, { startedAt: number }>();
// 送信時に uuid を控えておく
function onSend(uuid: string) {
pending.set(uuid, { startedAt: Date.now() });
}
// 受信側
function onMessage(message: any) {
if (message.type === "system" && message.subtype === "thinking_tokens") {
const key = message.user_message_uuid;
if (key && pending.has(key)) {
render(key, message.estimated_tokens);
}
}
}近い間隔で複数送ると、Claude Codeが1つのターンにまとめることがあります。その場合、user_message_uuid には最後のメッセージの uuid だけが載り、まとめたすべての uuid は user_message_uuids で分かります。サブエージェントのフレームと、Claude Code自身が生成したプロンプトによるターンでは、このフィールドが付かないことがあります。フィールドはオプション扱いで、なければ「紐づけなし」として描画しましょう。
ストリーミング出力との使い分け
思考中の様子を見る手段は、SDKに複数あります。
進捗表示の3つの手段
thinking_tokens
推定トークン数の増加だけが届きます。中身が隠れた思考ブロックでも出るため、「動いている」ことの表示に向きます。
includePartialMessages
部分メッセージのストリームイベントを受け取ります。message_delta の usage から、応答の出力数が伸びる様子も追えます。
Todo・タスクの進捗
何を終え、何が残っているかを示します。思考の量ではなく作業の進み具合です。
思考の文面や、ツール呼び出しの合間のメモを出したい場合は、別の記事で扱っています。thinking.display の設定側はthinking display updatesの解説にまとめています。作業の工程を見せる方法はAgent SDKのTodo Trackingが詳しいです。ターン全体の流れはエージェントループの解説で押さえられます。
thinking オプションには display があり、"summarized" と "omitted" を選べます。Claude Opus 4.7以降ではAPIの既定が "omitted" で、思考内容を thinking ブロックで受け取りたければ "summarized" を指定します。thinking_tokens は文面とは別系統の数値なので、文面を出さない構成でも進捗の数字だけは出せる、という設計がしやすくなります。
落とし穴: assistantメッセージのoutput_tokensは使えない
進捗や使用量をassistantメッセージの usage から読もうとすると、ずれます。assistantメッセージの output_tokens と output_tokens_details は message_start 時点のプレースホルダーで、実際の出力数ではありません。1回のAPIレスポンスから複数のassistantメッセージができる場合も、すべて同じプレースホルダーを持ちます。
実際の出力数は、結果メッセージの usage か modelUsage から読みます。生成中に伸びる様子を見たければ、includePartialMessages を有効にして message_delta の usage を読みます。また、Claude Codeが合成したassistantメッセージ(APIエラーのメッセージなど)では、output_tokens_details 自体が null になります。
const tokens = message.usage.output_tokens_details?.thinking_tokens ?? 0;null と undefined の両方をこの1行で受けられます。
Pythonで同じことをしたい場合
Python Agent SDKのリファレンスには、thinking_tokens というメッセージ型の記載がありません。確定値の側は、TypeScriptの ModelUsage.thinkingTokens と同じ内訳をPythonでも取れます。結果メッセージの model_usage のモデル別エントリに thinkingTokens があり、outputTokens に含まれた思考トークンの数を表します。TypedDictには宣言されていないため、.get() で読みます。
| SDK | 項目 | 必要なバージョン |
|---|---|---|
| TypeScript | 項目modelUsage[model].thinkingTokens | 必要なバージョンAgent SDK v0.3.257以降 |
| Python | 項目model_usage[model].get("thinkingTokens") | 必要なバージョンPython Agent SDK 0.2.150以降(同梱のCLIが記録) |
進捗の表示をPythonで作りたい場合は、リファレンスに記載のあるメッセージ(部分メッセージのストリームなど)で組み立てることになります。
実装時の確認項目
- Claude Code v2.1.153以降が動いているか(
thinking_tokensメッセージ自体の要件) - ターンごとの紐づけが要る場合、Agent SDKがv0.3.260以降か
- 進捗の数字は推定値と明示しているか。確定値と混ぜて表示していないか
- 思考の確定値を
usageから読むとき、サブエージェント分が含まれない前提で集計しているか - サブエージェント込みの思考量を
modelUsageのthinkingTokensで合計しているか(v0.3.257以降。outputTokensと二重に数えない) - 内訳を返さないプロバイダーで
0になる前提の表示になっているか output_tokens_detailsの読み出しに?.を使っているか