Files
interactive-story/planning/archive/phase0/PHASE-0B-AI-ADVENTURE-OLLAMA.md
T
JesseMarkowitzandClaude Opus 5 d27ee34901 Docs: consolidate active planning and archive historical material
The planning package had grown to where a new agent could not tell what was
authoritative. Phase 0 execution prompts sat beside the specification; four
completed milestone reports sat beside the current one; and upstream AI-DnD's
own `plan/` build log and `docs/` project site still described a hosted,
scripted, multi-user product with accounts — every screenshot in it showed a
Scripts tab and a Sign up button, none of which has existed since M2.

`planning/archive/` now holds the history and says so in its own README:
`phase0/` for the research that chose AI-DnD, `milestone-reports/` for M1 and
M2, `decisions/` for ADR 008, the Phase-0-before-build gate Phase 0 satisfied.
`planning/reports/` holds only the current milestone's report, because that is
the one M4 planning has to read; it moves to the archive when M4's replaces it.

Deleted rather than archived: the Phase 0B execution prompts and the
handoff/status/summary documents, the Phase 0A discovery and triage reports,
upstream's `plan/` and `docs/` trees, and `frontend/README.md`, which was Vite's
template boilerplate. All of it is in Git history, and the two recommendation
reports carry every conclusion the deleted research reached.

Archived documents are kept verbatim. Paths written inside them point at where
those files were when the document was written, which is the point: an evidence
record that has been quietly edited is no longer evidence.

Active documentation is corrected where it pointed at the removed trees or
described removed capability as present. `DEVELOPMENT.md`'s "things M1 did not
touch" list had gone stale at M2 and claimed QuickJS scripting was still tested;
its test count was 604 against an actual 638. `README.md` loses the upstream CI
badge, which reported upstream's pipeline rather than this fork's, and a
reference to `backend/app/worldstate/engine.py`, a file that does not exist.
`planning/README.md` is rewritten as the documentation index.

New: `planning/PROJECT-SOURCES.md` and `planning/project-sources.txt`, the
manifest of what belongs in the ChatGPT project's Sources.

Source comments referring to the deleted trees are reworded; no behaviour
changes. 638 backend tests pass, frontend lints and builds, and a reference scan
over all 48 tracked Markdown files reports no unresolved path in active
documentation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCbwH7yLGKsj1rhXXzKSCu
2026-09-03 14:33:07 -04:00

4.7 KiB

Phase 0B — Experiment C: ai-adventure

C1. Tests

python -m unittest discover -s tests   →  Ran 76 tests in 1.9s  OK

C2. Provider abstraction — the Ollama adapter is zero code

llm/backend.py defines a ModelBackend Protocol with a single generate method plus typed ModelRequest/ModelResponse. llm/lm_studio.py is not LM-Studio-specific at all: it POSTs /v1/chat/completions and reads /v1/models — the same OpenAI-compatible surface Ollama serves. llm/scripted.py is a deterministic fake used by the tests.

The only change needed was two lines of world.toml:

base_url = "http://127.0.0.1:11434"     # was 127.0.0.1:1234
name     = "qwen2.5:3b-instruct"        # was "replace-with-a-local-model"
python -m local_adventure doctor --world <world>
  [PASS] SQLite FTS5 available
  [PASS] Database schema at version 2
  [PASS] World valid: Ember Hollow
  [PASS] LM Studio reachable: http://127.0.0.1:11434
  [PASS] Configured model visible: qwen2.5:3b-instruct

A real turn then committed:

TURN: (30ad56d4…, 1, 'committed', 'I search the observatory for the brass k',
       'You rummage through the dusty shelves, your fingers brushing away
        decades of cobwebs...')
MODELCALL: ('lm_studio', 'qwen2.5:3b-instruct', attempt 1, 832 bytes, errors=False)

ModelSettings.backend is typed Literal["lm_studio"], so adding a second backend means widening one literal and one factory call in cli.py — the transport itself already works. Phase 0A's "MODIFY — LM Studio primary provider" overstated this.

Caveat: with qwen2.5:0.5b the same turn failed with "The model response could not be validated; no turn was saved." That is the validation contract working as designed — the engine refuses to commit an unvalidated turn — but the typed-event contract needs a reasonably capable model, where the freeform-prose candidates tolerate a weak one.

C3. Undo / branch / checkpoint / replay — verified independently

Run directly against GameService with a temporary database, not by trusting the project's own tests:

after 2 turns:            turns=2 events=2  has_brass_key=False

UNDO
  turns 2 → 2   (DELETED 0)
  state after undo: has_brass_key=True
  abandoned turn id_3 still in DB: True

BRANCH from the restored point, then play on the branch
  branch state:  has_brass_key=True  gate_open=True
  ORIGINAL session leaked 'gate_open'?  False
  total turns in DB: 3  (nothing destroyed)

CHECKPOINT RESTORE ("before_gate")
  restored has_brass_key=True
  later turn id_3 still present: True
  turns in DB after restore: 3

This is the specification's history model, working. undo sets session.head_turn_id to the parent; branch inserts a new session sharing the same head; restore_checkpoint replays ancestry; _replay_to walks turns.ancestry(head) and reduces state_events.

C4. Schema — the target data model

sessions(session_id, ..., head_turn_id, initial_state_json)
turns(turn_id, session_id, parent_turn_id, turn_number,
      player_input, narration, status CHECK IN ('committed','failed'), model_call_id)
state_events(event_id, turn_id, sequence_number, event_type, payload_json)   -- append-only
state_cache(session_id, head_turn_id, state_json)                            -- replay cache
model_calls(..., request_hash, response_hash, parsed_response_json,
            validation_errors_json, prompt_eval_count, eval_count, duration_ms)
named_checkpoints(checkpoint_id, session_id, turn_id, name, UNIQUE(session_id,name))
summaries(summary_id, session_id, through_turn_id, kind CHECK IN ('scene','campaign'))
lore_documents + lore_documents_fts  (FTS5)

Note summaries.through_turn_id — anchored to a turn, not a count. That is exactly the property Open Dungeon lacks.

C5. Coupling to the CLI

The layering is clean: cli.py → app/commands.py → app/game_service.py / app/turn_service.py → storage/repositories.py. Authoritative state lives in state/ (events, reducer, validator) and context assembly in context/. None of that imports the CLI. A browser/API layer could sit beside cli.py and call the same services without moving state logic.

What it does not have, and would all be new work: any HTTP layer, streaming, in-app campaign setup (worlds are hand-authored TOML + Markdown on disk), embeddings or semantic memory, document import, media, and prompt inspection beyond stored hashes.

C6. Lore / FTS — reusable

lore/indexer.py + lore_documents_fts (FTS5) with content hashing and modified_ns for incremental reindex, scoped by world_id, and a kind column already carrying a classification axis. This is a good starting shape for the Canon/Reference/Inspiration tiers in IMPORTED-KNOWLEDGE-DESIGN.md, and it is deterministic and offline.