CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-05-28中級

Claude API のストリーミングで tool_use の引数が JSON パースに失敗する原因と対処

Claude API のストリーミングで tool_use を受け取ると、なぜか JSON.parse が SyntaxError を返す。input_json_delta の正しい組み立て手順と、再接続時の取りこぼし対策をまとめます。

Claude API115streaming16tool use5JSONtroubleshooting61

2014年から個人開発でアプリを書き続けていますが、Claude API を本番のチャットボットに組み込んで最初にハマったのが、ストリーミング応答での tool_use の引数パースでした。messages.create({ stream: true }) で受け取ったイベントを順番に JSON.parse していくと、ほぼ確実に SyntaxError: Unexpected end of JSON input で落ちます。サンプルコードを丁寧に書き写したつもりが動かない、という典型的なパターンです。同じところで詰まっている方の助けになればと思って、原因と落としどころをまとめておきます。

症状: input_json_delta を JSON.parse すると必ず SyntaxError

ストリームの中身を console.log で覗くと、こんな順番で届きます。

{ "type": "message_start", "message": { ... } }
{ "type": "content_block_start", "index": 0, "content_block": { "type": "tool_use", "id": "toolu_01...", "name": "get_weather", "input": {} } }
{ "type": "content_block_delta", "index": 0, "delta": { "type": "input_json_delta", "partial_json": "{\"loc" } }
{ "type": "content_block_delta", "index": 0, "delta": { "type": "input_json_delta", "partial_json": "ation\": \"" } }
{ "type": "content_block_delta", "index": 0, "delta": { "type": "input_json_delta", "partial_json": "Tokyo\"}" } }
{ "type": "content_block_stop", "index": 0 }
{ "type": "message_delta", "delta": { "stop_reason": "tool_use" } }
{ "type": "message_stop" }

