> ## Documentation Index
> Fetch the complete documentation index at: https://vetta.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Harnesses

> The agent loop is one field. Which loop you pick changes how the work is done — and what an idle session costs.

A **harness** is the agent loop itself: assemble a turn, call the model, parse the tool calls, decide what carries into the next turn. It is the one layer of Vetta you select, and it is a single field on an [agent](/docs/concepts/agents):

```json theme={"system"}
{ "name": "Reconciler", "model": "zai-org/GLM-5.2-FP8", "harness": "pi" }
```

Everything under it is unchanged when you change it — the [durable loop](/docs/concepts/runtime), the [budget gate](/docs/concepts/budgets), the [tool policy](/docs/concepts/policies), the [event log](/docs/concepts/events-and-streaming) and the ledger all sit *beneath* the harness, not inside it. That is what makes the field safe to change on an existing agent.

## The loops

| `harness`     | what it is                                                                                     | runs           | idle session holds |
| ------------- | ---------------------------------------------------------------------------------------------- | -------------- | ------------------ |
| `pi`          | The default. A coding agent embedded in Vetta's composition, driving our sandbox tools.        | on a micro-VM  | the machine        |
| `vetta`       | Our own loop, held to a small measured core. The only one that runs **in the session itself**. | in the session | nothing            |
| `hermes`      | An external coding agent, run as a process.                                                    | on a micro-VM  | the machine        |
| `claude_code` | Claude Code, run as a process.                                                                 | on a micro-VM  | the machine        |

