- Rust 100%
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`. |
||
|---|---|---|
| crates | ||
| plan | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| README.md | ||
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 optionalrequest_iddeduplication) store their prompt parts, so apps can redraw file and paste chips after reload;run_start/run_endmarkers and finished assistant messages are persisted before tool calls execute. - Reopen recovery: When a session with an unfinished run reopens under
RecoveryPolicy::Resume,Replay::Safetools (read,grep,find,ask) re-run automatically andReplay::Nevertools (bash,write,edit) receive an interrupted result pointing to any spooled output. Five consecutive resumes without progress park the run asParked. - Live attach:
session.attach(from_seq)returns missed durable entries sincefrom_seq, aLiveSnapshotof any run or maintenance in progress (partial reply, running tools, pendingaskquestions, queued submissions), and a live broadcast receiver. - Branching & forking:
session.navigate(target, options)moves the persistedleafpointer inside the tree (optionally summarizing the abandoned branch into abranch_summaryentry);session.fork(entry_id)clones the path into a new session file and returns the forked user message's parts asFork::editablefor 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-runCommandPolicy) 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 byrequest_id./modeland/thinkingrun alongside a turn;/compact,/shake, and/navigatereturnError::Busyduring one; maintenance (compaction, shake, branch summary) blocks every command. An unknown/wordstays ordinary prompt text. - Prompt parts:
prompt,steer, andfollow_uptakeimpl Into<PromptInput>: a plain string, or a list ofPromptParts (typed text, pastes,@file references that give the model only the path, and handed-over content that axo saves under the session'slocal://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::Elidereplaces 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'sartifacts/directory;ImagesandThinkingstrip 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,axochooses 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"),axorequests the checkpoint summary inside the live conversation withToolChoice::Noneso 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 (withinkeepRecentTokens), 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 emptySYSTEM.md,WORK.md, orPROSE.mddisables that section. - Ordered
contextFiles& nested loading: Startup loads~/.axo/AGENTS.md(and~/.agents/AGENTS.mdwhenhomeAgentsDir: true), then walks from the git root tocwdloading enabled names fromsettings.contextFiles(AGENTS.md,AGENTS.local.md,CLAUDE.md,CLAUDE.local.md,.agents/AGENTS.md,.axo/AGENTS.md). Touching a file in a subdirectory withread,write, oreditattaches 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(throughprompt,steer, orfollow_up). Skills appear insession.commands()with their optionalargument-hintfrontmatter; a skill named like a built-in command gets a warning diagnostic, is left out of the catalog, and/nameruns 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.devShelland!commanddefaultModel). Dev shells remain disabled even if enabled in user settings."trust"permits all project settings, including!commanddefaultModel, and is required for dev shells regardless of which settings file enablesbash.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
- Implementation Reference (
crates/axo/docs/): Start atcrates/axo/docs/README.mdforconfiguration.md,prompts-and-context.md,sessions-and-compaction.md, andextending.md. - Planning & Research (
plan/): Seeplan/design.md,plan/plan.md,plan/pi/, andplan/research/.
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.