UNPKG

ai

Version:

AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.

348 lines (287 loc) • 11.3 kB
--- title: WorkflowChatTransport description: API Reference for the WorkflowChatTransport class. --- # `WorkflowChatTransport` A [`ChatTransport`](/docs/ai-sdk-ui/transport) implementation for [`useChat`](/docs/reference/ai-sdk-ui/use-chat) that enables automatic stream reconnection for workflow-based chat apps. It posts messages to a chat endpoint, extracts the `x-workflow-run-id` response header, and reconnects to a `/{runId}/stream` endpoint on interruption (network failures, page refreshes, function timeouts). Unlike [`DefaultChatTransport`](/docs/ai-sdk-ui/transport) which assumes the full response arrives in a single HTTP request, `WorkflowChatTransport` is designed for the [Workflow SDK](https://vercel.com/docs/workflow) where the initial response stream may be interrupted by function timeouts. The transport automatically detects missing `finish` events and reconnects to resume from where the stream left off. ```tsx 'use client'; import { useChat } from '@ai-sdk/react'; import { WorkflowChatTransport } from '@ai-sdk/workflow'; export default function Chat() { const { messages, sendMessage } = useChat({ transport: new WorkflowChatTransport({ api: '/api/chat', maxConsecutiveErrors: 5, initialStartIndex: -50, }), }); // ... render chat UI } ``` ## Import <Snippet text={`import { WorkflowChatTransport } from "@ai-sdk/workflow"`} prompt={false} /> ## Constructor ### Parameters <PropertiesTable content={[ { name: 'api', type: 'string', isOptional: true, description: "API endpoint for chat requests. The reconnection endpoint is derived from this as `{api}/{runId}/stream`. Default: '/api/chat'.", }, { name: 'fetch', type: 'typeof fetch', isOptional: true, description: 'Custom fetch implementation to use for HTTP requests. Default: global fetch.', }, { name: 'maxConsecutiveErrors', type: 'number', isOptional: true, description: 'Maximum number of consecutive errors allowed during reconnection attempts before giving up. Default: 3.', }, { name: 'initialStartIndex', type: 'number', isOptional: true, description: 'Default chunk index to start from when reconnecting. Negative values read from the end of the stream (e.g., -50 fetches the last 50 chunks), useful for resuming after a page refresh without replaying the full conversation. Can be overridden per-call via reconnectToStream options. Default: 0.', }, { name: 'onChatSendMessage', type: '(response: Response, options: SendMessagesOptions) => void | Promise<void>', isOptional: true, description: 'Callback invoked after the initial POST request succeeds. Useful for inspecting response headers (e.g., extracting workflow run ID) or tracking chat history on the client side.', }, { name: 'onChatEnd', type: '({ chatId, chunkIndex }) => void | Promise<void>', isOptional: true, description: 'Callback invoked when the stream ends (receives a finish chunk). Receives the chat ID and total chunk count. Useful for cleanup or state updates.', }, { name: 'prepareSendMessagesRequest', type: 'PrepareSendMessagesRequest', isOptional: true, description: 'Function to customize the POST request before sending. Can override the API endpoint, headers, credentials, and body.', }, { name: 'prepareReconnectToStreamRequest', type: 'PrepareReconnectToStreamRequest', isOptional: true, description: 'Function to customize the reconnection GET request. Can override the API endpoint, headers, and credentials.', }, ]} /> ## Methods ### `sendMessages()` Sends messages to the chat endpoint via POST and returns a streaming response. If the stream is interrupted (no `finish` event received), the transport automatically reconnects via GET to `{api}/{runId}/stream?startIndex={chunkIndex}` to resume from where it left off. The POST request includes the messages as JSON and expects the response to include an `x-workflow-run-id` header identifying the workflow run. ```ts const stream = await transport.sendMessages({ chatId: 'chat-123', trigger: 'submit-message', messages: [...], abortSignal: controller.signal, }); ``` <PropertiesTable content={[ { name: 'chatId', type: 'string', description: 'Unique identifier for the chat session.', }, { name: 'trigger', type: "'submit-message' | 'regenerate-message'", description: 'The type of message submission.', }, { name: 'messageId', type: 'string | undefined', description: 'ID of the message to regenerate, or undefined for new messages.', }, { name: 'messages', type: 'UIMessage[]', description: 'Array of UI messages representing the conversation history.', }, { name: 'abortSignal', type: 'AbortSignal | undefined', description: 'Signal to abort the request. Propagated to both the initial POST and any reconnection GET requests.', }, ]} /> #### Returns Returns a `Promise<ReadableStream<UIMessageChunk>>` that includes chunks from both the initial POST response and any automatic reconnection. ### `reconnectToStream()` Reconnects to an existing chat stream that was previously interrupted. Useful for resuming after a page refresh or when the client needs to re-establish a connection. ```ts const stream = await transport.reconnectToStream({ chatId: 'chat-123', startIndex: -50, // Optional: fetch last 50 chunks }); ``` <PropertiesTable content={[ { name: 'chatId', type: 'string', description: 'The chat ID to reconnect to. Used to construct the reconnection URL.', }, { name: 'abortSignal', type: 'AbortSignal | undefined', description: 'Signal to abort the reconnection request.', }, { name: 'startIndex', type: 'number', isOptional: true, description: "Override the start index for this reconnection. Negative values read from the end of the stream. When omitted, falls back to the constructor's initialStartIndex.", }, ]} /> #### Returns Returns a `Promise<ReadableStream<UIMessageChunk> | null>`. ## How Reconnection Works The transport follows this flow: 1. **POST** to `{api}` with messages. The response must include an `x-workflow-run-id` header. 2. **Stream** the SSE response, counting chunks as they arrive. 3. **Detect interruption**: If the stream closes without a `finish` event (e.g., function timeout, network error), the transport knows the response is incomplete. 4. **Reconnect** via GET to `{api}/{runId}/stream?startIndex={chunkIndex}` to resume from the last received chunk. 5. **Retry**: If the reconnection stream also interrupts, retry up to `maxConsecutiveErrors` times. 6. **Complete**: Once a `finish` event is received, call `onChatEnd` and close the stream. ### Negative Start Index When `initialStartIndex` is negative (e.g., `-50`), the transport sends it as-is in the first reconnection request. The server should resolve this to an absolute position and return the `x-workflow-stream-tail-index` response header so the transport can compute the correct position for subsequent retries. If the header is missing or invalid, the transport falls back to replaying from the beginning (`startIndex=0`). ## Server Requirements For `WorkflowChatTransport` to work, your server must provide two endpoints: ### POST `{api}` (e.g., `/api/chat`) - Accept messages as JSON body - Return an SSE stream of `UIMessageChunk` events - Include an `x-workflow-run-id` response header ### GET `{api}/{runId}/stream` (e.g., `/api/chat/{runId}/stream`) - Accept a `startIndex` query parameter - Return the SSE stream starting from the given chunk index - For negative `startIndex`, resolve to the tail and include `x-workflow-stream-tail-index` response header See the [WorkflowAgent guide](/docs/agents/workflow-agent) for complete endpoint examples. ## Examples ### Basic Usage with useChat ```tsx 'use client'; import { useChat } from '@ai-sdk/react'; import { WorkflowChatTransport } from '@ai-sdk/workflow'; import { useMemo } from 'react'; export default function Chat() { const transport = useMemo( () => new WorkflowChatTransport({ api: '/api/chat' }), [], ); const { messages, sendMessage, status } = useChat({ transport }); return ( <div> {messages.map(message => ( <div key={message.id}> {message.role === 'user' ? 'User: ' : 'AI: '} {message.parts.map((part, index) => part.type === 'text' ? <span key={index}>{part.text}</span> : null, )} </div> ))} <button onClick={() => sendMessage({ text: 'Hello!' })}>Send</button> </div> ); } ``` ### With Callbacks and Page Refresh Recovery ```tsx 'use client'; import { useChat } from '@ai-sdk/react'; import { WorkflowChatTransport } from '@ai-sdk/workflow'; import { useMemo } from 'react'; export default function Chat() { const transport = useMemo( () => new WorkflowChatTransport({ api: '/api/chat', maxConsecutiveErrors: 5, initialStartIndex: -50, // Resume from last 50 chunks on page refresh onChatSendMessage: response => { const runId = response.headers.get('x-workflow-run-id'); console.log('Workflow run started:', runId); }, onChatEnd: ({ chatId, chunkIndex }) => { console.log(`Chat ${chatId} complete, ${chunkIndex} chunks`); }, }), [], ); const { messages, sendMessage } = useChat({ transport }); // ... render chat UI } ``` ### Server-Side Endpoints (Next.js) ```ts filename="app/api/chat/route.ts" import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow'; import { createUIMessageStreamResponse, type UIMessage } from 'ai'; import { start } from 'workflow/api'; import { chat } from '@/workflow/agent-chat'; export async function POST(request: Request) { const { messages }: { messages: UIMessage[] } = await request.json(); const run = await start(chat, [messages]); return createUIMessageStreamResponse({ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()), headers: { 'x-workflow-run-id': run.runId, }, }); } ``` ```ts filename="app/api/chat/[runId]/stream/route.ts" import { createModelCallToUIChunkTransform } from '@ai-sdk/workflow'; import type { NextRequest } from 'next/server'; import { getRun } from 'workflow/api'; export async function GET( request: NextRequest, { params }: { params: Promise<{ runId: string }> }, ) { const { runId } = await params; const startIndex = Number( new URL(request.url).searchParams.get('startIndex') ?? '0', ); const run = await getRun(runId); const readable = run .getReadable({ startIndex }) .pipeThrough(createModelCallToUIChunkTransform()); return new Response(readable, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', 'x-workflow-run-id': runId, }, }); } ```