# Harness adapters (/brain/docs/adapters/harness)

A **harness adapter** runs the model loop, manages conversation context and compaction, and writes native session state inside the session's `native/` directory while Brain coordinates requests, tool dispatch, event logs, and checkpoints.

## Go lifecycle (`harness.Factory` and `harness.Runner`) [#go-lifecycle-harnessfactory-and-harnessrunner]

`Factory.Open(ctx, opts)` receives a `harness.Options` struct containing the session ID, the `native/` working directory (`Dir`), the immutable session `Config`, enabled `Tools` schemas, resolved `Provider` credentials (`APIKey`, `BaseURL`, `API`), and two callbacks: `Event` (for emitting `assistant.delta` and `assistant.message`) and `Call` (for executing bound tools).

The application package exports these contracts as `brain.HarnessFactory`, `brain.Harness`, and `brain.HarnessOptions`. `Open` returns a runner with five methods (see [Go interfaces](/brain/docs/adapters/interfaces)):

| Method | Purpose |
| --- | --- |
| `Prompt(ctx, text)` | Runs one user turn to completion (including any model-initiated tool calls and follow-up generations) and returns when the turn settles. |
| `Steer(ctx, text)` | Steers an active turn if `Factory.SupportsSteering()` is enabled. |
| `Interrupt(ctx)` | Aborts the active turn cleanly when the caller sends `user.interrupt` or closes the session. |
| `Checkpoint(ctx)` | Returns the list of `core.NativeFile` entries (`path`, `size`, `append_only`) relative to `Dir` required to restore the conversation. |
| `Close()` | Terminates the runner and releases its resources. |

### Core runner invariants [#core-runner-invariants]

- **Passive open:** Opening an existing session directory must load saved native state without calling the model, re-running tools, or starting background cache-warming timers.
- **Settled return:** `Prompt` blocks until the turn finishes or is interrupted.
- **Clean checkpoints:** `Checkpoint` is called only after `Prompt` settles and must return regular, append-only relative file paths and exact byte sizes in `Dir`, excluding any provider credentials.

## Child process stdio protocol (`harness.ProcessFactory`) [#child-process-stdio-protocol-harnessprocessfactory]

You do not need to write Go code to add a harness runner. Every entry in the `"harnesses"` configuration block uses `harness.ProcessFactory` (`internal/harness/process.go`) to spawn a child process (such as `node runners/pi/dist/main.js`) and communicate over newline-delimited JSON frames (`RunnerFrame`) on `stdin` and `stdout`.

- **Frame limit:** Each line is capped at `4 MiB` (`MaxFrame = 4 << 20`) with strict JSON decoding (`DisallowUnknownFields`).
- **Standard streams:** `stdout` is reserved strictly for `RunnerFrame` JSON lines. Brain suppresses runner diagnostic payloads from logs.
- **Startup timeout:** The initial `open` request during `Factory.Open` has a 30-second timeout.

### Brain → Runner requests (`stdin`) [#brain--runner-requests-stdin]

Brain sends RPC requests with `attachment`, `id`, `method`, and `params`. An attachment identifies one native session within a shared host. `prompt`, `steer`, `interrupt`, events, and tool callbacks also carry the active `turn` identity; replies echo the request's attachment and turn. Open, checkpoint, and detach have no active turn.

```json
{"attachment":"ATTACHMENT_ID","id":"REQUEST_ID","method":"open","params":{"session_id":"SESSION_ID","dir":"/path/to/native","config":{"model":"openai/gpt-4.1-mini","tools":["echo"]},"tools":[{"name":"echo","description":"Echo text","input_schema":{"type":"object"}}],"provider":{"api_key":"PROVIDER_TOKEN"}}}
{"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"REQUEST_ID","method":"prompt","params":{"text":"Hello"}}
{"attachment":"ATTACHMENT_ID","id":"REQUEST_ID","method":"checkpoint","params":null}
```

The runner replies on `stdout` with a matching `id` and either `result` or `error` (`code: "provider_error"` or `"brain_error"`):

```json
{"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"REQUEST_ID","result":{}}
{"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"REQUEST_ID","error":{"code":"provider_error","message":"upstream rate limit exceeded"}}
```

| Method | `params` | `result` |
| --- | --- | --- |
| `open` | `harness.Options` (`session_id`, `dir`, `config`, `tools`, `provider`) | `{}` (or diagnostic metadata such as `{"tools": [...]}`) |
| `prompt` | `{"text": "..."}` (`core.Text`) | `{}` (or `{"interrupted": true}`) |
| `steer` | `{"text": "..."}` (`core.Text`) | `{}` |
| `interrupt` | `null` | `{}` |
| `checkpoint` | `null` | `[{"path": "session/file.jsonl", "size": 1024, "append_only": true}]` (`[]core.NativeFile`) |
| `detach` | `null` | `{}`; release this settled attachment without stopping peers. |

### Runner → Brain events and tool calls (`stdout`) [#runner--brain-events-and-tool-calls-stdout]

While `prompt` is running, the runner can emit streaming events and reverse `tool.call` requests (up to 32 concurrent tool calls per runner):

```json
{"attachment":"ATTACHMENT_ID","turn":"TURN_ID","event":"assistant.delta","data":{"text":"Hello"}}
{"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"CALL_FRAME_ID","method":"tool.call","params":{"id":"TOOL_CALL_ID","name":"echo","arguments":{"value":"hi"}}}
{"attachment":"ATTACHMENT_ID","turn":"TURN_ID","event":"assistant.message","data":{"text":"Hello!","usage":{"input":12,"output":4},"stop_reason":"stop"}}
```

For each `tool.call`, Brain validates that the tool is bound to the session, appends a `tool.call` event, dispatches the call to the external tool service, appends a `tool.result` event, and writes the correlated response frame back to the runner's `stdin`:

```json
{"attachment":"ATTACHMENT_ID","turn":"TURN_ID","id":"CALL_FRAME_ID","result":{"content":[{"type":"text","text":"hi"}],"is_error":false}}
```

Wire types for this protocol are generated in `internal/harness` and `runners/pi/src/generated.ts` via `go run ./cmd/generate`.

## Isolation and verification [#isolation-and-verification]

A harness adapter must disable all built-in local tools (such as local shell execution or file editors), ambient plugins, and background cache warmers so the model can only invoke tools explicitly bound in `config.tools`. `harness.ProcessFactory` passes only minimal OS environment variables (`PATH`, `SystemRoot`, `WINDIR`, `COMSPEC`, `PATHEXT`, `TMP`, `TEMP`, `TMPDIR`, `LANG`, `LC_ALL`, and `NO_COLOR=1`) and supplies provider credentials exclusively through the `open` frame.

When testing a new harness adapter, verify:

1. **Passive open:** Reopening a saved directory makes zero model or tool calls until the next `Prompt`.
2. **Checkpoint fidelity:** Restoring a checkpoint into a fresh directory produces the exact same prompt context on the next turn.
3. **Failure separation:** Tool errors (`is_error: true`) return to the model as tool results, whereas broken stdio framing, unexpected exits, or disk failures abort the turn with `brain_error`.
4. **Clean interruption:** Interrupting a turn while a tool call is waiting stops execution cleanly and leaves prior checkpoint files intact.
