# avatar.artifact Source: https://codexceed.mintlify.app/api-reference/artifact ArtifactManager — the final task summary (§14). The artifact reports, never re-derives: `status` is exactly `state.outcome`, and the rest is read off `TaskState` plus the workspace diff (the deliverable — the harness never commits, §15). Keeping `status` a copy of `outcome` is what lets a caller distinguish "give me more budget" (`incomplete`) from "this can't be verified" (`failed`) from "I need input" (`blocked`). ## Classes ### `Artifact` The structured final summary of one task (§14). ```python theme={null} Artifact(*, status: str, summary: str, files_changed: list[str] = , commands_run: list[str] = , verification: list[str] = , diff_ref: str = '') -> None ``` **Fields** | Field | Type | Required | | --------------- | ----------- | -------- | | `status` | `str` | yes | | `summary` | `str` | yes | | `files_changed` | `list[str]` | no | | `commands_run` | `list[str]` | no | | `verification` | `list[str]` | no | | `diff_ref` | `str` | no | ### `ArtifactManager` Builds and renders the terminal artifact from `TaskState` (§14). ```python theme={null} ArtifactManager() ``` #### `ArtifactManager.build(self, state: TaskState, ws: Workspace) -> Artifact` Assemble the artifact for a terminal `state`; `status` mirrors `outcome`. Args: state: The terminal task state to report from. ws: The workspace, queried for the deliverable diff. Returns: The assembled `Artifact`. #### `ArtifactManager.render(self, artifact: Artifact) -> str` Render the artifact as the plain-text summary block (§14). Args: artifact: The artifact to render. Returns: The plain-text summary block. # avatar.bus Source: https://codexceed.mintlify.app/api-reference/bus EventBus — bounded, non-blocking fan-out + one privileged journal (ADR-0001, Phase 3.1). Stamps the global order (`event_id`) on every event, appends it losslessly to the privileged `JsonlEventJournal` (if wired), then fans it out to each subscriber's **bounded** queue. The bound is *soft*: only droppable `*_update` events are shed when a subscriber is at its cap; lifecycle/control events are always enqueued (they may exceed the cap). A slow or broken subscriber can therefore never stall publishing or its peers, and a subscriber's drops show up as `event_id` gaps — never silent reordering. The journal stays lossless regardless of what any subscriber sheds. This was the foundation's simple unbounded fan-out (lived in `session.py`); Lane 1 grew it into its own module behind the same `events()` / `EventSink` API. ## Classes ### `EventBus` Stamps, journals, and **fans events to every subscriber's bounded queue** (§13). Each `subscribe()` gets its own queue, so independent observers — a TUI, a telemetry exporter, a benchmark collector — each see the same stream rather than competing for one. Fan-out is non-blocking: a full subscriber sheds droppable updates instead of blocking the publisher. A late subscriber sees only events published after it subscribed; the `journal` (and the in-memory `history`) is the lossless record. Args: session\_id: Stamped on every event so a stream/journal groups back to its run. journal: The privileged lossless sink; every published event is appended to it in order before fan-out. `None` disables on-disk journaling (the foundation behavior — in-memory `history` is still kept). ```python theme={null} EventBus(session_id: str, *, journal: JsonlEventJournal | None = None) -> None ``` #### `EventBus.close(self) -> None` Signal end-of-stream to every subscriber and close the journal (idempotent). #### `EventBus.emit(self, draft: Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]) -> Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]` Awaitable publish — the frozen async interface (delegates to `publish_nowait`). Args: draft: The event to publish. Returns: The stamped event. #### `EventBus.publish_nowait(self, draft: Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]) -> Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]` Stamp `draft`, journal it losslessly, and fan it out (never blocks, §13). Args: draft: The event to publish; mutated in place with the ordering keys. Returns: The stamped event. #### `EventBus.subscribe(self, *, max_queue: int | None = None) -> 'Queue'` Register an independent consumer and return its bounded queue. Args: max\_queue: The soft cap for this consumer (droppable updates are shed above it). Defaults to a generous value that never drops a normal-size stream. Returns: A queue that will receive every event published from now on (subject to the drop policy for `*_update` events), then a `None` close sentinel. An already-closed bus returns a queue holding only the sentinel. # avatar.cli Source: https://codexceed.mintlify.app/api-reference/cli CLI entry point — the batch shell. `main()` drives the real loop via `run_agent`. The CLI stays a thin shell over the loop — wiring components and event subscribers, nothing more. It is deliberately **TUI-free**: the harness is an independent core under many consumers, and the interactive cockpit ships as a separate `jo-cli` package (the `jo` command) so the import direction stays strictly consumer → core. ## Functions ### `main(argv: list[str] | None = None, *, config: HarnessConfig | None = None, model_client: ModelClient | None = None, task_kind: Literal['edit', 'investigate', 'test_only'] = 'investigate') -> int` CLI entry point: parse args, wire the loop, run the task, render the artifact. Args: argv: Argument vector; falls back to `sys.argv` when omitted. config: Harness config; constructed from the environment when omitted. model\_client: Model client; a default `OpenAIModelClient` if omitted (injectable for tests). task\_kind: The verification contract to apply (`investigate` / `edit` / `test_only`). Returns: Process exit code: `0` on `success`, `2` on a dirty workspace, `1` otherwise. ### `run_agent(task: str, *, config: HarnessConfig, emitter: Emitter, model_client: ModelClient | None = None, allow_dirty: bool = False, task_kind: Literal['edit', 'investigate', 'test_only'] = 'investigate') -> TaskState` Run the agent loop over `task`. Args: task: The natural-language task to run. config: Harness config wiring the loop. emitter: Sink for observation events. model\_client: Model client; a default `OpenAIModelClient` if omitted. allow\_dirty: When `True`, open the workspace despite uncommitted tracked changes (§15). task\_kind: The verification contract to apply (`investigate` / `edit` / `test_only`). Returns: The terminal `TaskState` after the loop settles. # avatar.config Source: https://codexceed.mintlify.app/api-reference/config Harness configuration. Loaded from defaults, then overridden by environment variables (prefix `AVATAR_`) or a local `.env`. This is the single, explicit config object threaded through `RunDeps` (§8) — never read from globals. ## Classes ### `HarnessConfig` The single, explicit run configuration: budgets, session, workspace, endpoint (§8). ```python theme={null} HarnessConfig(_case_sensitive: 'bool | None' = None, _nested_model_default_partial_update: 'bool | None' = None, _env_prefix: 'str | None' = None, _env_prefix_target: 'EnvPrefixTarget | None' = None, _env_file: 'DotenvType | None' = PosixPath('.'), _env_file_encoding: 'str | None' = None, _env_ignore_empty: 'bool | None' = None, _env_nested_delimiter: 'str | None' = None, _env_nested_max_split: 'int | None' = None, _env_parse_none_str: 'str | None' = None, _env_parse_enums: 'bool | None' = None, _cli_prog_name: 'str | None' = None, _cli_parse_args: 'bool | list[str] | tuple[str, ...] | None' = None, _cli_settings_source: 'CliSettingsSource[Any] | None' = None, _cli_parse_none_str: 'str | None' = None, _cli_hide_none_type: 'bool | None' = None, _cli_avoid_json: 'bool | None' = None, _cli_enforce_required: 'bool | None' = None, _cli_use_class_docs_for_groups: 'bool | None' = None, _cli_exit_on_error: 'bool | None' = None, _cli_prefix: 'str | None' = None, _cli_flag_prefix_char: 'str | None' = None, _cli_implicit_flags: "bool | Literal['dual', 'toggle'] | None" = None, _cli_ignore_unknown_args: 'bool | None' = None, _cli_kebab_case: "bool | Literal['all', 'no_enums'] | None" = None, _cli_shortcuts: 'Mapping[str, str | list[str]] | None' = None, _secrets_dir: 'PathType | None' = None, _build_sources: 'tuple[tuple[PydanticBaseSettingsSource, ...], dict[str, Any]] | None' = None, *, max_iterations: int = 50, max_wall_clock_seconds: int | None = 1800, max_consecutive_failures: int = 5, max_repair_attempts: int = 3, max_context_tokens: int = 100000, max_declaration_nudges: int = 3, approval_timeout_seconds: float | None = None, autonomous_amendment_policy: Literal['deny', 'approve'] = 'deny', autonomous_escalation_policy: Literal['deny', 'approve'] = 'deny', escalation_thrash_repeats: Annotated[int, Ge(ge=1)] = 3, test_command: str = '', lint_command: str = '', command_timeout_seconds: int = 120, request_timeout_seconds: Annotated[float, Gt(gt=0)] = 0, request_idle_timeout_seconds: Annotated[float, Gt(gt=0)] = 0, stream_model_calls: bool = True, transport_max_retries: Annotated[int, Ge(ge=0)] = 2, command_output_budget: Annotated[int, Ge(ge=256)] = 16000, planner_model: str | None = None, interactive: bool = True, log_path: str | None = None, workspace_root: str = '.', sandbox_mode: Literal['none', 'hermetic-env', 'sandbox-exec', 'bwrap', 'container'] = 'hermetic-env', sandbox_allow_network: bool = False, sandbox_rlimits: bool = False, sandbox_image: str = '', sandbox_container_runtime: Literal['podman', 'docker'] = 'podman', sensitive_path_globs: list[str] = , model: str = 'openai/gpt-4o-mini', base_url: str = 'https://ai/api/v1', api_key: str | None = None, native_tool_calls: bool = True, temperature: float = 0, context_max_detail_chars: int = 16000, context_detail_char_budget: int = 48000, context_verifier_pin_count: int = 2, classifier_model: str | None = 'openai/gpt-5-nano') -> None ``` **Fields** | Field | Type | Required | | | ------------------------------ | ----------------------------------------------------------------------- | -------- | -- | | `max_iterations` | `int` | no | | | `max_wall_clock_seconds` | \`int | None\` | no | | `max_consecutive_failures` | `int` | no | | | `max_repair_attempts` | `int` | no | | | `max_context_tokens` | `int` | no | | | `max_declaration_nudges` | `int` | no | | | `approval_timeout_seconds` | \`float | None\` | no | | `autonomous_amendment_policy` | `Literal['deny', 'approve']` | no | | | `autonomous_escalation_policy` | `Literal['deny', 'approve']` | no | | | `escalation_thrash_repeats` | `int` | no | | | `test_command` | `str` | no | | | `lint_command` | `str` | no | | | `command_timeout_seconds` | `int` | no | | | `request_timeout_seconds` | `float` | no | | | `request_idle_timeout_seconds` | `float` | no | | | `stream_model_calls` | `bool` | no | | | `transport_max_retries` | `int` | no | | | `command_output_budget` | `int` | no | | | `planner_model` | \`str | None\` | no | | `interactive` | `bool` | no | | | `log_path` | \`str | None\` | no | | `workspace_root` | `str` | no | | | `sandbox_mode` | `Literal['none', 'hermetic-env', 'sandbox-exec', 'bwrap', 'container']` | no | | | `sandbox_allow_network` | `bool` | no | | | `sandbox_rlimits` | `bool` | no | | | `sandbox_image` | `str` | no | | | `sandbox_container_runtime` | `Literal['podman', 'docker']` | no | | | `sensitive_path_globs` | `list[str]` | no | | | `model` | `str` | no | | | `base_url` | `str` | no | | | `api_key` | \`str | None\` | no | | `native_tool_calls` | `bool` | no | | | `temperature` | `float` | no | | | `context_max_detail_chars` | `int` | no | | | `context_detail_char_budget` | `int` | no | | | `context_verifier_pin_count` | `int` | no | | | `classifier_model` | \`str | None\` | no | # avatar.context Source: https://codexceed.mintlify.app/api-reference/context ContextBuilder — the compact per-turn working packet (§9). Assembles only what the model needs *this* turn: goal, phase, recent evidence (summaries, most-recent-first-budgeted), and the tools allowed for the current phase. The model discovers the repo incrementally through tools; it never receives the whole repository by default. ## Classes ### `ContextBuilder` Builds the per-turn `ContextPacket` from `TaskState` under a fixed budget (§9). Evidence is compacted *less-lossily* (Phase 2.5): recent items keep full detail until a char budget is spent, older items degrade to their summary line (not dropped), the last N verifier outputs are pinned verbatim, and adjacent duplicates collapse to one `... (xN)` line. Action history is cheap (one line each) so it is kept on a longer horizon than evidence. Args: detail\_char\_budget: Total chars of evidence *detail* shown verbatim before older items degrade to summary-only. max\_detail\_chars: Per-item detail truncation budget. max\_evidence\_lines: Hard cap on rendered evidence lines (newest kept). max\_actions: Max prior-action lines to surface. verifier\_pin\_count: How many recent verifier outputs keep detail verbatim regardless of budget — a repair loop needs its recent failure history, not just the latest verdict. ```python theme={null} ContextBuilder(detail_char_budget: int = 48000, max_detail_chars: int = 16000, max_evidence_lines: int = 40, max_actions: int = 25, verifier_pin_count: int = 2) -> None ``` #### `ContextBuilder.build(self, state: TaskState, ws: Workspace, registry: ToolRegistry) -> ContextPacket` Assemble the working packet for the current turn from `state` (§9). Args: state: The task state, source of truth for the packet. ws: The run-scoped `Workspace` handle. registry: The `ToolRegistry`, for phase-active tools. Returns: The `ContextPacket` for this turn. ### `ContextPacket` The compact, per-turn working set assembled for one model decision (§9). ```python theme={null} ContextPacket(*, goal: str, constraints: list[str] = , conversation: list[ConversationTurn] = , phase: str, task_kind: str = 'investigate', plan: list[str] = , files_read: list[str] = , files_modified: list[str] = , prior_actions: list[str] = , recent_evidence: list[str] = , allowed_tools: list[ToolSummary] = , latest_error: str | None = None, has_uncommitted_changes: bool = False) -> None ``` **Fields** | Field | Type | Required | | | ------------------------- | ------------------------ | -------- | -- | | `goal` | `str` | yes | | | `constraints` | `list[str]` | no | | | `conversation` | `list[ConversationTurn]` | no | | | `phase` | `str` | yes | | | `task_kind` | `str` | no | | | `plan` | `list[str]` | no | | | `files_read` | `list[str]` | no | | | `files_modified` | `list[str]` | no | | | `prior_actions` | `list[str]` | no | | | `recent_evidence` | `list[str]` | no | | | `allowed_tools` | `list[ToolSummary]` | no | | | `latest_error` | \`str | None\` | no | | `has_uncommitted_changes` | `bool` | no | | ### `ToolSummary` A tool's name, description, and input schema, as shown to the model. ```python theme={null} ToolSummary(*, name: str, description: str, input_schema: dict = ) -> None ``` **Fields** | Field | Type | Required | | -------------- | ------ | -------- | | `name` | `str` | yes | | `description` | `str` | yes | | `input_schema` | `dict` | no | # avatar.deps Source: https://codexceed.mintlify.app/api-reference/deps Run-scoped dependencies handed to tools and the runtime (§8). Passed explicitly — never via globals — so a run stays self-contained and replayable. ## Classes ### `CancellationToken` Trips to abort an in-flight tool (per-tool timeout, user interrupt — §23). `cancelled` is the source of truth (polled at loop checkpoints). `event()` exposes the same trip as an `asyncio.Event` so the runner can *race* a cancel against an in-flight model call (ADR-0029 R5) — true mid-call abort, not just between-turn polling. The Event is created lazily inside the running loop, so a no-loop construction (and `CancellationToken( cancelled=True)` in tests) still works. ```python theme={null} CancellationToken(cancelled: bool = False) -> None ``` #### `CancellationToken.cancel(self) -> None` Trip the token so an in-flight tool or model call can abort at its next checkpoint. #### `CancellationToken.event(self) -> Event` Return the loop-bound cancel Event, creating it on first call (must be inside a loop). Returns: An `asyncio.Event` set iff the token is cancelled — already set if `cancel()` ran first. ### `RunDeps` The run-scoped dependencies handed explicitly to tools and the runtime (§8). `verification_plan` mirrors the plan the runner froze onto `TaskState` (ADR-0007), so `run_tests`/`run_linter` can ride the resolved contract when no config override exists — the model exercises the same rubric the verifier grades. ```python theme={null} RunDeps(workspace: Workspace, config: HarnessConfig, cancellation: CancellationToken, verification_plan: list[PlannedCheck] | None = None, declared_contract: list[PlannedCheck] | None = None, declared_change_kinds: list[str] | None = None) -> None ``` # avatar.event_types Source: https://codexceed.mintlify.app/api-reference/event_types Typed lifecycle events — the `HarnessEvent` discriminated union (ADR-0001/0002, Phase 3.0). The closed, versioned union the engine *emits* and the cockpit *renders*. Unlike the sync `Emitter`'s raw dicts (`events.py`, kept for back-compat + the CLI), these are exhaustively matchable and carry a `schema_version` + global `event_id`, so the journal can round-trip them verbatim and a renderer can switch on `type` safely. `event_id` / `session_id` / `ts` are *stamped by the bus at publish* (see `session.EventBus`); a freshly built event carries placeholder defaults until then. ## Classes ### `AgentEnd` A run has settled to a terminal `outcome` (the stream's end). ```python theme={null} AgentEnd(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['agent_end'] = 'agent_end', outcome: str | None = None) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ---------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['agent_end']` | no | | | `outcome` | \`str | None\` | no | ### `AgentStart` A run has begun on `goal`. `task_kind` is the resolved kind the run opens with; `mode_source` records how it was decided ("override"/"classifier"/"heuristic", or `None` when the core was driven directly) so a dogfood journal can distinguish a classifier miss from a classifier outage. ```python theme={null} AgentStart(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['agent_start'] = 'agent_start', goal: str = '', task_kind: str = '', mode_source: str | None = None) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ------------------------ | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['agent_start']` | no | | | `goal` | `str` | no | | | `task_kind` | `str` | no | | | `mode_source` | \`str | None\` | no | ### `ApprovalController` The awaited control hook the runner consults for a gated (tier-3 `ask`) call. The controller *announces* the need (an `ApprovalRequested` event) and then blocks this run only until a control method resolves it — the §13 two-plane boundary. ```python theme={null} ApprovalController(*args, **kwargs) ``` #### `ApprovalController.request_approval(self, approval_id: str, tool: str, reason: str, tool_input: dict) -> bool` Announce the gated call and block this run until a control method resolves it. Args: approval\_id: Correlates the announcement with its resolution. tool: The tool name awaiting approval. reason: The gate's reason, shown to the human. tool\_input: The proposed call arguments. Returns: True iff the call was allowed. ### `ApprovalRequested` A gated (tier-3 `ask`) call awaits a human decision — announce only (§13). ```python theme={null} ApprovalRequested(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['approval_requested'] = 'approval_requested', approval_id: str, tool: str, reason: str = '', input: dict = ) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ------------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['approval_requested']` | no | | | `approval_id` | `str` | yes | | | `tool` | `str` | yes | | | `reason` | `str` | no | | | `input` | `dict` | no | | ### `ApprovalResolved` A pending approval (`approval_id`) was decided via the control plane. `via` records who decided: `"human"` answered a prompt, `"grant"` was auto-allowed by a session-scoped `ApprovalGrant` with no prompt, `"auto"` was auto-denied by an unattended/batch run's deny-by-default disposition or the approval-timeout backstop (no human present). All are observable (invariant #5). ```python theme={null} ApprovalResolved(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['approval_resolved'] = 'approval_resolved', approval_id: str, allowed: bool, via: Literal['human', 'grant', 'auto'] = 'human') -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ----------------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['approval_resolved']` | no | | | `approval_id` | `str` | yes | | | `allowed` | `bool` | yes | | | `via` | `Literal['human', 'grant', 'auto']` | no | | ### `CancellationObserved` The loop observed a tripped cancellation token and is stopping (§8). ```python theme={null} CancellationObserved(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['cancellation_observed'] = 'cancellation_observed', reason: str = '') -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ---------------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['cancellation_observed']` | no | | | `reason` | `str` | no | | ### `DecisionError` A malformed model reply — either recovered by an in-client retry or a lost turn (§6). Closes the observability gap where a failed decision attempt (e.g. a truncated `str_replace` emission) left no trace: every malformed attempt is journaled with its error and a capped excerpt of the raw reply, so a struggling run is legible live and debuggable after the fact (invariant #5). ```python theme={null} DecisionError(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['decision_error'] = 'decision_error', error: str = '', raw: str = '', recovered: bool = True) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | --------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['decision_error']` | no | | | `error` | `str` | no | | | `raw` | `str` | no | | | `recovered` | `bool` | no | | ### `DeclarationRequired` A greenfield edit was refused pending a declared verification contract (ADR-0038). Emitted at the investigating → editing boundary when the task is greenfield (no detected/cited/configured contract) and the model tried to edit without first calling `declare_verification`. `nudge`/`max_nudges` track the bounded-nudge budget: at the cap the runner stops refusing and falls back to the smoke floor. ```python theme={null} DeclarationRequired(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['declaration_required'] = 'declaration_required', nudge: int = 0, max_nudges: int = 0) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | --------------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['declaration_required']` | no | | | `nudge` | `int` | no | | | `max_nudges` | `int` | no | | ### `EventBase` Fields common to every lifecycle event — the journal's ordering/versioning keys. The `type` discriminator is declared on each concrete event (not here): it is per-event by nature, and a shared mutable base field can't be narrowed to a `Literal` soundly. `HarnessEvent` is the discriminated union over the concretes. ```python theme={null} EventBase(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = ) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ------------ | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | ### `EventSink` Where the runner publishes typed events (the bus stamps `event_id`/`session_id`/`ts`). Foundation publishing is fire-and-forget via `publish_nowait` onto an unbounded queue; the awaited `emit` is the frozen async interface lane 1 fills in with bounded, backpressured fan-out. Both must keep `event_id` monotonic. ```python theme={null} EventSink(*args, **kwargs) ``` #### `EventSink.emit(self, draft: 'HarnessEvent') -> 'HarnessEvent'` Awaitable publish — the interface lane 1 fills in with backpressure. Args: draft: The event to publish. Returns: The stamped event. #### `EventSink.publish_nowait(self, draft: 'HarnessEvent') -> 'HarnessEvent'` Stamp and enqueue `draft` without blocking. Args: draft: The event to publish. Returns: The stamped event. ### `ModelDecisionEvent` The model chose an action this turn (thought + a one-line brief). ```python theme={null} ModelDecisionEvent(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['model_decision'] = 'model_decision', thought: str = '', action_type: str = '', action: str = '', transport: str = '') -> None ``` **Fields** | Field | Type | Required | | | ---------------- | --------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['model_decision']` | no | | | `thought` | `str` | no | | | `action_type` | `str` | no | | | `action` | `str` | no | | | `transport` | `str` | no | | ### `ModelUpdate` A streamed model-output delta — display only; never private chain-of-thought (ADR-0001 D6). ```python theme={null} ModelUpdate(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['model_update'] = 'model_update', delta: str = '', channel: Literal['display'] = 'display') -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['model_update']` | no | | | `delta` | `str` | no | | | `channel` | `Literal['display']` | no | | ### `ModelUsage` Provider-reported token usage for one turn (in-client retries summed). The journal's per-turn cost record — the eval harness (ADR-0004) sums these for tokens/\$ per solved task; without them cost is unmeasurable (invariant #5). ```python theme={null} ModelUsage(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['model_usage'] = 'model_usage', prompt_tokens: int = 0, completion_tokens: int = 0) -> None ``` **Fields** | Field | Type | Required | | | ------------------- | ------------------------ | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['model_usage']` | no | | | `prompt_tokens` | `int` | no | | | `completion_tokens` | `int` | no | | ### `PhaseChanged` The control phase advanced from `old` to `new` (§7). ```python theme={null} PhaseChanged(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['phase_changed'] = 'phase_changed', old: str, new: str) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | -------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['phase_changed']` | no | | | `old` | `str` | yes | | | `new` | `str` | yes | | ### `TaskEscalated` The task was escalated `investigate → edit` mid-run (ADR-0048). Emitted when a consented `switch_to_editing` (model-requested, or a thrash-nudged request) flips the task **kind only** — deliberately not the phase, and not the frozen plan. The task becomes a normal edit task still sitting in `investigating`, so the standard edit-intent bootstrap runs the declaration gate and advances the phase on the next edit: escalation never *jumps* that gate. `trigger` records what caused it (`model` = the model asked unprompted; `thrash` = the harness's thrash detector nudged it there). The transition is one-directional and once-only, so this only ever reads `investigate → edit`. ```python theme={null} TaskEscalated(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['task_escalated'] = 'task_escalated', from_kind: str = 'investigate', to_kind: str = 'edit', trigger: str = 'model') -> None ``` **Fields** | Field | Type | Required | | | ---------------- | --------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['task_escalated']` | no | | | `from_kind` | `str` | no | | | `to_kind` | `str` | no | | | `trigger` | `str` | no | | ### `ToolEnd` A tool call finished — `success` plus its summary/content (§10). ```python theme={null} ToolEnd(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['tool_end'] = 'tool_end', call_id: str = '', tool: str, success: bool, summary: str = '', content: str = '') -> None ``` **Fields** | Field | Type | Required | | | ---------------- | --------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['tool_end']` | no | | | `call_id` | `str` | no | | | `tool` | `str` | yes | | | `success` | `bool` | yes | | | `summary` | `str` | no | | | `content` | `str` | no | | ### `ToolStart` A tool call (`call_id`) is about to execute. ```python theme={null} ToolStart(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['tool_start'] = 'tool_start', call_id: str = '', tool: str, input: dict = ) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ----------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['tool_start']` | no | | | `call_id` | `str` | no | | | `tool` | `str` | yes | | | `input` | `dict` | no | | ### `TurnEnd` The current loop iteration has finished. ```python theme={null} TurnEnd(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['turn_end'] = 'turn_end') -> None ``` **Fields** | Field | Type | Required | | | ---------------- | --------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['turn_end']` | no | | ### `TurnStart` A new loop iteration (`iteration`) has begun. ```python theme={null} TurnStart(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['turn_start'] = 'turn_start', iteration: int = 0) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ----------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['turn_start']` | no | | | `iteration` | `int` | no | | ### `VerificationEnd` The verifier returned a verdict — `passed` plus its summary (§12). ```python theme={null} VerificationEnd(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['verification_end'] = 'verification_end', passed: bool, summary: str = '') -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ----------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['verification_end']` | no | | | `passed` | `bool` | yes | | | `summary` | `str` | no | | ### `VerificationPlanFrozen` The per-session verification plan was resolved and frozen (ADR-0007). Journaled at the investigating → editing boundary, before any verification: each check carries its command and provenance, so every run's rubric — and where each check came from — is auditable. An empty `checks` records that nothing was discovered (the verifier will fail legibly). `change_kinds` records the kinds a model-declared contract covers (ADR-0044) — the model's stated intent, auditable against the diff; `None` = no declaration (tiers 1-3), including journals written before the field existed. ```python theme={null} VerificationPlanFrozen(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['verification_plan_frozen'] = 'verification_plan_frozen', checks: list[PlannedCheck] = , change_kinds: list[str] | None = None) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ------------------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['verification_plan_frozen']` | no | | | `checks` | `list[PlannedCheck]` | no | | | `change_kinds` | \`list\[str] | None\` | no | ### `VerificationStart` The harness-owned verifier has begun (§12). ```python theme={null} VerificationStart(*, schema_version: Literal[1] = 1, event_id: int = 0, session_id: str = '', task_id: str | None = None, turn: int | None = None, ts: datetime = , type: Literal['verification_start'] = 'verification_start') -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ------------------------------- | -------- | -- | | `schema_version` | `Literal[1]` | no | | | `event_id` | `int` | no | | | `session_id` | `str` | no | | | `task_id` | \`str | None\` | no | | `turn` | \`int | None\` | no | | `ts` | `datetime` | no | | | `type` | `Literal['verification_start']` | no | | ## Functions ### `dump_event(event: Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]) -> str` Serialize an event to one JSON line for the journal. Args: event: The event to serialize. Returns: Its compact JSON representation. ### `load_events(path: Path) -> list[Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]]` Reload a JSONL journal back into typed events, in file order. Args: path: The JSONL journal written via `dump_event` / a typed `EventLog`. Returns: The events, one per non-blank line, validated through the union. ### `parse_event(data: dict | str | bytes) -> Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]` Validate a dict / JSON string into the right `HarnessEvent` variant. Raises `pydantic.ValidationError` if `type` is unknown (the union is closed) or fields are invalid. Args: data: A mapping or JSON text carrying a known `type` discriminator. Returns: The validated event. # avatar.eventlog Source: https://codexceed.mintlify.app/api-reference/eventlog EventLog — a JSONL subscriber to the emitter (§13). Writes one JSON object per event, append-only, for replay, debugging, audit, and eval data. It is a plain subscriber: it observes, never influences. ## Classes ### `EventLog` An append-only JSONL subscriber: one timestamped record per event (§13). Accepts both the sync `Emitter`'s raw-dict events and typed `HarnessEvent` models (Phase 3.0) — a typed event is serialized via `model_dump_json`, so the same log can be reloaded by `event_types.load_events`. Args: path: JSONL file the events are appended to. ```python theme={null} EventLog(path: Path) -> None ``` # avatar.events Source: https://codexceed.mintlify.app/api-reference/events Lifecycle event emitter — observation only (§13). Synchronous, fire-and-forget. Subscribers react to what happened; they can *never* block or redirect the loop. `emit` returns `None` by design: there is no value a caller could branch on. This is the deliberate, narrow line that keeps control out of the emitter — permission and context are awaited control hooks elsewhere, not subscribers here. ## Classes ### `Emitter` Fans lifecycle events out to observation-only subscribers (§13). Args: session\_id: Stamped on every emitted event to group a run's events. A run is one process invocation; in a future REPL it is the long-lived process, so grouping is intentional rather than incidental. `None` omits the key. ```python theme={null} Emitter(session_id: str | None = None) -> None ``` #### `Emitter.emit(self, event_type: str, **payload: object) -> None` Build the event and deliver it to every subscriber, isolating failures. Args: event\_type: The event's `type` tag. \*\*payload: Extra event fields merged after `type` and `ts`. #### `Emitter.subscribe(self, subscriber: Callable[[dict[str, object]], None]) -> None` Register a subscriber to receive every subsequent event. Args: subscriber: Callable invoked with each emitted event. # avatar.harness Source: https://codexceed.mintlify.app/api-reference/harness Harness — the importable facade over the agent loop (§20, Phase 2.6). `Harness` performs the default collaborator wiring the CLI used to hardcode in `run_agent`: the tool registry, model client, context builder, verifier, and permission policy. Every seam is overridable via a constructor kwarg, so a downstream user can swap a provider, a tool set, the verification contract, or the permission gate without touching the core. `from_env` builds a `HarnessConfig` from the environment (the same `AVATAR_*` settings the CLI uses) and applies the same defaults. The facade only *assembles* the run; the `AgentRunner` still owns all state mutation (invariant #2). `run` returns the terminal `TaskState`, identical to what `cli.run_agent` returns, so the CLI can delegate straight through. ## Classes ### `Harness` The importable facade: default wiring with every collaborator overridable. Each collaborator defaults to the standard MVP wiring (the same the CLI used to construct inline) and is replaceable via its kwarg — the Principle-A seam. The facade constructs nothing on the run's behalf that the runner should own; it only assembles the collaborators. Args: config: Harness config (budgets, workspace root, commands, denylist). model: Model client; a default `OpenAIModelClient(config)` if omitted. tools: The active `ToolRegistry`; `default_registry()` if omitted. verifier: The completion verifier; `Verifier(config)` if omitted. policy: The before-tool-call permission gate; the standard tier policy (threaded with `config.sensitive_path_globs`) if omitted. context\_builder: The per-turn context assembler; if omitted, a `ContextBuilder` budgeted from the config's `AVATAR_CONTEXT_*` compaction fields. emitter: The observation-only event emitter; a fresh `Emitter()` if omitted. ```python theme={null} Harness(*, config: HarnessConfig, model: ModelClient | None = None, tools: ToolRegistry | None = None, verifier: Verifier | None = None, policy: PermissionPolicy | None = None, context_builder: ContextBuilder | None = None, emitter: Emitter | None = None) -> None ``` #### `Harness.arun(self, task: str, *, task_kind: Literal['edit', 'investigate', 'test_only'] = 'investigate', allow_dirty: bool = False) -> TaskState` Async twin of `run` — the bare loop, for callers already on an event loop. Use `session` instead when you also want the typed event stream + approval/cancel controls; `arun` returns only the terminal state. Args: task: The natural-language task to run. task\_kind: The verification contract to apply (`investigate` / `edit` / `test_only`). allow\_dirty: When `True`, open the workspace despite uncommitted tracked changes (§15). Returns: The terminal `TaskState` after the loop settles. #### `Harness.run(self, task: str, *, task_kind: Literal['edit', 'investigate', 'test_only'] = 'investigate', allow_dirty: bool = False) -> TaskState` Run the agent loop over `task` synchronously and return the terminal `TaskState`. The batch/library entry point; for an interactive UI use `session` (observation + control) or `arun` (the bare async loop). Args: task: The natural-language task to run. task\_kind: The verification contract to apply (`investigate` / `edit` / `test_only`). allow\_dirty: When `True`, open the workspace despite uncommitted tracked changes (§15). Returns: The terminal `TaskState` after the loop settles. #### `Harness.session(self, task: str, *, task_kind: Literal['edit', 'investigate', 'test_only'] = 'investigate', allow_dirty: bool = False, conversational: bool = False, advisory: bool = False, journal: JsonlEventJournal | None = None, unattended: bool = False) -> Session` Open an interactive `Session` over `task` — the two-plane SDK surface (§13, §23). Returns a not-yet-started session: drive it with `await session.run()` while consuming `session.events()` (observation) and calling `session.resolve_approval()` / `session.cancel()` (control). This is the shape a TUI or autonomous wrapper binds to. Args: task: The natural-language task to run. task\_kind: The verification contract to apply (`investigate` / `edit` / `test_only`). allow\_dirty: When `True`, open the workspace despite uncommitted tracked changes (§15). conversational: Terminal-boundary authority (§23.5, ADR-0046). The verifier steers in either mode; `False` (default) pronounces repair exhaustion `failed`, `True` (the interactive `ReplSession` default) defers to the human (`blocked`) at exhaustion. A failed verdict is never laundered to `success`. advisory: External-grading authority (ADR-0040 option A). `True` runs the verifier as advisory — it reports but does not steer or gate, and a failed verdict is delivered as `success` for a held-out grader (the eval success-probe) to judge. For callers that grade externally (the eval harness); never the REPL. Precedes `conversational`. journal: The write-ahead `JsonlEventJournal` for the run's typed events; `None` (default) keeps the stream in memory only. unattended: When `True`, the session auto-denies any tier-3/denylist `ask` instead of awaiting a human — the right disposition for a batch/eval/autonomous run, where blocking on a `resolve_approval` that never comes would deadlock the loop. `False` (default) keeps the interactive path for a TUI/human at a REPL. Returns: A `Session` wrapping the run — observation out, control in. # avatar.intent Source: https://codexceed.mintlify.app/api-reference/intent ModeClassifier — LLM mode routing, visible and correctable (revises ADR-0002 D3). The first-word heuristic misrouted a conversational follow-up ("Now make the UI richer…" → `investigate`) and the run burned its whole budget structurally unable to edit (dogfood `events/04849a5a…jsonl`). D3's objection to a classifier was *hiddenness*, not LLM-ness — so this one is: a one-shot, schema-constrained call on a cheap dedicated model (`AVATAR_CLASSIFIER_MODEL`), whose verdict the cockpit displays and `/mode` overrides. Any failure returns `None` and the caller falls back to the heuristic — classification can degrade, never block. ## Classes ### `ModeClassifier` Classifies a goal into a `task_kind` with one cheap, schema-constrained call. Args: config: Harness config; `classifier_model` names the (cheap) model to use and `base_url`/`api_key` are shared with the main endpoint. client: An injected OpenAI-compatible client, or `None` to build one lazily on first use (mirrors `OpenAIModelClient` — credentials are call-time only). ```python theme={null} ModeClassifier(config: HarnessConfig, client: Any = None) -> None ``` #### `ModeClassifier.classify(self, prompt: str, history: Sequence[str] = ()) -> str | None` Classify `prompt` (in conversation context) into a task kind, or `None`. `None` means "no usable verdict" — endpoint error, junk reply, unknown kind — and the caller falls back to the heuristic. The classifier can degrade the routing quality, never the goal itself. Args: prompt: The user's new request. history: Recent conversation lines (newest last) for context. Returns: One of `edit`/`investigate`/`test_only`, or `None` when unusable. # avatar.journal Source: https://codexceed.mintlify.app/api-reference/journal JsonlEventJournal — the privileged, lossless write-ahead sink (ADR-0001, Phase 3.1). Unlike an observation subscriber (bounded, droppable), the journal is *part of the engine*: every published event is appended in global order and **flushed per event**, so a crash mid-run leaves a durable, replayable prefix. It is the substrate 3.3's semantics-aware resume will replay (`load_events`); Lane 1 builds the record, not the replay engine. It keeps the file handle open for the run (append mode) and flushes on every write — the write-ahead property — rather than reopening per event like the sync-`Emitter` `EventLog`. The two are deliberately distinct: `EventLog` is an emitter *subscriber*; this is the engine's durable commit log for the typed `HarnessEvent` stream. ## Classes ### `JsonlEventJournal` An append-only, per-event-flushed JSONL record of the typed event stream. Args: path: JSONL file the events are appended to (parent dirs are created). ```python theme={null} JsonlEventJournal(path: Path) -> None ``` #### `JsonlEventJournal.append(self, event: Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]) -> None` Append `event` as one JSON line and flush it to disk (write-ahead). Args: event: The stamped event to persist (must carry its ordering keys). #### `JsonlEventJournal.close(self) -> None` Close the journal file handle (idempotent). ## Functions ### `resolve_log_path(arg: str | None, session_id: str) -> Path` Pick the event-log path: an explicit override, else the managed per-session layout. The default `events/<session_id>.jsonl` makes one session one file — grouping is physical and the filename is self-identifying — instead of appending every run to a shared static log that must be filtered apart. Shared by every shell that journals a sitting (the batch CLI and `jo-cli` alike), so the layout cannot drift. Args: arg: An explicit `--log` value, or `None` to use the per-session default. session\_id: This run's id, used to name the default log. Returns: The resolved log path. ### `update_latest_pointer(log_path: Path) -> None` Point `latest.jsonl` at this run's log so the newest session is always reachable. Best-effort: a platform without symlink support (or a permission error) just leaves the per-session log — the pointer is a convenience, not the source of truth. Args: log\_path: This run's per-session log file. # avatar.model_client Source: https://codexceed.mintlify.app/api-reference/model_client Model decision protocol: constrained, validated decisions (§6). The model returns one of three actions, never arbitrary prose. The harness validates every decision before acting; a malformed decision is a *recoverable* error fed back to the model, never executed and never fatal. `parse_decision` is the pure validation boundary (no network), so it — and the fakes that stand in for a real client in tests — are trivially testable. ## Classes ### `AskUser` Decision to ask the user a question (blocks in a non-interactive run) (§6). ```python theme={null} AskUser(*, type: Literal['ask_user'] = 'ask_user', question: str) -> None ``` **Fields** | Field | Type | Required | | ---------- | --------------------- | -------- | | `type` | `Literal['ask_user']` | no | | `question` | `str` | yes | ### `DecisionParseError` Malformed model output — recoverable; fed back to the model (§6), never fatal. Carries `usage` when the client exhausts its in-client retries, so a lost turn is still billed — the expensive failure mode is exactly the one that must not be undercounted (PR-#31 review). Args: message: The parse-failure description fed back to the model. usage: Tokens spent across the failed attempts, or `None` if unreported. ```python theme={null} DecisionParseError(message: str = '', usage: 'DecisionUsage | None' = None) -> None ``` ### `DecisionRetryNote` One malformed in-client attempt: what was wrong, and a capped raw excerpt. ```python theme={null} DecisionRetryNote(*, error: str, raw: str = '') -> None ``` **Fields** | Field | Type | Required | | ------- | ----- | -------- | | `error` | `str` | yes | | `raw` | `str` | no | ### `DecisionUsage` Provider-reported token usage for one decision (all in-client attempts summed). ```python theme={null} DecisionUsage(*, prompt_tokens: int = 0, completion_tokens: int = 0) -> None ``` **Fields** | Field | Type | Required | | ------------------- | ----- | -------- | | `prompt_tokens` | `int` | no | | `completion_tokens` | `int` | no | ### `EmptyResponseError` The provider returned an empty / whitespace-only / all-NUL body (a 200 with no content). The OpenAI SDK does not retry this (it is a *successful* HTTP response), so it must be caught explicitly and treated as a transport failure — not routed into the model parse-retry, which would re-prompt the model for what is really a dead/stalled provider reply (ADR-0028 R2). ```python theme={null} EmptyResponseError(message: str = '', usage: 'DecisionUsage | None' = None) -> None ``` ### `FinalAnswer` Decision claiming the task is complete — a proposal for the verifier (§6, §12). ```python theme={null} FinalAnswer(*, type: Literal['final_answer'] = 'final_answer', answer: str) -> None ``` **Fields** | Field | Type | Required | | -------- | ------------------------- | -------- | | `type` | `Literal['final_answer']` | no | | `answer` | `str` | yes | ### `ModelClient` Anything that turns a context packet into a validated decision (§6). The real implementation calls an OpenAI-compatible endpoint and runs the result through `parse_decision`; tests substitute a scripted fake. ```python theme={null} ModelClient() ``` #### `ModelClient.adecide(self, context: ContextPacket) -> ModelDecision` Async entry point for one decision (ADR-0029 R5); defaults to offloading sync `decide`. `OpenAIModelClient` overrides this with a cancellable streaming path; every fake inherits this bridge unchanged — its sync `decide` runs in a worker thread (uncancellable mid-call, but fast and deterministic, so the runner's cancel-race still resolves promptly). Args: context: The assembled context packet. Returns: The validated decision for the current turn. #### `ModelClient.decide(self, context: ContextPacket) -> ModelDecision` Turn a context packet into a validated decision for the current turn. Args: context: The assembled context packet. Returns: The validated decision for the current turn. ### `ModelDecision` One validated model decision: a thought plus exactly one action (§6). `retry_trace` is a **harness-owned diagnostics channel**: the model client annotates the decision with any malformed attempts it recovered from in-client, so the runner can record them as evidence and journal them (invariant #5). It is never accepted from raw model output — `parse_decision` clears it. ```python theme={null} ModelDecision(*, thought_summary: str = '', action: ToolCall | FinalAnswer | AskUser, retry_trace: list[DecisionRetryNote] = , transport_trace: list[str] = , streaming_fallback: str = '', usage: DecisionUsage | None = None, transport: str = '') -> None ``` **Fields** | Field | Type | Required | | | | -------------------- | ------------------------- | ----------- | --------- | --- | | `thought_summary` | `str` | no | | | | `action` | \`ToolCall | FinalAnswer | AskUser\` | yes | | `retry_trace` | `list[DecisionRetryNote]` | no | | | | `transport_trace` | `list[str]` | no | | | | `streaming_fallback` | `str` | no | | | | `usage` | \`DecisionUsage | None\` | no | | | `transport` | `str` | no | | | ### `OpenAIModelClient` Calls an OpenAI-compatible endpoint and validates the reply (§6, §18). A malformed reply is fed back to the model for a bounded number of retries before surfacing as a `DecisionParseError` (which the runner treats as a recoverable, model-correctable error). Args: config: The harness configuration. client: An injected OpenAI-compatible client, or `None` to build one lazily on first use — so construction needs no credentials; the optional `openai` extra and an API key are required only when `decide()` is first called. max\_parse\_retries: Number of retries on malformed model output. transport\_max\_retries: Transport-layer retries on a NUL/empty body or a request failure (ADR-0028 R3); `None` takes `config.transport_max_retries`. sleep: Backoff sleeper, injectable so tests exercise retries without real delay. aclient: An injected async OpenAI-compatible client (the ADR-0029 R5 streaming path), or `None` to build an `AsyncOpenAI` lazily on first `adecide()`. asleep: Async backoff sleeper, injectable so async tests skip real delay. ```python theme={null} OpenAIModelClient(config: HarnessConfig, client: Any = None, max_parse_retries: int = 2, *, transport_max_retries: int | None = None, sleep: Callable[[float], None] = , aclient: Any = None, asleep: Callable[[float], Awaitable[None]] = ) -> None ``` #### `OpenAIModelClient.adecide(self, context: ContextPacket) -> ModelDecision` Call the endpoint asynchronously, streaming by default for idle-timeout + cancellation. Streams native tool-calls when enabled (ADR-0029 R5) so a stall is caught at the idle timeout regardless of generation length; a provider that can't stream trips `_streaming_unsupported` and the SAME request is re-issued non-streaming for the rest of the session (D4). Either path is cancellable mid-call (the runner races a cancel against it) and raises `TransportError`/`DecisionParseError` exactly like the sync `decide`. Args: context: The assembled context packet. Returns: The validated decision for the current turn. #### `OpenAIModelClient.decide(self, context: ContextPacket) -> ModelDecision` Call the endpoint and validate the reply, retrying on malformed output (§6). The default transport is native provider tool-calling (ADR-0003 A) — the provider owns the call envelope, so a large patch can't die in hand-escaping; `config.native_tool_calls=False` restores the legacy single-JSON-object protocol. Either path raises `DecisionParseError` when every attempt is malformed. Args: context: The assembled context packet. Returns: The validated decision for the current turn. ### `StreamingUnsupportedError` The provider can't stream tool-calls — trip the per-instance flag and fall back (ADR-0029 D4). Deliberately NOT a `TransportError`: re-issuing the same streaming request would fail identically, so it must not flow into the transport-retry. The client catches it once, flips `_streaming_unsupported`, and re-issues the SAME request non-streaming for the rest of the session. A capability verdict (a streaming-rejection 4xx, or unusable tool-call framing), never a transient fault — when in doubt the discrimination defaults to `TransportError` (ADR-0029 D). ### `ToolCall` Decision to invoke a named tool with validated input (§6). ```python theme={null} ToolCall(*, type: Literal['tool_call'] = 'tool_call', name: str, input: dict = ) -> None ``` **Fields** | Field | Type | Required | | ------- | ---------------------- | -------- | | `type` | `Literal['tool_call']` | no | | `name` | `str` | yes | | `input` | `dict` | no | ### `TransportError` A model call failed at the *transport* layer — NOT model-correctable (§16, ADR-0028). A request timeout, connection reset, or an empty/NUL body (`EmptyResponseError`) means the provider returned nothing usable. Unlike `DecisionParseError`, this is **never** fed back to the model: it is retried in-client at the transport layer (re-issue the same request with backoff), and on exhaustion surfaced to the runner as a system failure. Carries `usage` so the billed-but-lost attempts are not undercounted. Args: message: A short description of the transport failure. usage: Tokens spent across the failed attempts, or `None` if unreported. ```python theme={null} TransportError(message: str = '', usage: 'DecisionUsage | None' = None) -> None ``` ## Functions ### `build_messages(context: ContextPacket, *, native_tools: bool = False) -> list[dict[str, str]]` Assemble the system + user messages for one decision (§9 packet → prompt). Args: context: The assembled context packet. native\_tools: `True` for the native tool-calling transport (ADR-0003 A) — the provider carries the tool schemas, so the prompt drops the JSON-envelope contract and the prose tool list; `False` keeps the legacy protocol verbatim. Returns: The system + user messages for one decision. ### `build_tool_schemas(context: ContextPacket) -> list[dict]` The function schemas for one decision: the advertised tools + the decision actions. Each phase-admitted tool rides its real pydantic `input_schema`; `final_answer` and `ask_user` become functions too, so every §6 decision shape is a structured call the provider validates — never a hand-escaped JSON envelope (ADR-0003 A). Args: context: The assembled context packet (its `allowed_tools` are advertised). Returns: OpenAI-style `tools=` entries. ### `parse_decision(raw: str) -> ModelDecision` Validate raw model output into a `ModelDecision`, or raise a recoverable error. Args: raw: The raw model output to validate. Returns: The validated `ModelDecision`. Raises: DecisionParseError: If `raw` is not valid JSON or not a valid decision. # avatar.permission Source: https://codexceed.mintlify.app/api-reference/permission PermissionPolicy — the before-tool-call control gate (§11). An *awaited control hook*, not an observation event (§13): the runner calls it before every execution and acts on the `ToolPermission` it returns (allow / block / ask). It can block and redirect control flow; the event emitter cannot. Keeping it a direct call — never an emitter subscriber — is the whole point. Tiers (§11): 0 reads (allow) · 1 edits — str\_replace/write\_file (allow iff every target path resolves inside the workspace) · 2 commands (allow) · 3+ destructive / external (blocked by default in the non-interactive MVP; `ask` lands with the Phase 3 REPL). Tier 1 is allowed for every task kind, including `investigate` (ADR-0005): transient instrumentation is legal there, and the verifier's net-zero-diff contract — not this gate — enforces that the tree matches the pinned baseline at verification (detection where prevention used to be). The sensitive-path denylist and workspace confinement still apply to every kind. ## Classes ### `PermissionPolicy` Evaluates a tool call against the §11 tier table before it runs. Args: sensitive\_path\_globs: The denylist enforced over every tool's declared paths. Defaults to the built-in set (secure by default); the runner threads the configured `HarnessConfig.sensitive_path_globs` through. ```python theme={null} PermissionPolicy(sensitive_path_globs: Sequence[str] | None = None) -> None ``` #### `PermissionPolicy.check(self, tool: ToolDefinition, raw_input: dict, state: TaskState, ws: Workspace) -> ToolPermission` Return the control decision for `tool` with `raw_input` (allow / block / ask). Tier 1 (mutation) is allowed for every task kind: investigate tasks may instrument transiently (ADR-0005), with the verifier's net-zero-diff contract as the enforcement point. Args: tool: The tool definition, carrying its `permission_tier` and declared paths. raw\_input: The proposed tool arguments. state: The current task state (reserved for state-aware policies). ws: The run-scoped workspace, used for path confinement. Returns: The `ToolPermission` the runner acts on. ### `ToolPermission` The gate's decision for one tool call — the runner acts on this (§11). ```python theme={null} ToolPermission(*, blocked: bool, reason: str = '', ask: bool = False) -> None ``` **Fields** | Field | Type | Required | | --------- | ------ | -------- | | `blocked` | `bool` | yes | | `reason` | `str` | no | | `ask` | `bool` | no | # avatar.planner Source: https://codexceed.mintlify.app/api-reference/planner VerificationPlanner — per-session verification-plan resolution (ADR-0007). The harness-owned collaborator that resolves *what proves this repo's work*, once per session, before editing begins. Resolution is tiered: 1. **Config override (always wins):** `AVATAR_TEST_COMMAND` / `AVATAR_LINT_COMMAND`, per slot. The user's stated contract is never overridden. 2. **Deterministic detection (no LLM, no Python assumption):** read repo artifacts — CI workflows, `package.json`, `pyproject.toml`, tox/nox, `Cargo.toml`, `go.mod`, `.pre-commit-config.yaml`, Makefile — and extract their *declared* test/lint invocations. CI-derived commands rank above arbitrary Makefile targets (CI is the gate the project actually trusts — least gameable). 3. **LLM fallback (evidence-grounded only, opt-in via `AVATAR_PLANNER_MODEL`):** the model may *propose* a command for a slot detection left empty, but only citing the artifact it came from; the harness validates the citation before accepting. A proposal without valid provenance is rejected. No model configured → this tier simply does not exist (resolution stays deterministic and offline). The resolved plan is a list of `PlannedCheck` (name, command, kind, provenance). The *runner* freezes it onto `TaskState` at the investigating → editing boundary and journals it; the `Verifier` then executes it with zero language knowledge. Python-ecosystem tools are emitted as `python -m <tool>` so an installed-but-not- on-PATH tool still resolves (the ADR's robustness floor). ## Classes ### `VerificationPlanner` Resolves the per-session verification plan (ADR-0007) — harness-owned. Args: config: Harness config; `test_command`/`lint_command` are the override tier and `planner_model` (unset by default) opts into the LLM fallback. client: An injected OpenAI-compatible client for the fallback, or `None` to build one lazily on first use (the `ModeClassifier` precedent; the fallback is never consulted unless `planner_model` is set). ```python theme={null} VerificationPlanner(config: HarnessConfig | None = None, client: Any = None) -> None ``` #### `VerificationPlanner.propose_smoke_check(self, ws: Workspace, files_modified: list[str]) -> PlannedCheck | None` Have the model author ONE executable smoke check for freshly-written code. The greenfield floor: used only when tiers 1-3 resolved nothing and the run wrote code, so there is genuinely no declared contract to discover. The model chooses *which* command; the harness still runs it and reads the real exit code, so this is author-and-run, never the self-certification §5 forbids. Resolved at verification time (the artifact under test does not exist at the freeze boundary). Runs on `config.model` (the main model) so the floor needs zero extra config; any endpoint failure degrades to "no floor", never blocks. Args: ws: The run-scoped workspace whose just-written files are excerpted. files\_modified: The repo-relative paths the run created or changed. Returns: The `model-smoke` check, or `None` when nothing runnable was proposed. #### `VerificationPlanner.resolve(self, ws: Workspace) -> list[PlannedCheck]` Resolve the verification plan for `ws`: override → detection → LLM fallback. Args: ws: The run-scoped workspace whose repo artifacts are read. Returns: The resolved checks, test slot first; empty when nothing is declared anywhere (the verifier then fails legibly — never an invented default). ## Functions ### `check_covers_content(command: str) -> bool` Whether a declared check covers a `content` change: anchored + falsifiable (ADR-0044). The `content` rulebook replaces "must run what you build" (meaningless for a textual deliverable) with two demands, judged together per stage (`_stage_inspects_content`): **anchored** — some stage receives a content artifact (`.md`/`.rst`/`.txt`/`.adoc`, behavior-bearing manifests excluded) as a real operand, so a plain `pytest` check never silently "covers" the docs half of a mixed change and a quoted filename inside a code string anchors nothing; **falsifiable** — that stage asserts (an assertive inspector with an operand, or a real executor), and no trailing `||` alternative that cannot fail (`|| true`, `|| echo fine`) neutralizes the line to exit 0. The stage split is quote-blind like `vacuous_declared_check` — live declared checks are single argvs by the time they reach here (the ADR-0045 gate rejects/splits operators first), so the multi-stage handling is defense-in-depth for direct callers, kept so the two layers can be edited independently. The immutable floor beneath the contract is the ultimate anchor. Args: command: The declared check command to classify. Returns: `True` when the command satisfies the `content` rulebook. ### `classify_change_paths(paths: Iterable[str]) -> set[Literal['code', 'content']]` The change kinds present in a set of changed paths (ADR-0044). The verification-time half of the declared-kind audit: `.md`/`.rst`/`.txt`/`.adoc` classify as `content`; every other suffix — and behavior-bearing `.txt` manifests (`requirements*.txt`, `CMakeLists.txt`) — classifies as `code`, so ambiguity fails toward the stricter rulebook. Args: paths: Workspace-relative changed paths (from the diff / `files_modified`). Returns: The subset of `{"code", "content"}` present; empty for no paths. ### `config_override_checks(config: HarnessConfig | None) -> list[PlannedCheck]` The override tier: checks declared explicitly in config (always win, per slot). Shared by the planner (tier 1) and the verifier's no-plan fallback, so the two read the user's stated contract identically. Args: config: The harness config, or `None` (no overrides). Returns: Up to one test and one lint check, for the non-empty config commands. ### `effective_invocation(command: str) -> tuple[str, list[str]]` The effective program + args of one command segment (program-position parse). Strips leading env-var assignments (`CI=1 pytest`) and unwraps runner wrappers (`sudo`, `uv run`, `npx`, `python -m <module>`) so classification keys on what actually executes — never on a token appearing anywhere in the line. Args: command: One command segment (no `&&`/`;` chaining). Returns: `(program, args)` — program is the basename (empty when nothing remains). ### `vacuous_declared_check(command: str) -> bool` Whether a model-declared verification command is vacuous (proves nothing — ADR-0038). A check is vacuous when it is empty or when *every* executed program on the line — each `&&`/`||`/`;` segment and each `|` pipeline stage, after unwrapping env/`sudo`/`uv run`/`npx`/`python -m` — is a no-op/inspector (`true`, `echo`, `grep`, …). Such a line exits 0 regardless of the artifact, so it can't be a real contract. One real stage redeems the line: `printf 'q' | python -m game` drives the program under test via stdin and must not be rejected for its `printf` head (dogfood 7e49b161 — the rejection cost a turn plus a tier-3 amendment approval for an equivalent check). The split is quote-blind, so an in-quote `|`/`;` can only mis-split toward *accepting* — e.g. `echo "a | b"` now passes because its phantom second stage `b"` reads as an unknown (real) program. That is the safe direction for a lower-bound guard: the immutable floor beneath the declared contract is what ultimately anchors non-vacuity. Args: command: The declared check command to validate. Returns: `True` when the command is vacuous and must be rejected. # avatar.runner Source: https://codexceed.mintlify.app/api-reference/runner AgentRunner — the bounded loop that terminates on verification (§5). The runner owns *all* `TaskState` mutation (§8); tools and the verifier are pure-ish workers. Phase 1 covers the read-only path: tier-0 tools, no permission gate (every tool is tier 0), and the minimal `investigate` verifier. The loop is deliberately a near-verbatim transcription of the §5 pseudocode. ## Classes ### `AgentRunner` The bounded loop that owns all state mutation and ends on verification (§5, §8). Collaborators are injected explicitly so a run is self-contained and replayable; the runner orchestrates them but mutates `TaskState` itself. Args: model\_client: Proposes each turn's `ModelDecision`. registry: The active `ToolRegistry`. deps: Run-scoped `RunDeps` (workspace, etc.). context\_builder: Assembles the per-iteration context packet (§9). verifier: Disposes of "done" on external evidence (§12). emitter: Observation-only event emitter (§13). config: Budgets and harness settings. policy: The before-tool-call control gate (§11); defaults to the standard tier policy. planner: Resolves the per-session verification plan (ADR-0007); defaults to a `VerificationPlanner(config)`. event\_sink: Optional typed-event sink (a `Session`); absent on the batch/sync path. approval\_controller: Optional awaited gate for tier-3 `ask` calls (a `Session`). conversational: Terminal-boundary authority (§23.5, ADR-0046). The verifier steers (repair loop) in both strict and conversational modes; this flag only changes the disposition at repair exhaustion — `False` (default, `--auto`) pronounces `failed`; `True` (interactive) defers to the human, blocking with an `open_question`. It never makes a failed verdict advisory. advisory: External-grading authority (ADR-0040 option A). `True` runs the verifier as advisory — it reports but does not steer or gate, and a failed verdict is delivered as `success` for a held-out grader (the eval probe) to judge. Off by default; set only by external-grading callers (the eval harness), never the REPL. Takes precedence over `conversational`. ```python theme={null} AgentRunner(*, model_client: ModelClient, registry: ToolRegistry, deps: RunDeps, context_builder: ContextBuilder, verifier: Verifier, emitter: Emitter, config: HarnessConfig, policy: PermissionPolicy | None = None, planner: VerificationPlanner | None = None, event_sink: EventSink | None = None, approval_controller: ApprovalController | None = None, conversational: bool = False, advisory: bool = False) -> None ``` #### `AgentRunner.arun(self, state: TaskState) -> TaskState` Drive the loop to a terminal outcome asynchronously — the real loop (§5). A near-verbatim async transcription of the §5 pseudocode: blocking model/tool/ verifier bodies are offloaded with `asyncio.to_thread` so the event loop stays responsive (spinners, keystrokes, ESC), and typed lifecycle events are published to the sink as the run progresses. With no sink/controller wired it matches `run()`. Args: state: The task state to drive; mutated in place. Returns: The final `TaskState` with a terminal `outcome`. Raises: TransportError: When a model call fails at the transport layer (timeout / NUL body) past the client's retries — a system failure surfaced to the caller, never fed back to the model or masked as a soft `incomplete` (§16, ADR-0028 R4). asyncio.CancelledError: When the run task itself is cancelled (the signal / `_run_task.cancel()` path, ADR-0030) — re-raised after aborting the in-flight model call so the loop unwinds without orphaning the request. #### `AgentRunner.run(self, state: TaskState) -> TaskState` Drive the loop to a terminal outcome synchronously (§5). The thin sync wrapper over the async core: `arun()` *is* the loop, `run()` wraps it via `asyncio.run()` for batch/library callers. Behavior is identical to the async path with no sink/controller wired. Args: state: The task state to drive; mutated in place. Returns: The final `TaskState` with a terminal `outcome`. # avatar.sandbox Source: https://codexceed.mintlify.app/api-reference/sandbox Hermetic execution at the command seam (ADR-0042). Every command the harness runs — the verifier's frozen checks *and* the model's `run_command` — funnels through `Workspace._run_unlogged`. A `Sandbox` transforms that one call: it computes the environment the child runs under and (in the OS/ container backends) wraps the argv in a launcher that denies network and confines writes. It never *runs* the command — the `Workspace` still does — so the seam stays a single chokepoint and `prepare()` is a pure transform (design invariant #5). Scope is **Threat C only** (ADR-0042): it closes runtime/substrate gaming — an inherited `PYTEST_ADDOPTS`/`PYTHONPATH`, a phone-home, a fork bomb — not a model that authors weak tests (Threat B) or guts the graded ones (Threat A). Workspace- *discovered* config (`conftest.py`, `pyproject` addopts) is a file, not env, and is out of scope here by decision (the adequacy/diff-scope lever, ADR-0040). Backends, weakest→strongest, all behind the same `prepare()`: * `none` — identity argv + the full inherited environment (today's behavior; the back-compat escape hatch). * `hermetic-env` — identity argv + an env scrubbed to a language-neutral allowlist, so `PYTEST_ADDOPTS`/`NODE_OPTIONS`/`RUBYOPT`/`PYTHONPATH`/`CLASSPATH` vanish *by construction* (no per-language whack-a-mole). The portable floor: pure Python, every OS, no dependencies. **The default.** * `sandbox-exec` — the allowlisted env + argv wrapped in the macOS launcher with a network-deny profile (Apple-deprecated, an optional native fast-path). * `bwrap` — Linux bubblewrap: the allowlisted env + a read-only root with only the workspace writable + a network namespace unshared (Increment 2). * `container` — a Podman/Docker run: net-deny, read-only rootfs with the workspace bind-mounted writable, a clean env, kernel-enforced pid cap. The one genuinely cross-platform strong sandbox (Increment 2); needs the runtime + an image. Resource limits (`RLimits`) ride `ExecSpec.preexec_fn` for the direct-exec backends and container flags for the container backend. They ship **off by default**: `preexec_fn` runs between `fork` and `exec`, which is not thread-safe against the multithreaded eval runner (ADR-0026), so they are an opt-in toggle, not baked into the default. ## Classes ### `Bwrap` `bwrap` (Linux): a bubblewrap sandbox — read-only root, writable workspace, no network. The Linux-native strong backend (ADR-0042 Increment 2). Binds the whole root read-only, re-binds the workspace writable, mounts a fresh `/tmp`, and (unless network is allowed) unshares the network namespace. The env is allowlist-scrubbed via the child's inherited environment (no `--clearenv` needed — `subprocess` sets it). Args: allow\_network: When `True`, keep the network namespace (no `--unshare-net`). rlimits: Optional resource ceilings applied via `preexec_fn` (opt-in; see `RLimits`). ```python theme={null} Bwrap(*, allow_network: 'bool' = False, rlimits: 'RLimits | None' = None) -> 'None' ``` #### `Bwrap.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'` Wrap `argv` in bubblewrap with `cwd` the only writable path. Args: argv: The command's argument vector. cwd: The workspace root, re-bound writable inside the otherwise read-only sandbox. Returns: An `ExecSpec` whose argv is the `bwrap` invocation, under the scrubbed environment. ### `Container` `container`: a Podman/Docker run — the cross-platform strong sandbox (ADR-0042 Increment 2). Net-denied by default, a read-only rootfs with the workspace bind-mounted writable at `/workspace`, a fresh tmpfs `/tmp`, and a kernel-enforced pid cap. The guest env is the image's own toolchain plus only the portable vars forwarded via `-e` — the host `PATH`/ `VIRTUAL_ENV` are meaningless inside and are not passed. Needs the runtime on `PATH` and an image carrying the task's toolchain. Args: image: The container image (e.g. `docker.io/library/python:3.12-slim`). runtime: The container CLI (`podman` or `docker`). allow\_network: When `True`, use `--network bridge` instead of `--network none`. rlimits: Resource ceilings; the pid cap is always applied, CPU/FSIZE are guest-managed. ```python theme={null} Container(*, image: 'str', runtime: 'str' = 'podman', allow_network: 'bool' = False, rlimits: 'RLimits | None' = None) -> 'None' ``` #### `Container.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'` Wrap `argv` in a `podman run` (or `docker run`) with the workspace bind-mounted. Args: argv: The command's argument vector, run as the container's command. cwd: The workspace root, bind-mounted read-write at `/workspace`. Returns: An `ExecSpec` whose argv is the container-runtime invocation; its own env is the host environment (so the runtime CLI resolves), while the guest env is set via `-e`. ### `ExecSpec` What to exec and under what environment — the pure output of `Sandbox.prepare`. A small spec rather than a bare `(argv, env)` tuple because resource limits can't be expressed as env: `preexec_fn` carries POSIX child setup for the backends that use it. The `Workspace` executes this; the sandbox never does. Args: argv: The argument vector to actually exec (identity, or launcher-wrapped). env: The complete environment the child runs under (never inherited implicitly). preexec\_fn: Optional POSIX child setup (resource limits); `None` for the env-only backends, which keeps execution thread-safe (see `RLimits`). ```python theme={null} ExecSpec(argv: 'list[str]', env: 'dict[str, str]', preexec_fn: 'Callable[[], None] | None' = None) -> None ``` ### `HermeticEnv` `hermetic-env`: identity argv + an allowlist-scrubbed environment (the portable default). Closes the env-injection sub-route of Threat C on every OS with no dependencies. It does *not* deny network (no OS mechanism at this layer) — that needs `sandbox-exec`/`bwrap`/ `container`. Args: rlimits: Optional resource ceilings applied via `preexec_fn` (opt-in; see `RLimits`). ```python theme={null} HermeticEnv(*, rlimits: 'RLimits | None' = None) -> 'None' ``` #### `HermeticEnv.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'` Return `argv` unchanged under an allowlist-scrubbed environment. Args: argv: The command's argument vector. cwd: The working directory (unused — this backend transforms only the environment). Returns: An `ExecSpec` with `argv` verbatim and the environment filtered to the allowlist. ### `NoSandbox` `none`: identity — the full inherited environment, exactly today's behavior (escape hatch). ```python theme={null} NoSandbox() ``` #### `NoSandbox.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'` Return `argv` unchanged under the full inherited environment. Args: argv: The command's argument vector. cwd: The working directory (unused — identity backend imposes no confinement). Returns: An `ExecSpec` with `argv` verbatim and the whole `os.environ`. ### `RLimits` POSIX resource ceilings a backend imposes on the child (ADR-0042 Increment 2). Off by default (opt-in): applied via `preexec_fn` for the direct-exec backends, which is not thread-safe between `fork` and `exec` against the multithreaded eval runner (ADR-0026). Best-effort — a limit that cannot be lowered is skipped, never a crash. Args: cpu\_seconds: RLIMIT\_CPU ceiling (CPU-seconds) — a runaway loop is killed. fsize\_bytes: RLIMIT\_FSIZE ceiling — a command cannot fill the disk. pids: The container pid cap (`--pids-limit`) — a fork bomb cannot wedge the host. ```python theme={null} RLimits(cpu_seconds: 'int' = 300, fsize_bytes: 'int' = 536870912, pids: 'int' = 1024) -> None ``` #### `RLimits.preexec(self) -> 'Callable[[], None] | None'` Return a child-setup callable that lowers CPU/FSIZE limits, or `None` off-POSIX. Returns: A no-arg callable for `subprocess`' `preexec_fn`, or `None` when `resource` is unavailable (non-POSIX). ### `Sandbox` Transforms a command invocation for hermetic execution (ADR-0042); never runs it. ```python theme={null} Sandbox(*args, **kwargs) ``` #### `Sandbox.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'` Return the `ExecSpec` the `Workspace` should exec for `argv` under `cwd`. Args: argv: The command's argument vector (post `shlex.split`). cwd: The workspace root the command runs in. Returns: The transformed `ExecSpec` (argv, environment, optional child setup). ### `SandboxExec` `sandbox-exec` (macOS): allowlisted env + argv wrapped in the launcher, network denied. Increment 1 confines the *network* only; write-confinement is deferred to the container backend (Increment 2), which does it cleanly with a read-only rootfs — macOS write profiles are finicky and the launcher itself is Apple-deprecated (kept as a native fast-path, never the sole backend, ADR-0042). Args: allow\_network: When `True`, skip the wrap and run env-scrubbed only (network open). rlimits: Optional resource ceilings applied via `preexec_fn` (opt-in; see `RLimits`). ```python theme={null} SandboxExec(*, allow_network: 'bool' = False, rlimits: 'RLimits | None' = None) -> 'None' ``` #### `SandboxExec.prepare(self, argv: 'list[str]', cwd: 'Path') -> 'ExecSpec'` Return the allowlisted env with `argv` wrapped in the launcher (unless network is allowed). Args: argv: The command's argument vector. cwd: The working directory (unused — Increment 1 confines network, not writes). Returns: An `ExecSpec` under the scrubbed environment: `argv` verbatim when network is allowed, else wrapped in `sandbox-exec` with a network-deny profile. ## Functions ### `hermetic_env(source: 'Mapping[str, str]') -> 'dict[str, str]'` Filter `source` to the language-neutral allowlist — the portability trick (ADR-0042). Dropping everything unlisted removes env-based rigging for *every* language at once, rather than blocklisting each runtime's injection var. `PATH`/`VIRTUAL_ENV` survive so legitimate verification (`python -m pytest`, `py_compile`) still resolves the interpreter. Args: source: The environment to filter (usually `os.environ`). Returns: A new dict holding only the allowlisted keys (exact names + `LC_*`). ### `make_sandbox(mode: 'str', *, allow_network: 'bool' = False, rlimits: 'RLimits | None' = None, image: 'str' = '', runtime: 'str' = 'podman') -> 'Sandbox'` Construct the `Sandbox` for a config `sandbox_mode` (ADR-0042). Args: mode: One of `none` / `hermetic-env` / `sandbox-exec` / `bwrap` / `container`. allow\_network: Passed to the OS/container backends; ignored by `none`/`hermetic-env`, which cannot gate network at their layer. rlimits: Optional resource ceilings (opt-in; ignored by `none`). image: The container image — required when `mode == "container"`. runtime: The container CLI for `mode == "container"` (`podman` or `docker`). Returns: The matching `Sandbox`. Raises: ValueError: For an unknown mode, or `container` without an image. # avatar.session Source: https://codexceed.mintlify.app/api-reference/session Session — the two-plane interactive boundary over one task run (ADR-0001/0002, Phase 3.0). Observation flows OUT via `events()` (an async stream that can never block or redirect the run); control flows IN via `resolve_approval()` and `cancel()`. An event may *announce* that approval is needed, but only the control method decides it (§13). The cockpit binds to exactly this surface; the engine stays unchanged. The `EventBus` fan-out + the privileged `JsonlEventJournal` live in `bus.py`/`journal.py` (Lane 1); this module owns the session boundary and approval/grant control. ## Classes ### `ApprovalGrant` A session-scoped standing approval: auto-allow one tool's calls sharing a program. Stored when a human approves a tier-3 call with `[a] always`. Scoped to a `(tool, prefix, tier)` triple — never global (an empty `prefix` matches nothing) and never tier-4 (destructive actions always re-prompt). ```python theme={null} ApprovalGrant(*, tool: str, prefix: str, tier: int) -> None ``` **Fields** | Field | Type | Required | | -------- | ----- | -------- | | `tool` | `str` | yes | | `prefix` | `str` | yes | | `tier` | `int` | yes | #### `ApprovalGrant.matches(self, tool: str, program: str, tier: int) -> bool` Whether this grant auto-allows a call to `tool`/`program` at `tier`. Args: tool: The tool name of the incoming call. program: The incoming call's program prefix (see `_grant_prefix`). tier: The incoming call's permission tier. Returns: True iff the grant covers the call (same tool + program, at or below the granted tier, and below the tier-4 ceiling). A blank prefix never matches, and an ungrantable tool (`alter_verification`) never matches — a contract amendment is ratified by a human every time. ### `Session` A live, interruptible task run exposing the two-plane API (§13, §23). Args: runner: The `AgentRunner` to drive (its `event_sink`/`approval_controller` are wired to this session for the duration of `run`). state: The task state to execute. session\_id: Stable id stamped on events; generated if omitted. journal: The privileged write-ahead sink for this run's events; `None` (default) keeps the in-memory `history` only. `run()` closes it when the run ends. grants: The standing-approval list to consult and append to. Pass the multi-turn `SessionState.grants` (by reference) so a `[a] always` granted in one task persists to later tasks in the conversation; omit for a fresh per-run list. unattended: When `True` (batch/eval/autonomous wrappers), an `ask` is auto-denied immediately rather than awaiting a human — no `resolve_approval` will ever come, so awaiting one deadlocks the run. The deny is still announced + recorded (`ApprovalResolved(via="auto")`). `False` (default) keeps the interactive path. approval\_timeout: A backstop, in seconds, on a *blocking* (attended) approval: if no `resolve_approval` arrives within it, the call is auto-denied so a run can't hang forever inside the gate (the wall-clock budget can't preempt an awaited approval). `None` (default) waits indefinitely — the right shape for a human at a REPL. amendment\_policy: The unattended disposition for an `alter_verification` amendment (ADR-0039). `"deny"` (default) keeps ADR-0016's deny-only posture; `"approve"` lets an autonomous run self-ratify that one action (never any other `ask`). escalation\_policy: The unattended disposition for a `switch_to_editing` escalation (ADR-0048). `"deny"` (default) refuses the mid-run investigate→edit switch with no human; `"approve"` lets an autonomous run self-ratify that one action — the same vocabulary as `amendment_policy`, since the two knobs share their semantics. ```python theme={null} Session(runner: AgentRunner, state: TaskState, *, session_id: str | None = None, journal: JsonlEventJournal | None = None, grants: list[ApprovalGrant] | None = None, unattended: bool = False, approval_timeout: float | None = None, amendment_policy: str = 'deny', escalation_policy: str = 'deny') -> None ``` #### `Session.cancel(self, reason: str = 'cancelled') -> None` Control plane: trip the cancellation token; the loop observes it at the next turn. Also denies any in-flight approval so a run blocked on the gate can reach the cancellation checkpoint rather than hanging. Args: reason: Why the run is being cancelled (retained for display; the loop records its own cancellation feedback when it observes the token). #### `Session.events(self) -> AsyncIterator[Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]]` The observation plane: an independent async stream of typed events (§13). Subscribes *eagerly* (at call time, not first iteration), so a consumer created before `run()` never misses early events. Each call yields a fresh, independent stream — many observers can watch the same run concurrently. Returns: An async iterator over this run's `HarnessEvent`s, ending on `agent_end`. #### `Session.request_approval(self, approval_id: str, tool: str, reason: str, tool_input: dict) -> bool` Announce a gated call (observation) and await the human's decision (control). Called by the runner on a tier-3 `ask`. Emits `ApprovalRequested`, then disposes of it per mode: an attended session blocks *this run only* until `resolve_approval` completes the future (decision never returns through the event stream, §13); an `unattended` session auto-denies immediately (no human will come); and an `approval_timeout` denies a blocking wait that no human answers in time. Every deny is recorded as `ApprovalResolved(via="auto")`. A standing `ApprovalGrant` from an earlier `[a] always` short-circuits the human: the call is auto-allowed and recorded as `ApprovalResolved(via="grant")` with **no** `ApprovalRequested` (that event means "a human must decide"; a grant skips the human). Args: approval\_id: Correlates the announcement with its resolution. tool: The tool name awaiting approval. reason: The gate's reason, shown to the human. tool\_input: The proposed call arguments. Returns: True iff the call was allowed. #### `Session.resolve_approval(self, approval_id: str, *, allow: bool, remember: bool = False) -> None` Control plane: resolve a pending approval (the decision the event announced). Tolerant of an unknown id (resolves the sole pending request if there is exactly one) so a caller that didn't capture the id can still unblock the run. With `allow` and `remember` both set (the `[a] always` choice), stores a session-scoped `ApprovalGrant` for the call's program prefix so matching calls auto-allow later. `remember` is ignored on a denial (there is no "always deny"), on a blank prefix, and on an ungrantable tool (`alter_verification` — a contract amendment is ratified by a human every time; `remember` degrades to allow-once). Args: approval\_id: The id from the `ApprovalRequested` event. allow: Whether to permit the gated call. remember: Whether to store a standing grant (only meaningful with `allow`). #### `Session.run(self) -> TaskState` Drive the run with this session as the event sink + approval controller. Returns: The terminal `TaskState`. # avatar.session_state Source: https://codexceed.mintlify.app/api-reference/session_state SessionState + ReplSession — the multi-turn scope above one task (§23, Phase 3.1 Lane 2a). `TaskState` (§7) is per-goal and unchanged. `SessionState` is the scope *above* it: the conversation `history`, the sequence of per-goal `tasks`, the session-scoped approval `grants`, and the current `mode`. `ReplSession` is the thin driver: each goal becomes one fresh `TaskState` run through the existing single-task `Session` (one code path — batch is the degenerate one-`submit` case, §23.2), seeded with prior history and carrying grants forward. The Textual cockpit (Lane 2b) renders this; here it stays pure logic. Mode routing (revised ADR-0002 D3) is **visible and correctable, never hidden**: an explicit `set_mode` override wins; else a one-shot LLM classification (`ModeClassifier`, when configured) seeds `task_kind` from the prompt + conversation; else the hardened word heuristic. The verdict is announced before the run and `/mode` re-routes. **Plan mode** (ADR-0002 D5) is the one mode that isn't a `task_kind`: a no-net-change plan task (`investigate` kind: the tree must net to zero diff at verification, ADR-0005) → human approve/revise → the approved plan seeds the edit task as a constraint; `submit_plan` drives that flow. Local **meta commands** (`/help` `/quit` `/state` `/mode` `/plan` `/diff` `/permissions`) are handled by `run_meta` and never reach the model (§23.2). ## Classes ### `MetaResult` The outcome of a local meta command — the cockpit interprets `kind`, displays `text`. `kind`: `message` (show text) · `mode_set` (mode changed) · `state` (session summary) · `diff` (text is a unified diff → the diff modal) · `quit` (end the session). ```python theme={null} MetaResult(kind: Literal['message', 'mode_set', 'state', 'diff', 'quit'], text: str) -> None ``` ### `PlanDecision` The human's verdict on a proposed plan — the `PlanModal` choice, decoupled from Textual. `approved`: build with this plan (transition into editing). `text`: the (possibly edited) plan — on approval an empty `text` keeps the proposed plan; on revise it is the revision request fed back into the re-planning turn. ```python theme={null} PlanDecision(approved: bool, text: str) -> None ``` ### `ReplSession` Drives a multi-turn conversation over the unchanged single-task engine (§23.2). Each goal runs as one fresh `TaskState` through a per-goal `Session`; history seeds the next task and grants persist across tasks. `submit` is the simple run-to-completion path; `start`/`record` are the lower-level pair the cockpit uses so it can observe the per-task event stream and answer approvals between them. Args: harness: The configured `Harness`; supplies the per-goal run wiring. session\_id: Stable conversation id; generated if omitted. auto: Terminal-boundary authority (§23.5, ADR-0046). The verifier steers (repair loop) either way; this only sets the disposition at repair exhaustion. The default (`False`) is *conversational* — the model repairs or proposes a gated amendment, and at exhaustion the turn defers to the human (`blocked` + an open question). `auto=True` is the strict §12 gate (repair exhaustion → `failed`; the `--auto` flag, wired by the CLI in 3.2e). A failed verdict is never advisory. journal: One write-ahead `JsonlEventJournal` for the whole sitting, threaded into every per-goal `Session` (shared by reference, like `grants`) so the multi-turn conversation lands in one durable file. Each goal's `bus.close()` closes the handle; `append` reopens it for the next goal. `None` (default) keeps the interactive stream in memory only. classifier: The LLM mode router (revised ADR-0002 D3); if omitted, built from `config.classifier_model` when set (`None`/empty → heuristic-only routing). Its verdict is displayed and `/mode`-overridable — visible, never silent. allow\_dirty: `True` acknowledges a dirty tree at the *start* of the sitting (the `--allow-dirty` flag). Regardless of this, **session-owned dirt is always tolerated**: the §15 clean-start check applies to the sitting's first goal only — after that, uncommitted changes are the session's own work product, and a follow-up goal must not be refused because the previous one succeeded. ```python theme={null} ReplSession(harness: Harness, *, session_id: str | None = None, auto: bool = False, journal: JsonlEventJournal | None = None, classifier: ModeClassifier | None = None, allow_dirty: bool = False) -> None ``` #### `ReplSession.extract_plan(state: TaskState) -> str` The proposed plan text from a plan task: its `final_answer`, else its `current_plan`. Args: state: A terminal plan `TaskState`. Returns: The plan text (possibly empty when the run produced nothing). #### `ReplSession.is_meta(self, text: str) -> bool` Whether `text` is a meta command (handled locally, never run as a goal). Args: text: The raw user input. Returns: True iff the input begins with `/`. #### `ReplSession.mode` The explicit mode override (incl. `plan`), or `None` when the heuristic decides. #### `ReplSession.plan_is_approvable(state: TaskState) -> bool` Whether a finished plan run may be offered to the human for approval (§23.5). False for an empty plan (you can't approve `""`) or one that terminated abnormally (`blocked`/`incomplete`); a non-empty verifier-rejected (`failed`) plan is approvable — the human, not the structural gate, is the authority. Args: state: A terminal plan `TaskState`. Returns: True iff the plan is non-empty and did not terminate abnormally. #### `ReplSession.record(self, state: TaskState) -> None` Record a finished goal: append the terminal task and the agent's reply turn. A goal that ended by asking the user records the **question** as its agent turn, not the bare outcome. Otherwise the next user message — which is the *answer* — seeds history as `agent: blocked` and reads to the model as a fresh, contextless goal, so it re-asks and the conversation goes in circles (dogfood `events/f0957ed4…jsonl`). Preference: the final answer, then the open question, then the outcome. Args: state: The terminal `TaskState` returned by `session.run()`. #### `ReplSession.record_goal(self, prompt: str, state: TaskState) -> TaskState` Record one plan-flow goal: append its user turn (once) and the terminal task. Used by the plan flow (`submit_plan` and the cockpit), whose plan/build sessions are built with `append_turn=False` — the goal's user turn is recorded here, *after* they run, so neither echoes the current goal into its own history evidence (matching `start`). Args: prompt: The user's goal. state: The terminal task to record (a build, or a surfaced planning state). Returns: The recorded terminal `TaskState`. #### `ReplSession.resolve_mode(self, prompt: str) -> Literal['edit', 'investigate', 'test_only', 'plan']` The mode for `prompt`: explicit override → classifier → heuristic. The classifier verdict is memoized per prompt (`start()` re-resolves internally; a goal pays at most one classification call) and any classifier failure degrades to the heuristic — routing can lose quality, never block. `last_mode_source` records how the verdict was reached, for display. Args: prompt: The user's goal. Returns: The resolved mode. Classifier/heuristic only ever yield a `task_kind`; `plan` is opt-in (set explicitly), never inferred. #### `ReplSession.run_meta(self, text: str) -> MetaResult` Handle a `/command` locally and return a result the cockpit renders/routes (§23.2). Args: text: The raw `/command [arg]` input. Returns: The `MetaResult` for the command (unknown commands are reported, never run). #### `ReplSession.set_mode(self, mode: Literal['edit', 'investigate', 'test_only', 'plan']) -> None` Pin the mode for subsequent goals (the `/mode` override; overrides the heuristic). Args: mode: The mode to force on later goals until changed (a `task_kind`, or `plan`). #### `ReplSession.start(self, prompt: str) -> Session` Build (but don't run) the next per-goal `Session`: resolve mode, seed history + the turn. In `plan` mode this is the **plan task** (`task_kind="investigate"` with the planning directive) — the first step of the plan flow the cockpit drives; otherwise it is a direct run of the resolved `task_kind`. The returned session is wired with the session-scoped grant list (shared by reference), so a `[a] always` persists across goals. Args: prompt: The user's goal. Returns: A not-yet-started `Session` for this goal. #### `ReplSession.start_build(self, prompt: str, plan: str) -> Session` Build the edit task for an approved plan: the plan rides as a `constraint` (§12, D5). The build is a normal `edit` task — it rides the `investigating → editing` gate; the approved plan is surfaced to the model as a constraint. No user turn is appended: the goal's turn was recorded when planning began (this is the same goal, continued). Args: prompt: The user's goal. plan: The approved plan text to seed as a constraint. Returns: A not-yet-started edit `Session`. #### `ReplSession.start_plan(self, prompt: str, *, revision: str | None = None) -> Session` Build the plan `Session` for `prompt` (observable; the cockpit streams it). An `investigate` task (net-zero-diff contract, ADR-0005) seeded with the planning directive (and, on a revise, the revision note so the model refines). No user turn is appended — the plan flow records the goal's turn once via `record_goal`, so plan and build don't double it. Args: prompt: The user's goal. revision: A prior revision request to fold in, or `None` for the first plan. Returns: A not-yet-started plan `Session`. #### `ReplSession.submit(self, prompt: str) -> TaskState` Run one goal to completion and record it — the simple (batch-shaped) path. Args: prompt: The user's goal. Returns: The terminal `TaskState`. Raises: ValueError: In `plan` mode — planning is interactive (approve/revise), so it has no run-to-completion path; use `submit_plan`, or `set_mode` to switch modes. #### `ReplSession.submit_plan(self, prompt: str, decide: Callable[[str], PlanDecision]) -> TaskState` Drive the plan flow: plan → approve/revise → build (ADR-0002 D5, §23). Proposes a plan with a no-net-change `investigate` task, then calls `decide` (the `PlanModal` in the cockpit; an injected callback in tests). On revise it re-runs the plan task with the revision fed back so the model refines it; on approval it runs the edit task seeded with the approved plan as a constraint and returns its terminal state. A plan run that produced nothing usable — empty, or terminated as `blocked`/`incomplete` — is never offered for approval: its terminal planning state is recorded and returned instead (you can't approve `""`). A non-empty verifier-rejected plan *is* shown to the human (the human is the authority, not the structural gate). A `decide` that never approves stops at the revision budget and returns `incomplete`. Args: prompt: The user's goal. decide: Called with each proposed plan; returns the human's `PlanDecision`. Returns: The terminal `TaskState` — the build (edit) task on approval, else the terminal planning state when there was nothing approvable or the revision budget was hit. ### `SessionState` The scope above `TaskState`: what the user and agent have discussed/done (§23.1). Args: session\_id: Stable id for the whole conversation. workspace\_root: The repo the session operates on. config: The harness config in effect for the session. history: Conversational turns, carried across goals as context. tasks: One terminal `TaskState` per goal run so far. grants: Session-scoped standing approvals (`[a] always`); never cross-session. mode: The explicit mode override (incl. `plan`), or `None` for the per-prompt heuristic. ```python theme={null} SessionState(*, session_id: str, workspace_root: str, config: HarnessConfig, history: list[Turn] = , tasks: list[TaskState] = , grants: list[ApprovalGrant] = , mode: Literal['edit', 'investigate', 'test_only', 'plan'] | None = None) -> None ``` **Fields** | Field | Type | Required | | | ---------------- | ------------------------------------------------------- | -------- | -- | | `session_id` | `str` | yes | | | `workspace_root` | `str` | yes | | | `config` | `HarnessConfig` | yes | | | `history` | `list[Turn]` | no | | | `tasks` | `list[TaskState]` | no | | | `grants` | `list[ApprovalGrant]` | no | | | `mode` | \`Literal\['edit', 'investigate', 'test\_only', 'plan'] | None\` | no | ### `Turn` One conversational turn — a user prompt or an agent reply (§23.1). ```python theme={null} Turn(*, role: Literal['user', 'agent'], text: str, task_id: str | None = None) -> None ``` **Fields** | Field | Type | Required | | | --------- | -------------------------- | -------- | -- | | `role` | `Literal['user', 'agent']` | yes | | | `text` | `str` | yes | | | `task_id` | \`str | None\` | no | ## Functions ### `default_mode(prompt: str) -> Literal['edit', 'investigate', 'test_only']` Heuristically route a free-form prompt to an initial mode (`task_kind`). A first-word imperative (`fix …`, `add …`) reads as an edit goal; anything else (questions, "explain …", "why …") defaults to investigation (grounded answer, net-zero diff at verification). This is the *visible* default the status bar shows and the user can override — not a classifier. Args: prompt: The user's natural-language goal. Returns: `"edit"` for an edit-shaped prompt, otherwise `"investigate"`. # avatar.shell_syntax Source: https://codexceed.mintlify.app/api-reference/shell_syntax Shell-syntax boundary: vet command strings the way the harness executes them (ADR-0045). `Workspace.run` executes a single argv with NO shell (`shlex.split` + `subprocess.run`), so shell operators are never operators — a `&&` chain runs its FIRST program with the rest of the line as literal arguments, and a heredoc blocks forever on stdin that no shell will ever feed. One dogfood journal (`tetris_glm/events/be46ea…jsonl`) showed both ends of that failure: a declared grep chain that verification passed vacuously (later patterns became unopenable *filenames*; `grep -q` exit-0'd on the first match anyway) and a declared heredoc that hung to timeout and fed a finalization spiral. This module is the shared gate every model-authored command boundary applies BEFORE a string reaches `Workspace.run`: `&&` normalizes to a conjunction of single-argv segments (matching the planner's per-segment classification, §12/ADR-0044); every other operator is a legible, model-correctable rejection (§10) — never a silent mangle. ## Functions ### `argv_segments(command: str) -> tuple[list[str], str]` Split `command` on `&&` into single-argv segments, rejecting other shell operators. Args: command: The model-supplied, shell-style command string. Returns: `(segments, "")` — each segment re-joined via `shlex.join`, argv-equivalent to the original — or `([], reason)` naming the unrunnable construct. # avatar.state Source: https://codexceed.mintlify.app/api-reference/state TaskState — the structured source of truth for one task (§7). The model's message history is *derived* from this; it is not the source of truth. State is explicit, append-mostly, and fully serializable so a run can be inspected and replayed. The runner owns all mutation (§8); these helpers are the only sanctioned mutations. ## Classes ### `CheckResult` One verifier check with an explicit status (§12). A skipped check is not a passed check: `skip_reason` is required when `status == "skip"` so the gate can distinguish allowed skips from evasions. ```python theme={null} CheckResult(*, name: str, kind: Literal['required', 'optional'], status: Literal['pass', 'fail', 'skip'], evidence: str, skip_reason: str | None = None) -> None ``` **Fields** | Field | Type | Required | | | ------------- | --------------------------------- | -------- | -- | | `name` | `str` | yes | | | `kind` | `Literal['required', 'optional']` | yes | | | `status` | `Literal['pass', 'fail', 'skip']` | yes | | | `evidence` | `str` | yes | | | `skip_reason` | \`str | None\` | no | ### `CommandRecord` A command the harness ran on the workspace's behalf. ```python theme={null} CommandRecord(*, step: int, command: str, exit_code: int | None = None, summary: str = '') -> None ``` **Fields** | Field | Type | Required | | | ----------- | ----- | -------- | -- | | `step` | `int` | yes | | | `command` | `str` | yes | | | `exit_code` | \`int | None\` | no | | `summary` | `str` | no | | ### `ConversationTurn` One cross-goal conversation turn, replayed to the model as a real chat message (ADR-0017). Distinct from `Evidence`: prior user goals and agent replies are sent as genuine `role="user"`/`role="assistant"` messages preceding the working packet, not flattened into "Recent evidence" bullets the model under-weights. ```python theme={null} ConversationTurn(*, role: Literal['user', 'assistant'], content: str) -> None ``` **Fields** | Field | Type | Required | | --------- | ------------------------------ | -------- | | `role` | `Literal['user', 'assistant']` | yes | | `content` | `str` | yes | ### `DecisionRecord` Why the agent chose what it chose, and how it turned out (§7). `outcome` is filled in after the action runs (tool summary/error, verifier verdict, block reason) so the action ledger can show `chosen → outcome`. ```python theme={null} DecisionRecord(*, step: int, rationale: str, chosen: str, key: str = '', rejected: list[str] = , outcome: str = '') -> None ``` **Fields** | Field | Type | Required | | ----------- | ----------- | -------- | | `step` | `int` | yes | | `rationale` | `str` | yes | | `chosen` | `str` | yes | | `key` | `str` | no | | `rejected` | `list[str]` | no | | `outcome` | `str` | no | ### `Evidence` A test output, command result, file finding, or error (§7). ```python theme={null} Evidence(*, step: int, kind: str, summary: str, detail: str | None = None) -> None ``` **Fields** | Field | Type | Required | | | --------- | ----- | -------- | -- | | `step` | `int` | yes | | | `kind` | `str` | yes | | | `summary` | `str` | yes | | | `detail` | \`str | None\` | no | ### `PlannedCheck` One resolved verification check: what runs, and where it came from (ADR-0007). The unit of the per-session verification plan. `provenance` names the artifact the command was resolved from (`config:AVATAR_TEST_COMMAND`, `ci:.github/...`, `Makefile:test`, `llm:<cited path>`, `model-smoke`, `model-declared`), so every run's rubric is auditable. `smoke` is the greenfield floor (ADR-0014): a model-authored check the harness still runs itself, resolved at verification time rather than frozen up front. `declared` is a greenfield model-declared contract (ADR-0038): a real executing check the model authors up front (mandatory for greenfield edits), frozen like tiers 1-3 but semi-frozen — amendable only through a gated action. `floor` is the immutable non-vacuity anchor beneath a declared contract; the model can never amend it away. `chain` links checks split from one `&&` line (ADR-0045): the verifier stops a chain at its first failure, preserving shell short-circuit semantics — a failing segment still guards a later mutating one. `None` for independent checks. Optional with a default, so journals written before the field parse unchanged (version-skew rule). ```python theme={null} PlannedCheck(*, name: str, command: str, kind: Literal['test', 'lint', 'smoke', 'declared', 'floor'], provenance: str, chain: str | None = None) -> None ``` **Fields** | Field | Type | Required | | | ------------ | ------------------------------------------------------- | -------- | -- | | `name` | `str` | yes | | | `command` | `str` | yes | | | `kind` | `Literal['test', 'lint', 'smoke', 'declared', 'floor']` | yes | | | `provenance` | `str` | yes | | | `chain` | \`str | None\` | no | ### `TaskState` The full, serializable state of one task — the harness's source of truth (§7). Carries the two independent axes (`phase` = where the work is, `outcome` = how it ended), the bounding counters, and the accumulated evidence/decisions the context builder draws on. The model's message history is derived from this. ```python theme={null} TaskState(*, task_id: str = , goal: str, constraints: list[str] = , task_kind: Literal['edit', 'investigate', 'test_only'] = 'edit', mode_source: str | None = None, phase: Literal['investigating', 'editing', 'verifying'] = 'investigating', outcome: Literal['success', 'incomplete', 'blocked', 'failed'] | None = None, iterations: int = 0, consecutive_failures: int = 0, repair_failures: int = 0, declaration_nudges: int = 0, smoke_floor_attempted: bool = False, escalated: bool = False, escalation_thrash_streak: int = 0, escalation_nudged: bool = False, prompt_tokens: int = 0, completion_tokens: int = 0, files_read: set[str] = , files_modified: set[str] = , commands_run: list[CommandRecord] = , conversation: list[ConversationTurn] = , evidence: list[Evidence] = , decisions: list[DecisionRecord] = , verifier_results: list[VerifierResult] = , verification_plan: list[PlannedCheck] | None = None, declared_change_kinds: list[str] | None = None, current_plan: list[str] = , open_questions: list[str] = , latest_error: str | None = None, final_answer: str | None = None) -> None ``` **Fields** | Field | Type | Required | | | -------------------------- | -------------------------------------------------------- | -------- | -- | | `task_id` | `str` | no | | | `goal` | `str` | yes | | | `constraints` | `list[str]` | no | | | `task_kind` | `Literal['edit', 'investigate', 'test_only']` | no | | | `mode_source` | \`str | None\` | no | | `phase` | `Literal['investigating', 'editing', 'verifying']` | no | | | `outcome` | \`Literal\['success', 'incomplete', 'blocked', 'failed'] | None\` | no | | `iterations` | `int` | no | | | `consecutive_failures` | `int` | no | | | `repair_failures` | `int` | no | | | `declaration_nudges` | `int` | no | | | `smoke_floor_attempted` | `bool` | no | | | `escalated` | `bool` | no | | | `escalation_thrash_streak` | `int` | no | | | `escalation_nudged` | `bool` | no | | | `prompt_tokens` | `int` | no | | | `completion_tokens` | `int` | no | | | `files_read` | `set[str]` | no | | | `files_modified` | `set[str]` | no | | | `commands_run` | `list[CommandRecord]` | no | | | `conversation` | `list[ConversationTurn]` | no | | | `evidence` | `list[Evidence]` | no | | | `decisions` | `list[DecisionRecord]` | no | | | `verifier_results` | `list[VerifierResult]` | no | | | `verification_plan` | \`list\[PlannedCheck] | None\` | no | | `declared_change_kinds` | \`list\[str] | None\` | no | | `current_plan` | `list[str]` | no | | | `open_questions` | `list[str]` | no | | | `latest_error` | \`str | None\` | no | | `final_answer` | \`str | None\` | no | #### `TaskState.add_feedback(self, summary: str, *, detail: str | None = None, kind: str = 'feedback') -> None` Append evidence the next context build will surface (§5 repair loop). Args: summary: One-line evidence the next context build surfaces. detail: Optional verbatim detail kept out of the model's summary view. kind: Evidence category, e.g. `feedback` or `blocker`. #### `TaskState.amend_declared_contract(self, checks: list[PlannedCheck]) -> None` Replace the model-declared checks in the frozen plan, preserving the floor (ADR-0038). The one sanctioned mid-run rewrite of a declared contract, applied by the runner only after a gated `alter_verification` is approved. Entries whose kind is not `"declared"` (the immutable floor, any detected checks) are preserved — the model can move its own goalposts, never the harness's floor. Args: checks: The new declared checks (`kind="declared"`) replacing the old ones. Raises: RuntimeError: When there is no frozen plan to amend. #### `TaskState.append_verification_floor(self, check: PlannedCheck) -> None` Append the immutable non-vacuity floor beneath a model-declared contract (ADR-0038). The floor is a harness-authored check the model can never declare or amend; it is appended (`kind="floor"`) to the frozen plan so `success` always requires it *in addition* to whatever the model declared. Applied at most once — a plan that already carries a floor is left untouched. Args: check: The floor check to append (`kind="floor"`). Raises: RuntimeError: When no plan is frozen yet. #### `TaskState.block(self, reason: str) -> None` Terminal: the task needs human input (§5 ask\_user in a non-interactive run). Args: reason: Why the task is blocked. #### `TaskState.freeze_verification_plan(self, plan: list[PlannedCheck]) -> None` Freeze the resolved verification plan — once, before editing begins (ADR-0007). The freeze is an authority transfer away from the model: after it, the rubric cannot move. A second freeze attempt is a harness bug, not a retry. Args: plan: The resolved checks (may be empty: "nothing discovered"). Raises: RuntimeError: When a plan is already frozen onto this state. #### `TaskState.set_smoke_floor(self, checks: list[PlannedCheck]) -> None` Late-bind the greenfield smoke floor onto an otherwise-empty frozen plan (ADR-0014). The one sanctioned exception to "the rubric never moves mid-run": it applies ONLY when tiers 1-3 discovered nothing (`verification_plan == []`), turning the empty no-contract plan into a model-authored smoke check resolved at verify time. A non-empty (a real contract won) or unfrozen (`None`) plan is never touched. Args: checks: The resolved smoke check(s) to bind. Raises: RuntimeError: When the frozen plan is not the empty no-contract plan. #### `TaskState.terminal` Whether the task has reached a terminal outcome (loop should stop). ### `VerifierResult` The verifier's verdict for one verification attempt (§12). ```python theme={null} VerifierResult(*, passed: bool, summary: str, checks: list[CheckResult] = , recommended_next_action: str | None = None) -> None ``` **Fields** | Field | Type | Required | | | ------------------------- | ------------------- | -------- | -- | | `passed` | `bool` | yes | | | `summary` | `str` | yes | | | `checks` | `list[CheckResult]` | no | | | `recommended_next_action` | \`str | None\` | no | # avatar.tools Source: https://codexceed.mintlify.app/api-reference/tools Typed, phase-gated tools the model acts through (§10). ## Functions ### `default_registry() -> ToolRegistry` A registry with the MVP tool surface registered, phase-gated by definition. Read tools are active in every phase; the edit tools (`str_replace` (the string-anchored editor), `write_file`, `delete_file` — ADR-0015) only in `editing`; the command tools all in `editing`/`verifying` (§10/§21 capability groups) — `run_tests`/`run_linter` at tier-2 (allowed), `run_command` at tier-3 (default-blocked in batch, approval-gated in the REPL — ADR-0002 D4). The `ContextBuilder` exposes only the phase-active subset to the model. Returns: A `ToolRegistry` with the MVP tools registered. # avatar.tools.base Source: https://codexceed.mintlify.app/api-reference/tools-base Tool contracts: result shape, definition, registry, and runtime (§10). Tools are narrow, typed, and self-describing. The runtime validates every call (known name, well-formed input) before executing; model-correctable errors come back as `ToolResult(success=False, error=...)` — recoverable feedback for the model, never an exception thrown at the loop (§10 retry semantics). ## Classes ### `ToolDefinition` A registered tool: its schema, handler, the phases it is active in, and tier (§10). `paths` self-declares which of the tool's *validated* inputs are filesystem paths (§11, Phase 2.5). The permission gate runs confinement + the sensitive-path denylist over them centrally, so the policy can't drift across tools. The default is a pass-through (no paths) — only path-bearing tools override it. ```python theme={null} ToolDefinition(name: str, description: str, input_model: type[BaseModel], handler: Callable[[Any, RunDeps], ToolResult], phases: frozenset[str], permission_tier: int = 0, paths: Callable[[Any], Sequence[str]] = >) -> None ``` #### `ToolDefinition.paths(_args)` *No description.* ### `ToolRegistry` The set of available tools, queryable by name and by active phase (§10). ```python theme={null} ToolRegistry() -> None ``` #### `ToolRegistry.active_for_phase(self, phase: str) -> list[ToolDefinition]` Return the tools enabled in the given phase (§10/§21 capability groups). Args: phase: The phase to filter active tools by. Returns: The tool definitions whose `phases` include `phase`. #### `ToolRegistry.admitted_for(self, phase: str, task_kind: str) -> list[ToolDefinition]` Return the tools the runner will admit in `phase` for `task_kind` (§2.6). Like `active_for_phase`, but also includes the edit-intent bootstrap, so the model is advertised *exactly* what the runner's gate would let it execute — notably `str_replace` from `investigating` on an edit task. Args: phase: The phase to filter by. task\_kind: The task's kind, gating the edit-intent bootstrap. Returns: The tool definitions admitted in `phase` for `task_kind`. #### `ToolRegistry.get(self, name: str) -> ToolDefinition | None` Return the tool registered under `name`, or None if unknown. Args: name: The tool name to look up. Returns: The matching `ToolDefinition`, or None if no tool is registered under `name`. #### `ToolRegistry.register(self, tool: ToolDefinition) -> None` Add (or replace) a tool definition by name. Args: tool: The tool definition to register, keyed by its `name`. ### `ToolResult` The typed outcome of one tool call; the model only ever sees `content` (§10). ```python theme={null} ToolResult(*, tool_name: str, success: bool, content: str = '', summary: str = '', error: str | None = None, files_read: list[str] = , files_changed: list[str] = , terminate: bool = False) -> None ``` **Fields** | Field | Type | Required | | | --------------- | ----------- | -------- | -- | | `tool_name` | `str` | yes | | | `success` | `bool` | yes | | | `content` | `str` | no | | | `summary` | `str` | no | | | `error` | \`str | None\` | no | | `files_read` | `list[str]` | no | | | `files_changed` | `list[str]` | no | | | `terminate` | `bool` | no | | ### `ToolRuntime` Validates and dispatches tool calls; never raises into the loop (§10). Args: registry: The registry to resolve tool calls against. deps: The run-scoped `RunDeps` passed to every handler. ```python theme={null} ToolRuntime(registry: ToolRegistry, deps: RunDeps) -> None ``` #### `ToolRuntime.execute(self, name: str, raw_input: dict) -> ToolResult` Resolve, validate, and run a tool call; errors return as a failed `ToolResult`. Args: name: The registered name of the tool to invoke. raw\_input: The unvalidated call arguments, validated against the tool's input model. Returns: The handler's `ToolResult`, or a failed one for an unknown name, invalid input, or a handler that raised (isolated so a buggy tool never crashes the run). ## Functions ### `admits_transient_edit(task_kind: str, tool: ToolDefinition) -> bool` Whether `tool` is admitted as a transient edit in an investigate task (ADR-0005). Investigation sometimes *instruments*: add a debug print, run, observe, revert. Tier-1 mutation is therefore legal in `investigate` tasks — the enforcement point is the verifier's unchanged net-zero-diff contract (`no_unintended_diff`: the tree must match the pinned baseline at verification), detection where prevention used to be. An explicit rule, distinct from `is_edit_intent`, so the edit-intent phase bootstrap stays edit-kinds-only and investigate's phase flow is unchanged. Args: task\_kind: The task's kind; only `investigate` rides this rule. tool: The resolved tool definition. Returns: True when `tool` is the mutating tier (1) and the task is an investigation. ### `is_edit_intent(task_kind: str, tool: ToolDefinition) -> bool` Whether `tool` is the model's edit intent: the mutating tier on an edit-shaped task. Deliberately edit-kinds-only: an investigate task's transient edits (ADR-0005) are admitted by `admits_transient_edit` instead, so they never ride this bootstrap and never advance the phase. Args: task\_kind: The task's kind (only `edit`/`test_only` permit mutation, §7). tool: The resolved tool definition. Returns: True when `tool` is the mutating tool (tier 1) and the task kind permits edits. ### `phase_admits_tool(phase: str, task_kind: str, tool: ToolDefinition) -> bool` Whether `tool` may run *and* be advertised in `phase` for `task_kind` (§2.6). The single source of truth shared by the runner's gate (what may execute) and the `ContextBuilder` (what the model is told it may call) — keeping them in lockstep so the model never loops blind on a tool the runner would have admitted. True when the tool is active in the phase, or it is the edit-intent tool reachable from `investigating` via the bootstrap exception, or it is a transient edit in an investigate task (ADR-0005). Args: phase: The current control phase. task\_kind: The task's kind, gating the edit-intent bootstrap and the transient-edit rule. tool: The resolved tool definition. Returns: True if `phase` is in the tool's phases, or `tool` is an edit-intent tool on an edit-shaped task (the bootstrap that surfaces `str_replace` from `investigating`), or `tool` is tier-1 on an investigate task (transient instrumentation, ADR-0005). The escalation tool is the one *narrowing* rule: investigate-only (ADR-0048). # avatar.tools.commands Source: https://codexceed.mintlify.app/api-reference/tools-commands Command tools: run\_tests, run\_linter (§10, tier 2). These let the model *investigate* by running the configured test/lint commands. They are complementary to — never a substitute for — the harness-owned verifier, which runs its own command to set the success outcome (§5). A command that runs and reports failures is DATA (`success=True`, failures in `content`); only a command that could not run (timeout, target not found) is a failed `ToolResult`. ## Classes ### `RunCommandInput` Input for `run_command`: one project command (run as an argv, no shell metacharacters). ```python theme={null} RunCommandInput(*, command: str) -> None ``` **Fields** | Field | Type | Required | | --------- | ----- | -------- | | `command` | `str` | yes | ### `RunLinterInput` Input for `run_linter`: none — it runs the configured lint command. ```python theme={null} RunLinterInput() -> None ``` ### `RunTestsInput` Input for `run_tests`: an optional target appended to the configured command. ```python theme={null} RunTestsInput(*, target: str | None = None) -> None ``` **Fields** | Field | Type | Required | | | -------- | ----- | -------- | -- | | `target` | \`str | None\` | no | # avatar.tools.edit Source: https://codexceed.mintlify.app/api-reference/tools-edit Editing tools: str\_replace + write\_file + delete\_file (§10, tier 1, ADR-0015). The mutating tools. `str_replace` makes a targeted, string-anchored edit to an existing file — `old_string` → `new_string`, no line numbers, no diff; a miss comes back as a model-correctable `ToolResult`, never an exception thrown at the loop (§10). `write_file` (ADR-0003 B) is the plain-content transport for creation and explicit whole-file rewrites, and `delete_file` removes one. Modification is anchored on the exact existing text (the read-before-edit staleness proof), which replaced the line-numbered unified diff models could not produce reliably (ADR-0015). ## Classes ### `DeleteFileInput` Input for `delete_file`: remove one existing workspace file (ADR-0015). ```python theme={null} DeleteFileInput(*, path: str) -> None ``` **Fields** | Field | Type | Required | | ------ | ----- | -------- | | `path` | `str` | yes | ### `StrReplaceInput` Input for `str_replace`: an exact-text find/replace in one existing file (ADR-0015). ```python theme={null} StrReplaceInput(*, path: str, old_string: str, new_string: str, replace_all: bool = False) -> None ``` **Fields** | Field | Type | Required | | ------------- | ------ | -------- | | `path` | `str` | yes | | `old_string` | `str` | yes | | `new_string` | `str` | yes | | `replace_all` | `bool` | no | ### `WriteFileInput` Input for `write_file`: create a file with full content (overwrite only if explicit). ```python theme={null} WriteFileInput(*, path: str, content: str, overwrite: bool = False) -> None ``` **Fields** | Field | Type | Required | | ----------- | ------ | -------- | | `path` | `str` | yes | | `content` | `str` | yes | | `overwrite` | `bool` | no | # avatar.tools.filesystem Source: https://codexceed.mintlify.app/api-reference/tools-filesystem Read-only filesystem tools: read\_file, list\_files (§10, tier 0). ## Classes ### `ListFilesInput` Input for `list_files`: a glob pattern (defaults to the whole tree). ```python theme={null} ListFilesInput(*, glob: str = '**/*') -> None ``` **Fields** | Field | Type | Required | | ------ | ----- | -------- | | `glob` | `str` | no | ### `ReadFileInput` Input for `read_file`: a workspace path and optional 1-indexed line range. `line_range` is a `list[int]` (`[start, end]`), not a `tuple` — a tuple renders to JSON Schema as `prefixItems` *without* an `items` key, which Gemini's request validator rejects (ADR-0019). A plain list emits a provider-agnostic `{"type": "array", "items": {"type": "integer"}}`; the exactly-two / ordering contract is enforced by the validator. ```python theme={null} ReadFileInput(*, path: str, line_range: list[int] | None = None) -> None ``` **Fields** | Field | Type | Required | | | ------------ | ------------ | -------- | -- | | `path` | `str` | yes | | | `line_range` | \`list\[int] | None\` | no | # avatar.tools.search Source: https://codexceed.mintlify.app/api-reference/tools-search Repository text search via ripgrep (§10, tier 0). ## Classes ### `SearchRepoInput` Input for `search_repo`: the ripgrep query pattern. ```python theme={null} SearchRepoInput(*, query: str) -> None ``` **Fields** | Field | Type | Required | | ------- | ----- | -------- | | `query` | `str` | yes | # avatar.tools.verification Source: https://codexceed.mintlify.app/api-reference/tools-verification Verification-contract tools: `declare_verification` (§10, ADR-0038). Greenfield edit tasks declare nothing the planner can detect or cite, so the model authors its own **real** verification contract — executing checks the harness still runs itself and grades on the real exit code (never self-certification, §5). The tool only *buffers* the declared checks onto `RunDeps`; the runner drains them into the frozen plan at the investigating→editing boundary (tools never mutate `TaskState`, §8). A non-vacuity guard rejects no-op commands so the declared contract can't be a bar the model trivially clears. ## Classes ### `AlterVerificationInput` Input for `alter_verification`: the replacement checks plus why the old ones are obsolete. ```python theme={null} AlterVerificationInput(*, checks: list[DeclaredCheckInput], rationale: str, change_kinds: list[Literal['code', 'content']] = ) -> None ``` **Fields** | Field | Type | Required | | -------------- | ---------------------------------- | -------- | | `checks` | `list[DeclaredCheckInput]` | yes | | `rationale` | `str` | yes | | `change_kinds` | `list[Literal['code', 'content']]` | no | ### `DeclareVerificationInput` Input for `declare_verification`: the checks that define 'done' for this greenfield task. ```python theme={null} DeclareVerificationInput(*, checks: list[DeclaredCheckInput], change_kinds: list[Literal['code', 'content']] = ) -> None ``` **Fields** | Field | Type | Required | | -------------- | ---------------------------------- | -------- | | `checks` | `list[DeclaredCheckInput]` | yes | | `change_kinds` | `list[Literal['code', 'content']]` | no | ### `DeclaredCheckInput` One check in a model-declared verification contract (ADR-0038). ```python theme={null} DeclaredCheckInput(*, command: str, kind: str = 'test') -> None ``` **Fields** | Field | Type | Required | | --------- | ----- | -------- | | `command` | `str` | yes | | `kind` | `str` | no | ### `SwitchToEditingInput` Input for `switch_to_editing`: why this investigation is actually a fix (ADR-0048). ```python theme={null} SwitchToEditingInput(*, reason: str) -> None ``` **Fields** | Field | Type | Required | | -------- | ----- | -------- | | `reason` | `str` | yes | # avatar.verifier Source: https://codexceed.mintlify.app/api-reference/verifier Verifier — proves completion via external evidence, never self-certification (§12). It runs **no model**: every check is a predicate over structured `TaskState` plus the workspace. For `edit`/`test_only` the *external signal* is the task's frozen **verification plan** (ADR-0007) — commands the harness resolved (config override → deterministic detection → cited LLM proposal) and froze onto `TaskState` before editing began. The verifier is a **pure executor** over that plan: zero language knowledge, it runs each frozen command itself via `ws.run` and reads the real exit code. The gate is harness-owned, so the model can never self-certify (§5). The three pass criteria (§12): no required check fails; no required check is skipped for a disallowed reason; at least one positive signal. When no plan was frozen (a direct library call), the config override tier alone is used as the plan — never a detected or invented default. ## Classes ### `Verifier` Disposes of a completion proposal via external evidence, never a model (§12). Args: config: The harness config; its `test_command`/`lint_command` override tier doubles as the plan when no frozen plan exists. `None` leaves both empty. ```python theme={null} Verifier(config: HarnessConfig | None = None) -> None ``` #### `Verifier.verify(self, state: TaskState, ws: Workspace) -> VerifierResult` Run the verification contract for the task's `task_kind` (§12). Args: state: The task state. ws: The run-scoped workspace. Returns: The disposition over the contract's checks. Raises: NotImplementedError: For an unrecognized `task_kind`. # avatar.workspace Source: https://codexceed.mintlify.app/api-reference/workspace Workspace — a tracked, path-confined handle to the repo (§8, §15). Tools touch the filesystem and run commands *only* through this handle, so a tool physically cannot reach outside the workspace root. It also owns the diff baseline: at construction it pins the current git HEAD, and `diff()` compares the working tree against that pinned baseline (not the git index), so the task's delta is well-defined and the harness never needs to commit (§15). ## Classes ### `AmbiguousMatchError` The `old` anchor matched more than once and `replace_all` was not set (ADR-0015). Args: path: The workspace-relative file the anchor matched in. count: How many times the anchor matched (>1); exposed as `.count` for the tool's message. ```python theme={null} AmbiguousMatchError(path: str, count: int) -> None ``` ### `CommandOutput` The captured result of one command run through the workspace. ```python theme={null} CommandOutput(command: str, stdout: str, stderr: str, exit_code: int | None, timed_out: bool = False) -> None ``` ### `DirtyWorkspaceError` The workspace has uncommitted changes at open and they were not acknowledged (§15). ### `EmptyAnchorError` `old` was empty — it would match between every character and rewrite the file (ADR-0015). Rejected at the `Workspace.replace` chokepoint so a direct SDK caller can't corrupt a file, not only the `str_replace` tool layer. ### `MatchNotFoundError` The `old` anchor was absent from the file — a stale or mistyped anchor (§10). ### `PatchError` A patch failed to apply cleanly (stale context) — model-correctable (§10). Application is all-or-nothing: when this is raised, nothing was written. ### `PathOutsideWorkspaceError` Raised when a requested path resolves outside the workspace root. ### `ReplaceError` A string-anchored replace could not be applied cleanly (ADR-0015) — model-correctable. All-or-nothing: when raised, the file is byte-for-byte unchanged. ### `SensitivePathError` Raised when a *resolved* path matches the sensitive-path denylist (§11, Phase 2.5). Enforced at the workspace chokepoint (not just the permission gate), so the check sees the symlink-resolved target — an innocuously-named symlink cannot launder a secret — and a non-gated caller still cannot read/patch it. ### `Workspace` A tracked, path-confined handle to the repo; tools reach the FS only here (§8, §15). Args: root: The workspace root all paths are confined to. allow\_dirty: When `True`, skip the clean-tree check at open (§15). sensitive\_path\_globs: The denylist refused on read/patch (resolved-path check, §11). Defaults to the built-in set (secure by default); the runner threads `HarnessConfig.sensitive_path_globs` through to match the permission gate. log\_path: The harness's own event-journal path (`HarnessConfig.log_path`), hidden from the agent's file tools so it can't list/read/search the harness's plumbing. sandbox: The execution strategy applied at the command seam (ADR-0042). Defaults to `NoSandbox` (the full inherited environment — today's behavior), so a bare `Workspace(root)` and every read-only inspection stay unchanged; the entry points that actually run model/verifier commands inject `make_sandbox(config.sandbox_mode)`. ```python theme={null} Workspace(root: Path | str, *, allow_dirty: bool = False, sensitive_path_globs: Sequence[str] | None = None, log_path: Path | str | None = None, sandbox: Sandbox | None = None) -> None ``` #### `Workspace.apply_patch(self, diff: str) -> list[str]` Apply a (possibly multi-file) unified diff atomically; return changed paths. An internal/SDK primitive — no longer a model-facing tool (the agent edits via `replace`/`str_replace`, ADR-0015), but retained for programmatic callers and for multi-file/creation diffs `replace` does not cover. Confinement first: every target path must resolve inside the root, else `PathOutsideWorkspaceError` (nothing written). Then a `git apply --check` dry run gates the real apply, so a stale diff raises `PatchError` and the workspace is left byte-for-byte unchanged — all-or-nothing (§10). Args: diff: The unified diff to apply. Returns: Sorted workspace-relative paths the diff changed. Raises: PatchError: When the diff names no targets or fails to apply cleanly. #### `Workspace.baseline_paths(self) -> list[str]` Workspace-relative paths tracked at the *pinned baseline* commit (empty when none). Lets the verifier tell a genuinely test-less repo (a legitimate no-tests skip) from one whose tests were suppressed after the baseline (ADR-0042, Threat A): if the baseline HAD tests but a frozen `kind="test"` check now collects none, that exit-5 is laundering, not absence. Reads the baseline tree via `git ls-tree` — not the working tree — so a just-deleted/emptied test is still seen as having existed. Empty when the workspace is not a git repo (no baseline was pinned). Returns: The sorted workspace-relative POSIX paths tracked at the pinned baseline. #### `Workspace.contains(self, rel_path: str) -> bool` Whether `rel_path` resolves inside the root, without raising (for the gate). Args: rel\_path: The path to test. Returns: `True` if `rel_path` resolves inside the root, else `False`. #### `Workspace.diff(self) -> str` Working-tree delta vs. the pinned baseline (empty when no baseline). Returns: The unified diff against the pinned baseline, empty when none. #### `Workspace.is_ignored(self, relpath: str) -> bool` Whether `relpath` (workspace-relative POSIX) is hidden harness plumbing. Args: relpath: A workspace-relative POSIX path. Returns: `True` for the harness's own journal directory (any file under it) or, when the journal sits directly in the root, its file and `latest.jsonl` pointer. #### `Workspace.list_files(self, glob: str) -> list[str]` Return workspace-relative paths of files matching `glob`, sorted. A glob that matches a *directory* expands to the files under it (recursively), so `list_files("pkg")` lists `pkg/`'s contents rather than silently returning nothing — the dogfood gap where `rich*` matched a dir and was dropped. Hidden (dot-prefixed) entries are skipped by wildcards, mirroring ripgrep's default — pathlib's glob matches them, so a venv or `.git` inside the workspace otherwise turns `*`/`**/*` into thousands of junk paths. A pattern that *names* a dot-prefixed segment (e.g. `.github/**/*`) opts into hidden, the same way an explicit path does for rg; `read_file` on an explicit hidden path always works. Args: glob: The glob pattern to match against the root. Returns: Sorted workspace-relative paths of the matching files (dir matches expanded; hidden entries skipped unless the pattern names a dot-prefixed segment). #### `Workspace.read(self, path: str, line_range: tuple[int, int] | None = None) -> str` Read a workspace file, optionally a 1-indexed inclusive line range. Args: path: The file to read. line\_range: A 1-indexed inclusive `(start, end)` range, or `None` for all. Returns: The file text, sliced to `line_range` when given. Raises: FileNotFoundError: When the path is the harness's own (hidden) journal. #### `Workspace.remove(self, path: str) -> str` Delete a workspace file and stage the removal so `diff()` reflects it (ADR-0015). The deletion counterpart to `write_file`/`replace` — the capability the removed `apply_patch` `/dev/null` hunk used to carry. Staging the removal makes a baseline-tracked file show as deleted in `git diff <baseline>` and lets a transiently-created file (staged by `write_file`) net back to zero. Confinement and the sensitive-path denylist apply at this chokepoint like every other write. Args: path: The workspace-relative file to delete. Returns: The workspace-relative path deleted. Raises: FileNotFoundError: When the target does not exist (nothing to delete). #### `Workspace.replace(self, path: str, old: str, new: str, *, replace_all: bool = False) -> str` Swap an exact string in a file; the anchor proves non-staleness (§10, ADR-0015). The string-anchored modification primitive that supersedes `apply_patch`'s diff costume: `old` must match the *current* file text exactly — read-before-edit (§10) enforced by the anchor, not by line arithmetic. It must match once, unless `replace_all`. All-or-nothing: a rejected match leaves the file byte-for-byte unchanged. Confinement, the sensitive-path denylist, and staging apply at this chokepoint like every other write; the resulting diff is *derived* by `diff()`, never authored by the model (§5). Args: path: The workspace-relative file to edit. old: The exact existing text to find (the anchor); must be non-empty. new: The replacement text; an empty string deletes the matched span. replace\_all: Replace every occurrence instead of requiring a unique match. Returns: The workspace-relative path edited. Raises: FileNotFoundError: When the target file does not exist (create with `write_file`). EmptyAnchorError: When `old` is empty (would rewrite the whole file). MatchNotFoundError: When `old` is absent (stale or mistyped anchor). AmbiguousMatchError: When `old` matches more than once and `replace_all` is unset. Note: The input contract lives HERE, at the chokepoint, not only in the `str_replace` tool — a direct SDK caller gets the same guarantees. Confinement and the sensitive-path denylist apply via `_resolve`/`_assert_not_sensitive`. An empty `new` is deliberately allowed (span deletion); an empty `old` is rejected. #### `Workspace.run(self, command: str, timeout: int | None = None) -> CommandOutput` Run `command` confined to the root, capturing output; bounded by `timeout`. Never raises into the loop (ADR-0007 robustness floor): a missing binary, an empty command, or an unparseable command line all come back as a failed `CommandOutput` (shell convention: exit 127 = command not found) so the verifier and tools see a legible failure, not a `FileNotFoundError`. Args: command: The shell-style command to run. timeout: Seconds before the command is killed, or `None` for no bound. Returns: The captured stdout, stderr, exit code, and timeout flag. #### `Workspace.stage(self, paths: list[str]) -> None` `git add` the given paths so they enter the pinned-HEAD `diff` (§15). Mirrors `apply_patch`'s `git apply --index`: a command-created (untracked) file is invisible to `git diff <baseline>` until staged, so staging is what makes codegen/migration output show up in the diff, artifact, and verifier. No-op when not a git repo or given no paths. Args: paths: Workspace-relative paths to stage. #### `Workspace.status_paths(self) -> set[str]` Workspace-relative paths git currently sees as changed *or untracked*. Used to attribute a command's side effects: snapshot before and after a `run` and the delta is what the command touched. Includes untracked files (unlike `diff`, which is `git diff <baseline>`), so command-created files are visible. Empty when not a git repo. Returns: The set of paths from `git status --porcelain` (rename → its new path). #### `Workspace.write_file(self, path: str, content: str, *, overwrite: bool = False) -> str` Create a file with `content` and stage it; refuse an existing target by default. The plain-content twin of `apply_patch` for the no-anchor case (ADR-0003 B): creation needs no diff costume, while *modification* stays diff-anchored — without `overwrite`, an existing target raises `FileExistsError` so the clean-apply staleness invariant can't be bypassed casually. Confinement and the sensitive-path denylist apply at this chokepoint like every other access, and the new file is staged so it appears in `diff()` (matching `apply_patch --index` and the `run_command` mutation capture). Args: path: The workspace-relative file to create. content: The full file content to write. overwrite: `True` to deliberately replace an existing file. Returns: The workspace-relative path written. Raises: FileExistsError: When the target exists and `overwrite` is `False`. ## Functions ### `path_is_sensitive(rel_path: str, globs: Sequence[str]) -> bool` Whether `rel_path` matches any denylist glob (§11, Phase 2.5). A pattern *without* a slash matches any single path component (gitignore-style "match anywhere" — so `.env` hits `a/b/.env` and `.ssh` hits `.ssh/id_rsa`). A pattern *with* a slash is matched against the whole relative path. Matching is **case-insensitive** (ADR-0021): a case-insensitive filesystem (macOS APFS, Windows) resolves `CREDENTIALS` to the same file as `credentials`, so a case-sensitive denylist is bypassable by varying case. We case-fold both sides via `fnmatchcase` on lowered strings — deterministic across platforms (unlike `fnmatch`, whose case behavior rides `os.path.normcase`, a no-op on macOS) and conservative (over-matching a denylist is the safe direction). Args: rel\_path: The workspace-relative path to test. globs: The denylist patterns. Returns: `True` if any pattern matches, else `False`. # Architecture Source: https://codexceed.mintlify.app/concepts/architecture The component map — runner, state, per-turn workers, gates, and observation. A central **`AgentRunner`** owns the loop and the `TaskState`. Everything else is a stateless worker or a passive store. ```mermaid theme={null} flowchart TD CLI["CLI intake: goal + constraints"] --> R R["AgentRunner: loop, budgets, phases"] R -.->|repair loop| R R --> TS["TaskState: source of truth"] R --> CB["ContextBuilder: compact per-turn packet"] R --> MC["ModelClient: constrained decision protocol"] R --> TR["ToolRuntime: typed, phase-gated tools"] R --> V["Verifier: external-evidence gate"] R -->|emits| EM["Emitter: observation only"] EM --> EL["EventLog (JSONL)"] EM --> UI["CLI display"] TR --> WS["Workspace: path-confined, diff, command exec"] V --> WS ``` ## The per-turn cycle 1. **`ContextBuilder`** assembles a compact packet from `TaskState` — goal, phase, recent evidence, and the tools allowed for the current phase. The model discovers the repo incrementally through tools, never receiving the whole repo. 2. **`ModelClient`** returns exactly one validated `ModelDecision` (`tool_call`, `final_answer`, or `ask_user`). Malformed output is fed back as a recoverable error, never executed. 3. **`ToolRuntime`** validates and dispatches tool calls. Tools reach the filesystem only through the **`Workspace`**, which confines paths to the repo root and pins a diff baseline. 4. The **`AgentRunner`** applies the result to `TaskState` and emits observation events. A `final_answer` triggers the **`Verifier`**. ## Verification, not self-certification The `Verifier` runs **no model**. Every check is a predicate over structured `TaskState` plus the workspace diff, selected by `task_kind` (`edit` / `investigate` / `test_only`) so the right contract applies. It passes only on *required* checks with positive external signal — never vacuously on skipped checks. This page is a synthesized overview. For the full design rationale, each component's docstring (see the **API reference**) cites the relevant design section. # Core invariants Source: https://codexceed.mintlify.app/concepts/invariants The five load-bearing rules that thread through every component. Five invariants hold the design together. Violating one quietly breaks the harness, so they are stated up front and enforced throughout. The model's message history is *derived* from `TaskState` each turn — not the other way around. State is explicit, structured (pydantic), and append-mostly, so a run is inspectable and replayable. Tools are pure-ish: they receive a run-scoped `RunDeps`, touch the filesystem or run commands only through the `Workspace` handle, and return a `ToolResult`. They never mutate `TaskState`. The runner applies results *after* logging and permission checks. A final answer marks a task *ready for verification* — it never ends the run. Only the harness-owned `Verifier` sets `outcome = "success"`, and only on positive external evidence. The model never self-certifies. The permission gate is an awaited control hook that can block or redirect the loop. The event emitter is observation-only: synchronous, fire-and-forget, and unable to alter control flow. Never route control through the emitter. Work happens on a tracked, path-confined `Workspace`; every edit is an inspectable diff. An append-only JSONL event log gives replay, debugging, and eval data for free. ## Two axes, kept separate | Axis | Values | Role | | --------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------- | | `phase` | `investigating → editing → verifying` | **Control** — gates which tools are active and how context is built. | | `outcome` | `success` / `incomplete` / `blocked` / `failed` (or `None` while live) | **Terminal result** — what the run ultimately reports. | Conflating them is what leaves "ran out of budget" and "failed verification" ambiguous. Keeping them separate keeps the loop's exit conditions legible. # Quickstart Source: https://codexceed.mintlify.app/guides/quickstart From install to a verifier-checked answer in five minutes. ## Install avatar-harness is a Python 3.12+ library. It is **not yet published to PyPI** — install it from source: The repo is a uv workspace with two packages: the SDK (`avatar-harness/`, import `avatar`) and the cockpit (`jo-cli/`, import `jo`). ```bash theme={null} # From a clone (editable): git clone https://github.com/codexceed/avatar-harness.git cd avatar-harness pip install -e './avatar-harness[openai]' # the SDK + default OpenAI-compatible model client pip install -e ./jo-cli # + the Textual cockpit, the `jo` command (optional) # Or straight from GitHub into your own project's environment # (uses your git credentials while the repo is private): pip install 'avatar-harness[openai] @ git+https://github.com/codexceed/avatar-harness#subdirectory=avatar-harness' # uv equivalent: uv add 'avatar-harness[openai] @ git+https://github.com/codexceed/avatar-harness#subdirectory=avatar-harness' ``` Always **quote the extras** — zsh otherwise glob-expands the brackets and fails with `no matches found: avatar-harness[openai]`. The core imports without either extra — `openai` is needed only to use the default `OpenAIModelClient` (you can inject any `ModelClient` instead), and `textual` only for the bundled TUI. Two external binaries must be on `PATH`: * **ripgrep** (`rg`) — the `search_repo` tool shells out to it. * **git** — patches apply via `git apply`; the workspace pins `HEAD` as its diff baseline. Working on this repo itself, `make install` (a thin `uv sync`) sets up everything including the dev tools. ## Configure Configuration comes from `AVATAR_*` environment variables or a local `.env`. The only required value is an API key: ```bash theme={null} # .env AVATAR_API_KEY=sk-or-... # required; falls back to OPENAI_API_KEY AVATAR_MODEL=openai/gpt-4o-mini # default; any model your endpoint serves AVATAR_BASE_URL=https://openrouter.ai/api/v1 # default (OpenRouter); change for OpenAI/local AVATAR_WORKSPACE_ROOT=. # the repo the agent operates on (default: cwd) ``` The full knob list (budgets, verification commands, the sensitive-path denylist, context-compaction budgets) is in the [SDK guide](/guides/sdk#configuration). ## First run — the CLI ```bash theme={null} avatar "where does the agent loop terminate, and what sets outcome=success?" ``` The CLI streams a timestamped event trajectory as the agent works, then prints a `Status:` line and the cited answer. The whole run is journaled to `events/.jsonl` for replay and debugging. The workspace refuses to start on a tree with uncommitted **tracked** changes (the diff baseline must be well-defined — untracked files are fine). Commit, stash, or pass `--allow-dirty`. ## First run — the library The same engine, importable. Five lines to a verifier-checked answer: ```python theme={null} from avatar import Harness harness = Harness.from_env() # AVATAR_* / .env; no key needed to construct state = harness.run("explain how str_replace anchors edits") print(state.outcome, "\n", state.final_answer) ``` `state.outcome` is `"success"` only when the harness-owned verifier found positive external evidence — the model never self-certifies. The other terminal outcomes are `incomplete` (budget exhausted), `blocked` (needs human input), and `failed` (a completion claim that wouldn't verify). Every collaborator is an injectable seam: ```python theme={null} from avatar import Harness, HarnessConfig harness = Harness( # test_command is the always-wins override; left unset, the harness detects # the repo's declared contract (CI workflows, manifests, Makefile — ADR-0007) config=HarnessConfig(workspace_root="./repo", test_command="python -m pytest -q"), model=my_model_client, # any ModelClient — no `openai` extra needed ) state = harness.run("fix the failing auth test", task_kind="edit") ``` The `task_kind` (`investigate` / `edit` / `test_only`) selects the **verification contract** — what evidence is required before the run may be called a success. `investigate` is the default: a grounded answer with the tree netting to zero diff vs the pinned baseline at verification (transient instrumentation is legal mid-task, but must be reverted — ADR-0005). ## Where to go next The full surface: the two-plane `Session`, typed events, multi-turn `ReplSession`, and every configuration knob. A step-by-step tutorial: your own streaming, approval-answering agent in about 90 lines. # SDK guide Source: https://codexceed.mintlify.app/guides/sdk 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. 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: | 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. # avatar-harness Source: https://codexceed.mintlify.app/index A bounded, verifiable loop around an LLM — a coding-agent harness, not a chat app. ## What this is **avatar-harness** is the runtime *around* a language model that turns a natural-language task into a bounded, verifiable engineering loop. The defining inversion versus a chat app: > The model **proposes** actions; the harness **owns** execution, state, > permissions, logging, and verification. The loop terminates on **external > verification**, not on a text reply from the model. ## How the loop works Each turn, the harness assembles a compact context packet from `TaskState`, asks the model for exactly one validated decision (a tool call, a final answer, or a question), executes it through a path-confined `Workspace`, records the result, and applies it back to state. A proposed "final answer" is not the end — it is a *proposal* the `Verifier` disposes of using external evidence (tests, lint, diff). Only the harness marks a task `success`, and only on positive signal. ## Where to go next Install to a verifier-checked answer in five minutes. The curated surface: Harness, the two-plane Session, typed events, config. A streaming, approval-answering agent of your own in \~90 lines. The five load-bearing rules that thread through every component. The component map: runner, state, tools, verifier, events. The **API reference** tab is generated directly from the codebase's docstrings — it is always in sync with the source. # Tutorial: build a terminal agent Source: https://codexceed.mintlify.app/tutorials/terminal-agent A bare-bones streaming, approval-answering terminal agent on the harness SDK — about 90 lines. The bundled Textual cockpit is just one consumer of the SDK. This tutorial builds another from scratch: a single-file terminal agent that streams the agent's activity, answers approval prompts on stdin, cancels gracefully on Ctrl-C, and leaves a replayable journal. Everything it uses is on the exported surface — no internals. It comes in two parts: a four-line **one-shot** agent to ground the core ideas (Part 1), then a **streaming** agent built one capability at a time (Part 2). The two are separate programs — Part 1 is the black-box version, Part 2 shows the work. The full listing of the streaming agent is at the [end](#the-full-listing). ## Setup Not yet on PyPI — install from source (quote the extras; zsh glob-expands the brackets otherwise): ```bash theme={null} # from a clone: pip install -e './avatar-harness[openai]' # or straight from GitHub (uses your git credentials while the repo is private): pip install 'avatar-harness[openai] @ git+https://github.com/codexceed/avatar-harness#subdirectory=avatar-harness' ``` Plus `rg` and `git` on `PATH`, and a `.env` (or exported variables): ```bash theme={null} AVATAR_API_KEY=sk-or-... AVATAR_MODEL=openai/gpt-4o-mini AVATAR_BASE_URL=https://openrouter.ai/api/v1 AVATAR_WORKSPACE_ROOT=/path/to/the/repo/to/work/on # the verification contract for this tutorial's greenfield workspace (see note) AVATAR_TEST_COMMAND="python -m pytest -q" AVATAR_LINT_COMMAND="python -m ruff check" ``` The harness resolves *what proves the work* per session (ADR-0007): an explicit `AVATAR_TEST_COMMAND`/`AVATAR_LINT_COMMAND` always wins; otherwise it detects the repo's declared contract (CI workflows, manifests, Makefile targets). This tutorial's workspace starts **empty** — nothing to detect — so declare the contract explicitly as above, or the `edit` run will (correctly) fail verification with "no verification contract discovered". The workspace must be a git repo with no uncommitted *tracked* changes — the harness pins `HEAD` as its diff baseline (see the [quickstart](/guides/quickstart#first-run--the-cli) for the `allow_dirty` escape hatch). ## Part 1 — the one-shot agent The smallest possible consumer — no streaming, no UI. Complete and runnable exactly as written: ```python theme={null} from avatar import Harness state = Harness.from_env().run("create a basic chatbot in chatbot.py", task_kind="edit") print(state.outcome, "\n", state.final_answer) ``` That one line *builds* something: the agent inspects the repo, writes `chatbot.py`, and the harness-owned verifier confirms the result before `run()` returns. The diff lands in your workspace — `git diff` shows it. Two things to absorb: * **`outcome` is verifier-owned.** `"success"` means the verifier found positive external evidence — for an `edit`, a real diff that clears a check (lint or tests) run over it, never the model's say-so. "Done" is only ever a *proposal* the verifier disposes of. * **`task_kind` selects the verification contract.** `edit` (used here) requires a verifiable diff with a passing check over it; `investigate` is grounded Q\&A that must leave the tree with zero net diff (transient instrumentation is legal but must be reverted — ADR-0005); `test_only` requires new tests that pass. ## Part 2 — the streaming agent Part 1 is a whole agent, but a black box: you get the verdict, never the work. The rest of the tutorial builds a **separate** program — a single-file terminal agent that shows each step as it happens. From here the code accretes toward the [full listing](#the-full-listing); each section adds one capability. ### Stream the run `harness.session(...)` returns a not-yet-started `Session`: `run()` drives the engine while `events()` yields a typed stream you render. This split is the **two-plane contract** — the stream is observation-only and can never alter the run. Put the render loop in its own coroutine, and a `main()` that wires the goal and lets the two run together: ```python theme={null} import asyncio from avatar import ( AgentEnd, Harness, ModelDecisionEvent, PhaseChanged, ToolEnd, ToolStart, VerificationEnd, ) async def render(session) -> None: # the observation plane async for event in session.events(): if isinstance(event, ModelDecisionEvent): if event.thought: # often empty under native tool-calling print(f" · {event.thought}") elif isinstance(event, ToolStart): print(f" → {event.tool} {event.input}") elif isinstance(event, ToolEnd): print(f" {'✓' if event.success else '✗'} {event.summary}") elif isinstance(event, PhaseChanged): print(f"— phase: {event.old} → {event.new}") elif isinstance(event, VerificationEnd): print(f"— verification {'✓' if event.passed else '✗'} {event.summary}") elif isinstance(event, AgentEnd): print(f"\noutcome: {event.outcome}") async def main() -> int: harness = Harness.from_env() goal, kind = "create a basic chatbot in chatbot.py", "edit" session = harness.session(goal, task_kind=kind) run_task = asyncio.create_task(session.run()) # drive the engine await render(session) # observe it state = await run_task return 0 if state.outcome == "success" else 1 ``` `events()` subscribes **eagerly** — at call time, not first iteration — so creating the run task before the loop loses nothing. The stream ends by itself after `agent_end`. The full event catalog is in the [SDK guide](/guides/sdk#the-event-catalog). Each section below extends this skeleton — a new branch in `render`, or a few lines in `main` — and the imports grow to match (the [full listing](#the-full-listing) has the complete set). ### Answer approvals Gated calls (tier-3 tools like `run_command`, or a sensitive-path hit) are *announced* on the event stream as `ApprovalRequested`, and the run blocks until you decide via the **control plane**. Add one more branch to `render`'s loop (and `ApprovalRequested` to the imports): ```python theme={null} elif isinstance(event, ApprovalRequested): answer = await asyncio.to_thread( # input() would block the event loop input, f" ⚠ {event.tool}: {event.reason}\n allow? [y]es / [a]lways / [d]eny " ) choice = answer.strip().lower()[:1] await session.resolve_approval( event.approval_id, allow=choice in ("y", "a"), remember=choice == "a" ) ``` * The event announces; only `resolve_approval` decides — nothing on the stream can approve anything. * `remember=True` (the "always" choice) stores a session-scoped `ApprovalGrant`: later calls sharing the same program (`argv[0]`) auto-allow, observable as `ApprovalResolved(via="grant")`. * A blocking `input()` would stall the very event loop the run lives on — `asyncio.to_thread` keeps it breathing. Creating `chatbot.py` won't prompt: the edit tools (`str_replace`/`write_file`/`delete_file`) are tier 1 and auto-allow once their target paths validate inside the workspace — the verifier judges the resulting diff. To see an approval fire, give a goal that needs a command (a build step, a migration), or target a denylisted path and watch it get refused. ### Cancel gracefully on Ctrl-C `session.cancel()` trips the run's cancellation token; the loop observes it at the next checkpoint, records the interruption as feedback, and still settles to a terminal state — so `await run_task` always returns a `TaskState`, never hangs. Register the handler in `main`, before starting the run task: ```python theme={null} asyncio.get_running_loop().add_signal_handler( signal.SIGINT, lambda: asyncio.ensure_future(session.cancel("user interrupt")) ) ``` Any in-flight approval is denied on cancel, so a run blocked on the gate can't hang either. (`add_signal_handler` is POSIX-only; on Windows, catch `KeyboardInterrupt` around `asyncio.run` instead and accept the harder stop.) ### Journal the run Pass a `JsonlEventJournal` to `harness.session(...)` in `main` and every event is committed to disk **before** any subscriber renders it — a crash loses nothing: ```python theme={null} journal = JsonlEventJournal(Path("events") / "mini-agent.jsonl") session = harness.session(goal, task_kind=kind, journal=journal) ``` Reload it later with `load_events(path)` — the same typed objects, for replay, debugging, or evals. ### The final report The terminal `TaskState` carries everything a report needs — swap the bare `return` in `main` for a report: ```python theme={null} if state.final_answer: print(f"\n{state.final_answer}") if state.files_modified: print("\nchanged files:", ", ".join(sorted(state.files_modified))) if state.verifier_results: print("verification:", state.verifier_results[-1].summary) return 0 if state.outcome == "success" else 1 ``` For the richer formatted artifact the batch CLI prints (status + files + commands + diff reference), see `avatar.artifact.ArtifactManager` — not yet on the exported surface, so treat it as internal for now. ## The full listing Verified end-to-end (the live run below, lightly elided): ```python expandable theme={null} """mini-agent: a bare-bones terminal agent on avatar-harness (~90 lines).""" import asyncio import signal import sys from pathlib import Path from avatar import ( AgentEnd, ApprovalRequested, Harness, JsonlEventJournal, ModelDecisionEvent, PhaseChanged, ToolEnd, ToolStart, VerificationEnd, ) async def render(session) -> None: """The observation plane: draw each typed event; answer approvals on stdin.""" async for event in session.events(): if isinstance(event, ModelDecisionEvent): if event.thought: # often empty under native tool-calling print(f" · {event.thought}") elif isinstance(event, ToolStart): print(f" → {event.tool} {event.input}") elif isinstance(event, ToolEnd): print(f" {'✓' if event.success else '✗'} {event.summary}") elif isinstance(event, PhaseChanged): print(f"— phase: {event.old} → {event.new}") elif isinstance(event, VerificationEnd): print(f"— verification {'✓' if event.passed else '✗'} {event.summary}") elif isinstance(event, AgentEnd): print(f"\noutcome: {event.outcome}") elif isinstance(event, ApprovalRequested): answer = await asyncio.to_thread( input, f" ⚠ {event.tool}: {event.reason}\n allow? [y]es / [a]lways / [d]eny " ) choice = answer.strip().lower()[:1] await session.resolve_approval( event.approval_id, allow=choice in ("y", "a"), remember=choice == "a" ) async def main(goal: str, kind: str) -> int: harness = Harness.from_env() journal = JsonlEventJournal(Path("events") / "mini-agent.jsonl") session = harness.session(goal, task_kind=kind, journal=journal) asyncio.get_running_loop().add_signal_handler( signal.SIGINT, lambda: asyncio.ensure_future(session.cancel("user interrupt")) ) run_task = asyncio.create_task(session.run()) await render(session) state = await run_task if state.final_answer: print(f"\n{state.final_answer}") if state.files_modified: print("\nchanged files:", ", ".join(sorted(state.files_modified))) if state.verifier_results: print("verification:", state.verifier_results[-1].summary) return 0 if state.outcome == "success" else 1 if __name__ == "__main__": goal = " ".join(sys.argv[1:]) or "create a basic chatbot in chatbot.py" raise SystemExit(asyncio.run(main(goal, "edit"))) ``` ```text theme={null} $ python mini_agent.py "create a basic chatbot in chatbot.py" → list_files {'glob': '**/*.py'} ✓ 0 file(s) matching '**/*.py' — phase: investigating → editing → write_file {'path': 'chatbot.py', 'content': '"""A basic command-line chatbot.\n…'} ✓ wrote chatbot.py → read_file {'path': 'chatbot.py'} ✓ read chatbot.py — phase: editing → verifying — verification ✓ verification passed outcome: success Implemented `chatbot.py` as a basic command-line chatbot: - `generate_response(user_message)` — rule-based replies (greetings, "how are you", name, help) with a fallback - `main()` — an `input("You: ")` loop that prints replies and exits on quit/exit/bye […] changed files: chatbot.py verification: verification passed ``` The agent looked for existing Python, found none, wrote `chatbot.py`, then the verifier ran the declared contract: a diff is present and `python -m ruff check` passes over the new file (`python -m pytest -q` skipped — no tests collected), so the run settles to `success`. (Your output will differ — it's a live model run; the shape is what matters.) ## Sidebar — run it offline `ModelClient` is a one-method protocol, so the whole tutorial runs without an API key by injecting a scripted model — exactly how the project's own test suite drives the engine. An `edit` task needs a clean git workspace (the verifier diffs against `HEAD`), so the snippet spins up a throwaway repo: ```python theme={null} import subprocess import tempfile from pathlib import Path from avatar import FinalAnswer, Harness, HarnessConfig, ModelClient, ModelDecision, ToolCall CHATBOT = '''"""A basic command-line echo chatbot.""" def main() -> None: print("bot: hi! type ctrl-d to quit.") while True: try: user = input("you: ") except EOFError: print("bot: bye!") break print(f"bot: you said {user!r}") if __name__ == "__main__": main() ''' 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 root = Path(tempfile.mkdtemp()) subprocess.run(["git", "init", "-q"], cwd=root, check=True) subprocess.run(["git", "commit", "-q", "--allow-empty", "-m", "root"], cwd=root, check=True) harness = Harness( # a greenfield repo declares its contract explicitly (the override tier, ADR-0007) config=HarnessConfig( workspace_root=str(root), test_command="python -m pytest -q", lint_command="python -m ruff check", ), model=ScriptedModel([ ModelDecision(thought_summary="create the chatbot", action=ToolCall(name="write_file", input={"path": "chatbot.py", "content": CHATBOT})), ModelDecision(thought_summary="report what was built", action=FinalAnswer(answer="Created chatbot.py — a basic CLI echo loop.")), ]), ) state = harness.run("create a basic chatbot in chatbot.py", task_kind="edit") ``` Swap this `harness` into `main()` and the same flow runs offline. It exercises the real contract end-to-end: `write_file` creates a genuine diff, the declared test command skips with no tests collected (a tolerated skip), and the declared lint command passes over the new file — the positive signal that settles verification to `success`. Real loop, real verifier, zero network. Keep escape sequences out of the embedded `CHATBOT` source — a literal `\n` inside the triple-quoted string would become a real newline in the written file and break its string literals (and fail lint). The echo loop above sticks to plain strings for exactly this reason. ## Where to go from here * **Multi-turn:** wrap goals in a `ReplSession` — history seeding, grant carry-over, `/`-meta commands, plan mode. See the [SDK guide](/guides/sdk#multi-turn-conversations--replsession). * **Your own tools / model / verifier:** every collaborator is a `Harness` constructor kwarg. * **A real UI:** the cockpit (the separate `jo-cli` package, the `jo` command) is this same surface rendered with Textual — `jo-cli/jo/app.py` is the reference implementation, and `jo-cli/jo/cli.py` shows how a consumer ships its own entry point over the core.