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
text/mdx
---
title: UI
description: Use AI SDK harnesses with useChat.
---
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.
```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>
</>
);
}
```
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.',
});
```
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.
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.
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.
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`.
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.