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.
- 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_phasein 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
apibackend (modelopenrouter,OPENROUTER_API_KEY/OPENROUTER_MODEL), live-validated - Venice support in the
apibackend (modelvenice,VENICE_API_KEY/VENICE_MODEL; Venice's injected system prompt disabled per request) - Shared
llm-backendspackage: the sixnovel_agent/tools/backend modules are now compatibility shims over it; the fiction persona is passed explicitly on theapibackend (tools/llm_setup.py) andDEFAULT_API_MODELis read from the package instead of hardcoded - Write-until-concluded scene loop: scenes are sized from
generation.scene_word_targetsand 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-callllm.timeouthonored 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, eachautoby default) settable per project, vianovel new --name-bank, or in the wizard. Bank choice is the author's:name.generateno longer advertises agenreparameter, 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 indocs/MASTERS_BLOCK_GRAMMAR_STUDY.mdsection 12 - Chapter-length calibration (
generation.scene_length_preset, defaulthouseand 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-inmasterspreset (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 indocs/MASTERS_BLOCK_GRAMMAR_STUDY.mdsection 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 innovel_agent/data/block_grammar_v1.json, 26-book refresh pre-validated); reusable gates methodology inexperiments/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-authoredscene_skeletonwins 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, flaggingrewritten_after_planningon a mismatch). The plan, its compliance, the section seams and the segment accounting are persisted on the committed scene'smetadata, 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)
- Loop-aging pressure (older open loops surface louder for payoff) — the gauge landed, the pressure has not. Slice L1 ships
agent/loop_aging.py, aloop_agepayload per tick,novel loops, and the corpus baseline indocs/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 ahorizonand onlyarcloops 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 lowersceneshare inloop_horizon_countswithout 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 bycompute_arc_phaseso it does not fight the arc-phase mandate. The number to read first isloop_shown_oldest_ageagainstloop_unshown_oldest_age, because_filter_relevant_loopsranks 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, soloop_overdue_totalis 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.mdAddendum 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_occursLLM-judge checker - Gate A re-run against a sampler with
_SCENE_TYPESremoved (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_blockssets granularity (1 = literal per-sub-block). Requiresenable_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.mdSlice 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_wordsin 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_loopsspecifically: 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)
- 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 aregisterargument,_firsts()ignores it, so given name and surname are drawn independently. Harmless for the Victorian bank, whosegentry/commonsplit 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_firststhe same register lookup_lastshas (~5 lines) and author per-register given names; the tool already exposesregister, 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_mentionedandlast_mentioned_tickare 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, andnovel goalsalways prints "Mentioned in 0 scenes". And the loop-aging classifier's throughline test degrades to an idiom guess, because its sound branch is similarity tostate.story_goals.primary, which is almost never set. The fix needs a decision about the signal's source: beatadvances_loopsclaims (already sanitized and stored, but plot-first only, which is off by default) or a newloops_addressedfield on the tactical plan (works in both modes, butllm.planner_max_tokensis 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_skipandrolling_horizonare 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