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

# SDK guide

> The curated public surface — what to import, which seam to build on, and how the two planes relate.

Everything documented here is exported from `avatar.__all__` — the
stable surface to build against. Deep imports of internal modules may break
without notice; the generated **API reference** covers internals page by page.

## The SDK map

The `__all__` list is grouped by role, and the grouping *is* the map:

| Group                     | Exports                                                                                           | Build on it when…                                                          |
| ------------------------- | ------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Core entry points & state | `Harness`, `HarnessConfig`, `TaskState`, `RunDeps`, `Workspace`                                   | you want to run tasks (everyone starts here).                              |
| Model decisions           | `ModelClient`, `ModelDecision`, `ToolCall`, `FinalAnswer`, `AskUser`                              | you're swapping the model provider or scripting a fake model for tests.    |
| Tools                     | `ToolDefinition`, `ToolRegistry`, `ToolResult`                                                    | you're adding or replacing tools.                                          |
| Two-plane async surface   | `Session`, `EventBus`, `JsonlEventJournal`, `EventSink`, `ApprovalController`, `ApprovalGrant`    | you're building a UI, a supervisor, or an autonomous wrapper over one run. |
| Multi-turn session scope  | `ReplSession`, `SessionState`, `Turn`                                                             | you're building a conversation (many goals, shared history and grants).    |
| Typed lifecycle events    | `HarnessEvent`, `AgentStart` … `CancellationObserved`, `parse_event`, `dump_event`, `load_events` | you're rendering, journaling, or replaying a run.                          |

## The `Harness` facade

`Harness` assembles a run's collaborators; every one is overridable via a
constructor kwarg (model, tools, verifier, policy, context builder, emitter) —
swap a seam, keep the rest:

```python theme={null}
from avatar import Harness

harness = Harness.from_env()    # HarnessConfig() from AVATAR_* / .env
```

Three entry points, one engine:

| Call                                                                    | Returns                     | Use when                                                                                                                                                                                                |
| ----------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `harness.run(task, *, task_kind, allow_dirty)`                          | terminal `TaskState`        | batch / scripting — run to completion, synchronously.                                                                                                                                                   |
| `await harness.arun(...)`                                               | terminal `TaskState`        | you're already on an event loop and only need the result.                                                                                                                                               |
| `harness.session(task, *, task_kind, allow_dirty, journal, unattended)` | a not-yet-started `Session` | you want the event stream and the approval/cancel controls. Pass `unattended=True` for a batch/autonomous run so a tier-3/denylist `ask` is auto-denied instead of blocking on a human who isn't there. |

`Harness.from_env()` constructs without an API key — credentials are needed
only when the model is first called, so a fully wired harness (e.g. with a fake
model, in tests) never touches the network.

## The two-plane `Session`

The interactive contract, and the load-bearing boundary of the whole SDK
(design §13): **observation flows out, control flows in, and the two never mix.**

```python theme={null}
import asyncio
from avatar import ApprovalRequested, Harness

harness = Harness.from_env()
session = harness.session("fix the failing auth test", task_kind="edit")

run_task = asyncio.create_task(session.run())     # drive the engine
async for event in session.events():              # observation plane (out)
    render(event)
    if isinstance(event, ApprovalRequested):      # control plane (in)
        await session.resolve_approval(event.approval_id, allow=True)
state = await run_task
```

**Observation — `session.events()`.** An async stream of typed `HarnessEvent`s.
It subscribes *eagerly* (at call time, not first iteration), so a consumer
created before `run()` misses nothing. Each call yields a fresh, independent
stream — any number of observers can watch one run, and a slow or broken
observer can never block the loop or its peers. The stream ends after
`agent_end`.

**Control — two methods, nothing else.**

* `await session.resolve_approval(approval_id, *, allow, remember=False)` —
  answer a pending `ApprovalRequested`. With `allow=True, remember=True` (the
  "always" choice) the session stores an `ApprovalGrant` scoped to the call's
  program (`argv[0]`), so later calls sharing that program auto-allow — observable
  as `ApprovalResolved(via="grant")`, never silent. Grants are session-scoped,
  never global, and never cover tier-4 (destructive/external) actions.
* `await session.cancel(reason)` — trip the cancellation token. The loop
  observes it at the next checkpoint, records the interruption as feedback, and
  settles to a terminal state; any in-flight approval is denied so a gated run
  can't hang.

