- 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.
364 lines
5.9 KiB
Markdown
364 lines
5.9 KiB
Markdown
# 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
|
|
|
|
1. Truth Engine MUST NEVER parse natural language
|
|
2. Only structured Actions may mutate world state
|
|
3. All mutations must be validated before execution
|
|
4. World state is modified ONLY through engine functions
|
|
5. LLM output is never trusted without validation
|
|
6. Every turn must be fully traceable
|
|
|
|
---
|
|
|
|
# 📦 Core Data Contracts
|
|
|
|
## Action (STRICT UNION TYPE)
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
export type NormalizedAction = Action;
|
|
```
|
|
|
|
If invalid → reject BEFORE validation.
|
|
|
|
---
|
|
|
|
## ValidationResult
|
|
|
|
```ts
|
|
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)
|
|
|
|
```ts
|
|
export type Turn = {
|
|
id: string;
|
|
rawText: string;
|
|
|
|
parsedActions: unknown[];
|
|
normalizedActions: NormalizedAction[];
|
|
|
|
validationResults: ValidationResult[];
|
|
|
|
appliedActions: NormalizedAction[];
|
|
|
|
timestamp: number;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
# 🌍 World State (STRICT SCHEMA)
|
|
|
|
```ts
|
|
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:**
|
|
```ts
|
|
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:**
|
|
```ts
|
|
unknown[]
|
|
```
|
|
|
|
**Output:**
|
|
```ts
|
|
NormalizedAction[]
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Truth Engine (`engine/validate.ts`)
|
|
|
|
**Responsibility:**
|
|
- Determine if action is valid
|
|
- MUST be deterministic
|
|
- MUST NOT mutate state
|
|
|
|
**Input:**
|
|
```ts
|
|
NormalizedAction + WorldState
|
|
```
|
|
|
|
**Output:**
|
|
```ts
|
|
ValidationResult
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Mutation Engine (`engine/apply.ts`)
|
|
|
|
**Responsibility:**
|
|
- Apply ONLY successful actions
|
|
- Mutate world state
|
|
|
|
**Input:**
|
|
```ts
|
|
NormalizedAction + WorldState
|
|
```
|
|
|
|
**Output:**
|
|
```ts
|
|
WorldState
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Persistence Layer (`storage/`)
|
|
|
|
**Responsibility:**
|
|
- Store:
|
|
- world state
|
|
- turns
|
|
- history
|
|
|
|
---
|
|
|
|
# 🔄 Turn Execution Pipeline
|
|
|
|
```ts
|
|
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 |