crates.io · Docs · API · GitHub · DeepWiki · Changelog
The agent loop for Rust. Stream from any of 7 LLM protocols, run tools, loop until done.
git clone https://github.com/yologdev/yoagent && cd yoagent
ollama serve & # any local model works
cargo run --example cli -- --provider ollamaThat's a working coding agent in your terminal — file read/write/edit, shell, ripgrep search, streaming output, skills. No signup, no key, nothing to configure.
yoagent cli — mini coding agent
Type /quit to exit, /clear to reset
model: llama3.1:8b
cwd: /home/user/my-project
> find all TODO comments in src/
▶ search 'TODO' ✓
Found 3 TODOs:
src/main.rs:42: // TODO: handle edge case
src/lib.rs:15: // TODO: add tests
src/utils.rs:8: // TODO: optimize this
tokens: 1250 in / 89 out
Point it at a hosted model instead by swapping the flag:
ANTHROPIC_API_KEY=sk-... cargo run --example cli
GROQ_API_KEY=... cargo run --example cli -- --provider groq --model llama-3.3-70b-versatile
cargo run --example cli -- --api-url http://localhost:1234/v1 --model my-model # LM Studio, llama.cpp, vLLM[dependencies]
yoagent = "0.15"
tokio = { version = "1", features = ["full"] }An agent that actually uses a tool — the thing the crate exists for:
use yoagent::provider::ModelConfig;
use yoagent::{tools, Agent, AgentEvent, StreamDelta};
#[tokio::main]
async fn main() {
// The provider is selected from the config's protocol and the key is read
// from ANTHROPIC_API_KEY. Call `.with_api_key(k)` to pass one explicitly.
let mut agent = Agent::from_config(ModelConfig::claude_sonnet_5())
.with_system_prompt("You are a coding assistant.")
.with_tools(tools::default_tools());
let mut events = agent.prompt("Find every TODO in src/ and summarise them").await;
while let Some(event) = events.recv().await {
match event {
AgentEvent::MessageUpdate { delta: StreamDelta::Text { delta }, .. } => print!("{delta}"),
AgentEvent::ToolExecutionStart { tool_name, .. } => println!("\n▶ {tool_name}"),
AgentEvent::AgentEnd { .. } => break,
_ => {}
}
}
agent.finish().await;
}Swap the model by swapping the config — the provider follows, and the key is read from that provider's conventional env var:
Agent::from_config(ModelConfig::groq("llama-3.3-70b-versatile", "Llama 3.3 70B")); // GROQ_API_KEY
Agent::from_config(ModelConfig::google("gemini-2.5-pro", "Gemini 2.5 Pro")); // GEMINI_API_KEY
Agent::from_config(ModelConfig::ollama("http://localhost:11434", "llama3.1:8b")); // no keyyoagent is deliberately narrow. It is the loop, tool execution, and the machinery you need to run that loop in production. It ships no vector stores, embedding pipelines, or task-graph layer — if your problem is retrieval or orchestration, one of these is the better fit:
| If you need | Look at |
|---|---|
| RAG pipelines, vector stores, embeddings, transcription and image generation | rig — "Build modular and scalable LLM Applications in Rust" |
| Typed task graphs and streaming RAG indexing alongside agents | swiftide — "Composable LLM agents and harness, typed task graphs, and streaming RAG pipelines in Rust" |
| A tool-calling loop you host, gate, steer, branch, and record | yoagent |
What that focus bought:
- The loop is a free function.
agent_loop()is stateless and takes everything it needs as arguments.Agentis an optional wrapper that adds history and queues. You can drive the loop yourself without adopting our state model. - 7 native wire protocols, not one OpenAI-compat shim with adapters bolted on. Anthropic Messages, OpenAI Completions, OpenAI Responses, Azure, Gemini, Vertex, and Bedrock each have a real implementation, so provider-specific features (thinking budgets, prompt-cache breakpoints, reasoning deltas) survive instead of being flattened away.
- Every tool call passes one gate.
ToolMiddlewarecan allow, modify, or deny each call at a single choke point shared by all execution strategies — the mechanism behind approval prompts and policy engines. - Steer a run that's already going. Inject guidance mid-flight; it's picked up between tool batches without restarting the turn.
- History is a tree, not a list.
Sessionforks, checkpoints, and seeks. Edit an earlier turn and re-run it without destroying the original branch. - Runs are recordable. With
features = ["gasp"], a run becomes an append-only semantic event log in a git repo — restore is clone + replay. Conformance-checked in CI. - The whole loop is testable offline.
MockProviderscripts multi-turn tool-calling conversations and honours cancellation, so abort and steering paths are testable with no network. 456 of our 463 tests need no key.
yoyo-evolve — a coding
agent that evolves its own source in public. It began as 200 lines of Rust; every commit since has
been agent-written and gated on tests. It runs on this loop with the
openapi feature enabled.
Also built on yoagent:
| Project | What it is |
|---|---|
rab |
A lightweight, extensible Rust coding agent |
greatsage |
"Rimuru's Unique Skill, you know the one" |
yoclaw |
OpenClaw reborn in Rust — a single-binary agent that remembers you |
Built something on yoagent? Open a PR and add it here — we'd like to see it.
The loop & control
- Full event stream:
AgentStart→TurnStart→MessageUpdate(deltas) →ToolExecution*→TurnEnd→AgentEnd - Parallel tool execution by default;
SequentialandBatched { size }strategies available - Steering — interrupt mid-run; follow-ups — queue work after completion; both queues are inspectable and editable
ToolMiddleware— asyncAllow/Modify(args)/Deny(reason)hooks gating every call. A denial becomes an error tool result the model sees, so the loop keeps goingInputFilter— rewrite or reject user input before it reaches the model (PII redaction, prompt-injection guards)- Execution limits (max turns, max tokens, wall-clock timeout),
abort(), and lifecycle callbacks (before_turn,after_turn,on_error) - Automatic retry with exponential backoff and ±20% jitter, for rate-limit and network errors only
Providers — 7 protocols, 20+ providers
| Protocol | Providers |
|---|---|
| Anthropic Messages | Anthropic (Claude) |
| OpenAI Completions | OpenAI, xAI, Groq, Cerebras, OpenRouter, Mistral, DeepSeek, MiniMax, Z.ai, Qwen, Meta (Muse Spark), Ollama, local servers, custom compatible APIs |
| OpenAI Responses | OpenAI (Responses API) |
| Azure OpenAI | Azure OpenAI |
| Google Generative AI | Google Gemini |
| Google Vertex | Google Vertex AI |
| Bedrock ConverseStream | Amazon Bedrock |
ModelConfig presets cover the common providers; ModelConfig::openai_compat(..) handles anything
else with a base_url. Per-provider quirks (auth style, reasoning format, max_tokens field name)
live in OpenAiCompat / AnthropicCompat flags — 12 compat profiles ship in the box.
The opencode_zen(..) / opencode_go(..) gateways pick the wire protocol from the model id
automatically, so one config reaches models across several vendors.
Thinking/reasoning controls are wired for all 7 protocols. Client-side prompt-cache breakpoints are Anthropic-specific; most other providers cache server-side, and Bedrock does not cache automatically. Context-overflow detection is centralised across 15+ provider-specific error strings.
Tools — built-in, custom, MCP, OpenAPI
Built in: bash (timeout, deny patterns), read_file / write_file (line numbers, path
restrictions), edit_file (fuzzy-match hints on failure), list_files, search (ripgrep).
Tools return stdout and stderr even on failure, so the model can self-correct.
Custom tools implement one trait:
#[async_trait::async_trait]
impl AgentTool for GreetTool {
fn name(&self) -> &str { "greet" }
fn label(&self) -> &str { "Greet" }
fn description(&self) -> &str { "Greets someone" }
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({ "type": "object", "properties": { "name": { "type": "string" } } })
}
async fn execute(&self, params: serde_json::Value, _ctx: ToolContext)
-> Result<ToolResult, ToolError>
{
let name = params["name"].as_str().unwrap_or("stranger");
Ok(ToolResult {
content: vec![Content::Text { text: format!("Hello, {name}!") }],
details: serde_json::Value::Null,
})
}
}MCP — with_mcp_server_stdio() / with_mcp_server_http() connect to Model Context Protocol
servers over stdio or Streamable HTTP (session ids, SSE framing, incremental parsing) and register
their tools transparently.
OpenAPI (features = ["openapi"]) — point with_openapi_url() at a spec and every operation
becomes a tool, filtered by OperationFilter.
Sub-agents & shared state
SubAgentTool delegates to a child loop with its own model, system prompt, tools, skills,
middleware, retry policy, and turn limits — a fully independent configuration, not a thin shim.
Run a cheap model for triage and an expensive one for the hard step in the same session.
SharedState is a pluggable key-value store (MemoryBackend, FileBackend, or your own via the
SharedStateBackend trait). A parent stores a large artifact once and sub-agents read it by key,
so it never gets re-pasted into every context window. Opt in with .with_shared_state(state) —
it injects the shared_state tool and a state summary into the sub-agent's system prompt.
Context, sessions & skills
ContextTracker— hybrid real-usage + estimation, calibrated against actual provider usage- Tiered compaction — truncate tool outputs → summarise old turns → drop middle turns
Session— history as an id/parent tree withappend,seek,checkpoint,branch_tips, and JSONL persistence. Appending after a seek forks a branch; it never overwrites- Skills — load AgentSkills-standard
SKILL.mddirectories. The agent sees a compact index and reads the full skill on demand, so skills stay cross-compatible with Claude Code, Codex CLI, Cursor, and others - Structured outputs —
prompt_structured::<T>()returns typed, schema-validated replies, enforced natively where supported (Anthropic tool-forcing, OpenAIjson_schema, GeminiresponseSchema)
Production concerns
- Cost tracking —
CostConfigcarries separate input/output/cache-read/cache-write rates;session_cost_usd()gives a running total, andis_configured()distinguishes "free" from "pricing unknown" - Telemetry —
tracingspans per loop / LLM stream / tool, recording tokens and cost. OpenTelemetry is bridged app-side viatracing-opentelemetry; the library carries no OTel dependency by design - GASP (
features = ["gasp"]) — record runs into a GASP agent repo; yoagent is a tested-conformant runtime, with the 7-check suite running in CI - Serde throughout — every core type is
Serialize/Deserialize/PartialEq, so sessions persist and replay set_model()— hot-swap the model mid-session without rebuilding the agent
Ten runnable examples in examples/. Five need no API key at all.
| Example | What it shows | Key needed |
|---|---|---|
cli |
A 370-line coding agent — all tools, skills, streaming, colored output. Like a baby Claude Code | optional¹ |
rlm |
An LLM that explores a codebase on its own by spawning sub-agents | yes |
code_review |
Three sub-agents reviewing a diff in parallel, results merged | yes |
shared_state |
Passing a large artifact between sub-agents by reference | yes |
sub_agent |
Delegation basics with a per-sub-agent model | yes |
basic |
The smallest possible agent | yes |
callbacks |
Lifecycle hooks and a custom tool | no |
persistence |
Save and restore a session | no |
telemetry |
tracing spans with token and cost fields |
no |
gasp_emit |
Recording a run into a GASP repo | no |
¹ --provider ollama or --api-url needs no key; hosted providers read their conventional env var.
MockProvider scripts a whole multi-turn tool-calling conversation with no network:
use yoagent::provider::mock::{MockProvider, MockResponse, MockToolCall};
let provider = MockProvider::new(vec![
MockResponse::ToolCalls(vec![MockToolCall {
name: "search".into(),
arguments: serde_json::json!({ "pattern": "TODO" }),
provider_metadata: None,
}]),
MockResponse::Text("Found 3 TODOs.".into()),
]);
let agent = Agent::from_provider(provider, ModelConfig::mock());It emits real StreamEvents and honours the CancellationToken, so abort and steering paths are
testable too.
- 463 tests, of which 456 run with no network and no API keys —
cargo test --all-features - Provider SSE streams tested at the HTTP level with
wiremockacross 8 suites clippy --all-targets --all-featureswith-Dwarnings,cargo fmt --check- Linux + macOS test matrix, a Windows compile check, a pinned MSRV 1.86 job, and a GASP conformance job
| Module | What lives there |
|---|---|
agent_loop |
The loop itself — agent_loop, agent_loop_continue, AgentLoopConfig, execution strategies |
agent |
Optional stateful wrapper — history, tool registry, steering/follow-up queues |
types |
Message, Content, AgentEvent, AgentTool, ToolMiddleware, InputFilter |
provider/ |
StreamProvider trait, ModelConfig, registry, and the 7 protocol implementations + MockProvider |
tools/ |
bash, file, edit, list, search, shared_state_tool |
sub_agent |
SubAgentTool — delegation to child loops |
shared_state |
SharedState + pluggable backends |
session |
Branching conversation trees with JSONL persistence |
context |
Token tracking, tiered compaction, execution limits |
skills |
AgentSkills SKILL.md loading |
retry |
Backoff with jitter |
mcp/ |
MCP client, stdio + HTTP transports, tool adapter |
openapi/ |
OpenAPI 3.0 → tools (feature openapi) |
gasp |
Run recording into a GASP repo (feature gasp) |
- The book — concepts, guides, and a page per provider (source)
- API reference — built with all features enabled
- CHANGELOG — every release
- CONTRIBUTING — how to build, test, and send a PR
MSRV is 1.86, enforced in CI. Raising it is a minor-version change.
MIT — see LICENSE.
Inspired by pi-agent-core (TypeScript).