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.

233 lines (188 loc) • 6.3 kB
--- title: UI description: Use AI SDK harnesses with useChat. --- # Harnesses with AI SDK UI Harness streams are compatible with AI SDK UI message streams. You can use `useChat()` on the client and stream `HarnessAgent` output from a server route. The important difference from model-based chat routes is session management. A harness owns its conversation state, so the route should resume or create a `HarnessAgentSession` for the chat id instead of replaying the whole UI message history into a model. ## Client ```tsx filename='app/page.tsx' 'use client'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; import { useState } from 'react'; export default function Page() { const [input, setInput] = useState(''); const { messages, sendMessage, status } = useChat({ id: 'example-chat', transport: new DefaultChatTransport({ api: '/api/chat', }), }); return ( <> {messages.map(message => ( <div key={message.id}> <strong>{message.role === 'user' ? 'You: ' : 'AI: '}</strong> {message.parts.map((part, index) => { if (part.type === 'text') { return <span key={index}>{part.text}</span>; } if (part.type.startsWith('tool-') || part.type === 'dynamic-tool') { return <pre key={index}>{JSON.stringify(part, null, 2)}</pre>; } return null; })} </div> ))} <form onSubmit={event => { event.preventDefault(); if (input.trim()) { sendMessage({ text: input }); setInput(''); } }} > <input value={input} onChange={event => setInput(event.target.value)} disabled={status !== 'ready'} /> <button type="submit" disabled={status !== 'ready'}> Send </button> </form> </> ); } ``` ## Agent Define the `HarnessAgent` on the server: ```ts filename='app/api/chat/agent.ts' import { HarnessAgent } from '@ai-sdk/harness/agent'; import { claudeCode } from '@ai-sdk/harness-claude-code'; import { createVercelSandbox } from '@ai-sdk/sandbox-vercel'; export const agent = new HarnessAgent({ harness: claudeCode, sandbox: createVercelSandbox({ runtime: 'node24', ports: [4000], }), instructions: 'You are a helpful coding assistant.', }); ``` ## Session Store Persist only the opaque resume state returned by `session.detach()`. If the turn paused for approval or was otherwise interrupted, that resume state carries the continuation state internally. The chat id can also be the harness `sessionId`, which gives the sandbox a stable identity across requests and processes. ```ts filename='app/api/chat/session-store.ts' import type { HarnessAgentResumeSessionState, HarnessAgentSession, } from '@ai-sdk/harness/agent'; const states: Record<string, HarnessAgentResumeSessionState | undefined> = {}; type SessionFactory = { createSession(options?: { sessionId?: string; resumeFrom?: HarnessAgentResumeSessionState; }): Promise<HarnessAgentSession>; }; export async function resumeOrCreateSession({ agent, chatId, }: { agent: SessionFactory; chatId: string; }) { const resumeFrom = states[chatId]; return agent.createSession( resumeFrom ? { sessionId: chatId, resumeFrom } : { sessionId: chatId }, ); } export async function detachAndPersist({ chatId, session, }: { chatId: string; session: HarnessAgentSession; }) { states[chatId] = await session.detach(); } ``` Use durable storage instead of an in-memory map in production. ## Route Convert UI messages to model messages, run the harness turn, and convert the result stream back to a UI message stream: ```ts filename='app/api/chat/route.ts' import { agent } from './agent'; import { detachAndPersist, resumeOrCreateSession } from './session-store'; import { convertToModelMessages, createUIMessageStreamResponse, toUIMessageStream, type UIMessage, } from 'ai'; export async function POST(request: Request) { const body: { id?: string; messages: UIMessage[]; } = await request.json(); if (!body.id) { throw new Error('Missing chat id'); } const chatId = body.id; const messages = await convertToModelMessages(body.messages); const session = await resumeOrCreateSession({ agent, chatId }); const result = await agent.stream({ session, messages }); return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream, onEnd: async () => { await detachAndPersist({ chatId, session }); }, }), }); } ``` Do not use `createAgentUIStreamResponse` directly with `HarnessAgent` unless you wrap the agent to inject the required session. `HarnessAgent.stream()` requires `session` on every call. ## Detach or Stop Use `session.detach()` when you want to park the harness runtime and keep the sandbox warm for the next request. Bridge-backed adapters can usually reattach or replay efficiently. If the turn is unfinished, `detach()` includes the turn continuation state in the returned resume state. Use `session.stop()` when you want to save resume state and stop the runtime and sandbox after each turn. The next request resumes from persisted state and continues any unfinished turn before accepting a new prompt. ## Rendering Harness Parts Harness output contains the same UI message part shapes used by AI SDK model streams: - `text` and `reasoning` parts for generated content. - typed tool parts such as `tool-bash`, `tool-read`, or a host tool like `tool-weather`. - `dynamic-tool` parts for dynamic events such as `fileChange` and `compaction`. Render typed harness built-ins the same way you render normal AI SDK tool parts. Check `part.state` for `input-streaming`, `input-available`, and `output-available`. ## Type-Safe Tool Parts Until `HarnessAgent` session options are part of the base `Agent` call parameters, infer UI tools from `agent.tools`: ```ts import type { InferUITools, UIMessage } from 'ai'; import { agent } from './agent'; export type HarnessMessage = UIMessage< unknown, never, InferUITools<typeof agent.tools> >; ``` Then use `useChat<HarnessMessage>()` on the client.