cli: FIRST RUN!!!! ITS ALIVE!

This commit is contained in:
2026-07-09 02:51:57 +05:30
parent 701bc56d0c
commit fadea41e6f
11 changed files with 417 additions and 46 deletions

View File

@@ -7,6 +7,15 @@
".": "./dist/index.js"
},
"dependencies": {
"@omnia/core": "workspace:*",
"@omnia/spatial": "workspace:*",
"@omnia/memory": "workspace:*",
"@omnia/intent": "workspace:*",
"@omnia/architect": "workspace:*",
"@omnia/actor": "workspace:*",
"@omnia/llm": "workspace:*",
"@omnia/scenario-core": "workspace:*",
"better-sqlite3": "^12.11.1",
"dotenv": "^17.4.2"
}
}

View File

@@ -1,6 +1,287 @@
import dotenv from "dotenv";
import fs from "fs";
import path from "path";
import readline from "readline";
import Database from "better-sqlite3";
import { SQLiteRepository } from "@omnia/core";
import { BufferRepository } from "@omnia/memory";
import { Architect } from "@omnia/architect";
import {
ActorAgent,
ActorPromptBuilder,
IActorProseGenerator,
buildBufferEntryForIntent,
} from "@omnia/actor";
import { GeminiProvider } from "@omnia/llm";
import { ScenarioLoader } from "@omnia/scenario-core";
// Load environment variables once at CLI application entry point
// Load environment variables
dotenv.config();
export {};
class CLIProseGenerator implements IActorProseGenerator {
async generate(
entityId: string,
systemPrompt: string,
userContext: string,
): Promise<string> {
console.log(
"\n================================================================================",
);
console.log(`🎮 YOUR TURN: Playing as character "${entityId}"`);
console.log(
"================================================================================",
);
console.log(userContext);
console.log(
"================================================================================",
);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise<string>((resolve) => {
rl.question(
"\nDescribe what your character does, says, or thinks (or type 'exit' to quit):\n> ",
(answer) => {
rl.close();
const trimmed = answer.trim();
if (trimmed.toLowerCase() === "exit") {
console.log("\nExiting simulation. Goodbye!");
process.exit(0);
}
resolve(trimmed);
},
);
});
}
}
async function main() {
const args = process.argv.slice(2);
const scenarioArgIndex = args.indexOf("--scenario");
const scenarioPath =
scenarioArgIndex !== -1
? args[scenarioArgIndex + 1]
: "content/demo/scenarios/talking-room.json";
const playArgIndex = args.indexOf("--play");
const playEntityId = playArgIndex !== -1 ? args[playArgIndex + 1] : undefined;
const dbPath = path.resolve("./omnia.db");
console.log(`Initializing SQLite database at: ${dbPath}`);
const db = new Database(dbPath);
const coreRepo = new SQLiteRepository(db);
const bufferRepo = new BufferRepository(db);
const loader = new ScenarioLoader(coreRepo, bufferRepo);
// 1. Read Scenario JSON file
if (!fs.existsSync(scenarioPath)) {
console.error(
`Error: Scenario template file not found at: ${scenarioPath}`,
);
process.exit(1);
}
console.log(`Loading scenario template from: ${scenarioPath}`);
const scenarioJson = JSON.parse(fs.readFileSync(scenarioPath, "utf-8"));
// 2. Initialize World Instance
const worldInstanceId = `run-${Date.now()}`;
console.log(`Initializing live world instance: ${worldInstanceId}`);
await loader.initializeWorld(scenarioJson, worldInstanceId);
// Load the running world state
const worldState = coreRepo.loadWorldState(worldInstanceId);
if (!worldState) {
console.error(
`Error: Failed to load initialized world state: ${worldInstanceId}`,
);
process.exit(1);
}
// 3. Ensure API Key exists if we are running LLMs
const apiKey = process.env.GOOGLE_API_KEY;
if (!apiKey) {
console.error("Error: GOOGLE_API_KEY environment variable is missing.");
console.error(
"Please provide it in your .env file to enable LLM generators and decoders.",
);
process.exit(1);
}
const llmProvider = new GeminiProvider(apiKey);
const architect = new Architect(llmProvider, coreRepo);
console.log(
"\n================================================================================",
);
console.log(`SIMULATION STARTED: "${scenarioJson.name}"`);
console.log(`Description: ${scenarioJson.description}`);
if (playEntityId) {
console.log(`Player Role: Controlling entity "${playEntityId}"`);
} else {
console.log("Player Role: Observing fully autonomous NPC run");
}
console.log(
"================================================================================",
);
const isVerbose = args.includes("--verbose");
let turnCount = 1;
const maxTurns = 20; // safe loop breaker
while (turnCount <= maxTurns) {
console.log(
`\n\n--- TURN ${turnCount} (World Time: ${worldState.clock.get().toISOString()}) ---`,
);
// Reload world state from database to ensure fresh DB sync
const currentWorldState = coreRepo.loadWorldState(worldInstanceId);
if (!currentWorldState) {
console.error("Error: Synced world state lost.");
process.exit(1);
}
const entities = Array.from(currentWorldState.entities.values());
for (const entity of entities) {
// 1. Determine the ActorAgent generator: CLI input for player, LLM for NPCs
const isPlayer = playEntityId && entity.id === playEntityId;
const generator = isPlayer ? new CLIProseGenerator() : undefined;
const agent = new ActorAgent(llmProvider, bufferRepo, 20, generator);
// 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}":`);
console.log("\n--- SYSTEM PROMPT ---");
console.log(systemPrompt);
console.log("\n--- USER CONTEXT ---");
console.log(userContext);
console.log("--------------------------------------------------------------------------------");
}
if (!isPlayer) {
console.log(`\n[${entity.id}] is thinking...`);
}
// 2. Execute character turn
const turnResult = await agent.act(currentWorldState, entity);
if (!isPlayer) {
console.log(`\n[${entity.id}]: ${turnResult.narrativeProse}`);
} else {
console.log(`\n[You]: ${turnResult.narrativeProse}`);
}
// Verbose mode: Output decoded intent structures
if (isVerbose) {
console.log(`\n🔍 [VERBOSE] Decoded Intents from Prose:`);
console.log(JSON.stringify(turnResult.intents.intents, null, 2));
console.log("--------------------------------------------------------------------------------");
}
// 3. Process each generated intent sequence through physics and memory
for (const intent of turnResult.intents.intents) {
const outcome = await architect.processIntent(
currentWorldState,
intent,
);
const timestamp = currentWorldState.clock.get().toISOString();
// Verbose mode: Output architect evaluation
if (isVerbose) {
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}"`);
if (outcome.timeDelta) {
console.log(` Clock Delta: +${outcome.timeDelta.minutesToAdvance} min (${outcome.timeDelta.explanation})`);
}
}
console.log("--------------------------------------------------------------------------------");
}
// Save actor's subjective memory
const actorEntry = buildBufferEntryForIntent(
intent,
timestamp,
entity.locationId,
);
if (intent.type === "action") {
actorEntry.outcome = {
isValid: outcome.isValid,
reason: outcome.reason,
};
}
bufferRepo.save(actorEntry);
// Propagate public memories (dialogue/actions) to co-located observers
if (
entity.locationId &&
(intent.type === "dialogue" || intent.type === "action")
) {
for (const other of currentWorldState.entities.values()) {
if (
other.id !== entity.id &&
other.locationId === entity.locationId
) {
const observerEntry = buildBufferEntryForIntent(
intent,
timestamp,
entity.locationId,
);
if (intent.type === "action") {
observerEntry.outcome = {
isValid: outcome.isValid,
reason: outcome.reason,
};
}
bufferRepo.save({
...observerEntry,
ownerId: other.id,
});
}
}
}
// Print formatted logs
if (intent.type === "monologue") {
if (isPlayer) {
console.log(` (Thought processed: "${intent.description}")`);
}
} else if (intent.type === "dialogue") {
console.log(
` (Dialogue spoken: spoken to ${intent.targetIds.join(", ") || "someone"})`,
);
} else {
console.log(
` (Action result: ${outcome.isValid ? "Success" : `Failed - ${outcome.reason}`})`,
);
}
}
// 4. Save synced world state to repository
coreRepo.saveWorldState(currentWorldState);
}
turnCount++;
}
console.log("\nSimulation execution limit reached. Goodbye!");
db.close();
}
main().catch((err) => {
console.error("Simulation run aborted due to error:", err);
process.exit(1);
});

View File

@@ -5,5 +5,14 @@
"outDir": "dist"
},
"include": ["src"],
"references": []
"references": [
{ "path": "../packages/core" },
{ "path": "../packages/spatial" },
{ "path": "../packages/memory" },
{ "path": "../packages/intent" },
{ "path": "../packages/architect" },
{ "path": "../packages/actor" },
{ "path": "../packages/llm" },
{ "path": "../content/scenario-core" }
]
}

View File

@@ -45,13 +45,14 @@
],
"entities": [
{
"id": "subject-alpha",
"id": "7c9b83b3-8cfb-4e89-8d77-626a5757d591",
"locationId": "white-room",
"attributes": [
{
"name": "name",
"value": "Subject Alpha",
"visibility": "PUBLIC"
"value": "Bob",
"visibility": "PRIVATE",
"allowedEntities": ["7c9b83b3-8cfb-4e89-8d77-626a5757d591"]
},
{
"name": "appearance",
@@ -67,7 +68,7 @@
"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": ["subject-alpha"]
"allowedEntities": ["7c9b83b3-8cfb-4e89-8d77-626a5757d591"]
},
{
"name": "neural_erasure_dose",
@@ -77,7 +78,7 @@
}
],
"aliases": {
"subject-beta": "the person in the Beta jumpsuit"
"bf3f29d2-cf11-4b11-9a99-b13c126d400e": "the person in the Beta jumpsuit"
},
"initialMemories": [
{
@@ -88,20 +89,21 @@
"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",
"actorId": "subject-alpha",
"actorId": "7c9b83b3-8cfb-4e89-8d77-626a5757d591",
"targetIds": []
}
}
]
},
{
"id": "subject-beta",
"id": "bf3f29d2-cf11-4b11-9a99-b13c126d400e",
"locationId": "white-room",
"attributes": [
{
"name": "name",
"value": "Subject Beta",
"visibility": "PUBLIC"
"value": "Bill",
"visibility": "PRIVATE",
"allowedEntities": ["bf3f29d2-cf11-4b11-9a99-b13c126d400e"]
},
{
"name": "appearance",
@@ -117,7 +119,7 @@
"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": ["subject-beta"]
"allowedEntities": ["bf3f29d2-cf11-4b11-9a99-b13c126d400e"]
},
{
"name": "neural_erasure_dose",
@@ -127,7 +129,7 @@
}
],
"aliases": {
"subject-alpha": "the person in the Alpha jumpsuit"
"7c9b83b3-8cfb-4e89-8d77-626a5757d591": "the person in the Alpha jumpsuit"
},
"initialMemories": [
{
@@ -138,7 +140,7 @@
"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",
"actorId": "subject-beta",
"actorId": "bf3f29d2-cf11-4b11-9a99-b13c126d400e",
"targetIds": []
}
}

View File

@@ -24,9 +24,9 @@ export class ScenarioLoader {
// 2. Instantiate running WorldState using the target instance ID
const world = new WorldState(targetWorldId, new Date(scenario.startTime));
// Seed world-level attributes
world.addAttribute("name", scenario.name, AttributeVisibility.PUBLIC);
world.addAttribute("description", scenario.description, AttributeVisibility.PUBLIC);
// Seed world-level attributes as system-only (private, empty ACL)
world.addAttribute("name", scenario.name, AttributeVisibility.PRIVATE, new Set());
world.addAttribute("description", scenario.description, AttributeVisibility.PRIVATE, new Set());
if (scenario.world?.attributes) {
for (const attr of scenario.world.attributes) {

View File

@@ -36,6 +36,9 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
const world = coreRepo.loadWorldState(worldInstanceId);
expect(world).not.toBeNull();
expect(world!.attributes.get("name")?.getValue()).toBe("Talking Room");
expect(world!.attributes.get("name")?.visibility).toBe("PRIVATE");
expect(world!.attributes.get("description")?.getValue()).toBe(scenarioJson.description);
expect(world!.attributes.get("description")?.visibility).toBe("PRIVATE");
expect(world!.attributes.get("experiment_codename")?.getValue()).toBe("Project Tabula Rasa (Phase 3)");
expect(world!.attributes.get("experiment_codename")?.visibility).toBe("PRIVATE");
expect(world!.attributes.get("experiment_codename")?.getAllowedEntities()).toHaveLength(0); // System only!
@@ -47,38 +50,47 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => {
expect(locations[0].attributes.get("description")?.getValue()).toContain("A pristine, featureless room");
// 6. Assert entities and their private attributes / allowedEntities
const alpha = world!.getEntity("subject-alpha");
const alphaId = "7c9b83b3-8cfb-4e89-8d77-626a5757d591";
const betaId = "bf3f29d2-cf11-4b11-9a99-b13c126d400e";
const alpha = world!.getEntity(alphaId);
expect(alpha).toBeDefined();
expect(alpha!.locationId).toBe("white-room");
expect(alpha!.attributes.get("name")?.getValue()).toBe("Subject Alpha");
// Name visibility check
const alphaName = alpha!.attributes.get("name")!;
expect(alphaName.getValue()).toBe("Bob");
expect(alphaName.visibility).toBe("PRIVATE");
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("subject-alpha")).toBe(true);
expect(alphaKnowledge.hasAccess("subject-beta")).toBe(false);
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("subject-alpha")).toBe(false);
expect(alphaDose.hasAccess("subject-beta")).toBe(false);
expect(alphaDose.hasAccess(alphaId)).toBe(false);
expect(alphaDose.hasAccess(betaId)).toBe(false);
// Check subjective aliases
expect(alpha!.aliases.get("subject-beta")).toBe("the person in the Beta jumpsuit");
expect(alpha!.aliases.get(betaId)).toBe("the person in the Beta jumpsuit");
const beta = world!.getEntity("subject-beta");
const beta = world!.getEntity(betaId);
expect(beta).toBeDefined();
expect(beta!.locationId).toBe("white-room");
expect(beta!.aliases.get("subject-alpha")).toBe("the person in the Alpha jumpsuit");
expect(beta!.aliases.get(alphaId)).toBe("the person in the Alpha jumpsuit");
// 7. Assert initial pre-seeded memories
const alphaMemories = bufferRepo.listForOwner("subject-alpha");
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");
const betaMemories = bufferRepo.listForOwner("subject-beta");
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");

View File

@@ -56,8 +56,10 @@ Monologue (`"monologue"`) is the third intent type alongside `dialogue` and `act
├─ 1. ActorPromptBuilder.build(entity, worldState)
│ → system prompt + user context (subjective world + memory + time)
├─ 2. ILLMProvider.generateStructuredResponse({ schema: ActorResponseSchema })
→ { narrativeProse: string }
├─ 2. IActorProseGenerator.generate(entityId, systemPrompt, userContext)
├─ LLMActorProseGenerator: queries LLM via generateStructuredResponse
│ └─ CLIProseGenerator: prompts human player via CLI / readline interface
│ → narrativeProse: string
├─ 3. IntentDecoder.decode(worldState, actorId, prose)
│ → IntentSequence (dialogue | action | monologue intents)

View File

@@ -16,7 +16,8 @@
"watch": "tsc -b --watch",
"test": "vitest run --project unit",
"test:watch": "vitest --project unit",
"test:evals": "vitest run --project evals"
"test:evals": "vitest run --project evals",
"play": "node cli/dist/index.js"
},
"keywords": [],
"author": "",

View File

@@ -67,6 +67,7 @@ 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:
- 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.

View File

@@ -9,7 +9,38 @@ import {
IntentDecoder,
IntentSequence,
} from "@omnia/intent";
import { ActorPromptBuilder, ActorResponse, ActorResponseSchema } from "./actor-prompt-builder.js";
import { ActorPromptBuilder, ActorResponseSchema } from "./actor-prompt-builder.js";
/**
* Interface to generate narrative prose for an actor.
* Allows switching between LLM generators and human CLI inputs.
*/
export interface IActorProseGenerator {
generate(entityId: string, systemPrompt: string, userContext: string): Promise<string>;
}
/**
* Default implementation of IActorProseGenerator using an LLM.
*/
export class LLMActorProseGenerator implements IActorProseGenerator {
constructor(private llmProvider: ILLMProvider) {}
async generate(entityId: string, systemPrompt: string, userContext: string): Promise<string> {
const response = await this.llmProvider.generateStructuredResponse({
systemPrompt,
userContext,
schema: ActorResponseSchema,
});
if (!response.success || !response.data) {
throw new Error(
`Actor generation failed for entity "${entityId}": ${response.error || "Unknown LLM error"}`,
);
}
return response.data.narrativeProse;
}
}
/**
* Result of a single actor turn.
@@ -35,21 +66,24 @@ export interface ActorTurnResult {
export class ActorAgent {
private promptBuilder: ActorPromptBuilder;
private decoder: IntentDecoder;
private generator: IActorProseGenerator;
constructor(
private llmProvider: ILLMProvider,
bufferRepo?: BufferRepository,
memoryLimit?: number,
generator?: IActorProseGenerator,
) {
this.promptBuilder = new ActorPromptBuilder(bufferRepo, memoryLimit);
this.decoder = new IntentDecoder(llmProvider);
this.generator = generator ?? new LLMActorProseGenerator(llmProvider);
}
/**
* Has the entity produce its next beat of behavior.
*
* 1. Builds an epistemically-bounded prompt for the entity.
* 2. Asks the LLM for narrative prose.
* 2. Asks the generator (LLM or human) for narrative prose.
* 3. Decodes the prose into a structured IntentSequence.
*/
async act(
@@ -61,27 +95,20 @@ export class ActorAgent {
entity,
);
const response = await this.llmProvider.generateStructuredResponse({
const narrativeProse = await this.generator.generate(
entity.id,
systemPrompt,
userContext,
schema: ActorResponseSchema,
});
);
if (!response.success || !response.data) {
throw new Error(
`Actor generation failed for entity "${entity.id}": ${response.error || "Unknown LLM error"}`,
);
}
const prose: ActorResponse = response.data;
const intents = await this.decoder.decode(
worldState,
entity.id,
prose.narrativeProse,
narrativeProse,
);
return {
narrativeProse: prose.narrativeProse,
narrativeProse,
intents,
};
}

27
pnpm-lock.yaml generated
View File

@@ -253,6 +253,33 @@ importers:
cli:
dependencies:
'@omnia/actor':
specifier: workspace:*
version: link:../packages/actor
'@omnia/architect':
specifier: workspace:*
version: link:../packages/architect
'@omnia/core':
specifier: workspace:*
version: link:../packages/core
'@omnia/intent':
specifier: workspace:*
version: link:../packages/intent
'@omnia/llm':
specifier: workspace:*
version: link:../packages/llm
'@omnia/memory':
specifier: workspace:*
version: link:../packages/memory
'@omnia/scenario-core':
specifier: workspace:*
version: link:../content/scenario-core
'@omnia/spatial':
specifier: workspace:*
version: link:../packages/spatial
better-sqlite3:
specifier: ^12.11.1
version: 12.11.1
dotenv:
specifier: ^17.4.2
version: 17.4.2