Agent SDK のストリーミング出力 使い方(StreamEvent と部分メッセージ)

Claude Agent SDK エンジニアリング 6分で読めます

Claude Agent SDK は、既定では「ブロックが完成してから」メッセージを返します。テキストブロックやツール呼び出しごとに、Claude が生成し終えた時点で完全な AssistantMessage が1つ届く仕組みです。生成中のテキストを少しずつ画面に出したい場合は、部分メッセージのストリーミングを有効にします。

この記事では、公式ドキュメント「Stream responses in real-time」に沿って、有効化の方法、StreamEvent の中身、メッセージが届く順序、ツール呼び出しの逐次受信、ストリーミング UI の組み方、既知の制限を整理します。SDK の基本は「Claude Agent SDK でエージェントを構築する」を、メッセージ全体の流れは「Agent SDK のエージェントループ」を参照してください。

ストリーミング出力を有効にする

オプションで include_partial_messages(Python)または includePartialMessages(TypeScript)を true にします。すると SDK は、通常の AssistantMessage と ResultMessage に加えて、API の生イベントを包んだ StreamEvent を随時返すようになります。受け取る側のコードは次の3段階で判定します。

  1. メッセージの型を見て、StreamEvent とそれ以外を区別する
  2. StreamEvent なら event フィールドを取り出し、その type を調べる
  3. content_block_delta イベントのうち delta.type が text_delta のものが、実際のテキスト断片を持つ

次の例は、ストリーミングを有効にして、届いたテキスト断片をそのまま出力します。StreamEvent、content_block_delta、text_delta と型チェックが入れ子になる点に注目してください。

from claude_agent_sdk import query, ClaudeAgentOptions
from claude_agent_sdk.types import StreamEvent
import asyncio


async def stream_response():
    options = ClaudeAgentOptions(
        include_partial_messages=True,
        allowed_tools=["Bash", "Read"],
    )

    async for message in query(prompt="List the files in my project", options=options):
        if isinstance(message, StreamEvent):
            event = message.event
            if event.get("type") == "content_block_delta":
                delta = event.get("delta", {})
                if delta.get("type") == "text_delta":
                    print(delta.get("text", ""), end="", flush=True)


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

for await (const message of query({
  prompt: "List the files in my project",
  options: {
    includePartialMessages: true,
    allowedTools: ["Bash", "Read"]
  }
})) {
  if (message.type === "stream_event") {
    const event = message.event;
    if (event.type === "content_block_delta") {
      if (event.delta.type === "text_delta") {
        process.stdout.write(event.delta.text);
      }
    }
  }
}

StreamEvent のリファレンス

部分メッセージを有効にすると、Claude API のストリーミングイベントがそのまま1つのオブジェクトに包まれて届きます。この型の名前は SDK ごとに異なります。

どちらも届くのは生のイベントで、蓄積済みのテキストではありません。テキスト断片を連結する処理は自分で書く必要があります。event フィールドには Claude API のストリーミングイベントが入っており、代表的な種類は次のとおりです。

イベントの種類内容
message_start新しいメッセージの開始
content_block_start新しいコンテンツブロック(テキストまたはツール使用)の開始
content_block_deltaコンテンツの増分更新
content_block_stopコンテンツブロックの終了
message_deltaメッセージ単位の更新(停止理由や使用量など)
message_stopメッセージの終了

注意点が2つあります。第一に、parent_tool_use_id は Python では常に None、TypeScript では常に null です。ストリームイベントはメインセッションに対してのみ出力され、サブエージェントのトークン単位の差分は転送されません。サブエージェント由来の出力を判別したい場合は、parent_tool_use_id を持つ完全なメッセージを使います。第二に、Claude Code はターン最初の ping 以外のストリームイベントに user_message_uuid を付けます。このフィールドは TypeScript 側の型で扱え、Python の StreamEvent には公開されていません。

メッセージの流れと中断時の扱い

Claude Code は、空でないコンテンツブロックが完成するたびに AssistantMessage を1つ出力します。そのため、テキストブロックとツール呼び出しを含む応答なら AssistantMessage は2つ届きます。各メッセージには自分のブロックの内容だけが入り、2つは同じメッセージ ID を共有します(TypeScript では message.message.id、Python では message.message_id)。部分メッセージを有効にした場合、各 AssistantMessage はそのブロックの content_block_stop より前に届き、全体の順序は次のようになります。

StreamEvent (message_start)
StreamEvent (content_block_start) - text block
StreamEvent (content_block_delta) - text chunks...
AssistantMessage - complete text block
StreamEvent (content_block_stop)
StreamEvent (content_block_start) - tool_use block
StreamEvent (content_block_delta) - tool input chunks...
AssistantMessage - complete tool_use block
StreamEvent (content_block_stop)
StreamEvent (message_delta)
StreamEvent (message_stop)
... tool executes ...
... more streaming events for next turn ...
ResultMessage - final result

