feat(core): Added isAgent field for entities

This commit is contained in:
2026-07-15 19:19:29 +05:30
parent 3b2a85aeaf
commit 9838d4ce59
9 changed files with 408 additions and 129 deletions

View File

@@ -46,6 +46,7 @@ export interface EntityInfo {
id: string;
name: string;
isPlayer: boolean;
isAgent: boolean;
}
export interface WaitingContext {

View File

@@ -17,7 +17,12 @@ for (const c of envCandidates) {
}
}
import { BufferRepository, LedgerRepository, HandoffEngine, checkHandoffTrigger } from "@omnia/memory";
import {
BufferRepository,
LedgerRepository,
HandoffEngine,
checkHandoffTrigger,
} from "@omnia/memory";
import { Architect, AliasDeltaGenerator } from "@omnia/architect";
import {
ActorAgent,
@@ -25,7 +30,17 @@ import {
IActorProseGenerator,
buildBufferEntryForIntent,
} from "@omnia/actor";
import { GeminiProvider, ILLMProvider, MockLLMProvider, ProviderManager, OpenRouterProvider, IEmbeddingProvider, GeminiEmbeddingProvider, MockEmbeddingProvider, ModelProviderInstance } from "@omnia/llm";
import {
GeminiProvider,
ILLMProvider,
MockLLMProvider,
ProviderManager,
OpenRouterProvider,
IEmbeddingProvider,
GeminiEmbeddingProvider,
MockEmbeddingProvider,
ModelProviderInstance,
} from "@omnia/llm";
import { ScenarioLoader } from "@omnia/scenario";
import type {
@@ -69,15 +84,22 @@ interface SavedState {
providerMappings: Record<string, string>;
}
function loadSessionState(db: Database.Database, id: string): SavedState | null {
function loadSessionState(
db: Database.Database,
id: string,
): SavedState | null {
try {
db.prepare(`
db.prepare(
`
CREATE TABLE IF NOT EXISTS gui_meta (
id TEXT PRIMARY KEY,
state_json TEXT
)
`).run();
const row = db.prepare(`SELECT state_json FROM gui_meta WHERE id = ?`).get(id) as { state_json: string } | undefined;
`,
).run();
const row = db
.prepare(`SELECT state_json FROM gui_meta WHERE id = ?`)
.get(id) as { state_json: string } | undefined;
return row ? (JSON.parse(row.state_json) as SavedState) : null;
} catch {
return null;
@@ -129,7 +151,13 @@ class SimulationManager {
if (!activeInstance) {
const envKey = process.env.GOOGLE_API_KEY;
if (envKey) {
activeInstance = ProviderManager.create("Default (Env)", "google-genai", envKey, undefined, "generative");
activeInstance = ProviderManager.create(
"Default (Env)",
"google-genai",
envKey,
undefined,
"generative",
);
}
}
if (!activeInstance) {
@@ -143,7 +171,8 @@ class SimulationManager {
entities: [],
log: [],
entityIndex: 0,
error: "No active LLM Provider Instance found. Please configure a key in Settings first.",
error:
"No active LLM Provider Instance found. Please configure a key in Settings first.",
};
}
@@ -184,6 +213,7 @@ class SimulationManager {
id: e.id,
name: (e.attributes.get("name")?.getValue() as string) || e.id,
isPlayer: false,
isAgent: e.isAgent,
}));
let playerEntityId: string | undefined;
@@ -192,8 +222,7 @@ class SimulationManager {
if (!matched) {
for (const ent of rawEntities) {
const nameAttr = ent.attributes.get("name")?.getValue() as
| string
| undefined;
string | undefined;
if (nameAttr?.toLowerCase() === playEntityName.toLowerCase()) {
matched = ent;
break;
@@ -203,8 +232,7 @@ class SimulationManager {
if (!matched) {
for (const ent of rawEntities) {
const nameAttr = ent.attributes.get("name")?.getValue() as
| string
| undefined;
string | undefined;
if (
nameAttr?.toLowerCase().includes(playEntityName.toLowerCase()) ||
ent.id.toLowerCase().includes(playEntityName.toLowerCase())
@@ -232,7 +260,7 @@ class SimulationManager {
inst = active;
}
const key = inst ? inst.apiKey : (process.env.GOOGLE_API_KEY || "");
const key = inst ? inst.apiKey : process.env.GOOGLE_API_KEY || "";
const providerName = inst ? inst.providerName : "google-genai";
const modelName = inst ? inst.modelName : undefined;
const instanceName = inst ? inst.name : undefined;
@@ -254,7 +282,7 @@ class SimulationManager {
inst = ProviderManager.getActive("embedding");
}
const key = inst ? inst.apiKey : (process.env.GOOGLE_API_KEY || "");
const key = inst ? inst.apiKey : process.env.GOOGLE_API_KEY || "";
const providerName = inst ? inst.providerName : "google-genai";
const modelName = inst ? inst.modelName : undefined;
@@ -340,6 +368,12 @@ class SimulationManager {
const info = session.entities[session.entityIndex];
if (!info.isAgent) {
session.entityIndex++;
this.save(session);
return this.snapshot(session);
}
if (info.isPlayer) {
await this.preparePlayerTurn(session, info);
this.save(session);
@@ -402,8 +436,14 @@ class SimulationManager {
},
};
if (session.decoderProvider.lastCalls && session.decoderProvider.lastCalls.length > 0) {
const call = session.decoderProvider.lastCalls[session.decoderProvider.lastCalls.length - 1];
if (
session.decoderProvider.lastCalls &&
session.decoderProvider.lastCalls.length > 0
) {
const call =
session.decoderProvider.lastCalls[
session.decoderProvider.lastCalls.length - 1
];
entry.decoderPrompt = {
systemPrompt: call.systemPrompt,
userContext: call.userContext,
@@ -487,15 +527,17 @@ class SimulationManager {
session: SimSession,
info: EntityInfo,
): Promise<void> {
const worldState = session.coreRepo.loadWorldState(
session.worldInstanceId,
);
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
if (!worldState) throw new Error("World state lost");
const entity = worldState.getEntity(info.id);
if (!entity) throw new Error(`Entity "${info.id}" not found`);
const promptBuilder = new ActorPromptBuilder(session.bufferRepo, session.ledgerRepo, 20);
const promptBuilder = new ActorPromptBuilder(
session.bufferRepo,
session.ledgerRepo,
20,
);
const { systemPrompt, userContext } = promptBuilder.build(
worldState,
entity,
@@ -514,9 +556,7 @@ class SimulationManager {
session: SimSession,
info: EntityInfo,
): Promise<void> {
const worldState = session.coreRepo.loadWorldState(
session.worldInstanceId,
);
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
if (!worldState) throw new Error("World state lost");
const entity = worldState.getEntity(info.id);
@@ -539,8 +579,14 @@ class SimulationManager {
timestamp: worldState.clock.get().toISOString(),
};
if (session.actorProvider.lastCalls && session.actorProvider.lastCalls.length > 0) {
const actorCall = session.actorProvider.lastCalls[session.actorProvider.lastCalls.length - 1];
if (
session.actorProvider.lastCalls &&
session.actorProvider.lastCalls.length > 0
) {
const actorCall =
session.actorProvider.lastCalls[
session.actorProvider.lastCalls.length - 1
];
entry.rawPrompt = {
systemPrompt: actorCall.systemPrompt,
userContext: actorCall.userContext,
@@ -548,8 +594,14 @@ class SimulationManager {
entry.usage = actorCall.usage;
}
if (session.decoderProvider.lastCalls && session.decoderProvider.lastCalls.length > 0) {
const decoderCall = session.decoderProvider.lastCalls[session.decoderProvider.lastCalls.length - 1];
if (
session.decoderProvider.lastCalls &&
session.decoderProvider.lastCalls.length > 0
) {
const decoderCall =
session.decoderProvider.lastCalls[
session.decoderProvider.lastCalls.length - 1
];
entry.decoderPrompt = {
systemPrompt: decoderCall.systemPrompt,
userContext: decoderCall.userContext,
@@ -558,10 +610,7 @@ class SimulationManager {
}
for (const intent of result.intents.intents) {
const outcome = await session.architect.processIntent(
worldState,
intent,
);
const outcome = await session.architect.processIntent(worldState, intent);
const ts = worldState.clock.get().toISOString();
entry.intents.push({
@@ -593,10 +642,7 @@ class SimulationManager {
(intent.type === "dialogue" || intent.type === "action")
) {
for (const [, other] of worldState.entities) {
if (
other.id !== info.id &&
other.locationId === entity.locationId
) {
if (other.id !== info.id && other.locationId === entity.locationId) {
const observerEntry = buildBufferEntryForIntent(
intent,
ts,
@@ -619,9 +665,7 @@ class SimulationManager {
}
private async runHandoffResolution(session: SimSession): Promise<void> {
const worldState = session.coreRepo.loadWorldState(
session.worldInstanceId,
);
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
if (!worldState) throw new Error("World state lost");
const handoffEngine = new HandoffEngine(
@@ -633,24 +677,36 @@ class SimulationManager {
const entities = Array.from(worldState.entities.values());
for (const entity of entities) {
if (!entity.isAgent) continue;
const bufferEntries = session.bufferRepo.listForOwner(entity.id);
const maxContext = session.handoffProvider.maxContext !== undefined ? session.handoffProvider.maxContext : 32768;
const maxContext =
session.handoffProvider.maxContext !== undefined
? session.handoffProvider.maxContext
: 32768;
const trigger = checkHandoffTrigger(entity, bufferEntries, worldState.clock.get(), maxContext);
const trigger = checkHandoffTrigger(
entity,
bufferEntries,
worldState.clock.get(),
maxContext,
);
if (trigger !== "none") {
await handoffEngine.runHandoff(entity, bufferEntries, worldState.clock.get());
await handoffEngine.runHandoff(
entity,
bufferEntries,
worldState.clock.get(),
);
}
}
}
private async runAliasResolution(session: SimSession): Promise<void> {
const worldState = session.coreRepo.loadWorldState(
session.worldInstanceId,
);
const worldState = session.coreRepo.loadWorldState(session.worldInstanceId);
if (!worldState) throw new Error("World state lost");
const entities = Array.from(worldState.entities.values());
for (const viewer of entities) {
if (!viewer.isAgent) continue;
if (!viewer.locationId) continue;
for (const target of entities) {
if (viewer.id === target.id) continue;
@@ -723,18 +779,36 @@ class SimulationManager {
if (!inst) {
const envKey = process.env.GOOGLE_API_KEY;
if (envKey) {
inst = ProviderManager.create("Default (Env)", "google-genai", envKey, undefined, "generative");
inst = ProviderManager.create(
"Default (Env)",
"google-genai",
envKey,
undefined,
"generative",
);
}
}
if (!inst) {
throw new Error(`No active LLM Provider Instance found for task "${task}". Please configure a key in Settings first.`);
throw new Error(
`No active LLM Provider Instance found for task "${task}". Please configure a key in Settings first.`,
);
}
if (inst.providerName === "google-genai") {
return new GeminiProvider(inst.apiKey, inst.modelName, inst.name, inst.maxContext);
return new GeminiProvider(
inst.apiKey,
inst.modelName,
inst.name,
inst.maxContext,
);
} else if (inst.providerName === "openrouter") {
return new OpenRouterProvider(inst.apiKey, inst.modelName, inst.name, inst.maxContext);
return new OpenRouterProvider(
inst.apiKey,
inst.modelName,
inst.name,
inst.maxContext,
);
} else {
return new MockLLMProvider([]);
}
@@ -749,12 +823,20 @@ class SimulationManager {
if (!inst) {
const envKey = process.env.GOOGLE_API_KEY;
if (envKey) {
inst = ProviderManager.create("Default Embed (Env)", "google-genai", envKey, "gemini-embedding-001", "embedding");
inst = ProviderManager.create(
"Default Embed (Env)",
"google-genai",
envKey,
"gemini-embedding-001",
"embedding",
);
}
}
if (!inst) {
throw new Error(`No active Embedding Provider Instance found for task "embeddings". Please configure an embedding key in Settings first.`);
throw new Error(
`No active Embedding Provider Instance found for task "embeddings". Please configure an embedding key in Settings first.`,
);
}
if (inst.providerName === "google-genai") {
@@ -824,7 +906,9 @@ class SimulationManager {
if (!fs.existsSync(dbDir)) return [];
const snapshots: SimSnapshot[] = [];
const files = fs.readdirSync(dbDir).filter(f => f.startsWith("sim-") && f.endsWith(".db"));
const files = fs
.readdirSync(dbDir)
.filter((f) => f.startsWith("sim-") && f.endsWith(".db"));
for (const file of files) {
const id = file.replace(".db", "");
@@ -872,15 +956,19 @@ class SimulationManager {
const dbDir = path.resolve(process.cwd(), "data");
if (!fs.existsSync(dbDir)) return;
const files = fs.readdirSync(dbDir).filter(f => f.startsWith("sim-") && f.endsWith(".db"));
const files = fs
.readdirSync(dbDir)
.filter((f) => f.startsWith("sim-") && f.endsWith(".db"));
const list = ProviderManager.list();
let inst = newProviderInstanceId ? list.find((p) => p.id === newProviderInstanceId) : null;
let inst = newProviderInstanceId
? list.find((p) => p.id === newProviderInstanceId)
: null;
if (!inst || inst.type !== "embedding") {
inst = ProviderManager.getActive("embedding");
}
const key = inst ? inst.apiKey : (process.env.GOOGLE_API_KEY || "");
const key = inst ? inst.apiKey : process.env.GOOGLE_API_KEY || "";
const providerName = inst ? inst.providerName : "google-genai";
const modelName = inst ? inst.modelName : undefined;
@@ -898,12 +986,16 @@ class SimulationManager {
const db = activeSession ? activeSession.db : new Database(dbPath);
try {
const rows = db.prepare(`SELECT id, content FROM ledger_entries`).all() as { id: string; content: string }[];
const rows = db
.prepare(`SELECT id, content FROM ledger_entries`)
.all() as { id: string; content: string }[];
for (const row of rows) {
const vector = await embeddingProvider.embed(row.content);
const buffer = Buffer.from(new Float32Array(vector).buffer);
db.prepare(`UPDATE ledger_entries SET embedding = ? WHERE id = ?`).run(buffer, row.id);
db.prepare(
`UPDATE ledger_entries SET embedding = ? WHERE id = ?`,
).run(buffer, row.id);
}
} catch (err) {
console.error(`Failed to regenerate embeddings for ${file}:`, err);
@@ -932,18 +1024,26 @@ class SimulationManager {
providerMappings: session.providerMappings,
};
session.db.prepare(`
session.db
.prepare(
`
CREATE TABLE IF NOT EXISTS gui_meta (
id TEXT PRIMARY KEY,
state_json TEXT
)
`).run();
`,
)
.run();
session.db.prepare(`
session.db
.prepare(
`
INSERT INTO gui_meta (id, state_json)
VALUES (?, ?)
ON CONFLICT(id) DO UPDATE SET state_json = excluded.state_json
`).run(session.worldInstanceId, JSON.stringify(state));
`,
)
.run(session.worldInstanceId, JSON.stringify(state));
}
getSnapshot(id: string): SimSnapshot | null {

View File

@@ -1,23 +1,22 @@
import { Entity, WorldState } from "@omnia/core";
import { ILLMProvider } from "@omnia/llm";
import { BufferEntry, BufferRepository, LedgerRepository } from "@omnia/memory";
import { Intent, IntentDecoder, IntentSequence } from "@omnia/intent";
import {
BufferEntry,
BufferRepository,
LedgerRepository,
} from "@omnia/memory";
import {
Intent,
IntentDecoder,
IntentSequence,
} from "@omnia/intent";
import { ActorPromptBuilder, ActorResponseSchema } from "./actor-prompt-builder.js";
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>;
generate(
entityId: string,
systemPrompt: string,
userContext: string,
): Promise<string>;
}
/**
@@ -26,7 +25,11 @@ export interface IActorProseGenerator {
export class LLMActorProseGenerator implements IActorProseGenerator {
constructor(private llmProvider: ILLMProvider) {}
async generate(entityId: string, systemPrompt: string, userContext: string): Promise<string> {
async generate(
entityId: string,
systemPrompt: string,
userContext: string,
): Promise<string> {
const response = await this.llmProvider.generateStructuredResponse({
systemPrompt,
userContext,
@@ -89,7 +92,11 @@ export class ActorAgent {
decoderProv = llmProvider;
}
this.promptBuilder = new ActorPromptBuilder(bufferRepo, ledgerRepo, memoryLimit);
this.promptBuilder = new ActorPromptBuilder(
bufferRepo,
ledgerRepo,
memoryLimit,
);
this.decoder = new IntentDecoder(decoderProv);
this.generator = generator ?? new LLMActorProseGenerator(actorProv);
this.llmProvider = actorProv;
@@ -102,10 +109,13 @@ export class ActorAgent {
* 2. Asks the generator (LLM or human) for narrative prose.
* 3. Decodes the prose into a structured IntentSequence.
*/
async act(
worldState: WorldState,
entity: Entity,
): Promise<ActorTurnResult> {
async act(worldState: WorldState, entity: Entity): Promise<ActorTurnResult> {
if (!entity.isAgent) {
throw new Error(
`Entity "${entity.id}" is not an agent and cannot use the actor interface.`,
);
}
const { systemPrompt, userContext } = this.promptBuilder.build(
worldState,
entity,

View File

@@ -0,0 +1,19 @@
import { describe, it, expect } from "vitest";
import { WorldState, Entity } from "@omnia/core";
import { ActorAgent } from "../src/actor.js";
import { MockLLMProvider } from "@omnia/llm";
describe("ActorAgent Unit Tests", () => {
it("should throw an error if trying to act with a non-agent entity", async () => {
const world = new WorldState("world-123");
const nonAgentEntity = new Entity("stone", null, false); // isAgent = false
world.addEntity(nonAgentEntity);
const mockLlm = new MockLLMProvider([]);
const actor = new ActorAgent(mockLlm);
await expect(actor.act(world, nonAgentEntity)).rejects.toThrow(
'Entity "stone" is not an agent and cannot use the actor interface.',
);
});
});

View File

@@ -3,9 +3,11 @@ import { AttributableObject } from "./attribute.js";
export class Entity extends AttributableObject {
locationId: string | null = null;
readonly aliases: Map<string, string> = new Map();
isAgent: boolean = true;
constructor(id?: string, locationId?: string | null) {
constructor(id?: string, locationId?: string | null, isAgent?: boolean) {
super(id);
this.locationId = locationId ?? null;
this.isAgent = isAgent ?? true;
}
}

View File

@@ -73,6 +73,13 @@ export class SQLiteRepository {
} catch {
// Column already exists, ignore error
}
// Safely add is_agent column if it does not exist in an existing database
try {
this.db.exec("ALTER TABLE objects ADD COLUMN is_agent INTEGER;");
} catch {
// Column already exists, ignore error
}
}
save(obj: AttributableObject, type: string, worldId?: string): void {
@@ -85,15 +92,20 @@ export class SQLiteRepository {
let locationId: string | null = null;
let aliasesJson: string | null = null;
let connectionsJson: string | null = null;
let isAgent: number | null = null;
if (obj instanceof Entity) {
locationId = obj.locationId;
aliasesJson = JSON.stringify(Array.from(obj.aliases.entries()));
isAgent = obj.isAgent ? 1 : 0;
}
// Check if it's a location (using duck typing to avoid circular import of Location)
if (type === "location") {
const loc = obj as { parentId?: string | null; connections?: unknown[] };
const loc = obj as {
parentId?: string | null;
connections?: unknown[];
};
locationId = loc.parentId ?? null;
if (loc.connections) {
connectionsJson = JSON.stringify(loc.connections);
@@ -104,18 +116,28 @@ export class SQLiteRepository {
this.db
.prepare(
`
INSERT INTO objects (id, type, world_id, clock_iso, location_id, aliases_json, connections_json)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO objects (id, type, world_id, clock_iso, location_id, aliases_json, connections_json, is_agent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
type = excluded.type,
world_id = excluded.world_id,
clock_iso = excluded.clock_iso,
location_id = excluded.location_id,
aliases_json = excluded.aliases_json,
connections_json = excluded.connections_json
connections_json = excluded.connections_json,
is_agent = excluded.is_agent
`,
)
.run(obj.id, type, worldId || null, clockIso, locationId, aliasesJson, connectionsJson);
.run(
obj.id,
type,
worldId || null,
clockIso,
locationId,
aliasesJson,
connectionsJson,
isAgent,
);
// Get current attributes from db to delete the ones that are no longer present
const existingAttrs = this.db
@@ -208,16 +230,27 @@ export class SQLiteRepository {
const objRow = this.db
.prepare(
`
SELECT type, location_id, aliases_json FROM objects WHERE id = ?
SELECT type, location_id, aliases_json, is_agent FROM objects WHERE id = ?
`,
)
.get(id) as { type: string; location_id: string | null; aliases_json: string | null } | undefined;
.get(id) as
| {
type: string;
location_id: string | null;
aliases_json: string | null;
is_agent: number | null;
}
| undefined;
if (!objRow || objRow.type !== "entity") {
return null;
}
const entity = new Entity(id, objRow.location_id);
const entity = new Entity(
id,
objRow.location_id,
objRow.is_agent !== null ? objRow.is_agent === 1 : true,
);
if (objRow.aliases_json) {
const entries = JSON.parse(objRow.aliases_json) as [string, string][];
for (const [k, v] of entries) {
@@ -238,7 +271,13 @@ export class SQLiteRepository {
SELECT type, location_id, connections_json FROM objects WHERE id = ?
`,
)
.get(id) as { type: string; location_id: string | null; connections_json: string | null } | undefined;
.get(id) as
| {
type: string;
location_id: string | null;
connections_json: string | null;
}
| undefined;
if (!objRow || objRow.type !== "location") {
return null;
@@ -246,7 +285,9 @@ export class SQLiteRepository {
const location = factory(id, objRow.location_id);
if (objRow.connections_json) {
(location as { connections?: unknown[] }).connections = JSON.parse(objRow.connections_json);
(location as { connections?: unknown[] }).connections = JSON.parse(
objRow.connections_json,
);
}
this.reconstituteAttributes(location);
return location;
@@ -262,13 +303,19 @@ export class SQLiteRepository {
SELECT id, location_id, connections_json FROM objects WHERE type = 'location' AND world_id = ?
`,
)
.all(worldId) as { id: string; location_id: string | null; connections_json: string | null }[];
.all(worldId) as {
id: string;
location_id: string | null;
connections_json: string | null;
}[];
const locations: T[] = [];
for (const row of rows) {
const loc = factory(row.id, row.location_id);
if (row.connections_json) {
(loc as { connections?: unknown[] }).connections = JSON.parse(row.connections_json);
(loc as { connections?: unknown[] }).connections = JSON.parse(
row.connections_json,
);
}
this.reconstituteAttributes(loc);
locations.push(loc);
@@ -300,13 +347,19 @@ export class SQLiteRepository {
SELECT id, location_id, connections_json FROM objects WHERE type = 'location' AND world_id = ?
`,
)
.all(id) as { id: string; location_id: string | null; connections_json: string | null }[];
.all(id) as {
id: string;
location_id: string | null;
connections_json: string | null;
}[];
for (const row of locationRows) {
const loc = new GenericObject(row.id);
(loc as { parentId?: string | null }).parentId = row.location_id;
if (row.connections_json) {
(loc as { connections?: unknown[] }).connections = JSON.parse(row.connections_json);
(loc as { connections?: unknown[] }).connections = JSON.parse(
row.connections_json,
);
}
this.reconstituteAttributes(loc);
worldState.addLocation(loc);
@@ -316,13 +369,22 @@ export class SQLiteRepository {
const entityRows = this.db
.prepare(
`
SELECT id, location_id, aliases_json FROM objects WHERE type = 'entity' AND world_id = ?
SELECT id, location_id, aliases_json, is_agent FROM objects WHERE type = 'entity' AND world_id = ?
`,
)
.all(id) as { id: string; location_id: string | null; aliases_json: string | null }[];
.all(id) as {
id: string;
location_id: string | null;
aliases_json: string | null;
is_agent: number | null;
}[];
for (const row of entityRows) {
const entity = new Entity(row.id, row.location_id);
const entity = new Entity(
row.id,
row.location_id,
row.is_agent !== null ? row.is_agent === 1 : true,
);
if (row.aliases_json) {
const entries = JSON.parse(row.aliases_json) as [string, string][];
for (const [k, v] of entries) {
@@ -340,14 +402,22 @@ export class SQLiteRepository {
const rows = this.db
.prepare(
`
SELECT id, aliases_json FROM objects WHERE type = 'entity'
SELECT id, aliases_json, is_agent FROM objects WHERE type = 'entity'
`,
)
.all() as { id: string; aliases_json: string | null }[];
.all() as {
id: string;
aliases_json: string | null;
is_agent: number | null;
}[];
const entities: Entity[] = [];
for (const row of rows) {
const entity = new Entity(row.id);
const entity = new Entity(
row.id,
null,
row.is_agent !== null ? row.is_agent === 1 : true,
);
if (row.aliases_json) {
const entries = JSON.parse(row.aliases_json) as [string, string][];
for (const [k, v] of entries) {

View File

@@ -61,16 +61,21 @@ describe("Attribute & AttributableObject Unit Tests (Tier 1)", () => {
test("AttributableObject visibility filtering", () => {
const actor = new MockAttributable("actor");
actor.addAttribute("eyes", "blue", AttributeVisibility.PUBLIC);
actor.addAttribute("secret", "42", AttributeVisibility.PRIVATE, new Set(["friend"]));
actor.addAttribute(
"secret",
"42",
AttributeVisibility.PRIVATE,
new Set(["friend"]),
);
// Public viewer should only see public attributes
const publicAttrs = actor.getVisibleAttributesFor("stranger");
expect(publicAttrs.map(a => a.name)).toEqual(["eyes"]);
expect(publicAttrs.map((a) => a.name)).toEqual(["eyes"]);
// Authorized viewer should see both
const privateAttrs = actor.getVisibleAttributesFor("friend");
expect(privateAttrs.map(a => a.name)).toContain("eyes");
expect(privateAttrs.map(a => a.name)).toContain("secret");
expect(privateAttrs.map((a) => a.name)).toContain("eyes");
expect(privateAttrs.map((a) => a.name)).toContain("secret");
});
});
@@ -131,7 +136,12 @@ describe("SQLiteRepository Unit Tests (Tier 1)", () => {
const alice = new Entity("alice", "location-a");
alice.addAttribute("name", "Alice Smith", AttributeVisibility.PUBLIC);
// Secret attribute visible only to 'bob'
alice.addAttribute("diaries", "Private thoughts", AttributeVisibility.PRIVATE, new Set(["bob"]));
alice.addAttribute(
"diaries",
"Private thoughts",
AttributeVisibility.PRIVATE,
new Set(["bob"]),
);
world.addEntity(alice);
const bob = new Entity("bob");
@@ -161,7 +171,9 @@ describe("SQLiteRepository Unit Tests (Tier 1)", () => {
expect(loadedAlice!.locationId).toBe("location-a");
expect(loadedBob!.locationId).toBeNull();
expect(loadedAlice!.attributes.get("name")?.getValue()).toBe("Alice Smith");
expect(loadedAlice!.attributes.get("name")?.visibility).toBe(AttributeVisibility.PUBLIC);
expect(loadedAlice!.attributes.get("name")?.visibility).toBe(
AttributeVisibility.PUBLIC,
);
const diaryAttr = loadedAlice!.attributes.get("diaries")!;
expect(diaryAttr.getValue()).toBe("Private thoughts");
@@ -191,17 +203,46 @@ describe("SQLiteRepository Unit Tests (Tier 1)", () => {
db.close();
});
test("Save and load entity isAgent property", () => {
const db = new Database(":memory:");
const repo = new SQLiteRepository(db);
const world = new WorldState("world-xyz");
const alice = new Entity("alice", null, false);
const bob = new Entity("bob", null, true);
world.addEntity(alice);
world.addEntity(bob);
repo.saveWorldState(world);
const loadedWorld = repo.loadWorldState("world-xyz")!;
const loadedAlice = loadedWorld.getEntity("alice")!;
const loadedBob = loadedWorld.getEntity("bob")!;
expect(loadedAlice.isAgent).toBe(false);
expect(loadedBob.isAgent).toBe(true);
db.close();
});
});
describe("Serializer & serializeObjectiveWorldState Unit Tests (Tier 1)", () => {
test("serializeAttributes utility", () => {
const obj = new MockAttributable("obj-1");
obj.addAttribute("health", "100", AttributeVisibility.PUBLIC);
obj.addAttribute("secret", "key-123", AttributeVisibility.PRIVATE, new Set(["alice"]));
obj.addAttribute(
"secret",
"key-123",
AttributeVisibility.PRIVATE,
new Set(["alice"]),
);
const result = serializeAttributes(Array.from(obj.attributes.values()));
expect(result).toContain("* health: 100 (Visibility: PUBLIC)");
expect(result).toContain("* secret: key-123 (Visibility: PRIVATE) (Visible to: alice)");
expect(result).toContain(
"* secret: key-123 (Visibility: PRIVATE) (Visible to: alice)",
);
});
test("serializeObjectiveWorldState utility", () => {

View File

@@ -1,4 +1,9 @@
import { WorldState, Entity, SQLiteRepository, AttributeVisibility } from "@omnia/core";
import {
WorldState,
Entity,
SQLiteRepository,
AttributeVisibility,
} from "@omnia/core";
import { Location } from "@omnia/spatial";
import { BufferRepository } from "@omnia/memory";
import { ScenarioSchema, Scenario } from "./schema.js";
@@ -17,24 +22,40 @@ export class ScenarioLoader {
* @param targetWorldId The unique ID for the running instance to create (e.g. UUID).
* Allows launching multiple active runs from one scenario.
*/
async initializeWorld(scenarioJson: unknown, targetWorldId: string): Promise<string> {
async initializeWorld(
scenarioJson: unknown,
targetWorldId: string,
): Promise<string> {
// 1. Validate scenario template schema
const scenario: Scenario = ScenarioSchema.parse(scenarioJson);
// 2. Instantiate running WorldState using the target instance ID
const world = new WorldState(targetWorldId, new Date(scenario.startTime));
// 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());
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) {
const vis = attr.visibility === "PUBLIC" ? AttributeVisibility.PUBLIC : AttributeVisibility.PRIVATE;
const vis =
attr.visibility === "PUBLIC"
? AttributeVisibility.PUBLIC
: AttributeVisibility.PRIVATE;
world.addAttribute(
attr.name,
attr.value,
vis,
attr.name,
attr.value,
vis,
attr.allowedEntities ? new Set(attr.allowedEntities) : null,
);
}
@@ -47,10 +68,13 @@ export class ScenarioLoader {
if (scenario.locations) {
for (const locData of scenario.locations) {
const location = new Location(locData.id, locData.parentId ?? null);
if (locData.attributes) {
for (const attr of locData.attributes) {
const vis = attr.visibility === "PUBLIC" ? AttributeVisibility.PUBLIC : AttributeVisibility.PRIVATE;
const vis =
attr.visibility === "PUBLIC"
? AttributeVisibility.PUBLIC
: AttributeVisibility.PRIVATE;
location.addAttribute(
attr.name,
attr.value,
@@ -80,12 +104,19 @@ export class ScenarioLoader {
// 5. Instantiate and Persist Entities (with Aliases & Memory Buffers)
if (scenario.entities) {
for (const entData of scenario.entities) {
const entity = new Entity(entData.id, entData.locationId ?? null);
const entity = new Entity(
entData.id,
entData.locationId ?? null,
entData.isAgent,
);
// Load attributes
if (entData.attributes) {
for (const attr of entData.attributes) {
const vis = attr.visibility === "PUBLIC" ? AttributeVisibility.PUBLIC : AttributeVisibility.PRIVATE;
const vis =
attr.visibility === "PUBLIC"
? AttributeVisibility.PUBLIC
: AttributeVisibility.PRIVATE;
entity.addAttribute(
attr.name,
attr.value,

View File

@@ -38,10 +38,12 @@ export const ScenarioMemoryEntrySchema = z.object({
targetIds: z.array(z.string()),
modifiers: z.array(z.string()).optional(),
}),
outcome: z.object({
isValid: z.boolean(),
reason: z.string(),
}).optional(),
outcome: z
.object({
isValid: z.boolean(),
reason: z.string(),
})
.optional(),
});
export const ScenarioEntitySchema = z.object({
@@ -50,6 +52,7 @@ export const ScenarioEntitySchema = z.object({
attributes: z.array(ScenarioAttributeSchema).optional(),
aliases: z.record(z.string(), z.string()).optional(), // targetId -> subjective descriptor
initialMemories: z.array(ScenarioMemoryEntrySchema).optional(),
isAgent: z.boolean().optional(),
});
export const ScenarioSchema = z.object({
@@ -57,9 +60,11 @@ export const ScenarioSchema = z.object({
name: z.string(),
description: z.string(),
startTime: z.string(), // ISO string
world: z.object({
attributes: z.array(ScenarioAttributeSchema).optional(),
}).optional(),
world: z
.object({
attributes: z.array(ScenarioAttributeSchema).optional(),
})
.optional(),
locations: z.array(ScenarioLocationSchema).optional(),
entities: z.array(ScenarioEntitySchema).optional(),
});