|
| 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> |
0 commit comments