Axo harness
Find a file
Chris Toph e6518e8339 feat(session): add structured prompt parts and user commands
Introduce `PromptPart` and `PromptInput` for structured user submissions
(text, files, directories, image/audio attachments, content parts) and
implement user command routing (`commands.rs`) for built-in operations
(`/compact`, `/shake`, `/model`, `/thinking`, `/navigate`) and skills.
Save attachments and inline content to `local://` files, update durable
submissions, and expose the command catalog on `Session`.
2026-11-03 12:42:07 -05:00
crates feat(session): add structured prompt parts and user commands 2026-11-03 12:42:07 -05:00
plan docs: update Sworm to Axome; renamed 2026-11-02 23:17:20 -05:00
.gitignore feat(axo-ai): message model, streaming adapters, model catalog and auth 2026-10-22 04:00:30 -04:00
AGENTS.md feat(session): add structured prompt parts and user commands 2026-11-03 12:42:07 -05:00
Cargo.lock fix: address M1–M4 adversarial review findings 2026-10-22 13:07:06 -04:00
Cargo.toml fix: address M1–M4 adversarial review findings 2026-10-22 13:07:06 -04:00
flake.lock feat(axo-ai): message model, streaming adapters, model catalog and auth 2026-10-22 04:00:30 -04:00
flake.nix feat(axo-ai): message model, streaming adapters, model catalog and auth 2026-10-22 04:00:30 -04:00
LICENSE docs: add axo design, pi port notes and research 2026-10-22 03:10:44 -04:00
README.md feat(session): add structured prompt parts and user commands 2026-11-03 12:42:07 -05:00

axo

An embeddable, headless coding-agent harness core in Rust.

axo is built to be linked directly into Rust hosts such as Axome, running many durable agent sessions in a single process (~92 MiB PSS live) without a sidecar runtime or terminal UI.

It ports pi v1.3.0's core architecture—a sectioned system prompt stored in the transcript, delta-only streaming events, a tool-calling agent loop, and an append-only JSONL session tree—while adding crash recovery, recoverable /shake and cache-preserving compaction, cached Nix flake dev shells, and granular project access controls.


Crates & Capabilities

The workspace is split into three layered crates so hosts only recompile what changes and non-coding agents can use the loop and provider layers independently:

Crate Purpose Highlights
axo-ai LLM message model, streaming adapters, model catalog, and auth Anthropic Messages, OpenAI Chat Completions, OpenAI Responses, and OpenAI Codex Responses (auth.json OAuth + refresh locking); models.dev catalog with extendedContext price-tier caps; delta Accumulator; transient retry and context-overflow classification
axo-agent Generic tool-calling agent loop Parallel and sequential Tool execution, Replay crash-safety contracts (Safe vs Never), LoopHooks, steering and follow-up queues, CancelReason (User vs Shutdown), and split event delivery (ordered Persist subscriber + non-blocking broadcast)
axo Sessions, compaction, context assembly, config, and built-in tools Durable JsonlStore with incremental Projection and context_edits, single-writer OS locks, crash recovery, attach catch-up snapshots, recoverable /shake and live/separate compaction summaries, DevShellEnv Nix/direnv capture, and the built-in tool set

Built-in tools (crates/axo/src/tools)

Tool Replay Description
read Safe Reads UTF-8 text files (with offset/limit pagination) and images (png, jpeg, gif, webp, bmp), recording read hashes in the session's file ledger and loading nested directory AGENTS.md files on demand
write Never Writes new files or complete rewrites, guarded by the read ledger when overwriting existing files
edit Never Performs exact or whitespace/quote-fuzzy multi-hunk replacements against files recorded in the read ledger, rejecting stale external modifications
bash Never Executes commands under the session's ExecEnv (host, direnv, or cached Nix flake dev shell) in an isolated process group with a pipe-backed crash guard, head+tail inline truncation, and full output spooled under the session directory
grep Safe In-process regex search built on ignore and grep-searcher
find Safe In-process gitignore-aware file discovery built on ignore and globset
ask Safe Emits SessionEvent::QuestionPending with structured single- or multi-select questions and waits for any attached client to answer via Session::answer (registered when SessionOptions::ask_enabled is true)

Key Features

Durable sessions & crash recovery

