docs: Migrated to astro v7 for live docs

This commit is contained in:
2026-07-09 04:53:00 +05:30
parent 093f8de4c5
commit d9940a036a
26 changed files with 4273 additions and 516 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,7 @@
import { defineCollection } from "astro:content";
import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
export const collections = {
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};

View File

@@ -0,0 +1,93 @@
---
title: Actor Agent
description: The component that embodies a single entity and produces narrative prose
---
The Actor Agent is the system component that embodies a single entity and produces narrative prose describing what that entity does, says, or thinks next. It is the "inner voice" of an NPC (or player character), generating behavior proposals that are then validated and executed by the rest of the engine.
## Design Principles
1. **Epistemic boundedness** — The actor only sees what its entity would perceive: public attributes of other entities, private attributes explicitly ACL'd to it, its own memory buffer, and co-located entities. It does not have system-level access to all world state.
2. **Proposal, not mutation** — The actor generates a _proposal_ (narrative prose). It never mutates world state, persists to the database, or writes to memory directly. Validation, execution, and persistence are the Architect's job.
3. **Free prose → structured intents** — The actor outputs free natural-language prose. This is fed to the `IntentDecoder`, which splits and classifies it into a sequence of typed intents.
## Prompt Structure
The actor prompt is assembled by `ActorPromptBuilder` and has two parts:
### System Prompt
Establishes the role, rules, and output contract:
- The LLM **is** the character, not a narrator or system.
- The character may produce three kinds of behavior:
- **Spoken dialogue** → `dialogue` intent.
- **Physical/logical action** → `action` intent.
- **Inner thought / reflection** → `monologue` intent.
- The character must stay in-character, respect its knowledge bounds, and refer to others by subjective aliases (not system UUIDs).
- Not every turn requires an outward action — internal monologue alone is valid.
- The character controls only itself.
### User Context
Epistemically bounded, with these sections:
| Section | Content | Source |
|---|---|---|
| Current moment | The subjective present time | `worldState.clock.get().toISOString()` |
| The world as you perceive it | Self-visible attributes, co-located entities + their visible attributes, other presences elsewhere | `serializeSubjectiveWorldState()` |
| Your recent memory | Recent `BufferEntry`s, alias-substituted, with relative time phrasing | `serializeSubjectiveBufferEntry()` |
No system UUIDs, no private attributes the entity lacks ACL access to, and no objective-world-state dump are present.
## The Monologue Intent Type
Monologue (`"monologue"`) is the third intent type. Its properties:
- **No perceiver** — `targetIds` is always `[]`. No other entity perceives or can react to a monologue.
- **No validation** — The Architect's `processIntent` short-circuits for monologues.
- **Direct-to-memory** — Written directly to the actor's buffer with no `outcome` field.
- **Defensive guard** — `LLMValidator.validate` has an early-return guard so a stray monologue can never reach the validation LLM.
## Flow
```
[ActorAgent.act()]
├─ 1. ActorPromptBuilder.build(entity, worldState)
│ → system prompt + user context (subjective world + memory + time)
├─ 2. IActorProseGenerator.generate(entityId, systemPrompt, userContext)
│ ├─ LLMActorProseGenerator: queries LLM via generateStructuredResponse
│ └─ CLIProseGenerator: prompts human player via CLI / readline interface
│ → narrativeProse: string
├─ 3. IntentDecoder.decode(worldState, actorId, prose)
│ → IntentSequence (dialogue | action | monologue intents)
└─ returns { narrativeProse, intents }
[Caller (e.g. game loop)]
├─ for each intent in intents:
│ ├─ if intent.type === "monologue": short-circuit, write to buffer
│ ├─ if intent.type === "dialogue": validate (always valid), write to buffer
│ └─ if intent.type === "action": validate, generate time delta, advance clock
└─ world state persisted to DB
```
## Key Files
| File | Role |
|---|---|
| `packages/actor/src/actor-prompt-builder.ts` | Assembles the epistemically-bounded actor prompt |
| `packages/actor/src/actor.ts` | `ActorAgent` class: orchestrates prompt → LLM → decoder flow |
| `packages/actor/src/index.ts` | Package exports |
| `packages/core/src/world.ts:72` | `serializeSubjectiveWorldState()` |
| `packages/intent/src/intent.ts:8` | `IntentTypeSchema` — includes `"monologue"` |
| `packages/intent/src/intent-decoder.ts:30` | Decoder system prompt |
| `packages/architect/src/architect.ts:35` | Monologue short-circuit |
| `packages/architect/src/llm-validator.ts:19` | Defensive monologue guard |