An event can *announce* that approval is needed; only the control method
*decides* it. Nothing you do on the event stream can alter the run.

<Note>
  Approval prompts fire for `ask`-gated calls: tier-3 tools (`run_command`) and
  sensitive-path denylist hits. The edit tools (`str_replace`/`write_file`/`delete_file`)
  are tier 1 — they auto-allow once their target paths validate inside the workspace,
  and the verifier judges the resulting diff.
</Note>

**Journaling.** Pass `journal=JsonlEventJournal(path)` to `harness.session(...)`
and every event is written **write-ahead** — committed to disk before any
subscriber sees it — so even a crashed consumer leaves a complete, replayable
record. `load_events(path)` reloads the file as typed events.

## The event catalog

Every event extends `EventBase` (`schema_version`, bus-assigned monotonic
`event_id`, `session_id`, `task_id`, `turn`, `ts`) and is exhaustively matchable
on its `type` discriminator:

| Event                                   | Key payload                                       | Meaning                                                                              |
| --------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `AgentStart` / `AgentEnd`               | `goal` / `outcome`                                | A run began / settled (the stream's end).                                            |
| `TurnStart` / `TurnEnd`                 | `iteration`                                       | One model/tool iteration.                                                            |
| `PhaseChanged`                          | `old`, `new`                                      | The control phase advanced (`investigating → editing → verifying`).                  |
| `ModelDecisionEvent`                    | `thought`, `action_type`, `action`                | The model chose this turn's action.                                                  |
| `ModelUpdate`                           | `delta`, `channel="display"`                      | A streamed display delta (droppable under backpressure).                             |
| `ToolStart` / `ToolEnd`                 | `tool`, `input` / `success`, `summary`, `content` | A tool call executed.                                                                |
| `ApprovalRequested`                     | `approval_id`, `tool`, `reason`, `input`          | A gated call awaits a human — announce only.                                         |
| `ApprovalResolved`                      | `approval_id`, `allowed`, `via`                   | The decision (`via="human"`, `"grant"`, or `"auto"` for an unattended/timeout deny). |
| `VerificationStart` / `VerificationEnd` | — / `passed`, `summary`                           | The harness-owned verifier ran.                                                      |
| `CancellationObserved`                  | `reason`                                          | The loop saw a tripped token and is stopping.                                        |

Two further events ride the stream and the journal but are not yet exported in
`__all__` (deep-import from `avatar.event_types` for now): `ModelUsage`
(`prompt_tokens`/`completion_tokens` per turn) and `DecisionError` (a malformed
model reply, with a capped raw excerpt).

The union is **closed and versioned**: `parse_event` rejects unknown types, and
the journal round-trips every event verbatim.

## Multi-turn conversations — `ReplSession`

`ReplSession` is the scope *above* one task: conversation history, the sequence
of per-goal `TaskState`s, session-scoped grants, and the current mode. Each goal
runs as a fresh task through the same single-task `Session` — batch is just the
degenerate one-submit case.

```python theme={null}
from avatar import Harness, ReplSession

repl = ReplSession(Harness.from_env())
state = await repl.submit("how does the permission gate work?")   # goal 1
state = await repl.submit("now add a tier for network access")    # goal 2 — sees goal 1's history
```

What it gives you over raw sessions:

* **History seeding** — prior turns become explicit evidence on the next task
  (structured seeding, not transcript bleed).
* **Grant carry-over** — an "always allow" granted in one goal persists to later
  goals (shared by reference with each per-goal `Session`).
* **Mode routing** — each goal's `task_kind` resolves as explicit `/mode`
  override → LLM classifier (when `AVATAR_CLASSIFIER_MODEL` is set) → word
  heuristic. The verdict is visible (`repl.last_mode_source`) and always
  correctable — never silent control.
* **`@path` grounding** — `@src/foo.py` in a goal seeds that file as initial
  evidence, read through the workspace so the sensitive-path denylist applies.
* **Meta commands** — `repl.is_meta(text)` / `repl.run_meta(text)` handle
  `/help`, `/quit`, `/state`, `/mode`, `/plan`, `/diff`, `/permissions` locally;
  they never reach the model.
* **Plan mode** — `repl.submit_plan(prompt, decide)` drives a no-net-change plan →
  approve/revise → build, with the approved plan seeding the edit task as a
  constraint.
* **Verification authority** — the REPL default is *conversational*: the
  verifier always runs and **steers** (a failing check drives repair, or a
  gated contract amendment), and at repair exhaustion the turn defers to the
  human (`blocked` with a question) instead of pronouncing failure.
  `ReplSession(harness, auto=True)` makes exhaustion a hard `failed`. A failed
  verdict is never delivered as `success` (§23.5, ADR-0046).

The Textual cockpit (the separate `jo-cli` package, the `jo` command — a
consumer of the core, never part of it) is exactly this surface rendered
full-screen — nothing it does is unavailable to your own UI.

## Configuration

All fields of `HarnessConfig`, env-overridable with the `AVATAR_` prefix (or a
local `.env`):

| Variable                              | Default                        | Meaning                                                                                                                                                                                                                                                                                                                |
| ------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AVATAR_API_KEY`                      | —                              | Endpoint key; falls back to `OPENAI_API_KEY`.                                                                                                                                                                                                                                                                          |
| `AVATAR_MODEL`                        | `openai/gpt-4o-mini`           | Model name, as your endpoint serves it.                                                                                                                                                                                                                                                                                |
| `AVATAR_BASE_URL`                     | `https://openrouter.ai/api/v1` | Any OpenAI-compatible endpoint.                                                                                                                                                                                                                                                                                        |
| `AVATAR_WORKSPACE_ROOT`               | `.`                            | The repo the agent operates on.                                                                                                                                                                                                                                                                                        |
| `AVATAR_MAX_ITERATIONS`               | `50`                           | Iteration budget → `incomplete`.                                                                                                                                                                                                                                                                                       |
| `AVATAR_MAX_WALL_CLOCK_SECONDS`       | `1800`                         | Per-agent-run timeout → `incomplete` (reset each run, not cumulative across a sitting). Batch/eval keep the cap; the interactive cockpit (`jo`) **disables it by default** (unset ⇒ no clock — the human's Ctrl-C and `AVATAR_MAX_ITERATIONS` are the backstops). Set it explicitly to re-impose a cap in the cockpit. |
| `AVATAR_MAX_CONSECUTIVE_FAILURES`     | `5`                            | Thrash guard (tool errors in a row) → `incomplete`.                                                                                                                                                                                                                                                                    |
| `AVATAR_MAX_REPAIR_ATTEMPTS`          | `3`                            | Verification rejections in a row → `failed`.                                                                                                                                                                                                                                                                           |
| `AVATAR_AUTONOMOUS_ESCALATION_POLICY` | `deny`                         | Unattended disposition for a `switch_to_editing` escalation (ADR-0048): `deny` refuses the mid-run `investigate → edit` switch with no human present; `approve` self-ratifies it so a misrouted fix can recover on its own (same vocabulary as the amendment knob). Attended runs always ask.                          |
| `AVATAR_ESCALATION_THRASH_REPEATS`    | `3`                            | Repeated no-progress actions (while an investigation holds a non-empty diff) that trip the harness thrash nudge directing the model to `switch_to_editing` (ADR-0048).                                                                                                                                                 |
| `AVATAR_MAX_CONTEXT_TOKENS`           | `100000`                       | Hard bound on the per-turn context packet.                                                                                                                                                                                                                                                                             |
| `AVATAR_TEST_COMMAND`                 | *(empty)*                      | The verification plan's **override tier** (ADR-0007): a non-empty value always wins. Empty = unset → the planner detects the repo's declared test contract (CI workflows > manifests > Makefile). The harness runs the resolved command itself — never model-mediated.                                                 |
| `AVATAR_LINT_COMMAND`                 | *(empty)*                      | Same, for the lint/type slot of the verification plan.                                                                                                                                                                                                                                                                 |
| `AVATAR_PLANNER_MODEL`                | *(unset)*                      | Opt-in LLM fallback for verification-plan resolution: consulted only for slots detection left empty, may only *propose* a command citing the repo artifact that declares it (the harness validates the citation). Unset keeps resolution fully deterministic/offline.                                                  |
| `AVATAR_COMMAND_TIMEOUT_SECONDS`      | `120`                          | Per-command timeout.                                                                                                                                                                                                                                                                                                   |
| `AVATAR_REQUEST_TIMEOUT_SECONDS`      | `240`                          | Per-request ceiling for one non-streaming model call (ADR-0028). With streaming on (the default) the idle timeout below does the fast stall-detection, so this is a loose backstop; keep it under `AVATAR_MAX_WALL_CLOCK_SECONDS`.                                                                                     |
| `AVATAR_STREAM_MODEL_CALLS`           | `true`                         | Stream completions and bound the gap *between* chunks (ADR-0029 R5), so a stalled provider is caught in \~idle-timeout regardless of how long a legitimate generation runs — and a model call can be cancelled mid-flight. `false` restores the exact non-streaming behavior.                                          |
| `AVATAR_REQUEST_IDLE_TIMEOUT_SECONDS` | `30`                           | The streaming idle watchdog: abort (and transport-retry) a model call if no token arrives for this long (ADR-0029). Independent of total generation length.                                                                                                                                                            |
| `AVATAR_TRANSPORT_MAX_RETRIES`        | `2`                            | Transport-layer retries on a NUL/empty body, an idle stall, or a request failure (ADR-0028/0029): the same request is re-issued with exponential backoff + jitter — never re-prompted to the model. On exhaustion the run surfaces a system failure, never a silent `incomplete`.                                      |
| `AVATAR_COMMAND_OUTPUT_BUDGET`        | `16000`                        | Char budget for the command-tool (`run_tests`/`run_linter`/`run_command`) output excerpt shown to the model: keeps the head **and** tail, elides the middle. Floored at 256 — the bound is unconditional, never disable-able.                                                                                          |
| `AVATAR_APPROVAL_TIMEOUT_SECONDS`     | *(unset)*                      | Backstop on a blocking (attended) approval: deny it after N seconds so a run can't hang inside the gate. Unset = wait indefinitely (a human at a REPL); an `unattended` run never blocks regardless.                                                                                                                   |
| `AVATAR_SENSITIVE_PATH_GLOBS`         | built-in set                   | Path denylist (`.env`, `*.pem`, `.ssh`, …) refused for read/patch/search. A JSON list; *replaces* the defaults.                                                                                                                                                                                                        |
| `AVATAR_NATIVE_TOOL_CALLS`            | `true`                         | Decisions ride provider function-calling; `false` forces the legacy JSON protocol.                                                                                                                                                                                                                                     |
| `AVATAR_CONTEXT_MAX_DETAIL_CHARS`     | `16000`                        | Per-item cap on verbatim evidence detail in context.                                                                                                                                                                                                                                                                   |
| `AVATAR_CONTEXT_DETAIL_CHAR_BUDGET`   | `48000`                        | Total verbatim-detail budget per packet.                                                                                                                                                                                                                                                                               |
| `AVATAR_CONTEXT_VERIFIER_PIN_COUNT`   | `2`                            | Number of recent verifier outputs kept verbatim even when older evidence is compacted.                                                                                                                                                                                                                                 |
| `AVATAR_CLASSIFIER_MODEL`             | `openai/gpt-5-nano`            | The REPL's mode router (one cheap call per goal). Empty disables → heuristic only.                                                                                                                                                                                                                                     |
| `AVATAR_INTERACTIVE`                  | `true`                         | Whether `ask_user` may prompt; when off, an unanswered question yields `blocked`.                                                                                                                                                                                                                                      |

## Testing without a network

`ModelClient` is a one-method protocol, so a scripted fake makes any flow
deterministic and offline:

```python theme={null}
from avatar import FinalAnswer, Harness, HarnessConfig, ModelClient, ModelDecision, ToolCall

class ScriptedModel(ModelClient):
    def __init__(self, decisions):
        self._decisions, self._i = decisions, 0
    def decide(self, context):
        d = self._decisions[min(self._i, len(self._decisions) - 1)]
        self._i += 1
        return d

harness = Harness(
    config=HarnessConfig(workspace_root="."),
    model=ScriptedModel([
        ModelDecision(thought_summary="read it",
                      action=ToolCall(name="read_file", input={"path": "README.md"})),
        ModelDecision(thought_summary="answer",
                      action=FinalAnswer(answer="Per README.md (line 1), this repo is …")),
    ]),
)
state = harness.run("what does this repo do?")   # runs the real loop, verifier and all
```

This is exactly how the project's own test suite drives the engine.
