feat(interpreter): implement hybrid intent resolution with LLM and deterministic fallback

- Added new contracts for intent interpretation, including InterpreterOutput and ResolverMode.
- Implemented deterministic intent resolver with clarity checks for ambiguous references and empty input.
- Developed LLM intent resolver that communicates with an external model, handling JSON responses and fallback clarifications.
- Created an interpretTurn function to manage intent resolution based on the selected resolver mode.
- Introduced validation for interpreter output to ensure integrity before processing actions.
- Established a turn manager to orchestrate turn processing, including action validation and world state mutation.
- Added integration tests to verify the functionality of the new intent resolution system.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-26 14:06:14 -04:00
parent ff9b86c3e9
commit fc10e46ccc
23 changed files with 1530 additions and 1012 deletions

View File

@@ -1,364 +1,138 @@
# CharacterGarden MVP — Strict Architecture
# CharacterGarden MVP - Current Architecture (April 2026)
## 🧠 Core Principle
## Core Principle
> The system is a **deterministic simulation engine**.
> The LLM is **not a source of truth**. It is an **input/output translator only**.
The simulation engine is deterministic and authoritative.
The LLM layer is an intent interpreter and resolver, not a source of truth.
---
# 🧩 System Architecture
## Live System Layers
User / LLM Input (Prose)
[Parser Layer]
[Normalization Layer]
[Truth Engine (Validation)]
[State Mutation Engine]
|
[Intent Interpreter Layer]
|
[Turn Manager]
|
[Truth Engine + Scene Rulebook Validation]
|
[World Mutation Engine]
|
[Persistence Layer]
[LLM Output Generation]
|
[Frontend/API Response]
---
## Non-Negotiable Rules
# 🔒 Non-Negotiable Rules
1. Truth engine must never parse natural language.
2. Only structured actions can mutate world state.
3. Every mutation must pass validation before apply.
4. Rulebook rules are data-driven and editable.
5. Interpreter output is traceable and never auto-trusted when unresolved.
6. Every turn remains replayable end-to-end.
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
## Current Canonical Actions
---
- inspect
- move
- open
- take
- introduce
- describe
- transfer
# 📦 Core Data Contracts
## Action Contracts (Current)
## Action (STRICT UNION TYPE)
- Action shape is contract-based in app/src/contracts/action.ts
- Validation contracts in app/src/contracts/validation.ts
- Turn contracts in app/src/contracts/turn.ts
- Interpreter contracts in app/src/contracts/intent.ts
```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> };
```
## Scene Rulebook (Data-Driven Validation)
⚠️ DO NOT use generic `type: string` actions.
Validation is now externalized into SceneRulebook definitions.
---
Key capabilities already implemented:
## NormalizedAction
- Actor authorization checks (actorIdIn, actorNameIn)
- Conditional creation checks (actionMetadataEq)
- Inventory ownership checks (itemInInventory)
- Existing deterministic checks (entity type/exists, same location, attribute checks)
```ts
export type NormalizedAction = Action;
```
This supports:
If invalid → reject BEFORE validation.
- Restricting who can introduce characters
- Restricting who can create missing items via take
- Validating transfer only when actor owns item and recipient is valid
---
## Turn Execution (Current)
## ValidationResult
1. Interpret raw turn text using interpreter module.
2. If unresolved:
- return clarification/rejection state
- persist trace turn with no applied actions
3. If resolved:
- validate actions with truth engine + active rulebook
- apply successful actions
- persist turn, actions, validation results, and world state
```ts
export type ValidationResult = {
success: boolean;
reasonCode:
| "OK"
| "NOT_FOUND"
| "NOT_PRESENT"
| "LOCKED"
| "INVALID_TARGET"
| "MISSING_REQUIREMENT"
| "OUT_OF_TURN"
| "UNKNOWN";
message: string;
};
```
## Interpreter + Turn Manager (New)
---
- Interpreter module: app/src/interpreter/interpretTurn.ts
- Turn manager orchestrator: app/src/turns/turnManager.ts
- processTurn delegates to turn manager: app/src/turns/processTurn.ts
## Turn (Traceable Execution Unit)
Current interpreter statuses:
```ts
export type Turn = {
id: string;
rawText: string;
- resolved
- needs_clarification
- rejected
parsedActions: unknown[];
normalizedActions: NormalizedAction[];
## Current Domain Behaviors
validationResults: ValidationResult[];
- take can create missing items when createIfMissing is present and actor is authorized by rulebook
- introduce can create missing characters when rulebook allows
- transfer moves items between inventories when rulebook checks pass
appliedActions: NormalizedAction[];
## Persistence (Current)
timestamp: number;
};
```
SQLite tables already backing turns and world snapshots:
---
- turns
- actions
- validation_results
- entities
- world_states
- rulebooks
# 🌍 World State (STRICT SCHEMA)
## API Surface (Current)
```ts
export type Entity = {
id: string;
name: string;
locationId?: string;
attributes?: Record<string, unknown>;
};
- GET /api/state
- POST /api/turn
- POST /api/reset
- GET /api/rulebook
- PUT /api/rulebook
- GET /api/rulebooks
export type Location = {
id: string;
name: string;
connectedTo: string[];
};
## Operational Rule: Validation in Docker
export type WorldState = {
entities: Record<string, Entity>;
locations: Record<string, Location>;
inventory: Record<string, string[]>; // actorId → itemIds
flags: Record<string, boolean>;
};
```
Build and runtime checks should run in containers, not host Node.
---
- Backend build: docker compose run --rm app npm run build
- Frontend build: docker compose run --rm frontend npm run build
# 🧠 System Layers
## Immediate Path Forward
---
1. LLM adapter hardening
- Tune prompt/schema validation for model drift.
- Add configurable model + timeout policy per environment.
## 1. Parser Layer (`parser/`)
2. Rulebook governance
- Keep versioned rulebook migration path active as rule schema evolves.
- Split rules into policy packs: creation, transfer, social.
**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
3. Testing depth
- Expand Docker-executed integration tests for:
- createIfMissing authorization matrix
- transfer ownership/location checks
- unresolved clarification flows
- multi-action turn behavior