View File

@@ -0,0 +1,70 @@
---
title: World Architect
description: The validation and state-mutation layer that sits between intents and world state
---
The Architect is not a specialized model, nor a special entity. It is a special context along with a set of tools given to an LLM that dictates what happens to world state.
The Architect is provided with the `WorldState`, states of entities, state of a scene, location, along with their attributes, and is asked to judge whether an action (an `Intent`) makes canonical sense. It disallows intents that break the narrative flow (for example, item ownership, entity location and state tracking).
```mermaid
flowchart LR
A[Agent] -->|"Perform Action"| B(Action Intent)
subgraph "Architect Layer"
C[Architect]
D{"Validators"}
E{"Noise Layer 1"}
F{"Noise Layer 2"}
G[Delta Generators]
end
B --> C
C -->|"Tool Call"| D
C -->|"Spontaneity Bypass"| E
D --> F
E --> F
F -->|"Pass"| G
F -->|"Fail"| A
G --> |Deltas modify| H[State]
H --> I[Entity]
H --> J[Location]
H --> K[World]
```
Dialogue intents are exempted from validation and are not used for state manipulation. v0 defers atomic validators completely; instead, an umbrella LLM-based validator is used.
## Dynamic Validators (Deferred from v0)
A standard set of validators dictate if an action is possible. The Architect can dynamically generate its own validators which can be loaded and unloaded at runtime based on narration. These can be soft validators (if they fail, the intent is sent back to the entity for override confirmation).
## Noise Layers (Deferred from v0)
Since the system has complex action validation, the LLM may naturally steer towards low-stakes actions with minimal consequences, flattening the narrative. Noise layers introduce random, slightly non-sensical actions to introduce spontaneity and unexpectedness.
## Delta Generators
Delta Generators are single-responsibility components that compute discrete state updates ("deltas") from validated intents.
### How They Function
1. **Validation Prerequisite**: Execute only if validation returns `isValid: true`.
2. **Specialized Responsibility**: Each generator isolates a specific aspect of state transition (clock advancement, position updates, attribute modifications).
3. **Structured Outputs**: Generators query the LLM using Zod schemas for type-safe change deltas.
4. **Application and Persistence**: The delta is applied to the live `WorldState` by deterministic code and persisted to the database.
### Time Delta Generator
Calculates the physical time duration (in minutes) that a validated action takes to complete:
- **Inputs**: The validated action and the serialized objective `WorldState`.
- **Output (Zod Schema)**:
```json
{
"minutesToAdvance": 25,
"explanation": "Searching a locked desk thoroughly takes time."
}
```
- **Resolution**: The Architect advances `worldState.clock` by the returned minutes and saves to `SQLiteRepository`.
## A Note on Tech Debt
The Architect currently trusts an LLM's judgment about reasonable consequences rather than validating every change against declarative constraints. A general constraint solver is worth building eventually, but building it before anything is playable is foundational perfectionism that produces beautiful architecture and no game.

View File

