docs: add invention system refinements design

Covers duplicate fix (load bug), name normalization, stat-driven
invention selection, race condition guard, and token usage tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-03-09 20:15:34 +00:00
parent 45ecad89bb
commit bc0e1154b5
@@ -0,0 +1,100 @@
# Invention System Refinements — Design
## Problem
The invention system has several issues:
1. **Duplicate inventions**: After server restart, invented items are loaded into the timeline but not re-registered into `itemRegistry`/`recipeRegistry`. The duplicate check runs against an empty registry, so previously invented items can be invented again.
2. **Name inconsistency**: The LLM sometimes returns snake_case names ("wooden_hammer") instead of display names ("Wooden Hammer").
3. **No personality influence on invention choice**: Only INT and CUR affect invention *probability*, but no stats influence *what* gets invented. This limits diversity across worlds.
4. **No token usage visibility**: No way to track LLM API costs for future optimization.
## Changes
### 1. Fix: Re-register inventions on load
In `GameLoop.ts`, after loading inventions from the DB into the timeline, iterate through each `InventionEntry` and register it into `itemRegistry` and `recipeRegistry`. This restores the duplicate-prevention invariant across restarts.
### 2. Fix: Name normalization
Add "Always use Title Case for item names" to the LLM prompt. The `toSnakeCase()` ID derivation in `inventionValidator.ts` stays as-is — it correctly generates IDs from either format.
### 3. Feature: Stat-driven invention selection
Change the LLM prompt from "invent ONE item" to "consider 3 possible inventions, then select the one that best matches this NPC's personality." Include the full 10-stat block with readable names and scale context. The LLM returns a single JSON object (same schema as today) plus a short `reasoning` field. No extra calls, no inflated response.
#### Revised prompt
**System:**
```
You are an inventor in a medieval fantasy village simulation. Given available materials and an NPC's stats, consider 3 possible inventions, then select the one that best fits the NPC's personality. Respond ONLY with valid JSON. No markdown.
```
**User:**
```
{{npcName}} is having a creative moment.
Stats (each ranges 3-18, 10 is average):
Strength:{{str}} Dexterity:{{dex}} Constitution:{{con}} Intelligence:{{int}}
Perception:{{per}} Sociability:{{soc}} Courage:{{cou}} Curiosity:{{cur}}
Empathy:{{emp}} Temperament:{{tmp}}
Available materials:
{{materials}}
Known items (do not reinvent):
{{allItems}}
Consider 3 possible inventions using 2-3 existing materials, then pick
the one that best matches this NPC's personality and stats.
Use Title Case for the item name.
Respond with JSON:
{"name": "Item Name", "description": "brief flavor text",
"reasoning": "why this suits the NPC",
"category": "resource|tool|material|structure",
"inputs": [{"itemId": "existing_item_id", "quantity": N}],
"workshopType": null, "toolRequired": null}
```
Key differences from current prompt:
- Full 10-stat block with readable names and scale (3-18, 10 avg)
- "Known items" replaces separate seed/invented lists — covers both, prevents reinventing seed items
- "Consider 3, pick 1" guides LLM reasoning without inflating response
- Title Case instruction for consistent display names
- `reasoning` field added for debugging/potential UI display
### 4. Duplicate prevention (belt + suspenders)
**Prompt layer**: Send all known item names (seed + invented) in the "Known items" list, so the LLM avoids reinventing existing things (including seed items like "Hammer").
**Validation layer**: Existing `validateInvention()` check against `itemRegistry` remains unchanged. Fixing the load bug (section 1) makes it work correctly.
**Race condition**: Keep existing per-entity `pendingEntities` set. Add a global `pendingItemIds` set — when the LLM returns a valid result, add its derived `itemId` to pending before registration, clear after. This prevents two NPCs from inventing the same thing in the same tick window.
### 5. Feature: Token usage tracking
- **Capture**: In `openRouterClient.ts`, extract the `usage` object from the OpenRouter API response and return it alongside the completion text.
- **Storage**: In-memory ring buffer in `llmService.ts` — last 100 calls, each recording `{timestamp, promptTokens, completionTokens, totalTokens, templateName}`.
- **Logging**: Per-call one-liner: `[LLM] invention: 340 in / 120 out tokens`.
- **No persistence**: Dev/ops observability only, not game state.
## Files affected
- `server/src/game/GameLoop.ts` — re-register inventions on load
- `server/src/llm/templates.ts` — revised invention prompt
- `server/src/systems/inventionSystem.ts` — pass full stats, global pendingItemIds set
- `server/src/llm/openRouterClient.ts` — return token usage from response
- `server/src/llm/llmService.ts` — token usage ring buffer + logging
- `server/src/industry/inventionValidator.ts` — accept optional `reasoning` field
- `server/src/industry/inventionRegistrar.ts` — no changes expected
- `server/src/industry/inventionParser.ts` — parse `reasoning` field
## Config values
All existing invention config values in `industryConfig.ts` remain unchanged:
- `inventionCheckInterval: 100`
- `inventionBaseChance: 0.005`
- `inventionIntelligenceScale: 0.1`
- `inventionCuriosityScale: 0.1`
- `inventionMinChance: 0.001`
- `inventionMaxChance: 0.05`