mirror of
https://github.com/sortedcord/omnia.git
synced 2026-07-22 03:52:48 +05:30
cli: FIRST RUN!!!! ITS ALIVE!
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
285
cli/src/index.ts
285
cli/src/index.ts
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
27
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user