@@ -0,0 +1,88 @@
---
title: Intents
description: How narrative prose becomes structured, validated actions
---
The simple way of understanding intents is to think of it as a proposal, not an effect.
Intents are:
- **Declarative** — they describe what the character intends, not the final outcome.
- **High-level** — they capture the gist of an action or dialogue.
- **Allowed to be wrong** — validation happens downstream.
- **Cheap to generate** — LLM-friendly structured output.
The actor LLM doesn't directly generate an intent. To keep the narrative going, the actor agent generates continuing prose. If the intents validate, the narrative prose goes directly to the user while deltas generated from the intents modify the state.
```mermaid
flowchart LR
A[Actor Agent]
B[/Action Narrative Prose/]
C{{Intent Decoder}}
F[Architect]
E{{Intent Scheduler}}
A --> B
B --> C
subgraph D["Intent Sequence"]
direction TD
I1([Dialogue Intent])
I2([Action Intent])
I3([Action Intent])
I4([⋯])
I1 --> I2 --> I3 --> I4
end
C --> D
D --> |"Concurrent"| F
F --> E
```
## Intent Decoder
The Intent Decoder:
- Splits narrative prose into multiple intents when applicable.
- Classifies each intent to its type (`dialogue`, `action`, or `monologue`).
- Parses narrative text into structured JSON with minimal information loss.
- Contextually resolves the receiving parties/targets.
### Zod Schemas & Types
- **IntentType**: `"dialogue"` | `"action"` | `"monologue"`
- **Intent**:
- `type`: `IntentType`
- `originalText`: `string` — the slice of raw prose text containing the intent
- `description`: `string` — summarized intent action
- `actorId`: `string`
- `targetIds`: `string[]` — resolved recipient or target entity IDs
- **IntentSequence**:
- `intents`: `Intent[]`
### IntentDecoder Class
```typescript
export class IntentDecoder {
constructor(private llmProvider: ILLMProvider) {}
async decode(
worldState: WorldState,
actorId: string,
narrativeProse: string,
): Promise<IntentSequence>;
}
```
It serializes the world state and feeds all known entity IDs as context, enabling the model to resolve target IDs correctly.
## Names and LLMs
A fundamental issue emerged during early design: something as simple as a name is set to private. An entity's name is not common knowledge — you don't instantly know another person's name.
Although the internal system can identify an entity by its ID (defined in `AttributableObject`), UUIDs aren't helpful for LLMs. This creates several problems:
- **Unnamed entities**: How does the Architect orchestrate changes for an entity that doesn't even have a name?
- **Inconsistent identifiers**: An NPC might use "the hooded man" in one turn and "the shadowy figure" in the next.
- **Made-up names**: LLMs may invent names not present in the world state.
The **Subjective Alias System** solves this by maintaining a per-entity map from target IDs to subjective descriptors (see [Memory & Aliases](./memory)).

View File

