@@ -33,34 +33,34 @@ Recursive Experiential–Working Memory Evolution for Long-Horizon Agent Harness
3333
3434## 💡 Introduction
3535
36- An agent that fails a task the same way twice has a ** memory** problem, not a
37- capability problem. ** Recuris** separates the two: a ** frozen machine** that
38- never changes between arms, and an ** evolved memory** ` M = (E, W, ρ, C) ` that
39- does. A meta-agent proposes changes to the memory; a deterministic gate decides
40- whether they are kept, using paired held-out evidence and nothing else.
41-
42- That split is physical in this repository. ` src/recuris/ ` is the machine.
43- ` skill_memories/ ` is the memory, and it is ** the only thing that differs**
44- between a bare arm and a skill arm.
36+ ** Recuris** is a recursive self-improvement framework that ** improves a
37+ long-horizon agent by evolving its memory rather than its weights or its
38+ prompt** . A frozen agent is paired with a ** Skill Memory** ` M = (E, W, ρ, C) ` ;
39+ a meta-agent reads structured execution traces, localises each failure to one
40+ component of that memory, and patches only that component — and a deterministic
41+ validation gate decides on paired held-out evidence whether the patch survives.
42+ Recuris has the following key features:
43+
44+ - ** State-grounded memory use** — working memory drives skill invocation, so
45+ retrieval is conditioned on verified task state instead of a chat history that
46+ grows until the state is buried.
47+ - ** Targeted memory evolution** — structured trajectories ` (w_t, E_t, a_t, o_t) `
48+ attribute a failure to a specific component, instead of nudging a monolithic
49+ prompt from outcomes alone.
50+ - ** Bounded by a validation gate** — candidates are admitted by paired held-out
51+ arithmetic and nothing else. No model votes on its own patch, so a round that
52+ accepts nothing is a valid outcome.
53+ - ** Training-free and model-agnostic** — the downstream agent stays frozen, and
54+ a memory evolved on one model transfers to others unchanged.
55+
56+ Overall, Recuris delivers ** higher task success** , ** larger gains as the horizon
57+ grows** , and ** substantially fewer long-horizon failures** , across both frontier
58+ and open-weight agents.
4559
4660<p align =" center " >
4761 <img src =" assets/motivation.png " width =" 1000 " >
4862</p >
4963
50- Prior memory-based harnesses use memory ** statically** — retrieval competes with
51- a chat history that grows until the task state is buried — and evolve it from
52- ** outcomes only** , which says a trajectory failed but not * which part of memory*
53- was responsible. Recuris changes both ends:
54-
55- - ** Working Memory drives invocation.** ` W ` tracks what the task still needs and
56- selects from Experiential Memory on that basis, so skill retrieval is grounded
57- in verified current state rather than in a growing transcript.
58- - ** Evolution reads structured trajectories.** Each episode leaves
59- ` (w_t, E_t, a_t, o_t) ` , which lets a failure be attributed to one component and
60- patched * there* — instead of nudging a monolithic prompt and hoping.
61- - ** A gate, not a judge, decides.** Candidates are admitted only on paired
62- held-out evidence. Nothing in ` gates.py ` consults a model.
63-
6464> Across four long-horizon benchmarks and ten models, Recuris improves task
6565> success in ** 35 of 37** completed model–benchmark pairs, adding ** +17.8** to
6666> GPT-5.6 Sol and ** +15.6** to Claude Opus 5 on τ²-Bench — taking Opus 5 to
@@ -187,29 +187,241 @@ error.
187187
188188## 🚀 Quick Start
189189
190- Fetch τ²-Bench, then run a paired comparison:
190+ There are two things to do with this repository, and they are different jobs:
191+
192+ | | |
193+ | ---| ---|
194+ | ** [ Part 1 — Inference] ( #part1 ) ** | take a Skill Memory we already evolved, load it into a frozen agent, and measure what it buys |
195+ | ** [ Part 2 — Recursive Self-Improvement] ( #part2 ) ** | evolve a * new* Skill Memory for your own model, one gated round at a time |
196+
197+ Every example below is a ** pair** : a skill arm and a bare control that differ in
198+ the flags shown and in nothing else. Run both, or the number means nothing.
199+ [ Running Experiments] ( #-running-experiments ) has the full flag reference, the
200+ costs, and the caveats.
201+
202+ ---
203+
204+ <a name =" part1 " ></a >
205+
206+ ### Part 1 — Inference with a provided Skill Memory
207+
208+ <br />
209+
210+ #### 1a. Open-weight on τ²-Bench — Qwen3.6-27B
211+
212+ Serve the model on any OpenAI-compatible endpoint, then unfreeze ** only** the
213+ downstream agent — the user simulator and the assertion judge stay pinned, which
214+ is what keeps the comparison single-variable:
191215
192216``` bash
193- bash third_party/tau2/setup.sh
194- uv pip install -e external/tau2-bench
195- recuris check-data --benchmark tau2
217+ bash third_party/tau2/setup.sh && uv pip install -e external/tau2-bench
218+
219+ vllm serve Qwen/Qwen3.6-27B --port 8000 --served-model-name qwen3.6-27b
196220```
197221
198222``` bash
223+ export TAU2_GATE_TERM=1 TAU2_GATE_TERM_WM=1 TAU2_STATUS_BOARD=1
224+
225+ export SERVED=openai/qwen3.6-27b
226+ export ARGS=' {"api_base":"http://127.0.0.1:8000/v1","api_key":"dummy","temperature":0.0,"timeout":360,"num_retries":2,"extra_body":{"chat_template_kwargs":{"enable_thinking":false}}}'
227+
199228# skill arm
200229recuris tau2 --domain retail --agent recuris_agent --skill-memory tau2_retail \
201- --num-trials 4 --max-concurrency 4 --save-to retail_skill
230+ --open-downstream --agent-llm " $SERVED " --agent-llm-args " $ARGS " \
231+ --num-trials 4 --max-concurrency 4 --save-to retail_qwen27b_skill
202232
203- # bare control — the same command, two flags different
233+ # bare control — same endpoint, same decoding, no memory
204234recuris tau2 --domain retail --agent llm_agent \
205- --num-trials 4 --max-concurrency 4 --save-to retail_bare
235+ --open-downstream --agent-llm " $SERVED " --agent-llm-args " $ARGS " \
236+ --num-trials 4 --max-concurrency 4 --save-to retail_qwen27b_bare
206237
207- recuris compare --a retail_skill --b retail_bare
238+ recuris compare --a retail_qwen27b_skill --b retail_qwen27b_bare
239+ ```
240+
241+ Airline is the same with ` --domain airline --skill-memory tau2_airline ` .
242+
243+ ` --agent-llm-args ` must be ** byte-identical between the two arms** . It is
244+ validated rather than merged, so a misspelled key is an error instead of a
245+ silent drop — setting it once in a shell variable is the reliable way. The
246+ ` enable_thinking: false ` in ` extra_body ` is Qwen-specific; drop it for a server
247+ that does not take it.
248+
249+ #### 1b. A small open-weight model — Qwen3.6-4B
250+
251+ Identical, with a different served model:
252+
253+ ``` bash
254+ vllm serve Qwen/Qwen3.6-4B --port 8000 --served-model-name qwen3.6-4b
255+
256+ export SERVED=openai/qwen3.6-4b
257+ # ... same $ARGS, same two commands, --save-to retail_qwen4b_{skill,bare}
258+ ```
259+
260+ Worth calibrating expectations at this size. In our measurements a ~ 3–4B agent
261+ still gains on τ²-Retail — Granite-4.1-3B goes 9.7 → 23.0 (** +13.4** ) — but on
262+ SkillFlow the same class of model scores near zero in ** both** arms, which is a
263+ capability wall, not a memory effect. A Skill Memory can only recover failures
264+ the model is otherwise capable of avoiding. Always read the bare arm first: if
265+ it is at the floor, the pair has nothing to say.
266+
267+ #### 1c. Open-weight on SkillFlow — Qwen3.6-27B
268+
269+ ``` bash
270+ uv sync --extra skillflow && pip install huggingface_hub
271+ bash third_party/skillflow/setup.sh
272+ ./external/SkillFlow/docker/harbor-cli-base/build.sh
273+ python external/SkillFlow/utils/prebuild_task_images.py \
274+ --tasks-root external/SkillFlow/test_tasks
275+ ```
276+
277+ ``` bash
278+ export SERVED=openai/qwen3.6-27b
279+ export SERVED_BASE=http://127.0.0.1:8000/v1
280+
281+ recuris skillflow render-configs --arm bare \
282+ --model " $SERVED " --base-url " $SERVED_BASE " \
283+ --out configs/skillflow/generated
284+
285+ recuris skillflow render-configs --arm skill --routing default \
286+ --model " $SERVED " --base-url " $SERVED_BASE " \
287+ --skill-memory skillflow --out configs/skillflow/generated
288+
289+ # one job at a time -- concurrent harbor jobs exhaust the Docker IPv4 pool
290+ for cfg in configs/skillflow/generated/bare_* .yaml; do harbor run -c " $cfg " --yes; done
291+ for cfg in configs/skillflow/generated/skill_* .yaml; do harbor run -c " $cfg " --yes; done
292+
293+ recuris skillflow score --bare jobs/bare --skill jobs/skill
208294```
209295
210- The two commands differ in ` --agent ` and ` --skill-memory ` , and ** in nothing
211- else** . That is the design: the machine is identical, the memory is the
212- variable.
296+ Use ` --routing default ` for any model. ` --routing frozen_insample ` additionally
297+ applies six per-family overrides that were chosen by reading those families' own
298+ scores; it reproduces our reported arm, and it is in-sample.
299+
300+ #### 1d. Frontier models on τ²-Bench — GPT / Claude
301+
302+ Same shape, pointed at a provider endpoint. Frontier arms in the paper ran with
303+ the three treatment switches ** off** :
304+
305+ ``` bash
306+ unset TAU2_GATE_TERM TAU2_GATE_TERM_WM TAU2_STATUS_BOARD
307+
308+ export ARGS=' {"api_base":"' " $OPENAI_BASE_URL " ' ","api_key":"' " $OPENAI_API_KEY " ' ","temperature":0.0,"timeout":360,"num_retries":2,"reasoning_effort":"high","allowed_openai_params":["reasoning_effort"]}'
309+
310+ export MODEL=openai/< provider-model>
311+
312+ recuris tau2 --domain retail --agent recuris_agent --skill-memory tau2_retail \
313+ --open-downstream --agent-llm " $MODEL " --agent-llm-args " $ARGS " \
314+ --num-trials 4 --max-concurrency 4 --save-to retail_frontier_skill
315+
316+ recuris tau2 --domain retail --agent llm_agent \
317+ --open-downstream --agent-llm " $MODEL " --agent-llm-args " $ARGS " \
318+ --num-trials 4 --max-concurrency 4 --save-to retail_frontier_bare
319+
320+ recuris compare --a retail_frontier_skill --b retail_frontier_bare
321+ ```
322+
323+ Replace ` <provider-model> ` with the provider's id for GPT or Claude as your
324+ gateway spells it. Anything the provider requires goes in ` $ARGS ` and must be
325+ named — unknown keys are rejected, not dropped.
326+
327+ ---
328+
329+ <a name =" part2 " ></a >
330+
331+ ### Part 2 — Recursive Self-Improvement: evolve a Skill Memory
332+
333+ This is the loop the paper is about. A meta-agent (** upstream** ) reads failed
334+ trajectories from the agent being improved (** downstream** ), patches one memory
335+ component, and a gate admits the patch only on paired held-out evidence.
336+
337+ ``` bash
338+ uv sync --extra metaagent
339+ npm install -g @anthropic-ai/claude-code
340+ ```
341+
342+ ``` bash
343+ RECURIS_META_MODEL=... # the upstream meta-agent's model
344+ RECURIS_META_BASE_URL=...
345+ RECURIS_META_API_KEY=...
346+ ```
347+
348+ ``` bash
349+ # fails on the plumbing before you spend benchmark budget: one scoped
350+ # session, zero simulations
351+ recuris metaagent qualify --run-id qsmoke --proxy-port 4047
352+ ```
353+
354+ <br />
355+
356+ #### 2a. τ²-Bench with Doubao upstream and downstream
357+
358+ The reported configuration. ` --worker-model ` is the downstream agent being
359+ improved, ` --meta-model ` is the upstream meta-agent, and ` --simulator-model ` is
360+ the τ² user simulator, which stays frozen in every arm:
361+
362+ ``` bash
363+ recuris metaagent run --domain retail --run-id retail_doubao_v1 \
364+ --splits splits/tau2/retail_from0_v1_k4.json \
365+ --rounds 4 --k 4 --arm autonomous --base neutral \
366+ --meta-model doubao-seed-2-1-pro-260628 \
367+ --worker-model doubao-seed-2-0-pro-260215 \
368+ --simulator-model doubao-seed-2-0-pro-260215 \
369+ --round-gate progressive --power-gate warn --reg-cap 1 \
370+ --meta-workflow hierarchical --diagnosis-workers 3 \
371+ --max-concurrency 6 --max-sims 1400 --proxy-port 4047
372+ ```
373+
374+ Those three model flags are the defaults, so they can be omitted; they are
375+ spelled out here because which model plays which role is the thing readers most
376+ often get backwards. ` --base neutral ` starts from a deterministic seed package,
377+ so no hand-written domain profile enters the loop.
378+
379+ <a name =" part2b " ></a >
380+
381+ #### 2b. τ²-Retail with an open-weight downstream — GPT-OSS-20B
382+
383+ Same upstream, different agent being improved. ` --open-worker ` unfreezes the
384+ downstream only; the simulator stays on the reference model, so rounds stay
385+ comparable to each other and to every arm in Part 1:
386+
387+ ``` bash
388+ vllm serve openai/gpt-oss-20b --port 8000 --served-model-name gpt-oss-20b
389+ ```
390+
391+ ``` bash
392+ recuris metaagent run --domain retail --run-id retail_gptoss_v1 \
393+ --splits splits/tau2/retail_from0_v1_k4.json \
394+ --rounds 4 --k 4 --arm autonomous --base neutral \
395+ --meta-model doubao-seed-2-1-pro-260628 \
396+ --open-worker --worker-model openai/gpt-oss-20b \
397+ --worker-llm-args ' {"api_base":"http://127.0.0.1:8000/v1","api_key":"dummy","temperature":0.0,"timeout":360,"num_retries":2,"stop_token_ids":[200002,200012]}' \
398+ --simulator-model doubao-seed-2-0-pro-260215 \
399+ --round-gate progressive --power-gate warn --reg-cap 1 \
400+ --meta-workflow hierarchical --diagnosis-workers 3 \
401+ --max-concurrency 6 --max-sims 1400 --proxy-port 4047
402+ ```
403+
404+ ` --worker-llm-args ` takes ` api_base ` , ` temperature ` (0.0), ` timeout ` (360) and
405+ ` num_retries ` (2) as required keys, plus optional ` api_key ` , ` extra_body ` ,
406+ ` stop_token_ids ` , ` max_tokens ` , ` reasoning_effort ` and ` allowed_openai_params ` .
407+ It is checked by the same validator the standalone ` --open-downstream ` arm uses,
408+ so a campaign arm and a hand-launched arm are admitted on identical terms. The
409+ ` stop_token_ids ` above are GPT-OSS's harmony stop tokens: without them vLLM does
410+ not end the turn on a tool call, and the agent looks incapable when it is only
411+ mis-served.
412+
413+ Evolving * for* a specific downstream is worth doing rather than reusing a
414+ package evolved elsewhere. On GPT-OSS-20B a rebuilt package gained ** +10.2**
415+ where the general-purpose package transferred * negatively* .
416+
417+ > 💸 ** Cost.** A campaign is days of wall-clock and thousands of model calls —
418+ > our retail campaign at k=4 spent roughly eleven hours on the first two rounds
419+ > alone. Start with ` qualify ` , then a 1-round run, before committing budget.
420+
421+ Each round leaves a complete record: the evidence the session was given, the
422+ plan it produced, the lint and probe verdicts, the gate arithmetic, and the
423+ ledger entry. ** A round that accepts nothing is a result** — the gate rejecting
424+ everything is the gate working.
213425
214426## 🧪 Running Experiments
215427
@@ -415,6 +627,12 @@ domain profile enters the loop. The split file states its own selection rule,
415627including which tasks were excluded and why, which is the only credible evidence
416628the held-out set was fixed in advance.
417629
630+ To evolve a memory * for* an open-weight downstream, add ` --open-worker ` with
631+ ` --worker-model openai/<served-name> ` and ` --worker-llm-args ` ; see
632+ [ Part 2b] ( #part2b ) . The
633+ upstream meta-agent and the user simulator stay where they are — only the agent
634+ being improved moves.
635+
418636Each round leaves a complete record under the run directory: the evidence the
419637session was given, the plan it produced, the lint and probe verdicts, the gate
420638arithmetic, and the ledger entry. ** A round that accepts nothing is a result** —
0 commit comments