Claude Agent SDKで(no content)テキストが出る原因と対処法
Claude Agent SDKのstreaming inputモードでthinkingブロックの前に(no content)という空テキストが混入する不具合の原因・修正バージョン・防御的な実装をまとめます。
Claude Agent SDKで(no content)テキストが出る現象とは
Claude Agent SDK(TypeScript、@anthropic-ai/claude-agent-sdk)をstreaming inputモードで使うと、まれに奇妙なassistantメッセージが割り込むことがありました。contentが[{"type": "text", "text": "(no content)"}]だけのメッセージです。その直後に、本来届くはずのthinkingブロックを含む本物のメッセージが続けて届きます。この現象はclaude-agent-sdk-typescriptリポジトリのissue #153(2026年1月28日作成)で報告され、SDK v0.2.30で修正済みです。原因はSDK側がプレースホルダーのテキストメッセージを出力に混入させていたことで、モデルが壊れたテキストを生成していたわけではありません。
報告された出力の中身
issue #153の報告者が添えた再現例は、同じsession_idの中で次の2つのassistantメッセージが連続して届くというものでした。
{
"type": "assistant",
"message": {
"id": "msg_01SUL6QRbB7sDG2mt1tf3pQb",
"model": "claude-opus-4-5-20251101",
"content": [{ "type": "text", "text": "(no content)" }],
"usage": { "output_tokens": 9 }
}
}{
"type": "assistant",
"message": {
"id": "msg_01SUL6QRbB7sDG2mt1tf3pQb",
"model": "claude-opus-4-5-20251101",
"content": [{ "type": "thinking", "thinking": "...", "signature": "..." }],
"usage": { "output_tokens": 9 }
}
}2つのメッセージはmessage.idが同一です。1つ目のcontentは(no content)という文字列だけを持つtextブロック、2つ目に本来のthinkingブロックが入っています。同じメッセージが2つのフレームに分かれて届いたことになります。片方は中身のないプレースホルダーでした。
usage.output_tokensはどちらのフレームも9トークンで一致しています。トークン使用量の集計上は同じ1回の生成として数えられているとみられ、二重にAPI料金が発生するような問題ではありません。問題になるのは、あくまで受信側が2つのメッセージフレームをどう解釈するかという実装上の話です。
thinkingブロックはtextブロックより先に届くのが仕様
Claude APIの公式ドキュメントは、拡張思考(extended thinking)が有効なときの順序を明記しています。1つ以上のthinkingブロックが、textブロックより先に届くという順序です。issue #153の現象は、この順序の手前に空のtextブロックだけを持つメッセージが割り込む形になっていました。ドキュメントが定める順序と食い違う出力だったわけです。
報告者はこの現象を「確実には再現できない」としながらも、同じセッション内で3回起きるほどの頻度だったと説明しています。使用モデルはすべてclaude-opus-4-5-20251101でした。コメント欄には、別の利用者から「今日も頻繁に見た」という追認も寄せられています。
発生条件としてわかっていること
issueの本文とコメントから確認できる条件は次のとおりです。
- 使用モデルは
claude-opus-4-5-20251101 includePartialMessagesは有効化していない状態で発生。部分ストリーミングのイベントではなく、完了したassistantメッセージ側の現象- streaming inputモード(
AsyncGeneratorをpromptに渡す常駐セッション)での利用 - プロンプトキャッシュ(
cache_creation_input_tokens・cache_read_input_tokens)を伴うリクエストで観測された
これ以上の内部的な発生メカニズムは、issue上でもAnthropicから説明されていません。2026年3月26日付のコメントで、SDK v0.2.30での修正が案内されています。
すでにこの不具合に当たっているかどうかは、ログを1つの文字列で確認できます。SDKからのメッセージをファイルやコンソールに保存している場合、"text": "(no content)"という文字列でログを検索するのが手早い方法です。ヒットする行があれば、そのセッションで少なくとも1回はプレースホルダーメッセージを受け取っています。
対処法1: SDKをv0.2.30以降に更新する
CHANGELOG.mdのv0.2.30エントリには、修正内容が「Fixed "(no content)" placeholder messages being included in SDK output」と明記されています。npmでの最新版は0.3.278まで進んでおり、v0.2.30からマイナーバージョンが200近く積み重なりました。継続的にSDKを更新しているプロジェクトなら、この不具合にはすでに当たりません。対象になるのは、package.jsonでバージョンを固定している場合や、しばらくnpm updateをしていないプロジェクトだけです。
npm install @anthropic-ai/claude-agent-sdk@latestインストール後はnpm list @anthropic-ai/claude-agent-sdkで、実際に反映されたバージョンを確認します。CHANGELOG.mdはGitHubリポジトリ直下に置かれており、バージョンごとの変更点は見出し単位で追えます。アップデート前に、他の破壊的変更が挟まっていないかを0.2.30からの区間だけでも目を通しておくと安全です。特に0.3.142ではTypeScript Agent SDK V2廃止の理由と移行先で扱ったV2セッションAPIが廃止されているため、V2の実験的なAPI(unstable_v2_createSession等)を使っているプロジェクトは、そちらの移行作業も同時に必要になります。
対処法2: 防御的にcontentブロックをフィルタする
古いバージョンを使い続けざるを得ない場合や、同種の未知のプレースホルダー混入に備えたい場合は、assistantメッセージの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; // プレースホルダーのみのフレームはスキップ
// 通常の処理へ続ける
}
}このフィルタは、v0.2.30より前のSDKに対する緊急回避になるだけでなく、パース処理を「先頭ブロックの型を前提にしない」設計にしておく一般的な保険にもなります。
フィルタしないとどこで問題が表面化するか
この不具合を放置した場合、具体的には2つの経路で問題が表に出ます。1つ目はUI表示です。message.content[0].textのように先頭ブロックのテキストをそのまま画面に出すコードは、(no content)という文字列をユーザーにそのまま見せてしまいます。ユーザー視点では、Claudeが意味不明な返答を返したように映ります。
2つ目はブロック型による分岐処理です。「先頭がthinkingブロックなら思考中表示を出す」といったロジックを組んでいる場合、プレースホルダーのフレームでは先頭がtextブロックになるため、思考中表示のトリガーを取りこぼします。どちらの経路も、SDKのバージョンさえ上げれば根本的には解消しますが、外部から受け取ったSDKメッセージを無条件に信用しない実装にしておくほうが、同種の不具合に対して総合的に頑丈です。
影響範囲の早見表
issue #153が報告されたのはclaude-agent-sdk-typescriptリポジトリです。Python版のClaude Agent SDK(claude-agent-sdkパッケージ)での発生有無は、このissueからは分かりません。以下はTypeScript SDKを使っているプロジェクトに限定した状況です。
| 状況 | 影響 |
|---|---|
| SDK v0.2.30以降を使用 | 影響修正済み。(no content)は出力に混入しない |
| SDK v0.2.29以前を使用、streaming inputモード | 影響発生する可能性あり。再現条件は不定 |
| シングルメッセージ入力モードのみ利用 | 影響issue上で明示的な報告なし |
includePartialMessagesによるstream_eventのみ処理 | 影響この不具合の対象外(完了したassistantメッセージ側で発生) |
content配列の先頭を信用しすぎない設計にする
この不具合が示すのは、SDKが返すmessage.content配列の並びを「thinkingが必ず先頭」という前提で固定的にパースするコードは、SDK側の一時的な不具合や将来のフレーム分割の変更に弱いということです。Context editingでthinking blockとtool resultを併用する順序で扱ったclear_thinking_20251015のような設定も、配列の並び順に依存する仕様を持っています。ブロックをtypeフィールドで判定してから処理する実装にしておけば、今回のような単発の混入があっても壊れにくくなります。
content配列に含まれるブロックの種類は、今後も増える方向です。公式ドキュメントには、ツール呼び出しの合間にモデルが状況を一言で伝える「進行状況の更新」が、独立したthinkingブロックとして挟まる場合があると記載されています。こうしたブロックは通常の思考ブロックと隣接して現れるため、content[0]のような固定インデックスへの依存はますます危険になります。配列を先頭から順に走査し、typeごとに処理を振り分ける実装にしておけば、この種の仕様追加にも影響を受けにくくなります。
streaming inputモード自体の基本的な特性は、Agent SDKストリーミング入力とシングルメッセージ入力の使い分けで扱っているとおりです。(no content)の不具合は、この入力モードで持続的なセッションを回している最中に見つかりました。streaming inputモードでは他にも、非同期サブエージェントの完了通知が弾かれるonly prompt commands are supported in streaming modeエラーの原因のような、モード固有の不具合が過去に報告されています。どちらも、公式ドキュメントの「推奨モード」という位置づけとは裏腹に、単発の実装バグが混ざりやすい経路だったことを示しています。
まとめ
(no content)という文字列だけを持つassistantメッセージがthinkingブロックの前に届く不具合は、Claude Agent SDK(TypeScript)のstreaming inputモードで2026年1月に報告され、v0.2.30で修正済みです。現在の最新版(0.3.278)を使っていれば当たりませんが、バージョンを固定している場合はアップデートするか、content配列から(no content)のtextブロックを取り除く防御的なフィルタを挟んでおくと安全です。
まずnpm list @anthropic-ai/claude-agent-sdkで今のバージョンを確認し、v0.2.30より前であれば更新を検討します。すぐに更新できない事情がある場合は、フィルタの追加だけでも当座の回避になります。ログに(no content)という文字列が残っていないか確認しておけば、自分のプロジェクトが実際に影響を受けていたかどうかも判断できます。