Every session is an append-only JSONL entry tree (~/.axo/sessions/--<encoded-cwd>--/<timestamp>_<uuidv7>.jsonl) protected by an OS file lock (File::try_lock) so a second writer gets Error::SessionBusy and a crashed process releases the lock automatically.

  • Durable intent: Submissions (prompt, steer, follow_up, with optional request_id deduplication) store their prompt parts, so apps can redraw file and paste chips after reload; run_start / run_end markers and finished assistant messages are persisted before tool calls execute.
  • Reopen recovery: When a session with an unfinished run reopens under RecoveryPolicy::Resume, Replay::Safe tools (read, grep, find, ask) re-run automatically and Replay::Never tools (bash, write, edit) receive an interrupted result pointing to any spooled output. Five consecutive resumes without progress park the run as Parked.
  • Live attach: session.attach(from_seq) returns missed durable entries since from_seq, a LiveSnapshot of any run or maintenance in progress (partial reply, running tools, pending ask questions, queued submissions), and a live broadcast receiver.
  • Branching & forking: session.navigate(target, options) moves the persisted leaf pointer inside the tree (optionally summarizing the abandoned branch into a branch_summary entry); session.fork(entry_id) clones the path into a new session file and returns the forked user message's parts as Fork::editable for the client to edit and resubmit.

Commands & prompt parts

