-
Notifications
You must be signed in to change notification settings - Fork 0
CONVERSATIONAL_LOOPS
State machines for narrative interaction: how scenes, turns, and stories flow.
MONITOR operates through nested loops at different timescales:
- Main Loop (session) - What do you want to do?
- Story Loop (campaign) - Campaign/arc lifecycle
- Scene Loop (interactive) - Interactive narrative unit
- Turn Loop (exchange) - Individual user/GM exchanges
Each loop is a state machine with clear:
- Entry conditions
- State transitions
- Exit conditions
- Canonization checkpoints
Timescale: Continuous (user session) Purpose: Session management and mode selection Canonization: None (delegated to Story/Scene loops)
This is the outermost loop - the user's entry point to MONITOR.
Main States:
┌──────────────┐
│ Idle │ ← Waiting for user intent
└──────┬───────┘
│
▼
┌──────────────┐
│ Executing │ ← Running selected mode
└──────┬───────┘
│
▼
┌──────────────┐
│ Idle │ ← Return to menu
└──────────────┘
Main Loop Flow:
START (User Session)
│
├─→ Display Available Actions
│ - Start new story
│ - Continue existing story
│ - Manage universe (view/edit entities)
│ - Manage characters
│ - Ingest documents (upload PDF/manual)
│ - Query/retrieve (ask about canon)
│ - System admin (backups, etc.)
│
├─→ User Selects Mode
│ ↓
│ Decision Tree:
│
├─→ MODE: Start New Story
│ ├─→ Universe exists? If not → Ingest/Create flow
│ ├─→ Create PC? If needed → Character creation
│ ├─→ Define story outline
│ └─→ [STORY LOOP] ◄─── runs until story complete
│ ↓
│ Return to Main Loop
│
├─→ MODE: Continue Story
│ ├─→ Load existing Story from Neo4j
│ ├─→ Load last scene from MongoDB
│ └─→ [STORY LOOP or SCENE LOOP] ◄─── resume
│ ↓
│ Return to Main Loop
│
├─→ MODE: Ingest Documents
│ ├─→ Upload PDF → MinIO
│ ├─→ Extract + chunk → MongoDB
│ ├─→ Embed → Qdrant
│ ├─→ Propose universe/axioms → MongoDB
│ ├─→ User review → accept/reject
│ └─→ Canonize accepted → Neo4j
│ ↓
│ Return to Main Loop
│
├─→ MODE: Query/Retrieve
│ ├─→ User question
│ ├─→ Query Neo4j (canonical facts)
│ ├─→ Query Qdrant (semantic recall)
│ ├─→ Query MongoDB (narrative details)
│ └─→ Present results
│ ↓
│ Return to Main Loop
│
├─→ MODE: Manage Universe
│ ├─→ List entities/facts
│ ├─→ Edit/create entities (manual)
│ ├─→ View timeline
│ └─→ Export/visualize
│ ↓
│ Return to Main Loop
│
├─→ MODE: Manage Characters
│ ├─→ List PCs/NPCs
│ ├─→ Create new character
│ ├─→ Edit character (stats/memories)
│ └─→ View character history
│ ↓
│ Return to Main Loop
│
├─→ User exits session
│
END (Session closes)
Main Loop States:
| State | Description | Available Actions |
|---|---|---|
| Idle | Waiting for user command | Start story, continue, ingest, query, manage, exit |
| Executing | Running selected mode | (mode-specific) |
| Suspended | Story paused mid-scene | Resume, abandon, save checkpoint |
Data Persistence:
When user exits mid-story:
- MongoDB: scene.status = "active" (preserved)
- MongoDB: turns preserved
- MongoDB: proposals = "pending" (preserved)
- Neo4j: last canonized state (consistent)
On resume:
- Load scene state from MongoDB
- Continue Scene Loop from last turn
Critical Invariant: Main Loop does NOT write canon. It delegates to Story/Scene loops.
Timescale: Days to months Purpose: Campaign/arc continuity Canonization: Story endpoints and major beats Parent: Main Loop (launched from "Start/Continue Story" modes)
Story States:
┌─────────────┐
│ Created │ ← Story defined, universe linked
└──────┬──────┘
│
▼
┌─────────────┐
│ Active │ ← Scenes running
└──────┬──────┘
│
▼
┌─────────────┐
│ Completed │ ← Final facts canonized
└─────────────┘
Story Loop Flow:
START
│
├─→ Define Story (universe, theme, constraints)
│ ↓
│ MongoDB: story_outline
│ Neo4j: Story node (canonical container)
│
├─→ Create Initial Scene
│ ↓
│ MongoDB: scene (status=active)
│ Neo4j: Scene node (optional, for ordering)
│
├─→ [SCENE LOOP] ◄──────┐
│ ↓ │
│ Scene ends │
│ ↓ │
│ Decision: Continue? │
│ ├─ Yes → Create next scene ─┘
│ └─ No ↓
│
├─→ Finalize Story
│ ↓
│ MongoDB: story recap
│ Neo4j: Story.status = "completed"
│ Neo4j: final canonical facts/timeline
│
END
Data Written:
| Phase | MongoDB | Neo4j | Qdrant |
|---|---|---|---|
| Start | story_outline | Story node | - |
| Active | scenes, notes | Scene nodes (optional) | - |
| End | recap | final facts, timeline closure | story summary |
Timescale: Minutes to hours Purpose: Interactive narrative unit with canonization boundary Canonization: End of scene (batch commit) Parent: Story Loop (nested within active story)
This is the core interactive loop where users engage with the narrative.
Scene States:
┌──────────────┐
│ Created │ ← Context loaded
└──────┬───────┘
│
▼
┌──────────────┐
│ Active │ ← Turn loop running
└──────┬───────┘
│
▼
┌──────────────┐
│ Finalizing │ ← Canonization gate
└──────┬───────┘
│
▼
┌──────────────┐
│ Completed │ ← Canon written
└──────────────┘
Scene Loop Flow (Detailed):
S1: LOAD CONTEXT
├─→ Query Neo4j
│ - Scene-linked entities + relations
│ - Active facts/conditions (time_ref)
│ - Location/setting state
│
├─→ Query MongoDB
│ - Previous turns in this scene
│ - Character memories for participants
│ - GM notes/constraints
│
├─→ Query Qdrant (optional)
│ - Similar past scenes
│ - Relevant rule excerpts
│ - Character memory recall
│
└─→ OUTPUT: Context package (IDs + texts)
↓
S2: USER ACTION / INPUT
├─→ Write MongoDB
│ - Append Turn to scene_turns
│ - speaker, text, timestamp
│
└─→ State: waiting for resolution
↓
S3: RESOLVE OUTCOME
├─→ Rules-based OR narrative randomizer
│ - Success/partial/fail + effects
│
├─→ Write MongoDB
│ - ProposedChange records
│ - Resolution record (if dice/rules)
│ - Evidence citations
│
└─→ Proposals are STAGED, not canonical yet
↓
S4: CANONIZATION GATE (optional mid-scene)
│
├─→ IF critical event OR explicit commit:
│ ├─→ Evaluate proposals
│ ├─→ Write Neo4j (accepted facts)
│ ├─→ Mark proposals accepted/rejected
│ └─→ Continue scene
│
└─→ ELSE: defer to scene end
↓
S5: PERSIST NARRATIVE
├─→ Write MongoDB
│ - GM turn text (response)
│ - Updated scene state
│
└─→ Write Qdrant
- Embed new turn chunk
- Update scene summary vector
↓
S6: CONTINUE OR END SCENE
│
├─→ IF scene goals unmet: LOOP to S2
│
└─→ IF scene complete: FINALIZE
↓
FINALIZE (Canonization Checkpoint)
├─→ Review ALL proposed_changes
│ - Accept/reject by policy
│ - Batch canonical deltas
│
├─→ Write Neo4j
│ - Facts/Events (time_ref, participants)
│ - Relations (state changes)
│ - SUPPORTED_BY edges (→ Scene, Turns)
│
├─→ Write MongoDB
│ - Mark scene.status = "completed"
│ - scene.canonical_outcomes = [fact_ids]
│ - Final scene summary
│
└─→ Write Qdrant
- Scene summary embedding
- Key memory entries for participants
END SCENE
Critical Invariants:
-
Turns never write to Neo4j directly
- Turns are narrative artifacts (MongoDB only)
- Only canonization writes to graph
-
Scene is atomic canonization unit
- Primary: batch commit at scene end
- Optional: mid-scene checkpoints for critical events
-
Proposals are explicit
- ProposedChange records are structured, not free text
- Each has evidence pointers
- Each has status (pending/accepted/rejected)
Timescale: Seconds Purpose: Individual user/GM exchange Canonization: None (deferred to Scene) Parent: Scene Loop (embedded in steps S2-S5)
The Turn Loop is the innermost loop, embedded inside the Scene Loop.
TURN FLOW:
User Input
↓
Parse Intent
↓
Retrieve Context (if needed)
↓
Generate Response
↓
Extract Proposals (if any)
↓
Persist Turn (MongoDB)
↓
Continue Scene
Turn as Data:
Turns are append-only narrative logs.
They do NOT:
- Modify canonical state
- Write to Neo4j
- Make decisions about truth
They MAY:
- Propose canonical changes (staged in MongoDB)
- Trigger mid-scene commit (if critical)
- Reference canonical entities/facts
| Current State | Event | Next State | Actions |
|---|---|---|---|
| Created | Load context | Active | Load Neo4j/MongoDB/Qdrant |
| Active | User turn | Active | Append turn, generate proposals |
| Active | Critical event | Active | Mid-scene canonization |
| Active | Scene goal met | Finalizing | Begin canonization |
| Active | Explicit end | Finalizing | Begin canonization |
| Finalizing | Canon written | Completed | Mark scene done, update indices |
| Current State | Event | Next State | Actions |
|---|---|---|---|
| - | User input | Processing | Parse, retrieve context |
| Processing | Context loaded | Generating | LLM inference |
| Generating | Response ready | Proposing | Extract canonical deltas |
| Proposing | Proposals staged | Persisted | Write turn + proposals to MongoDB |
Trigger conditions:
- Character death
- Major discovery (new entity/location)
- Explicit user/GM "commit" command
- Turn count threshold (e.g., every 50 turns in long scene)
Process:
IF trigger:
├─→ Evaluate pending proposals
├─→ Accept high-confidence deltas
├─→ Write to Neo4j (Facts + SUPPORTED_BY)
├─→ Mark proposals as accepted
└─→ Continue scene (stay in Active state)
Trigger conditions:
- Scene goal achieved
- Location transition
- Explicit scene end
- Story beat complete
Process:
Scene Finalization:
├─→ Collect ALL pending proposals
├─→ Evaluate by policy (authority + confidence)
├─→ Batch write to Neo4j:
│ - Facts/Events
│ - Relations (new/updated)
│ - State tags
│ - SUPPORTED_BY edges
│
├─→ Update MongoDB:
│ - scene.status = "completed"
│ - proposal.status = "accepted"|"rejected"
│ - scene.canonical_outcomes = [fact_ids]
│
└─→ Update Qdrant:
- Scene summary embedding
- Character memory entries
Loops are nested and sequential within a session:
┌─────────────────────────────────────────────────────┐
│ MAIN LOOP (session/menu) │
│ │
│ User selects mode → Execute → Return │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ STORY LOOP (campaign) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ SCENE LOOP (interactive) │ │ │
│ │ │ │ │ │
│ │ │ ┌───────────────────────────────────┐ │ │ │
│ │ │ │ TURN LOOP (exchange) │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ User → Resolve → Persist │ │ │ │
│ │ │ └───────────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ Context → Turns → Canonize (scene end) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Story → Scenes → Story End (canonize) │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ Ingest | Query | Manage (non-story modes) │
└─────────────────────────────────────────────────────┘
Key principles:
- Main Loop coordinates all modes (story, ingest, query, manage)
- Inner loops do NOT canonize - only Scene end and Story end write to Neo4j
- Loops are resumable - exit mid-story, resume from MongoDB state
User Input
↓
MongoDB: Turn (append)
↓
MongoDB: ProposedChange (stage)
↓
[Repeat turns...]
↓
Scene Ends
↓
Neo4j: Facts/Events (batch write)
↓
Qdrant: Embeddings (update)
Critical: Neo4j writes are batched at checkpoints, not per-turn.
If scene loop crashes mid-scene:
- MongoDB has all turns (recoverable)
- Proposals are staged (recoverable)
- Neo4j has last checkpoint state (consistent)
- On restart:
- Resume scene from last turn
- Re-evaluate proposals
- User can choose: continue or finalize now
If Neo4j write fails during finalization:
- MongoDB scene state = "finalizing" (stuck)
- Proposals remain "pending"
- Recovery:
- Retry canonization with same proposals
- OR mark scene "failed" and manual review
- Never lose narrative (MongoDB has full log)
If only some proposals succeed:
- Mark accepted proposals (MongoDB)
- Mark failed proposals with error reason
- Allow user to:
- Retry failed proposals
- Skip and mark rejected
- Manual override
| Loop | Latency Target | Throughput Target | Canonization Cost |
|---|---|---|---|
| Main | < 100ms (mode switch) | N/A | None (delegates) |
| Story | Hours-days | - | 1 closure write (cheap) |
| Scene | 5-30 min | - | 1 batch write (cheap) |
| Turn | < 2s | 1 turn/2s | None (deferred) |
Key insights:
- Main Loop is lightweight (just coordination)
- Scene-level canonization keeps Neo4j writes low-frequency and high-value
- Turn latency is user-facing (must be fast)
Scene Start → Load Context
Neo4j: PC stats, enemy stats, location
MongoDB: No prior turns (new scene)
Turn 1: "I attack the orc"
MongoDB: Turn (user input)
Resolve: Roll → success
MongoDB: ProposedChange (orc takes 8 damage)
Turn 2: "The orc retaliates"
MongoDB: Turn (GM response)
Resolve: Roll → partial
MongoDB: ProposedChange (PC takes 3 damage)
Turn 3: "I finish him"
MongoDB: Turn (user input)
Resolve: Roll → success
MongoDB: ProposedChange (orc dies)
Scene End → Canonization
Neo4j:
- Event: "Combat with orc"
- Fact: "Orc died" (time_ref, participants)
- State: PC.wounded = true
- SUPPORTED_BY → Scene, Turns 1-3
MongoDB:
- scene.status = "completed"
- proposals marked "accepted"
Qdrant:
- Scene summary: "combat victory, PC wounded"
Scene Start → Load Context
Neo4j: PC, current location
MongoDB: No prior turns
Turn 1-10: Exploration dialogue
MongoDB: Turns, no proposals
Turn 11: "You find a hidden door"
MongoDB: Turn + ProposedChange (new location entity)
TRIGGER: Critical discovery → mid-scene commit
Mid-Scene Canonization:
Neo4j:
- Entity: HiddenChamber
- Relation: Location→HiddenChamber
- SUPPORTED_BY → Turn 11
MongoDB:
- proposal.status = "accepted"
Scene continues (status still "active")
Turn 12-20: Explore chamber
MongoDB: Turns
Scene End → Final Canonization
Neo4j:
- Facts from turns 12-20
- PC.location = HiddenChamber
- SUPPORTED_BY → Scene, Turns 12-20
To implement these loops, we need:
-
Loop Controllers (agent orchestration)
- See AGENT_ORCHESTRATION.md
- Who runs each loop
- How loops coordinate
-
State Validators
- Ensure transitions are legal
- Detect stuck states
- Recovery procedures
-
Canonization Policies (detailed)
- Authority resolution
- Confidence thresholds
- Conflict resolution
-
Performance Monitoring
- Loop cycle times
- Canonization batch sizes
- Error rates
- DATABASE_INTEGRATION.md - Data layer and canonization rules
- AGENT_ORCHESTRATION.md - Who runs these loops
- ONTOLOGY.md - Canonical data model