See [[An AI Tabletop Harness]] some pre-ideas that were hand written. # Narrative Engine Note, this is vibed. It will be edited later. The *Narrative Engine* is a fully incrementally computed AI-driven and AI-played [[Tabletop Role Playing Game|Tabletop RPG]] engine with a primary focus on testing and reviewing game rules. It does not generate content for playing tabletop RPGs and is not intended to be played with or by humans. ## Architecture The *Narrative Engine* can be thought of as a program that is continuously invoked over a git repo's state and produces mutations to the folders in that repo and commits them. Every commit is resumable and the engine itself contains very little state — its primary role is to facilitate communication and constrain the actions of the "players." The game state is encoded in an application with a simple core loop that takes the place of "going around the table" during traditional tabletop RPG gameplay. The DM is a model with broad access and control over the state of the game world. It is responsible for narrating and decision making, as with the traditional human counterpart. Actions the DM and players can take are tightly controlled by *Narrative* via scoped tool calls. All application state is continuously persisted to a well-defined text-based filesystem hierarchy with changes tracked using the [[Git]] version control system. After every action of a player or the DM, a commit is generated tracking the result. Every commit is fully resumable by the *Narrative Engine* allowing for branching and replayable storylines. The general flow is: 1. A campaign state is loaded or created - It should generally include files with information about NPCs, the world, and the events of the campaign 2. A session is started and corresponding folder is created 3. A scene is entered, creating a new state directory for the scene within the sessions directory 4. The DM begins narrating what is happening in the scene 5. The DM either asks all players for an action or goes around in turn order, asking for the actions of a specific player 6. The player(s) respond with an action 7. The DM updates the game state and narrates the responses 8. Go to #4 There's a lot that can happen between each step, but those details are heavily dependent on the rules of the game being played and are out of scope of this document. The actions that the players and DM are allowed to take are dependent on the abstract game state. For example, if the players are not in combat, the **attack** action is unlikely to be available. ## The Repo The standard campaign structure is a templated filesystem with a well-known structure inside of a git repository. The DM and players have constrained access to read and write to the information tracked in the filesystem indirectly through *Narrative*. The initial structure looks like this: ``` world/ npcs/ Character 1/ ABOUT.md STATS.yaml TIMELINE.yaml players/ Player 1/ ABOUT.md STATS.yaml TIMELINE.yaml events/ Event 1/ ABOUT.md story/ Story Event 1/ ABOUT.md rulings/ grappling-str-save.md flanking-advantage.md misc/ Misc Topic 1/ ABOUT.md player-notes/ Player 1/ npcs/ borin-stonehand.md locations/ iron-quarter.md clues/ vault-rumor.md Player 2/ ... rules/ manifest.yaml actions/ attack.ts cast-spell.ts use-item.ts grapple.ts conditions/ prone.yaml grappled.yaml equipment/ weapons.yaml armor.yaml tables/ encounter-level-1.yaml sessions/ session-1/ ABOUT.md 001-scene/ LOG.yaml player-1.md player-2.md SUMMARY.md SUMMARY.md SUMMARY.md current-scene next narrative-version ``` ### `world/` The `ABOUT.md` is a [[Markdown]] with [[YAML]] frontmatter that contains the primary information for a topic. The DM has query access to information in the `world/` directory at any time via `recall` and `record` tool calls. Players have access to information on their own characters freely, but can only append to `TIMELINE.yaml` with new major events that happen in their life and cannot directly control their `STATS.yaml` (which contains an analog to their character sheet). `world/rulings/` contains DM rulings — explicit decisions the DM has made about how to interpret ambiguous rules. Each ruling is a single `.md` file (not a directory with `ABOUT.md`), updated on demand by the DM when a decision is made. This is distinct from the base game rules — it captures house rulings and interpretations accumulated over play. ### `player-notes/` Campaign-global, per-player private knowledge namespace. Players organize subdirectories however they like — the engine doesn't enforce structure. Each note is a `.md` file with YAML frontmatter: ```markdown --- tags: [npc, dwarven] created: 2026-06-16T12:00:00Z modified: 2026-06-16T14:30:00Z --- # Borin Stonehand Dwarven smith met in [[The Iron Quarter]]. Knows about [[the sealed vault]]. ``` - `tags` — freeform, convention-based (`npc`, `location`, `event`, `clue`, `quest`, `faction`, `item`) - `created`/`modified` — ISO timestamps, auto-managed by the engine - `[[wikilinks]]` — parsed to build a link graph for backlink queries The DM **cannot** read `player-notes/`. These are strictly private to each player agent. ### `rules/` Game rules are expressed as actions in `rules/actions/`. Each action defines what it does, when it's available, and how it resolves. See [[#Rules and Actions]] for details. ### `sessions/` As scenes and sessions are played, the DM and players can explicitly "speak" at the table by appending records to the scene's `LOG.yaml`, where the table's outward conversation is stored. At the end of a session, the log for each scene is summarized and put into the session's `SUMMARY.md` for future use. As a scene is playing out, the DM or player can ask questions or perform actions. At almost any point, a player and the DM can converse privately — that conversation is recorded in the `<player name>.md` file. The DM can only query session and scene `SUMMARY.md` files when not in the active scene. Scene `LOG.yaml` is only accessible during the scene itself and is recorded for informational purposes. The summary is an artifact of the log. ### State files To facilitate the stateless operation of *Narrative*, the `current-scene` file is a plain text file containing the path to the currently active scene. `next` contains the action the narrative engine will be taking next, and `narrative-version` is metadata about the code and game used up to this point in the game's history. ## Context Management LLMs have finite context windows. A multi-session TTRPG campaign generates more content than can fit in a single context. The engine manages this through a three-tier context strategy. ### Tier 1 — Hot Context (always loaded) Assembled per-turn from filesystem state. Small and bounded: - Last *k* entries from the active scene's `LOG.yaml` (*k* configurable, ~15–20) - Character sheets (`STATS.yaml`) for all players in the scene - `ABOUT.md` for NPCs present in the scene - Current scene's `ABOUT.md` - The session's `SUMMARY.md` (recap of prior scenes) - The campaign's top-level `SUMMARY.md` (recap of prior sessions) - `narrative-version` and `next` state files ### Tier 2 — Warm Context (summarization pipeline) Hierarchical summarization keeps older content accessible in compressed form: ``` LOG.yaml → scene SUMMARY.md → session SUMMARY.md → campaign SUMMARY.md ``` Summarization fires at specific boundaries: - **Scene close:** `LOG.yaml` is condensed into that scene's `SUMMARY.md` (key events, NPCs met, rules exercised, unresolved threads) - **Session close:** All scene summaries plus remaining log → session `SUMMARY.md` - **Campaign milestone:** All session summaries → top-level `SUMMARY.md` For smoke testing, each `SUMMARY.md` carries a **rules-exercised** section — an audit trail of what game mechanics were tested. ### Tier 3 — Cold Context (on-demand retrieval) Everything else. Agents retrieve information via the `recall` tool, which queries by text grep, tags, or backlinks. See [[#recall and record Tools]]. ## Rules and Actions Game rules are encoded as **actions** in `rules/actions/`. The engine determines what actions are available to each agent based on the game state — what equipment they have, their stats, current conditions, whether they're in combat, etc. These constraints and possible actions are presented to the agents via tool call interfaces. Invalid actions are designed out — if you don't have a weapon, `attack` isn't in your available tools. If you do, the `target` param is an enum of valid targets. ### Action Interface Each action is a `.ts` (complex logic) or `.yaml` (simple definitions) module that conforms to: ```typescript interface Action { name: string; description: string; available(state: GameState, actor: string): boolean; execute(state: GameState, actor: string, params: Params): ActionResult; params: ParamDef[]; } interface ParamDef { name: string; type: "string" | "number" | "enum" | "target"; description: string; required: boolean; enum?: string[]; } interface ActionResult { stateDelta: Partial<GameState>; narrative: string; followUp?: Action[]; } ``` - `available()` — the engine's enforcement. If this returns false, the action is not presented to the model at all. - `execute()` — deterministic resolution. Dice rolls, stat checks, damage calculation all happen here. The model never rolls or reports dice. - `params` — defines the tool call schema presented to the model. Enums are populated dynamically from valid options in the current game state. ### YAML Shorthand Simple actions can be defined in YAML and are compiled into the same `Action` interface at runtime: ```yaml name: use-item description: Use a consumable item from your inventory params: - name: item type: enum required: true description: The item to use available: has_consumable: true execute: remove_item: "{item}" apply_effect: "{item.effect}" ``` If an action needs logic that YAML can't express, it graduates to `.ts`. ### Action Lifecycle 1. Engine hydrates `GameState` from repo files (read-only hydration from disk) 2. Engine evaluates all actions' `available()` for the current actor 3. Engine builds a constrained tool call schema (only valid actions + valid params) and presents it to the model 4. Model calls one of the presented tools with the provided params 5. Engine calls `action.execute(state, actor, params)` — rolls dice, computes result, returns `ActionResult` 6. Engine applies state mutations to the in-memory `GameState` object, which writes changes back to files immediately and commits 7. Engine returns `ActionResult` to the model Actions are one-shot by default. Sub-actions are possible but must resolve within a single tool call and commit — no multi-turn action chains hanging open. ### Game State `GameState` is an in-memory JavaScript object that hydrates from repo files on load. The repo files are the source of truth — `GameState` is a read view on top of them. Anything that mutates state modifies the object representations, which immediately sync the changes back to the corresponding files and commit. This ensures the repo is always consistent with the in-memory state. ### Manifest `rules/manifest.yaml` ties the game system together: ```yaml game: Cairn Hack version: "0.1" entry_actions: [explore, rest, travel] combat_actions: [attack, cast-spell, use-item, flee, grapple] combat_flow: initiative-based ``` ### Conditions `rules/conditions/` defines status effects and environmental states as YAML files. These are referenced by actions and the engine to modify available actions and resolution. ### Equipment `rules/equipment/` defines weapons, armor, and items as YAML files. These feed into action availability (you can't attack without a weapon) and resolution (damage dice, armor class). ### Tables `rules/tables/` contains random tables and encounter tables as YAML files. The engine can roll on these automatically when instructed by a DM action. ## Tool Calls ### DM Tools | Tool | Scope | Description | |---|---|---| | `recall` | `world/` (full) + `sessions/` (summaries only) | Query world state and session summaries by text, tags, or backlinks | | `record` | `world/` (full) | Create or update entries in the world directory | | `narrate` | Active scene `LOG.yaml` | Append narration and descriptions to the scene log | | `speak` | Active scene `<player>.md` | Private conversation with a specific player | | Game actions | Per `rules/manifest.yaml` | Any DM-available action (e.g., NPC attack, environment effect) | The DM **cannot** read `player-notes/`. ### Player Tools | Tool | Scope | Description | |---|---|---| | `recall` | `player-notes/<self>/` | Query own notes by text, tags, or backlinks | | `record` | `player-notes/<self>/` | Create or update notes in own private namespace | | `speak` | Active scene `LOG.yaml` | Speak at the table (public, all players hear) | | `whisper` | Active scene `<player>.md` | Private conversation with the DM | | Game actions | Per `rules/manifest.yaml` + `available()` | Any player-available action based on current game state | ### Log Writing The scene `LOG.yaml` has two writers: - **System** — auto-appends mechanical results (dice rolls, state changes, action outcomes) as part of the commit cycle - **Agents** — explicitly append narration, dialogue, and descriptions via `narrate` (DM) or `speak` (player) tool calls Both entry types coexist in the same log. System entries are the ground truth of what happened mechanically; agent entries are the narrative layer on top. ## recall and record Tools ### Index Computation The tag/backlink index is computed on demand as a pure function — `build_index(namespace) -> dict` — that: 1. Globs all `*.md` files in the namespace 2. Parses YAML frontmatter for tags 3. Parses `[[wikilinks]]` from bodies 4. Computes backlinks by scanning all link targets across notes 5. Returns an in-memory dict Called at the start of every `recall` invocation and cached per-turn (invalidated on `record`). Never persisted to disk — no derived index files in the repo. ### recall ``` recall( query: str | None = None, tags: list[str] | None = None, link_target: str | None = None, sort: "modified" | "relevance" = "modified", limit: int = 10 ) ``` - `query` — full-text grep across note bodies, returns snippets with match context - `tags` — AND-filter by tags (returns full files, not snippets) - `link_target` — "what links here" (backlinks from the in-memory index) - `sort` — `modified` for recency, `relevance` for query match density Combinations work: `recall(query="forge", tags=["npc"])` searches only NPC-tagged notes for "forge". ### record ``` record( note_path: str, content: str, tags: list[str] | None = None ) ``` - `note_path` — relative to the agent's namespace (e.g., `"npcs/borin-stonehand.md"`) - `content` — full markdown body (the engine wraps it with frontmatter) - `tags` — optional, merged into frontmatter The engine auto-sets `created`/`modified`, parses `[[links]]`, invalidates the index cache for that namespace, and commits. ## The Application The prototype of the *Narrative Engine* is a command-line application. It can initialize a repository with the appropriate state and begin executing the game. A major part of the application's functionality is controlling the context of the GM and agents and providing them with constrained options for actions they can take.