How does the harness thing work?
how does the harness thing work?
I now have a solid grounding. Here's the answer:
#How the AI SDK "harness" works
The harness abstraction in vercel/ai is a way to run entire agent runtimes (Claude Code, Codex, Pi, OpenCode, DeepAgents, …) behind a single AI-SDK–style API, separate from the provider/model abstraction. A "harness" is not just a model call — it owns workspace access, built-in tools, session state, compaction, permissions, and it always runs inside a sandbox.
The best two source docs are:
architecture/harness-abstraction.md(deep dive)content/docs/03-ai-sdk-harnesses/01-overview.mdx(user-facing overview)
##The four moving parts
From the overview and architecture doc:
HarnessAgent— the user-facing runtime wrapper you actually call in app code. It exposes AI-SDK-familiargenerate()/stream()returningGenerateTextResult/StreamTextResult(packages/harness/src/agent/harness-agent.ts).HarnessV1— the adapter specification one third-party agent runtime implements (packages/harness/src/v1/harness-v1.ts). It looks structurally likeLanguageModelV4:specificationVersion: 'harness-v1', aharnessId,builtinTools, optional bootstrap/lifecycle schema, and one entry pointdoStart()that returns aHarnessV1Session.HarnessV1SandboxProvider+HarnessV1NetworkSandboxSession— the sandbox contracts (e.g.@ai-sdk/sandbox-vercel,@ai-sdk/sandbox-just-bash). The adapter does not create the sandbox;HarnessAgentdoes and hands it in.- Session (
HarnessV1Session/HarnessAgentSession) — one live conversation + workspace. This is where prompt turns, continue-turns, compaction, suspend, detach, stop, destroy live.
##The key ownership rule
From architecture/harness-abstraction.md:
The key boundary is that
HarnessAgentowns the sandbox lifecycle, while the adapter owns the underlying coding-agent runtime.
So the flow is:
User → HarnessAgent.createSession()
→ SandboxProvider.createSession() → sandboxSession
→ HarnessAgent creates sessionWorkDir
→ adapter.doStart({ sandboxSession, sessionWorkDir })
→ adapter starts/attaches its runtime, returns HarnessV1Session
User → agent.generate() / stream()
→ adapter.doPromptTurn({ prompt, tools, emit })
→ emits HarnessV1StreamPart events
← projected into AI SDK GenerateTextResult / StreamTextResult
##Two adapter shapes
The doc explicitly calls out two implementation strategies:
- Host-driven (preferred): the agent runtime runs in your Node.js host process, and the sandbox is only used as the workspace/shell target through
Experimental_SandboxSessionAPIs. No in-sandbox bridge, no port. E.g. host-side SDKs. - Bridge-backed: the runtime insists on running inside the sandbox (needs local FS, local process state, a runtime-specific
$HOME, etc.). The adapter declares aHarnessV1Bootstraprecipe, the sandbox exposes a port, and the host talks to an in-sandbox bridge which drives the CLI/SDK and streams messages back. Claude Code, Codex, OpenCode, and DeepAgents adapters all have asrc/bridge/folder — that's this shape.
##Bootstrapping the sandbox
Bridge-backed adapters ship a getBootstrap() that returns a recipe. HarnessAgent computes a stable identity from that recipe and passes it to the sandbox provider, which can return a cached/snapshotted image so the bootstrap only runs once (packages/harness/src/agent/prepare-sandbox-for-harness.ts). This is how prewarming works.
##Filesystem discipline
sessionWorkDir is the user's working tree. Harness infrastructure (bridge code, marker files, harness-provided skills) must live elsewhere — typically /tmp/harness/<harness-id> or $HOME/.agents/skills. The architecture doc is explicit about this.
##Streams are AI-SDK-compatible
Even though a harness emits things a plain LLM doesn't (workspace file changes, compaction events, native tool calls, approvals), everything is translated to AI-SDK stream parts by packages/harness/src/agent/internal/translate-stream-part.ts and to-harness-stream.ts. Events that have no first-class AI-SDK part are surfaced as dynamic provider-executed tool parts. That's why you can pipe a HarnessAgent stream into toUIMessageStream and render it with useChat.
##Lifecycle: resume vs. continue
Adapters distinguish two recovery modes (see the "Lifecycle and Resume" section):
- Resume a session — reopen a parked session before the next user turn (
doStart({ resumeFrom })). - Continue a turn — recover an in-flight turn that was interrupted (
doStart({ continueFrom })). Bridge adapters may attach to a live runtime and replay buffered events; host-driven ones may need to re-drive part of the work.
Both feed off small, serializable, adapter-owned payloads. Optionally validated by lifecycleStateSchema.
##Where to look for concrete examples
- Adapter implementations:
packages/harness-claude-code/,packages/harness-codex/,packages/harness-opencode/,packages/harness-deepagents/,packages/harness-pi/ - End-to-end usage:
examples/harness-e2e-next/(Next.js) andexamples/harness-e2e-tui/(terminal), one file per (adapter × scenario) matrix - Bite-sized function examples:
examples/ai-functions/src/harness-agent/<adapter>/*.ts— e.g.stream-text.ts,multi-turn.ts,suspend-turn.ts,custom-tool-approval.ts,resume.ts
If you want, I can zoom into any particular piece — e.g. how doPromptTurn events get translated, how the bridge protocol works for one adapter, or how prepareSandboxForHarness interacts with snapshots.
END OF POST