Harness Agents

Who is this for: host authors wiring a full coding runtime (Claude Code, Codex, Pi) into Agent-Native as the agent. Building an app? Start with Creating Templates.

A harness agent is a full agent runtime — Claude Code, Codex, Pi, and similar — that owns its own loop, workspace, native file tools, session state, compaction, approval model, and sandbox behavior. Agent-Native runs these through the AgentHarness substrate in @agent-native/core/agent/harness, streams their events into the normal transcript, and persists their native session so a thread can pause and resume.

This is different from the built-in chat agent and from bringing your own chat runtime. The built-in agent and AgentEngine are for one model round trip beneath runAgentLoop. A harness is not an AgentEngine provider — it runs its own loop end to end, so Agent-Native drives it as a session, not as a single model call.

Which coding doc do I want?

You want to… Use
Run Claude Code / Codex / Pi as the agent, with their own loop + tools Harness agents (this page)
Render a Claude-Code/Codex-style coding workspace UI Agent-Native Code UI
Swap the backend that runs the agent's run-code tool Adapters
Wrap a CLI tool (gh, ffmpeg) for the agent to call Adapters

Adjacent surfaces: put an agent you built elsewhere behind Agent-Native's chat UI with AgentChatRuntime; let an external MCP host call into your app via External Agents; spawn background / sub-agent runs with Custom Agents & Teams.

Built-in harnesses

registerBuiltinAgentHarnesses() registers three adapters backed by the AI SDK HarnessAgent:

Name Runtime Sandbox Approvals
ai-sdk-harness:claude-code Claude Code yes yes
ai-sdk-harness:codex Codex yes no
ai-sdk-harness:pi Pi no yes

Their runtime packages are optional peer dependencies and load lazily, so an app that never uses a harness does not pay for it. Each adapter carries an installPackage hint (for example @ai-sdk/harness@latest @ai-sdk/harness-codex@latest); resolveAgentHarness throws a clear install error if the packages are missing, and isAgentHarnessPackageInstalled(entry) lets you check first.

registerBuiltinAgentHarnesses() also registers the ACP harnesses (acp, acp:gemini, acp:claude-code).

For sandboxed built-in harnesses, pass a sandbox provider through agentOptions.sandbox when registering them. The provider must implement resumeSession for rehydration after a process restart; a caller-owned sandbox session only covers initial creation.

ACP agents

Agent-Native can act as an ACP (Agent Client Protocol) client and drive a local coding agent — Gemini CLI, Claude Code, or any ACP-compliant agent — through this same substrate. The agent runs as a local subprocess that speaks newline-delimited JSON-RPC over stdio; ACP's editor ↔ agent model is exactly this shape.

