irregular6612 Claude Opus 4.8 (1M context) commited on
Commit
e073c00
·
1 Parent(s): 598e3bd

docs(cp7): design spec — LLM-generated memory pre-roll (hybrid handover)

Browse files

Memory generation stage: model self-plays a full-info episode of the same
scenario; trajectory persists as a per-model timestamped checkpoint and is
shown at the handover as prior experience, lengthening the believable history.
Hybrid keeps the scripted diagnostic Cut + step_reward + metrics unchanged
(127-preserving, optimal!=habit invariant intact). Offline by FakeProvider;
Ollama acceptance smoke. Curiosity/new-category deferred to CP8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

docs/superpowers/specs/2026-06-02-proteus-cp7-memory-preroll-design.md ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PROTEUS CP7 — LLM-Generated Memory Pre-roll
2
+
3
+ > CP7 sub-design under the immutable parent SSOT
4
+ > `docs/superpowers/specs/2026-06-01-proteus-arena-slice-design.md`
5
+ > (§4 Scenario contract, §6 SessionRunner, §7 trace/metrics, §11 CP table, §12 deferred).
6
+ > CP0–CP6 are complete (see `HANDOFF.md`; `.venv/bin/python -m pytest -q` → 127 passed).
7
+ > This document is the design SSOT for CP7.
8
+
9
+ **Date:** 2026-06-02
10
+
11
+ ## 0. One-line summary
12
+
13
+ CP7 adds an **LLM-generated memory pre-roll**: before the scored game, the playing model runs a
14
+ full-information self-play episode of the same scenario (the "memory generation stage"); that trajectory
15
+ is persisted as a per-model, timestamped **checkpoint** and shown at the handover as the model's *prior
16
+ experience*, lengthening the believable "this is your movement, continue" history. The scored game keeps
17
+ its hand-authored scripted Cut and diagnostic invariant **unchanged** (hybrid design), so all 127
18
+ existing tests and the `optimal_action != habit_action` measurement are preserved. Offline by default
19
+ (FakeProvider, deterministic); the acceptance smoke uses the Ollama API.
20
+
21
+ ## 1. Motivation / context
22
+
23
+ The handover framing is "what you are seeing is *your* movement — now continue." Today that history is
24
+ just the scripted Cut pre-roll (`cut_focal_policy` = "left" for `cut_length` 2–3 steps), so the model is
25
+ asked to "continue" a trajectory only 2–3 frames long. That is too thin to read as the model's own
26
+ established behaviour.
27
+
28
+ CP7 lengthens the believable history by having the LLM **generate its own pre-roll**: a transparent
29
+ full-information self-play run of the same scenario, recorded and replayed at the handover as "your
30
+ earlier play." Crucially, the **measurement** (does the model read the predator's motive at the
31
+ diagnostic Cut?) must not be weakened. A naive "let the LLM play the lead-in itself" would let a
32
+ competent model dodge the hand-authored dead-end, destroying the `optimal != habit` invariant. The
33
+ **hybrid** design resolves this: the self-generated memory is shown *in addition to* the unchanged
34
+ scripted Cut, not in place of it.
35
+
36
+ ## 2. Confirmed decisions (do not silently change)
37
+
38
+ - **Hybrid handover.** The turn-1 observation = `[MEMORY: LLM full-info self-play trajectory]` +
39
+ `[NOW: existing scripted diagnostic Cut frames]` → handover. The scripted Cut, `step_reward`,
40
+ `optimal_action`/`habit_action`, and all metrics are **unmodified** → 127 regression-free and the
41
+ diagnostic invariant preserved.
42
+ - **Memory generation = real LLM self-play.** The agent is given a transparent brief (full rules +
43
+ predator chase rule + map + goal) and **chooses each action itself**; the optimal action is never
44
+ revealed. The episode runs through the real engine (predator chases), capped at `memory_turns`,
45
+ stopping early on terminal.
46
+ - **Per-model, timestamped checkpoints.** Each generated memory is persisted as a single-file JSON
47
+ checkpoint at `runs/memory/<model>/<timestamp>.json`. `created_at` is produced by an **injectable
48
+ clock** (real UTC by default) so tests stay deterministic.
49
+ - **Reuse is flag-selected.** `proteus run --memory MODE` with `MODE ∈ {none(default), generate,
50
+ latest, <path>}` chooses whether to generate fresh, reuse the model's latest checkpoint, load a
51
+ specific file, or keep current behaviour.
52
+ - **Memory world = same scenario + difficulty + seed** as the scored game, played with full info and
53
+ free choice (the same map seen as "prior experience"). `memory_turns` default **10**.
54
+ - **Offline invariant holds.** All automated tests use FakeProvider (deterministic). The Ollama smoke
55
+ is a manual acceptance step; no provider SDK is added to `.venv`.
56
+ - **This is CP7;** the curiosity / new-motive-category work (spec §12) moves to **CP8**.
57
+
58
+ ## 3. Scope / non-goals
59
+
60
+ In scope: a memory data model + single-file persistence, a memory-generation self-play stage, hybrid
61
+ handover injection into `SessionRunner`, a scenario-sourced transparent brief, a `proteus memory`
62
+ subcommand + `proteus run --memory`, and an Ollama acceptance smoke.
63
+
64
+ Deferred (YAGNI, parent spec §12):
65
+ - **New motive categories** (curiosity, sociality) → CP8. CP7 touches only the survival scenario.
66
+ - **Generalizing `_PROBE_QUESTION` / `_ACTION_DIRECTIVE`** out of `SessionRunner`/`VanillaAgent`. CP7
67
+ adds a scenario-sourced `memory_brief` only; the probe/act-directive predator-framing stays as-is
68
+ (carried to CP8, where the second scenario forces it). This avoids scope creep.
69
+ - **Memory accumulation / multi-episode memory** (a checkpoint holds exactly one self-play episode),
70
+ LLM-as-judge scoring of memory reasoning, web UI.
71
+
72
+ ## 4. Section A — Memory data model + persistence
73
+
74
+ New module **`proteus/runtime/memory.py`** — pure pydantic models + single-file JSON I/O, importing
75
+ only pydantic + stdlib (`json`, `pathlib`, `datetime`). Like `trace.py`, it imports **no other runtime
76
+ module** (serialization boundary).
77
+
78
+ ```python
79
+ class MemoryTurn(BaseModel): # one self-play turn of the memory episode
80
+ turn_idx: int
81
+ frame_ascii: str # pre-move grid (what the model saw)
82
+ action: str
83
+ reasoning: str # excerpt (may be truncated for size)
84
+ focal_pos: tuple[int, int] # pre-move
85
+ predator_pos: tuple[int, int] # pre-move
86
+
87
+ class MemoryCheckpoint(BaseModel):
88
+ model: str
89
+ scenario: str
90
+ motive_category: str = "survival"
91
+ difficulty: str
92
+ seed: int | None
93
+ created_at: str # ISO-8601, from an injectable clock
94
+ memory_turns: list[MemoryTurn]
95
+ outcome: str # "survived" | "eliminated" (engine: budget done = survived)
96
+ transparent_prompt: str # the brief used (audit / reproducibility)
97
+ ```
98
+
99
+ Persistence helpers in the same module:
100
+
101
+ - `save_checkpoint(ckpt, root="runs/memory") -> Path` — writes
102
+ `<root>/<safe(model)>/<created_at-derived-stamp>.json` (creates parent dirs; model string sanitised
103
+ for the filesystem). Returns the path written.
104
+ - `load_checkpoint(path) -> MemoryCheckpoint` — reads one file (raises `FileNotFoundError` on miss).
105
+ - `latest_for_model(model, root="runs/memory") -> MemoryCheckpoint | None` — returns the newest
106
+ checkpoint for `model` (by filename stamp / mtime), or `None` if none exist.
107
+
108
+ The filename stamp is derived from `created_at` (e.g. `2026-06-02T10-40-56Z`) so the on-disk ordering
109
+ matches the logical creation order; ties break deterministically.
110
+
111
+ ## 5. Section B — Memory generation stage
112
+
113
+ New module **`proteus/runtime/memory_gen.py`** (engine-coupled, mirrors `rollout.py`'s replay pattern;
114
+ imports `grid`, `agents`, `ascii_view`, and the `memory` models).
115
+
116
+ ```python
117
+ def generate_memory(
118
+ scenario_name: str,
119
+ agent: Agent,
120
+ *,
121
+ difficulty: Difficulty,
122
+ seed: int | None,
123
+ memory_turns: int,
124
+ model_name: str,
125
+ clock: Callable[[], str] = _utc_now_iso,
126
+ ) -> MemoryCheckpoint: ...
127
+ ```
128
+
129
+ Flow:
130
+ 1. Build a fresh `scenario` + `MotiveGridGame` from `(scenario_name, seed, difficulty)` with
131
+ `max_steps = memory_turns` (**no scripted Cut** — the memory is free play from the initial layout).
132
+ 2. `system_prompt = scenario.memory_brief or scenario.rules_text` (the transparent brief, Section D.1).
133
+ 3. For `turn_idx` in `1..memory_turns`:
134
+ - render the pre-move observation (frame + legend + available actions);
135
+ - capture pre-move `focal_pos` / `predator_pos`;
136
+ - `result = agent.act(observation, actions, system_prompt)`;
137
+ - append a `MemoryTurn` (frame, `result.action`, `result.reasoning` excerpt, positions);
138
+ - `game.apply_motive_action(result.action)` + `scenario.record_focal_move(result.action)`;
139
+ - break if `game.eliminated or game.survived`.
140
+ 4. `outcome` = `"eliminated"` if captured, else `"survived"` (with `max_steps = memory_turns`, the
141
+ engine marks `survived` once the budget is spent — a completed practice episode).
142
+ 5. Return `MemoryCheckpoint(model=model_name, …, created_at=clock(), memory_turns=…, outcome=…,
143
+ transparent_prompt=system_prompt)`.
144
+
145
+ Determinism: with a FakeProvider (scripted actions) the checkpoint is a pure function of
146
+ `(scenario, seed, difficulty, scripted-actions, clock)`. The real engine drives the predator, so the
147
+ memory is a genuine chase episode (it may end early in capture).
148
+
149
+ ## 6. Section C — Hybrid handover injection
150
+
151
+ `SessionRunner.__init__` gains an optional `memory: MemoryCheckpoint | None = None` (default `None` →
152
+ **exactly current behaviour**, 127-preserving). The only behavioural change is in `_observation`:
153
+
154
+ - On **turn 1**, when `self._memory is not None`, prepend a labelled memory block built from
155
+ `memory.memory_turns` **before** the existing Cut block:
156
+
157
+ ```
158
+ MEMORY — your earlier play on this scenario:
159
+ Memory 1:
160
+ <frame_ascii>
161
+ you chose: up
162
+ Memory 2:
163
+ ...
164
+ NOW — this run so far:
165
+ Cut 1:
166
+ <existing scripted cut frame>
167
+ ...
168
+ Now:
169
+ <handover frame>
170
+ ```
171
+ - Turns 2+ are unchanged. The scripted Cut replay, the diagnostic snapshot
172
+ (`optimal_action`/`habit_action`), `step_reward`, `optimal_rollout`, and every metric are untouched.
173
+
174
+ The memory block is observation text only; it does **not** alter world state, the play budget, or any
175
+ answer key. Result: the handover history is materially longer while the measurement is identical.
176
+
177
+ The trace records that a memory was used via a new additive, optional field on `SessionTrace`:
178
+ `memory_ref: str | None = None` (the checkpoint path or stamp; `None` when no memory). This keeps
179
+ `compare`/`aggregate` and the existing schema backward-compatible (additive, defaulted).
180
+
181
+ ## 7. Section D — Scenario brief + CLI + smoke
182
+
183
+ ### 7.1 Scenario-sourced transparent brief
184
+
185
+ `Scenario` ABC gains a concrete class attribute `memory_brief: str = ""` (no new abstract method).
186
+ `predator_evade` sets it to a transparent description: the existing `rules_text` plus an explicit
187
+ statement that the predator takes one BFS shortest-path step toward the focal each turn, a note that
188
+ walls form dead-ends, and that this is a practice run to build experience (the optimal move is **not**
189
+ revealed). `generate_memory` uses `scenario.memory_brief or scenario.rules_text`, so scenarios that
190
+ omit a brief fall back gracefully.
191
+
192
+ ### 7.2 CLI
193
+
194
+ - **`proteus memory`** (new subcommand) — generate + save a checkpoint:
195
+ ```
196
+ proteus memory --scenario predator_evade --model fake:demo \
197
+ --difficulty easy --seed 42 --memory-turns 10 [--out PATH]
198
+ ```
199
+ Builds the agent from `--model` (same factory as `run`), calls `generate_memory`, saves via
200
+ `save_checkpoint` (auto-path `runs/memory/<model>/<stamp>.json` unless `--out`), prints the path +
201
+ the episode outcome. Exit codes mirror `run` (unknown model/scenario → 2).
202
+ - **`proteus run --memory MODE`** (new option, default `none`):
203
+ - `none` → current behaviour (no memory).
204
+ - `generate` → call `generate_memory` inline (same model/scenario/difficulty/seed), save it, inject.
205
+ - `latest` → `latest_for_model(model)`; clean stderr + exit 2 if none exists.
206
+ - `<path>` → `load_checkpoint(<path>)`.
207
+ The loaded/!-generated checkpoint is passed to `SessionRunner(memory=…)` and recorded as `memory_ref`.
208
+
209
+ ### 7.3 Offline tests (FakeProvider)
210
+
211
+ 1. **Persistence round-trip**: `save_checkpoint` → `load_checkpoint` reproduces the model; bytes are
212
+ stable; `latest_for_model` picks the newest of several.
213
+ 2. **Generation determinism**: a FakeProvider with scripted actions yields a fixed `MemoryCheckpoint`
214
+ (positions, actions, outcome) under a fixed clock.
215
+ 3. **Hybrid injection**: with a memory, turn-1 observation contains the memory frames and is strictly
216
+ longer than without; turns 2+ identical; the diagnostic snapshot and `metrics` are unchanged from
217
+ the no-memory run (same seed) → invariant + 127 preserved.
218
+ 4. **Comparability**: `memory_ref` is additive; `set(trace.metrics)` unchanged; human/LLM comparability
219
+ test still holds.
220
+ 5. **CLI**: `proteus memory` writes a loadable checkpoint headlessly; `run --memory generate` and
221
+ `--memory <path>` produce a trace with a populated `memory_ref`; `--memory latest` with no
222
+ checkpoint exits 2.
223
+
224
+ ### 7.4 Ollama acceptance smoke (manual, networked)
225
+
226
+ Using the CP4 recipe (throwaway venv with `httpx`; `.venv` stays SDK-free; the `OLLAMA_API_KEY` is the
227
+ first whitespace token of the `.env` value):
228
+
229
+ ```
230
+ proteus memory --scenario predator_evade --model ollama:gpt-oss:120b-cloud \
231
+ --difficulty easy --seed 42 --memory-turns 8
232
+ proteus run --scenario predator_evade --model ollama:gpt-oss:120b-cloud \
233
+ --difficulty easy --seed 42 --play-turns 10 --memory latest --out runs/cp7_real.jsonl
234
+ ```
235
+
236
+ Acceptance: the memory checkpoint is written under `runs/memory/gpt-oss:120b-cloud/…`, the run's turn-1
237
+ observation shows the memory block (verify via `replay`/trace), and the trace's `memory_ref` is set.
238
+
239
+ ## 8. Cross-cutting
240
+
241
+ ### 8.1 ABC / schema changes (summary)
242
+
243
+ - `Scenario.memory_brief: str = ""` — concrete class attribute (no new abstract method; all existing
244
+ scenarios keep working).
245
+ - `SessionRunner(..., memory: MemoryCheckpoint | None = None)` — optional, defaults to current
246
+ behaviour.
247
+ - `SessionTrace.memory_ref: str | None = None` — additive optional field.
248
+
249
+ No change to `step_reward`, `optimal_action`, `habit_action`, `advance_threat`, `build_level`,
250
+ `metrics`, `aggregate`, or the scripted Cut.
251
+
252
+ ### 8.2 Determinism
253
+
254
+ Memory generation is deterministic given `(scenario, seed, difficulty, agent-actions, clock)`. Tests
255
+ inject a fixed clock and a scripted FakeProvider. The scored game remains a pure function of
256
+ `(scenario, seed, difficulty)` + recorded actions; the memory block is observation-only and changes no
257
+ world state.
258
+
259
+ ### 8.3 Module layout
260
+
261
+ | File | Change |
262
+ |------|--------|
263
+ | `proteus/runtime/memory.py` (new) | `MemoryTurn`/`MemoryCheckpoint` pydantic + `save_checkpoint`/`load_checkpoint`/`latest_for_model` (no intra-runtime imports) |
264
+ | `proteus/runtime/memory_gen.py` (new) | `generate_memory` full-info self-play (engine-coupled, like `rollout.py`) |
265
+ | `proteus/grid/scenario.py` | add `memory_brief: str = ""` class attribute |
266
+ | `proteus/grid/scenarios/predator_evade.py` | set transparent `memory_brief` |
267
+ | `proteus/runtime/session.py` | accept `memory=`; prepend the memory block in `_observation` turn 1; record `memory_ref` |
268
+ | `proteus/runtime/trace.py` | add `SessionTrace.memory_ref: str | None = None` |
269
+ | `proteus/runtime/__init__.py` | export `MemoryCheckpoint`, `generate_memory`, persistence helpers |
270
+ | `proteus/cli.py` | `memory` subcommand + `run --memory MODE` |
271
+
272
+ Dependency direction (parent spec §3) preserved: `runtime` depends on `grid`; `grid`/`arc_grid` never
273
+ import `runtime`. `runtime/memory.py` (like `trace.py`) imports no other runtime module.
274
+
275
+ ### 8.4 Completion criteria (parent spec §11 analogue)
276
+
277
+ 1. Full offline suite passes (`.venv/bin/python -m pytest -q`) — 127 prior + new, no network/display/SDK.
278
+ 2. Persistence round-trip + `latest_for_model` selection tests pass.
279
+ 3. Deterministic memory generation under FakeProvider + fixed clock.
280
+ 4. Hybrid injection lengthens turn-1 observation; diagnostic invariant + all metrics unchanged at the
281
+ same seed; comparability holds.
282
+ 5. `proteus memory` + `run --memory {generate,latest,<path>,none}` headless tests pass.
283
+ 6. Ollama smoke: memory checkpoint written, run's handover shows the memory block, `memory_ref` set.
284
+
285
+ ## 9. Open questions (carry forward)
286
+
287
+ - **Reasoning size in checkpoints.** `MemoryTurn.reasoning` may be large for real models; CP7 stores an
288
+ excerpt (truncation length pinned in implementation). Revisit if full reasoning is needed for analysis.
289
+ - **Memory block rendering at scale.** With `memory_turns` = 10 and larger grids the turn-1 observation
290
+ grows; acceptable for the slice. A compact textual summary (positions only, no full frames) is a
291
+ possible future toggle.
292
+ - **Single-episode memory.** A checkpoint holds one episode; multi-episode / accumulated memory is
293
+ deferred (CP8+).
294
+ - **`memory generate` vs `latest` divergence.** `--memory generate` always uses the current model; a
295
+ reused `latest` checkpoint could be from an older model build — `memory_ref` records which was used so
296
+ analysis can tell them apart.
297
+ ```