pnpm installs streamdown in each example's node_modules, so the root-relative @source glob matched nothing and streamdown classes were never generated. chat also needs the shadcn source, since Tailwind skips the built package in node_modules. Fixes #312. Co-authored-by: Adham Ali <30821115+adhamali450@users.noreply.github.com>
Harness Agent Chat Example
json-render as the UI for agent harnesses.
This example runs a real coding agent -- pick Claude Code, Codex, or Pi -- in a Vercel Sandbox, driven through the AI SDK 7 HarnessAgent API, and renders its work as generative UI instead of a wall of markdown.
The agent edits files, runs commands, and executes tests inside the sandbox. When it reports back, it emits a json-render spec constrained to a catalog of work-report components (Steps, FileChange, Terminal, TestResults, Metric, BarChart, LineChart, ...), which streams into the chat as structured, rendered UI.
How it works
The round trip: the browser posts the chosen agent and prompt, the server streams a harness turn, and pipeJsonRender extracts the spec fence on the way back.
flowchart LR
subgraph browser [Browser]
UI["app/page.tsx<br/>useChat · AgentSelector"]
end
subgraph server ["app/api/agent/route.ts"]
SESS["getSession(chatId, agent)"]
AGENT["HarnessAgent<br/>Claude Code · Codex · Pi"]
SANDBOX[("Vercel Sandbox")]
PIPE["pipeJsonRender"]
end
UI -- "POST { messages, agent }" --> SESS
SESS --> AGENT
AGENT <-->|"bash · edit · test"| SANDBOX
AGENT -- "toUIMessageStream()" --> PIPE
PIPE -- "text · tool calls · data-spec parts" --> UI
What happens on a single turn:
sequenceDiagram
participant U as User
participant UI as page.tsx
participant API as /api/agent
participant AG as HarnessAgent
participant SB as Sandbox
U->>UI: pick agent + send prompt
UI->>API: POST { messages, agent }
API->>AG: getSession + stream(prompt)
AG->>SB: run commands / edit files
SB-->>AG: output
AG-->>API: prose + spec fence (streamed)
Note over API: pipeJsonRender splits the spec fence out
API-->>UI: text · tool calls · data-spec parts
UI-->>U: markdown + rendered report
lib/agents.tsis a client-safe catalog of the selectable agents;lib/agent.tsbuilds aHarnessAgentper agent (Claude Code, Codex, or Pi), each with a Vercel sandbox provider. The sharedinstructionsembedagentReportCatalog.prompt({ mode: "inline" }), teaching the runtime to wrap its UI report in a```specfence.app/api/agent/route.tsreads the chosenagentfrom the request body and keeps one live harness session per chat, locked to the agent that created it (the harness owns its own conversation history, so each turn sends only the fresh user message). It streams the turn and pipes it throughpipeJsonRender-- which extracts the spec fence into typeddata-specparts while passing text and tool calls through untouched.app/page.tsxrenders text with markdown, builtin tool calls (bash, edit, ...) as activity lines, and the spec inline with<ReportRenderer>viauseJsonRenderMessage. TheAgentSelectoron the first screen chooses which harness to run.
Because HarnessAgent.stream() returns a standard AI SDK StreamTextResult, the json-render pipeline is identical to the single-model chat example -- swapping a model call for a full agent harness changes nothing about the UI layer.
Setup
The AI SDK harness packages are experimental canary releases; expect breaking changes.
-
Give the sandbox provider Vercel credentials, either:
- Be logged in with the Vercel CLI (
vercel login) and run the dev server in a terminal (the SDK only falls back to CLI auth when attached to a TTY). It uses or creates avercel-sandbox-default-projectin your personal scope. - Or link a project and pull an OIDC token:
vercel link && vercel env pull.
- Be logged in with the Vercel CLI (
-
Provide model credentials.
AI_GATEWAY_API_KEY(Vercel AI Gateway) works for all three agents. Or use a provider key directly for the agent you run:ANTHROPIC_API_KEY(Claude Code) orOPENAI_API_KEY(Codex); Pi resolves credentials from the gateway. -
Optionally pin a model per agent:
CLAUDE_CODE_MODEL,CODEX_MODEL, orPI_MODEL(each defaults to that runtime's own default).
Run
pnpm install
pnpm dev
Then open harness-chat-demo.json-render.localhost:1355.
Note: the first message in a chat boots a fresh sandbox, which takes a while; follow-up messages reuse it. "Start Over" destroys the server-side session and its sandbox; idle sessions are destroyed after 10 minutes.