Skip to main content
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:

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:
Three entry points, one engine: 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.
Observation — session.events(). An async stream of typed HarnessEvents. 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.
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.
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: 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 TaskStates, 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.
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 commandsrepl.is_meta(text) / repl.run_meta(text) handle /help, /quit, /state, /mode, /plan, /diff, /permissions locally; they never reach the model.
  • Plan moderepl.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):

Testing without a network

ModelClient is a one-method protocol, so a scripted fake makes any flow deterministic and offline:
This is exactly how the project’s own test suite drives the engine.