From bfff93e793138d6c59bb0c79dd2eb9d0548f0f1b Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 11 Jul 2026 09:11:49 +0530 Subject: [PATCH] refactor(architect): Add self description for intent decoder and refine context (#22) --- apps/gui/src/components/play/PlayView.tsx | 30 +++++++++++++------ apps/gui/src/lib/simulation-types.ts | 1 + apps/gui/src/lib/simulation.ts | 4 ++- packages/architect/tests/architect.test.ts | 5 ++++ packages/intent/src/intent-decoder.ts | 35 ++++++++++++++++++++-- packages/intent/src/intent.ts | 3 ++ packages/intent/tests/intent.test.ts | 4 +++ packages/memory/src/buffer.ts | 33 ++++++++++---------- packages/memory/tests/memory.test.ts | 19 +++++++----- packages/scenario/tests/scenario.test.ts | 1 + tests/integration/actor-monologue.test.ts | 3 ++ tests/integration/game-loop.test.ts | 4 +++ 12 files changed, 106 insertions(+), 36 deletions(-) diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx index 23ecd39..1b102aa 100644 --- a/apps/gui/src/components/play/PlayView.tsx +++ b/apps/gui/src/components/play/PlayView.tsx @@ -17,8 +17,10 @@ import type { LLMProviderInstance } from "@omnia/llm"; function IntentTag({ intent, + isSelf, }: { intent: SimSnapshot["log"][number]["intents"][number]; + isSelf?: boolean; }) { const labels: Record = { monologue: "thought", @@ -33,9 +35,13 @@ function IntentTag({ outcome = intent.isValid ? " ✅" : ` ❌ (${intent.reason})`; } + const textToDisplay = (isSelf && intent.selfDescription) + ? intent.selfDescription + : intent.description; + return ( - [{label}] “{intent.description}”{outcome} + [{label}] “{textToDisplay}”{outcome} {intent.minutesToAdvance ? ` [+${intent.minutesToAdvance}min]` : ""} ); @@ -301,9 +307,11 @@ function formatSimTime(isoString: string) { function LogEntryCard({ entry, onShowPrompt, + isPlayerCard, }: { entry: SimSnapshot["log"][number]; onShowPrompt: (entry: SimSnapshot["log"][number]) => void; + isPlayerCard: boolean; }) { const showMenu = !!(entry.rawPrompt || entry.decoderPrompt); @@ -330,7 +338,7 @@ function LogEntryCard({
{entry.narrativeProse}
{entry.intents.map((intent, i) => ( - + ))}
@@ -744,13 +752,17 @@ export function PlayView() {
- {snapshot.log.map((entry, i) => ( - - ))} + {(() => { + const playerEntity = snapshot.entities.find((e) => e.isPlayer); + return snapshot.log.map((entry, i) => ( + + )); + })()} {loading && (
diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index b5a098c..33028fe 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -1,6 +1,7 @@ export interface IntentInfo { type: string; description: string; + selfDescription?: string; targetIds: string[]; isValid?: boolean; reason?: string; diff --git a/apps/gui/src/lib/simulation.ts b/apps/gui/src/lib/simulation.ts index 350d172..882239c 100644 --- a/apps/gui/src/lib/simulation.ts +++ b/apps/gui/src/lib/simulation.ts @@ -119,7 +119,7 @@ class SimulationManager { playEntityName?: string, providerInstanceId?: string, ): Promise { - let activeInstance = providerInstanceId + const activeInstance = providerInstanceId ? ProviderManager.list().find((p) => p.id === providerInstanceId) : ProviderManager.getActive(); @@ -385,6 +385,7 @@ class SimulationManager { entry.intents.push({ type: intent.type, description: intent.description, + selfDescription: intent.selfDescription, targetIds: intent.targetIds, isValid: outcome.isValid, reason: outcome.reason, @@ -528,6 +529,7 @@ class SimulationManager { entry.intents.push({ type: intent.type, description: intent.description, + selfDescription: intent.selfDescription, targetIds: intent.targetIds, isValid: outcome.isValid, reason: outcome.reason, diff --git a/packages/architect/tests/architect.test.ts b/packages/architect/tests/architect.test.ts index b8bf8dd..119c9aa 100644 --- a/packages/architect/tests/architect.test.ts +++ b/packages/architect/tests/architect.test.ts @@ -22,6 +22,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => { type: "action", originalText: "open the chest and read the scroll", description: "Open the chest and read the scroll", + selfDescription: "You open the chest and read the scroll.", actorId: "alice", targetIds: [], }; @@ -51,6 +52,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => { type: "action", originalText: "unlock the gate and escape", description: "Unlock the gate and escape", + selfDescription: "You unlock the gate and escape.", actorId: "bob", targetIds: [], }; @@ -72,6 +74,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => { type: "action", originalText: "haunt the mansion", description: "Haunt the mansion", + selfDescription: "You haunt the mansion.", actorId: "ghost", targetIds: [], }; @@ -110,6 +113,7 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", () type: "action", originalText: "pick the lock of the wooden chest", description: "Pick the lock of the wooden chest", + selfDescription: "You pick the lock of the wooden chest.", actorId: "alice", targetIds: [], }; @@ -152,6 +156,7 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", () type: "action", originalText: "run away", description: "Run away", + selfDescription: "You run away.", actorId: "bob", targetIds: [], }; diff --git a/packages/intent/src/intent-decoder.ts b/packages/intent/src/intent-decoder.ts index 8fd224a..d30f3a8 100644 --- a/packages/intent/src/intent-decoder.ts +++ b/packages/intent/src/intent-decoder.ts @@ -1,4 +1,4 @@ -import { WorldState, serializeObjectiveWorldState } from "@omnia/core"; +import { WorldState } from "@omnia/core"; import { ILLMProvider } from "@omnia/llm"; import { IntentSequence, IntentSequenceSchema } from "./intent.js"; @@ -37,7 +37,9 @@ For each intent you must: - "action": Any physical or logical action performed in the world (e.g., moving, picking up, opening, looking). - "monologue": An inner thought, reflection, or internal monologue. This is purely internal — not spoken aloud, not perceivable by any other entity, and not a physical action. Use this for any prose depicting the character thinking, reflecting, feeling, or narrating to themselves internally. 2. Extract the original text fragment from the prose that corresponds to this intent. -3. Write a concise, structured description of the intent (what is being done or said). Include as much detail about the action as possible that was extracted from the narrative prose. Do not make up qualities. +3. Populate "description" and "selfDescription": + - "description": No subject or name — a bare third-person verb phrase only (e.g. "clears their throat", "shakes their head slowly"), so it can be composed with any observer's alias for the actor. + - "selfDescription": The same event from the actor's own perspective, second person, complete sentence starting with "You" (e.g. "You clear your throat.", "You shake your head slowly."). This is shown directly in the actor's own memory — it must never say "the actor" or refer to them in the third person. 4. Identify the actorId (the entity performing the intent — this will always be "${actorId}"). 5. Identify targetIds — the entity IDs of the receiving parties. Use the "KNOWN ENTITY IDS" and "ACTOR ALIASES" mapping to resolve any subjective names, descriptions, or nicknames used in the prose to their correct system entity IDs. If no specific target, use an empty array. For "monologue" intents, targetIds must always be an empty array. @@ -58,7 +60,7 @@ The actor refers to other entities using these subjective names/aliases: ${aliasContext} === WORLD STATE === -${serializeObjectiveWorldState(worldState)} +${serializeSimplifiedWorldState(worldState)} === ACTOR === Actor ID: ${actorId} @@ -82,3 +84,30 @@ ${narrativeProse} return response.data; } } + +function serializeSimplifiedWorldState(worldState: WorldState): string { + const lines: string[] = []; + + lines.push("Locations:"); + if (worldState.locations.size > 0) { + for (const loc of worldState.locations.values()) { + const parentId = (loc as { parentId?: string | null }).parentId; + const parentStr = parentId ? ` (Parent: ${parentId})` : ""; + lines.push(` - Location [ID: ${loc.id}]${parentStr}`); + } + } else { + lines.push(" (No locations)"); + } + + lines.push("Entities:"); + if (worldState.entities.size > 0) { + for (const entity of worldState.entities.values()) { + const locStr = entity.locationId ? ` (Location: ${entity.locationId})` : ""; + lines.push(` - Entity [ID: ${entity.id}]${locStr}`); + } + } else { + lines.push(" (No entities)"); + } + + return lines.join("\n"); +} diff --git a/packages/intent/src/intent.ts b/packages/intent/src/intent.ts index de576d3..2f3e6b9 100644 --- a/packages/intent/src/intent.ts +++ b/packages/intent/src/intent.ts @@ -24,6 +24,9 @@ export const IntentSchema = z.object({ /** A concise, structured description of the intent's action or dialogue. */ description: z.string(), + /** The same event from the actor's own perspective (second person, "You"). */ + selfDescription: z.string(), + /** The entity ID of the actor performing the intent. */ actorId: z.string(), diff --git a/packages/intent/tests/intent.test.ts b/packages/intent/tests/intent.test.ts index 3bc7af8..d39db8b 100644 --- a/packages/intent/tests/intent.test.ts +++ b/packages/intent/tests/intent.test.ts @@ -15,6 +15,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { type: "action", originalText: "Alice opened the chest.", description: "Open the wooden chest.", + selfDescription: "You open the wooden chest.", actorId: "alice", targetIds: [], }, @@ -45,6 +46,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { type: "dialogue", originalText: '"Do you have the key?" Alice asked Bob.', description: "Alice asks Bob if he has the key.", + selfDescription: "You ask Bob if he has the key.", actorId: "alice", targetIds: ["bob"], }, @@ -78,6 +80,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { type: "dialogue", originalText: '"Cover me," Alice whispered to Bob.', description: "Alice whispers to Bob requesting cover.", + selfDescription: "You whisper to Bob requesting cover.", actorId: "alice", targetIds: ["bob"], }, @@ -85,6 +88,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { type: "action", originalText: "She crept towards the door and pulled the handle.", description: "Creep towards the door and pull the handle.", + selfDescription: "You creep towards the door and pull the handle.", actorId: "alice", targetIds: [], }, diff --git a/packages/memory/src/buffer.ts b/packages/memory/src/buffer.ts index 472625e..5983bda 100644 --- a/packages/memory/src/buffer.ts +++ b/packages/memory/src/buffer.ts @@ -25,27 +25,28 @@ export function serializeSubjectiveBufferEntry( entry: BufferEntry, viewer: Entity, ): string { - const actorAlias = resolveAlias(viewer, entry.intent.actorId); + const isSelf = viewer.id === entry.intent.actorId; - const targetAliases = entry.intent.targetIds.map((tid) => - resolveAlias(viewer, tid), - ); - - let details: string; - const content = entry.intent.description.trim() || entry.intent.originalText.trim(); - - if (entry.intent.type === "dialogue") { - details = `spoke to ${targetAliases.join(", ") || "someone"}: "${content}"`; - } else if (entry.intent.type === "monologue") { - details = `thought: "${content}"`; - } else { - details = content; - if (entry.outcome) { + if (isSelf) { + let details = (entry.intent.selfDescription || entry.intent.description || entry.intent.originalText).trim(); + if (details.length > 0) { + details = details.charAt(0).toUpperCase() + details.slice(1); + } + if (entry.intent.type === "action" && entry.outcome) { details += ` (Outcome: ${entry.outcome.isValid ? "Succeeded" : `Failed - ${entry.outcome.reason}`})`; } + return details; } - return `${actorAlias} ${details}`; + const actorAlias = resolveAlias(viewer, entry.intent.actorId); + const subjectStr = actorAlias.charAt(0).toUpperCase() + actorAlias.slice(1); + + let details = (entry.intent.description || entry.intent.originalText).trim(); + if (entry.intent.type === "action" && entry.outcome) { + details += ` (Outcome: ${entry.outcome.isValid ? "Succeeded" : `Failed - ${entry.outcome.reason}`})`; + } + + return `${subjectStr} ${details}`; } export class BufferRepository { diff --git a/packages/memory/tests/memory.test.ts b/packages/memory/tests/memory.test.ts index cbb8e26..411d5e8 100644 --- a/packages/memory/tests/memory.test.ts +++ b/packages/memory/tests/memory.test.ts @@ -32,14 +32,15 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { intent: { type: "dialogue", originalText: '"Hello there," Bob said to Charlie.', - description: "Bob greets Charlie", + description: "says, 'Hello there' to the bartender", + selfDescription: "You say, 'Hello there' to the bartender.", actorId: "bob", targetIds: ["charlie"], }, }; const result = serializeSubjectiveBufferEntry(entry, viewer); - expect(result).toBe('the hooded figure spoke to the bartender: "Bob greets Charlie"'); + expect(result).toBe("The hooded figure says, 'Hello there' to the bartender"); }); test("serializes action intent with outcome details", () => { @@ -54,7 +55,8 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { intent: { type: "action", originalText: "Bob tried to break the latch.", - description: "Bob attempts to break the lock latch", + description: "attempts to break the lock latch", + selfDescription: "You attempt to break the lock latch.", actorId: "bob", targetIds: [], }, @@ -65,7 +67,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { }; const result = serializeSubjectiveBufferEntry(entry, viewer); - expect(result).toBe('the hooded figure Bob attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)'); + expect(result).toBe('The hooded figure attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)'); }); test("serializes self-reference and unfamiliar actors", () => { @@ -80,13 +82,14 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { type: "action", originalText: "I opened the window.", description: "open the window", + selfDescription: "You open the window.", actorId: "alice", targetIds: [], }, }; const resultSelf = serializeSubjectiveBufferEntry(entrySelf, viewer); - expect(resultSelf).toBe("you open the window"); + expect(resultSelf).toBe("You open the window."); const entryUnfamiliar: BufferEntry = { id: "entry-unfamiliar", @@ -96,14 +99,15 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { intent: { type: "action", originalText: "Someone knocked.", - description: "knock on the door", + description: "knocks on the door", + selfDescription: "You knock on the door.", actorId: "stranger-1", targetIds: [], }, }; const resultUnfamiliar = serializeSubjectiveBufferEntry(entryUnfamiliar, viewer); - expect(resultUnfamiliar).toBe("an unfamiliar figure knock on the door"); + expect(resultUnfamiliar).toBe("An unfamiliar figure knocks on the door"); }); }); @@ -123,6 +127,7 @@ describe("BufferRepository Persistence Tests (Tier 1)", () => { type: "action", originalText: "Alice picked up a stick.", description: "Alice gathers a stick", + selfDescription: "You gather a stick.", actorId: "alice", targetIds: [], }; diff --git a/packages/scenario/tests/scenario.test.ts b/packages/scenario/tests/scenario.test.ts index 058930d..3cbce78 100644 --- a/packages/scenario/tests/scenario.test.ts +++ b/packages/scenario/tests/scenario.test.ts @@ -57,6 +57,7 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => { type: "action", originalText: "I entered the foyer.", description: "entered the house", + selfDescription: "You entered the house.", actorId: "investigator", targetIds: [], }, diff --git a/tests/integration/actor-monologue.test.ts b/tests/integration/actor-monologue.test.ts index 11c4d38..da8d166 100644 --- a/tests/integration/actor-monologue.test.ts +++ b/tests/integration/actor-monologue.test.ts @@ -62,6 +62,7 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => { type: "monologue", originalText: "I can't believe Bob hasn't noticed me yet, Alice thought.", description: "Alice internally reflects that Bob has not noticed her.", + selfDescription: "You internally reflect that Bob has not noticed you.", actorId: "alice", targetIds: [], }, @@ -69,6 +70,7 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => { type: "dialogue", originalText: '"Hey Bob," she called out softly.', description: "Alice softly calls out to Bob.", + selfDescription: "You softly call out to Bob.", actorId: "alice", targetIds: ["bob"], }, @@ -76,6 +78,7 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => { type: "action", originalText: "She reached for the ledger on the table.", description: "Alice reaches for the ledger on the table.", + selfDescription: "You reach for the ledger on the table.", actorId: "alice", targetIds: [], }, diff --git a/tests/integration/game-loop.test.ts b/tests/integration/game-loop.test.ts index 4bd7557..7784db4 100644 --- a/tests/integration/game-loop.test.ts +++ b/tests/integration/game-loop.test.ts @@ -33,6 +33,7 @@ describe("Omnia Integration Tests (Tier 2)", () => { type: "dialogue", originalText: '"Cover me," Alice whispered to Bob.', description: "Alice whispers to Bob to cover her.", + selfDescription: "You whisper to Bob to cover you.", actorId: "alice", targetIds: ["bob"], }, @@ -40,6 +41,7 @@ describe("Omnia Integration Tests (Tier 2)", () => { type: "action", originalText: "She crept towards the door and pulled the handle.", description: "Alice creeps to the door and pulls the handle.", + selfDescription: "You creep to the door and pull the handle.", actorId: "alice", targetIds: [], }, @@ -108,6 +110,7 @@ describe("Omnia Integration Tests (Tier 2)", () => { type: "action" as const, originalText: "She tries to unlock the gate with a hairpin.", description: "Alice attempts to pick the lock with a hairpin.", + selfDescription: "You attempt to pick the lock with a hairpin.", actorId: "alice", targetIds: [], }; @@ -116,6 +119,7 @@ describe("Omnia Integration Tests (Tier 2)", () => { type: "dialogue" as const, originalText: '"This is useless," she mutters.', description: "Alice mutters to herself.", + selfDescription: "You mutter to yourself.", actorId: "alice", targetIds: [], };