@@ -0,0 +1,91 @@
---
title: Memory & Subjective Aliases
description: The memory subsystem and how entities refer to each other
---
This document outlines the memory subsystem (`packages/memory`) and the Subjective Alias System.
## Subjective Alias System
System-level IDs (e.g. `alice`, `bob`, or UUIDs) are critical for state tracking, but placing them directly in prompts violates epistemic privacy and breaks narrative generation.
Each `Entity` class maintains a private **Subjective Alias Map**:
```typescript
class Entity extends AttributableObject {
locationId: string | null = null;
readonly aliases: Map<string, string> = new Map();
// Key: target entity ID (e.g., "bob")
// Value: subjective string (e.g., "the hooded figure" or "Gareth")
}
```
### Alias States
- **Unknown Name**: Defaults to a subjective label derived from the target's visible description (e.g., `"the hooded figure"`).
- **Known Name**: Updated to their name once learned (e.g., `"Gareth"`).
### How It Wires Into Prompts
- **Intent Decoder**: The decoder maps subjective labels like "the hooded figure" back to the correct system ID `bob`.
- **Prompt Injection**: World state, events, and memories are injected with raw target IDs replaced by subjective aliases.
### SQLite Persistence
Entity aliases are persisted in the `objects` table via the `aliases_json TEXT` column. The aliases map is stringified as JSON on save and parsed back upon entity reconstitution.
## Subjective Buffer Entry
A subjective `BufferEntry` records a discrete event from the perspective of an entity (the `owner`). It wraps a structured `Intent` and appends execution metadata.
### The Shape of a Buffer Entry
```typescript
interface BufferEntry {
id: string;
ownerId: string; // Whose subjective memory buffer this lives in
timestamp: string; // WorldClock.get().toISOString() at write time
locationId: string | null; // Actor's location when this happened
intent: Intent; // The actual dialogue/action intent, reused as-is
outcome?: { // Present only for "action" intents
isValid: boolean;
reason: string;
};
}
```
- **Intent Reuse**: Schema changes to `Intent` flow down automatically.
- **Write-time Location**: `locationId` is captured immediately when writing the entry.
## Buffer Serialization (Epistemic Substitute)
To prevent leaking system IDs, buffer memories are serialized using `serializeSubjectiveBufferEntry` with the `resolveAlias` helper:
```typescript
export function resolveAlias(viewer: Entity, targetId: string): string {
if (targetId === viewer.id) return "you";
return viewer.aliases.get(targetId) ?? "an unfamiliar figure";
}
```
This guarantees prompts read cleanly — e.g. `[12:03:00 PM] the hooded figure opened the wooden chest (Outcome: Succeeded)` — without exposing raw system IDs.
## SQLite Persistence & BufferRepository
The `BufferRepository` class uses the same SQLite database as core repositories:
```sql
CREATE TABLE IF NOT EXISTS buffer_entries (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
timestamp TEXT NOT NULL,
location_id TEXT,
intent_json TEXT NOT NULL,
outcome_json TEXT,
FOREIGN KEY (owner_id) REFERENCES objects(id) ON DELETE CASCADE
);
```
- **JSON Storage**: `intent` and `outcome` are serialized/deserialized as raw JSON, validated by Zod at creation time.
- **Cascade Deletes**: Deleting an entity removes all associated subjective memory entries.

View File

@@ -0,0 +1,43 @@
---
title: Architecture Overview
description: High-level architecture of the Omnia engine
---
Omnia is organized as a monorepo with the following subsystems:
```
omnia/
packages/
core/ entities, attributes, world state, clock, SQLite persistence
intent/ intent types (dialogue/action/monologue) and the prose decoder
architect/ World Architect: LLM validation plus time-delta generation
actor/ actor agent: epistemically-bounded prompts, pluggable prose generators
memory/ verbatim buffer; later the vector archive, dossier, and affect vectors
spatial/ location and POI graph, portal-based perception
llm/ ILLMProvider interface plus Gemini and deterministic mock implementations
content/
scenario-core/ scenario JSON schema and loader (JSON → SQLite)
scenario-builder/ Next.js web UI for authoring worlds
demo/ bundled scenarios (talking-room)
cli/ the playable loop (human or LLM actors, --scenario / --play flags)
tests/
integration/ cross-package tests against a mocked LLM
evals/ deliberate real-API evaluation runs
web/
landing/ Vite-based landing page
docs/ Astro-based documentation site
```
The engine core deliberately knows nothing about domain content (stats, traits, genres). Scenarios are plain JSON the loader ingests; what an attribute means is the scenario's business, not the engine's.
## Core Data Flow
1. An **Actor Agent** receives an epistemically-bounded view of the world and produces narrative prose.
2. The **Intent Decoder** splits prose into typed intents (`dialogue`, `action`, `monologue`).
3. The **World Architect** validates action intents against objective world state and generates structured deltas.
4. Deterministic code applies deltas to the **World State** (SQLite) and persists results.
5. Memory entries are written per-character, filtered through **Subjective Aliases**.
## A Research Instrument
Omnia's architecture doubles as an apparatus for studying how language models behave _as characters_ under controlled epistemic conditions. Monologue intents provide a window into private reasoning; attribute ACLs let you administer information with precision; identical initial conditions with swappable model providers enable reproducible experiments.

View File

