streaming について
streamingはassistant-uiの基盤となるストリーム処理ライブラリで、カスタムのAI応答エンドポイント、ストリームのデコード、再開可能な配信を扱う開発者向けです。バックエンドの出力を共通形式に正規化し、複数のワイヤー形式に対応するエンコーダーとデコーダーを備えています。
主な機能
- 01AssistantStreamChunk形式への正規化
- 023種類のワイヤー形式のエンコードとデコード
- 03カスタムストリーミング応答の生成
- 04再開可能なストリームのサポート
- 05テキスト、推論、ツール呼び出しの処理
活用シーン
- 01カスタムエンドポイントのストリーミング応答作成
- 02ストリームデータのデコードと確認
- 03ストリームへの再開機能の追加
- 04テキスト、推論、ツール呼び出し出力の処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ assistant-ui/ skills --skill streamingインストール後、Claude Code で/streamingを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/assistant-transport.mddata-stream.mdencoders.mdresumable.md詳細
assistant-ui ストリーミング
最新の API については、常に assistant-ui.com/llms.txt を参照してください。
assistant-stream は、assistant-ui のチャットランタイムの下にあるワイヤ層です。すべてのバックエンドを AssistantStreamChunk 値の単一のストリームに正規化し、3 つのワイヤフォーマット用のエンコーダーとデコーダーを提供し、そのいずれの上にも再開可能なストリーム層を追加します。バックエンドがすでに Vercel AI SDK を話す場合、このパッケージに直接触れることはほとんどありません(streamText と toUIMessageStream で十分です)。カスタムエンドポイントを書くとき、ストリームを自分でデコードする必要があるとき、または再開可能なストリームが欲しいときに利用してください。
リファレンス
- ./references/data-stream.md -- Data Stream プロトコル、
useDataStreamRuntime、およびそのワイヤフォーマット - ./references/assistant-transport.md -- Assistant Transport SSE フォーマットと
useAssistantTransportRuntime状態スナップショットランタイム - ./references/encoders.md -- エンコーダーとデコーダーのカタログ、
PlainTextEncoder、UIMessageStreamDecoder、アキュムレーター、およびデバッグ - ./references/resumable.md --
assistant-stream/resumable: コンテキスト、ストア、およびクライアントの接続
使用する場面
Streaming the model call through the Vercel AI SDK?
├─ Yes → streamText + toUIMessageStream/createUIMessageStreamResponse (or result.toUIMessageStreamResponse())
│ assistant-stream is optional: only needed to decode the response yourself or add resumable streams
└─ No → build the response with assistant-stream
├─ Emitting message parts (text, reasoning, tool calls) → Data Stream
└─ Streaming a full agent state snapshot with custom commands → Assistant Transport
インストール
npm install assistant-stream
@assistant-ui/ai-sdk は現在の AI SDK 統合パッケージです(フレームワーク非依存)。@assistant-ui/react-ai-sdk は古いインストール向けに同じ API を再エクスポートしていますが、新しいコードは @assistant-ui/ai-sdk からインポートすべきです。
カスタムストリーミングレスポンスを構築する
createAssistantStreamResponse は AssistantStreamController を引数にコールバックを実行し、Data Stream としてエンコードされた Response を返します(代替のエンコーダーについては data-stream.md を参照してください)。
import { createAssistantStreamResponse } from "assistant-stream";
export async function POST(req: Request) {
return createAssistantStreamResponse(async (controller) => {
controller.appendText("Hello ");
controller.appendText("world!");
controller.appendReasoning("Checking the forecast first.", {
unstable_summary: "Looking up the weather",
});
controller.appendSource({
type: "source",
sourceType: "url",
id: "s1",
url: "https://example.com/forecast",
title: "Forecast",
});
const tool = controller.addToolCallPart({ toolName: "get_weather" });
tool.argsText.append('{"city":"NYC"}');
tool.argsText.close();
tool.setResponse({ result: { temperature: 22 } });
controller.close();
});
}
close() はまだ開いている部分をすべて閉じてストリームを終了します。コールバック内でキャッチされなかった throw は、自動的に error チャンクに変換されます。
AssistantStreamController
サーバーサイドのすべてのストリームは、最終的にどのエンコーダーでラップされるかにかかわらず、このコントローラーを通じて書き込まれます(createAssistantStream、createAssistantStreamController、createAssistantStreamResponse はすべてこれを渡します)。
| メソッド | シグネチャ | 備考 |
|---|---|---|
appendText | (textDelta: string) => void | 最初の呼び出しでテキストパートを開き、次の呼び出しでそこに追記します |
appendReasoning | (reasoningDelta: string, options?: { unstable_summary?: string }) => void | options を渡すと常に新しいパートが開かれるため、サマリーは専用のパートに格納されます |
appendSource | (part: SourcePart) => void | SourcePart は { type: "source", sourceType: "url", id, url, title?, parentId? } です |
appendFile | (part: FilePart) => void | FilePart は { type: "file", data, mimeType, parentId? } です |
appendData | (part: DataPart) => void | DataPart は { type: "data", name, data, parentId? } で、アプリが定義する名前付きパートです |
addTextPart | () => TextStreamController | 明示的な { append(text), close() } ライターで、他のパートと交互に扱うためのものです |
addReasoningPart | (options?) => TextStreamController | addTextPart と同じライター形式です |
addToolCallPart | (toolName: string) => ToolCallStreamController | toolCallId を生成します。安定した id については後述のオブジェクトオーバーロードを参照してください |
addToolCallPart | (init: ToolCallPartInit) => ToolCallStreamController | { toolCallId?, toolName, argsText?, args?, response? } |
enqueue | (chunk: AssistantStreamChunk) => void | 生のエスケープハッチです。上記のヘルパーを優先してください |
merge | (stream: AssistantStream) => void | 別の AssistantStream のパートをこれに結合します |
withParentId | (parentId: string) => AssistantStreamController | 書き込み時に parentId を付与する(ネストされた、または関連するパート)コントローラーを返します |
close | () => void | 開いているパートを閉じ、その後ストリームを閉じます |
addToolCallPart は ToolCallStreamController を返します: { argsText: TextStreamController, setResponse(response), close() }。setResponse は { result, artifact?, isError?, modelContent?, messages? }(ToolResponse が返す形式)を受け取り、パートを自動的に閉じ、2 回目の呼び出しは無視します。
ストリームイベントとパートの種類
すべてのデコーダーは、ワイヤー形式にかかわらず、同じ正規化された AssistantStreamChunk ユニオン({ path: number[] } & { type, ... })を生成します:
type | 追加フィールド |
|---|---|
part-start | part: PartInit(下記参照) |
part-finish | なし |
tool-call-args-text-finish | なし |
text-delta | textDelta: string |
annotations | annotations: ReadonlyJSONValue[] |
data | data: ReadonlyJSONValue[] |
step-start | messageId: string |
step-finish | finishReason, usage: { inputTokens, outputTokens }, isContinued: boolean |
message-finish | finishReason, usage |
result | result, isError: boolean, artifact?, modelContent?, messages? |
error | error: string, code?, severity?: "critical" | "warning" | "info" |
update-state | operations: AssistantTransportStateOperation[](assistant-transport.md を参照) |
PartInit(part-start の part フィールド)は 6 つのパートタイプのいずれかで、すべてのバリアントが省略可能な parentId を持ちます:
type | 追加フィールド |
|---|---|
text | なし |
reasoning | unstable_summary?: string |
tool-call | toolCallId: string, toolName: string |
source | sourceType: "url", id, url, title? |
file | data: string, mimeType: string |
data | name: string, data: ReadonlyJSONValue |
よくある落とし穴
appendSource、appendFile、appendData がパートを黙って破棄する
typeフィールド("source"、"file"、"data")を含む完全なパートオブジェクトを渡してください。メソッド名がそれを暗黙に補ってくれるわけではありません。
UI でツール呼び出しがいつまでも完了しない
addToolCallPartにはtoolNameが必要です。id は自分で渡さない限り自動生成されます。argsTextを閉じる(または自動で閉じてくれるsetResponseを呼び出す)ようにしないと、パートは永遠に完了しません。レンダリングは非推奨のmakeAssistantToolUIではなく、"use generative"ツールキットで登録してください。詳しくは tools を参照してください。
クライアント上で 2 つの別々の推論パートが 1 つに統合されてしまう
- Data Stream のワイヤー上では、推論パートのパート開始フレームは
unstable_summaryが設定されている場合にのみ送信されます。単なるappendReasoning(text)呼び出しはテキストデルタとしてしか流れないため、デコーダーには新しいパートが始まったことを知る手がかりが他にありません。サマリーなしの推論パートを連続して 2 つ開くと(たとえばツール呼び出しを挟む場合など)、クライアント上では 1 つの連続した推論パートとして再構築されます。それぞれのパートにunstable_summaryを付ける(中身が空に感じられるものでも構いません)か、ツール呼び出しを別のメッセージステップに振り分けて、それぞれを別個に保ってください。
ストリームが UI を更新しない
- 実際に使用したエンコーダーに対して Content-Type を確認してください。
DataStreamEncoder(createAssistantStreamResponseのデフォルト)はtext/event-streamではなく、text/plain; charset=utf-8とx-vercel-ai-data-stream: v1を送信します。AssistantTransportEncoderと AI SDK の UI メッセージストリームはtext/event-streamを送信します。
デコーダーが「Stream ended abruptly without receiving [DONE] marker」をスローする
AssistantTransportDecoderとUIMessageStreamDecoderは終端の[DONE]センチネルを必要とします。ボディをバッファリングしたり切り詰めたりするプロキシ、CDN、ミドルウェアはこれを壊してしまいます。DataStreamDecoderにはそのようなマーカーはありません。
createAssistantStreamResponse は常に Data Stream としてエンコードする
- これは
DataStreamEncoderをハードコードしています。別のワイヤーフォーマットを使う場合は、手動でエンコードしてください:AssistantStream.toResponse(createAssistantStream(callback), new AssistantTransportEncoder())、またはcreateAssistantStreamControllerを使用して、返されたストリームを自分でエンコードしてください。