UNPKG

workflow

Version:

Workflow SDK - Build durable, resilient, and observable workflows

302 lines (254 loc) • 11.3 kB
--- title: WorkflowChatTransport description: Chat transport with automatic reconnection and recovery from interrupted streams. type: reference summary: Use WorkflowChatTransport as a drop-in AI SDK transport for automatic stream reconnection. prerequisites: - /docs/ai related: - /docs/ai/resumable-streams --- <Callout type="warn"> `WorkflowChatTransport` from `@workflow/ai` is deprecated. AI SDK ships a 1:1 port, so use [`WorkflowChatTransport` from `@ai-sdk/workflow`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#resumable-streaming-with-workflowchattransport) instead. This reference is kept for existing applications that still import it from `@workflow/ai`. </Callout> `WorkflowChatTransport` is an AI SDK chat transport that automatically reconnects to interrupted streams. It replaces the default AI SDK transport and recovers from network issues, page refreshes, or Vercel Functions timeouts. <Callout> `WorkflowChatTransport` implements the [`ChatTransport`](https://ai-sdk.dev/docs/ai-sdk-ui/transport) interface from the AI SDK and is designed to work with workflow-based chat applications. It requires endpoints that return the `x-workflow-run-id` header to enable stream resumption. </Callout> ```typescript lineNumbers import { useChat } from "@ai-sdk/react"; import { WorkflowChatTransport } from "@workflow/ai"; export default function Chat() { const { messages, sendMessage } = useChat({ transport: new WorkflowChatTransport(), }); return ( <div> {messages.map((m) => ( <div key={m.id}>{m.content}</div> ))} </div> ); } ``` ## API signature ### Class <TSDoc definition={` import { WorkflowChatTransport } from "@workflow/ai"; export default WorkflowChatTransport;`} /> ### WorkflowChatTransportOptions <TSDoc definition={` import type { WorkflowChatTransportOptions } from "@workflow/ai"; export default WorkflowChatTransportOptions;`} /> ## Key features - **Automatic reconnection**: Recovers from interrupted streams with configurable retry limits. - **Workflow integration**: Works with workflow-based endpoints that provide the `x-workflow-run-id` header. - **Customizable requests**: Allows intercepting and modifying requests through `prepareSendMessagesRequest` and `prepareReconnectToStreamRequest`. - **Stream callbacks**: Provides hooks for tracking the chat lifecycle through `onChatSendMessage` and `onChatEnd`. - **Custom fetch**: Supports custom fetch implementations for advanced use cases. ## Good to know - The transport expects chat endpoints to return the `x-workflow-run-id` header in the response to enable stream resumption. - By default, the transport posts to `/api/chat` and reconnects through `/api/chat/{runId}/stream`. - The `onChatSendMessage` callback receives the full response object, allowing you to extract and store the workflow run ID for session resumption. - Stream interruptions are automatically detected when a `finish` chunk is not received in the initial response. - The `maxConsecutiveErrors` option controls how many reconnection attempts are made before giving up (default: 3). - `initialStartIndex` (constructor option) sets the default chunk position for the **first** reconnection attempt (for example, after a page refresh). Subsequent retries within the same reconnection loop always resume from the last received chunk. Negative values (for example, `-20`) read from the end of the stream, which is useful for showing only recent output without replaying the full conversation. `startIndex` (per-call option on `reconnectToStream`) overrides `initialStartIndex` for a single reconnection. - When using a negative `initialStartIndex`, the reconnection endpoint must return the `x-workflow-stream-tail-index` response header (through `readable.getTailIndex()`). The transport reads this header to compute absolute chunk positions for retries. Without it, `startIndex` is assumed to be `0`, replaying the entire stream. ## Examples ### Basic chat setup ```typescript "use client"; import { useChat } from "@ai-sdk/react"; import { WorkflowChatTransport } from "@workflow/ai"; import { useState } from "react"; export default function BasicChat() { const [input, setInput] = useState(""); const { messages, sendMessage } = useChat({ transport: new WorkflowChatTransport(), }); return ( <div> <div className="space-y-4"> {messages.map((m) => ( <div key={m.id}> <strong>{m.role}:</strong> {m.content} </div> ))} </div> <form onSubmit={(e) => { e.preventDefault(); sendMessage({ text: input }); setInput(""); }} > <input value={input} placeholder="Say something..." onChange={(e) => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` ### With session persistence and resumption ```typescript "use client"; import { useChat } from "@ai-sdk/react"; import { WorkflowChatTransport } from "@workflow/ai"; import { useMemo, useState } from "react"; export default function ChatWithResumption() { const [input, setInput] = useState(""); const activeWorkflowRunId = useMemo(() => { if (typeof window === "undefined") return; return localStorage.getItem("active-workflow-run-id") ?? undefined; }, []); const { messages, sendMessage } = useChat({ resume: !!activeWorkflowRunId, transport: new WorkflowChatTransport({ onChatSendMessage: (response, options) => { // Save chat history to localStorage localStorage.setItem( "chat-history", JSON.stringify(options.messages) ); // Extract and store the workflow run ID for session resumption const workflowRunId = response.headers.get("x-workflow-run-id"); if (workflowRunId) { localStorage.setItem("active-workflow-run-id", workflowRunId); } }, onChatEnd: ({ chatId, chunkIndex }) => { console.log(`Chat ${chatId} completed with ${chunkIndex} chunks`); // Clear the active run ID when chat completes localStorage.removeItem("active-workflow-run-id"); }, }), }); return ( <div> <div className="space-y-4"> {messages.map((m) => ( <div key={m.id}> <strong>{m.role}:</strong> {m.content} </div> ))} </div> <form onSubmit={(e) => { e.preventDefault(); sendMessage({ text: input }); setInput(""); }} > <input value={input} placeholder="Say something..." onChange={(e) => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` ### With custom request configuration ```typescript "use client"; import { useChat } from "@ai-sdk/react"; import { WorkflowChatTransport } from "@workflow/ai"; import { useState } from "react"; export default function ChatWithCustomConfig() { const [input, setInput] = useState(""); const { messages, sendMessage } = useChat({ transport: new WorkflowChatTransport({ prepareSendMessagesRequest: async (config) => { return { ...config, api: "/api/chat", headers: { ...config.headers, "Authorization": `Bearer ${process.env.NEXT_PUBLIC_API_TOKEN}`, "X-Custom-Header": "custom-value", }, credentials: "include", }; }, prepareReconnectToStreamRequest: async (config) => { return { ...config, headers: { ...config.headers, "Authorization": `Bearer ${process.env.NEXT_PUBLIC_API_TOKEN}`, }, credentials: "include", }; }, maxConsecutiveErrors: 5, }), }); return ( <div> <div className="space-y-4"> {messages.map((m) => ( <div key={m.id}> <strong>{m.role}:</strong> {m.content} </div> ))} </div> <form onSubmit={(e) => { e.preventDefault(); sendMessage({ text: input }); setInput(""); }} > <input value={input} placeholder="Say something..." onChange={(e) => setInput(e.currentTarget.value)} /> </form> </div> ); } ``` ## Mid-part resumes A workflow stream is a flat sequence of chunks, but the AI SDK's user interface (UI) protocol groups chunks into logical parts: a `text-start` opens a text part that subsequent `text-delta`s extend and a `text-end` closes, and the same shape applies to `reasoning-*` and `tool-input-*`. The AI SDK client enforces that grammar: a `reasoning-delta` whose `reasoning-start` was never seen throws and breaks the chat. A non-zero `startIndex` (in particular a negative `initialStartIndex`) resolves to a chunk offset with no awareness of those part boundaries, so it can land in the middle of an open part. When that happens, `WorkflowChatTransport` will **drop chunks that reference a part it didn't see a start for** and log a one-time warning. The chat keeps working, but any partial part overlapping the resume cursor is discarded. Tool calls are an exception: `tool-input-available` / `tool-input-error` chunks are self-contained (they carry the full input), so a tool call is recovered as soon as one of those chunks appears in the resumed window. Only its streamed input deltas are lost. To preserve those partial parts, rewind to a step boundary on the server before returning the readable. `start-step` / `finish-step` chunks are the natural seams: no UI part is ever open across them. Sketch: {/*@skip-typecheck: incomplete code sample*/} ```typescript title="app/api/chat/[id]/stream/route.ts" const run = getRun(id); const tailIndex = await run.getReadable().getTailIndex(); let resolved = startIndex < 0 ? Math.max(0, tailIndex + 1 + startIndex) : startIndex; if (startIndex !== 0) { // Walk back from `resolved` to the most recent start-step (or chunk 0), // capping the lookback so a single huge step can't trigger an unbounded scan. const LOOKBACK = 200; const probe = run.getReadable({ startIndex: Math.max(0, resolved - LOOKBACK) }); let i = Math.max(0, resolved - LOOKBACK); let lastBoundary = i; for await (const chunk of probe as unknown as AsyncIterable<{ type: string }>) { if (i >= resolved) break; if (chunk.type === "start-step") lastBoundary = i; i++; } resolved = lastBoundary; } return createUIMessageStreamResponse({ stream: run.getReadable({ startIndex: resolved }), headers: { "x-workflow-stream-tail-index": String(tailIndex) }, }); ``` ## See also - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): Build durable, resumable AI agents (replaces `DurableAgent`) - [AI SDK `useChat` documentation](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat): Use `useChat` with custom transports - [Workflows and Steps](/docs/foundations/workflows-and-steps): Understand workflow fundamentals - [`flight-booking-app` example](https://github.com/vercel/workflow-examples/tree/main/flight-booking-app): View an example application that uses `WorkflowChatTransport`