diff --git a/plans/0000-roadmap.md b/plans/0000-roadmap.md index 8031e02..7ad6c91 100644 --- a/plans/0000-roadmap.md +++ b/plans/0000-roadmap.md @@ -106,16 +106,14 @@ the real design happens. The blurbs here stay deliberately loose. player earns them. The campaign log and transcript fuel it all. *You can now: meet the innkeeper without being told she's a retired assassin.* ([0010](completed/0010-context-stack.md)) -- [ ] **Phase 9: Session state and the context budget.** The missing - half of the context stack. A session-state domain holds the facts that - are only true right now: the current scene, the party, the NPCs in it, - and later the character's HP and spell slots. An entry's - `context: true` stays for what is always true; session state carries - what the moment makes true. The context stack reads it every turn - under a token budget, `recall` gets a result cap to match `lookup`'s, - and the conversation history gets a policy for growing old gracefully - instead of dying on a context-length error. *You can now: leave the - tavern, and the tavern leaves the prompt.* +- [ ] **Phase 9: Session state and the context budget.** The DM's + narration carries wikilinks that name entities, and the engine scans + those references to inject the right facts into each turn's prompt. + Entities the DM names become real as stubs, the DM's attention is what + keeps a fact alive, and the transcript is the scene — restarting a + session replays enough of it for the DM to pick up where it left off. + *You can now: leave the tavern, and the tavern leaves the prompt.* + ([0011](0011-session-state.md)) - [ ] **Phase 10: Persistence.** Character sheet, world entities, and worlds that live somewhere real on disk. This phase decides the world root, the one-line decision three plans have stepped around. A fresh diff --git a/plans/0011-session-state.md b/plans/0011-session-state.md new file mode 100644 index 0000000..4469818 --- /dev/null +++ b/plans/0011-session-state.md @@ -0,0 +1,147 @@ +# 0011: Session state and the context budget + +## What we're building + +The context stack (phase 8) pushes `context: true` entries and layer +`system.md` fragments into every turn's prompt. It has no idea what is +true right now. Walk into a tavern, walk out of it, and the tavern's +`context: true` entry stays in the prompt forever. The DM knows the +innkeeper is a retired assassin, but the DM does not know whether the +party is still in the inn. + +This phase gives the DM that knowledge. The mechanism is the DM's own +narration: the DM writes wikilinks when it references entities, and the +engine scans those references and injects the referenced facts into the +next turn's prompt. The transcript becomes the scene, and restarting a +session replays enough of it for the DM to pick up where it left off. + +## The problem with `context: true` + +An entry tagged `context: true` is pushed into every turn, forever. It +works for permanent facts — the core rules cheat sheet, the DM's +personality — but it is the wrong mechanism for transient facts like the +current location, the NPCs present, or the character's active conditions. +A flag that never turns off cannot represent something that stops being +true. + +We are not removing `context: true`. The SRD layer's `system.md` and +persistent lore entries still need it. But session-level facts move to a +different channel. + +## Wikilinks as context pointers + +When the DM narrates, it uses wikilinks to name entities: + +``` +Vera Blackwater +``` + +The DM writes `Vera Blackwater` in the narration, the engine +sees `Vera Blackwater` as the canonical name, resolves it to the entity +file, and injects that entity's `## Is` section (description, knows, +wants, will) into the next turn's prompt. The alias direction matters: +the part before the pipe is always the canonical address, so there is no +ambiguity about which entity to pull. + +If the DM writes `Vera` without a pipe, the engine does a best-match +resolution. If exactly one entity matches, it resolves. If more than one +matches, the engine reports the ambiguity and injects nothing. + +## Auto-establishment of stubs + +When the DM uses a wikilink that does not resolve to any existing entity, +the engine creates a stub: + +``` +worlds/{world}/entities/{canonical-name}.md +``` + +The stub contains only a name and two empty sections: + +```markdown +# Vera Blackwater + +## Is + + + +## Was + +``` + +The stub goes into the current turn's context (it adds only the name, +which is not much) and the entity is registered so future wikilinks +resolve to it. The DM can later call `establish` to fill in description, +knows, wants, will, which also moves the entity from the flat +`entities/` namespace into the typed directory (`npcs/`, `locations/`, +`items/`, etc.). + +A stub that is never established costs nothing. A future planner agent +can sweep stubs between sessions and establish the ones that matter. + +## The wikilink scanner + +The scanner is a listener on the TurnBus. Every delta of the DM's +narration passes through it. It extracts wikilinks, resolves them, and +accumulates the set of entities referenced this turn. Before the next +turn, those entities' `## Is` sections are pushed into the prompt. + +Entities referenced this turn stay in context for the next turn. An +entity that falls out of the narration falls out of the prompt. The DM's +attention is what keeps a fact alive. + +## The transcript as the scene + +The DM's own words are what describe the scene. "You enter the Rusty +Anchor. Vera Blackwater is at the bar, watching the door." That is the +scene. The transcript preserves it. No separate `set_scene` tool is +needed — the conversation is the record. + +On restart, the engine replays the last N turns of the transcript into +the conversation history. The wikilinks in the DM's narrated lines tell +the scanner which entities to load. The restart prompt carries the same +entities the DM was talking about last session, plus a campaign log +summary for the broader timeline. + +## Rendered wikilinks + +In the terminal, a wikilink renders as bold text using the display form +(the part after the pipe, or the canonical name if there is no pipe). +`Vera Blackwater` shows as **the old assassin**. The raw +wikilink syntax is preserved in the transcript file on disk, so the +engine can re-scan it on restart. + +## What this does not cover + +- **Character sheet.** HP, spell slots, inventory, conditions. That is + phase 10+, and its design will start from this phase's vocabulary. +- **The context budget.** The prompt grows with every entity the DM + references. A token budget that decides what to drop has to exist, but + it is not urgent while entities are small stubs. It will be a natural + follow-on once character sheets and richer entities push the prompt + toward a limit. +- **The planner agent.** A background agent that sweeps stubs and + establishes the important ones. Its design starts after this phase + ships, when we have real stubs to look at. + +## Implementation order + +1. The wikilink scanner as a TurnBus listener — extract `[[...]]` + patterns from narration deltas, resolve canonical names +2. Auto-establishment — create stub entity files when a wikilink does + not resolve +3. Context injection — push referenced entities' `## Is` sections into + the next turn's prompt +4. Wikilink rendering — format as bold in the terminal, preserve raw + syntax in the transcript +5. Restart mechanism — replay last N turns of the transcript, scan + wikilinks, inject context + +## Relationship to the Python prototype + +The Python prototype on `python-and-claude` had a `set_scene` tool that +the DM called after every turn to update location, present entities, and +a situation summary. It served the same purpose — giving the DM knowledge +of what is true right now — but it required the DM to explicitly +summarize every turn. The transcript makes that redundant. The DM already +said what is happening; the engine just needs to notice.