assistant-ui
assistant-ui renders runcell agents with zero glue: runcell speaks the AI SDK UI Message Stream protocol that assistant-ui's useChatRuntime consumes.
Backend
One route handler:
ts
// app/api/chat/route.ts
import { createAgent } from 'runcell';
const agent = createAgent({
model: 'anthropic/claude-sonnet-4-5',
tools: {
/* host tools */
},
});
export async function POST(req: Request): Promise<Response> {
const { messages } = await req.json();
return agent
.stream({ messages, signal: req.signal })
.toUIMessageStreamResponse();
}Frontend
Follow assistant-ui's getting started for components and styling. The runtime side is the default AI SDK setup:
tsx
import { AssistantRuntimeProvider } from '@assistant-ui/react';
import { useChatRuntime } from '@assistant-ui/react-ai-sdk';
export function Chat() {
const runtime = useChatRuntime(); // posts to /api/chat by default
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* assistant-ui components, e.g. <Thread /> */}
</AssistantRuntimeProvider>
);
}What renders
- Text and reasoning stream live. assistant-ui shows reasoning as a collapsible block.
- Tool calls appear as tool parts and change to
output-availablewhen the tool returns. Register per-tool UI components in assistant-ui to customize rendering. Runcell-internal tools (submitResult,fileChange) never reach the wire. - Repair turns arrive as additional steps of the same assistant message.
- Usage (token counts,
costUsd,costMeasured) rides the finish chunk'smessageMetadata, so the UI can show per-message cost. - Failures end the stream with an in-band
errorchunk. The message is masked by default (see below). - Attachments on the sent message (file parts with data URLs) are seeded into the run workspace under
attachments/, and the agent reads them with its file tools. With the optional peerspdfjs-distand@napi-rs/canvasinstalled, PDF attachments get a built-inreadPdfPagestool that renders pages as images the model can view. See messages input for limits.
Controlling what crosses the wire
Anything sent to the browser is visible in its network inspector, regardless of what the UI renders. Redaction therefore happens server-side, on the response:
ts
return agent.stream({ messages }).toUIMessageStreamResponse({
sendReasoning: false,
sendTools: { queryBilling: false, weather: 'names-only' },
onError: () => 'Something went wrong.',
});See wire-level controls for the full option reference.
Server-side conversation state
messages replays the client-held history on every request, which suits assistant-ui's default model. To keep durable server-side state instead, use threads with prompt and persist thread.toJSON(). The chat agent guide shows the full pattern.