A powerful, recursive, tool-equipped Command Line (CLI) AI Agent written in Rust, leveraging the Rig LLM framework and OpenRouter to enable interactive conversational sessions with local sqlite turn tracing, code execution, and sandboxed filesystem manipulation.
- System Overview & Goals
- Architectural Blueprint
- Key Technical Choices (Why Rust & These Crates?)
- File & Directory Structure
- Getting Started & Configuration
- Core Workflows
- Safety & Sandboxing Details
- Guide for AI Agents (How to Read & Extend)
The primary goal of rust-agent is to provide a local interactive terminal (REPL) where a user can converse with an advanced LLM (specifically DeepSeek V4 via OpenRouter by default) that has direct, programmatic access to its host machine under safe sandboxed boundaries.
The agent can:
- List, read, and write files in the current working directory (the directory from which you launch
rust-agent). - Run local system shell commands (e.g. running compilers, build scripts, tests).
- Template prompts using dynamic variable interpolation.
- Perform keyword, recent, or tool-specific searches on its own conversation history using an embedded SQL trace database.
- Hierarchical Memory: Retains active memory using a 16k token rolling conversation window (L1), summarizes evicted turns into a 4k token knowledge graph wiki in SQLite (L2), and archives full transcripts in the database (L3).
- Recursive delegation: Delegate sub-tasks to nested instances of itself (sub-agents) with similar tool sets.
All persistent data (database, rules, prompt templates) lives in ~/.rust-agent/ so the binary works identically from any directory once installed globally.
The project is written in idiomatic Rust and is structured into a main application module and several helper modules. Below is a high-level component diagram showing how data and control flows:
graph TD
User([User in REPL]) -->|User Prompt| Main[src/main.rs]
Main -->|Get Preambles & System Info| Rules[src/rules.rs]
Rules -->|Read OS Rules| RulesMD[(rules.md)]
Main -->|Initialize/Update Session & Turns| Trace[src/trace.rs]
Trace -->|Query / Insert Traces & Wiki| SQLite[(traces.db)]
Main -->|Create Agent| AgentBuilder[src/agent.rs]
AgentBuilder -->|Uses| Rig[Rig Framework]
AgentBuilder -->|Register| Tools[src/tools/mod.rs]
subgraph Tools Area
Tools --> FSTool[Filesystem Tool]
Tools --> ExecTool[Code Exec Tool]
Tools --> PromptTool[Prompt Template Tool]
Tools --> RecurTool[Recursive Agent Tool]
Tools --> SearchTool[Trace Search Tool]
end
FSTool -->|Sandbox check| Hooks[src/hooks.rs]
ExecTool -->|Safety check| Hooks
RecurTool -->|Spawn Sub-Agent| AgentBuilder
Main -->|Execute Turn| RigAgent[Rig Agent Instance]
RigAgent -->|Interventions & Formatting| CLI[src/cli.rs]
CLI -->|Intercepts Tracing Logs| User
To ensure optimal performance, reliability, and ease of deployment, we chose Rust. Below is the rationale for each major crate dependency in Cargo.toml:
rig(v0.39):- Why: Rig is a modern, lightweight, and highly ergonomic Rust framework for building LLM applications. Unlike python-based alternatives (like LangChain), Rig is compiled, strongly typed, and fits perfectly into Rust's async model. It simplifies the definition and registration of tools using standard traits, and manages provider communication cleanly.
tokio(v1 with full features):- Why: Rig operates asynchronously, sending HTTP requests to LLM APIs and running concurrent logic. Tokio is the de facto standard async runtime in Rust, providing a robust multi-threaded task scheduler, timers, and filesystem/network utilities.
rusqlite(v0.32 with bundled feature):- Why: Tracing is critical for LLM agents (for debugging, RAG, and self-inspection). SQLite is a zero-setup, serverless database engine. The
bundledfeature ensures SQLite compiles statically, eliminating external C-library dependencies.
- Why: Tracing is critical for LLM agents (for debugging, RAG, and self-inspection). SQLite is a zero-setup, serverless database engine. The
tracing&tracing-subscriber(v0.1 & v0.3):- Why: Rust's standard logging library. Rig emits diagnostic tracing span logs during tool executions. We implement a custom
tracing_subscriber::Layerto parse and format these events into beautiful, colorized terminal outputs in real-time, filtering out internal framework noise.
- Why: Rust's standard logging library. Rig emits diagnostic tracing span logs during tool executions. We implement a custom
serde&serde_json(v1):- Why: The standard serialization/deserialization framework in Rust. Crucial for converting tool arguments/results to JSON and serializing token usage stats.
thiserror&anyhow(v2 & v1):- Why: Standard error handling practices in Rust. We use
thiserrorfor writing structured, domain-specific enums in modules (like tool execution errors) andanyhowfor top-level application logic where error propagation with backtrace/context is desired.
- Why: Standard error handling practices in Rust. We use
dotenvy(v0.15):- Why: Safely extracts secrets (e.g.
OPENROUTER_API_KEY) from a local.envfile without committing credentials to source control.
- Why: Safely extracts secrets (e.g.
uuid(v1 with v4 feature):- Why: Generates cryptographically secure session IDs for session logging and tracing.
.
βββ Cargo.toml # Cargo package configuration and dependencies
βββ Cargo.lock # Pinned dependencies
βββ rules.md # Default safety rules (copied to ~/.rust-agent/ on first run)
βββ prompts/ # Default prompt templates (copied to ~/.rust-agent/prompts/ on first run)
β βββ code_review.md
β βββ debug.md
β βββ explain.md
βββ src/ # Rust source code
βββ main.rs # App entry point, first-run setup, REPL loop, turn persistence
βββ agent.rs # Agent builder: Rig config, model choice, tool registration
βββ cli.rs # Colorized console layer, banners, spinner, and tracing interceptor
βββ config.rs # Global config directory (~/.rust-agent/) and first-run initialization
βββ hooks.rs # Core safety filters for file manipulation and command executing
βββ rules.rs # Rule parser, environment analyzer, and preamble formatter
βββ trace.rs # TraceStore (SQLite tables, inserts, searches, wiki, embeddings)
βββ tools/ # Agent-usable tools implementing `rig::tool::Tool`
βββ mod.rs # Shared ToolContext for associating actions with database turns
βββ agent_tool.rs # Recursive sub-agent delegation tool
βββ code_exec.rs # PowerShell command executor tool
βββ filesystem.rs # PWD-sandboxed file reader/writer/lister tool
βββ prompt.rs # Lazy-loaded template loading and rendering tool
βββ trace_search.rs # LLM-facing interface to query SQLite traces (keyword, recent, by_tool, semantic)
~/.rust-agent/ # Global config directory (auto-created on first run)
βββ .env # Place your OPENROUTER_API_KEY here
βββ rules.md # Editable agent behaviour rules
βββ prompts/ # Editable prompt templates
β βββ code_review.md
β βββ debug.md
β βββ explain.md
βββ traces.db # SQLite database β all sessions, all projects
- Install Rust (which includes
cargo). - Obtain an API Key from OpenRouter.
Install the binary globally so you can use rust-agent from any directory:
cargo install --path .Then set your API key (pick one method):
Method 1 β Config file (created automatically on first run):
# After running rust-agent once, edit the generated file:
notepad $env:USERPROFILE\.rust-agent\.env
# Add this line:
# OPENROUTER_API_KEY=your-key-hereMethod 2 β System environment variable (persists across all terminals):
[System.Environment]::SetEnvironmentVariable("OPENROUTER_API_KEY", "your-key-here", "User")Now run from any directory:
rust-agentThe agent will operate on the current directory's files.
# Create a .env in the project root
copy NUL .env # then edit it
# Add: OPENROUTER_API_KEY=your-key-here
cargo run --releaseOnce your repo is public on GitHub:
cargo install --git https://github.com/YOUR_USERNAME/rust-agentType prompts in the REPL. Use /exit or Ctrl+D to end the session. End a line with \ for multiline input.
Located in main.rs, the REPL runs an async loop that:
- Flushes a prompt symbol (
>) to the terminal. - Accepts user input via stdin (supports multiline input with
\continuation). - Lines ending with
\are buffered for multiline input. - Inserts a placeholder turn record in the
turnstable in SQLite to retrieve a uniqueturn_id. - Stores the
turn_idinside a thread-safeToolContextmutex. - Invokes the Rig agent asynchronously with a 120-second timeout.
- The agent performs thinking, calls tools, and writes tool results back to SQLite via the
turn_idcontext. - Prints the final response to the user with a token usage summary.
- Resets the shared
turn_idin the context.
Implemented in agent_tool.rs, the agent tool allows the agent to break down a hard query by creating a sub-agent.
- Spawns a new Rig agent with identical toolkits and rules.
- Appends depth markers to the system prompt:
You are a sub-agent at recursion depth {n}. - Increments a shared atomic
depth_counter. - If the depth hits
MAX_DEPTH_WARNING(5), it issues a system warning log to prevent infinite loops. - When the sub-agent returns its result, the parent tool decrements the counter and passes the result back up.
Defined in trace.rs, three SQL tables exist:
sessions: Tracks session start, end, LLM model name, and the folder from which the session was launched (project_path).turns: Tracks individual user questions, LLM responses, timestamps, token usage, session references, and a 1024-dim vector embedding (viaqwen/qwen3-embedding-8b).tool_calls: Tracks what tools were called, their raw arguments, outputs, timestamps, and whether they succeeded.
All queries (search, recent history, tool history) are strictly isolated to the current project's directory (project_path) at the database query level to ensure security and clean contexts across different repositories.
After each turn, a background tokio task asynchronously generates a 1024-dim vector embedding of the prompt+response pair and stores it in the turns.embedding column. This enables the trace_search tool's semantic action to perform cosine-similarity search over past conversations without blocking the REPL response.
The REPL renders a structured, colorized UI using ANSI escape codes:
ββ You ββββββββββββββββββββββββββββββββββββββββββββββ β user prompt box
β <user input>
β°ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β <tool_name> <args_json> β tool call (parsed from Rig)
β³ <result_preview> β tool result (first line + line count)
β <tool> <inline progress> β tool progress (via interrupt_spinner)
β <tool> <inline progress>
ββ Agent ββββββββββββββββββββββββββββββββββββββββββββ β agent response box
β <agent output>
β°ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β³ <ms>ms Β· <in> in Β· <out> out β token/time stats
ββ background βββββββββββββββββββββββββββββββββββββββ β background tasks (queued)
β β³ embedding turn #N via qwen3-8bβ¦
β β embedded turn #N (1024 dims)
β°ββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Tool calls (
β): Parsed from Rig'stracing::info!("executed tool ...")events. Displayed inline between prompt and response viaCliLayer. - Tool progress: Tools with long-running steps (e.g. semantic search embedding) call
cli::interrupt_spinner()/cli::restore_spinner()to print progress inline during execution. - Background tasks: Emitted via
tracing::info!(target: "bg_task", ...). Queued inBG_QUEUEand flushed after the response to avoid stdout races with stdin.
To prevent agents from performing destructive commands (like deletion or directory escapes), the project enforces safety boundaries across two layers:
- Filesystem Safety (
check_filesystem_safetyin hooks.rs):- Secondary defence: blocks paths that obviously start with
..before any I/O is attempted. - Primary defence:
FilesystemTool::sandbox_pathin filesystem.rs resolves and normalises the full path, then uses component-awarePath::starts_withto ensure the result stays within the current working directory (PWD). This catches embedded traversals likesrc/../../etc/passwdeven if they don't start with...
- Secondary defence: blocks paths that obviously start with
- Code Execution Safety (
check_code_exec_safety):- Blocks execution of known dangerous shell sequences:
- PowerShell:
Remove-Item - Unix:
rm -rf - Windows cmd:
del /f - System level:
Format-Volume
- PowerShell:
- Commands are run in PowerShell (
powershell -NoProfile -Command ...).
- Blocks execution of known dangerous shell sequences:
Hello, fellow AI! Here is how you can quickly work with this codebase:
- Create a new source file in
src/tools/. - Define your arguments struct with
#[derive(Deserialize, Serialize)]and your output struct. - Implement
rig::tool::Toolfor your struct. - Add the module declaration to
src/tools/mod.rs. - Register the tool in
src/agent.rsusing.tool(YourNewTool::new(ctx.clone()))inside thebuild_agentfactory function.
- Your tool should accept
ToolContextin its constructor. - During tool execution (
call), get the active turn ID viaself.ctx.current_turn_id(). - Use
self.ctx.store.record_tool_callto insert a row in the database. Look at code_exec.rs for a clean example.
- Default LLM:
deepseek/deepseek-v4-flashvia OpenRouter (defined insrc/agent.rs). - Global Config Dir:
~/.rust-agent/(defined insrc/config.rsviaconfig_dir()). - Filesystem Sandbox: Current working directory (PWD) β wherever
rust-agentis launched from. - Database File:
~/.rust-agent/traces.db(path computed bytrace::db_path()). - Rules File:
~/.rust-agent/rules.md(loaded byrules::load_rules()). - Prompt Templates:
~/.rust-agent/prompts/(loaded bytools/prompt.rsTEMPLATESstatic).