<Warning>This table says what each loop **is**. It does not say what you can use — that varies by
environment, because a harness needs both an adapter compiled in and, for one that runs as a
process, a base image built in that environment's provider account. [`GET
/v1/harnesses`](/api/harnesses) is authoritative, and its `runnable` field answers for the deploy
you are talking to.</Warning>

## Two questions decide which one

They are separate, and both matter.

**What must the loop be able to do?** Harnesses differ in real ways, and the differences are published as capabilities:

| capability          | what it means                                                                                                                                                                                                                                                                                                                               |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `approvals`         | The loop can hold a tool call and wait for a decision. Required if any tool's permission is `ask`.                                                                                                                                                                                                                                          |
| `structured_output` | The loop can enforce [`structured_output_required`](/docs/capabilities/structured-outputs).                                                                                                                                                                                                                                                      |
| `streaming_deltas`  | The loop emits incremental `message.delta` events. Without it you still get `message.completed` — the answer arrives whole rather than as it is written.                                                                                                                                                                                    |
| `injected_tools`    | The platform's tools reach the loop's model — team tools, MCP servers, connected accounts, social and apps — governed by the agent's `tools` policy. It does not say *how*: a loop we host is handed them for the turn, and an agent that is a CLI process in a machine of its own is given a session-scoped tool endpoint to call instead. |

### Where the tools come from, and the two limits

Every harness now declares `injected_tools: true`, and these families reach every published loop:

* **The team tools.** `send_to_agent`, `wait_for_agents`, `list_agents`, `board_read` and `board_write`. A [coordinator](/docs/team/coordinator) runs on any harness.
* **[MCP servers](/docs/api/mcp)** declared on the agent, with their tools named `<server>.<tool>` as always.
* **[Connected accounts](/docs/identity/connections)** and the tools they contribute, named `<connector>.<tool>`, plus the tools for asking for a connection the agent does not yet have.
* **Social publishing** and your [apps](/docs/api/apps), including the tools an app declares for itself.

**On a harness whose agent is a CLI process in a machine of its own, these arrive by a different road.** The loop is given a session-scoped tool endpoint, scoped to that one session by a credential minted for the turn, and it calls the tools over it. You configure nothing: the endpoint is written into the machine for you at the start of every turn. The agent's own toolset — its file, shell and search tools — is unchanged and sits alongside ours.

<Note>
  **Vetta's own built-ins do not travel that second road yet** — [web search and fetch, image and
  video generation](/docs/capabilities/tools#built-in-tools), skill disclosure, `publish_file` and the
  managed browser are offered on `pi` and `vetta` only. A `claude_code` or `hermes` agent has its own
  CLI's web, file and search tools in their place; write your prompt against those, not against
  `publish_file`.
</Note>

**Your `tools` policy still governs all of it, by the same names.** A tool set to `deny` is never offered to the model on any harness: on these it is not merely refused when called, it is left out of the list the agent is given, so the model never learns it exists.

Two limits are real on this road, and both are consequences of `approvals: false`:

<Warning>
  **A tool whose permission is `ask` is left out entirely, not gated.** Nothing in such a machine can hold a call open while a person decides — the agent's own prompt is closed, and the call is one request that must be answered now. This is why a session whose toolset carries an `ask` is **refused when it starts** on a harness declaring `approvals: false`, naming `harness`. The one case you can meet without writing `ask` yourself is **social publishing**: `social.post` defaults to `ask`, so on these harnesses it is not offered unless you set it to `allow` explicitly.

  **`wait_for_agents` does not pause the turn.** A coordinator that fans work out and then calls it is not suspended mid-turn the way it is on a loop we host; it parks once the turn goes idle, and the delegated threads open correctly either way. The cost is a few extra model calls between the fan-out and the park, not a wrong answer.
</Warning>

<Card title="The values, per harness" icon="table-cells" href="/docs/concepts/harness-capabilities">
  **[Harness capabilities](/docs/concepts/harness-capabilities)** is the matrix: which of the four each
  loop declares, what an idle session holds, which pairings are refused and where — and the one
  catalogue entry that is still optimistic.
</Card>

A session that asks for a capability its harness does not declare is **refused when it starts**, with a `400` naming the field — and where the configuration can be judged earlier, the refusal happens earlier still, at the agent save. It is never a quiet best effort: an `ask` toolset that silently became `allow` would remove your human-in-the-loop gate with nothing to notice it by.

**What should an idle session cost?** This is the `execution` field, and it is not the same question as who operates the loop:

| execution | what an idle session holds                                                                                                                |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `isolate` | Nothing. The loop runs in the session itself and its workspace is stored with the session, so a session between turns costs storage only. |
| `sandbox` | A [micro-VM](/docs/computer/index). The agent is a process running on it, so the session holds the machine across turns.                       |
| `local`   | Runs on your own machine, against your own logged-in account. Not available to a hosted session.                                          |

<Note>A long-running task that spends most of its life waiting — for a schedule, for a person, for a webhook — costs very differently on `isolate` than on `sandbox`. That is usually the deciding factor for long-horizon work, and it is why the field is published rather than left for you to infer from a bill.</Note>

## Reading the catalogue

The set is not fixed in the documentation, because a harness needs an adapter compiled in and **an environment can have a different set**. Read it at runtime:

```ts theme={"system"}
const harnesses = await client.harnesses.list();

for (const h of harnesses.data) {
  console.log(h.base, h.execution, h.runnable ? "available" : "not on this deploy");
}
```

`runnable` answers for the deploy you are talking to. A harness can be published — so a picker can show it, disabled — while this environment cannot run it, in which case starting a session on it is a `501` naming the field rather than a failure part-way through a turn.

Full field reference: [`GET /v1/harnesses`](/docs/api/harnesses) and [`client.harnesses`](/docs/sdk/harnesses).

## Changing it

`harness` is versioned with the rest of the agent, so changing it creates a new [agent version](/docs/concepts/agents) and a session pins the version it started on. An in-flight session is never re-hosted onto a different loop mid-run.

The prompt, the skills, the budget and the window all carry across unchanged — that is the point of the seam.

**The tools carry across too, and that is newer than the rest of this page.** Moving an agent onto a harness that runs a CLI in a machine of its own no longer withdraws its team tools, its MCP servers or its connected accounts — they are served over a session-scoped tool endpoint instead of handed to the loop. What does not carry across is anything that needs a capability the destination does not declare: an `ask` in your toolset, or `structured_output_required`. Because `harness` is create-only you meet that at the agent save, where the refusal names the field to change.
