diff --git a/assistant-ui/skills/assistant-ui/SKILL.md b/assistant-ui/skills/assistant-ui/SKILL.md index 1987dee..a67bd18 100644 --- a/assistant-ui/skills/assistant-ui/SKILL.md +++ b/assistant-ui/skills/assistant-ui/SKILL.md @@ -7,7 +7,7 @@ license: MIT # assistant-ui -**Always consult [assistant-ui.com/docs](https://assistant-ui.com/docs) for latest API.** +**Always consult [assistant-ui.com/docs](https://assistant-ui.com/llms.txt) for latest API.** React library for building AI chat interfaces with composable primitives. diff --git a/assistant-ui/skills/assistant-ui/references/architecture.md b/assistant-ui/skills/assistant-ui/references/architecture.md index d396ed1..bf5d759 100644 --- a/assistant-ui/skills/assistant-ui/references/architecture.md +++ b/assistant-ui/skills/assistant-ui/references/architecture.md @@ -133,9 +133,30 @@ interface ThreadAssistantMessage { type MessagePart = | { type: "text"; text: string } | { type: "image"; image: string } - | { type: "tool-call"; toolCallId: string; toolName: string; args: unknown; result?: unknown } - | { type: "reasoning"; reasoning: string } - | { type: "source"; source: Source }; + | { + type: "tool-call"; + toolCallId: string; + toolName: string; + args: unknown; + argsText: string; + result?: unknown; + isError?: boolean; + artifact?: unknown; + } + | { type: "reasoning"; text: string } + | { + type: "source"; + sourceType: "url"; + id: string; + url: string; + title?: string; + } + | { + type: "file"; + filename?: string; + data: string; + mimeType: string; + }; ``` ## Branching Model diff --git a/assistant-ui/skills/assistant-ui/references/packages.md b/assistant-ui/skills/assistant-ui/references/packages.md index aedbbc5..aa14420 100644 --- a/assistant-ui/skills/assistant-ui/references/packages.md +++ b/assistant-ui/skills/assistant-ui/references/packages.md @@ -1,5 +1,36 @@ # assistant-ui Packages +## Published Packages + +**To check latest version:** Run `npm view version` or check the package on npmjs.com. + +- All published packages only expose the `latest` dist-tag (no `next/beta/canary`). +- Monorepo-only: `@assistant-ui/x-buildutils` (not on npm). + +| Package | Notes | +|---------|-------| +| @assistant-ui/react | Core UI library | +| @assistant-ui/react-ai-sdk | AI SDK v6 integration | +| @assistant-ui/react-langgraph | LangGraph integration | +| @assistant-ui/react-data-stream | Data stream utilities | +| @assistant-ui/react-markdown | Markdown rendering | +| @assistant-ui/react-syntax-highlighter | Code highlighting | +| @assistant-ui/styles | Pre-built CSS (no Tailwind) | +| @assistant-ui/store | State management | +| @assistant-ui/react-devtools | Developer tools | +| @assistant-ui/react-hook-form | React Hook Form integration | +| @assistant-ui/react-a2a | Agent-to-agent protocol | +| @assistant-ui/react-ag-ui | AG-UI protocol | +| @assistant-ui/tap | Testing utilities | +| @assistant-ui/mcp-docs-server | MCP documentation server | +| assistant-stream | Streaming protocol | +| assistant-cloud | Cloud persistence/auth | +| assistant-ui | CLI tool | +| create-assistant-ui | Project scaffolding | +| safe-content-frame | Sandboxed iframe content | +| tw-shimmer | Tailwind shimmer effects | +| chatgpt-app-studio | ChatGPT app builder | + ## Core Packages ### @assistant-ui/react diff --git a/assistant-ui/skills/cloud/references/persistence.md b/assistant-ui/skills/cloud/references/persistence.md index 0008615..e2d0842 100644 --- a/assistant-ui/skills/cloud/references/persistence.md +++ b/assistant-ui/skills/cloud/references/persistence.md @@ -141,9 +141,24 @@ interface AUIv0Message { type MessagePart = | { type: "text"; text: string } | { type: "image"; image: string } - | { type: "tool-call"; toolCallId: string; toolName: string; args: unknown; result?: unknown } - | { type: "reasoning"; reasoning: string } - | { type: "source"; source: { url: string; title: string } }; + | { + type: "tool-call"; + toolCallId: string; + toolName: string; + args: unknown; + argsText: string; + result?: unknown; + isError?: boolean; + artifact?: unknown; + } + | { type: "reasoning"; text: string } + | { + type: "source"; + sourceType: "url"; + id: string; + url: string; + title?: string; + }; ``` ## Thread History Adapter diff --git a/assistant-ui/skills/integrations/SKILL.md b/assistant-ui/skills/integrations/SKILL.md index 04a2ced..bf9767a 100644 --- a/assistant-ui/skills/integrations/SKILL.md +++ b/assistant-ui/skills/integrations/SKILL.md @@ -74,7 +74,7 @@ export async function POST(req: Request) { messages, }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse(); } ``` @@ -299,7 +299,7 @@ npm install @ai-sdk/react ``` **Streaming not working** -- Verify `toDataStreamResponse()` is used +- Verify `toUIMessageStreamResponse()` is used - Check Content-Type header - Look for CORS errors diff --git a/assistant-ui/skills/integrations/references/ai-sdk.md b/assistant-ui/skills/integrations/references/ai-sdk.md index 54dd4dc..597bb6e 100644 --- a/assistant-ui/skills/integrations/references/ai-sdk.md +++ b/assistant-ui/skills/integrations/references/ai-sdk.md @@ -46,7 +46,7 @@ export async function POST(req: Request) { messages, }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse(); } ``` @@ -95,13 +95,13 @@ const runtime = useChatRuntime({ ```ts import { openai } from "@ai-sdk/openai"; -import { streamText, tool } from "ai"; +import { streamText, tool, stepCountIs } from "ai"; import { z } from "zod"; const tools = { search: tool({ description: "Search the web for information", - parameters: z.object({ + inputSchema: z.object({ query: z.string().describe("Search query"), limit: z.number().optional().default(5), }), @@ -119,10 +119,10 @@ export async function POST(req: Request) { model: openai("gpt-4o"), messages, tools, - maxSteps: 5, // Allow multi-step tool use + stopWhen: stepCountIs(5), // Allow multi-step tool use }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse(); } ``` @@ -251,19 +251,24 @@ streamText({ ```ts import { z } from "zod"; -import { generateObject } from "ai"; +import { generateText, Output } from "ai"; +import { openai } from "@ai-sdk/openai"; -const result = await generateObject({ +const { output } = await generateText({ model: openai("gpt-4o"), - schema: z.object({ - name: z.string(), - age: z.number(), - hobbies: z.array(z.string()), + output: Output.object({ + schema: z.object({ + name: z.string(), + age: z.number(), + hobbies: z.array(z.string()), + }), }), prompt: "Generate a user profile", }); ``` +AI SDK v6 uses `generateText` + `Output.object` for structured output; `generateObject` is the older pattern. + ## Error Handling ```tsx @@ -329,6 +334,6 @@ export async function POST(req: Request) { : openai(model); const result = streamText({ model: provider, messages }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse(); } ``` diff --git a/assistant-ui/skills/integrations/references/custom-backend.md b/assistant-ui/skills/integrations/references/custom-backend.md index 0019bf4..4bf482a 100644 --- a/assistant-ui/skills/integrations/references/custom-backend.md +++ b/assistant-ui/skills/integrations/references/custom-backend.md @@ -4,10 +4,12 @@ Connect assistant-ui to any backend. ## Using useLocalRuntime -For backends that return streaming responses. +For backends that return streaming responses. Emit `ChatModelRunResult` chunks (append-only `content` parts). ### Basic Setup +Plain-text streaming only. For AI SDK Data Stream responses, use `toUIMessageStreamResponse()` + `useChatRuntime` or decode with `DataStreamDecoder` and convert to content parts. + ```tsx import { useLocalRuntime, AssistantRuntimeProvider, Thread } from "@assistant-ui/react"; @@ -24,17 +26,27 @@ function Chat() { const reader = response.body?.getReader(); const decoder = new TextDecoder(); + let buffer = ""; + + while (reader) { + const { done, value } = await reader.read(); + if (done) break; - while (reader) { - const { done, value } = await reader.read(); - if (done) break; + buffer += decoder.decode(value, { stream: true }); + const parts = buffer.split("\n"); + buffer = parts.pop() ?? ""; - const text = decoder.decode(value); - yield { type: "text-delta", textDelta: text }; + for (const textChunk of parts.filter(Boolean)) { + yield { content: [{ type: "text", text: textChunk }] }; } - }, + } + + if (buffer) { + yield { content: [{ type: "text", text: buffer }] }; + } }, - }); + }, +}); return ( @@ -46,6 +58,8 @@ function Chat() { ### With SSE Parsing +Simple SSE `data:` lines only (not AI SDK Data Stream prefixes like `0:`/`b:`/`c:`). + ```tsx const runtime = useLocalRuntime({ model: { @@ -73,7 +87,14 @@ const runtime = useLocalRuntime({ if (line === "data: [DONE]") return; const data = JSON.parse(line.slice(6)); - yield { type: "text-delta", textDelta: data.content }; + yield { content: [{ type: "text", text: data.content }] }; + } + } + + if (buffer.startsWith("data: ")) { + const data = JSON.parse(buffer.slice(6)); + if (data?.content) { + yield { content: [{ type: "text", text: data.content }] }; } } }, @@ -93,32 +114,41 @@ const runtime = useLocalRuntime({ signal: abortSignal, }); + const toolCalls = new Map< + string, + { toolCallId: string; toolName: string; args: unknown; argsText: string } + >(); + for await (const event of parseResponse(response)) { - switch (event.type) { - case "text": - yield { type: "text-delta", textDelta: event.content }; - break; - - case "tool_use": - yield { - type: "tool-call-begin", - toolCallId: event.id, - toolName: event.name, - }; - yield { - type: "tool-call-done", - toolCallId: event.id, - args: event.input, - }; - break; - - case "tool_result": - yield { - type: "tool-result", - toolCallId: event.tool_use_id, - result: event.content, - }; - break; + if (event.type === "text") { + yield { content: [{ type: "text", text: event.content }] }; + } + + if (event.type === "tool_use") { + const toolCall = { + toolCallId: event.id, + toolName: event.name, + args: event.input ?? {}, + argsText: JSON.stringify(event.input ?? {}), + }; + toolCalls.set(event.id, toolCall); + yield { content: [{ type: "tool-call", ...toolCall }] }; + } + + if (event.type === "tool_result") { + const toolCall = toolCalls.get(event.tool_use_id); + yield { + content: [ + { + type: "tool-call", + toolCallId: event.tool_use_id, + toolName: toolCall?.toolName ?? "tool", + args: toolCall?.args ?? {}, + argsText: toolCall?.argsText ?? "{}", + result: event.content, + }, + ], + }; } } }, diff --git a/assistant-ui/skills/integrations/references/langgraph.md b/assistant-ui/skills/integrations/references/langgraph.md index 975d747..c659e9d 100644 --- a/assistant-ui/skills/integrations/references/langgraph.md +++ b/assistant-ui/skills/integrations/references/langgraph.md @@ -196,24 +196,36 @@ async function* parseStream(response: Response) { switch (data.event) { case "on_chat_model_stream": yield { - type: "text-delta", - textDelta: data.data.chunk.content, + content: [{ type: "text", text: data.data.chunk.content }], }; break; case "on_tool_start": yield { - type: "tool-call-begin", - toolCallId: data.run_id, - toolName: data.name, + content: [ + { + type: "tool-call", + toolCallId: data.run_id, + toolName: data.name, + args: data.input ?? {}, + argsText: JSON.stringify(data.input ?? {}), + }, + ], }; break; case "on_tool_end": yield { - type: "tool-result", - toolCallId: data.run_id, - result: data.data.output, + content: [ + { + type: "tool-call", + toolCallId: data.run_id, + toolName: data.name, + args: data.input ?? {}, + argsText: JSON.stringify(data.input ?? {}), + result: data.data.output, + }, + ], }; break; } diff --git a/assistant-ui/skills/primitives/SKILL.md b/assistant-ui/skills/primitives/SKILL.md index 756f5e0..d0b1476 100644 --- a/assistant-ui/skills/primitives/SKILL.md +++ b/assistant-ui/skills/primitives/SKILL.md @@ -165,11 +165,11 @@ Use `.If` for conditional content: Reasoning: ({ part }) => (
Thinking... - {part.reasoning} + {part.text}
), Source: ({ part }) => ( - {part.source.title} + {part.title} ), }} /> diff --git a/assistant-ui/skills/primitives/references/message.md b/assistant-ui/skills/primitives/references/message.md index 2ab2674..e5c6230 100644 --- a/assistant-ui/skills/primitives/references/message.md +++ b/assistant-ui/skills/primitives/references/message.md @@ -61,17 +61,20 @@ Renders message content parts (text, images, tool calls, etc.). Reasoning: ({ part }) => (
Thinking... -

{part.reasoning}

+

{part.text}

), Source: ({ part }) => ( - - {part.source.title} + + {part.title} ), File: ({ part }) => ( - - 📄 {part.file.name} + + 📄 {part.filename ?? "file"} ), }} @@ -84,10 +87,10 @@ Renders message content parts (text, images, tool calls, etc.). |------|-------------|------------| | `Text` | Plain text | `text` | | `Image` | Image attachment | `image` (URL) | -| `ToolCall` | Tool invocation | `toolName`, `args`, `result`, `status` | -| `Reasoning` | Chain-of-thought | `reasoning` | -| `Source` | Citation/reference | `source.url`, `source.title` | -| `File` | File attachment | `file.name`, `file.url` | +| `ToolCall` | Tool invocation | `toolName`, `args`, `argsText`, `result?`, `isError?`, `artifact?` | +| `Reasoning` | Chain-of-thought | `text` | +| `Source` | Citation/reference | `url`, `title` | +| `File` | File attachment | `filename?`, `data`, `mimeType` | ## MessagePrimitive.Avatar diff --git a/assistant-ui/skills/runtime/SKILL.md b/assistant-ui/skills/runtime/SKILL.md index d585c7a..141a20a 100644 --- a/assistant-ui/skills/runtime/SKILL.md +++ b/assistant-ui/skills/runtime/SKILL.md @@ -142,10 +142,30 @@ type MessageStatus = type MessagePart = | { type: "text"; text: string } | { type: "image"; image: string } - | { type: "tool-call"; toolCallId: string; toolName: string; args: unknown; result?: unknown } - | { type: "reasoning"; reasoning: string } - | { type: "source"; source: { url: string; title: string } } - | { type: "file"; file: { name: string; url: string } }; + | { + type: "tool-call"; + toolCallId: string; + toolName: string; + args: unknown; + argsText: string; + result?: unknown; + isError?: boolean; + artifact?: unknown; + } + | { type: "reasoning"; text: string } + | { + type: "source"; + sourceType: "url"; + id: string; + url: string; + title?: string; + } + | { + type: "file"; + filename?: string; + data: string; + mimeType: string; + }; ``` ## Thread Operations diff --git a/assistant-ui/skills/runtime/references/local-runtime.md b/assistant-ui/skills/runtime/references/local-runtime.md index f0851be..112fc7b 100644 --- a/assistant-ui/skills/runtime/references/local-runtime.md +++ b/assistant-ui/skills/runtime/references/local-runtime.md @@ -36,7 +36,7 @@ function App() { ## Streaming Response -Use a generator for streaming: +Use a generator and emit `ChatModelRunResult` chunks (append-only content parts): ```tsx const runtime = useLocalRuntime({ @@ -50,13 +50,27 @@ const runtime = useLocalRuntime({ const reader = response.body?.getReader(); const decoder = new TextDecoder(); + let buffer = ""; while (reader) { const { done, value } = await reader.read(); if (done) break; - const text = decoder.decode(value); - yield { type: "text-delta", textDelta: text }; + buffer += decoder.decode(value, { stream: true }); + + // Split on newlines for this plain-text example (not Data Stream) + const parts = buffer.split("\n"); + buffer = parts.pop() ?? ""; + + for (const textChunk of parts.filter(Boolean)) { + yield { + content: [{ type: "text", text: textChunk }], + }; + } + } + + if (buffer) { + yield { content: [{ type: "text", text: buffer }] }; } }, }, @@ -98,13 +112,8 @@ interface ChatModelRunResultFinal { content: MessagePart[]; } -// Stream events -type ChatModelRunResultStream = - | { type: "text-delta"; textDelta: string } - | { type: "tool-call-begin"; toolCallId: string; toolName: string } - | { type: "tool-call-delta"; toolCallId: string; argsTextDelta: string } - | { type: "tool-call-done"; toolCallId: string; args: unknown } - | { type: "tool-result"; toolCallId: string; result: unknown }; +// Streamed chunks are ChatModelRunResult objects +type ChatModelRunResultStream = ChatModelRunResult; ``` ## With OpenAI Direct @@ -136,7 +145,7 @@ const runtime = useLocalRuntime({ if (abortSignal.aborted) break; const delta = chunk.choices[0]?.delta?.content; if (delta) { - yield { type: "text-delta", textDelta: delta }; + yield { content: [{ type: "text", text: delta }] }; } } }, @@ -146,25 +155,44 @@ const runtime = useLocalRuntime({ ## With Tools -```tsx -import { z } from "zod"; +Emit tool calls as message parts (`type: "tool-call"`) and include `argsText` plus optional `result`: +```tsx const runtime = useLocalRuntime({ model: { async *run({ messages, abortSignal }) { - // ... fetch from your API - - // Stream tool call - yield { type: "tool-call-begin", toolCallId: "1", toolName: "get_weather" }; - yield { type: "tool-call-delta", toolCallId: "1", argsTextDelta: '{"city":"NYC"}' }; - yield { type: "tool-call-done", toolCallId: "1", args: { city: "NYC" } }; - - // Execute tool and yield result + const toolCallId = "1"; + + // Yield tool call with parsed arguments + yield { + content: [ + { + type: "tool-call", + toolCallId, + toolName: "get_weather", + args: { city: "NYC" }, + argsText: '{"city":"NYC"}', + }, + ], + }; + + // Execute tool const result = await getWeather({ city: "NYC" }); - yield { type: "tool-result", toolCallId: "1", result }; - // Continue with text response - yield { type: "text-delta", textDelta: "The weather in NYC is..." }; + // Send result on the same tool-call part + yield { + content: [ + { + type: "tool-call", + toolCallId, + toolName: "get_weather", + args: { city: "NYC" }, + argsText: '{"city":"NYC"}', + result, + }, + { type: "text", text: `The weather in NYC is ${result.temp}°C` }, + ], + }; }, }, }); diff --git a/assistant-ui/skills/setup/SKILL.md b/assistant-ui/skills/setup/SKILL.md index f731385..a8c907d 100644 --- a/assistant-ui/skills/setup/SKILL.md +++ b/assistant-ui/skills/setup/SKILL.md @@ -77,7 +77,7 @@ export async function POST(req: Request) { messages, }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse(); } ``` @@ -211,7 +211,7 @@ npm install @ai-sdk/react@latest ai@latest ### Streaming not working 1. Check API returns correct Content-Type: `text/event-stream` -2. Verify `toDataStreamResponse()` is used (not `toTextStreamResponse()`) +2. Verify `toUIMessageStreamResponse()` is used 3. Check browser console for CORS errors ### "runtime is undefined" diff --git a/assistant-ui/skills/setup/references/ai-sdk-v6.md b/assistant-ui/skills/setup/references/ai-sdk-v6.md index 1def7be..594629f 100644 --- a/assistant-ui/skills/setup/references/ai-sdk-v6.md +++ b/assistant-ui/skills/setup/references/ai-sdk-v6.md @@ -59,7 +59,7 @@ export async function POST(req: Request) { messages, }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse(); } ``` @@ -107,13 +107,13 @@ const runtime = useChatRuntime({ ```ts // app/api/chat/route.ts import { openai } from "@ai-sdk/openai"; -import { streamText, tool } from "ai"; +import { streamText, tool, stepCountIs } from "ai"; import { z } from "zod"; const tools = { get_weather: tool({ description: "Get weather for a city", - parameters: z.object({ + inputSchema: z.object({ city: z.string().describe("City name"), unit: z.enum(["celsius", "fahrenheit"]).optional(), }), @@ -131,10 +131,10 @@ export async function POST(req: Request) { model: openai("gpt-4o"), messages, tools, - maxSteps: 5, // Allow multi-step tool use + stopWhen: stepCountIs(5), // Allow multi-step tool use }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse(); } ``` @@ -272,7 +272,7 @@ npm install @ai-sdk/react@latest ai@latest ``` **Streaming stops mid-response** -Check for `maxSteps` when using tools - default is 1. +Check `stopWhen` when using tools - use `stepCountIs(n)` to allow multi-step. **Tool results not showing** Ensure you return from tool.execute(), not just mutate state. diff --git a/assistant-ui/skills/setup/references/langgraph.md b/assistant-ui/skills/setup/references/langgraph.md index 86bc1ce..d6ca61d 100644 --- a/assistant-ui/skills/setup/references/langgraph.md +++ b/assistant-ui/skills/setup/references/langgraph.md @@ -122,17 +122,10 @@ const runtime = useLangGraphRuntime({ ## LangGraph Event Types -The stream function should yield events in this format: - -```typescript -type LangGraphEvent = - | { type: "text-delta"; textDelta: string } - | { type: "tool-call-begin"; toolCallId: string; toolName: string } - | { type: "tool-call-delta"; toolCallId: string; argsTextDelta: string } - | { type: "tool-call-done"; toolCallId: string; args: unknown } - | { type: "tool-result"; toolCallId: string; result: unknown } - | { type: "message-done"; message: ThreadMessage }; -``` +The stream callback should yield append-only content updates (same shape as `ChatModelRunResult` content parts). Common cases: + +- Text: `{ content: [{ type: "text", text: "partial text" }] }` +- Tool call start/result (single part): `{ content: [{ type: "tool-call", toolCallId, toolName, args, argsText, result? }] }` ## With Tool UI diff --git a/assistant-ui/skills/streaming/SKILL.md b/assistant-ui/skills/streaming/SKILL.md index 6453822..1004f61 100644 --- a/assistant-ui/skills/streaming/SKILL.md +++ b/assistant-ui/skills/streaming/SKILL.md @@ -21,7 +21,7 @@ The `assistant-stream` package handles streaming from AI backends. ``` Using Vercel AI SDK? -├─ Yes → Data Stream format (toDataStreamResponse) +├─ Yes → UI Message Stream (toUIMessageStreamResponse) └─ No ├─ Want richest features? │ └─ Yes → Assistant Transport format @@ -39,11 +39,11 @@ npm install assistant-stream | Format | Use Case | Features | |--------|----------|----------| -| Data Stream | AI SDK apps | Tool calls, multi-step | -| Assistant Transport | Custom backends | All features, optimized | +| UI Message Stream | AI SDK + assistant-ui | Tool calls, multi-step, optimized | +| Assistant Transport | Custom backends | All features, native format | | Plain Text | Simple text | Basic streaming | -## AI SDK Data Stream (Most Common) +## AI SDK UI Message Stream (Recommended) ### Backend @@ -60,7 +60,7 @@ export async function POST(req: Request) { messages, }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse(); } ``` @@ -77,101 +77,45 @@ const runtime = useChatRuntime({ ## Custom Streaming Response -### Using DataStreamEncoder +### Using AssistantStream helpers (Data Stream SSE) ```ts -import { DataStreamEncoder } from "assistant-stream"; +import { createAssistantStreamResponse } from "assistant-stream"; export async function POST(req: Request) { - const encoder = new DataStreamEncoder(); + return createAssistantStreamResponse(async (stream) => { + stream.appendText("Hello "); + stream.appendText("world!"); - const stream = new ReadableStream({ - async start(controller) { - // Stream text - encoder.writeTextDelta("Hello "); - controller.enqueue(encoder.flush()); + // Optional: tool call + const tool = stream.addToolCallPart({ toolCallId: "1", toolName: "get_weather" }); + tool.argsText.append('{"city":"NYC"}'); + tool.argsText.close(); + tool.setResponse({ result: { temperature: 22, unit: "celsius" } }); - await delay(100); - - encoder.writeTextDelta("world!"); - controller.enqueue(encoder.flush()); - - // Complete - encoder.close(); - controller.enqueue(encoder.flush()); - controller.close(); - }, - }); - - return new Response(stream, { - headers: { "Content-Type": "text/event-stream" }, + stream.close(); // flush + finish message }); } ``` ## Stream Events -### Text Events +AssistantStream chunks (decoded from Data Stream) look like: -```ts -// Streaming text -{ type: "text-delta", textDelta: "partial text" } - -// Completion markers -{ type: "text-created" } -{ type: "text-done", text: "full text" } -``` - -### Tool Call Events - -```ts -// Tool call lifecycle -{ type: "tool-call-begin", toolCallId: "1", toolName: "search" } -{ type: "tool-call-delta", toolCallId: "1", argsTextDelta: '{"query":' } -{ type: "tool-call-delta", toolCallId: "1", argsTextDelta: '"NYC"}' } -{ type: "tool-call-done", toolCallId: "1", args: { query: "NYC" } } - -// Tool result -{ type: "tool-result", toolCallId: "1", result: { ... } } -``` - -### Control Events - -```ts -{ type: "error", error: "Error message" } -{ type: "finish", finishReason: "stop" } // or "length", "tool-calls" -``` - -## AssistantStream Class - -Core streaming abstraction: - -```ts -import { AssistantStream } from "assistant-stream"; - -// From async generator -const stream = AssistantStream.fromAsyncIterable(async function* () { - yield { type: "text-delta", textDelta: "Hello " }; - yield { type: "text-delta", textDelta: "world!" }; -}); - -// From ReadableStream -const stream = AssistantStream.fromReadableStream(responseStream); - -// From Response -const stream = AssistantStream.fromResponse(response); - -// Consume -for await (const event of stream) { - console.log(event); -} -``` +- `part-start` with `part.type` = `"text" | "reasoning" | "tool-call" | "source" | "file"` +- `part-finish` and `tool-call-args-text-finish` +- `text-delta` with streamed text +- `result` with tool results (`result`, `artifact?`, `isError`) +- `annotations` / `data` arrays +- `step-start`, `step-finish`, `message-finish` (AI SDK bookkeeping) +- `error` strings ## Using with useLocalRuntime +`useLocalRuntime` expects `ChatModelRunResult` chunks (content parts). Stream by yielding text/tool-call parts: + ```tsx import { useLocalRuntime } from "@assistant-ui/react"; -import { AssistantStream } from "assistant-stream"; const runtime = useLocalRuntime({ model: { @@ -183,10 +127,26 @@ const runtime = useLocalRuntime({ signal: abortSignal, }); - const stream = AssistantStream.fromResponse(response); + const reader = response.body?.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + while (reader) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + // Plain-text streaming only (not AI SDK Data Stream SSE) + const parts = buffer.split("\n"); + buffer = parts.pop() ?? ""; - for await (const event of stream) { - yield event; + for (const textChunk of parts.filter(Boolean)) { + yield { content: [{ type: "text", text: textChunk }] }; + } + } + + if (buffer) { + yield { content: [{ type: "text", text: buffer }] }; } }, }, @@ -196,32 +156,25 @@ const runtime = useLocalRuntime({ ## Tool Handling ```ts -import { Tool } from "assistant-stream"; -import { z } from "zod"; - -const weatherTool = new Tool({ - name: "get_weather", - description: "Get weather for a city", - parameters: z.object({ city: z.string() }), - execute: async ({ city }) => { - return { temperature: 22, unit: "celsius" }; - }, -}); +import { createAssistantStreamResponse } from "assistant-stream"; + +export async function POST(req: Request) { + return createAssistantStreamResponse(async (stream) => { + const tool = stream.addToolCallPart({ + toolCallId: "1", + toolName: "get_weather", + }); -// Use in streaming context -const stream = AssistantStream.fromAsyncIterable(async function* () { - // Tool call - yield { type: "tool-call-begin", toolCallId: "1", toolName: "get_weather" }; - yield { type: "tool-call-delta", toolCallId: "1", argsTextDelta: '{"city":"NYC"}' }; - yield { type: "tool-call-done", toolCallId: "1", args: { city: "NYC" } }; + tool.argsText.append('{"city":"NYC"}'); + tool.argsText.close(); - // Execute and yield result - const result = await weatherTool.execute({ city: "NYC" }); - yield { type: "tool-result", toolCallId: "1", result }; + const result = await fetchWeather("NYC"); + tool.setResponse({ result }); - // Continue with response - yield { type: "text-delta", textDelta: "The weather in NYC is 22°C" }; -}); + stream.appendText("The weather in NYC is 22°C"); + stream.close(); + }); +} ``` ## Debugging Streams @@ -229,7 +182,9 @@ const stream = AssistantStream.fromAsyncIterable(async function* () { ### Log All Events ```ts -const stream = AssistantStream.fromResponse(response); +import { AssistantStream, DataStreamDecoder } from "assistant-stream"; + +const stream = AssistantStream.fromResponse(response, new DataStreamDecoder()); for await (const event of stream) { console.log("Event:", JSON.stringify(event, null, 2)); @@ -263,7 +218,7 @@ headers: { - Check for CORS errors in browser console **Tool calls not rendering** -- Ensure `tool-call-begin` includes `toolCallId` and `toolName` +- Ensure `addToolCallPart` sets both `toolCallId` and `toolName` - Register tool UI with `makeAssistantToolUI` - Check tool name matches exactly diff --git a/assistant-ui/skills/streaming/references/assistant-transport.md b/assistant-ui/skills/streaming/references/assistant-transport.md index 56cc551..7b8dfa2 100644 --- a/assistant-ui/skills/streaming/references/assistant-transport.md +++ b/assistant-ui/skills/streaming/references/assistant-transport.md @@ -17,235 +17,80 @@ Assistant Transport is assistant-ui's optimized format with support for all feat ### Server ```ts -import { AssistantTransportEncoder } from "assistant-stream"; +import { + AssistantStream, + AssistantTransportEncoder, + createAssistantStreamController, +} from "assistant-stream"; export async function POST(req: Request) { - const encoder = new AssistantTransportEncoder(); - - const stream = new ReadableStream({ - async start(controller) { - // Start message - encoder.writeMessageStart({ - id: "msg_1", - role: "assistant", - }); - controller.enqueue(encoder.flush()); - - // Stream text - encoder.writeTextDelta("Hello "); - controller.enqueue(encoder.flush()); - - encoder.writeTextDelta("world!"); - controller.enqueue(encoder.flush()); - - // Complete message - encoder.writeMessageDone({ - id: "msg_1", - role: "assistant", - content: [{ type: "text", text: "Hello world!" }], - status: "complete", - }); - controller.enqueue(encoder.flush()); + const [stream, controller] = createAssistantStreamController(); - encoder.close(); - controller.close(); - }, - }); + controller.appendText("Hello "); + controller.appendText("world!"); + controller.close(); - return new Response(stream, { - headers: { "Content-Type": "text/event-stream" }, - }); + return AssistantStream.toResponse(stream, new AssistantTransportEncoder()); } ``` ### Client ```ts -import { AssistantTransportDecoder } from "assistant-stream"; - -const decoder = new AssistantTransportDecoder(); - -for await (const event of decoder.decode(responseStream)) { - switch (event.type) { - case "message-start": - console.log("Message started:", event.message.id); - break; - case "text-delta": - console.log("Text:", event.textDelta); - break; - case "message-done": - console.log("Message complete:", event.message); - break; - } -} -``` - -## AssistantTransportEncoder API - -```ts -const encoder = new AssistantTransportEncoder(); - -// Message lifecycle -encoder.writeMessageStart(message: Partial); -encoder.writeMessageDone(message: ThreadMessage); - -// Text -encoder.writeTextDelta(text: string); +import { AssistantStream, AssistantTransportDecoder } from "assistant-stream"; -// Tool calls -encoder.writeToolCallBegin(toolCallId: string, toolName: string); -encoder.writeToolCallDelta(toolCallId: string, argsTextDelta: string); -encoder.writeToolCallDone(toolCallId: string, args: object); -encoder.writeToolResult(toolCallId: string, result: unknown); +const stream = AssistantStream.fromResponse( + response, + new AssistantTransportDecoder() +); -// Rich content -encoder.writeReasoning(reasoning: string); -encoder.writeSource(source: { url: string; title: string }); -encoder.writeImage(url: string); - -// Control -encoder.writeError(message: string); - -// Get encoded data -const bytes = encoder.flush(); -encoder.close(); -``` - -## Event Types - -### Message Events - -```ts -// Start -{ - type: "message-start", - message: { - id: "msg_1", - role: "assistant", - } -} - -// Done -{ - type: "message-done", - message: { - id: "msg_1", - role: "assistant", - content: [{ type: "text", text: "Full response" }], - status: "complete", - createdAt: "2024-01-01T00:00:00Z" - } +for await (const chunk of stream) { + console.log(chunk); } ``` -### Content Events +## Event Types -```ts -// Text -{ type: "text-delta", textDelta: "Hello " } - -// Reasoning (chain-of-thought) -{ type: "reasoning", reasoning: "Let me think about this..." } - -// Source citation -{ - type: "source", - source: { - url: "https://example.com", - title: "Example Source" - } -} +### Chunk Shapes -// Image -{ type: "image", image: "https://..." } -``` +AssistantStream chunks (decoded from AssistantTransport) match the core types: -### Tool Events - -```ts -{ type: "tool-call-begin", toolCallId: "1", toolName: "search" } -{ type: "tool-call-delta", toolCallId: "1", argsTextDelta: '...' } -{ type: "tool-call-done", toolCallId: "1", args: {...} } -{ type: "tool-result", toolCallId: "1", result: {...} } -``` +- `part-start` with `part.type` = `"text" | "reasoning" | "tool-call" | "source" | "file"` +- `part-finish` and `tool-call-args-text-finish` +- `text-delta` / `annotations` / `data` +- `result` (tool results) +- `step-start` / `step-finish` / `message-finish` +- `error` ## Complete Example ```ts -import { AssistantTransportEncoder } from "assistant-stream"; +import { + AssistantStream, + AssistantTransportEncoder, + createAssistantStreamController, +} from "assistant-stream"; async function streamResponse(query: string) { - const encoder = new AssistantTransportEncoder(); - const messageId = `msg_${Date.now()}`; - - const stream = new ReadableStream({ - async start(controller) { - // Start message - encoder.writeMessageStart({ id: messageId, role: "assistant" }); - controller.enqueue(encoder.flush()); - - // Reasoning (if model supports it) - encoder.writeReasoning("I need to search for information about " + query); - controller.enqueue(encoder.flush()); - - // Tool call - const toolCallId = `tool_${Date.now()}`; - encoder.writeToolCallBegin(toolCallId, "search"); - controller.enqueue(encoder.flush()); - - encoder.writeToolCallDelta(toolCallId, JSON.stringify({ query })); - controller.enqueue(encoder.flush()); - - encoder.writeToolCallDone(toolCallId, { query }); - controller.enqueue(encoder.flush()); - - // Execute tool and get result - const searchResult = await performSearch(query); - encoder.writeToolResult(toolCallId, searchResult); - controller.enqueue(encoder.flush()); - - // Stream response with sources - encoder.writeTextDelta("Based on my search, "); - controller.enqueue(encoder.flush()); - - encoder.writeTextDelta("here's what I found:\n\n"); - controller.enqueue(encoder.flush()); - - encoder.writeTextDelta(searchResult.summary); - controller.enqueue(encoder.flush()); - - // Add source citations - for (const source of searchResult.sources) { - encoder.writeSource({ - url: source.url, - title: source.title, - }); - controller.enqueue(encoder.flush()); - } + const [stream, controller] = createAssistantStreamController(); + const toolCallId = `tool_${Date.now()}`; - // Complete message - encoder.writeMessageDone({ - id: messageId, - role: "assistant", - content: [ - { type: "reasoning", reasoning: "..." }, - { type: "tool-call", toolCallId, toolName: "search", args: { query }, result: searchResult }, - { type: "text", text: "Based on my search, here's what I found:\n\n" + searchResult.summary }, - ...searchResult.sources.map(s => ({ type: "source", source: s })), - ], - status: "complete", - createdAt: new Date(), - }); - controller.enqueue(encoder.flush()); + controller.appendText("Based on my search, "); - encoder.close(); - controller.close(); - }, + const tool = controller.addToolCallPart({ + toolCallId, + toolName: "search", }); + tool.argsText.append(JSON.stringify({ query })); + tool.argsText.close(); - return new Response(stream, { - headers: { "Content-Type": "text/event-stream" }, - }); + const searchResult = await performSearch(query); + tool.setResponse({ result: searchResult }); + + controller.appendText(`here's what I found:\n\n${searchResult.summary}`); + controller.close(); + + return AssistantStream.toResponse(stream, new AssistantTransportEncoder()); } ``` @@ -253,7 +98,7 @@ async function streamResponse(query: string) { ```tsx import { useLocalRuntime } from "@assistant-ui/react"; -import { AssistantTransportDecoder } from "assistant-stream"; +import { AssistantStream, AssistantTransportDecoder } from "assistant-stream"; const runtime = useLocalRuntime({ model: { @@ -264,10 +109,50 @@ const runtime = useLocalRuntime({ signal: abortSignal, }); - const decoder = new AssistantTransportDecoder(); - - for await (const event of decoder.decode(response.body)) { - yield event; + let currentTool: + | { + toolCallId: string; + toolName: string; + args: Record; + argsText: string; + } + | undefined; + + const stream = AssistantStream.fromResponse( + response, + new AssistantTransportDecoder() + ); + + for await (const chunk of stream) { + // Convert AssistantStreamChunk into ChatModelRunResult content parts + if (chunk.type === "text-delta") { + yield { content: [{ type: "text", text: chunk.textDelta }] }; + } + + // Track current tool-call to attach result to it + if (chunk.type === "part-start" && chunk.part.type === "tool-call") { + currentTool = { + toolCallId: chunk.part.toolCallId, + toolName: chunk.part.toolName, + args: {}, + argsText: "{}", + }; + yield { content: [currentTool] }; + } + + if (chunk.type === "result" && currentTool) { + yield { + content: [ + { + ...currentTool, + result: chunk.result, + artifact: chunk.artifact, + isError: chunk.isError, + }, + ], + }; + currentTool = undefined; + } } }, }, diff --git a/assistant-ui/skills/streaming/references/data-stream.md b/assistant-ui/skills/streaming/references/data-stream.md index 6b56bbe..4dc7f8a 100644 --- a/assistant-ui/skills/streaming/references/data-stream.md +++ b/assistant-ui/skills/streaming/references/data-stream.md @@ -4,7 +4,7 @@ AI SDK compatible streaming format. ## Overview -Data Stream is the format used by Vercel AI SDK's `toDataStreamResponse()`. It's the most common format when using AI SDK. +Data Stream is the underlying format used by Vercel AI SDK. For assistant-ui, use `toUIMessageStreamResponse()` (preferred) which builds on Data Stream with additional features. ## Usage @@ -22,41 +22,36 @@ export async function POST(req: Request) { messages, }); - return result.toDataStreamResponse(); + // Preferred for assistant-ui + return result.toUIMessageStreamResponse(); + + // Or use toDataStreamResponse() for raw Data Stream + // return result.toDataStreamResponse(); } ``` -### Manual Encoding +### Custom Backend (Data Stream SSE) ```ts -import { DataStreamEncoder } from "assistant-stream"; +import { createAssistantStreamResponse } from "assistant-stream"; export async function POST(req: Request) { - const encoder = new DataStreamEncoder(); - - const stream = new ReadableStream({ - async start(controller) { - // Text streaming - encoder.writeTextDelta("Hello "); - controller.enqueue(encoder.flush()); - - encoder.writeTextDelta("world!"); - controller.enqueue(encoder.flush()); - - // Finish - encoder.writeFinish("stop"); - controller.enqueue(encoder.flush()); - - encoder.close(); - controller.close(); - }, - }); - - return new Response(stream, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - }, + return createAssistantStreamResponse(async (stream) => { + // Text + stream.appendText("Hello "); + stream.appendText("world!"); + + // Tool call + const tool = stream.addToolCallPart({ + toolCallId: "call_123", + toolName: "search", + }); + tool.argsText.append('{"query":"weather NYC"}'); + tool.argsText.close(); + tool.setResponse({ result: { temperature: 22 } }); + + // Finish + stream.close(); }); } ``` @@ -64,165 +59,61 @@ export async function POST(req: Request) { ### Decoding ```ts -import { DataStreamDecoder } from "assistant-stream"; - -const decoder = new DataStreamDecoder(); - -for await (const event of decoder.decode(responseStream)) { - switch (event.type) { - case "text-delta": - console.log("Text:", event.textDelta); - break; - case "tool-call-begin": - console.log("Tool call started:", event.toolName); - break; - case "tool-result": - console.log("Tool result:", event.result); - break; - case "finish": - console.log("Done:", event.finishReason); - break; - } -} -``` - -## DataStreamEncoder API - -```ts -const encoder = new DataStreamEncoder(); - -// Text -encoder.writeTextCreated(); -encoder.writeTextDelta(text: string); -encoder.writeTextDone(fullText: string); - -// Tool calls -encoder.writeToolCallBegin(toolCallId: string, toolName: string); -encoder.writeToolCallDelta(toolCallId: string, argsTextDelta: string); -encoder.writeToolCallDone(toolCallId: string, args: object); -encoder.writeToolResult(toolCallId: string, result: unknown); - -// Control -encoder.writeFinish(reason: "stop" | "length" | "tool-calls"); -encoder.writeError(message: string); - -// Get encoded data -const bytes = encoder.flush(); - -// Close stream -encoder.close(); -``` - -## Event Types - -### Text Events - -```ts -// Created (optional, signals start) -{ type: "text-created" } - -// Delta (streaming content) -{ type: "text-delta", textDelta: "Hello " } -{ type: "text-delta", textDelta: "world!" } +import { AssistantStream, DataStreamDecoder } from "assistant-stream"; -// Done (optional, final content) -{ type: "text-done", text: "Hello world!" } -``` - -### Tool Events - -```ts -// Begin -{ - type: "tool-call-begin", - toolCallId: "call_abc123", - toolName: "search" -} +const stream = AssistantStream.fromResponse(response, new DataStreamDecoder()); -// Arguments streaming -{ - type: "tool-call-delta", - toolCallId: "call_abc123", - argsTextDelta: '{"query":' -} -{ - type: "tool-call-delta", - toolCallId: "call_abc123", - argsTextDelta: '"NYC"}' +for await (const chunk of stream) { + if (chunk.type === "text-delta") console.log("Text:", chunk.textDelta); + if (chunk.type === "result") console.log("Result:", chunk.result); } +``` -// Complete -{ - type: "tool-call-done", - toolCallId: "call_abc123", - args: { query: "NYC" } -} +## AssistantStreamController (what you get in createAssistantStreamResponse) -// Result -{ - type: "tool-result", - toolCallId: "call_abc123", - result: { results: [...] } -} -``` +- `appendText(text: string)` +- `appendReasoning(reasoning: string)` +- `appendSource({ sourceType: "url", id, url, title?, parentId? })` +- `appendFile({ data, mimeType })` +- `addToolCallPart({ toolCallId, toolName, parentId? })` → controller with: + - `argsText.append(text)`, `argsText.close()` + - `setResponse({ result, artifact?, isError? })` + - `close()` +- `close()` to end the message -### Control Events +## Event Types -```ts -// Finish reasons -{ type: "finish", finishReason: "stop" } // Normal completion -{ type: "finish", finishReason: "length" } // Max tokens reached -{ type: "finish", finishReason: "tool-calls" } // Tool use +Decoded `AssistantStreamChunk` shapes: -// Error -{ type: "error", error: "Rate limit exceeded" } -``` +- `part-start` with `part.type` = `"text" | "reasoning" | "tool-call" | "source" | "file"` +- `part-finish` and `tool-call-args-text-finish` +- `text-delta` / `annotations` / `data` +- `result` (tool results) +- `step-start` / `step-finish` / `message-finish` +- `error` ## With Tools Example ```ts -import { DataStreamEncoder } from "assistant-stream"; +import { createAssistantStreamResponse } from "assistant-stream"; async function streamWithTools(req: Request) { - const encoder = new DataStreamEncoder(); - - const stream = new ReadableStream({ - async start(controller) { - // Initial text - encoder.writeTextDelta("Let me search for that...\n\n"); - controller.enqueue(encoder.flush()); - - // Tool call - const toolCallId = "call_" + Date.now(); - encoder.writeToolCallBegin(toolCallId, "search"); - controller.enqueue(encoder.flush()); - - encoder.writeToolCallDelta(toolCallId, '{"query":"weather NYC"}'); - controller.enqueue(encoder.flush()); - - encoder.writeToolCallDone(toolCallId, { query: "weather NYC" }); - controller.enqueue(encoder.flush()); - - // Execute tool - const result = await searchWeather("NYC"); - - encoder.writeToolResult(toolCallId, result); - controller.enqueue(encoder.flush()); - - // Continue with response - encoder.writeTextDelta(`The current weather in NYC is ${result.temp}°F`); - controller.enqueue(encoder.flush()); - - encoder.writeFinish("stop"); - controller.enqueue(encoder.flush()); - - encoder.close(); - controller.close(); - }, - }); - - return new Response(stream, { - headers: { "Content-Type": "text/event-stream" }, + return createAssistantStreamResponse(async (stream) => { + stream.appendText("Let me search for that...\n\n"); + + const toolCallId = "call_" + Date.now(); + const tool = stream.addToolCallPart({ + toolCallId, + toolName: "search", + }); + tool.argsText.append('{"query":"weather NYC"}'); + tool.argsText.close(); + + const result = await searchWeather("NYC"); + tool.setResponse({ result }); + + stream.appendText(`The current weather in NYC is ${result.temp}°F`); + stream.close(); }); } ``` @@ -234,15 +125,21 @@ Data Stream uses Server-Sent Events (SSE) format: ``` 0:"Hello " 0:"world!" -e:{"finishReason":"stop"} +d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}} ``` Each line: - `0:` - Text content - `9:` - Tool call +- `b:` - Tool call start +- `c:` - Tool call args text delta - `a:` - Tool result -- `e:` - Finish event +- `d:` - Message finish +- `e:` - Step finish - `3:` - Error +- `h:` - Source +- `k:` - File +- `aui-*` - assistant-ui extensions (state updates, parented deltas) ## Integration with useChatRuntime diff --git a/assistant-ui/skills/streaming/references/encoders.md b/assistant-ui/skills/streaming/references/encoders.md index 8f399a2..1e426db 100644 --- a/assistant-ui/skills/streaming/references/encoders.md +++ b/assistant-ui/skills/streaming/references/encoders.md @@ -6,55 +6,27 @@ Encode and decode streaming formats. | Encoder | Format | Use Case | |---------|--------|----------| -| `DataStreamEncoder` | AI SDK format | Most common, AI SDK compatible | -| `AssistantTransportEncoder` | Native format | All features, custom backends | -| `PlainTextEncoder` | Simple text | Basic text streaming | +| `DataStreamEncoder` | AI SDK Data Stream | Default (used by `toUIMessageStreamResponse`) | +| `AssistantTransportEncoder` | Native SSE (`data: {chunk}`) | Custom backends that want all chunk types | +| `PlainTextEncoder` | Text-only | Very simple demos | ## DataStreamEncoder -AI SDK compatible format. +AI SDK compatible format. You normally don't call it directly—wrap an `AssistantStream`: ```ts -import { DataStreamEncoder, DataStreamDecoder } from "assistant-stream"; +import { AssistantStream, DataStreamEncoder, DataStreamDecoder } from "assistant-stream"; -// Encoding -const encoder = new DataStreamEncoder(); -encoder.writeTextDelta("Hello "); -encoder.writeTextDelta("world!"); -encoder.writeFinish("stop"); -const bytes = encoder.flush(); -encoder.close(); +// Server +const response = AssistantStream.toResponse(stream, new DataStreamEncoder()); -// Decoding -const decoder = new DataStreamDecoder(); -for await (const event of decoder.decode(stream)) { - console.log(event); +// Client +const stream = AssistantStream.fromResponse(response, new DataStreamDecoder()); +for await (const chunk of stream) { + console.log(chunk); } ``` -### Methods - -```ts -// Text -encoder.writeTextCreated() -encoder.writeTextDelta(text: string) -encoder.writeTextDone(fullText: string) - -// Tools -encoder.writeToolCallBegin(toolCallId: string, toolName: string) -encoder.writeToolCallDelta(toolCallId: string, argsTextDelta: string) -encoder.writeToolCallDone(toolCallId: string, args: object) -encoder.writeToolResult(toolCallId: string, result: unknown) - -// Control -encoder.writeFinish(reason: "stop" | "length" | "tool-calls") -encoder.writeError(message: string) - -// Output -encoder.flush(): Uint8Array -encoder.close() -``` - ## AssistantTransportEncoder Native assistant-ui format with all features. @@ -65,50 +37,16 @@ import { AssistantTransportDecoder, } from "assistant-stream"; -// Encoding -const encoder = new AssistantTransportEncoder(); -encoder.writeMessageStart({ id: "1", role: "assistant" }); -encoder.writeTextDelta("Response text"); -encoder.writeReasoning("Thinking process..."); -encoder.writeSource({ url: "...", title: "..." }); -encoder.writeMessageDone({ ... }); -const bytes = encoder.flush(); -encoder.close(); +// Encoding: wrap AssistantStream chunks +const response = AssistantStream.toResponse(stream, new AssistantTransportEncoder()); // Decoding -const decoder = new AssistantTransportDecoder(); -for await (const event of decoder.decode(stream)) { - console.log(event); +const stream = AssistantStream.fromResponse(response, new AssistantTransportDecoder()); +for await (const chunk of stream) { + console.log(chunk); } ``` -### Methods - -```ts -// Message lifecycle -encoder.writeMessageStart(message: Partial) -encoder.writeMessageDone(message: ThreadMessage) - -// Content -encoder.writeTextDelta(text: string) -encoder.writeReasoning(reasoning: string) -encoder.writeSource(source: { url: string; title: string }) -encoder.writeImage(url: string) - -// Tools -encoder.writeToolCallBegin(toolCallId: string, toolName: string) -encoder.writeToolCallDelta(toolCallId: string, argsTextDelta: string) -encoder.writeToolCallDone(toolCallId: string, args: object) -encoder.writeToolResult(toolCallId: string, result: unknown) - -// Control -encoder.writeError(message: string) - -// Output -encoder.flush(): Uint8Array -encoder.close() -``` - ## PlainTextEncoder Simple text-only streaming. @@ -144,88 +82,35 @@ for await (const update of decoder.decode(stream)) { ## Creating Custom Streams -### From Async Generator - -```ts -import { AssistantStream } from "assistant-stream"; - -const stream = AssistantStream.fromAsyncIterable(async function* () { - yield { type: "text-delta", textDelta: "Hello " }; - await delay(100); - yield { type: "text-delta", textDelta: "world!" }; -}); -``` - -### From ReadableStream - -```ts -const stream = AssistantStream.fromReadableStream(readableStream); -``` - ### From Response ```ts const response = await fetch("/api/chat", { ... }); -const stream = AssistantStream.fromResponse(response); +const stream = AssistantStream.fromResponse(response, new DataStreamDecoder()); ``` ## Server Response Helpers ### Create Streaming Response +Use `createAssistantStreamController` to build an `AssistantStream` and encode it: + ```ts -function createStreamingResponse( - generator: AsyncGenerator, - encoder: DataStreamEncoder | AssistantTransportEncoder -) { - const stream = new ReadableStream({ - async start(controller) { - try { - for await (const event of generator) { - // Write event based on type - if (event.type === "text-delta") { - encoder.writeTextDelta(event.textDelta); - } - // ... handle other event types - controller.enqueue(encoder.flush()); - } - } finally { - encoder.close(); - controller.close(); - } - }, - }); - - return new Response(stream, { - headers: { - "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", - "Connection": "keep-alive", - }, - }); -} -``` +import { + AssistantStream, + AssistantTransportEncoder, + createAssistantStreamController, +} from "assistant-stream"; -### Error Handling +export async function POST() { + const [stream, controller] = createAssistantStreamController(); -```ts -const stream = new ReadableStream({ - async start(controller) { - try { - for await (const chunk of source) { - encoder.writeTextDelta(chunk); - controller.enqueue(encoder.flush()); - } - encoder.writeFinish("stop"); - } catch (error) { - encoder.writeError(error.message); - } finally { - controller.enqueue(encoder.flush()); - encoder.close(); - controller.close(); - } - }, -}); + controller.appendText("Hello "); + controller.appendText("world!"); + controller.close(); + + return AssistantStream.toResponse(stream, new AssistantTransportEncoder()); +} ``` ## Debugging diff --git a/assistant-ui/skills/tools/SKILL.md b/assistant-ui/skills/tools/SKILL.md index 3a2bbf9..160c5c7 100644 --- a/assistant-ui/skills/tools/SKILL.md +++ b/assistant-ui/skills/tools/SKILL.md @@ -41,7 +41,7 @@ import { z } from "zod"; const tools = { get_weather: tool({ description: "Get weather for a city", - parameters: z.object({ city: z.string() }), + inputSchema: z.object({ city: z.string() }), execute: async ({ city }) => ({ temp: 22, city }), }), }; @@ -136,13 +136,13 @@ useAssistantToolUI({ toolName, render }); ```ts // app/api/chat/route.ts import { openai } from "@ai-sdk/openai"; -import { streamText, tool } from "ai"; +import { streamText, tool, stepCountIs } from "ai"; import { z } from "zod"; const tools = { search: tool({ description: "Search the web", - parameters: z.object({ + inputSchema: z.object({ query: z.string(), limit: z.number().optional().default(5), }), @@ -160,10 +160,10 @@ export async function POST(req: Request) { model: openai("gpt-4o"), messages, tools, - maxSteps: 5, // Allow multi-step tool use + stopWhen: stepCountIs(5), // Allow multi-step tool use }); - return result.toDataStreamResponse(); + return result.toUIMessageStreamResponse(); } ``` @@ -338,7 +338,7 @@ function MyToolComponent() { **Tool not being called** - Check backend tool description is clear -- Verify `maxSteps` allows tool use +- Verify `stopWhen` allows tool use (e.g., `stepCountIs(5)`) **Result not showing** - Tool must return a value (not just mutate state) diff --git a/assistant-ui/skills/tools/references/human-in-loop.md b/assistant-ui/skills/tools/references/human-in-loop.md index fa7e9cd..e2dd342 100644 --- a/assistant-ui/skills/tools/references/human-in-loop.md +++ b/assistant-ui/skills/tools/references/human-in-loop.md @@ -14,7 +14,7 @@ Ask user to confirm before executing: // Backend tool returns requires-action status const deleteTool = tool({ description: "Delete a file (requires user confirmation)", - parameters: z.object({ path: z.string() }), + inputSchema: z.object({ path: z.string() }), execute: async ({ path }) => { // Return requires-action to wait for confirmation return { action: "confirm", path };