This adapter is scoped to local coding. The child process inherits the parent environment, so the agent reuses whatever local CLI login it already has (for example gemini or claude auth in the user's home dir). It is not a hosted or sandboxed transport, and it is not a chat/A2A transport — for those, see Agent Surfaces.

Name Default command Resumable*
acp (supply command/args via config) yes
acp:gemini npx -y @google/gemini-cli --experimental-acp yes
acp:claude-code npx -y @zed-industries/claude-code-acp yes

*Resume works when the agent advertises the loadSession capability and degrades to a fresh session otherwise.

import {
  registerBuiltinAgentHarnesses,
  resolveAgentHarness,
} from "@agent-native/core/agent/harness";

registerBuiltinAgentHarnesses();

// A built-in preset (command/args are overridable through the resolve config):
const adapter = resolveAgentHarness("acp:gemini");

// Or any ACP agent by command:
const custom = resolveAgentHarness("acp", {
  command: "gemini",
  args: ["--experimental-acp"],
});

The protocol transport (@zed-industries/agent-client-protocol) is an optional dependency loaded lazily through the installPackage hint, just like the AI SDK harnesses. The agent binary itself (@google/gemini-cli, @zed-industries/claude-code-acp, …) is a separate external CLI; the presets launch it through npx and the command/args stay overridable because agent ACP entry flags still evolve.

permissionMode maps onto ACP session/request_permission using the tool-call kind the agent reports: reads always run, edits run under allow-edits, and everything risky prompts unless allow-all. Approvals surface as the normal approval-request events. The adapter serves fs/read_text_file and fs/write_text_file against the session workspace (refusing paths that escape it) and writes emit file-change events; terminal methods are not advertised, so the agent uses its own shell.

Codex auth: Code UI vs harness adapters

There are two Codex surfaces:

  • Agent-Native Code / Desktop runs codex exec on the user's machine and reuses the local Codex CLI login reported by codex login status.
  • ai-sdk-harness:codex uses the upstream AI SDK Harness native subscription flow where supported. No extra setting is required: credentials stay on the host, explicit API-key credentials take precedence, and the resulting auth is bridged into the sandbox without copying the local auth file. Set harnessOptions.auth to "ai-gateway" to use AI Gateway only; that mode never reads a native subscription.

The built-in ai-sdk-harness:codex adapter only supports permissionMode: "allow-all", because that is the stable Codex runtime's only accepted mode. Omit permissionMode to use that default; allow-reads and allow-edits are rejected for this adapter.

Register and resolve

import {
  registerBuiltinAgentHarnesses,
  resolveAgentHarness,
} from "@agent-native/core/agent/harness";

registerBuiltinAgentHarnesses();
const adapter = resolveAgentHarness("ai-sdk-harness:codex");

resolveAgentHarness(name, config?) returns an AgentHarnessAdapter. The optional config is forwarded to the adapter factory — for the AI SDK adapters that maps to AiSdkHarnessAdapterOptions (label, description, permissionMode, harnessOptions, and agentOptions). Use listAgentHarnesses() to enumerate what is registered for a picker.

Run a turn

startAgentHarnessRun bridges a harness session into the shared run-manager lifecycle. It creates (or reuses) the native session, persists it, streams the turn, translates each harness event into transcript events, and detaches the resumable state when the turn completes.

import { startAgentHarnessRun } from "@agent-native/core/agent/harness";

const run = startAgentHarnessRun({
  runId,
  threadId,
  adapter,
  input: { prompt },
  createSession: {
    sessionId,
    resumeState, // opaque value from a previous turn, if resuming
    instructions,
    sandbox, // required for sandboxed harnesses — see Sandbox Adapters
    permissionMode: "allow-all",
    tools, // a narrow, intentional set of host tools (see below)
  },
  ownerEmail,
  orgId,
});

startAgentHarnessRun returns the ActiveRun from the run-manager, so the turn shows up through the existing run routes, transcript, and cancellation just like any other agent run. Pass an already-created session instead of createSession to continue a session you are holding in memory.

Sessions and resume

A harness owns long-lived native session state. Agent-Native persists it in SQL so a thread can survive across turns, processes, and deploys. The resumeState is opaque — Agent-Native stores it and hands it back, but never inspects or interprets it.

import {
  getLatestAgentHarnessSessionForThread,
  listAgentHarnessSessions,
} from "@agent-native/core/agent/harness";

const last = await getLatestAgentHarnessSessionForThread(threadId);
// Feed last?.resumeState into createSession.resumeState on the next turn.

The store also exposes saveAgentHarnessSession, updateAgentHarnessSession, getAgentHarnessSession, getAgentHarnessSessionByRunId, markAgentHarnessSessionStopped, and ensureAgentHarnessSessionTables. startAgentHarnessRun calls the save/update/stop paths for you; reach for them directly only in a custom host.

Host tools and permissions

A harness brings its own native tools (read, edit, write, shell, and so on), so you do not re-expose file editing as host tools. Pass only a narrow, intentional set of Agent-Native actions through createSession.tools when you want the harness to reach specific app operations — and keep defineAction auth, request context, timeouts, truncation, and read-only metadata intact when you do.

permissionMode gates what the harness may do without approval:

Mode Meaning
allow-reads Default. Reads run; edits and risky actions prompt
allow-edits Reads and edits run; other risky actions prompt
allow-all No approval gating

When a harness pauses for approval it emits an approval-request event and the session is marked idle with the pending approval recorded, so the UI can surface it and resume on the user's decision. See Human Approval for the approval surface.

Events

A harness session streams AgentHarnessEvent values, which Agent-Native translates to the standard AgentChatEvent stream with agentHarnessEventToAgentChatEvents. The event union covers text-delta, thinking-delta, activity, tool-start, tool-done (which can carry an mcpApp payload for native widgets), approval-request, file-change, compaction, usage, error, and done. Because tool results flow through the same translation, action-declared native widgets still render — see Native Chat UI.

Background runs and the UI

Harness runs project into the shared BackgroundAgentRun shape with createAgentHarnessBackgroundAgentController() and are available through the existing run routes as goalId=agent-harness. That means a long-running Claude Code or Codex session appears in the same background-run and transcript surfaces as Agent Teams and other adapters, with listAgentHarnessBackgroundRuns, listAgentHarnessBackgroundTranscriptEvents, getAgentHarnessBackgroundRun, and stopAgentHarnessBackgroundRun available for custom hosts.

Custom adapters

To wrap a runtime that is not one of the built-ins, implement AgentHarnessAdapter and register it. The adapter declares its capabilities and creates sessions; a session exposes streamTurn and optional continueTurn, approve, detach, stop, and destroy.

import {
  registerAgentHarness,
  type AgentHarnessAdapter,
} from "@agent-native/core/agent/harness";

const myHarness: AgentHarnessAdapter = {
  name: "acme:my-coder",
  label: "Acme Coder",
  description: "Runs the Acme coding agent.",
  installPackage: "@acme/coder",
  capabilities: {
    sandbox: true,
    resumable: true,
    approvals: true,
    hostTools: true,
    fileEvents: true,
  },
  async createSession(opts) {
    // Build your native session and adapt it to AgentHarnessSession.
    return createAcmeSession(opts);
  },
};

registerAgentHarness({
  name: myHarness.name,
  label: myHarness.label,
  description: myHarness.description,
  installPackage: myHarness.installPackage,
  capabilities: myHarness.capabilities,
  create: () => myHarness,
});

Keep the runtime package optional with a dynamic import in createSession and an installPackage hint. For bridge-backed coding harnesses, require a real sandbox/workspace provider rather than running an arbitrary coding agent in the host process — see Sandbox Adapters. The AI SDK adapter (createAiSdkHarnessAdapter, backed by HarnessAgent from @ai-sdk/harness) is one implementation of this contract, not the public abstraction.

Don't

  • Don't add Claude Code, Codex, Cursor, Mastra, or Pi as an AgentEngine. They own their loop; running one under AgentEngine.stream() double-runs the loop and loses session lifecycle semantics.
  • Don't replay full Agent-Native chat history into a harness each turn. Resume the harness session with its resumeState instead.
  • Don't store resumeState in application_state. It belongs in the harness session SQL table.
  • Don't expose every app action to every harness session by default. Hand it a small, intentional tool set.

What's next