An agentic feedback loop that evolves Rust functions.
The LLM generates type-safe code, the harness compiles and hot-swaps it into your running binary, evaluates the results, and feeds performance back to the LLM for the next iteration — bare-metal execution, zero interpreter overhead.
Declare the function signature and provide an evaluation function. The agent autonomously implements, and refines the code each iteration — the harness validates, compiles, and hot-swaps the native code in-place. Diagnostics and failures are fed back as prompt steering — constrained generation, with bare-metal execution.
symbiont::evolvable! {
fn step(counter: &mut usize) {
// default implementation body, entirely evolved by the LLM
*counter += 1;
println!("doing stuff in iteration {}", counter);
}
}
#[tokio::main]
async fn main() -> symbiont::Result<()> {
let runtime = symbiont::Runtime::new(SYMBIONT_DECLS, SYMBIONT_PRELUDE, symbiont::Profile::Debug).await?;
let agent = symbiont::inference::init_agent(None)?;
let fn_sigs = runtime.fn_sigs();
let base_prompt = format!(
"Give a concise implementation for this function signature: ```{}```, \
that increments the counter by a constant in the range (5..20). \
Give Rust Code Only.",
fn_sigs[0]
);
let mut counter = 0;
let mut last_evolution = std::time::Instant::now();
loop {
step(&mut counter); // bare-metal: calls into the hot-loaded native dylib
println!("counter: {counter}");
if last_evolution.elapsed() >= std::time::Duration::from_secs(10) {
// LLM rewrites the function, harness validates + compiles + hot-swaps
runtime.evolve(&agent, &base_prompt).await?;
last_evolution = std::time::Instant::now();
// New Agent code runs the next time `step` is called, natively.
}
}
}Similar code to the above can be run with cargo run -p counter-example in the repo to showcase the function evolution.
Here the function is evolved every 5 seconds.
Now that you grok the core concept, a whole new world of opportunity opens up...
The project uses Nix for reproducible builds. Examples run against any OpenAI-compatible inference endpoint, configured in .env:
{O}export{E} API_KEY={Y}""{E}
{O}export{E} BASE_URL={Y}"http://127.0.0.1:8000/v1"{E}
{O}export{E} MODEL={Y}"Qwen/Qwen3.8-27B-FP8"{E}
{G}# Rust nightly, cargo tools and formatters{E}
nix develop
cargo run -p counter-exampleAn interactive window whose per-pixel shader is written by the LLM and hot-swapped into the running binary as optimized native code. Type a prompt — "an animated Julia set, c orbiting the main cardioid, with a glowing sunset palette" — and the agent implements fn shade(x: f64, y: f64, t: f64) -> u32; the live animation morphs in place, no restart.
shade is called once per pixel (~0.5M calls/frame), parallelized over all cores — an interpreted agent-code loop would be orders of magnitude too slow to animate.The most complete demonstration: an LLM evolves a futures trading strategy as compiled Rust against a realistic exchange simulation. The evolvable fn decide(state: &mut State, candles: &[Candle], account: &AccountState) -> Action keeps its own State between candles, and must discover features, position sizing and fee-awareness — quantitative reasoning as hot-swapped native code.

