Skip to content

Latest commit

 

History

History
71 lines (63 loc) · 19 KB

ROADMAP.md

File metadata and controls

71 lines (63 loc) · 19 KB

Roadmap — StoryDaemon

Status: active · updated 2026-09-01

An agentic long-form fiction generator: an autonomous LLM agent runs iterative "story tick" cycles (plan → tools → write → evaluate) that grow characters, lore, and plot emergently rather than from a pre-plan. Python, multi-backend. The active strategic plan is docs/EMERGENT_COHERENCE_PLAN.md.

Shipped

  • Agentic story-tick loop (plan → tool use → write → evaluate)
  • Multi-backend LLM support (Codex / Gemini / Claude Code CLIs + OpenAI / Claude / Gemini APIs / Self-hosted OpenAI compatible)
  • Deep-POV writing with POV validation + character-continuity checks
  • Dynamic memory — characters, locations, factions, relationships, scenes persist and evolve
  • ChromaDB vector search for semantic context retrieval
  • Story Foundation system (immutable constraints + interactive setup)
  • Emergent plot-first mode — rolling-horizon beats, beat-constrained writing + verification (Phase 2)
  • Multi-stage planner (strategic → semantic → tactical, with token reduction)
  • Tension tracking (LLM 0–10 scoring, dynamic pacing, arc-pressure control)
  • Arc-phase planner mandate (rising/peak/falling/resolution derived from the tension curve; per-phase event mandates into the planner, futile-rewrite skip, arc_phase in metrics) (Phase 3). Validated on a descent re-run (docs/progress_report_20260709.md): de-escalation largely fixed (final scene 8 to 6 against a target of 4; resolution drift 2.35 to 1.65)
  • Beat-generation bridge: arc tension targets + phase feed plot beat generation (per-beat schedule, per-phase authoring directives, sanitize-not-trust reconciliation of authored targets; both generation paths) (Phase 3)
  • Contracts Slice 1: beat-embedded pre/postconditions authored at beat-generation time from a closed checker vocabulary, evaluated at beat verification (generation.use_contracts, default off) (Phase 3)
  • OpenRouter support in the api backend (model openrouter, OPENROUTER_API_KEY/OPENROUTER_MODEL), live-validated
  • Venice support in the api backend (model venice, VENICE_API_KEY/VENICE_MODEL; Venice's injected system prompt disabled per request)
  • Shared llm-backends package: the six novel_agent/tools/ backend modules are now compatibility shims over it; the fiction persona is passed explicitly on the api backend (tools/llm_setup.py) and DEFAULT_API_MODEL is read from the package instead of hardcoded
  • Write-until-concluded scene loop: scenes are sized from generation.scene_word_targets and continued in bounded segments instead of truncating at a flat token ceiling (Phase 3)
  • Honest loop accounting (interleaving Slice 0): judged loop closure (coherence.loop_closure), creation-time dedup and a per-tick creation cap, judged extractor resolutions (Phase 3)
  • Sacred finale (coherence.sacred_finale): on plot-first runs Python owns the final tick — guaranteed beat ask, fresh re-rolls against a finale tension cap, and quarantine of the finale's freshly minted loops (Phase 3)
  • Story-thread registry (interleaving Slices T1/T1.5): threads minted in Python and selected by the beat author via thread_id, per-tick attribution, novel threads (Phase 3)
  • Tension-curve presets (coherence.curve_preset) grounded in the masters decile tables, plus the construction-pressure detector (instrumentation only, Slice T4a) (Phase 3)
  • Hardening batch: beat-level dedup at authoring time (generation.beat_dedup), a per-call llm.timeout honored on the api backend, loop-dedup threshold recalibration (Phase 3)
  • Contradiction detection (lore contradictions, dispute quarantine)
  • Per-tick coherence rubric (loop churn, contradictions, tension vs. target, goal relevance) via novel metrics
  • Goal hierarchy (immediate / arc / story goals, auto-promotion, throughline gate + LLM goal-relevance judge)
  • Lore system (rules, constraints, facts, capabilities per tick, dedup)
  • Checkpointing (project snapshots + restore)
  • Manuscript compilation (Markdown / HTML / prose / EPUB / PDF export, scene filtering)
  • Grounded name generator (Phase 1): seven person-name banks (syllable-coined scifi/fantasy, drawn lists for modern, historical, victorian, western, gothic), per-genre place and rank-roster banks, and a names: config block (person_bank/place_bank/title_bank/register, each auto by default) settable per project, via novel new --name-bank, or in the wizard. Bank choice is the author's: name.generate no longer advertises a genre parameter, so the model selects a name but never its source
  • Full CLI (novel new, tick, run, resume, status, goals, lore, compile, plot, …)
  • Neutral-CWD isolation for CLI backends (avoid repo-awareness derailment)
  • Scene layer (L2) induction, negative result: the excursion-return excess that Section 7 of the masters study read as evidence for a level above the block is second-order memory, not hierarchy. A plain second-order chain reproduces it (0.353 vs the masters' 0.355, cross-validated across four book-level folds) where first-order gives 0.234, and no fitted HMM matches it at any state count tried. Three recognisable scene types do emerge and sit close to the constants already shipping. Tooling: scripts/scene_layer_hmm.py (LLM-free). Closes gap 1 of the study's next steps: it retires inducing L2 from block labels, not the idea of a scene. Scenes are real and scene-sized here (the induced conversation state runs 1,000 to 1,500 words), and the scene question that matters is about intent (same place, cast, purpose), which block labels cannot see at all and which needs a different annotation pass to measure. Note the scope: all of this concerns the plan generator, not the proportion control the DSL exists for, which lives in the writer prompt and is unaffected. Details in docs/MASTERS_BLOCK_GRAMMAR_STUDY.md section 12
  • Chapter-length calibration (generation.scene_length_preset, default house and byte-identical): Gate A run against the production sampler for the first time (experiments/block_grammar_poc/gate_a_production.py) passes 25/25 at chapter scale but fails DIALOGUE share at the 1400-word default (0.510 vs 0.565). It is a length effect: scene maps to chapter 1:1, the grammar's opening orientation is measured per chapter, and the house targets sit below the corpus p10 with the house maximum below the per-chapter average of every book in it (masters median 3,165 words; shortest book 1,989). The opt-in masters preset (1,600/2,150/3,150/4,450) scores 24/24. Default unchanged: adopting it roughly doubles prose per tick, a cost and pacing decision. Details in docs/MASTERS_BLOCK_GRAMMAR_STUDY.md section 13
  • Masters block grammar + evaluation harness: block-transition grammar, chapter boundary rules, run lengths, and the excursion-return kernel measured on the 21-masterwork nd1 corpus (docs/MASTERS_BLOCK_GRAMMAR_STUDY.md, data in novel_agent/data/block_grammar_v1.json, 26-book refresh pre-validated); reusable gates methodology in experiments/block_grammar_poc/ (statistical Gate A, compliance Gate B, outcome-A/B Gate C, corpus-judge scene scoring)
  • Scene skeletons (contracts Slice 4), generation.enable_scene_skeleton (default off): typed paragraph plan sampled from the masters grammar (planner-authored scene_skeleton wins when present), sized by the scene word target, tension-conditioned, carried in the writer prompt with per-item [n] markers, stripped with compliance recording (both halves: every plan item written, and nothing written that was not a plan item; recounted at commit time against the text actually saved, flagging rewritten_after_planning on a mismatch). The plan, its compliance, the section seams and the segment accounting are persisted on the committed scene's metadata, so a finished scene can be audited block by block. A scene written to a paragraph plan is not offered the step 7.6 whole-scene prose rewrite, which knew nothing about the plan and collapsed it; it gets the selective paragraph revision below instead. Production A/B vs unguided (gpt-5.5): every solidly measured block statistic moved toward the masters; 16/16 marker compliance on all production scenes; no surface regression (docs/SLICE4_SCENE_SKELETON_RESULTS.md)
  • Pipeline hardening from the skeleton trials: character/location IDs can no longer leak into prose as names ("C0"); a failing scene evaluation with an empty issues list is non-fatal; fact extraction survives explicit JSON nulls
  • 800+ tests (unit, integration, manual scenarios)

Next

  • Loop-aging pressure (older open loops surface louder for payoff) — the gauge landed, the pressure has not. Slice L1 ships agent/loop_aging.py, a loop_age payload per tick, novel loops, and the corpus baseline in docs/LOOP_AGING_BASELINE.md; nothing reads any of it for decisions. What the baseline changed about the job: across 26 novels only 220 of 1,548 loops ever closed (14%), and closure is near-immediate or never (60% of closures land within three ticks of birth), but a third of the ledger is not debt at all — scene-local suspense that carries its own deadline ("before she leaves the building"), which the story answers by continuing and the closure judge rightly refuses to close, plus 4% that restate the story's own throughline and are meant to stay open to the last page, plus POV-asymmetric questions the reader already knows the answer to (one surveillance-POV scene minted six). So every loop now carries a horizon and only arc loops can be overdue. Candidate (b) of the two, creation-side hygiene, has now shipped (Slice L2a, coherence.loop_scene_local_filter): the extraction prompt states what an open loop is for the first time (three exclusion tests, and an empty list is a legitimate answer), with a deterministic refusal at creation for the blatant cases. Unvalidated: a run has to show a lower scene share in loop_horizon_counts without a drop in arc debt, since an extractor told to prefer few loops could stop registering the real thing too. Candidate (a) remains open: age-weighted selection plus an overdue section, scaled by compute_arc_phase so it does not fight the arc-phase mandate. The number to read first is loop_shown_oldest_age against loop_unshown_oldest_age, because _filter_relevant_loops ranks by overlap with the intention the planner just wrote and therefore hides the oldest loops by construction. Classifier honesty: 70% agreement with a 60-loop hand-labelled sample, and its errors over-claim the two exempt horizons, so loop_overdue_total is a floor on the real debt
  • Thread interleaving (tension by scene selection) — partly landed: the registry, thread identity by selection, and the construction-pressure detector are in (see Shipped); construction itself (Slice T4b) and selection are not. The tension curve becomes a scene-selection policy over a portfolio of story threads; a calm page comes from cutting away to a calmer thread (leaving a cliffhanger), never from becalming a hot event. Design: docs/THREAD_INTERLEAVING_DESIGN.md. Replaces the "forced low-tension scenes" idea, which the fork experiment falsified (progress_report_20260710.md Addendum 2: pruning 80 percent of the writer prompt moved mean tension by 0.0; the overshoot lives in the assigned event, not the prompt). Empirical grounding now available: the masters corpus thread-architecture data (thread counts, run lengths, convergence shapes) and the nd1 26-book reference bands
  • Contracts Slice 2: precondition pressure (unmet preconditions become planner setup pressure at beat selection, never a hard raise)
  • Contracts Slice 3: bounded contract repair (mirror the tension-rewrite pattern) plus an event_occurs LLM-judge checker
  • Gate A re-run against a sampler with _SCENE_TYPES removed (second-order kernel plus measured openers/closers only). Section 12 finds the scene layer earns none of the return rate and costs a little accuracy on shares and run lengths, but Gate A's 25/25 was scored against the hybrid, so the comparison has to be made on the same instrument first. The run must add a commitment measure (set-piece length) at matched sequence lengths: commitment is the gap the decomposition study named, Gate A's current checks would not notice a sampler that stopped sustaining set-pieces, and the one measurement taken so far is confounded by skeleton length
  • Sectioned scene writing (contracts Slice 5), generation.subblock_generation (default off): the scene is written across several plan-addressed calls, each owning a contiguous range of the skeleton's [n] blocks and seeing all prose so far, sized from its own mode mix. Motivated by the chapter-length finding rather than the original richness hypothesis: a single request does not reliably produce a masters-length chapter, and a 3,150-word plan becomes five calls of 500-800 words each. subblock_section_blocks sets granularity (1 = literal per-sub-block). Requires enable_scene_skeleton; falls back to single-shot on any failure. Unvalidated: seam quality, voice continuity and cost per scene all need the measured A/B the landing sketch specifies
  • Selective paragraph revision (DSL Slice 5, the repair-pass half): when a scene follows a paragraph plan, the tension rewrite revises only the tension-carrying paragraphs (chosen from the corpus's measured tension-band shares) and leaves the rest byte-identical, so structure, length and marker compliance survive by construction. Replaces the whole-scene rewrite that was silently undoing Slice 4. Unvalidated: a plan-constrained revision may move tension less than a free one, which the keep-only-if-closer rule makes safe but not necessarily useful
  • Measured A/B for sectioned writing per docs/BLOCKS_CONTRACTS_LANDING_SKETCH.md Slice 5: prose-richness judge, voice-continuity judge across section seams, cost per scene, and a human read, against skeleton-guided single-shot on the same foundation. Promote only if richness wins and voice continuity holds. Note what the mode is not for: the original richness hypothesis was answered "not needed for gpt-5.5" by the Slice 4 production runs (single-shot honors paragraph fullness), so the case to prove here is length
  • Skeleton refinements from the Slice 4 reading notes. Dialogue-run paragraphing is built, not yet validated: paragraph length is now measured per mode on the masters corpus (paragraph_words in the grammar, study section 5b), every plan item draws its own word target from that mode's measured decile ladder (deterministic per plan, rescaled so the plan's total stays unbiased; one fixed range per mode either brackets the median and under-sizes every scene or brackets the mean and forbids the short turns that are half the corpus), consecutive DIALOGUE items are one speech turn each, and the sampler fills a word budget instead of dividing by a flat 90. The predicted lift in excursion-return rate needs a re-run to confirm, and the PoC harness's writer prompt has to be synced to the production one first. Still open: genre-conditioned grammars; MTLD over-diversity as a candidate metric
  • Add support for guided generation per tick (largely superseded by Slice 4 scene skeletons for structure; reword or retire once remaining intent is defined)
  • Validate the throughline gate with a headroom scenario (goal-aligned foundations hit a ceiling — the LLM gauge is sound, but the pressure shows no lift when the story is already on-goal)
  • Resolve embedding-similarity placeholders in multi_stage_planner.py. Note for _filter_relevant_loops specifically: embeddings would sharpen its bias against old loops rather than fix it, since it ranks against the intention the planner just wrote. Decide it with the loop-aging series, not as a TODO cleanup
  • Scene-summarization callback stub in cli/main.py (Phase 2)

Backlog

  • Phase 4 — setup/payoff foresight (planted-element ledger for Chekhov's guns)
  • Plan explicit low-tension beats if arc-pressure proves insufficient (note: Slice 4 skeleton sampling already tension-conditions the prose-level block mix; this item concerns beat selection, which it does not touch)
  • Name-bank registers are surname-only (noted, not critical): _lasts() honours a register argument, _firsts() ignores it, so given name and surname are drawn independently. Harmless for the Victorian bank, whose gentry/common split is one population stratified by class sharing a given-name pool. Westerns are where it bites, because a western cast is not one population but several — Anglo settlers, Californio and Mexican-American families, Irish immigrants (Callahan, Donnelly, Driscoll, Rafferty, Kearney and Halloran are already in the bank), Chinese railroad labour, German farmers, Black cowboys — and which one a character belongs to governs the given name as much as the surname. Adding a Spanish register today would yield "Wyatt Vega" as readily as "Ramón Vega". Related data gap: the western bank holds two Spanish-family names and no Spanish surnames, while 39 sit unused in the scifi and modern banks. Fix when wanted is small — give _firsts the same register lookup _lasts has (~5 lines) and author per-register given names; the tool already exposes register, so the LLM selects it per character, which is the Phase 1 pattern. A weighted population mix was considered and set aside: the weights would be a historical claim with no gauge behind them, against this project's ship-the-gauge-first habit. Mixed families are real, so any register constraint should be a strong default rather than a rule
  • Loop mention tracking is dead, and it silently disables emergent story goals (found during loop-aging Slice L1, deliberately not fixed there): OpenLoop.scenes_mentioned and last_mentioned_tick are read in three places and written in none, so both are 0/None on all 1,548 loops on disk. Two consequences. _check_goal_promotion (ticks 10 to 15) requires a loop mentioned 5 or more times, so it can never fire: no run has ever auto-promoted a story goal, and novel goals always prints "Mentioned in 0 scenes". And the loop-aging classifier's throughline test degrades to an idiom guess, because its sound branch is similarity to state.story_goals.primary, which is almost never set. The fix needs a decision about the signal's source: beat advances_loops claims (already sanitized and stored, but plot-first only, which is off by default) or a new loops_addressed field on the tactical plan (works in both modes, but llm.planner_max_tokens is load-bearing and must rise with it). L1 records loop exposure to the planner instead, which is free and exact
  • Wedged-beat problem (noted, deliberately not auto-fixed): a beat whose contract keeps failing retries forever when allow_beat_skip and rolling_horizon are both off (observed live: 4 consecutive failures on one beat, 2026-07-10 smoke run). A bounded give-up rule was considered and rejected as dangerous (silently drifting off-outline); the preferred fix is making scenes comply with their tension targets (see forced low-tension scenes) so contracts stop failing in the first place