部分メッセージを有効にしない場合は、StreamEvent 以外のメッセージがすべて届きます。代表的なのは、セッション初期化を知らせる SystemMessage、完全なコンテンツブロックを持つ AssistantMessage、最終結果の ResultMessage、そして会話履歴が圧縮されたことを示す境界メッセージ(TypeScript では SDKCompactBoundaryMessage、Python ではサブタイプ "compact_boundary" の SystemMessage)です。

ストリームが途中で切れた場合

ターンを中断した場合や接続が切れた場合など、メッセージの途中でストリームが切れても、そのメッセージの message_stop はターンが終わる前に届きます。切れたテキストブロックや思考ブロックには content_block_stop も届きます。一方、切れたツール呼び出しには届きません。したがって、ツール呼び出しのブロックが開いたまま message_stop が来たら、そのツール呼び出しの入力は不完全なものとして扱ってください。

Claude Code v2.1.290 より前は、切れたストリームが message_stop なしでターンを終えることがあり、ストリームイベントから描画した応答が「処理中」の表示のまま残る場合がありました。TypeScript Agent SDK は v0.3.290 以降、Python Agent SDK は v0.2.164 以降で v2.1.290 以降の Claude Code を同梱しています。ターン終了後も応答が処理中のままなら、SDK を更新してください。

ツール呼び出しのストリーミング

ツール呼び出しも少しずつ届きます。ツールの開始、生成中の入力、完了を順に追えます。使うイベントは次の3つです。

次の TypeScript 例は、現在のツール名を記録し、入力 JSON の断片を連結していき、ブロックが閉じた時点で完成した入力を表示します。断片 partial_json は単独では JSON として解釈できないので、content_block_stop まで連結してから使います。

import { query } from "@anthropic-ai/claude-agent-sdk";

let currentTool: string | null = null;
let toolInput = "";

for await (const message of query({
  prompt: "Read the README.md file",
  options: {
    includePartialMessages: true,
    allowedTools: ["Read", "Bash"]
  }
})) {
  if (message.type === "stream_event") {
    const event = message.event;

    if (event.type === "content_block_start") {
      if (event.content_block.type === "tool_use") {
        currentTool = event.content_block.name;
        toolInput = "";
        console.log(`Starting tool: ${currentTool}`);
      }
    } else if (event.type === "content_block_delta") {
      if (event.delta.type === "input_json_delta") {
        toolInput += event.delta.partial_json;
      }
    } else if (event.type === "content_block_stop") {
      if (currentTool) {
        console.log(`Tool ${currentTool} called with: ${toolInput}`);
        currentTool = null;
      }
    }
  }
}

Python でも同じ流れです。event.get("type") で分岐し、content_block の name を保持して、delta の partial_json を文字列に足していきます。

ストリーミング UI を組む

テキストとツールのストリーミングを組み合わせると、マルチステップのタスクで進捗を見せるチャット UI の骨格になります。公式の例では、ツール実行中かどうかを示す in_tool フラグを持たせ、ツールが走っている間は [Using Read...] のような状態表示を出し、完了したら「done」を出しています。ツール以外のときは、テキストを通常どおり流します。

import { query } from "@anthropic-ai/claude-agent-sdk";

let inTool = false;

for await (const message of query({
  prompt: "Find all TODO comments in the codebase",
  options: {
    includePartialMessages: true,
    allowedTools: ["Read", "Bash", "Grep"]
  }
})) {
  if (message.type === "stream_event") {
    const event = message.event;

    if (event.type === "content_block_start") {
      if (event.content_block.type === "tool_use") {
        process.stdout.write(`\n[Using ${event.content_block.name}...]`);
        inTool = true;
      }
    } else if (event.type === "content_block_delta") {
      if (event.delta.type === "text_delta" && !inTool) {
        process.stdout.write(event.delta.text);
      }
    } else if (event.type === "content_block_stop") {
      if (inTool) {
        console.log(" done");
        inTool = false;
      }
    }
  } else if (message.type === "result") {
    console.log("\n\n--- Complete ---");
  }
}

実装の要点は3つです。ツール開始時にフラグを立てて状態表示を出すこと、フラグが立っている間はテキストを出力しないこと、content_block_stop でフラグを戻して完了を表示することです。最後に届く ResultMessage(TypeScript では type が result)を、エージェントの全作業が終わった合図として使います。

既知の制限

公式ドキュメントが挙げている制限は、構造化出力との組み合わせです。部分メッセージを有効にすると、JSON は検証前の input_json_delta の断片としてツール呼び出しの形でストリームされ、検証を通った結果だけが最終的な ResultMessage.structured_output に入ります。ストリーム途中の JSON をそのまま信用して使わず、確定値は ResultMessage から読んでください。構造化出力の詳細は公式の Structured outputs のページを参照してください。

本ページは Stream responses in real-time の内容に基づく非公式の日本語解説で、イベントの種類は Claude API のストリーミングのイベント種別に対応しています(確認日 2026-10-24)。コード例は一部を簡略化し、コメントを省いています。最新の仕様は公式ドキュメントを確認してください。