axo owns the user command catalog and the shape of a user message, so hosts only render them:

  • Built-in commands: /compact, /shake <mode>, /model <provider/id>, /thinking <level>, and /navigate <entry> [summarize]. session.commands() lists them (each with a JSON Schema for its arguments and a mid-run CommandPolicy) followed by the session's skills; session.run_command(command) runs one directly. Typed as prompt text, a built-in runs instead of being submitted: it is never saved as a submission or sent as a user prompt, and is not deduplicated by request_id. /model and /thinking run alongside a turn; /compact, /shake, and /navigate return Error::Busy during one; maintenance (compaction, shake, branch summary) blocks every command. An unknown /word stays ordinary prompt text.
  • Prompt parts: prompt, steer, and follow_up take impl Into<PromptInput>: a plain string, or a list of PromptParts (typed text, pastes, @ file references that give the model only the path, and handed-over content that axo saves under the session's local:// folder; saved images also reach the model as pixels).

Cache-aware compaction & /shake

axo manages context pressure through append-only context_edit and compaction entries without mutating earlier history in place:

  • Manual /shake (session.shake(mode) or the /shake <mode> command): Shrinks context without a model call. ShakeMode::Elide replaces older tool results and large fenced/XML blocks outside the newest ~4k tokens with [shaken ~N tokens; full text: artifact://<n>] pointing to recoverable files in the session's artifacts/ directory; Images and Thinking strip older media or reasoning blocks. Undo is an ordinary branch navigation to the entry before the shake.
  • One action per automatic pass: When context tokens cross W - reserve, axo chooses either recoverable clearing (eliding tool results older than the newest 40k tokens of tool output when that frees at least 20k tokens below the trigger) or summarization, never both in the same pass.
  • Live then separate summaries: By default (compaction.summaryRequest: "live"), axo requests the checkpoint summary inside the live conversation with ToolChoice::None so the entire history hits the prompt cache, falling back to a standalone tool-free summarizer request ("separate") if the response is rejected or triggered by context overflow. Recent user messages (within keepRecentTokens), tracked read/modified file lists, and the session file path carry over automatically.

Sectioned system prompt, context files & skills

The system prompt lives in the transcript as named <section> blocks (system, docs, tools, schemes, work, prose, project_context, skills, cwd, dev_shell, date, plus host sections). Mid-session updates emit a diff SystemMessage containing only changed sections.

  • Replaceable prompt files: SYSTEM.md, WORK.md, PROSE.md, and the five summarizer prompts can be overridden in ~/.axo/prompts/ or <cwd>/.axo/prompts/. Writing an empty SYSTEM.md, WORK.md, or PROSE.md disables that section.
  • Ordered contextFiles & nested loading: Startup loads ~/.axo/AGENTS.md (and ~/.agents/AGENTS.md when homeAgentsDir: true), then walks from the git root to cwd loading enabled names from settings.contextFiles (AGENTS.md, AGENTS.local.md, CLAUDE.md, CLAUDE.local.md, .agents/AGENTS.md, .axo/AGENTS.md). Touching a file in a subdirectory with read, write, or edit attaches any unseen nested context files onto that tool result without invalidating the system prompt cache.
  • Skills (SKILL.md): Discovered from project .agents/skills/ and user ~/.axo/skills/. Listed in <available_skills> for on-demand model reads, or expanded inline when the user types /name args (through prompt, steer, or follow_up). Skills appear in session.commands() with their optional argument-hint frontmatter; a skill named like a built-in command gets a warning diagnostic, is left out of the catalog, and /name runs the built-in.

Nix flake & direnv dev shells (DevShellEnv)

When bash.devShell is set to "nix" or "direnv" in a trusted project, axo captures the project's environment once (via nix print-dev-env or an approved .envrc), caches Nix captures under ~/.axo/cache/devshell/ with a GC root, and runs every subsequent bash command directly against the captured environment (~3 ms overhead per command instead of ~460 ms for nix develop -c). Captures invalidate automatically when flake.nix, flake.lock, tracked .nix files, or bash.devShellWatch globs change.

Project access levels (ignore | read | trust)

Configured exclusively in the user's ~/.axo/settings.jsonc (a project cannot elevate its own access):

{
  "defaultModel": "anthropic/claude-sonnet-4-5",
  "bash": {
    "devShell": "nix"
  },
  "project": {
    "access": "read",
    "paths": {
      "/repo/**": "trust",
      "~/scratch/**": "ignore"
    }
  }
}
  • "ignore" loads nothing from the repository.
  • "read" (default) loads markdown context files, skills, prompt overrides, and safe settings, while dropping code-running project settings (bash.devShell and !command defaultModel). Dev shells remain disabled even if enabled in user settings.
  • "trust" permits all project settings, including !command defaultModel, and is required for dev shells regardless of which settings file enables bash.devShell.

Embedding in Rust

use std::path::PathBuf;
use axo::{Axo, PromptPart, RecoveryPolicy, SessionOptions};

let axo = Axo::builder().build().await?;

let session = axo
    .create_session(SessionOptions {
        cwd: PathBuf::from("/repo/rust/axo"),
        model: Some("anthropic/claude-sonnet-4-5".into()),
        ask_enabled: true,
        recovery: RecoveryPolicy::Resume,
        ..SessionOptions::default()
    })
    .await?;

let mut events = session.attach(None).await.events;
let outcome = session
    .prompt("Run the test suite and summarize any failures.")
    .await?;

// Structured parts: a file reference plus handed-over image bytes saved under `local://`.
let outcome = session
    .prompt(vec![
        PromptPart::Text { text: "Compare ".into() },
        PromptPart::File { path: "docs/design.png".into() },
        PromptPart::Text { text: " with this screenshot: ".into() },
        PromptPart::Content { name: "shot.png".into(), bytes: std::fs::read("/tmp/shot.png")? },
    ])
    .await?;

Layout

axo/
├── crates/
│   ├── axo-ai/                     # Message model, streaming adapters, model catalog, OAuth & credentials
│   ├── axo-agent/                  # Tool-calling loop, hooks, queues, cancellation, split event delivery
│   └── axo/                        # Axo & Session APIs, JsonlStore, compaction, context, DevShellEnv, tools
│       ├── prompts/                # Bundled SYSTEM.md, PROSE.md, and summarizer markdown prompts
│       ├── examples/               # `drive` (CLI session driver) and `memory` (PSS benchmark)
│       └── docs/                   # Bundled implementation reference for the `<docs>` system-prompt section
│           ├── README.md           # Documentation index
│           ├── configuration.md    # ~/.axo/ and .axo/ layout, settings.jsonc, models.jsonc, project access
│           ├── prompts-and-context.md     # System prompt sections, prompt overrides, contextFiles, skills, prompt parts
│           ├── sessions-and-compaction.md # JsonlStore, projection, recovery, attach, /shake, and compaction
│           └── extending.md        # Declarative files, Rust embedding (Session, commands), and extension traits
└── plan/                           # Architecture design, pi port studies, and background research
    ├── design.md                   # Goals, non-goals, architecture, and settled core decisions
    ├── plan.md                     # Active planning, next tracks, and open design discussions
    ├── pi/                         # Source studies of pi v1.3.0
    └── research/                   # Background studies and memory/compaction measurements

Development

Enter the Nix development shell for the Rust toolchain (cargo, rustc, clippy, rustfmt, rust-analyzer):

nix develop
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --check

Drive a live or resumed session from the command line with the drive example:

cargo run -p axo --example drive -- --cwd . --model zebes/qwen3.8-27b "Inspect @crates/axo/src/lib.rs"
cargo run -p axo --example drive -- --cwd . --attach /tmp/shot.png "What is in this image?"
cargo run -p axo --example drive -- --cwd . --commands
cargo run -p axo --example drive -- --resume ~/.axo/sessions/.../session.jsonl "/model openai-codex/gpt-6-luna"
cargo run -p axo --example drive -- --resume ~/.axo/sessions/.../session.jsonl "/shake elide"

Measure idle and mid-turn proportional set size (PSS) across N concurrent sessions in one process:

cargo run --release -p axo --example memory -- --sessions 3

Documentation

License

MIT. axo ports ideas and prompt text from pi and oh-my-pi, both MIT-licensed; code and prompts adapted from them retain their copyright notices.