Multi-provider · tool-capable end-to-end · structured permissions · real MCP · zero unsafe.
┌─ user ──┐ ┌─── QueryLoop ───────────────────────────────────────┐
│ prompt │───▶│ Streaming → ToolDispatch → ToolCollecting → Yield │──▶ Event::*
└─────────┘ │ ↑ │ │
│ └──── auto-compact / hooks / cost ◀─────┘ │
└─────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
Anthropic OpenAI-* Ollama MCP servers
(SSE) (compat) (local) (stdio / HTTP)
use agent::prelude::*;
use std::sync::Arc;
let provider = Arc::new(AnthropicProvider::new(std::env::var("ANTHROPIC_API_KEY")?));
let engine = QueryEngine::new(provider, "claude-opus-4-7").with_system("Be concise.");
let mut stream = engine.run("Summarize Rust's borrow checker in two lines.", AbortController::new()).await?;
while let Some(event) = futures::StreamExt::next(&mut stream).await {
if let Event::TextDelta { delta } = event? { print!("{delta}") }
}That's a complete agent — provider streaming, tool dispatch, hooks, permissions, auto-compaction, USD cost tracking — all wired up. Drop in Files API attachments, MCP servers, or the bundled coding tool pack with one extra line each.
- 🦀 Rust-native, library-only. No
tokio::mainhijack, no global state, nopanic!on bad input.#![forbid(unsafe_code)]in every crate. Drop it into a CLI, an IDE plugin, a desktop app, or a server — the runtime doesn't care. - 🔌 Three providers, one event vocabulary. Anthropic Messages (hand-rolled SSE — full prompt-cache + extended-thinking betas, no SDK dep),
async-openai0.36 (DeepSeek / Moonshot / Groq / OpenRouter / LM Studio), and local Ollama. StreamEvent::TextDelta,ToolUse,Usage,Result— same shape from every backend. - 🛠 Tool-capable end-to-end. Define a tool, register it, the runtime wires the JSON Schema into the request body, dispatches
ToolUseevents to your code, feeds results back. Multi-turn loop with a phase machine. Receipt-order concurrent execution. Permissions and cost tracking are wired through, not bolted on. - 🛡 Structured permissions that fail safe. A 7-step decision chain (deny / ask / callback / bypass / allow / default-ask / dont_ask), composable
PermissionMatcherrules over tool input shapes (JSON-pointer fields, glob/prefix/regex patterns, AnyOf / AllOf / Not), and a 4-levelSafetyClasslattice whereUnknown ≡ Destructivefor gating — so unclassified tools never slip through. - 🔗 MCP that actually plugs in. Full Model Context Protocol client lifecycle: stdio child processes, streamable HTTP, OAuth 2.0 + PKCE, server-initiated elicitation, channel permissions, stale-handle reconnect repair. Tool calls don't serialize on a mutex.
close()doesn't deadlock during slow RPCs. - 💸 Cost accounting in nanodollar precision.
Event::Usageflows into aCostTrackerwith a model-price catalog (Anthropic + GPT defaults, BYO entries trivially).u128integer accumulator — no f64 drift across long sessions. - 📎 Files API for big attachments.
FilesClienttrait +AnthropicFilesClient. Smart helpers auto-route between inline base64 and uploadedfile_idreferences based on size. Beta header gets added automatically when any block (including those nested in tool results) carries afile_id. - ♻️ Reactive auto-compaction. Token estimator + LLM-driven
<analysis>/<summary>summarization, microcompact, session memory, post-cleanup file restoration. Long sessions stay inside the context window without losing critical state. - 📦 Optional batteries. Companion
agent-tools-codecrate ships generic FileRead/Write/Edit, Grep/Glob (gitignore-aware viaignore), Bash, WebFetch, TodoWrite, NotebookEdit (Jupyter .ipynb cells), andToolSearchfor deferred-tool discovery. Every tool declares itsSafetyClass; aWorkspacePolicyenforces path containment + size caps + symlink rules. Pull only the features you want.
flowchart TB
Host[Host application]
subgraph Runtime[agent crate]
QL[query - phase machine]
Prov[provider - Anthropic / OpenAI / Ollama]
Tool[tool trait + registry]
Perm[permission - 7-step chain + matchers]
Hook[hook - 27 typed events]
Comp[compact - reactive auto-compact]
Cost[cost - USD accounting]
Sess[session - JSONL persistence]
Atch[attachments - Files API]
MCP[mcp - rmcp connector]
end
subgraph Optional[agent-tools-code]
FS[FileRead / Write / Edit / ...]
Search[Grep / Glob / ToolSearch]
Shell[Bash]
Web[WebFetch]
Todo[TodoWrite]
end
Models[Models - Anthropic / OpenAI / Ollama / MCP]
Host --> QL
QL --> Prov
QL --> Tool
QL --> Hook
QL --> Perm
QL --> Comp
QL --> Cost
QL --> Sess
Perm --> Tool
Atch --> Tool
Tool --> Optional
Prov --> Models
MCP --> Models
Streaming Events are the universal language: every provider emits the same Event taxonomy, so swap providers without touching tool code.
Two crates, both versioned together. Pull only what you need.
[dependencies]
# Runtime — always
agent = { git = "https://github.com/ZSeven-W/agent-rs", default-features = false, features = ["anthropic", "session-jsonl"] }
# Optional: ready-made coding tool pack (FileRead/Write/Edit, Grep/Glob, Bash, WebFetch, TodoWrite, ToolSearch)
agent-tools-code = { git = "https://github.com/ZSeven-W/agent-rs", default-features = false, features = ["fs", "search"] }| Flag | Pulls in | Notes |
|---|---|---|
anthropic (default) |
reqwest + eventsource-stream |
Hand-rolled Anthropic SSE — no SDK dep. |
openai |
async-openai 0.36 |
OpenAI-compatible providers. |
ollama |
ollama-rs 0.3 |
Local models. |
mcp |
rmcp 1.5 |
MCP client + production stdio/HTTP connector + OAuth/PKCE. |
session-jsonl |
fs4 |
JSONL persistence with file lock. |
swarm |
fs4 + notify |
Sub-agents, mailbox, teams. |
tiktoken |
tiktoken-rs |
Real BPE token counts (cl100k / o200k / p50k / r50k). |
full |
all of the above |
| Flag | Pulls in | Tools |
|---|---|---|
fs (default) |
(none) | FileRead / Write / Edit / ListDir / Mkdir / Move / Remove |
search (default) |
regex + ignore |
Grep · Glob (gitignore-aware) |
shell |
shell-words |
Bash (timeout, abort, output cap) |
bash-async |
(none) | BashRun + BashOutput + KillShell (background shells, ring-buffer poll) |
web |
reqwest + futures |
WebFetch (HTML→text, size cap) |
web-search |
web |
WebSearch (pluggable backend, ships Tavily) |
task |
futures |
Task (spawn a child QueryLoop) |
todo |
(none) | TodoWrite (in-memory shared state) |
notebook |
(none) | NotebookEdit (Jupyter .ipynb cell-level edits) |
all |
all of the above |
ToolSearch is always-on (no feature flag) and lets you expose 50+ MCP tools without flooding the model's tool list — it picks them up via select:Name1,Name2 or keyword search.
Runnable examples live under each crate's examples/ directory:
| Example | Crate | What it shows |
|---|---|---|
anthropic_basic |
agent |
Minimal provider + QueryLoop + stream — the README TL;DR as a real binary. |
with_tools |
agent |
Wires the bundled coding tool pack into the loop and asks the model to grep + read the workspace. |
notebook_edit |
agent-tools-code |
Calls NotebookEditTool directly (no LLM) to edit a synthesized .ipynb. |
web_search_tavily |
agent-tools-code |
Tavily Search via WebSearchTool. Needs TAVILY_API_KEY. |
ANTHROPIC_API_KEY=sk-... cargo run --example anthropic_basic --features anthropic -p agent
ANTHROPIC_API_KEY=sk-... cargo run --example with_tools --features anthropic -p agent
cargo run --example notebook_edit --features notebook -p agent-tools-code
TAVILY_API_KEY=tv-... cargo run --example web_search_tavily --features web-search -p agent-tools-codeuse agent::prelude::*;
use agent_tools_code::{register_default, WorkspacePolicy};
use std::sync::Arc;
let policy = WorkspacePolicy::new(std::env::current_dir()?)?.into_arc();
let mut tools = ToolRegistry::new();
register_default(&mut tools, policy); // FileRead, Write, Edit, ListDir,
// Mkdir, Move, Remove, Grep, Glob
let provider = Arc::new(AnthropicProvider::new(std::env::var("ANTHROPIC_API_KEY")?));
let qloop = QueryLoop::builder(provider, "claude-opus-4-7")
.tools(Arc::new(tools))
.build();
let mut stream = qloop.run("List the .rs files in src/, then summarize main.rs.", AbortController::new()).await?;
while let Some(event) = futures::StreamExt::next(&mut stream).await {
match event? {
Event::TextDelta { delta } => print!("{delta}"),
Event::ToolUse { name, .. } => eprintln!("\n→ calling {name}"),
_ => {}
}
}That's the full picture: registry → provider → loop. The runtime handles tool dispatch, permission gating, hooks, cost tracking, and auto-compaction without you wiring anything else.
agent crate — runtime (15+ modules)
| Module | Purpose |
|---|---|
provider/ |
Multi-provider LLM client. Tool definitions wired into request bodies; capability flags + streaming Event vocabulary. |
query/ |
QueryLoop multi-turn phase machine. Reactive auto-compaction wired in. |
tool/ |
Tool trait, ToolRegistry, SafetyClass lattice. Receipt-order concurrent execution via ToolExecutor. |
permission/ |
7-step chain + structured PermissionMatcher (Always / Field / ExactJson / AnyOf / AllOf / Not) + StringPattern. External-queue async approval. |
hook/ |
27 typed HookEvent variants. |
message/ |
DAG-aware MessageStore. ContentBlock::Document for PDFs; ImageSource::File for Files-API references. |
stream/ |
Event taxonomy: TextDelta / Thinking / ToolUse / ToolResult / Result / Usage / Error / Notice. |
session/ |
JSONL persistence (schema v1) with atomic-rename + file lock. |
swarm/ |
Sub-agents / teams. File-locked mailbox, in-process / tmux / iTerm2 backends. |
compact/ |
Reactive auto-compaction. LLM-driven summarization, partial directions, microcompact, session memory. |
context/ |
Sliding-window trim. |
| Module | Purpose |
|---|---|
api/ |
Retry with decorrelated jitter, error classification, prompt-cache-break detection, secret redaction. |
cost/ |
Model-price-aware USD accounting. u128 nanodollars — no f64 drift. |
attachments/ |
FilesClient + AnthropicFilesClient, smart size-aware routing. |
tokenizer/ |
Pluggable trait. Real tiktoken plugs in via the trait. |
| Module | Purpose |
|---|---|
mcp/ (feature mcp) |
Full MCP client + production RmcpConnector. |
memdir/ |
MEMORY.md directory loader with frontmatter + relevance scoring. |
skills/ · plugins/ · state/ · bootstrap/ · context_analysis/ · tasks/ · memory_extract/ · remote/ |
See crates/agent/src/. |
agent-tools-code crate — optional coding tool pack
| Tool | Class | Feature |
|---|---|---|
FileReadTool |
ReadOnly |
fs |
FileWriteTool |
Mutating |
fs |
FileEditTool |
Mutating |
fs |
ListDirTool |
ReadOnly |
fs |
MkdirTool |
Mutating |
fs |
MoveTool |
Mutating |
fs |
RemoveTool |
Destructive |
fs |
GrepTool |
ReadOnly |
search |
GlobTool |
ReadOnly |
search |
BashTool |
Mutating |
shell |
BashRunTool |
Mutating |
bash-async |
BashOutputTool |
ReadOnly |
bash-async |
KillShellTool |
Mutating |
bash-async |
WebFetchTool |
ReadOnly |
web |
WebSearchTool |
ReadOnly |
web-search |
TaskTool |
Mutating |
task |
TodoWriteTool |
Mutating |
todo |
NotebookEditTool |
Mutating |
notebook |
ToolSearchTool |
ReadOnly |
(always) |
A shared WorkspacePolicy enforces path containment, file-size caps, and symlink rules. register_default(registry, policy) bulk-registers every enabled tool.
Library-only. No global state, no
tokio::main, nopanic!on bad input — every error path is a typedAgentError.Provider-agnostic at the runtime layer. Concrete tools live outside the
agentcrate. The runtime defines the trait; companions ship implementations.Streaming first. Every provider is a streaming source. Multi-turn / tool dispatch / compaction are coordinated through one
Eventvocabulary, no polling.Cancellation everywhere. Every async surface honors an
AbortController— including thetokio::task::spawn_blockingworkers used by Grep / Glob.No
unsafe.#![forbid(unsafe_code)]in both crates.Defensive against the model. Permissions fail safe (Unknown ≡ Destructive for gating). Tool schemas validated before reaching the wire. Path operations canonicalize before any I/O. Idempotent writes detect no-ops.
Cost-aware. Tool schemas, prompt cache, and token usage feed an integer-precision USD accumulator. Long-running sessions don't drift.
cargo test --workspace --all-features
# 844 unit · 13 integration · 5 doc · 4 ignored (real-API gates)
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo fmt --all -- --check
cargo deny --all-features checkThe 4 #[ignore]-gated tests hit real APIs (Anthropic / OpenAI / Ollama / Anthropic Files) when their environment variables are set. CI runs the full suite with mocks; real-API runs are manual.
This is a pre-release project — every change lives under Unreleased in CHANGELOG.md until 0.1.0 ships. The runtime API surface has stabilized and 863 tests guard it; the open work is wiring more tools into the optional companion crate and tagging a release.
See openpencil-docs/agent-rs/notes/2026-05-02-claude-code-non-tui-gaps.md for what's intentionally host-side vs. what's pending.
PRs welcome. Two ground rules:
- No product-specific imports in the
agentcrate. Generic concepts only — anything tied to a specific app belongs in a downstream crate. - Adversarial review every change. Open an issue first for anything bigger than a small fix so we can align on direction.
MIT licensed. See LICENSE.
Built with caffeine, codex review, and an unreasonable number of tests.