State, so a strategy can remember its entry, wait out a cooldown or keep its own estimators between candles.The runtime forgets every evolve call on purpose, so request sizes stay bounded in long-lived processes. The optional symbiont-memory crate renders what a population learned into the next prompt — plain data owned by one task, with a sans-IO reflection step that any model can serve.
let rendered = memory.render(island, &parents);
let prompt = format!("{task}{}{hint}", rendered.text());
// ... evolve, then evaluate the candidate ...
memory.commit_trace(info.trace());
memory.commit(Evaluated::builder()
.id(source_hash).source(code).score(score).report(report)
.island(island).bullets_shown(rendered.bullets().to_vec())
.build());
if memory.reflection_due(island) {
let request = memory.reflection_request(island, &task).expect("due");
let answer = reflector.run_without_tools(request.user_prompt(), vec![]).await?.output;
memory.apply_reflection(&request, &answer)?;
}RUST_LOG=symbiont_memory=debug shows every commit, rendered section, reflection, new or pruned lesson and migration as it happens.evolve_batch runs one lane per prompt, concurrently — thirty-two slightly different prompts, thirty-two independent retry budgets, thirty-two candidate implementations. Against a server that batches (vLLM, SGLang, llama-server --parallel n) the lanes merge into shared forward passes, so a whole population round costs a fraction of the same evolutions run in a loop.
| Lanes in flight | Wall | Decode tok/s | Lanes converged |
|---|---|---|---|
| 1 | 407.1s | 72 | 18/32 |
| 2 | 679.6s | 88 | 18/32 |
| 4 | 149.8s | 196 | 18/32 |
| 8 | 102.3s | 300 | 16/32 |
| 16 | 57.2s | 475 | 16/32 |
| 32 | 40.8s | 656 | 17/32 |
The 2-lane row is an outlier: it drew a set of lanes that ground through their full retry budgets and generated twice the tokens of every other level. Retries within a lane are sequential, so they lengthen that lane's critical path — and a level's wall clock is its slowest lane.
RevisionFn handles and commit to a winner with activate_revision.cargo build.Agents express intent as Rust functions with enforced signatures. The compiler is the guardrail.
Parse errors, signature mismatches, and compiler diagnostics steer the LLM until it produces valid code.
Functions compile to native shared libraries and swap in-place via libloading — no process restart.
~1 ns dispatch overhead. Lock-free hot path via AtomicPtr. Multi-thread safe.
One lane per prompt, run concurrently and merged into shared forward passes by the inference server. A whole population per round, for a fraction of the wall clock.
Every hot-swapped dylib stays loaded and addressable. Roll back, A/B compare, or run an ensemble of past revisions concurrently — no recompilation.
<name>_fn(revision) returns a typed RevisionFn pinned to any retained revision. Ensembles, tournaments and A/B comparisons dispatch at active-pointer speed.
Declare type State in evolvable! and the agent defines the struct its functions keep between calls. The dylib owns the value; a live state pins its revision.
Optional symbiont-memory: archive of attempts, known pitfalls, an ACE playbook of curated lessons and islands — rendered into the next prompt.
Compile errors come back numbered with line numbers into the agent’s own code; it edits instead of retyping. rustc’s machine-applicable fixes are applied before the agent is asked at all.
Register any rig Tool and the agent can run tests, probe black-box systems or query data before it commits to an implementation.
The pipeline as tool calls: build, edit, read back and submit revisions inside one run, compare variants with a host evaluator, and choose the best.
Metrics for attempts, failures, LLM usage, request sizes, pipeline timings, revisions and dylib sizes via the metrics facade, with an optional Prometheus exporter.
~5.5k lines of code for the agent harness and constrained generation, without comments, blank lines and tests — ~12k for the whole crate.
Runtime panics in LLM code are caught inside the dylib and fed back as prompt context automatically.
Any OpenAI-compatible provider via rig. Local or cloud.
Symbiont is purpose-built for workloads with fast evaluation requirements and expensive state that can't be reloaded on every iteration.
The LLM evolves a futures trading strategy as compiled Rust against a realistic exchange simulation — fees, spread and margin included. The best strategy is judged on a held-out test segment.
The LLM evolves a function to satisfy a test suite. Solved in 1 evolution round.
Reverse-engineer the 2-D Rastrigin function from sample data. LLM only sees sample points and deltas. Exact formula found in 2 rounds.
Evolve a sorting algorithm from scratch. Bubble sort to custom, optimized implementation — 1000x improvement.
The LLM evolves a game-playing function tested against random, smart, and minimax opponents. Discovers center control, fork creation, and blocking strategies.
Evolve a quantization function that minimizes reconstruction error (MSE) while reducing distinct output levels. Tracks a Pareto frontier across 20 rounds.
The function specification is deliberately withheld. The agent must call a probe tool granting black-box access to a hidden rule, experiment until the behaviour is clear, then implement it as native code.
The simplest example: a counter function hot-swapped every 5 seconds with a new LLM implementation.
An island population on a symbolic-regression task: an archive of attempts, pitfalls from the traces, one reflection call per island and round curating an ACE playbook, ring migration and a near-copy check.
Eight prompt variants evolved concurrently, benchmarked against each other through pinned revision handles, and only the fastest correct one activated.
An interactive canvas whose per-pixel shader the LLM writes and hot-swaps while the animation keeps running. The user is the evaluator.