Skip to content

Commit 04046d0

Browse files
committed
docs: add AI SDK harness agent pattern for chat.agent
Show how to run a Vercel AI SDK HarnessAgent (Claude Code/Codex/Pi) inside a chat.agent run(): the harness supplies the agent brain, while chat.agent supplies durable sessions, suspend/resume, and the useChat transport. HarnessAgent.stream() returns a StreamTextResult, so it pipes from run() exactly like streamText. https://claude.ai/code/session_018PVZy5kT3QcyKixcAF9sSF
1 parent 034058b commit 04046d0

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
---
2+
title: "AI SDK harness agents"
3+
sidebarTitle: "AI SDK harness"
4+
description: "Run a Vercel AI SDK HarnessAgent (Claude Code, Codex, Pi) inside a chat.agent run() — the harness supplies the agent brain, chat.agent supplies durable sessions, suspend/resume, and transport."
5+
---
6+
7+
import RcBanner from "/snippets/ai-chat-rc-banner.mdx";
8+
9+
<RcBanner />
10+
11+
The Vercel AI SDK's [harness abstraction](https://ai-sdk.dev/v7/docs/ai-sdk-harnesses/overview) wraps a complete agent *runtime* — Claude Code, Codex, or Pi — behind one AI SDK surface. A `HarnessAgent` owns the things that live *above* a model call: workspace access, built-in coding tools, the runtime's native session state, compaction, and permission flows.
12+
13+
`chat.agent` owns something different: durability. One long-lived task per conversation, [three layers of persistence](/ai-chat/how-it-works#three-layers-of-persistence), suspend/resume across idle gaps, and a `useChat` transport with no API routes.
14+
15+
These compose. `HarnessAgent.stream()` returns a standard AI SDK [`StreamTextResult`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text), which is exactly what [`chat.agent`'s `run()`](/ai-chat/backend#simple-return-a-streamtextresult) already knows how to pipe. So you return the harness stream from `run()` and get both: the harness as the brain, `chat.agent` as the durable substrate around it.
16+
17+
<Note>
18+
**The two abstractions answer different questions.** The AI SDK harness answers *"which agent runtime runs the loop?"* — swap Claude Code for Codex without touching your UI. `chat.agent` answers *"where does the conversation live and how does it survive a refresh, deploy, or crash?"* Neither replaces the other.
19+
</Note>
20+
21+
## Where each layer sits
22+
23+
```mermaid
24+
flowchart TB
25+
Browser["Browser — useChat + TriggerChatTransport"]
26+
subgraph Agent["chat.agent run() — durable, suspend/resume"]
27+
Harness["HarnessAgent (Claude Code / Codex / Pi)"]
28+
Sandbox["Sandbox + coding tools + skills"]
29+
Harness --> Sandbox
30+
end
31+
Browser <-->|"slim wire protocol"| Agent
32+
Agent -->|"StreamTextResult piped to .out"| Browser
33+
```
34+
35+
- **`chat.agent`** keeps the conversation alive across turns, checkpoints the run between messages, and streams chunks to the browser over the durable `.out` channel.
36+
- **`HarnessAgent`** runs *inside* one turn — it does the agentic loop, drives its sandbox, and emits an AI SDK stream.
37+
38+
<Warning>
39+
The AI SDK harness packages (`@ai-sdk/harness`, `@ai-sdk/harness-claude-code`) are **experimental** and ship in AI SDK 7. Treat the adapter configuration below as illustrative — check the [AI SDK harness docs](https://ai-sdk.dev/v7/docs/ai-sdk-harnesses/overview) for the current option names before copying verbatim. The integration *shape* — return `harness.stream(...)` from `run()` — is the stable part.
40+
</Warning>
41+
42+
## Minimal example
43+
44+
A `chat.agent` whose `run()` delegates the turn to a Claude Code `HarnessAgent`. Because `.stream()` returns a `StreamTextResult`, returning it from `run()` is all the wiring you need.
45+
46+
```ts trigger/coding-agent.ts
47+
import { chat } from "@trigger.dev/sdk/ai";
48+
import { stepCountIs } from "ai";
49+
import { HarnessAgent } from "@ai-sdk/harness/agent";
50+
import { claudeCode } from "@ai-sdk/harness-claude-code";
51+
52+
const agent = new HarnessAgent({
53+
harness: claudeCode(),
54+
instructions: "You are a senior engineer. Make focused, well-tested changes.",
55+
});
56+
57+
export const codingAgent = chat.agent({
58+
id: "coding-agent",
59+
run: async ({ messages, signal }) => {
60+
// HarnessAgent.stream() returns an AI SDK StreamTextResult,
61+
// so chat.agent pipes it to the frontend automatically.
62+
return agent.stream({
63+
...chat.toStreamTextOptions(), // compaction, steering, telemetry, stored prompt
64+
messages,
65+
abortSignal: signal,
66+
stopWhen: stepCountIs(50), // coding loops run long — give the harness room
67+
});
68+
},
69+
});
70+
```
71+
72+
The frontend is unchanged from any other `chat.agent``useChat` over a `TriggerChatTransport`. See the [Quick Start](/ai-chat/quick-start) for the matching server actions and frontend component.
73+
74+
<Tip>
75+
Spread `chat.toStreamTextOptions()` first (see the [warning in Backend](/ai-chat/backend#simple-return-a-streamtextresult)). It wires up `prepareStep` for [compaction](/ai-chat/compaction), [steering](/ai-chat/pending-messages), and [background injection](/ai-chat/background-injection), and injects the system prompt from [`chat.prompt()`](/ai/prompts). Your explicit options (like `messages` and `stopWhen`) win on conflict.
76+
</Tip>
77+
78+
## Swapping the harness
79+
80+
The whole point of the AI SDK harness abstraction is portability. Switch runtimes by changing one import and one factory call — `run()`, the transport, and the UI stay identical:
81+
82+
```ts
83+
// Codex instead of Claude Code
84+
import { codex } from "@ai-sdk/harness-codex";
85+
86+
const agent = new HarnessAgent({ harness: codex() });
87+
```
88+
89+
## Why run a harness on chat.agent instead of standalone
90+
91+
A `HarnessAgent` on its own is ephemeral — it runs where you invoke it and stops when the call returns. It has no answer for the conversation outliving the process. That's the gap `chat.agent` fills:
92+
93+
| Concern | HarnessAgent alone | HarnessAgent inside `chat.agent` |
94+
| --- | --- | --- |
95+
| Multi-turn conversation memory | Runtime's native session, scoped to the process | Durable [Session](/ai-chat/sessions) keyed by `chatId`, survives run boundaries |
96+
| User goes idle mid-task | Process must stay up | Run [suspends](/ai-chat/how-it-works#suspended); compute freed, in-memory state checkpointed |
97+
| Page refresh mid-stream | Stream is lost | [`lastEventId` cursor](/ai-chat/how-it-works#layer-3-the-lasteventid-cursor-browser) replays `.out` — no re-run of the model |
98+
| Deploy mid-conversation | Connection drops | [Version upgrade](/ai-chat/patterns/version-upgrades) flow migrates to the new code on the next turn |
99+
| OOM / crash | Work lost | [Recovery boot](/ai-chat/patterns/recovery-boot) from the S3 snapshot + `.out` tail |
100+
| Long-running coding loop (minutes) | Ties up a request | First-class — a turn can take as long as it needs |
101+
102+
A coding harness is *exactly* the workload that benefits: turns are long, sandboxes are expensive to warm, and humans wander off mid-task. `chat.agent` lets the harness's session persist while the compute parks between messages.
103+
104+
## Managing the sandbox across turns
105+
106+
If your harness warms an expensive sandbox, treat it like any other per-run resource — warm it in `onTurnStart`, dispose it in `onChatSuspend`. This is the same lifecycle the [code execution sandbox](/ai-chat/patterns/code-sandbox) pattern uses; the only difference is the harness owns the sandbox rather than a standalone `executeCode` tool.
107+
108+
```ts
109+
export const codingAgent = chat.agent({
110+
id: "coding-agent",
111+
onChatSuspend: async ({ ctx }) => {
112+
// Tear down the harness's sandbox right before the run suspends,
113+
// so you're not paying for idle compute between messages.
114+
await disposeHarnessSandbox(ctx.run.id);
115+
},
116+
run: async ({ messages, signal }) => {
117+
return agent.stream({
118+
...chat.toStreamTextOptions(),
119+
messages,
120+
abortSignal: signal,
121+
stopWhen: stepCountIs(50),
122+
});
123+
},
124+
});
125+
```
126+
127+
See [Code execution sandbox](/ai-chat/patterns/code-sandbox) for why `onChatSuspend` (not `onTurnComplete`) is the right teardown point.
128+
129+
## Harness vs. native chat.agent capabilities
130+
131+
A harness brings its *own* compaction, permission flows, and sub-agents. `chat.agent` also has [compaction](/ai-chat/compaction), [HITL tool approvals](/ai-chat/patterns/human-in-the-loop), and [sub-agents](/ai-chat/patterns/sub-agents). When you nest one inside the other, decide which layer owns each concern:
132+
133+
- **Let the harness own** what's intrinsic to its runtime: its built-in coding tools, its workspace/sandbox, its internal step loop.
134+
- **Let `chat.agent` own** what's intrinsic to the conversation: durability, the `useChat` transport, persistence to your database via [`onTurnComplete`](/ai-chat/lifecycle-hooks), and dashboard observability.
135+
136+
Avoid double-compacting — if the harness compacts its own context, don't also enable `chat.agent` compaction over the same history. Pick the layer closest to the source of truth.
137+
138+
## When this is the right combination
139+
140+
**Good fit:**
141+
- You want a Claude Code / Codex / Pi coding agent as a *persistent, multi-turn chat* your users return to.
142+
- You want runtime portability (swap harnesses) without rebuilding durability each time.
143+
- Turns are long and idle gaps are unpredictable.
144+
145+
**Reach for something simpler when:**
146+
- It's a single-shot, fire-and-forget harness invocation with no conversation — call the `HarnessAgent` directly, no `chat.agent` needed.
147+
- You don't need a harness at all — a plain `streamText` (or the AI SDK [`Agent`](https://ai-sdk.dev/docs/agents/overview) class) inside `run()` is lighter. See [Backend](/ai-chat/backend).
148+
149+
## See also
150+
151+
- [How it works](/ai-chat/how-it-works) — the durability model the harness runs on top of.
152+
- [Code execution sandbox](/ai-chat/patterns/code-sandbox) — sandbox lifecycle with `chat.agent` hooks.
153+
- [Sub-agents](/ai-chat/patterns/sub-agents) — delegate to other durable agents from a tool call.
154+
- [Running Claude Code on Trigger.dev](/guides/ai-agents/claude-code-trigger) — the coding-harness-on-Trigger guide.
155+
</content>
156+
</invoke>

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
"ai-chat/patterns/persistence-and-replay",
139139
"ai-chat/patterns/branching-conversations",
140140
"ai-chat/patterns/code-sandbox",
141+
"ai-chat/patterns/ai-sdk-harness",
141142
"ai-chat/patterns/human-in-the-loop",
142143
"ai-chat/patterns/tool-result-auditing",
143144
"ai-chat/patterns/large-payloads",

0 commit comments

Comments
 (0)