@@ -0,0 +1,23 @@
---
title: Attributes & Privacy
description: Attribute-level access control and epistemic privacy model
---
Every entity, item, and location in Omnia is an attribute bag. Each attribute carries its own visibility (`PUBLIC` or `PRIVATE`) with an explicit access list.
## How Privacy Works
"The sword is cursed" is a private attribute checked in code, not a rule the model is politely asked to honor. Privacy lives at the level of the fact, not the entity:
- A character can be publicly a blacksmith and privately a spy.
- Even facts about _itself_ can be hidden from it unless explicitly granted (amnesia, repression, and sleeper agents come free with the model).
## Prompt-Injection-Proof Secrets
There is no instruction to override because the information was never serialized into the prompt. Epistemic privacy turns "the model shouldn't say this" (hard, unreliable) into "the model doesn't know this" (trivial, absolute).
## Key Components
- `hasAccess()` / ACL grant-revoke logic
- `getVisibleAttributesFor()` — viewer-relative attribute filtering
- Attribute schema with visibility metadata

View File

@@ -0,0 +1,24 @@
---
title: Spatial System
description: The spatial graph model for location and perception
---
Space in Omnia is modeled as a graph, not a coordinate grid.
## The Graph Model
```
world → region → location → point of interest
```
These nodes are connected by **portals** with sound and vision propagation values. When something happens, perception information bubbles outward through portals.
Today, actors perceive:
- Co-located entities
- Their location's visible attributes
Portal-propagated perception is on the roadmap.
## Design Rationale
There are no coordinates, no pathfinding algorithms, no collision geometry. A narrative engine doesn't need a tactical simulation — a discrete graph is sufficient for modeling who is where and who can perceive whom.

View File

@@ -0,0 +1,79 @@
---
title: Testing Strategy
description: Three-tiered testing architecture for deterministic and non-deterministic code
---
The central testing challenge: most of Omnia is deterministic and highly testable, but a few critical parts (LLM behavior) are non-deterministic. Treating both categories the same way — either mocking everything or hitting the real LLM API — is an anti-pattern.
## Test Tiers
### Tier 1: Unit Tests (Per-Package, No LLM)
Unit tests reside within each package's `tests/` directory. They do not use LLMs and do not perform I/O. This tier covers the majority of the codebase.
Examples:
- `hasAccess()` / ACL grant-revoke logic
- `addAttribute` rejecting duplicate names
- `WorldClock.advance()` / `getTimeOfDay()` boundaries
- The spatial bubble-up algorithm
- Zod schemas rejecting malformed input
**Execution:** Runs on every save and every commit.
### Tier 2: Integration Tests (Cross-Package, Mocked LLM)
Integration tests live in the root `tests/integration/` directory. They test cross-package flows using `MockLLMProvider` for speed and determinism.
```typescript
export class MockLLMProvider implements ILLMProvider {
providerName = "mock";
constructor(private responses: unknown[]) {}
private callCount = 0;
async generateStructuredResponse<T extends z.ZodTypeAny>(
request: LLMRequest<T>,
): Promise<LLMResponse<z.infer<T>>> {
const next = this.responses[this.callCount++];
return { success: true, data: request.schema.parse(next) };
}
}
```
A shared contract test suite runs against both `MockLLMProvider` and `GeminiProvider` to enforce real interchangeability.
### Tier 3: Evals (Real API, Run Deliberately)
Evals live in `tests/evals/`. They use real LLM APIs and are run manually via `test:evals`, excluded from the default Vitest run.
This tier tests privacy guarantees that unit tests cannot verify:
```typescript
// tests/evals/privacy-leak.eval.ts
const RUNS = 15;
let leaks = 0;
for (let i = 0; i < RUNS; i++) {
const response = await askAboutPrivateFact(npcWithoutAccess, secretFact);
if (containsFact(response, secretFact)) leaks++;
}
expect(leaks).toBe(0);
```
**Execution:** Run deliberately (e.g., weekly or pre-release).
## Directory Structure
```
omnia/
packages/
core/ src/ tests/ # Tier 1: Unit — no I/O, no LLM
intent/ src/ tests/
spatial/ src/ tests/
memory/ src/ tests/
architect/ src/ tests/
llm/ src/ tests/ # Includes MockLLMProvider + shared contract suite
tests/
integration/ # Tier 2: Cross-package flows, mocked LLM
evals/ # Tier 3: Real LLM calls, slow/costly/non-deterministic
```

