- Added a new SceneRulebook system to manage data-driven validation rules for actions. - Introduced rule checks for actions like "take", "open", "move", "introduce", and "describe". - Created a rulebook engine to evaluate conditions and enforce rules during action validation. - Enhanced action handling with support for scene entry and character descriptions. - Updated the architecture documentation to reflect the new rule-based validation approach. - Added new endpoints and improved the persistence layer for rulebooks.
5.9 KiB
CharacterGarden MVP — Strict Architecture
🧠 Core Principle
The system is a deterministic simulation engine.
The LLM is not a source of truth. It is an input/output translator only.
🧩 System Architecture
User / LLM Input (Prose) ↓ [Parser Layer] ↓ [Normalization Layer] ↓ [Truth Engine (Validation)] ↓ [State Mutation Engine] ↓ [Persistence Layer] ↓ [LLM Output Generation]
🔒 Non-Negotiable Rules
- Truth Engine MUST NEVER parse natural language
- Only structured Actions may mutate world state
- All mutations must be validated before execution
- World state is modified ONLY through engine functions
- LLM output is never trusted without validation
- Every turn must be fully traceable
📦 Core Data Contracts
Action (STRICT UNION TYPE)
export type Action =
| { type: "move"; actorId: string; targetId: string }
| { type: "take"; actorId: string; targetId: string }
| { type: "open"; actorId: string; targetId: string }
| { type: "close"; actorId: string; targetId: string }
| { type: "use"; actorId: string; targetId: string; toolId?: string }
| { type: "speak"; actorId: string; content: string }
| { type: "introduce"; actorId: string; targetId?: string; metadata?: Record<string, unknown> };
⚠️ DO NOT use generic type: string actions.
NormalizedAction
export type NormalizedAction = Action;
If invalid → reject BEFORE validation.
ValidationResult
export type ValidationResult = {
success: boolean;
reasonCode:
| "OK"
| "NOT_FOUND"
| "NOT_PRESENT"
| "LOCKED"
| "INVALID_TARGET"
| "MISSING_REQUIREMENT"
| "OUT_OF_TURN"
| "UNKNOWN";
message: string;
};
Turn (Traceable Execution Unit)
export type Turn = {
id: string;
rawText: string;
parsedActions: unknown[];
normalizedActions: NormalizedAction[];
validationResults: ValidationResult[];
appliedActions: NormalizedAction[];
timestamp: number;
};
🌍 World State (STRICT SCHEMA)
export type Entity = {
id: string;
name: string;
locationId?: string;
attributes?: Record<string, unknown>;
};
export type Location = {
id: string;
name: string;
connectedTo: string[];
};
export type WorldState = {
entities: Record<string, Entity>;
locations: Record<string, Location>;
inventory: Record<string, string[]>; // actorId → itemIds
flags: Record<string, boolean>;
};
🧠 System Layers
1. Parser Layer (parser/)
Responsibility:
- Convert prose → rough actions
- May use LLM
Output:
unknown[]
⚠️ Parser output is NOT trusted.
2. Normalization Layer (parser/normalizeActions.ts)
Responsibility:
- Enforce schema
- Resolve references (
he→john) - Fill missing fields
- Reject invalid structures
Input:
unknown[]
Output:
NormalizedAction[]
3. Truth Engine (engine/validate.ts)
Responsibility:
- Determine if action is valid
- MUST be deterministic
- MUST NOT mutate state
Input:
NormalizedAction + WorldState
Output:
ValidationResult
4. Mutation Engine (engine/apply.ts)
Responsibility:
- Apply ONLY successful actions
- Mutate world state
Input:
NormalizedAction + WorldState
Output:
WorldState
5. Persistence Layer (storage/)
Responsibility:
- Store:
- world state
- turns
- history
🔄 Turn Execution Pipeline
function processTurn(rawText: string): Turn {
const parsed = parse(rawText);
const normalized = normalize(parsed);
const validationResults = normalized.map(action =>
validate(action, worldState)
);
const successfulActions = normalized.filter((_, i) =>
validationResults[i].success
);
const newState = apply(successfulActions, worldState);
const turn: Turn = {
id: generateId(),
rawText,
parsedActions: parsed,
normalizedActions: normalized,
validationResults,
appliedActions: successfulActions,
timestamp: Date.now()
};
persist(turn, newState);
return turn;
}
🧠 Reference Resolution Rules
Handled in normalization layer.
Examples:
| Input | Output |
|---|---|
| "he" | actorId |
| "the door" | door_1 |
| "my key" | key_owned_by_actor |
Must be:
- deterministic
- context-aware
- testable
🧪 Debug & Traceability (REQUIRED)
Every turn MUST store:
- raw input
- parsed output
- normalized actions
- validation results
- applied actions
This enables:
- replay
- debugging
- AI correction
⚙️ Initial Action Rules (MVP)
move
- actor must exist
- target must be connected location
take
- item must be in same location
- item not already owned
open
- target must exist
- if locked → fail unless key present
introduce
- creates entity OR brings into scene
🚫 Anti-Patterns (DO NOT DO)
❌ Let LLM mutate state
❌ Store unvalidated actions
❌ Use dynamic/untyped actions
❌ Skip normalization
❌ Combine validation + mutation
❌ Allow hidden side effects
🚀 Future Extensions (Planned)
- Memory system (vector + summaries)
- Belief vs truth separation
- Multi-agent turns
- Time-based simulation
- Rule plugins per scenario
- UI action inspector
🧠 Mental Model
This system is:
A deterministic simulation engine with LLM-based I/O
NOT:
A chatbot with memory
✅ Definition of Done (MVP)
- Actions fully typed
- Normalization layer implemented
- Validation fully deterministic
- State mutation isolated
- Turn trace persisted
- Simple scenario (door + key) works
💬 Guidance for Copilot
When generating code:
- Prefer explicit types over generic objects
- Avoid dynamic structures
- Keep functions pure where possible
- Do not introduce hidden state
- Follow pipeline strictly