Claude Agent SDKで(no content)テキストが出る原因と対処法
Claude Agent SDKでthinkingブロックの前に(no content)という空テキストが混入する不具合の原因・修正バージョン・防御的な実装をまとめます。
Claude Agent SDKで(no content)テキストが出る現象とは
Claude Agent SDK(TypeScript、@anthropic-ai/claude-agent-sdk)でメッセージを受け取っていると、contentが[{"type": "text", "text": "(no content)"}]だけのassistantメッセージが割り込むことがありました。その直後に、本来届くはずのthinkingブロックを含むメッセージが続きます。
claude-agent-sdk-typescriptリポジトリのissue #153で報告され、SDK v0.2.30で修正されています。CHANGELOGの記述は「プレースホルダーのメッセージがSDKの出力に含まれていた」というもので、モデルの出力に問題があったとは書かれていません。
issue #153が修正されるまで
- 2026年1月28日issue #153が作成される
(no content)のtextブロックがthinkingブロックの前に出力される、という報告です。 - 2026年1月31日別の利用者が追認
「今日もかなり頻繁に見た」というコメントが付きました。
- 2026年2月3日v0.2.30がnpmに公開される
CHANGELOGに修正が載ったバージョンです。
- 2026年3月26日issueがクローズされる
「v0.2.30以降なら出ないはず。再発したらreopenしてほしい」という案内で閉じられました。
修正の公開からissueの案内までは、約7週間空いています。issueが開いたままの間も、v0.2.30以降には修正が入っていました。
報告された出力の中身
issueの再現例では、同じsession_idの中で次の2つのassistantメッセージが続けて届いています。1つ目のstop_reasonはnullで、使用モデルはclaude-opus-4-5-20251101でした。
{
"type": "assistant",
"message": {
"id": "msg_01SUL6QRbB7sDG2mt1tf3pQb",
"model": "claude-opus-4-5-20251101",
"content": [{ "type": "text", "text": "(no content)" }],
"stop_reason": null,
"usage": {
"input_tokens": 3,
"cache_creation_input_tokens": 53657,
"cache_read_input_tokens": 17895,
"output_tokens": 9
}
}
}{
"type": "assistant",
"message": {
"id": "msg_01SUL6QRbB7sDG2mt1tf3pQb",
"model": "claude-opus-4-5-20251101",
"content": [{ "type": "thinking", "thinking": "...", "signature": "..." }],
"stop_reason": null,
"usage": { "output_tokens": 9 }
}
}2つのmessage.idは同じで、usage.output_tokensも9で揃っています。2つ目のJSONは、元のissueからusageの一部を省略した形です。
1つ目はtextブロックが(no content)という文字列だけを持ち、2つ目に本来のthinkingブロックが入っています。同じAPIメッセージが、ブロックごとの別フレームとして届いた形です。
issueの2つ目のJSONには、キャッシュの内訳としてephemeral_1h_input_tokens: 53657も出ています。この報告のセッションでは、1時間のキャッシュが使われていたことになります。報告者は同じセッション内で3回起きたと書き、再現手順は示していません。
thinkingブロックが先に届くのが本来の順序
拡張思考が有効なとき、最初のthinkingブロックはtextブロックより前に届くとされています。公式ドキュメントも、1つ以上のthinkingブロックがtextブロックの前に来ると説明しています。ストリーミングでは、thinkingのデルタとsignature_deltaのあとにtextのデルタが続きます。issue #153では、その手前に空のtextだけのメッセージが1つ増えていました。
どの条件で起きたか
issueの本文とコメントに書かれている条件は次のとおりです。
- 使用モデルは
claude-opus-4-5-20251101 includePartialMessagesは有効にしていない- キャッシュ読み書きのトークンが入るリクエスト(
cache_creation_input_tokens・cache_read_input_tokensが0でない)で観測された - 同じセッションの中で3回起きた
issueには、streaming inputかシングルメッセージ入力かの記載がありません。Python版のclaude-agent-sdkへの報告も、このissueには出てきません。
Anthropic側の原因の説明は、issueにありません。
自分のプロジェクトが当たっていたかは、保存しているSDKメッセージのログを"text": "(no content)"で検索すれば分かります。ヒットした行があれば、そのセッションでプレースホルダーを受け取っています。
対処法1: SDKをv0.2.30以降に更新する
更新して直ったことを確かめる
- 1
今のバージョンを見る
npm list @anthropic-ai/claude-agent-sdkで、実際にインストールされている版を確かめます。0.2.30より前なら対象です。 - 2
更新する
下のコマンドで最新版に上げます。
package.jsonで版を固定している場合は、固定値も書き換えます。 - 3
ログを検索する
更新後のログに
(no content)のtextが残っていないことを確かめます。
npm install @anthropic-ai/claude-agent-sdk@latestCHANGELOGのv0.2.30には、debug・debugFileオプションの追加とFileReadToolInputのpagesフィールド追加などと並んで、Fixed "(no content)" placeholder messages being included in SDK outputが載っています。v0.2.30以降のバージョンなら、この修正が入っています。
古い版から一気に上げるときは、途中の破壊的変更に注意が要ります。CHANGELOGの0.3.142は、次の3点を破壊的変更として挙げています。
- v2セッションAPI(
unstable_v2_createSessionなど)の削除。query()を使います - MCPサーバーが既定でバックグラウンド接続になり、遅いサーバーは
initでstatus: "pending"と報告される - ヘッドレス・SDKセッションが
TodoWriteではなくTaskツールを使う
v2を使っている場合の移行は、TypeScript Agent SDK V2廃止の理由と移行先に書いています。
対処法2: 受け取る側で(no content)だけを除く
更新できない事情がある場合は、assistantメッセージのcontentから、文字列が(no content)のtextブロックだけを除くフィルタを挟めます。
function stripPlaceholderText(content: Array<{ type: string; text?: string }>) {
return content.filter(
(block) => !(block.type === "text" && block.text === "(no content)")
);
}
for await (const message of query({ prompt, options })) {
if (message.type === "assistant") {
const content = stripPlaceholderText(message.message.content as any[]);
if (content.length === 0) continue; // プレースホルダーだけのフレームは飛ばす
// 通常の処理へ続ける
}
}条件をtype === "text"かつ文字列の完全一致に絞っているのは、正規の空に近いブロックを巻き込まないためです。この点は次の節で扱います。
除いてよい空ブロックと、除いてはいけない空ブロック
thinkingまわりには、中身が空でも正規の挙動であるブロックがあります。(no content)のフィルタを広げて「空に見えるものは全部捨てる」と書くと、これらを壊します。
空に見えるブロックの扱い
除いてよい: (no content)のtext
typeがtextで、文字列が(no content)のブロックです。issue #153の不具合で、v0.2.30で出力から外れました。
除いてはいけない: 空のthinking
display: "omitted"では、thinkingブロックのthinkingが空でsignatureだけが入ります。進行状況の更新ブロックも、どのdisplayでも空で返ることがあります。
公式ドキュメントは、空のthinkingを持つブロックについて、画面には何も描画しないよう案内しています。そのうえで、signatureを含むブロックは、ほかのthinkingブロックと同じように、そのままAPIへ返す扱いです。
プレースホルダーと同じ型で、見た目の違うものもあります。max_tokensなどで応答が止まった直後には、進行状況の更新ブロックが最後に来て、This part of the response was interrupted before it finished.という文が入ることがあります。これはthinkingブロックの本文で、正規の内容です。
フィルタしない場合に問題が表に出る場所
不具合を放置すると、問題は2つの経路で表に出ます。1つ目は画面表示です。message.content[0].textのように先頭ブロックの文字をそのまま出すコードは、(no content)をユーザーに見せます。ユーザーには、Claudeが意味不明な返事をしたように映ります。
2つ目はブロック型による分岐です。「先頭がthinkingなら思考中の表示を出す」というロジックは、先頭がtextのフレームでトリガーを取りこぼします。
逆向きの誤りにも注意が要ります。ストリーミングで進行状況の更新を待つ場合、ブロックが開くまで数秒の間が空くのは正常だと公式ドキュメントは説明しています。この間をプレースホルダーの混入や停止と見なして、再接続や再送をかける処理は不要です。更新のテキストは要約で、通常は1〜2文ですが、長さには頼らないよう書かれています。出力トークンには、要約ではなく更新の全文の長さが数えられます。
content[0]に頼らない読み方
この不具合が見せるのは、message.contentの並びを「thinkingが先頭」と決め打ちしたコードの弱さです。Context editingでthinking blockとtool resultを併用する順序で扱ったclear_thinking_20251015も、ブロックの並びに依存する仕様を持っています。ブロックはtypeで判定してから処理するほうが、単発の混入では壊れません。
ブロックの種類は、これからも増えます。公式ドキュメントによると、Claude Fable 5.1・Mythos 5.1・Opus 5.5・Sonnet 5.5・Fable 5は、ツール呼び出しの合間に進行状況の更新を書けます。更新は、通常の思考ブロックとは別のthinkingブロックとして、対応するtool_useの直前に置かれます。1回のツール呼び出しの前には、多くても1つだけです。
通常の思考ブロックと進行状況の更新が隣り合うこともあるため、固定インデックスは危険です。配列を先頭から順に走査し、typeごとに振り分ければ、こうした追加にも耐えます。
streaming inputモード自体の特性は、Agent SDKストリーミング入力とシングルメッセージ入力の使い分けにあります。公式ドキュメントは、streaming inputモードを「Recommended」としています。このモードには、非同期サブエージェントの完了通知が弾かれるonly prompt commands are supported in streaming modeエラーの原因のように、モード固有の不具合も報告されています。
まとめ
空のthinkingブロックとsignatureは不具合ではありません。SDKを固定している場合は、更新するか、除く対象を(no content)のtextブロックに絞ったフィルタを挟みます。そうしておけば、displayの設定を変えても壊れません。