ここで partial_json を 1 イベントずつパースしようとすると、{"loc の時点で SyntaxError になります。一見すると JSON のフラグメントですが、Claude API はパースしやすい区切りで送ってくれるわけではありません。中括弧の途中・キーの途中・値の途中、どこででも切ってきます。

つまり partial_jsonJSON チャンクではなく、最終的に連結すると JSON になる「ただの文字列断片」 です。これを 1 件ずつパースする実装は、たまたまブロックが小さくて全部 1 通に収まったときだけ動いて、入力が長くなった途端に壊れます。

原因: ブロック単位でしかパースできない契約になっている

公式のメッセージング API ドキュメントを読み直すと、input_json_delta の項に「partial_json フィールドを順次連結し、content_block_stop を受け取った時点でパースする」と書かれています。連結のキーは content_block_delta.index で、同じ index に属するデルタを 1 本のバッファに足していくのが正解です。

text_delta のような他の delta 種別と取り違えやすいのもハマりどころです。text ブロックなら 1 文字ずつ UI に流していけますが、tool_usepartial_json はそうはいかない。「同じイベント型に見えても、delta.type で挙動を切り替える」ことを忘れると静かに壊れます。

具体的に止まるのはおおむね次の 3 ケースでした。

  • 旅行系のチャットで日付範囲("2026-06-01""2026-06-07")を 1 ブロックで受け取ったとき、"2026-06- で 1 通切られて落ちる
  • 並列ツール呼び出し(disable_parallel_tool_use: false)で index: 0index: 1 が交互に届き、バッファを 1 本にしてしまって構文崩壊
  • 連続会話の 2 ターン目で過去ターンの tool_use.input を Cookie に保存していて、ストリーミング途中の中途半端な値がそのまま記録される

修正: index ごとにバッファを持って、stop で初めて parse する

最小限の正しい実装はこんな形になります。Node.js + 公式 @anthropic-ai/sdk v0.39 系を想定していますが、ロジックは生 SSE を扱う場合でも同じです。

import Anthropic from "@anthropic-ai/sdk";
 
const client = new Anthropic();
 
type ToolBuffer = {
  id: string;
  name: string;
  json: string; // partial_json を index 順に連結する
};
 
async function callWithStreamingTools() {
  const stream = client.messages.stream({
    model: "claude-sonnet-4-6",
    max_tokens: 1024,
    tools: [/* ... */],
    messages: [{ role: "user", content: "東京の天気と明日のニュースを教えてください" }],
  });
 
  const buffers = new Map<number, ToolBuffer>();
  const completedToolUses: Array<{ id: string; name: string; input: unknown }> = [];
 
  for await (const event of stream) {
    if (event.type === "content_block_start" && event.content_block.type === "tool_use") {
      buffers.set(event.index, {
        id: event.content_block.id,
        name: event.content_block.name,
        json: "",
      });
    } else if (event.type === "content_block_delta" && event.delta.type === "input_json_delta") {
      const buf = buffers.get(event.index);
      if (buf) buf.json += event.delta.partial_json;
    } else if (event.type === "content_block_stop") {
      const buf = buffers.get(event.index);
      if (buf) {
        // ここで初めて parse する。途中ではしない
        const input = buf.json.length === 0 ? {} : JSON.parse(buf.json);
        completedToolUses.push({ id: buf.id, name: buf.name, input });
        buffers.delete(event.index);
      }
    }
  }
 
  return completedToolUses;
}

ポイントは 3 つあります。

  1. content_block_starttool_use を見つけた時だけ index にバッファを確保する。テキストブロックと混ぜない
  2. partial_json+= で連結し、その場でパースしない
  3. content_block_stop で初めて JSON.parse する。空文字列のときは {} にフォールバックする(引数を取らないツールが空 input で来るケース対策)

実体験として、ここを直しただけで運用中の AI 自動応答パイプラインで週 1 件ほど出ていた 500 系エラーが消えました。

連結途中で接続が切れたときの取りこぼし対策

ストリーミングはネットワーク中断との戦いでもあります。私が公開している iOS 用壁紙アプリ(累計5,000万DL規模で運用していた経験から AdMob 周りの収益計上を Claude API で要約させる仕組みを作っていた頃)でも、機内モード復帰の瞬間に SSE が途切れ、content_block_stop を受け取らないまま終了するケースがありました。

中断検知には 2 つのシグナルを併用します。

try {
  for await (const event of stream) { /* 上のロジック */ }
} catch (err) {
  // 接続切れ。バッファに残っている tool_use は未完成として破棄
  for (const [index, buf] of buffers) {
    console.warn(`incomplete tool_use index=${index} name=${buf.name} bytes=${buf.json.length}`);
  }
  throw err;
}
 
// 正常終了でも final response の stop_reason を必ず確認
const final = await stream.finalMessage();
if (final.stop_reason !== "tool_use" && final.stop_reason !== "end_turn") {
  // max_tokens で途中切断された可能性。retry もしくは継続を検討
}

未完成バッファをそのままアプリ層に渡すと、後段のスキーマ検証が「location フィールドが必須なのに無い」のような分かりにくい例外を出します。「途中で切れた tool_use はアプリ層に渡さない」を徹底するだけで、障害解析の時間がだいぶ短くなりました。

ライブラリで吸収するか、生 SSE で書くか

公式 SDK は messages.stream(...) を使うと on("inputJson", (delta, snapshot) => ...) のように完全文字列のスナップショットを毎回渡してくれるイベントが用意されています。アプリ側で「ストリーミング中にツール引数の途中経過を UI に出したい」という要件が無いなら、SDK のイベントに任せて自前で連結しないほうが安全です。

一方で、Cloudflare Workers のように fetchReadableStream を生で扱う環境では、自分でイベントパースを書くケースもあります。その場合は次の点に注意します。

  • SSE のフレーム区切り(\n\n)でバッファをスライスする
  • 1 フレーム内に複数イベントが来る場合がある(小さい delta が連発する)
  • data: プレフィックスを取り除いてから JSON.parse する
  • partial_json の中身に改行が含まれることがある(JSON エスケープ済みなので連結時に何もしなくてよい)

Cloudflare Workers + Anthropic API の組み合わせで動かしたとき、私は手書きの SSE パーサーを書く前に SDK の BetaMessageStream を試して、要件が満たせるなら基本そちらを採用しています。Worker のバンドルサイズには影響しますが、本番のデバッグ容易性とトレードした方が安く付くという判断です。

デバッグ手順: どこで壊れているか機械的に切り分ける

実装が動かないときの私の切り分け順序は次のとおりです。

// 1) 生イベントをそのままダンプ
for await (const event of stream) {
  console.log(JSON.stringify(event));
}

ログを見て確認するのは以下の項目です。

  • content_block_startcontent_block.typetool_use になっているか(text の混在で連結先を間違えていないか)
  • partial_json を文字列連結した結果が、最終的に有効な JSON になるか(cat events.jsonl | jq -s '...' で確認)
  • content_block_stop のあとに message_delta.stop_reasontool_use で来ているか
  • 1 メッセージで複数の tool_use が来ているなら、index が連番(0, 1, 2 ...)になっているか

ここまで揃っていれば、あとはバッファのキー(index)の取り扱いミスを潰すだけで動きます。実装を書き直す前に、まず生イベントの形を信じることがいちばんの近道でした。

関連する落とし穴: disable_parallel_tool_use と部分継続

最後に、ストリーミング × tool_use 周りでもう 1 つ気をつけているのが並列実行の扱いです。Sonnet 4.6 以降のモデルは標準で並列ツール呼び出しが有効で、1 メッセージで複数の tool_use ブロックが返ってきます。これを直列に処理したいときは disable_parallel_tool_use: true を指定しますが、ストリーミング側のコードを変えずに切り替えると、バッファ管理は壊れません(index が 0 だけになるだけ)。

逆に並列を有効にしたまま「1 つ目の結果が出るまで 2 つ目の引数を捨てる」最適化を入れると、content_block_stop のタイミングで未来のブロックを取りこぼします。並列にする場合は すべての tool_use を受け取りきってから tool_result を返す のがいちばん安全でした。

実装中の API の挙動は変わることがあるので、本番に出す前に最新の SDK でレスポンスをダンプし直す癖をつけておくと事故が減ります。今回の話が同じところで悩んでいる方の助けになれば幸いです。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

API & SDK2026-07-11
本番で観測した引数分布からツールスキーマを締め直す
Claudeのツール呼び出しが渡してくる引数を本番で記録し、その分布からJSON Schemaにenumやpatternを足して締め直す運用手法を、計測コードと実測値つきで整理しました。
API & SDK2026-06-29
ツールが返す画像を Claude に「見せる」 — tool_result を画像ブロックにしてトークンを約10分の1にする
ツールが返した画像を tool_result に文字列で詰めると、同じ画像が約10〜20倍のトークンを消費します。image コンテンツブロックとして返す正しい組み立て方と、SDK 実装・コスト見積もり・落とし穴を実コードでまとめました。
API & SDK2026-06-23
思考が常時オンのモデルに移すと、プレフィルが静かに効かなくなる — Fable 5 でストリーミングとトークン予算を直した記録
Fable 5 は思考が常時オンです。プレフィルが使えない・ストリーミングの先頭ブロックがテキストではない・max_tokens に思考分の余白が要る、という三つの前提崩れを、自分の自動投稿パイプラインで直した実装メモです。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →