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_shellout 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.
What's New in v0.5.0
- HTTP API — axum-based REST server with sessions, SSE streaming, OpenAPI spec
- Terminal UI — ratatui-based TUI with streaming tool indicators, plan mode
- Multi-Agent — agent pool, shared memory, messaging bus, pipeline & team orchestration
- Python SDK —
pip install recursive-clientfor programmatic access - Loop Mode —
recursive loopfor self-scheduling autonomous agent runs
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)
# loop mode — agent self-schedules wakeups
# HTTP API server
# Terminal UI (connects to HTTP server)
# 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
Docker / Cloud Deployment
Single-container (local mode)
The image defaults to recursive http --addr 0.0.0.0:3000 and exposes /health for probes.
Full cloud stack (Redis + S3)
Use the bundled docker-compose.yml to spin up Redis (session hot-state) and
LocalStack S3 (transcript persistence) locally:
Then talk to the agent over HTTP:
# create a session
SESSION=
# send a message
Environment variables — full reference
LLM provider
| 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_PROVIDER_TYPE |
openai |
Protocol: openai or anthropic |
RECURSIVE_MAX_STEPS |
32 |
Max tool-call loop iterations per run |
RECURSIVE_TEMPERATURE |
0.2 |
Sampling temperature |
RECURSIVE_SYSTEM_PROMPT_FILE |
(built-in) | Path to a custom system-prompt file |
RECURSIVE_WORKSPACE |
cwd | Filesystem sandbox root |
HTTP server
| Env | Default | Purpose |
|---|---|---|
RECURSIVE_HTTP_ADDR |
0.0.0.0:3000 |
Bind address |
RECURSIVE_HTTP_AUTH_KEYS |
(none, open) | Comma-separated X-API-Key allowlist |
Cloud storage — Redis (session hot-state)
Requires the cloud-runtime feature flag (--features cloud-runtime).
| Env | Default | Purpose |
|---|---|---|
RECURSIVE_REDIS_URL |
(disabled) | Redis connection URL e.g. redis://host:6379 |
RECURSIVE_REDIS_KEY_PREFIX |
recursive: |
Key namespace prefix |
RECURSIVE_REDIS_SESSION_TTL_SECS |
7200 |
Session expiry (2 h) |
Cloud storage — S3 (transcript + memory)
Requires the cloud-runtime feature flag.
| Env | Default | Purpose |
|---|---|---|
RECURSIVE_S3_BUCKET |
(disabled) | S3 bucket name |
RECURSIVE_S3_PREFIX |
recursive |
Object key prefix |
RECURSIVE_S3_TENANT_ID |
default |
Tenant namespace inside the bucket |
AWS_ACCESS_KEY_ID |
(from SDK) | AWS credential |
AWS_SECRET_ACCESS_KEY |
(from SDK) | AWS credential |
AWS_DEFAULT_REGION |
us-east-1 |
AWS region |
AWS_ENDPOINT_URL |
(AWS) | Override for LocalStack / MinIO |
Sandbox
| Env | Default | Purpose |
|---|---|---|
RECURSIVE_SANDBOX_MODE |
local |
local / policy / docker / e2b |
RECURSIVE_E2B_API_KEY |
(required for e2b mode) | E2B API key |
RECURSIVE_E2B_TEMPLATE |
base |
E2B sandbox template ID |
RECURSIVE_E2B_TIMEOUT_SECS |
300 |
Sandbox timeout in seconds |
RECURSIVE_SHELL_TIMEOUT_SECS |
30 |
Per-command shell timeout |
Local vs cloud mode — cheatsheet
| Concern | Local (default) | Cloud (cloud-runtime feature) |
|---|---|---|
| Transcript persistence | Local JSONL (~/.recursive/...) |
S3 via S3StorageBackend |
| Session hot-state | In-memory (NoopSessionStore) |
Redis via RedisSessionStore |
| Tool execution | Host shell | Docker (L2) or E2B microVM (L3) |
| Horizontal scaling | Single process | Stateless HTTP pods + shared Redis/S3 |
| Resume across restarts | Via --session flag |
Automatic via restore_from_storage() |
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
540+ tests covering:
- 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.
- HTTP API: health, tools, run, sessions CRUD, SSE streaming, OpenAPI spec.
- TUI: app state, key handling, message styling, scroll, plan mode.
- Multi-Agent: pool, roles, shared memory, messaging bus, pipeline, orchestrator.
- End-to-end smoke (
tests/smoke.rs): scriptedMockProviderdriving real filesystem tools.
Python SDK
&&
=
# "ok"
=
TUI
The terminal UI is in crates/recursive-tui/. For an experience-level
comparison against fake-cc (Claude Code-style baseline), see
docs/tui-fake-cc-gap.md.
License
MIT — see LICENSE.