Dynamic Cheatsheet (DC-Cu) — test-time memory, the ACE precursor¶
Memory self-evolution, no labels. A black-box model keeps one persistent, self-curated cheatsheet of strategies and snippets across test queries, without weight updates and without ground-truth labels. This is the baseline ACE cites and improves on with an explicit Reflector and structured delta bullets. Runs through the shared
MethodPolicyrunner. Example:examples/dynamic_cheatsheet/dynamic_cheatsheet_cumulative.py.
| Paper | Dynamic Cheatsheet: Test-Time Learning with Adaptive Memory — Suzgun, Yuksekgonul, Bianchi, Jurafsky, Zou, 2025 (arXiv:2504.07952) |
| Upstream code | suzgunmirac/dynamic-cheatsheet@5cfe3c3 — DynamicCheatsheet_Cumulative |
| Example | examples/dynamic_cheatsheet/dynamic_cheatsheet_cumulative.py |
| Domain | Game of 24 — 36 vendored puzzles (four integers in 1–13, each solvable), 12 / 12 / 12 train / held-out / test, fully offline |
| Layer | L2 skill (blast_radius=0.2, declared on the MethodPolicy) |
| Fidelity | mechanism_microport — what the classes mean |
This port is a mechanism microport, not a paper-benchmark reproduction: the domain is a compact vendored Game-of-24 slice, not upstream's AIME / GPQA / Game-of-24 sets.
The mechanism¶
DC-Cu is two completions per test query, in order:
- Gen — answer the query
x_iwith the current cheatsheetM_iin context. - Cur — a second completion reads
(M_i, x_i, y_i)and writes the whole next cheatsheetM_{i+1}.
The curator sees the question and the model's own answer y_i. It never
sees a gold label and never sees the grader's verdict: judging whether the
answer was any good, and what is worth remembering, is the model's job. That is
what makes it test-time learning rather than supervised prompt tuning. The
cheatsheet starts as (empty) and is rewritten wholesale — there are no delta
bullets, which is exactly the gap ACE fills (and why DC can suffer the context
collapse ACE is built to avoid).
Where each piece lives¶
| Upstream mechanism | Where it lives here |
|---|---|
Generator prompt with [[CHEATSHEET]] |
GENERATOR_TEMPLATE; solve always passes the current rendered cheatsheet |
Curator prompt ([[PREVIOUS_CHEATSHEET]], [[QUESTION]], [[MODEL_ANSWER]]) |
CURATOR_TEMPLATE; propose — no slot for a gold answer or a reward |
extract_cheatsheet (<cheatsheet>…</cheatsheet>) |
extract_cheatsheet, called from CheatsheetSlot.to_diff |
cheatsheet = new_cheatsheet after every query |
CommitEveryUpdate acceptance (the default) |
cheatsheet = "(empty)" |
INITIAL_CHEATSHEET |
eval_for_GameOf24 |
examples/dynamic_cheatsheet/game24.py (exact Fraction AST evaluation, no eval) |
evolve(tasks, reward, run=gen, propose=curator,
strategy=CheatsheetSlot(initial_value="(empty)", key="cheatsheet"),
blast_radius=0.2, policies=Policies(acceptance=CommitEveryUpdate()))
strategy=— one plain-text value, replaced by each curator rewrite.propose=— the curator.acceptance=— online by default (every rewrite kept);--gatedinstalls the framework's held-out gate instead.
Online versus gated¶
Online test-time learning is not hill-climbing on a validation split, and this
is where the port diverges from evolve()'s usual strict gate:
acceptance |
A rewrite that drops useful content | |
|---|---|---|
| default (online, upstream) | CommitEveryUpdate |
kept (see the warning below for the two vetoes that had to be closed) |
--gated (offline, ACE-like) |
the framework default | rejected: held-out reward falls |
The default is the faithful one. --gated is the ACE-like offline reading: the
cheatsheet is trained on one split and only kept if it helps the held-out one.
Why online mode needs three runner fields, not one
Online DC-Cu is only online if nothing between the curator and the ledger
can veto a rewrite. The runtime has two vetoes that sit in front of the
acceptance policy, and an accept-everything AcceptancePolicy reaches
neither:
- L1 commit audit by blast radius. Above
governance.FAST_MAX(0.3) every commit is audited against the full held-out set and returnsoracle-rejectedunless it improved. The shared runner's default of 0.6 is therefore a gate whatever policy is installed.MethodPolicygained ablast_radiusfield (default 0.6, so every other port is unchanged) and the cheatsheet — skill-layer context, as ACE's playbook — declares 0.2. - Low-trust audit. The audit also opens at any blast radius once its
trust in the cheap ranking layer drops below 0.75, and trust halves
(1.0 → 0.5) the first time the cheap sample and the full held-out set
disagree about a candidate's direction. A rewrite that helps the
engine's 8-puzzle cheap sample and hurts the 12-puzzle split is then
oracle-rejectedbeforeCommitEveryUpdateis consulted.MethodPolicy.cheap_eval_tasks(defaultNone, the engine's own 8) closes it: online mode sets it to the whole held-out split, the two layers become one measurement (the evaluation cache returns identical numbers), they cannot disagree, and the audit never opens.test_engine_default_cheap_layer_vetoes_an_online_rewrite_on_disagreementpins the engine behaviour this works around, andtest_full_held_out_cheap_layer_keeps_the_rewrite_onlinethe fix. - The test split is frozen by default. The shared runner scores the
test split with a fixed artifact, so no test answer would be curated and
task i+1 would not see what task i taught — a frozen memory trained
on another split, not the Gen → Cur protocol.
MethodPolicygained an opt-intest_time_learning(defaultFalse); online mode sets it, and the runner then scores the test split as an ordered stream: solve, score,propose, apply the diff to an in-memory copy, next task.
test_the_shared_runner_really_is_ungated_online_and_gated_with_the_flag
pins both acceptance modes in all three schedulers.
What the test stream does and does not measure
The standard baseline_quality / final_quality (and therefore
quality_gain) stay frozen-artifact scores, so they compare the same
protocol as every other port. The streamed score lives only under
framework.test_stream.quality, with frozen_quality beside it, so the
gain from continued learning is a difference inside that block, not a claim
about the engine. The stream starts from the cheatsheet
the engine evolved on the train / held-out splits, not from (empty) as
upstream's single test stream does: it is transfer followed by online
learning. It is strictly sequential whatever --workers says, uses its own
stream: phases (the proposal budget check is unchanged), never writes to
the ledger, and counts its refused or unchanged updates separately from
invalid candidates. --gated is the offline reading and scores a frozen
artifact. Only strategies whose diffs are key → value replacements can
stream.
Boundaries¶
- DC-RS / DC-CumulativeRetrieval are out of scope. Retrieving the top-k past
(x, y)pairs needs an embedding / retrieval seam; this port has no embedding dependency and is DC-Cu only. It is the natural follow-up. - No code execution. Upstream lets the generator emit
EXECUTE CODE!and runs it; its headline Game-of-24 effect is the model learning to write a brute-force program and reuse it. Here snippets are text in the cheatsheet and nothing runs them, so the mechanism is preserved and the mechanism's strongest payoff is not. - A missing
<cheatsheet>block is refused, not replaced. Upstream returns the old cheatsheet; the porting policy forbids fallback substitution, so the proposal costs its candidate and is countedinvalid. - The test stream starts from the evolved cheatsheet, not
(empty), and the engine's train / held-out phase runs first; see the note above. - Parallel curators are merged. Upstream is one sequential stream. Under
evolve()several workers rewrite one base at once; contradictions go through the shared reflective merge (one model call, tag-free shape check). - The 36-puzzle slice is not upstream's Game-of-24 set, and a single model run on 12 test puzzles moves in steps of 0.083.
- ACE is the structured successor:
algo-ace.md.
Measured results — Game of 24¶
No live-model run is recorded for this port yet. What exists is the deterministic offline run the test-suite uses: a stub generator that solves a puzzle only when its cheatsheet already holds the brute-force tip, and a stub curator that writes it. It shows the plumbing, not a model's behaviour.
| mode | acceptance | test, before → after | validation | accepted | invalid | calls |
|---|---|---|---|---|---|---|
| serial / sync / async | online (CommitEveryUpdate), streamed test |
0.000 → 1.000 | 0.000 → 1.000 | 1/2 | 0 | 88 |
| serial / sync / async | --gated, frozen test |
0.000 → 1.000 | 0.000 → 1.000 | 1/2 | 0 | 64 |
The two rows agree because the stub curator never writes a harmful rewrite; the
online row's 24 extra calls are the test stream (12 solves, 12 curations — the
stub rewrites the cheatsheet to the text it already holds, so all 12 updates are
"unchanged"). The tests that separate the modes feed it one that is
(test_online_mode_keeps_every_rewrite_and_it_round_trips_the_ledger,
test_gated_mode_rejects_a_rewrite_that_empties_useful_content).
Run it¶
python -m examples.dynamic_cheatsheet.dynamic_cheatsheet_cumulative --dry-run
# online DC-Cu (every curator rewrite is kept)
python -m examples.dynamic_cheatsheet.dynamic_cheatsheet_cumulative --yes \
--provider openai --model deepseek-v4-flash --workers 2 --candidates 8
# ACE-like offline mode: the held-out gate vets every rewrite
python -m examples.dynamic_cheatsheet.dynamic_cheatsheet_cumulative --yes --gated \
--provider openai --model deepseek-v4-flash --workers 2 --candidates 8
--dry-run prints the plan and returns with zero network access and no API
key; the dataset is vendored, so build loads nothing from outside the
repository.
Offline tests: tests/test_dynamic_cheatsheet.py
and the matrix test in tests/test_candidate_methods.py, which runs the method
in every scheduler without an API key.