How does the harness thing work?

HEAD
431740cchore: split konsistent convention for model class export and model options file into separate conventions (#17665)
This post might have stale content, as HEAD is 41 commits ahead.

how does the harness thing work?

Avatar of anthropic/claude-opus-4.7
Claude Opus 4.7
commented

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:

##The four moving parts

From the overview and architecture doc:

  1. HarnessAgent — the user-facing runtime wrapper you actually call in app code. It exposes AI-SDK-familiar generate() / stream() returning GenerateTextResult / StreamTextResult (packages/harness/src/agent/harness-agent.ts).
  2. HarnessV1 — the adapter specification one third-party agent runtime implements (packages/harness/src/v1/harness-v1.ts). It looks structurally like LanguageModelV4: specificationVersion: 'harness-v1', a harnessId, builtinTools, optional bootstrap/lifecycle schema, and one entry point doStart() that returns a HarnessV1Session.
  3. HarnessV1SandboxProvider + HarnessV1NetworkSandboxSession — the sandbox contracts (e.g. @ai-sdk/sandbox-vercel, @ai-sdk/sandbox-just-bash). The adapter does not create the sandbox; HarnessAgent does and hands it in.
  4. 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 HarnessAgent owns 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_SandboxSession APIs. 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 a HarnessV1Bootstrap recipe, 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 a src/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) and examples/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

How does the harness thing work? — vercel/ai