mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
docs: Migrated to astro v7 for live docs
This commit is contained in:
33
web/docs/astro.config.mjs
Normal file
33
web/docs/astro.config.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import starlight from "@astrojs/starlight";
|
||||
|
||||
export default defineConfig({
|
||||
site: "https://omnia.omniasimulation.com",
|
||||
base: "/docs",
|
||||
integrations: [
|
||||
starlight({
|
||||
title: "Omnia Docs",
|
||||
logo: {
|
||||
src: "./src/assets/img/logo.png",
|
||||
},
|
||||
social: [{ icon: "github", label: "GitHub", href: "https://github.com/sortedcord/omnia" }],
|
||||
sidebar: [
|
||||
{
|
||||
label: "Introduction",
|
||||
slug: "intro",
|
||||
},
|
||||
{
|
||||
label: "Architecture",
|
||||
items: [{ autogenerate: { directory: "architecture" } }],
|
||||
},
|
||||
{
|
||||
label: "Guides",
|
||||
items: [{ autogenerate: { directory: "guides" } }],
|
||||
},
|
||||
],
|
||||
editLink: {
|
||||
baseUrl: "https://github.com/sortedcord/omnia/edit/main/web/docs/",
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
17
web/docs/package.json
Normal file
17
web/docs/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.41.3",
|
||||
"astro": "^7.0.7",
|
||||
"sharp": "^0.33.5"
|
||||
}
|
||||
}
|
||||
BIN
web/docs/src/assets/img/logo.png
Normal file
BIN
web/docs/src/assets/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
7
web/docs/src/content.config.ts
Normal file
7
web/docs/src/content.config.ts
Normal 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() }),
|
||||
};
|
||||
93
web/docs/src/content/docs/architecture/actor.md
Normal file
93
web/docs/src/content/docs/architecture/actor.md
Normal 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 |
|
||||
70
web/docs/src/content/docs/architecture/architect.md
Normal file
70
web/docs/src/content/docs/architecture/architect.md
Normal 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.
|
||||
88
web/docs/src/content/docs/architecture/intents.md
Normal file
88
web/docs/src/content/docs/architecture/intents.md
Normal 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)).
|
||||
91
web/docs/src/content/docs/architecture/memory.md
Normal file
91
web/docs/src/content/docs/architecture/memory.md
Normal 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.
|
||||
43
web/docs/src/content/docs/architecture/overview.md
Normal file
43
web/docs/src/content/docs/architecture/overview.md
Normal 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.
|
||||
23
web/docs/src/content/docs/architecture/privacy.md
Normal file
23
web/docs/src/content/docs/architecture/privacy.md
Normal 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
|
||||
24
web/docs/src/content/docs/architecture/spatial.md
Normal file
24
web/docs/src/content/docs/architecture/spatial.md
Normal 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.
|
||||
79
web/docs/src/content/docs/guides/testing.md
Normal file
79
web/docs/src/content/docs/guides/testing.md
Normal 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
|
||||
```
|
||||
45
web/docs/src/content/docs/intro.md
Normal file
45
web/docs/src/content/docs/intro.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Introduction
|
||||
description: What is Omnia and why it exists
|
||||
---
|
||||
|
||||

|
||||
|
||||
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.**
|
||||
11
web/docs/tsconfig.json
Normal file
11
web/docs/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@assets/*": ["src/assets/*"]
|
||||
}
|
||||
},
|
||||
"include": [".astro/types.d.ts", "**/*"],
|
||||
"exclude": ["dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user