Self-Improving Agents
One of Recursive's most distinctive features is that it runs its own development loop. The same agent kernel you use to build your own tools is the one that implements new features in Recursive itself.
How it works
The self-improvement loop lives in .dev/scripts/self-improve.sh. At a high level:
1. Read goal from .dev/goals/ or .dev/ROADMAP.md
2. Launch recursive loop with coding tools (read_file, write_file, apply_patch, run_shell)
3. Agent reads the codebase, understands the goal, makes changes
4. Run `cargo test` to verify
5. Run `cargo clippy` to check quality
6. If all pass: commit the changes
7. If fail: rollback, try again with adjusted approach
8. Emit an observation to .dev/journal/ for the next runThe observation system
After each run, the agent writes a journal entry to .dev/journal/. These entries contain:
- What was attempted
- What succeeded or failed
- Lessons learned for next time
On the next run, the agent reads recent journal entries before starting. This creates a persistent feedback loop — the agent learns from its mistakes without any external training.
Key invariants
The self-improve loop enforces several invariants documented in .dev/AGENTS.md:
| Invariant | Description |
|---|---|
| #1 | Agent loop stays small — new capabilities go into tools, not the loop |
| #3 | Sandbox — all fs/shell tools use resolve_within |
| #5 | No unwrap() in product code |
| #8 | Tool-call ↔ tool-result pairing preserved |
These invariants are checked in code — clippy and tests enforce them, not documentation.
Using the loop for your own project
You can use the same pattern for any codebase:
- Create a
.dev/goals/directory with goal files - Add an
AGENTS.mdat the project root describing invariants, conventions, and context - Run
recursive loop --workspace . "read .dev/goals/ and implement the next unfinished goal"
# Create a goal
cat > .dev/goals/01-add-caching.md << 'EOF'
## Goal: Add in-memory caching to the API layer
The /api/users endpoint is slow because it queries the DB on every request.
Add a simple TTL cache (5 minutes) using a HashMap wrapped in RwLock.
Acceptance criteria:
- Cache hit ratio > 80% in load test
- No data races (use Arc<RwLock<...>>)
- Cache invalidated on write operations
EOF
# Run the loop
recursive loop "read .dev/goals/ and implement the next unfinished goal"The apply_patch discipline
One metric the observation system tracks is the apply_patch : write_file ratio.
- High ratio = agent makes surgical edits → good
- Low ratio = agent kept failing
apply_patchand fell back to rewriting files → indicates poor anchoring in patches
When apply_patch fails (ambiguous context lines), the correct response is to widen the anchor — not to fall back to write_file.
Monitoring a run
Subscribe to the AgentEvent stream via a ChannelSink to monitor what the agent is doing:
use recursive::event::{AgentEvent, ChannelSink};
use std::sync::Arc;
let (sink, mut rx) = ChannelSink::new(128);
let mut runtime = AgentRuntime::builder()
.llm(llm)
.tools(tools)
.event_sink(Arc::new(sink))
.build()?;
// Spawn a task to consume events
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
match event {
AgentEvent::ToolCall { name, arguments, .. } => {
if name == "apply_patch" {
println!("Patching…");
} else if name == "run_shell" {
println!("Shell: {}", arguments);
}
}
AgentEvent::TurnFinished { reason, steps } => {
println!("Done after {} steps: {}", steps, reason);
}
_ => {}
}
}
});
let outcome = runtime.run("implement the next goal").await?;