Skip to content
ishan-pariharPublic

About

Your terminal-native AI agent. Persistent memory, 60+ JSON-schema tools, skills, MCP, and messaging channels — built in Rust, run from your shell, and fully under your control.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3,450 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Operant

On this page

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.

Operant hero


Quick start

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"

What it is

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-wire provider is an embedded engine (hybrid BM25 + token-overlap search) called directly over its sync retain/recall API: no server to spawn, no port to wait on, and a catch_unwind boundary 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 --integrations refreshes 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 sourcehound binary 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 = true to adopt.

How the agent loop works

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-turn queue_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).

Crate map

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).


Features

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

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.

Layout and format

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.

Seeding (fresh installs are agent-ready from scratch)

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 from

Managing skills

operant 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)

Configuring operant

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 overview

Highlights:

[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 = true

See operant.example.toml for the full reference — every section is annotated.

Context caps

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 ContextExhausted instead 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.

Tool-result compression (opt-in)

[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 successful results 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_recall tool).
  • min_size is in bytes (512 by default); a smaller result is left alone.
  • shell names 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), or rtk (the rtk binary executes the terminal tool's commands, so the layer leaves the terminal tool's output alone).
  • A family's enabled flag, 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: methods lists 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, dedup and the rest; operant.example.toml carries the full list. The deny-list is config-only: operant doctor prints 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.

When a turn runs out of room

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.

What wins: environment, .env, or operant.toml

  1. Process environment — OPENAI_BASE_URL, HERMES_MODEL and the OPERANT_* aliases override the file on every load. operant config show lists 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.
  2. ~/.operant/.env — credential pins written by /connect, re-read before every gateway turn and TUI turn. A project-local .env is read only when the home file is absent; when both exist the loader warns once, naming the file it ignores.
  3. ~/.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.

Undoing a bad config change

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 in

A 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.

Terminal scrollback mode (opt-in)

[tui]
terminal_scroll_mode = true

Renders 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.

Scrolling the transcript with the keyboard (app mode)

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.

Prompt history in the composer

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.


CLI reference

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).


Build prerequisites

  • 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 sourcehound binary, for the web tools and the stealth browser (no API key; omit it and those tools report a clear error)

Development

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 sweep

Development 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.


Licensing

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


☕ Support & Sponsorship

If you find this project useful, consider supporting ongoing development:

Sponsor Donate

Your support funds new features, releases, and infrastructure for the whole ecosystem.

About

Your terminal-native AI agent. Persistent memory, 60+ JSON-schema tools, skills, MCP, and messaging channels — built in Rust, run from your shell, and fully under your control.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages