- Quick start
- What it is
- How the agent loop works
- Features
- Skills
- Configuring operant
- CLI reference
- Build prerequisites
- Development
- Licensing
Your terminal-native AI agent. Persistent memory, 70+ JSON-schema tools, skills, MCP, and messaging channels — built in Rust, run from your shell, and fully under your control.
git clone https://github.com/ishan-parihar/operant.git && cd operant
./scripts/install.sh # release build → /usr/local/bin/operant
# or: scripts/build.sh --release
operant setup # interactive wizard: provider, memory, TTS, gateway
operant chat # start chatting (TUI)One-shot runs need no TUI at all:
operant run --query "Audit the browser stack: check the sourcehound web tools and the CDP browser"Operant is a production-grade ReAct agent runtime written in Rust. It replaces the "script per task" pattern with one agent that can think, use tools, and remember — over a long-lived terminal session or in scriptable one-shot runs.
Why it is different:
- A real agentic loop, not a chat wrapper — think → act → observe with a JSON-schema tool registry, automatic memory-context injection, provider fallbacks, and self-healing retries.
- Memory that stays in-process — the default
memory-wireprovider is an embedded engine (hybrid BM25 + token-overlap search) called directly over its syncretain/recallAPI: no server to spawn, no port to wait on, and acatch_unwindboundary that turns a panic into a memory miss instead of a dead turn. Context is prefetched before every turn and written back after it. - Skills you can point at a directory — import an entire skill tree (with recursive security scan), bundle multiple skills, autoload at boot, and let the agent curate new ones. Vendor skill packs can self-update too: the ponytail integration installs upstream's tag tarball under versioned
~/.operant/integrations/dirs (guard-scanned, never in your own~/.operant/skills/), and its skills become invocable like your own (/skill ponytail-review) —operant update --integrationsrefreshes it. - Only enabled, functional tools reach the model — the registry serves the intersection of registered ∩ available ∩ not-disabled, so the agent never sees tools that can't run.
- One stealth browser for everything — the
sourcehoundbinary serves both the web tools (web_search/web_scrape/web_extract/web_crawl) and the browser (cloakctl.navigate/read/act) over its own MCP server on stdio. No API keys; if the binary is missing, every web tool degrades to a helpful error. - Local-first — no telemetry, no account required; bring any OpenAI-compatible endpoint or a local model.
- Persistent Kernel (opt-in) — a stateful Python kernel whose variables survive across turns, a continual-harness store (session-local + global prompt/subagent lessons with snapshot rollback), and a tool bridge that lets one kernel cell call allowlisted tools in a loop (RLM-lite). See docs/library/kernel.md. Disabled by default;
git submodule update --init --recursive+[tools.kernel] enabled = trueto adopt.
The agent loop is a classic ReAct cycle, executed with bounded iterations and tool timeouts:
user ──▶ model ──▶ act (tool registry) ──▶ observe ──▶ next step
▲ │
└──────── memory context ◀───────┘
- Registry — every tool exposes a runtime JSON schema;
get_schemas()feeds exactly what is enabled. - Memory — pre-turn
prefetch()injects<memory_context>; post-turnqueue_prefetch()warms the provider; compression hooks fire on long sessions. - Resilience — fallback model chains, rate-limit buckets with exponential backoff, and tool call healing (
max_healing_attempts).
operant
├── crates/
│ ├── operant-core agent loop · tool registry · memory provider · config
│ ├── operant-cli TUI (ratatui) · commands · app adapter
│ ├── operant-tools built-in tool implementations
│ ├── operant-plugins WASM plugin bridge
│ ├── operant-config config schema · validation · defaults
│ ├── operant-harness unified harness (persistence seam + config)
│ └── operant-api · operant-os (8 crates total)
The workspace is self-contained; dead crates from earlier rounds were deleted, not disabled (see AGENTS.md for the history).
| Capability | Implementation |
|---|---|
| Memory | memory-wire in-process hybrid semantic memory (BM25 + token-overlap), panic-guarded — the one engine (legacy MEMORY.md/USER.md import once on upgrade) |
| Tools | 70+ JSON-schema tools: fs, git, web (sourcehound search/scrape/extract/crawl), browser (CDP), shell, code, http, memory, skills, cron, kanban, process, notes, checkpoints |
| Browser | Stealth sourcehound — the engine's own profile cookie jar and DevTools endpoint, driven over MCP and shared with the web tools |
| Skills | Directory import with recursive security scan · bundles · autoload · curator |
| Postures | Self-updating prompt packs (content integrations): ponytail — /skill ponytail-review on invoke, or [agent.ponytail] inject = "always_on" to ride the frozen prefix as a <ponytail> block · /ponytail [lite|full|ultra|off] flips it live; OFF by default |
| Models | Any OpenAI-compatible endpoint (base_url), local llama.cpp, Ollama; fallback chains + token-bucket rate limiting |
| MCP | Native client (stdio + HTTP, deferred loading) and server (operant mcp serve — stdio: initialize → tools/list → tools/call); reconnect materializes tools mid-session |
| Channels | Telegram · Discord · Slack · WhatsApp · email · SMS · webhooks via the gateway (7 platforms) |
| Autonomy | operant autonomous — a self-directed dev loop over TODO.md with test-command guardrails |
| Org governance | Per-employee permission scoping — seat policies (yolo/standard/scoped/lockdown) with deny/allow globs, plus per-seat budget overrides. An interactive approval resolves an in-flight ask; the grant ledger, the escalation queue and the worklog store live in Holon, not here |
| Plugins | WASM plugin tools · Hooks — a lifecycle hook registry (agent_start, agent_end, turn_start, turn_end) plus the operator's own shell commands (operant hooks add --event agent_end -- <cmd>) |
| Interface | ratatui TUI · interactive chat · scriptable run · one-shot test |
Skills are markdown instruction packs — the same mechanism hermes-agent uses — that the agent loads and injects into its context on demand. They teach operant how to do things (debugging protocols, git workflows, security methodology) so it behaves like an experienced operator rather than a generic model. Invoke one in the TUI with /skill <name>; the agent can also call the skill tool itself mid-loop.
SOPs are the operator's own playbooks for recurring tasks: TOML files in ~/.operant/sops/ (operant sop list shows them). The agent reaches them through the sop tool — list the playbooks, read one — so a procedure you wrote down is one the harness can actually follow.
Operant ships a categorized pool in the repo (skills/<category>/<skill>/) and installs it flat into ~/.operant/skills/ — one directory per skill, containing a required SKILL.md (YAML frontmatter: name, description, metadata.operant.tags … plus optional references/, scripts/, examples/) whose instructions are injected verbatim on invoke. 107 skills in the repo pool.
| Path | What it does |
|---|---|
./scripts/install.sh |
Builds the binary and seeds ~/.operant/skills from the repo pool (offline, idempotent) |
| First run | If the skills dir is empty, the binary auto-seeds from the bundled pool (OPERANT_BUNDLED_SKILLS_DIR override, then <repo>/skills, then <exe>/../skills) |
operant skills seed [--source DIR] [--force] |
Manual re-seed; --force overwrites local edits |
Location overrides (the binary honors these too):
export HERMES_HOME=~/.operant # home root (skills default under it)
export HERMES_SKILLS_DIR=/custom/skills # exact skills dir (config [skills] root_dir)
export OPERANT_BUNDLED_SKILLS_DIR=/path/to/pool # where seeding reads fromoperant skills list # installed skills (+ /name for the TUI)
operant skills install ./foo # import a skill dir or URL (recursive security scan)
operant skills bundle # combine skills into one
operant skills audit # scan for unsafe patterns (skills_guard)
operant curator # agent-curated skill lifecycle (archive/backup/restore)Config lives at ~/.operant/operant.toml (secrets in ~/.operant/.env). Start from the annotated reference:
cp operant.example.toml ~/.operant/operant.toml
operant doctor # validate config + dependencies
operant status # system overviewHighlights:
[client] # any OpenAI-compatible endpoint
base_url = "https://api.openai.com/v1"
[agent]
model = "gpt-4"
fallback_models = ["gpt-4o-mini"] # tried in order when the primary fails
max_iterations = 20
show_reasoning = true # TUI: thinking stays until your next prompt; /thinking-display full|current|off
context_window = 128000
# Context caps — BOTH axes bind and the SMALLER one wins; the absolute axis is OFF by default (0).
context_soft_cap_tokens = 0 # ...or an absolute ceiling: compact at the turn boundary
context_soft_cap_percent = 0.80 # ...or at 80% of the window, whichever is smaller
context_hard_cap_tokens = 0 # ...or an absolute ceiling: never send a request over this
context_hard_cap_percent = 0.95 # ...or over 95% of the window, whichever is smaller
[memory]
provider = "memory-wire" # the only engine; other names warn + are ignored
[browser]
provider = "sourcehound" # the only browser engine; other names warn + are ignored
[skills]
autoload = trueSee operant.example.toml for the full reference — every section is annotated.
The agent keeps its context under two limits, and both axes count: an
absolute token limit and a percentage of context_window. Whichever is
smaller binds first.
- The soft cap (350k tokens / 80% by default) is the turn-boundary trigger: crossing it compacts the history before the next request goes out.
- The hard cap (400k tokens / 95% by default) is the line no request
crosses: a turn that grows past it mid-stream — a big tool result, a long
answer — compacts the request list before the next provider call. A
context that cannot be compacted under the cap fails with a typed
ContextExhaustedinstead of being sent and rejected by the provider.
With the defaults a 1M-token window compacts at 350k and never exceeds 400k;
a 128k window keeps the old 80%/95% behaviour (102k / 121k). Set an axis to
0 to disable it. The compaction rewrites the request, never the stored
transcript — a resumed session still has its whole history.
The token count the caps act on is what the provider actually charges: the
assistant's reasoning is not counted, because no provider bills it against
the window (measured on the live endpoint: 18 kB of reasoning_content cost
15 prompt tokens). Counting it made the agent compact requests that were
nowhere near the cap.
[compression] turns on the evidence layer: a tool result is compressed
by its SHAPE, not by its size. A grep result is grouped by file, a JSON API
body becomes a table, a source file has its comments stripped, a fetch has
its HTML boilerplate removed — instead of every one of them receiving the
same head/tail cut.
- It runs after secret redaction and before the spend guard, so the guard measures the cost that shaping could not remove.
- Only
successfulresults are compressed: an error reaches the model verbatim, because the detail is the point. - Three guarantees: never worse (a result never grows), fail-open (a
method that panics is caught, the payload untouched), and recoverable (a
lossy method only runs when the full text is stored, and the compressed
result names the key — resolved by the
evidence_recalltool). min_sizeis in bytes (512 by default); a smaller result is left alone.shellnames who owns shell output:aft(the default — AFT owns the output of its own bash tool, so the layer skips that one result and compresses operant's own shell tools:terminal,code_execution,process),native(the layer compresses every shell payload itself), orrtk(thertkbinary executes the terminal tool's commands, so the layer leaves the terminal tool's output alone).- A family's
enabledflag, when present, decides for that family — it is not limited by the master switch, so one family can be turned on or off without a rebuild. - A family can also switch individual methods off:
methodslists the method names to drop for that family, so one aggressive filter goes without swapping the whole chain. The names are the methods' own —toon,cap,code-filter,code-filter-aggressive,ansi,dedupand the rest;operant.example.tomlcarries the full list. The deny-list is config-only:operant doctorprints each family's on/off/inherit state and its chain, never the deny-list.
It is off by default and behaviour-neutral until you set
enabled = true. Measure before you tune:
operant doctor --tools-tokens ranks your tool families by model-visible
tokens. See operant.example.toml for the full commented schema.
Database growth: every tool result whose model-facing copy differs from
the tool's own output now stores BOTH, whether or not [compression] is on —
measured on a live install, 47.3 MB of tool content in 42 689 rows becomes
~94.7 MB, so a 147.6 MB database grows to roughly 195 MB for the same history.
A turn that hits its iteration budget, the wall clock or the seat cap ends with a toolless wrap-up — one last request asking the model to summarise what it did. If that wrap-up produces a usable summary, the operator gets the partial answer.
If it produces nothing usable (empty, or a reasoning-leak artifact the quality gate rejects), the turn does not simply stop: the core issues an auto-poke — a bounded synthetic continuation that gives the turn one more pass with its tools, bypassing the limit that just stopped it. The poke is bounded three ways: at most two per turn, each granting three iterations past the limit; only when there is work to continue (an open todo list, or a tool call that succeeded); and never after a non-retryable provider error. A poke shows in the transcript as "Auto-poke", not as a message you typed.
- Process environment —
OPENAI_BASE_URL,HERMES_MODELand theOPERANT_*aliases override the file on every load.operant config showlists every key that came from the environment and the loader warns once per key, so an export left over from another tool cannot win silently. ~/.operant/.env— credential pins written by/connect, re-read before every gateway turn and TUI turn. A project-local.envis read only when the home file is absent; when both exist the loader warns once, naming the file it ignores.~/.operant/operant.toml— the file everything else falls back to.
Two traps worth knowing: ~/.operant/config.toml is read by no binary path (the loader warns when it sits beside operant.toml), and proxy environment variables (HTTP(S)_PROXY, ALL_PROXY) never redirect provider traffic.
Every write that changes operant.toml — config set, channel add/remove, config migrate --apply, the setup wizard, a plugin toggle — saves the previous bytes first, into backups/config/ beside the file. The five most recent copies are kept; a write rejected by the loader leaves nothing behind, so what is listed is always a version that actually existed.
operant config backups list # newest first
operant config backups restore <path-from-list> # validated, then swapped inA restore is itself undoable: the file being replaced is saved as a copy before it goes, and the copy being restored is re-parsed through the normal loader first, so a truncated snapshot can never become the live config. Snapshots are owner-only, because the config may hold a provider key.
One switch worth knowing: operant checks GitHub once per session for a newer aft/sourcehound build, in the background. OPERANT_NO_AUTO_UPDATE=1 turns that off — set it on a machine that must not reach GitHub on its own.
[tui]
terminal_scroll_mode = trueRenders in your terminal's main buffer with a small live region at the bottom instead of the alternate screen, so the terminal owns the history:
- Scroll with the wheel,
Shift+PgUp/Shift+PgDn, or tmux copy-mode (prefix+[). - Select and copy with the terminal's own selection — Operant never captures the mouse in this mode, so drag-select, double-click and your terminal's copy shortcut work across the whole session.
- Full-screen views (
/rewind, settings, pickers) still take the alternate screen while open and hand the scrollback back when closed. - Default is off (alt-screen app mode, with in-app mouse scroll/selection) until the live terminal sweep (tmux, kitty, ghostty, alacritty) is signed off.
In the default app mode the transcript scrolls with the keyboard alone — no mouse click needed to "unlock" scrolling (that gate was removed in iter-1226; a keyboard-only session in tmux or a plain tty used to be unable to scroll at all):
| Key | Action |
|---|---|
PgUp / PgDn |
Page up / down (step = the visible transcript height, minus two rows) |
Ctrl+Up / Ctrl+Down |
Fine scroll, 3 rows |
Alt+Up / Alt+Down |
Fast scroll, 20 rows |
Shift+Home |
Jump to the top of the transcript |
Shift+End |
Jump to the tail and resume following new messages |
Alt+G |
Toggle a scroll bookmark and jump back to it (the bookmark moved from Ctrl+G, which now opens the external editor) |
Unmodified Home / End stay composer caret moves. The status line announces where you are ("Top of transcript.", "Latest — following new messages."). /hotkeys lists the full ladder.
| Key | Action |
|---|---|
Up / Down |
Step through previously sent prompts (single-line drafts; multi-line drafts move the caret). Slash commands never enter this ladder — it is prompts only |
Ctrl+R |
Search the full persisted history (prompts and commands) and paste the selected row into the composer |
F5 / Alt+R |
Retry the last failed turn — the failed prompt is restored to the composer for editing; nothing is resubmitted behind your back |
Ctrl+P |
File search — ripgrep the workspace for files/lines (also /search or /find) |
A recalled prompt never re-opens the slash-command popup, so Up/Down keep stepping history even when the recalled text starts with /. History persists in ~/.operant/history.jsonl across sessions.
| Command | What it does |
|---|---|
operant / operant chat |
Interactive chat (TUI) |
operant run --query "…" |
One-shot run (scriptable, --record-trajectory for replay) |
operant autonomous |
Self-directed development loop |
operant setup |
Interactive setup wizard |
operant tools list |
Inspect the enabled tool registry |
operant skills list / install / audit |
Skill management (install a directory or URL) |
operant mcp list / add / test / serve |
MCP servers; serve runs Operant as an MCP server over stdio; /mcp r in the TUI reconnects deferred servers |
operant memory query / export |
Search / export memory |
operant sessions list |
Session history (--interrupted filters to sessions an exit marker flagged) |
operant sessions resume |
Re-orient on a session (--last picks the interrupted one) |
operant model get / set |
Active model configuration |
operant cron / operant kanban |
Scheduled jobs / task boards |
operant gateway start |
Messaging gateway (telegram, discord, slack, whatsapp); /sessions browses transcripts on telegram + discord |
operant plugins list |
WASM plugins |
operant doctor / operant status |
Health checks and system overview |
operant update integrations |
Force-check sourcehound/aft updates (auto: first use per session, 6h throttle) |
operant debug memory |
This process's memory footprint: RSS, store size, thread count |
operant --help lists the full set (sessions, checkpoints, profiles, auth, completion, backups, SOPs, and more).
- Rust 1.89+ (edition 2024)
- 4 GB RAM (8 GB recommended)
- A model: any OpenAI-compatible endpoint, or a local llama.cpp / Ollama server
- Optional: the
sourcehoundbinary, for the web tools and the stealth browser (no API key; omit it and those tools report a clear error)
This repo is developed in a strict iteration model: one change = one
iteration = one commit pushed to origin/main, labeled <type>(iter-N).
Read AGENTS.md top-to-bottom before your first change — it is
the single source of truth for the protocol (iteration numbering, parallel-agent
coordination, deploy procedure).
source scripts/dev-env.sh # libclang / ONNX Runtime / bindgen env
# Scope every check to the crate you touched — a full workspace build is 10+ min:
./scripts/check.sh check -p operant-core --lib
cargo test -p operant-core --lib -- <filter>
cargo fmt --all
bash scripts/clippy-warning-gate.sh # incremental gate: fails only on NEW warnings
# Final verification before push (slow):
./scripts/check.sh check --workspace
./scripts/check.sh test --workspace
./scripts/self-test.sh # full pre-push sweepDevelopment conventions enforced by the gate: no .unwrap()/.expect() in
production code (justified sites carry #[expect(..., reason)]), typed errors
in library crates (thiserror-style; anyhow only in binaries), and scoped
compilation to keep iteration cycles fast.
The GitHub Actions quota for this repo is exhausted, so the local loop is the
gate — no workflow runs on a push or a tag. The commands above are what
verifies a change; scripts/self-test.sh runs the whole sweep.
Work is planned in dated docs (docs/plan-YYYY-MM-DD-*.md) with executed rows
marked in place, and every user-visible change lands in
docs/CHANGELOG.md in the same iteration.
Architecture, porting notes, and parity decisions live in AGENTS.md and docs/ (audits, BUGS, CHANGELOG, TODO). Which document owns which question is docs/INDEX.md; the map of the one-off audit and design pages is docs/PAGE-INDEX.md. The open defect ledger is docs/audit/2026-06-19-BUGS.md (the old root BUGS.md is docs/BUGS.md, now a tombstone), and the implementation-gap inventories are docs/REMAINING-GAPS.md and docs/agentic-os/REMAINING-GAPS.md.
MIT or Apache-2.0 — see LICENSE-MIT or LICENSE-APACHE.
Contributing: docs/CONTRIBUTING.md · Security: docs/SECURITY.md · Conduct: docs/CODE_OF_CONDUCT.md · Attribution: docs/ATTRIBUTION.md
If you find this project useful, consider supporting ongoing development:
Your support funds new features, releases, and infrastructure for the whole ecosystem.