Recursive
A minimal, orthogonal, embeddable coding agent kernel in Rust.
Recursive is a tiny ReAct-style agent loop that wires together:
- an LLM provider (OpenAI-compatible HTTP by default; works with OpenAI, GLM/Zhipu, DeepSeek, Moonshot, MiniMax, Together, Ollama, vLLM, …)
- a tool registry (
read_file,write_file,apply_patch,list_dir,run_shell,count_linesout of the box; trivially extensible) - a transcript plus a
StepEventstream you can observe
The whole kernel is intentionally small enough to read in one sitting.
At a glance
use Arc;
use ;
# async
Design
The kernel has five concepts, each independently testable:
| Concept | Where | Role |
|---|---|---|
Message |
src/message.rs |
The only data primitive: chat messages with optional tool calls. |
LlmProvider |
src/llm/ |
Trait for model backends. Adapters: HTTP (OpenAI-compatible), Mock. |
Tool + ToolRegistry |
src/tools/ |
Trait for side effects the model can request. Sandboxed to a workspace. |
Agent |
src/agent.rs |
The loop. Receives a goal, alternates model ↔ tools, emits events. |
StepEvent |
src/agent.rs |
Observer channel for UI / logging / replay. |
Orthogonality
- New tool? Implement
Tool, register it. No agent changes. - New model backend? Implement
LlmProvider. No tool/agent changes. - New UI / observer? Subscribe to the
StepEventchannel. No loop changes. - New finish reason? Add a variant to
FinishReason. Callers can match if they care.
Safety primitives baked in
- Every fs / shell tool resolves paths through
tools::resolve_within, which rejects anything escaping the configured workspace root. run_shellenforces a configurable timeout and caps captured output.- Agent loop respects a step budget (
max_steps) and emitsFinishReason::BudgetExceededrather than looping forever.
CLI
The crate is published as
recursive-agentbecause the namerecursivewas taken on crates.io. The installed binary is still calledrecursive, and the library is imported asuse recursive::*;.
# one-off goal
# interactive REPL (one goal per line, :q to exit)
# inspect what tools are registered (no API key needed)
Configuration
Anything OpenAI-compatible works. Override via env vars (or CLI flags):
| Env | Default | Purpose |
|---|---|---|
RECURSIVE_API_BASE |
https://api.openai.com/v1 |
Chat-completions endpoint |
RECURSIVE_API_KEY |
(required) | Bearer token |
RECURSIVE_MODEL |
gpt-4o-mini |
Model name |
RECURSIVE_MAX_STEPS |
32 |
Loop budget |
RECURSIVE_TEMPERATURE |
0.2 |
Sampling temperature |
RECURSIVE_WORKSPACE |
cwd | Root all fs/shell tools are sandboxed to |
RECURSIVE_SYSTEM_PROMPT_FILE |
(built-in) | Path to a system prompt to load |
Example with GLM (Zhipu):
Example with a local Ollama:
# ollama ignores it but the field is required
Library API
recursive is also a library — embed the loop in your own program if the CLI
isn't the right shell for your use case. See the example above; the public
surface lives in src/lib.rs.
Testing
Coverage includes:
- Agent loop: termination, tool dispatch, error recovery, step budget, event stream order.
- Tool registry: dispatch, unknown-tool error, path sandboxing.
- Filesystem tools: round-trip, parent-dir creation, sort order, escape rejection.
- Shell tool: success / non-zero status / timeout.
- HTTP provider: request shape (with and without tools), response parsing (plain text / tool-call), tool-call argument round-trip.
- End-to-end smoke (
tests/smoke.rs): scriptedMockProviderdriving real filesystem tools.
License
MIT — see LICENSE.