mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 20:12:48 +05:30
Compare commits
7 Commits
53b221f80c
...
907c3b8ed7
| Author | SHA1 | Date | |
|---|---|---|---|
| 907c3b8ed7 | |||
| 9642e2bb54 | |||
| 8934422a4d | |||
| 63badedf75 | |||
| 717f9f20d4 | |||
| 3da043952c | |||
| 17c5b95f3b |
163
cli/src/index.ts
163
cli/src/index.ts
@@ -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,8 +58,92 @@ 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 logFileIndex = args.indexOf("--log-file");
|
||||
let logStream: fs.WriteStream | undefined;
|
||||
if (logFileIndex !== -1 && args[logFileIndex + 1]) {
|
||||
const logFilePath = path.resolve(args[logFileIndex + 1]);
|
||||
logStream = fs.createWriteStream(logFilePath, {
|
||||
flags: "w",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
// Monkeypatch console.log
|
||||
const originalLog = console.log;
|
||||
console.log = (...messageArgs: unknown[]) => {
|
||||
originalLog(...messageArgs);
|
||||
const text =
|
||||
messageArgs
|
||||
.map((arg) => {
|
||||
if (typeof arg === "object" && arg !== null) {
|
||||
try {
|
||||
return JSON.stringify(arg, null, 2);
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
})
|
||||
.join(" ") + "\n";
|
||||
logStream?.write(text);
|
||||
};
|
||||
|
||||
// Monkeypatch console.error
|
||||
const originalError = console.error;
|
||||
console.error = (...messageArgs: unknown[]) => {
|
||||
originalError(...messageArgs);
|
||||
const text =
|
||||
messageArgs
|
||||
.map((arg) => {
|
||||
if (typeof arg === "object" && arg !== null) {
|
||||
try {
|
||||
return JSON.stringify(arg, null, 2);
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
})
|
||||
.join(" ") + "\n";
|
||||
logStream?.write("[ERROR] " + text);
|
||||
};
|
||||
|
||||
process.on("exit", () => {
|
||||
logStream?.end();
|
||||
});
|
||||
}
|
||||
const scenarioArgIndex = args.indexOf("--scenario");
|
||||
const scenarioPath =
|
||||
scenarioArgIndex !== -1
|
||||
@@ -114,6 +198,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 +231,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 +246,56 @@ 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);
|
||||
console.log(`\n🔍 [VERBOSE] Assembled Prompts for "${entity.id}":`);
|
||||
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("\n--- CONTEXT BREAKDOWN ---");
|
||||
|
||||
const userSections = userContext.split("\n\n");
|
||||
const momentSection =
|
||||
userSections.find((s) => s.startsWith("=== CURRENT MOMENT ===")) ||
|
||||
"";
|
||||
const worldSection =
|
||||
userSections.find((s) =>
|
||||
s.startsWith("=== THE WORLD AS YOU PERCEIVE IT ==="),
|
||||
) || "";
|
||||
const memorySection =
|
||||
userSections.find((s) =>
|
||||
s.startsWith("=== YOUR RECENT MEMORY ==="),
|
||||
) || "";
|
||||
|
||||
const systemChars = systemPrompt.length;
|
||||
const momentChars = momentSection.length;
|
||||
const worldChars = worldSection.length;
|
||||
const memoryChars = memorySection.length;
|
||||
const totalChars = systemChars + userContext.length;
|
||||
|
||||
const estTokens = (chars: number) => Math.ceil(chars / 4);
|
||||
|
||||
console.log(
|
||||
` ├─ System Instructions: ${systemChars.toLocaleString()} chars (~${estTokens(systemChars)} tokens)`,
|
||||
);
|
||||
console.log(
|
||||
` ├─ Current Moment Context: ${momentChars.toLocaleString()} chars (~${estTokens(momentChars)} tokens)`,
|
||||
);
|
||||
console.log(
|
||||
` ├─ World Perception: ${worldChars.toLocaleString()} chars (~${estTokens(worldChars)} tokens)`,
|
||||
);
|
||||
console.log(
|
||||
` ├─ Recent Memory Buffer: ${memoryChars.toLocaleString()} chars (~${estTokens(memoryChars)} tokens)`,
|
||||
);
|
||||
console.log(
|
||||
` └─ TOTAL ESTIMATED INPUT: ${totalChars.toLocaleString()} chars (~${estTokens(totalChars)} tokens)`,
|
||||
);
|
||||
console.log(
|
||||
"--------------------------------------------------------------------------------",
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlayer) {
|
||||
@@ -182,9 +313,11 @@ async function main() {
|
||||
|
||||
// Verbose mode: Output decoded intent structures
|
||||
if (isVerbose) {
|
||||
console.log(`\n🔍 [VERBOSE] Decoded Intents from Prose:`);
|
||||
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
|
||||
@@ -197,18 +330,24 @@ async function main() {
|
||||
|
||||
// Verbose mode: Output architect evaluation
|
||||
if (isVerbose) {
|
||||
console.log(`\n🔍 [VERBOSE] Architect Intent Processing:`);
|
||||
console.log(`\n[VERBOSE] Architect Intent Processing:`);
|
||||
console.log(` Type: ${intent.type}`);
|
||||
console.log(` Description: "${intent.description}"`);
|
||||
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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -52,12 +52,12 @@ export class ActorPromptBuilder {
|
||||
worldState: WorldState,
|
||||
entity: Entity,
|
||||
): { systemPrompt: string; userContext: string } {
|
||||
const systemPrompt = this.buildSystemPrompt(entity);
|
||||
const systemPrompt = this.buildSystemPrompt();
|
||||
const userContext = this.buildUserContext(worldState, entity);
|
||||
return { systemPrompt, userContext };
|
||||
}
|
||||
|
||||
private buildSystemPrompt(entity: Entity): string {
|
||||
private buildSystemPrompt(): string {
|
||||
return `
|
||||
You are an actor agent embodying a single character in a narrative simulation. You ARE this character — act immersively, naturally, and in-character at all times. Do not break character, do not reference being an AI or a system, and do not narrate from outside the character's perspective.
|
||||
|
||||
@@ -67,21 +67,18 @@ Your output is a short block of narrative prose describing what your character d
|
||||
- Think internally / reflect / feel → this becomes a "monologue" intent. NO ONE else perceives it. It bypasses all validation and is written straight to your private memory. Use this for inner thoughts, doubts, plans, and feelings that you would not voice aloud.
|
||||
|
||||
Guidelines:
|
||||
- Always write in the first person (e.g., "I do this", "I say", "I think").
|
||||
- Only describe your character's own actions, spoken words, and internal reactions. Do NOT narrate or describe the environment, the room, your surroundings, or other characters' actions, as these are managed by the simulation engine.
|
||||
- Stay strictly within what your character knows. If an attribute, entity, or fact is not present in your context below, your character does not know it — do not invent it or act on it.
|
||||
- Refer to other entities by the subjective names/aliases given in your context, never by raw system IDs.
|
||||
- Keep your prose vivid but concise. A single response may contain more than one intent (e.g., you may think, then speak, then act) — write them in natural narrative order.
|
||||
- Not every response requires an outward action. It is perfectly valid to only think (a monologue) and do nothing perceivable.
|
||||
- Never speak or act on another entity's behalf — you only control your own character.
|
||||
|
||||
Your character's identifier in this world is "${entity.id}".
|
||||
".
|
||||
`.trim();
|
||||
}
|
||||
|
||||
private buildUserContext(
|
||||
worldState: WorldState,
|
||||
entity: Entity,
|
||||
): string {
|
||||
private buildUserContext(worldState: WorldState, entity: Entity): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// --- Subjective present time ---
|
||||
@@ -96,7 +93,10 @@ Your character's identifier in this world is "${entity.id}".
|
||||
);
|
||||
|
||||
// --- Recent memory ---
|
||||
const memorySection = this.buildMemorySection(entity, worldState.clock.get());
|
||||
const memorySection = this.buildMemorySection(
|
||||
entity,
|
||||
worldState.clock.get(),
|
||||
);
|
||||
if (memorySection) {
|
||||
sections.push(memorySection);
|
||||
}
|
||||
@@ -119,12 +119,22 @@ Your character's identifier in this world is "${entity.id}".
|
||||
}
|
||||
|
||||
const recent = entries.slice(-this.memoryLimit);
|
||||
const lines = recent.map((entry) => {
|
||||
const groupedLines: string[] = [];
|
||||
let currentGroup: string | null = null;
|
||||
|
||||
for (const entry of recent) {
|
||||
const serialized = serializeSubjectiveBufferEntry(entry, entity);
|
||||
const when = naturalizeTime(now, new Date(entry.timestamp));
|
||||
return `${serialized} (${when})`;
|
||||
});
|
||||
|
||||
return `=== YOUR RECENT MEMORY ===\n${lines.join("\n")}`;
|
||||
if (when !== currentGroup) {
|
||||
currentGroup = when;
|
||||
const header = when.charAt(0).toUpperCase() + when.slice(1);
|
||||
groupedLines.push(header);
|
||||
}
|
||||
|
||||
groupedLines.push(` - ${serialized}`);
|
||||
}
|
||||
|
||||
return `=== YOUR RECENT MEMORY ===\n${groupedLines.join("\n")}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,13 @@ export const TimeDeltaSchema = z.object({
|
||||
export type TimeDelta = z.infer<typeof TimeDeltaSchema>;
|
||||
|
||||
export interface IDeltaGenerator<T> {
|
||||
generate(
|
||||
worldState: WorldState,
|
||||
intent: Intent,
|
||||
): Promise<T>;
|
||||
generate(worldState: WorldState, intent: Intent): Promise<T>;
|
||||
}
|
||||
|
||||
export class TimeDeltaGenerator implements IDeltaGenerator<TimeDelta> {
|
||||
constructor(private llmProvider: ILLMProvider) {}
|
||||
|
||||
async generate(
|
||||
worldState: WorldState,
|
||||
intent: Intent,
|
||||
): Promise<TimeDelta> {
|
||||
async generate(worldState: WorldState, intent: Intent): Promise<TimeDelta> {
|
||||
const systemPrompt = `
|
||||
You are the Time Delta Generator for the World Architect.
|
||||
Your task is to judge how much time (in minutes) a proposed action would logically take to execute in the physical world.
|
||||
@@ -57,9 +51,71 @@ Target IDs: ${intent.targetIds.join(", ") || "(None)"}
|
||||
});
|
||||
|
||||
if (!response.success || !response.data) {
|
||||
throw new Error(`Failed to generate time delta: ${response.error || "Unknown LLM error"}`);
|
||||
throw new Error(
|
||||
`Failed to generate time delta: ${response.error || "Unknown LLM error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
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.
|
||||
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. Keep the phrase very short. Not more than 5 words. These aliases can also be internal nicknames for those entities.
|
||||
6. 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,13 +25,6 @@ export function serializeSubjectiveBufferEntry(
|
||||
entry: BufferEntry,
|
||||
viewer: Entity,
|
||||
): string {
|
||||
const dateObj = new Date(entry.timestamp);
|
||||
// Ensure a deterministic timezone/format for testing and model inputs:
|
||||
const timeStr = dateObj.toLocaleTimeString("en-US", {
|
||||
hour12: true,
|
||||
timeZone: "UTC",
|
||||
});
|
||||
|
||||
const actorAlias = resolveAlias(viewer, entry.intent.actorId);
|
||||
|
||||
const targetAliases = entry.intent.targetIds.map((tid) =>
|
||||
@@ -48,7 +41,7 @@ export function serializeSubjectiveBufferEntry(
|
||||
}
|
||||
}
|
||||
|
||||
return `[${timeStr}] ${actorAlias} ${details}`;
|
||||
return `${actorAlias} ${details}`;
|
||||
}
|
||||
|
||||
export class BufferRepository {
|
||||
|
||||
@@ -39,7 +39,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
};
|
||||
|
||||
const result = serializeSubjectiveBufferEntry(entry, viewer);
|
||||
expect(result).toBe('[12:00:00 PM] the hooded figure spoke to the bartender: "Bob greets Charlie"');
|
||||
expect(result).toBe('the hooded figure spoke to the bartender: "Bob greets Charlie"');
|
||||
});
|
||||
|
||||
test("serializes action intent with outcome details", () => {
|
||||
@@ -65,7 +65,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
};
|
||||
|
||||
const result = serializeSubjectiveBufferEntry(entry, viewer);
|
||||
expect(result).toBe('[12:05:00 PM] the hooded figure Bob attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)');
|
||||
expect(result).toBe('the hooded figure Bob attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)');
|
||||
});
|
||||
|
||||
test("serializes self-reference and unfamiliar actors", () => {
|
||||
@@ -86,7 +86,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
};
|
||||
|
||||
const resultSelf = serializeSubjectiveBufferEntry(entrySelf, viewer);
|
||||
expect(resultSelf).toBe("[12:10:00 PM] you open the window");
|
||||
expect(resultSelf).toBe("you open the window");
|
||||
|
||||
const entryUnfamiliar: BufferEntry = {
|
||||
id: "entry-unfamiliar",
|
||||
@@ -103,7 +103,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
|
||||
};
|
||||
|
||||
const resultUnfamiliar = serializeSubjectiveBufferEntry(entryUnfamiliar, viewer);
|
||||
expect(resultUnfamiliar).toBe("[12:15:00 PM] an unfamiliar figure knock on the door");
|
||||
expect(resultUnfamiliar).toBe("an unfamiliar figure knock on the door");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import starlight from "@astrojs/starlight";
|
||||
import mermaid from "astro-mermaid";
|
||||
|
||||
export default defineConfig({
|
||||
site: "https://omnia.omniasimulation.com",
|
||||
base: "/docs",
|
||||
integrations: [
|
||||
mermaid(),
|
||||
starlight({
|
||||
title: "Omnia Docs",
|
||||
logo: {
|
||||
src: "./src/assets/img/logo.png",
|
||||
replacesTitle: true,
|
||||
},
|
||||
social: [{ icon: "github", label: "GitHub", href: "https://github.com/sortedcord/omnia" }],
|
||||
social: [{ icon: "github", label: "GitHub", href: "https://github.com/sortedcord/omnia-consolidated" }],
|
||||
sidebar: [
|
||||
{
|
||||
label: "Introduction",
|
||||
slug: "intro",
|
||||
slug: "index",
|
||||
},
|
||||
{
|
||||
label: "Architecture",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user