feat: Added Alias Resolver DeltaGenerator

This commit is contained in:
2026-07-09 09:26:56 +05:30
10 changed files with 187 additions and 880 deletions

View File

@@ -1,4 +1,4 @@
![Omnia Logo](./docs/assets/img/logo.png)
![Omnia Logo](web/docs/src/assets/img/logo.png)
An LLM-assisted narrative simulation engine where the <b>world state lives outside the model</b>, characters act through <b>intents that get validated</b> and applied by engine code, and each character's knowledge, memory, and emotional state are subjective and partial by construction.

View File

@@ -3,9 +3,9 @@ import fs from "fs";
import path from "path";
import readline from "readline";
import Database from "better-sqlite3";
import { SQLiteRepository } from "@omnia/core";
import { WorldState, SQLiteRepository } from "@omnia/core";
import { BufferRepository } from "@omnia/memory";
import { Architect } from "@omnia/architect";
import { Architect, AliasDeltaGenerator } from "@omnia/architect";
import {
ActorAgent,
ActorPromptBuilder,
@@ -27,7 +27,7 @@ class CLIProseGenerator implements IActorProseGenerator {
console.log(
"\n================================================================================",
);
console.log(`🎮 YOUR TURN: Playing as character "${entityId}"`);
console.log(`YOUR TURN: Playing as character "${entityId}"`);
console.log(
"================================================================================",
);
@@ -58,6 +58,34 @@ class CLIProseGenerator implements IActorProseGenerator {
}
}
/**
* Checks for co-located entities who do not have subjective aliases for each other,
* and calls the AliasDeltaGenerator to synthesize names based on visible attributes.
*/
async function runAliasResolution(
worldState: WorldState,
aliasGenerator: AliasDeltaGenerator,
coreRepo: SQLiteRepository,
): Promise<void> {
const entities = Array.from(worldState.entities.values());
for (const viewer of entities) {
if (!viewer.locationId) continue;
for (const target of entities) {
if (viewer.id === target.id) continue;
if (target.locationId === viewer.locationId) {
if (!viewer.aliases.has(target.id)) {
const alias = await aliasGenerator.generate(viewer, target);
viewer.aliases.set(target.id, alias);
console.log(`\n🔍 [Alias Resolved] "${viewer.id}" sees "${target.id}" -> alias: "${alias}"`);
// Save the viewer state with the new alias
coreRepo.saveEntity(viewer, worldState.id);
}
}
}
}
}
async function main() {
const args = process.argv.slice(2);
const scenarioArgIndex = args.indexOf("--scenario");
@@ -114,6 +142,7 @@ async function main() {
const llmProvider = new GeminiProvider(apiKey);
const architect = new Architect(llmProvider, coreRepo);
const aliasGenerator = new AliasDeltaGenerator(llmProvider);
console.log(
"\n================================================================================",
@@ -146,6 +175,9 @@ async function main() {
process.exit(1);
}
// Auto-resolve aliases for co-located entities who don't know each other yet
await runAliasResolution(currentWorldState, aliasGenerator, coreRepo);
const entities = Array.from(currentWorldState.entities.values());
for (const entity of entities) {
@@ -158,13 +190,18 @@ async function main() {
// Verbose mode: Output the generated prompt builder context before generation
if (isVerbose) {
const promptBuilder = new ActorPromptBuilder(bufferRepo, 20);
const { systemPrompt, userContext } = promptBuilder.build(currentWorldState, entity);
const { systemPrompt, userContext } = promptBuilder.build(
currentWorldState,
entity,
);
console.log(`\n🔍 [VERBOSE] Assembled Prompts for "${entity.id}":`);
console.log("\n--- SYSTEM PROMPT ---");
console.log(systemPrompt);
console.log("\n--- USER CONTEXT ---");
console.log(userContext);
console.log("--------------------------------------------------------------------------------");
console.log(
"--------------------------------------------------------------------------------",
);
}
if (!isPlayer) {
@@ -184,7 +221,9 @@ async function main() {
if (isVerbose) {
console.log(`\n🔍 [VERBOSE] Decoded Intents from Prose:`);
console.log(JSON.stringify(turnResult.intents.intents, null, 2));
console.log("--------------------------------------------------------------------------------");
console.log(
"--------------------------------------------------------------------------------",
);
}
// 3. Process each generated intent sequence through physics and memory
@@ -203,12 +242,18 @@ async function main() {
if (intent.type === "monologue") {
console.log(" Validation: Bypassed (monologue)");
} else {
console.log(` Validation Result: isValid = ${outcome.isValid}, reason = "${outcome.reason}"`);
console.log(
` Validation Result: isValid = ${outcome.isValid}, reason = "${outcome.reason}"`,
);
if (outcome.timeDelta) {
console.log(` Clock Delta: +${outcome.timeDelta.minutesToAdvance} min (${outcome.timeDelta.explanation})`);
console.log(
` Clock Delta: +${outcome.timeDelta.minutesToAdvance} min (${outcome.timeDelta.explanation})`,
);
}
}
console.log("--------------------------------------------------------------------------------");
console.log(
"--------------------------------------------------------------------------------",
);
}
// Save actor's subjective memory

View File

@@ -64,12 +64,6 @@
"value": "A clean, sterile grey jumpsuit with 'Alpha' embroidered in black lettering on the chest pocket.",
"visibility": "PUBLIC"
},
{
"name": "knowledge",
"value": "I woke up here. I know my name is Alpha. I know I agreed to participate in a paid scientific experiment, but my memories of who I was or my life before this room are gone.",
"visibility": "PRIVATE",
"allowedEntities": ["7c9b83b3-8cfb-4e89-8d77-626a5757d591"]
},
{
"name": "neural_erasure_dose",
"value": "150mg Compound-TR9",
@@ -77,18 +71,15 @@
"allowedEntities": []
}
],
"aliases": {
"bf3f29d2-cf11-4b11-9a99-b13c126d400e": "the person in the Beta jumpsuit"
},
"initialMemories": [
{
"id": "alpha-wake",
"timestamp": "2026-07-09T07:58:00.000Z",
"locationId": "white-room",
"intent": {
"type": "action",
"originalText": "I woke up on the floor. I stood up and looked around the white room, realizing my memories were wiped.",
"description": "wake up on the cold floor and stand up",
"type": "monologue",
"originalText": "I didn't have a choice. I would have been sent to jail if I hadn't agreed to do this experiement.",
"description": "",
"actorId": "7c9b83b3-8cfb-4e89-8d77-626a5757d591",
"targetIds": []
}
@@ -115,12 +106,6 @@
"value": "A clean, sterile grey jumpsuit with 'Beta' embroidered in black lettering on the chest pocket.",
"visibility": "PUBLIC"
},
{
"name": "knowledge",
"value": "I remember my name is Beta. I remember signing up for a paid research study. My past memories have been completely erased. I am disoriented.",
"visibility": "PRIVATE",
"allowedEntities": ["bf3f29d2-cf11-4b11-9a99-b13c126d400e"]
},
{
"name": "neural_erasure_dose",
"value": "150mg Compound-TR9",
@@ -128,9 +113,6 @@
"allowedEntities": []
}
],
"aliases": {
"7c9b83b3-8cfb-4e89-8d77-626a5757d591": "the person in the Alpha jumpsuit"
},
"initialMemories": [
{
"id": "beta-wake",
@@ -138,8 +120,8 @@
"locationId": "white-room",
"intent": {
"type": "action",
"originalText": "I opened my eyes, sitting against the wall. I rubbed my temples, trying to remember how I got here.",
"description": "wake up sitting against the wall and rub temples",
"originalText": "Why can't I remember anything before the research agreement. It's like my memory was erased.",
"description": "",
"actorId": "bf3f29d2-cf11-4b11-9a99-b13c126d400e",
"targetIds": []
}

View File

@@ -64,25 +64,30 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
expect(alphaName.hasAccess(alphaId)).toBe(true);
expect(alphaName.hasAccess(betaId)).toBe(false);
// Check private knowledge attribute visibility
const alphaKnowledge = alpha!.attributes.get("knowledge")!;
expect(alphaKnowledge.visibility).toBe("PRIVATE");
expect(alphaKnowledge.hasAccess(alphaId)).toBe(true);
expect(alphaKnowledge.hasAccess(betaId)).toBe(false);
// Check system-only attribute (neural_erasure_dose)
const alphaDose = alpha!.attributes.get("neural_erasure_dose")!;
expect(alphaDose.visibility).toBe("PRIVATE");
expect(alphaDose.hasAccess(alphaId)).toBe(false);
expect(alphaDose.hasAccess(betaId)).toBe(false);
// Check subjective aliases
expect(alpha!.aliases.get(betaId)).toBe("the person in the Beta jumpsuit");
// Verify subjective aliases are initially undefined
expect(alpha!.aliases.get(betaId)).toBeUndefined();
const beta = world!.getEntity(betaId);
expect(beta).toBeDefined();
expect(beta!.locationId).toBe("white-room");
expect(beta!.aliases.get(alphaId)).toBe("the person in the Alpha jumpsuit");
expect(beta!.aliases.get(alphaId)).toBeUndefined();
// Verify subjective aliases can be dynamically resolved via AliasDeltaGenerator
const { AliasDeltaGenerator } = await import("@omnia/architect");
const { MockLLMProvider } = await import("@omnia/llm");
const llmProvider = new MockLLMProvider([{ alias: "the person in the Beta jumpsuit" }]);
const aliasGenerator = new AliasDeltaGenerator(llmProvider);
const generatedAlias = await aliasGenerator.generate(alpha!, beta!);
expect(generatedAlias).toBe("the person in the Beta jumpsuit");
alpha!.aliases.set(betaId, generatedAlias);
expect(alpha!.aliases.get(betaId)).toBe("the person in the Beta jumpsuit");
// Verify subjective world state serializes the location attributes (epistemic inclusion)
const { serializeSubjectiveWorldState } = await import("@omnia/core");
@@ -103,12 +108,16 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
const alphaMemories = bufferRepo.listForOwner(alphaId);
expect(alphaMemories).toHaveLength(1);
expect(alphaMemories[0].id).toBe("alpha-wake");
expect(alphaMemories[0].intent.description).toBe("wake up on the cold floor and stand up");
expect(alphaMemories[0].intent.type).toBe("monologue");
expect(alphaMemories[0].intent.originalText).toContain("jail");
expect(alphaMemories[0].intent.description).toBe("");
const betaMemories = bufferRepo.listForOwner(betaId);
expect(betaMemories).toHaveLength(1);
expect(betaMemories[0].id).toBe("beta-wake");
expect(betaMemories[0].intent.description).toBe("wake up sitting against the wall and rub temples");
expect(betaMemories[0].intent.type).toBe("action");
expect(betaMemories[0].intent.originalText).toContain("agreement");
expect(betaMemories[0].intent.description).toBe("");
db.close();
});

View File

@@ -5,7 +5,7 @@ import tseslint from "typescript-eslint";
export default [
{
ignores: ["**/dist/**", "**/node_modules/**", "content/scenario-builder/**"],
ignores: ["**/dist/**", "**/node_modules/**", "content/scenario-builder/**", "**/.astro/**"],
},
js.configs.recommended,

View File

@@ -63,3 +63,58 @@ Target IDs: ${intent.targetIds.join(", ") || "(None)"}
return response.data;
}
}
export const AliasDeltaSchema = z.object({
alias: z.string(),
});
export type AliasDelta = z.infer<typeof AliasDeltaSchema>;
export class AliasDeltaGenerator {
constructor(private llmProvider: ILLMProvider) {}
/**
* Generates a natural, subjective descriptive alias for a target entity
* based on its visible attributes from the perspective of a viewer entity.
*/
async generate(
viewer: import("@omnia/core").Entity,
target: import("@omnia/core").Entity,
): Promise<string> {
const visibleAttrs = target.getVisibleAttributesFor(viewer.id);
const attrsStr = visibleAttrs.map((a) => `* ${a.name}: ${a.getValue()}`).join("\n");
const systemPrompt = `
You are the Alias Delta Generator for the World Architect.
Your task is to generate a natural, subjective, descriptive alias (noun phrase) that a viewer entity would use to refer to a target entity they are seeing for the first time, based ONLY on the target entity's visible public attributes.
Rules:
1. The alias must be a simple, natural, subjective noun phrase (e.g. "the tall woman in the grey jumpsuit", "the red-haired man", "the hooded figure").
2. Base the description strictly on the target's visible attributes. Do not invent details not present in the attributes.
3. Never use raw system IDs or UUIDs in the alias description.
4. Do not use the target's private name attribute unless they have explicit access to it (which is already filtered in the attributes list).
5. Return a structured JSON object containing:
- "alias": string representing the descriptive alias.
`.trim();
const userContext = `
Viewer Entity ID: ${viewer.id}
Target Entity ID: ${target.id}
Target's Visible Attributes:
${attrsStr || "(No visible attributes)"}
`.trim();
const response = await this.llmProvider.generateStructuredResponse({
systemPrompt,
userContext,
schema: AliasDeltaSchema,
});
if (!response.success || !response.data) {
throw new Error(`Failed to generate alias delta: ${response.error || "Unknown LLM error"}`);
}
return response.data.alias;
}
}

View File

@@ -1,8 +1,8 @@
import { describe, test, expect } from "vitest";
import Database from "better-sqlite3";
import { WorldState, Entity, SQLiteRepository } from "@omnia/core";
import { WorldState, Entity, SQLiteRepository, AttributeVisibility } from "@omnia/core";
import { MockLLMProvider } from "@omnia/llm";
import { Architect } from "@omnia/architect";
import { Architect, AliasDeltaGenerator } from "@omnia/architect";
import { Intent } from "@omnia/intent";
describe("Architect & LLMValidator Unit Tests (Tier 1)", () => {
@@ -172,3 +172,25 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
db.close();
});
});
describe("AliasDeltaGenerator Unit Tests (Tier 1)", () => {
test("successfully generates descriptive alias based on visible attributes", async () => {
const world = new WorldState("world-1");
const viewer = new Entity("viewer-1");
const target = new Entity("target-1");
target.addAttribute("appearance", "A tall elf with silver hair", AttributeVisibility.PUBLIC);
target.addAttribute("clothing", "A green tunic", AttributeVisibility.PUBLIC);
world.addEntity(viewer);
world.addEntity(target);
const mockResponse = {
alias: "the tall silver-haired elf in the green tunic",
};
const llmProvider = new MockLLMProvider([mockResponse]);
const generator = new AliasDeltaGenerator(llmProvider);
const result = await generator.generate(viewer, target);
expect(result).toBe("the tall silver-haired elf in the green tunic");
});
});

833
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,21 @@ Calculates the physical time duration (in minutes) that a validated action takes
```
- **Resolution**: The Architect advances `worldState.clock` by the returned minutes and saves to `SQLiteRepository`.
### Alias Delta Generator
Dynamically synthesizes subjective names/aliases when an entity perceives another entity for the first time:
- **Trigger**: Run automatically during simulation loops for any co-located entities who do not yet have a record in their subjective alias registry.
- **Epistemic Constraints**: Uses only the target entity's visible public attributes (e.g. appearance, clothing) as context, ensuring private names and details remain hidden from the observer.
- **Inputs**: The observer (`viewer: Entity`) and the observed (`target: Entity`), along with the target's visible attributes.
- **Output (Zod Schema)**:
```json
{
"alias": "the tall silver-haired elf in the green tunic"
}
```
- **Resolution**: Registers the generated descriptive alias to the observer's `aliases` map and persists the entity change to the SQLite database.
## 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.

View File

@@ -89,3 +89,15 @@ CREATE TABLE IF NOT EXISTS buffer_entries (
- **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.
## Time Naturalization
LLMs are poor at tracking quantized clock times, and real entities do not recall exact timestamps for past events. To make memories psychologically realistic, timestamps are converted into relative natural language phrases prior to prompt injection:
* **Utility**: `naturalizeTime(now: Date, past: Date): string` converts raw dates into subjective relative strings.
* **Granularity Tiers**:
* **Relative (< 6 hours)**: Returns short offsets like `"just now"`, `"moments ago"`, `"a couple hours ago"`, or `"a few hours ago"`.
* **Same Subjective Day (6h to 18h)**: Detects waking hours (05:00 - 21:59). If both times occur within the same waking block, it returns `"earlier today, in the {period}"` (where period is `morning`, `afternoon`, or `evening`).
* **Plausible Sleep Boundaries**: Past events from sleep hours are mapped to `"last night"`, `"around midnight"`, or `"late last night"`.
* **Coarse (>= 48 hours)**: Returns broad descriptors like `"a couple days ago"`, `"about a week ago"`, `"a couple months ago"`, or `"years ago"`.