View File

@@ -0,0 +1,45 @@
---
title: Introduction
description: What is Omnia and why it exists
---
![Omnia Logo](../../assets/img/logo.png)
An LLM-assisted narrative simulation engine where the **world state lives outside the model**, characters act through **intents that get validated** and applied by engine code, and each character's knowledge, memory, and emotional state are subjective and partial by construction.
Omnia is an engine for building narrative RPG-style worlds where characters are played by a language model. It is built to survive long play sessions instead of falling apart after twenty minutes.
## The Problem with the Naive Approach
Single-agent, single-context systems (AI Dungeon and its descendants) prompt one model to _be_ the world and everyone in it. That breaks in predictable ways over long sessions:
- **State Leaks:** Characters know things they had no way of learning, because a model with full context cannot help but use it. The assassin's target greets him by name.
- **Secrets Refuse to Stay Secret:** "Don't reveal this" is a suggestion a model can argue past, not a mechanism that says no. One clever player question and the conspiracy folds.
- **Consequences Evaporate:** Betray someone, apologize, and they forgive you a turn later because nothing is tracking the betrayal as a persistent fact.
- **Emotional Drift:** Emotional state is either frozen into a meaningless number (`trust: 40`) or handed to the model to grade itself, producing drifting, arbitrary values.
- **World Rot:** The world state slowly contradicts itself because the model has no structured place to keep it. The locked door is open, then locked, then never existed.
- **Everyone Is One Person:** Every character shares one context, so every character shares one mind. They can't genuinely surprise each other, lie to each other, or know different things — they're sock puppets on the same hand.
The root cause is the same in every case: the model is being asked to be the database, the physics engine, the referee, and the whole cast simultaneously — inside a context window that forgets, blends, and leaks.
## The Omnia Solution
Omnia answers every one of these failures with the same move: pull the thing that has to stay consistent out of the model and into structured, queryable, code-controlled state.
- **World State:** Lives in a SQLite database, not in a context window. It cannot drift, because nothing regenerates it — it only changes through validated deltas.
- **Actions:** Actions are proposals (Intents) that engine code validates and applies; they are never direct edits the model makes to the world. The model proposes; deterministic code disposes.
- **Epistemic Privacy:** Knowledge, memory, and emotion are modeled per character and kept partial on purpose. A character literally cannot reach for what it has not earned the right to know — the secret is not in its prompt, so there is nothing to jailbreak out of it.
## What This Buys You
The payoff is scenario complexity that uni-agent systems structurally cannot represent, no matter how good the model gets:
- **Real secrets, real dramatic irony.** One NPC knows the sword is cursed; the other does not. This holds for hundreds of turns not because the model is disciplined, but because the second NPC's prompts are constructed from an attribute set that simply does not contain the fact.
- **Genuine deception between characters.** Because each character acts from its own bounded view, characters can lie to each other — and be believed — with the truth intact in the world state.
- **Betrayal that stays betrayed.** Events persist as per-observer memory entries with outcomes. An apology adds a memory; it does not delete one.
- **Divergent accounts of the same event.** Two witnesses to the same scene hold two different buffer entries, filtered through their own aliases and vantage points.
- **Identity as information.** Characters refer to each other through subjective alias maps. Recognizing someone, being recognized, or staying anonymous are all mechanical states.
- **A physics referee that can say no.** "I pick the lock with a hairpin" is validated against world state by the Architect before anything changes.
- **Time that behaves.** A world clock advances by validated, per-action deltas, and memory is recalled with psychologically natural phrasing.
The general principle: **anything that must remain true is state; the model only ever supplies behavior.**