From 9838d4ce595ce6330c534c0d0d1406af7f7d6143 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Wed, 15 Jul 2026 19:19:29 +0530 Subject: [PATCH 1/3] feat(core): Added isAgent field for entities --- apps/gui/src/lib/simulation-types.ts | 1 + apps/gui/src/lib/simulation.ts | 222 +++++++++++++++++++-------- packages/actor/src/actor.ts | 44 ++++-- packages/actor/tests/actor.test.ts | 19 +++ packages/core/src/entity.ts | 4 +- packages/core/src/repository.ts | 110 ++++++++++--- packages/core/tests/core.test.ts | 57 ++++++- packages/scenario/src/loader.ts | 61 ++++++-- packages/scenario/src/schema.ts | 19 ++- 9 files changed, 408 insertions(+), 129 deletions(-) create mode 100644 packages/actor/tests/actor.test.ts diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index f67e264..c4ee4bd 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -46,6 +46,7 @@ export interface EntityInfo { id: string; name: string; isPlayer: boolean; + isAgent: boolean; } export interface WaitingContext { diff --git a/apps/gui/src/lib/simulation.ts b/apps/gui/src/lib/simulation.ts index 38cf80a..6980e50 100644 --- a/apps/gui/src/lib/simulation.ts +++ b/apps/gui/src/lib/simulation.ts @@ -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; } -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 { - 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 { - 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 { - 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 { - 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 { diff --git a/packages/actor/src/actor.ts b/packages/actor/src/actor.ts index 6f1e3da..a7528b7 100644 --- a/packages/actor/src/actor.ts +++ b/packages/actor/src/actor.ts @@ -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; + generate( + entityId: string, + systemPrompt: string, + userContext: string, + ): Promise; } /** @@ -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 { + async generate( + entityId: string, + systemPrompt: string, + userContext: string, + ): Promise { 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 { + async act(worldState: WorldState, entity: Entity): Promise { + 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, diff --git a/packages/actor/tests/actor.test.ts b/packages/actor/tests/actor.test.ts new file mode 100644 index 0000000..87cc08b --- /dev/null +++ b/packages/actor/tests/actor.test.ts @@ -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.', + ); + }); +}); diff --git a/packages/core/src/entity.ts b/packages/core/src/entity.ts index 17f4193..037ce3c 100644 --- a/packages/core/src/entity.ts +++ b/packages/core/src/entity.ts @@ -3,9 +3,11 @@ import { AttributableObject } from "./attribute.js"; export class Entity extends AttributableObject { locationId: string | null = null; readonly aliases: Map = 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; } } diff --git a/packages/core/src/repository.ts b/packages/core/src/repository.ts index cfc1cc7..1dd49fd 100644 --- a/packages/core/src/repository.ts +++ b/packages/core/src/repository.ts @@ -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) { diff --git a/packages/core/tests/core.test.ts b/packages/core/tests/core.test.ts index 887580e..2401fe6 100644 --- a/packages/core/tests/core.test.ts +++ b/packages/core/tests/core.test.ts @@ -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", () => { diff --git a/packages/scenario/src/loader.ts b/packages/scenario/src/loader.ts index 13ad99c..ff31959 100644 --- a/packages/scenario/src/loader.ts +++ b/packages/scenario/src/loader.ts @@ -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 { + async initializeWorld( + scenarioJson: unknown, + targetWorldId: string, + ): Promise { // 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, diff --git a/packages/scenario/src/schema.ts b/packages/scenario/src/schema.ts index 307522a..bbfe4ec 100644 --- a/packages/scenario/src/schema.ts +++ b/packages/scenario/src/schema.ts @@ -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(), }); From 9d18444220ab074cabce9a1f1b9290af8cec72ba Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Wed, 15 Jul 2026 19:20:13 +0530 Subject: [PATCH 2/3] chore: Format files --- .github/workflows/deploy-docs.yml | 10 +- apps/gui/src/app/actions.ts | 60 +- apps/gui/src/app/builder/page.tsx | 4 +- apps/gui/src/app/play/page.tsx | 12 +- apps/gui/src/components/config/ConfigView.tsx | 3 +- .../config/ProviderInstancesConfig.tsx | 4 +- apps/gui/src/components/play/PlayView.tsx | 465 +- apps/gui/src/components/play/PromptModal.tsx | 12 +- apps/gui/src/components/play/ScenarioCard.tsx | 18 +- apps/gui/src/components/ui/accordion.tsx | 30 +- apps/gui/src/components/ui/badge.tsx | 18 +- apps/gui/src/components/ui/button.tsx | 35 +- apps/gui/src/components/ui/card.tsx | 33 +- apps/gui/src/components/ui/checkbox.tsx | 16 +- apps/gui/src/components/ui/dialog.tsx | 51 +- apps/gui/src/components/ui/empty.tsx | 33 +- apps/gui/src/components/ui/input.tsx | 10 +- apps/gui/src/components/ui/item.tsx | 56 +- apps/gui/src/components/ui/label.tsx | 14 +- .../gui/src/components/ui/navigation-menu.tsx | 48 +- apps/gui/src/components/ui/select.tsx | 47 +- apps/gui/src/components/ui/separator.tsx | 20 +- apps/gui/src/components/ui/sheet.tsx | 59 +- apps/gui/src/components/ui/sidebar.tsx | 248 +- apps/gui/src/components/ui/skeleton.tsx | 6 +- apps/gui/src/components/ui/spinner.tsx | 8 +- apps/gui/src/components/ui/table.tsx | 32 +- apps/gui/src/components/ui/tabs.tsx | 28 +- apps/gui/src/components/ui/textarea.tsx | 10 +- apps/gui/src/components/ui/tooltip.tsx | 22 +- apps/gui/src/hooks/use-mobile.tsx | 24 +- apps/gui/tsconfig.json | 14 +- eslint.config.mjs | 7 +- .../actor/tests/actor-prompt-builder.test.ts | 14 +- packages/architect/src/architect.ts | 11 +- packages/architect/src/index.ts | 1 - packages/architect/src/llm-validator.ts | 3 +- packages/architect/tests/architect.test.ts | 43 +- packages/core/src/clock.ts | 2 +- packages/core/src/index.ts | 1 - packages/core/src/world.ts | 98 +- packages/core/tests/clock.test.ts | 98 +- packages/intent/tests/intent.test.ts | 6 +- packages/intent/tsconfig.json | 5 +- packages/llm/src/llm.ts | 3 +- packages/llm/src/provider-manager.ts | 386 +- packages/llm/src/providers/google-genai.ts | 38 +- packages/llm/src/providers/mock.ts | 16 +- packages/llm/src/providers/openrouter.ts | 28 +- packages/llm/tests/mock.test.ts | 2 +- packages/llm/tests/openrouter.test.ts | 4 +- packages/llm/tests/provider-manager.test.ts | 21 +- packages/memory/src/buffer.ts | 10 +- packages/memory/src/handoff.ts | 54 +- packages/memory/src/ledger.ts | 62 +- packages/memory/tests/handoff.test.ts | 39 +- packages/memory/tests/ledger.test.ts | 34 +- packages/memory/tests/memory.test.ts | 15 +- packages/scenario/tests/scenario.test.ts | 28 +- packages/scenario/tests/talking-room.test.ts | 46 +- pnpm-lock.yaml | 11801 ++++++++++------ pnpm-workspace.yaml | 2 +- tests/evals/google-genai.eval.ts | 5 +- tests/evals/intent-decoder.eval.ts | 24 +- tests/integration/actor-monologue.test.ts | 48 +- tests/integration/game-loop.test.ts | 67 +- web/docs/astro.config.mjs | 8 +- .../src/content/docs/architecture/actor.md | 30 +- .../src/content/docs/architecture/handoff.md | 36 +- .../src/content/docs/architecture/intents.md | 1 + .../docs/architecture/llm-providers.md | 24 +- .../content/docs/architecture/memory-tier2.md | 27 +- .../src/content/docs/architecture/memory.md | 22 +- .../src/content/docs/architecture/spatial.md | 1 + web/docs/src/content/docs/guides/testing.md | 1 + web/docs/wrangler.jsonc | 8 +- web/landing/src/counter.ts | 12 +- web/landing/src/main.ts | 16 +- 78 files changed, 9416 insertions(+), 5242 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 5862155..47370b2 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -5,9 +5,9 @@ on: branches: - master paths: - - 'web/docs/**' - - 'pnpm-lock.yaml' - - '.github/workflows/deploy-docs.yml' + - "web/docs/**" + - "pnpm-lock.yaml" + - ".github/workflows/deploy-docs.yml" workflow_dispatch: jobs: @@ -28,7 +28,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - cache: 'pnpm' + cache: "pnpm" - name: Install Dependencies run: pnpm install --frozen-lockfile @@ -41,4 +41,4 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - workingDirectory: 'web/docs' + workingDirectory: "web/docs" diff --git a/apps/gui/src/app/actions.ts b/apps/gui/src/app/actions.ts index 4503301..eefc5c1 100644 --- a/apps/gui/src/app/actions.ts +++ b/apps/gui/src/app/actions.ts @@ -4,7 +4,12 @@ import path from "path"; import fs from "fs"; import { simulationManager } from "@/lib/simulation"; import type { SimSnapshot } from "@/lib/simulation"; -import { ProviderManager, ModelProviderInstance, AVAILABLE_PROVIDERS, ModelProviderMeta } from "@omnia/llm"; +import { + ProviderManager, + ModelProviderInstance, + AVAILABLE_PROVIDERS, + ModelProviderMeta, +} from "@omnia/llm"; function resolveScenarioPath(relative: string): string { const cwd = process.cwd(); @@ -25,8 +30,7 @@ function resolveScenarioPath(relative: string): string { } type ActionResult = - | { ok: true; snapshot: SimSnapshot } - | { ok: false; error: string }; + { ok: true; snapshot: SimSnapshot } | { ok: false; error: string }; export async function startSimulation(input: { scenario?: string; @@ -168,8 +172,7 @@ export async function getConfigStatus(): Promise<{ } export async function listSavedSimulations(): Promise< - | { ok: true; sessions: SimSnapshot[] } - | { ok: false; error: string } + { ok: true; sessions: SimSnapshot[] } | { ok: false; error: string } > { try { const sessions = simulationManager.listSavedSessions(); @@ -197,7 +200,9 @@ export async function resumeSimulation(simId: string): Promise { } } -export async function getScenarioEntities(scenarioPath: string): Promise< +export async function getScenarioEntities( + scenarioPath: string, +): Promise< | { ok: true; entities: { id: string; name: string }[] } | { ok: false; error: string } > { @@ -207,10 +212,12 @@ export async function getScenarioEntities(scenarioPath: string): Promise< return { ok: false, error: `Scenario file not found: ${scenarioPath}` }; } const content = JSON.parse(fs.readFileSync(resolved, "utf-8")); - const entities = (content.entities || []).map((e: { id: string; name?: string }) => ({ - id: e.id, - name: e.name || e.id, - })); + const entities = (content.entities || []).map( + (e: { id: string; name?: string }) => ({ + id: e.id, + name: e.name || e.id, + }), + ); return { ok: true, entities }; } catch (err) { return { @@ -220,9 +227,9 @@ export async function getScenarioEntities(scenarioPath: string): Promise< } } -export async function deleteSimulation(simId: string): Promise< - { ok: true } | { ok: false; error: string } -> { +export async function deleteSimulation( + simId: string, +): Promise<{ ok: true } | { ok: false; error: string }> { try { simulationManager.deleteSession(simId); return { ok: true }; @@ -234,7 +241,9 @@ export async function deleteSimulation(simId: string): Promise< } } -export async function listProviderInstances(): Promise { +export async function listProviderInstances(): Promise< + ModelProviderInstance[] +> { return ProviderManager.list(); } @@ -246,7 +255,14 @@ export async function createProviderInstance( type: "generative" | "embedding" = "generative", maxContext?: number, ): Promise { - return ProviderManager.create(name, providerName, apiKey, modelName, type, maxContext); + return ProviderManager.create( + name, + providerName, + apiKey, + modelName, + type, + maxContext, + ); } export async function deleteProviderInstance(id: string): Promise { @@ -266,7 +282,15 @@ export async function updateProviderInstance( type: "generative" | "embedding" = "generative", maxContext?: number, ): Promise { - ProviderManager.update(id, name, providerName, apiKey, modelName, type, maxContext); + ProviderManager.update( + id, + name, + providerName, + apiKey, + modelName, + type, + maxContext, + ); } export async function getProviderMappings(): Promise> { @@ -284,6 +308,8 @@ export async function getAvailableProviders(): Promise { return AVAILABLE_PROVIDERS; } -export async function regenerateEmbeddings(newProviderInstanceId?: string): Promise { +export async function regenerateEmbeddings( + newProviderInstanceId?: string, +): Promise { await simulationManager.regenerateAllEmbeddings(newProviderInstanceId); } diff --git a/apps/gui/src/app/builder/page.tsx b/apps/gui/src/app/builder/page.tsx index b8b973c..0d397b7 100644 --- a/apps/gui/src/app/builder/page.tsx +++ b/apps/gui/src/app/builder/page.tsx @@ -4,7 +4,9 @@ export default function BuilderPage() { return (
-

Scenario Builder

+

+ Scenario Builder +

Scenario builder interface coming soon... diff --git a/apps/gui/src/app/play/page.tsx b/apps/gui/src/app/play/page.tsx index b7d5487..b4b327c 100644 --- a/apps/gui/src/app/play/page.tsx +++ b/apps/gui/src/app/play/page.tsx @@ -5,11 +5,13 @@ import { Suspense } from "react"; export default function PlayPage() { return ( - -

Loading...
-
- }> + +
Loading...
+
+ } + > ); diff --git a/apps/gui/src/components/config/ConfigView.tsx b/apps/gui/src/components/config/ConfigView.tsx index f4ec9ee..fc060ff 100644 --- a/apps/gui/src/components/config/ConfigView.tsx +++ b/apps/gui/src/components/config/ConfigView.tsx @@ -224,7 +224,8 @@ export function ConfigView() { {instances .filter( - (inst) => (inst.type || "generative") === task.type, + (inst) => + (inst.type || "generative") === task.type, ) .map((inst) => ( diff --git a/apps/gui/src/components/config/ProviderInstancesConfig.tsx b/apps/gui/src/components/config/ProviderInstancesConfig.tsx index f57c209..4b9da0e 100644 --- a/apps/gui/src/components/config/ProviderInstancesConfig.tsx +++ b/apps/gui/src/components/config/ProviderInstancesConfig.tsx @@ -282,7 +282,9 @@ export function ProviderInstancesConfig({ {inst.providerName}
{inst.isActive && Active} - {inst.type === "generative" ? "gen" : "embed"} + + {inst.type === "generative" ? "gen" : "embed"} +
diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx index dccd739..a8d8a92 100644 --- a/apps/gui/src/components/play/PlayView.tsx +++ b/apps/gui/src/components/play/PlayView.tsx @@ -422,242 +422,247 @@ export function PlayView() {

- {/* Simulation Global Controls */} -
- {snapshot.status !== "done" && snapshot.status !== "error" && ( - <> - {snapshot.status === "running" && - (loading ? ( - - ) : ( - - ))} - - - )} -
- -
- - Status:{" "} - - {getUnifiedStatus()} - - - - Turn:{" "} - {snapshot.turn} - -
- {statusMessage() && ( -

- {loading && "⏳ "} - {statusMessage()} -

- )} - - - {/* Scrollable Center Viewport */} -
- {activeTab === "interact" ? ( -
- {(() => { - const playerEntity = snapshot.entities.find((e) => e.isPlayer); - return snapshot.log.map((entry, i) => ( - - )); - })()} - {loading && ( -
- - {statusText || "Processing..."} -
- )} -
-
- ) : ( -
- {/* Simulation Info */} -
-

- Simulation Info -

-
-
- - Session ID - - - {snapshot.id} - -
-
- - Max Turns - - - {snapshot.maxTurns} - -
-
- - Turn Count - - - {snapshot.turn} - -
-
- - Entities Registered - - - {snapshot.entities.length} - -
-
-
- - {/* Entities Involved */} -
-

- Entities Involved -

-
- {snapshot.entities.map((ent) => ( -
-
- - {ent.name} - - - ID: {ent.id} - -
-
- {ent.isPlayer ? ( - - PLAYER - - ) : ( - - NPC - - )} -
-
- ))} -
-
-
- )} -
- - {/* Sticky Chat / Interaction Input Footer */} - {activeTab === "interact" && ( -