From 32ee8cda3a88b85ee889ca650f4e64ec98defb55 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 18 Jul 2026 11:48:54 +0530 Subject: [PATCH 01/15] feat(gui): Added handoff llm call log details to gui --- apps/gui/src/components/play/HandoffModal.tsx | 231 ++++++++++++++++++ apps/gui/src/components/play/PlayView.tsx | 62 ++++- apps/gui/src/components/ui/alert.tsx | 62 +++++ apps/gui/src/components/ui/button.tsx | 1 + apps/gui/src/lib/simulation-types.ts | 2 + apps/gui/src/lib/simulation/alias-handoff.ts | 27 +- packages/llm/src/base-provider.ts | 1 + packages/llm/src/llm.ts | 1 + packages/llm/src/providers/mock.ts | 16 +- packages/memory/src/handoff.ts | 6 +- 10 files changed, 393 insertions(+), 16 deletions(-) create mode 100644 apps/gui/src/components/play/HandoffModal.tsx create mode 100644 apps/gui/src/components/ui/alert.tsx diff --git a/apps/gui/src/components/play/HandoffModal.tsx b/apps/gui/src/components/play/HandoffModal.tsx new file mode 100644 index 0000000..0124d7e --- /dev/null +++ b/apps/gui/src/components/play/HandoffModal.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { useState } from "react"; +import type { SimSnapshot } from "@/lib/simulation-types"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; + +interface HandoffModalProps { + entry: SimSnapshot["log"][number]; + onClose: () => void; +} + +export function HandoffModal({ entry, onClose }: HandoffModalProps) { + const [activeTab, setActiveTab] = useState<"chunks" | "prompt" | "output">( + "chunks", + ); + + const handoffResult = entry.handoffResult; + const chunks = handoffResult?.chunks || []; + + const getImportanceColor = (score: number) => { + if (score >= 8) + return "bg-destructive/10 text-destructive border-destructive/30"; + if (score >= 5) return "bg-amber-500/10 text-amber-500 border-amber-500/30"; + return "bg-emerald-500/10 text-emerald-500 border-emerald-500/30"; + }; + + return ( + !open && onClose()}> + + + + Memory Handoff Details — {entry.entityName} + {entry.usage && ( + + {entry.usage.modelName || "Handoff Model"} + + )} + + + + {/* Custom Tab Switcher */} +
+ + + +
+ +
+ {activeTab === "chunks" && ( +
+ {entry.usage && ( +
+
+ + Input Tokens + + + {entry.usage.inputTokens} + +
+
+ + Output Tokens + + + {entry.usage.outputTokens} + +
+
+ + Total Tokens + + + {entry.usage.totalTokens} + +
+
+ )} + + {chunks.length === 0 ? ( +
+

+ No memories were promoted to the Ledger during this turn. +

+

+ All working memory buffer entries were summarized or + forgotten. +

+
+ ) : ( +
+

+ Ledger Additions +

+ {chunks.map((chunk: any, index: number) => ( +
+
+
+ {chunk.content} +
+ + Importance: {chunk.importance} + +
+ + {chunk.quotes && chunk.quotes.length > 0 && ( +
+ {chunk.quotes.map((quote: string, qIdx: number) => ( +
“{quote}”
+ ))} +
+ )} + +
+ {chunk.retainInBuffer ? ( + + Pinned in Buffer + + ) : ( + + Pruned from Buffer + + )} + + {chunk.involvedEntityIds && + chunk.involvedEntityIds.length > 0 && ( +
+ Entities: + {chunk.involvedEntityIds.map((entId: string) => ( + + {entId} + + ))} +
+ )} +
+
+ ))} +
+ )} +
+ )} + + {activeTab === "prompt" && entry.rawPrompt && ( +
+
+

+ System Prompt +

+
+                  {entry.rawPrompt.systemPrompt}
+                
+
+ +
+

+ User Context (Candidates) +

+
+                  {entry.rawPrompt.userContext}
+                
+
+
+ )} + + {activeTab === "output" && ( +
+

+ Raw JSON Output +

+
+                {handoffResult
+                  ? JSON.stringify(handoffResult, null, 2)
+                  : "No JSON Output recorded."}
+              
+
+ )} +
+
+
+ ); +} diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx index a8d8a92..b75a5b6 100644 --- a/apps/gui/src/components/play/PlayView.tsx +++ b/apps/gui/src/components/play/PlayView.tsx @@ -12,6 +12,13 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Spinner } from "@/components/ui/spinner"; import { PromptModal } from "./PromptModal"; +import { HandoffModal } from "./HandoffModal"; +import { + Alert, + AlertAction, + AlertDescription, + AlertTitle, +} from "@/components/ui/alert"; import { cn } from "@/lib/utils"; import { ChevronLeft } from "lucide-react"; import { @@ -163,6 +170,9 @@ export function PlayView() { const [selectedEntryForModal, setSelectedEntryForModal] = useState< SimSnapshot["log"][number] | null >(null); + const [selectedHandoffForModal, setSelectedHandoffForModal] = useState< + SimSnapshot["log"][number] | null + >(null); const logEndRef = useRef(null); const steppingRef = useRef(false); @@ -490,14 +500,43 @@ export function PlayView() { const playerEntity = snapshot.entities.find( (e) => e.isPlayer, ); - return snapshot.log.map((entry, i) => ( - - )); + return snapshot.log.map((entry, i) => { + if (entry.isHandoff) { + return ( + +
+ + Handoff triggered for {entry.entityName} + + + Memories were transferred from Buffer to Memory + Ledger + +
+ + + +
+ ); + } + return ( + + ); + }); })()} {loading && (
@@ -662,6 +701,13 @@ export function PlayView() { onClose={() => setSelectedEntryForModal(null)} /> )} + + {selectedHandoffForModal && ( + setSelectedHandoffForModal(null)} + /> + )}
); diff --git a/apps/gui/src/components/ui/alert.tsx b/apps/gui/src/components/ui/alert.tsx new file mode 100644 index 0000000..591cad1 --- /dev/null +++ b/apps/gui/src/components/ui/alert.tsx @@ -0,0 +1,62 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +const AlertAction = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +AlertAction.displayName = "AlertAction"; + +export { Alert, AlertTitle, AlertDescription, AlertAction }; diff --git a/apps/gui/src/components/ui/button.tsx b/apps/gui/src/components/ui/button.tsx index 6af517f..a9b62d9 100644 --- a/apps/gui/src/components/ui/button.tsx +++ b/apps/gui/src/components/ui/button.tsx @@ -22,6 +22,7 @@ const buttonVariants = cva( size: { default: "h-10 px-4 py-2", sm: "h-9 px-3 text-xs", + xs: "h-7 px-2.5 text-xs", lg: "h-11 px-8 text-base", icon: "h-10 w-10", }, diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index c4ee4bd..e2b6c72 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -16,6 +16,8 @@ export interface LogEntry { narrativeProse: string; intents: IntentInfo[]; timestamp: string; + isHandoff?: boolean; + handoffResult?: any; rawPrompt?: { systemPrompt: string; userContext: string; diff --git a/apps/gui/src/lib/simulation/alias-handoff.ts b/apps/gui/src/lib/simulation/alias-handoff.ts index dc17697..9ff4f25 100644 --- a/apps/gui/src/lib/simulation/alias-handoff.ts +++ b/apps/gui/src/lib/simulation/alias-handoff.ts @@ -33,11 +33,36 @@ export async function runHandoffResolution(session: SimSession): Promise { maxContext, ); if (trigger !== "none") { - await handoffEngine.runHandoff( + const ran = await handoffEngine.runHandoff( entity, bufferEntries, worldState.clock.get(), ); + if (ran) { + const lastCall = + session.handoffProvider.lastCalls?.[ + (session.handoffProvider.lastCalls?.length || 0) - 1 + ]; + const info = session.entities.find((e) => e.id === entity.id); + const entityName = info?.name || entity.id; + session.log.push({ + turn: session.turn, + entityId: entity.id, + entityName, + narrativeProse: `Handoff triggered for ${entityName}: memories were transferred from Buffer to Memory Ledger`, + intents: [], + timestamp: worldState.clock.get().toISOString(), + isHandoff: true, + rawPrompt: lastCall + ? { + systemPrompt: lastCall.systemPrompt, + userContext: lastCall.userContext, + } + : undefined, + usage: lastCall?.usage, + handoffResult: lastCall?.response, + }); + } } } } diff --git a/packages/llm/src/base-provider.ts b/packages/llm/src/base-provider.ts index 4540b25..2d33436 100644 --- a/packages/llm/src/base-provider.ts +++ b/packages/llm/src/base-provider.ts @@ -105,6 +105,7 @@ export abstract class BaseLLMProvider implements ILLMProvider { systemPrompt: request.systemPrompt, userContext: request.userContext, usage, + response: parsed, }); return { success: true, data: parsed, usage }; diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index 7d136da..03bb135 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -33,6 +33,7 @@ export interface LLMCallRecord { providerInstanceName?: string; maxContext?: number; }; + response?: any; } export interface ILLMProvider { diff --git a/packages/llm/src/providers/mock.ts b/packages/llm/src/providers/mock.ts index b898133..116cffb 100644 --- a/packages/llm/src/providers/mock.ts +++ b/packages/llm/src/providers/mock.ts @@ -48,15 +48,21 @@ export class MockLLMProvider implements ILLMProvider { return { success: false, error: "Mock responses exhausted" }; } const usage = { inputTokens: 100, outputTokens: 50, totalTokens: 150 }; - this.lastCalls.push({ - systemPrompt: request.systemPrompt, - userContext: request.userContext, - usage, - }); try { const parsed = request.schema.parse(next); + this.lastCalls.push({ + systemPrompt: request.systemPrompt, + userContext: request.userContext, + usage, + response: parsed, + }); return { success: true, data: parsed, usage }; } catch (e) { + this.lastCalls.push({ + systemPrompt: request.systemPrompt, + userContext: request.userContext, + usage, + }); return { success: false, error: e instanceof Error ? e.message : String(e), diff --git a/packages/memory/src/handoff.ts b/packages/memory/src/handoff.ts index 4e61028..98e84e3 100644 --- a/packages/memory/src/handoff.ts +++ b/packages/memory/src/handoff.ts @@ -230,10 +230,12 @@ export class HandoffEngine { You are the memory Handoff Engine. Your task is to process a list of recent working memory buffer entries for an entity and select which memories to promote to the long-term Ledger, and which to forget or summarize. Instructions: -1. **Cluster** related consecutive buffer entries into high-level narrative beats or events (e.g. a full back-and-forth conversation or a single physical action and its outcome). Combine them into a single summary chunk. -2. **Write in the third-person** for the "content" of each chunk (e.g. "John asked Mary for the key, and Mary reluctantly handed it over"). +1. **Cluster** related consecutive buffer entries into high-level narrative beats or events (e.g. physical action and its outcome or trivial actions). Combine them into a single chunk. +2. **Write in the third-person** for the events of other entities. (eg. Alan did that. Sarah did this, etc) +2. **Write in first-person for the events that you yourself did. (eg. I did this, I did that.) 3. **verbatim Quotes**: Extract verbatim, high-salience quotes from dialogue if relevant. Do not modify or invent quotes. 4. **Determine Importance**: Assign an importance score from 1 (trivial, e.g. waking up) to 10 (life-altering, e.g. witnessing a crime). +4. Discard small body movements like looking around, sighing, etc that do not contextually hold any meaning after it is done. 5. **Involved Entities**: Identify all entity IDs involved in the memories in this chunk. 6. **Retain in Buffer (Pinning)**: If a beat represents an unresolved high-stakes situation (e.g. a standing threat, an unanswered accusation, an ongoing chase or conflict), set "retainInBuffer" to true so it remains in the working memory buffer for immediate context. Otherwise, set it to false so it is safely pruned from the buffer. 7. **Exclude stage business**: Glances, sighs, ambient noticing, and irrelevant sensory details should be ignored and not included in any promoted chunk. They will be forgotten. From b4bf70dbaed6a240aff2fb4fbee69c51ba33a92a Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 18 Jul 2026 15:50:12 +0530 Subject: [PATCH 02/15] refactor(memory,gui): Streamline memory archtiecture and terminology - Short term memory/ recent events are collectively now Cognitive Buffer (Tier 1) - Long term memory/ memory ledgers are just Memory Ledger (Tier 2) - Dossiers stay Dossiers (Tier 3) --- README.md | 10 +++---- apps/gui/next-env.d.ts | 2 +- apps/gui/src/components/config/ConfigView.tsx | 4 +-- apps/gui/src/components/play/HandoffModal.tsx | 8 ++--- apps/gui/src/components/play/InteractView.tsx | 3 +- apps/gui/src/components/play/PromptModal.tsx | 12 ++++---- apps/gui/src/lib/simulation/alias-handoff.ts | 4 +-- packages/actor/src/actor-prompt-builder.ts | 20 ++++++------- .../actor/tests/actor-prompt-builder.test.ts | 18 +++++------ packages/memory/src/handoff.ts | 8 ++--- .../src/content/docs/architecture/actor.md | 4 +-- .../src/content/docs/architecture/handoff.md | 16 +++++----- .../content/docs/architecture/memory-tier2.md | 30 +++++++++---------- 13 files changed, 70 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 955e6dd..048396f 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Access the application locally at `http://localhost:3000`. ### The Actor Agent -Each entity takes turns through an **Actor Agent** that receives a strictly epistemically bounded prompt: its own attributes (public, plus private ones explicitly granted to itself), its subjective memory buffer, the entities co-present at its location, and the current moment. Nothing else. The actor responds with free narrative prose. +Each entity takes turns through an **Actor Agent** that receives a strictly epistemically bounded prompt: its own attributes (public, plus private ones explicitly granted to itself), its **Cognitive Buffer**, the entities co-present at its location, and the current moment. Nothing else. The actor responds with free narrative prose. Prose is decoded into typed intents: @@ -121,8 +121,8 @@ Space is a graph: `world → region → location → point of interest`, connect ### Memory Tiers -- **Verbatim Buffer (implemented):** Per-character subjective event log. Every entry is stored from the owner's perspective actors resolved through the owner's alias map, outcomes attached — and recalled with naturalized time phrasing. -- **Vector Archive (implemented):** Summarized, embedded memory entries for semantic retrieval, keeping verbatim quotes only for high-salience lines. +- **Cognitive Buffer (implemented):** Per-character subjective event log. Every entry is stored from the owner's perspective actors resolved through the owner's alias map, outcomes attached — and recalled with naturalized time phrasing. +- **Memory Ledger (implemented):** Summarized, embedded memory entries for semantic retrieval, keeping verbatim quotes only for high-salience lines. - **Dossier (planned):** Each observer's subjective beliefs about another character. Memory is per-character on purpose: recall is testimony from a vantage point, which is what makes interrogating two witnesses interesting. @@ -156,14 +156,14 @@ The finish line for the first milestone is small on purpose. `v0` is almost on t - [x] Typed intent pipeline: `dialogue` / `action` / `monologue`, decoded from free prose. - [x] World Architect: LLM validation plus time-delta generation, end-to-end for single actions. - [x] Actor Agent with epistemically-bounded prompts (self, memory, co-located entities, subjective time). -- [x] Verbatim memory buffer with per-observer subjective serialization and alias resolution. +- [x] Verbatim Cognitive Buffer with per-observer subjective serialization and alias resolution. - [x] Spatial location graph (data model; perception is co-location only). - [x] Scenario loader (JSON → SQLite) and a playable CLI loop with human or LLM actors. **[The `v0` Milestone:](https://github.com/sortedcord/omnia-consolidated/milestone/1)** - [x] Two hand-authored NPCs live in one location, playable via CLI. -- [x] Each has buffer and vector-archive memory and recalls something said a few turns earlier. +- [x] Each has Cognitive Buffer and Memory Ledger memory and recalls something said a few turns earlier. - [x] One NPC knows a fact the other does not and, provably by testing, will not leak it. - [x] The Architect processes at least one non-trivial action per exchange with a visible state change. - [x] The whole thing persists to a SQLite file and reloads identically. diff --git a/apps/gui/next-env.d.ts b/apps/gui/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/apps/gui/next-env.d.ts +++ b/apps/gui/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/gui/src/components/config/ConfigView.tsx b/apps/gui/src/components/config/ConfigView.tsx index a713b5d..ab80062 100644 --- a/apps/gui/src/components/config/ConfigView.tsx +++ b/apps/gui/src/components/config/ConfigView.tsx @@ -177,13 +177,13 @@ export function ConfigView() { { key: "handoff", label: "Memory Handoff Engine", - desc: "Promotes entities' working memories to the long-term Ledger via LLM summarization and pruning.", + desc: "Promotes entities' Cognitive Buffer entries to the Memory Ledger via LLM summarization and pruning.", type: "generative", }, { key: "embeddings", label: "Text Embeddings Generator", - desc: "Generates vector embeddings for long-term memory retrieval.", + desc: "Generates vector embeddings for Memory Ledger retrieval.", type: "embedding", }, ].map((task) => ( diff --git a/apps/gui/src/components/play/HandoffModal.tsx b/apps/gui/src/components/play/HandoffModal.tsx index 0124d7e..4daa079 100644 --- a/apps/gui/src/components/play/HandoffModal.tsx +++ b/apps/gui/src/components/play/HandoffModal.tsx @@ -113,17 +113,17 @@ export function HandoffModal({ entry, onClose }: HandoffModalProps) { {chunks.length === 0 ? (

- No memories were promoted to the Ledger during this turn. + No memories were promoted to the Memory Ledger during this + turn.

- All working memory buffer entries were summarized or - forgotten. + All Cognitive Buffer entries were summarized or forgotten.

) : (

- Ledger Additions + Memory Ledger Additions

{chunks.map((chunk: any, index: number) => (
- Memories were transferred from Buffer to Memory Ledger + Memories were transferred from Cognitive Buffer to Memory + Ledger
diff --git a/apps/gui/src/components/play/PromptModal.tsx b/apps/gui/src/components/play/PromptModal.tsx index 36c940f..ea0cc90 100644 --- a/apps/gui/src/components/play/PromptModal.tsx +++ b/apps/gui/src/components/play/PromptModal.tsx @@ -30,8 +30,8 @@ export function PromptModal({ entry, onClose }: PromptModalProps) { userContext: string, inputTokens: number, ) => { - const recentHeader = "=== RECENT EVENTS ==="; - const ledgerHeader = "=== YOUR MEMORIES ==="; + const recentHeader = "=== COGNITIVE BUFFER ==="; + const ledgerHeader = "=== MEMORY LEDGER ==="; const recentIdx = userContext.indexOf(recentHeader); let worldStr = userContext; @@ -54,14 +54,14 @@ export function PromptModal({ entry, onClose }: PromptModalProps) { { label: "System Prompt", type: "system", content: systemPrompt }, { label: "World Info", type: "world", content: worldStr }, { - label: "Recent Events", + label: "Cognitive Buffer", type: "events", - content: recentStr || "(No recent events.)", + content: recentStr || "(No cognitive buffer entries.)", }, { - label: "Long-Term Memories", + label: "Memory Ledger", type: "memories", - content: ledgerStr || "(No long-term memories.)", + content: ledgerStr || "(No memory buffer entries.)", }, ]; diff --git a/apps/gui/src/lib/simulation/alias-handoff.ts b/apps/gui/src/lib/simulation/alias-handoff.ts index 9ff4f25..36204fc 100644 --- a/apps/gui/src/lib/simulation/alias-handoff.ts +++ b/apps/gui/src/lib/simulation/alias-handoff.ts @@ -3,7 +3,7 @@ import type { SimSession } from "./types"; /** * Runs the HandoffEngine for every agent entity that has accumulated enough - * buffer entries to warrant a handoff (compression to long-term memory). + * buffer entries to warrant a handoff (compression to the Memory Ledger). */ export async function runHandoffResolution(session: SimSession): Promise { const worldState = session.coreRepo.loadWorldState(session.worldInstanceId); @@ -49,7 +49,7 @@ export async function runHandoffResolution(session: SimSession): Promise { turn: session.turn, entityId: entity.id, entityName, - narrativeProse: `Handoff triggered for ${entityName}: memories were transferred from Buffer to Memory Ledger`, + narrativeProse: `Handoff triggered for ${entityName}: memories were transferred from Cognitive Buffer to Memory Ledger`, intents: [], timestamp: worldState.clock.get().toISOString(), isHandoff: true, diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index 81a93d8..2bb252d 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -33,17 +33,17 @@ export type ActorResponse = z.infer; * * The prompt is strictly epistemically bounded: the entity only sees what * it is allowed to see (public attributes + private attributes explicitly - * ACL'd to it), its own recent memory buffer, and the entities co-located + * ACL'd to it), its own Cognitive Buffer, and the entities co-located * with it. System UUIDs are surfaced as subjective aliases. */ export class ActorPromptBuilder { /** - * @param bufferRepo Used to fetch the actor's recent memory. Optional — + * @param bufferRepo Used to fetch the actor's Cognitive Buffer. Optional — * if absent, the memory section is omitted. - * @param ledgerRepo Used to fetch long-term memories. Optional. - * @param memoryLimit Maximum number of recent buffer entries to inject. + * @param ledgerRepo Used to fetch Memory Ledger entries. Optional. + * @param memoryLimit Maximum number of recent Cognitive Buffer entries to inject. * Defaults to 20. - * @param ledgerLimit Maximum number of long-term memories to retrieve. + * @param ledgerLimit Maximum number of Memory Ledger entries to retrieve. * Defaults to 5. */ constructor( @@ -111,13 +111,13 @@ Guidelines: } } - // --- Recent memory --- + // --- Cognitive Buffer --- const memorySection = this.buildMemorySection(entity, recentEntries, now); if (memorySection) { sections.push(memorySection); } - // --- Recalled Long-Term memory --- + // --- Recalled Memory Ledger --- const ledgerSection = this.buildLedgerSection( worldState, entity, @@ -139,7 +139,7 @@ Guidelines: if (!this.bufferRepo) return null; if (entries.length === 0) { - return `=== RECENT EVENTS ===\n(No recent events recorded.)`; + return `=== COGNITIVE BUFFER ===\n(No recent events recorded.)`; } const recent = entries.slice(-this.memoryLimit); @@ -159,7 +159,7 @@ Guidelines: groupedLines.push(` - ${serialized}`); } - return `=== RECENT EVENTS ===\n${groupedLines.join("\n")}`; + return `=== COGNITIVE BUFFER ===\n${groupedLines.join("\n")}`; } private buildLedgerSection( @@ -263,6 +263,6 @@ Guidelines: } } - return `=== YOUR MEMORIES ===\n${groupedLines.join("\n")}`; + return `=== MEMORY LEDGER ===\n${groupedLines.join("\n")}`; } } diff --git a/packages/actor/tests/actor-prompt-builder.test.ts b/packages/actor/tests/actor-prompt-builder.test.ts index 1c9a7c4..757c980 100644 --- a/packages/actor/tests/actor-prompt-builder.test.ts +++ b/packages/actor/tests/actor-prompt-builder.test.ts @@ -4,7 +4,7 @@ import { WorldState, Entity } from "@omnia/core"; import { BufferRepository, LedgerRepository } from "@omnia/memory"; import { ActorPromptBuilder } from "../src/actor-prompt-builder"; -describe("ActorPromptBuilder with Long-Term Memory Integration", () => { +describe("ActorPromptBuilder with Memory Ledger Integration", () => { let db: Database.Database; let bufferRepo: BufferRepository; let ledgerRepo: LedgerRepository; @@ -31,7 +31,7 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => { db.close(); }); - it("should inject both recent memory and recalled long-term memory with subjective aliases resolved", () => { + it("should inject both Cognitive Buffer and recalled Memory Ledger entries with subjective aliases resolved", () => { const world = new WorldState( "world-123", new Date("2024-01-10T12:00:00.000Z"), @@ -60,7 +60,7 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => { }, }); - // 2. Populate ledger repository (long-term memory) + // 2. Populate ledger repository (Memory Ledger) ledgerRepo.save({ id: "ledger1", ownerId: "alice", @@ -76,12 +76,12 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => { const builder = new ActorPromptBuilder(bufferRepo, ledgerRepo, 20, 5); const { userContext } = builder.build(world, alice); - // Check recent memory exists - expect(userContext).toContain("=== RECENT EVENTS ==="); + // Check Cognitive Buffer exists + expect(userContext).toContain("=== COGNITIVE BUFFER ==="); expect(userContext).toContain("Alice greets Bob"); - // Check long-term memory exists - expect(userContext).toContain("=== YOUR MEMORIES ==="); + // Check Memory Ledger exists + expect(userContext).toContain("=== MEMORY LEDGER ==="); // Bob should be resolved to Strider in the ledger content expect(userContext).toContain("alice met Strider at the tavern."); expect(userContext).toContain('Quote: "I am a ranger."'); @@ -98,7 +98,7 @@ describe("ActorPromptBuilder with Long-Term Memory Integration", () => { const builder = new ActorPromptBuilder(bufferRepo, ledgerRepo, 20, 5); const { userContext } = builder.build(world, alice); - expect(userContext).toContain("=== RECENT EVENTS ==="); - expect(userContext).not.toContain("=== YOUR MEMORIES ==="); + expect(userContext).toContain("=== COGNITIVE BUFFER ==="); + expect(userContext).not.toContain("=== MEMORY LEDGER ==="); }); }); diff --git a/packages/memory/src/handoff.ts b/packages/memory/src/handoff.ts index 98e84e3..6908bce 100644 --- a/packages/memory/src/handoff.ts +++ b/packages/memory/src/handoff.ts @@ -33,7 +33,7 @@ export function getMemorySectionLength( now: Date, ): number { if (entries.length === 0) { - return `=== RECENT EVENTS ===\n(No recent events recorded.)`.length; + return `=== COGNITIVE BUFFER ===\n(No recent events recorded.)`.length; } const groupedLines: string[] = []; @@ -52,7 +52,7 @@ export function getMemorySectionLength( groupedLines.push(` - ${serialized}`); } - return `=== RECENT EVENTS ===\n${groupedLines.join("\n")}`.length; + return `=== COGNITIVE BUFFER ===\n${groupedLines.join("\n")}`.length; } function checkSceneExit(entity: Entity, bufferEntries: BufferEntry[]): boolean { @@ -227,7 +227,7 @@ export class HandoffEngine { .join("\n---\n"); const systemPrompt = ` -You are the memory Handoff Engine. Your task is to process a list of recent working memory buffer entries for an entity and select which memories to promote to the long-term Ledger, and which to forget or summarize. +You are the memory Handoff Engine. Your task is to process a list of Cognitive Buffer entries for an entity and select which memories to promote to the Memory Ledger, and which to forget or summarize. Instructions: 1. **Cluster** related consecutive buffer entries into high-level narrative beats or events (e.g. physical action and its outcome or trivial actions). Combine them into a single chunk. @@ -237,7 +237,7 @@ Instructions: 4. **Determine Importance**: Assign an importance score from 1 (trivial, e.g. waking up) to 10 (life-altering, e.g. witnessing a crime). 4. Discard small body movements like looking around, sighing, etc that do not contextually hold any meaning after it is done. 5. **Involved Entities**: Identify all entity IDs involved in the memories in this chunk. -6. **Retain in Buffer (Pinning)**: If a beat represents an unresolved high-stakes situation (e.g. a standing threat, an unanswered accusation, an ongoing chase or conflict), set "retainInBuffer" to true so it remains in the working memory buffer for immediate context. Otherwise, set it to false so it is safely pruned from the buffer. +6. **Retain in Cognitive Buffer (Pinning)**: If a beat represents an unresolved high-stakes situation (e.g. a standing threat, an unanswered accusation, an ongoing chase or conflict), set "retainInBuffer" to true so it remains in the Cognitive Buffer for immediate context. Otherwise, set it to false so it is safely pruned from the Cognitive Buffer. 7. **Exclude stage business**: Glances, sighs, ambient noticing, and irrelevant sensory details should be ignored and not included in any promoted chunk. They will be forgotten. 8. **Forget by omission**: Any buffer entry ID that you do not include in any chunk's "sourceEntryIds" will be permanently deleted and forgotten. `.trim(); diff --git a/web/docs/src/content/docs/architecture/actor.md b/web/docs/src/content/docs/architecture/actor.md index f292217..161cc25 100644 --- a/web/docs/src/content/docs/architecture/actor.md +++ b/web/docs/src/content/docs/architecture/actor.md @@ -7,7 +7,7 @@ The Actor Agent is the system component that embodies a single entity and produc ## Design Principles -1. **Epistemic boundedness** — The actor only sees what its entity would perceive: public attributes of other entities, private attributes explicitly ACL'd to it, its own memory buffer, and co-located entities. It does not have system-level access to all world state. +1. **Epistemic boundedness** — The actor only sees what its entity would perceive: public attributes of other entities, private attributes explicitly ACL'd to it, its own Cognitive Buffer, and co-located entities. It does not have system-level access to all world state. 2. **Proposal, not mutation** — The actor generates a _proposal_ (narrative prose). It never mutates world state, persists to the database, or writes to memory directly. Validation, execution, and persistence are the Architect's job. @@ -38,7 +38,7 @@ Epistemically bounded, with these sections: | ---------------------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------- | | Current moment | The subjective present time | `worldState.clock.get().toISOString()` | | The world as you perceive it | Self-visible attributes, co-located entities + their visible attributes, other presences elsewhere | `serializeSubjectiveWorldState()` | -| Your recent memory | Recent `BufferEntry`s, alias-substituted, with relative time phrasing | `serializeSubjectiveBufferEntry()` | +| Your cognitive buffer | Recent `BufferEntry`s, alias-substituted, with relative time phrasing | `serializeSubjectiveBufferEntry()` | No system UUIDs, no private attributes the entity lacks ACL access to, and no objective-world-state dump are present. diff --git a/web/docs/src/content/docs/architecture/handoff.md b/web/docs/src/content/docs/architecture/handoff.md index 3a6e11b..23db2d3 100644 --- a/web/docs/src/content/docs/architecture/handoff.md +++ b/web/docs/src/content/docs/architecture/handoff.md @@ -1,15 +1,15 @@ --- title: Memory Handoff Pipeline -description: The Tier 1 (Working Buffer) to Tier 2 (Long-Term Ledger) memory promotion architecture +description: The Cognitive Buffer to Memory Ledger promotion architecture sidebar: order: 6 --- :::tip[Status: Fully Implemented] -The handoff pipeline is fully integrated into the simulation step loop. The `HandoffEngine` and `checkHandoffTrigger` components process working memory entries, promoting them to the persistent episodic Ledger, while safely pruning the subjective working memory buffer. +The handoff pipeline is fully integrated into the simulation step loop. The `HandoffEngine` and `checkHandoffTrigger` components process Cognitive Buffer entries, promoting them to the persistent Memory Ledger, while safely pruning the Cognitive Buffer. ::: -The **Handoff** pipeline manages the promotion of subjective experiences from the short-term working memory (Tier 1 Buffer) to long-term episodic memory (Tier 2 Ledger). This process regulates context window utilization by deciding **when** promotion occurs, **how much** of the buffer is processed, and **what** details are preserved or pruned. +The **Handoff** pipeline manages the promotion of subjective experiences from the **Cognitive Buffer** to the **Memory Ledger**. This process regulates context window utilization by deciding **when** promotion occurs, **how much** of the Cognitive Buffer is processed, and **what** details are preserved or pruned. To maintain performance and prevent context bloat, the pipeline separates the process into three decoupled stages: @@ -46,7 +46,7 @@ Voluntary triggers identify natural narrative boundaries where the entity is ina Involuntary triggers enforce context constraints before building the next prompt to prevent token limit overflows: -- **Buffer Ceiling**: The serialized length of the working memory section (as generated by `getMemorySectionLength`) exceeds a configured fraction (default: 60%) of the provider's context window. +- **Buffer Ceiling**: The serialized length of the Cognitive Buffer section (as generated by `getMemorySectionLength`) exceeds a configured fraction (default: 60%) of the provider's context window. - **Event Velocity**: The number of buffer entries exceeds a safe threshold (default: 20), indicating an event-heavy scene generating history faster than voluntary boundaries occur. If the buffer is empty, trigger detection exits immediately with `"none"`. @@ -55,13 +55,13 @@ If the buffer is empty, trigger detection exits immediately with `"none"`. ## 2. Watermark Partitioning -To preserve short-term narrative continuity and avoid immediate memory decay, recent events are protected from handoff. The pipeline divides the active buffer into a protected **Watermark Tail** and a **Candidate Pool**. +To preserve narrative continuity and avoid immediate memory decay, recent events are protected from handoff. The pipeline divides the active Cognitive Buffer into a protected **Watermark Tail** and a **Candidate Pool**. The watermark boundary is computed as: $$\text{Watermark} = \max(K \text{ entries}, \text{entries bucketed as fresh by } \textit{naturalizeTime})$$ -- **Hard Floor ($K = 8$)**: The last 8 entries in the buffer are always preserved. +- **Hard Floor ($K = 8$)**: The last 8 entries in the Cognitive Buffer are always preserved. - **Temporal Freshness**: Any entries that `naturalizeTime` classifies within the immediate temporal horizon (`"just now"`, `"moments ago"`, `"a few minutes ago"`, or `"several minutes ago"`) are also protected. Entries older than the watermark boundary constitute the **Candidate Pool** and are eligible for handoff processing. @@ -100,8 +100,8 @@ The prompt instructs the model to apply the following cognitive operations: When a chunk represents an unresolved high-stakes situation (e.g., an active conflict or standing threat), the LLM sets `retainInBuffer: true`. -- The summary is committed to the long-term Ledger as normal. -- The source buffer entries are exempted from pruning, remaining in the working memory buffer with `pinned: true` until a future handoff pass determines the thread is resolved. +- The summary is committed to the Memory Ledger as normal. +- The source Cognitive Buffer entries are exempted from pruning, remaining in the Cognitive Buffer with `pinned: true` until a future handoff pass determines the thread is resolved. --- diff --git a/web/docs/src/content/docs/architecture/memory-tier2.md b/web/docs/src/content/docs/architecture/memory-tier2.md index f1ff716..2caeae4 100644 --- a/web/docs/src/content/docs/architecture/memory-tier2.md +++ b/web/docs/src/content/docs/architecture/memory-tier2.md @@ -1,15 +1,15 @@ --- -title: Tier 2 Memory (Long-Term Ledger) +title: Memory Ledger description: Persistent episodic memory storage, semantic indexing, and retrieval mechanics --- -**Tier 2 Memory (the Ledger)** represents an entity's long-term episodic memory. It archives historical summaries of past events, providing a persistent record of character experiences that can be retrieved and injected into LLM prompts as needed. +**Memory Ledger** represents an entity's persistent episodic memory. It archives historical summaries of past events, providing a durable record of character experiences that can be retrieved and injected into LLM prompts as needed. --- ## 1. Data Model -A long-term memory is represented by a `LedgerEntry`. It includes metadata for deterministic database-level filtering, structured narrative content, and vector embeddings for semantic similarity scoring. +A Memory Ledger entry is represented by a `LedgerEntry`. It includes metadata for deterministic database-level filtering, structured narrative content, and vector embeddings for semantic similarity scoring. ```ts interface LedgerEntry { @@ -30,7 +30,7 @@ interface LedgerEntry { ## 2. Storage Model -To support fast, low-latency queries across large historical datasets, Tier 2 memory is stored in standard SQLite tables. Vector embeddings are stored in raw binary format as `Float32Array` BLOBs. +To support fast, low-latency queries across large historical datasets, Memory Ledger entries are stored in standard SQLite tables. Vector embeddings are stored in raw binary format as `Float32Array` BLOBs. Standard secondary indices optimize query execution time to microseconds, eliminating the compilation and cross-platform installation overhead of native vector database extensions (such as `sqlite-vec` or `node-gyp` binaries). @@ -64,7 +64,7 @@ CREATE INDEX IF NOT EXISTS idx_ledger_involved_entity ON ledger_involved_entitie ## 3. The Handoff Pipeline -Working memory (Tier 1 Buffer) entries are promoted to the Ledger through the automated [Handoff Pipeline](./handoff). +**Cognitive Buffer** entries are promoted to the Memory Ledger through the automated [Handoff Pipeline](./handoff). During handoff: @@ -72,7 +72,7 @@ During handoff: 2. Ambient stage business and redundant details are pruned. 3. Chunks are synthesized into third-person summaries and assigned importance scores. 4. Text embeddings are generated for the summary. -5. The processed memories are committed to the SQLite store, and the short-term buffer is pruned. +5. The processed memories are committed to the SQLite store, and the Cognitive Buffer is pruned. --- @@ -116,13 +116,13 @@ Once the candidate pool is loaded, the ranking engine evaluates entries in appli ## 5. Active Focus & Attention Loop -In crowded settings (e.g., a room with many characters), retrieving long-term memories for all co-located entities would exhaust the prompt context window. To prevent this, retrieval uses an **Active Focus** selection strategy: +In crowded settings (e.g., a room with many characters), retrieving Memory Ledger entries for all co-located entities would exhaust the prompt context window. To prevent this, retrieval uses an **Active Focus** selection strategy: -- **Active Focus Set**: The prompt builder scans the last 10 entries of the character's working memory buffer. Any entity targeted by, spoken to, or mentioned in these entries is added to the Active Focus set. +- **Active Focus Set**: The prompt builder scans the last 10 entries of the character's Cognitive Buffer. Any entity targeted by, spoken to, or mentioned in these entries is added to the Active Focus set. - **Dynamic Capacity Limits**: - - If the number of co-located characters is small ($\le 3$), long-term memory is retrieved for all of them. - - If the environment is crowded ($> 3$), long-term retrieval is strictly restricted to the top 3 characters in the Active Focus set. -- This creates a realistic attention loop: when a new character interacts with the actor, they enter the working buffer, triggering the retrieval of their long-term history on the subsequent turn. + - If the number of co-located characters is small ($\le 3$), Memory Ledger entries are retrieved for all of them. + - If the environment is crowded ($> 3$), Memory Ledger retrieval is strictly restricted to the top 3 characters in the Active Focus set. +- This creates a realistic attention loop: when a new character interacts with the actor, they enter the Cognitive Buffer, triggering the retrieval of their Memory Ledger history on the subsequent turn. --- @@ -130,15 +130,15 @@ In crowded settings (e.g., a room with many characters), retrieving long-term me Retrieved ledger entries are formatted into the prompt chronologically and mapped to subjective aliases. Internal metrics (e.g., importance numbers and raw system IDs) are omitted to preserve immersion. -- Working memory entries are injected under `=== RECENT EVENTS ===` (narrated relative to the present moment). -- Recalled ledger entries are injected under `=== YOUR MEMORIES ===` (presented as long-term recollections). +- Cognitive Buffer entries are injected under `=== COGNITIVE BUFFER ===` (narrated relative to the present moment). +- Recalled Memory Ledger entries are injected under `=== MEMORY LEDGER ===` (presented as episodic recollections). ```text -=== RECENT EVENTS === +=== COGNITIVE BUFFER === Moments ago - You spoke to Strider: "Hello there" -=== YOUR MEMORIES === +=== MEMORY LEDGER === A couple days ago - You met a hooded figure named Strider at The Prancing Pony. Quote: "I can avoid being seen, if I wish, but to disappear entirely, that is a rare gift." From 1ed1edf4cf055b3ba1823f3aa01bb26dc92ea8f4 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 19 Jul 2026 07:03:03 +0530 Subject: [PATCH 03/15] refactor!(memory): Streamline memory terminology and architecture --- README.md | 2 +- apps/gui/src/components/play/PromptModal.tsx | 2 +- content/demo/scenarios/talking-room.json | 4 +- docs | 2 +- packages/actor/src/actor-prompt-builder.ts | 2 +- packages/architect/src/architect.ts | 2 +- packages/intent/src/intent.ts | 2 +- packages/memory/src/buffer.ts | 2 +- packages/memory/src/handoff.ts | 4 +- tests/integration/actor-monologue.test.ts | 2 +- web/docs/src/assets/img/mem1.png | Bin 0 -> 37162 bytes web/docs/src/assets/img/mem2.png | Bin 0 -> 86944 bytes .../src/content/docs/architecture/memory.md | 8 ++-- .../src/content/docs/architecture/overview.md | 2 +- .../content/docs/diary/2_Finalizing_memory.md | 42 ++++++++++++++++++ 15 files changed, 59 insertions(+), 17 deletions(-) create mode 100644 web/docs/src/assets/img/mem1.png create mode 100644 web/docs/src/assets/img/mem2.png create mode 100644 web/docs/src/content/docs/diary/2_Finalizing_memory.md diff --git a/README.md b/README.md index 048396f..cc33661 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ omnia/ intent/ intent types (dialogue/action/monologue) and the prose decoder architect/ World Architect: LLM validation plus time-delta generation actor/ actor agent: epistemically-bounded prompts, pluggable prose generators - memory/ verbatim buffer; later the vector archive, dossier, and affect vectors + memory/ Cognitive Buffer; Memory Ledger (vector archive), dossier, and affect vectors spatial/ location and POI graph, portal-based perception llm/ ILLMProvider interface plus Gemini and deterministic mock implementations scenario/ scenario JSON schema and loader (JSON → SQLite) diff --git a/apps/gui/src/components/play/PromptModal.tsx b/apps/gui/src/components/play/PromptModal.tsx index ea0cc90..eb46f93 100644 --- a/apps/gui/src/components/play/PromptModal.tsx +++ b/apps/gui/src/components/play/PromptModal.tsx @@ -61,7 +61,7 @@ export function PromptModal({ entry, onClose }: PromptModalProps) { { label: "Memory Ledger", type: "memories", - content: ledgerStr || "(No memory buffer entries.)", + content: ledgerStr || "(No memory ledger entries.)", }, ]; diff --git a/content/demo/scenarios/talking-room.json b/content/demo/scenarios/talking-room.json index bbec88d..8a65d53 100644 --- a/content/demo/scenarios/talking-room.json +++ b/content/demo/scenarios/talking-room.json @@ -72,7 +72,7 @@ ], "initialMemories": [ { - "id": "alpha-wake", + "id": "ab3f29d2-cf11-4111-9a99-b13c126d123e", "timestamp": "2026-07-09T07:58:00.000Z", "locationId": "white-room", "intent": { @@ -119,7 +119,7 @@ ], "initialMemories": [ { - "id": "beta-wake", + "id": "7c9b83b3-8cfb-4e89-8d77-626a5757d591", "timestamp": "2026-07-09T07:58:30.000Z", "locationId": "white-room", "intent": { diff --git a/docs b/docs index 51037cd..9f43cd9 120000 --- a/docs +++ b/docs @@ -1 +1 @@ -web/docs/src \ No newline at end of file +web/docs/src/content/docs \ No newline at end of file diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index 2bb252d..0387bee 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -139,7 +139,7 @@ Guidelines: if (!this.bufferRepo) return null; if (entries.length === 0) { - return `=== COGNITIVE BUFFER ===\n(No recent events recorded.)`; + return `=== COGNITIVE BUFFER ===\n(No entries recorded.)`; } const recent = entries.slice(-this.memoryLimit); diff --git a/packages/architect/src/architect.ts b/packages/architect/src/architect.ts index 56234fc..f5514cd 100644 --- a/packages/architect/src/architect.ts +++ b/packages/architect/src/architect.ts @@ -50,7 +50,7 @@ export class Architect { * "monologue" intents are internal thoughts — they bypass validation and * time-delta generation entirely: the clock does not advance, the world * state is not mutated or persisted. The caller is responsible for writing - * the monologue to the actor's memory buffer. + * the monologue to the actor's Cognitive Buffer. */ async processIntent( worldState: WorldState, diff --git a/packages/intent/src/intent.ts b/packages/intent/src/intent.ts index dc42974..705c67a 100644 --- a/packages/intent/src/intent.ts +++ b/packages/intent/src/intent.ts @@ -6,7 +6,7 @@ import { z } from "zod"; * - "action": A physical or logical action performed in the world. * - "monologue": An inner thought or internal monologue. Not perceivable by * any other entity. Bypasses the Architect/validators entirely and is - * written directly to the actor's memory buffer with no outcome. + * written directly to the actor's Cognitive Buffer with no outcome. */ export const IntentTypeSchema = z.enum(["dialogue", "action", "monologue"]); export type IntentType = z.infer; diff --git a/packages/memory/src/buffer.ts b/packages/memory/src/buffer.ts index 394cc56..733419b 100644 --- a/packages/memory/src/buffer.ts +++ b/packages/memory/src/buffer.ts @@ -4,7 +4,7 @@ import { Intent } from "@omnia/intent"; export interface BufferEntry { id: string; - ownerId: string; // Whose subjective memory buffer this lives in + ownerId: string; // Whose Cognitive Buffer this entry lives in timestamp: string; // WorldClock.get().toISOString() at write time locationId: string | null; // Actor's location when this happened diff --git a/packages/memory/src/handoff.ts b/packages/memory/src/handoff.ts index 6908bce..8eee51c 100644 --- a/packages/memory/src/handoff.ts +++ b/packages/memory/src/handoff.ts @@ -33,7 +33,7 @@ export function getMemorySectionLength( now: Date, ): number { if (entries.length === 0) { - return `=== COGNITIVE BUFFER ===\n(No recent events recorded.)`.length; + return `=== COGNITIVE BUFFER ===\n(No entries recorded.)`.length; } const groupedLines: string[] = []; @@ -246,7 +246,7 @@ Instructions: Subject Entity ID: ${entity.id} Current Time: ${now.toISOString()} -Working Memory Candidates for Handoff: +Cognitive Buffer Candidates for Handoff: ${candidatesList} `.trim(); diff --git a/tests/integration/actor-monologue.test.ts b/tests/integration/actor-monologue.test.ts index 4e877bd..9d32d0d 100644 --- a/tests/integration/actor-monologue.test.ts +++ b/tests/integration/actor-monologue.test.ts @@ -166,7 +166,7 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => { const expectedTime = new Date(startTime.getTime() + 3 * 60_000); expect(world.clock.get().toISOString()).toBe(expectedTime.toISOString()); - // 7. All three intents persisted to Alice's memory buffer. + // 7. All three intents persisted to Alice's Cognitive Buffer. const aliceMemory = bufferRepo.listForOwner("alice"); expect(aliceMemory).toHaveLength(3); expect(aliceMemory[0].intent.type).toBe("monologue"); diff --git a/web/docs/src/assets/img/mem1.png b/web/docs/src/assets/img/mem1.png new file mode 100644 index 0000000000000000000000000000000000000000..f5d037b929d0da5a10b4df1c0c0e90cb534ad3d2 GIT binary patch literal 37162 zcmY(qWmsI%&Nf^qP_zZ6P+W_$@KfT+}RM?meXZBVNb;k8b8i&aRl;$=_G{K zt+}MB>XwQByw49{t^!`zL_Ha;p89VQSPJ+l9povdB*W1EE%b)QuN2|PWXs+4%42K=lyZmQE$qPj z=5#$&f3{MrQ{7o!t*sP^m~sH=f`y7hgpykGD<7eyD(6Qrg-=p4%s5ewr{{%N(mpGb zdSeb;8=~&R1+`Nr62yc&jLx=x5jrJg*5=A}$?LE+RuSw)>=d$hj0!w7|(%D~j37Y#l(ezz_p76}Hlfkx}(MrM=BywruTp!#oKKN#nJ zBQ~)G+`F2;JB@WCpQsKz`^I!jZmZY#LEs&l4<&bI?Q3p9D1Y*wof*_<2WE$RL}iIp zs(~!vU7BESKI0r$mc{~{b8=J2O_tyzu)w=yJKH}Ppepf*G|jlo_3_qt_C117mP9uo zlt21vXq!OTbI0}bsqZEm7C!Ky-~I#H$+urTp4{}tYw?KMc3gT zxHUP6wr35uknXv8UD%eiHj_;)6mB58NzdAzDu(FydaMRJIpXa7QFt<;YdsAvxchaH3-z#8z5ZlAobT6_`R7h_8;SDNYweS7fNZbe|L!hnv% z-F@3VcSwCWcS9x0KCjh>lMcTd9g#<|?P#Izfl1RZimb`5x{+Q877P<~b0)t!GVf?4 zeKAhw3wtnMU0N6WkKmjE2X=#9L5(eOXRa4(pi`3PwL)CE2e0!D@%G1=0K6UIS$^xi zaOaq~Gl$8ugkRp&cCFduQw@c_?{7V3yZ8SZ?JL-5XnXc%Pj6FGRe#}hqpFY?9NUIN zJqe1|1rPHB_2~h*zLUE+cUxHkIM4~RZkBT;ewc~1 zO(AfCss(cOK;7ds(+8D^42Fw5HV^n=^8?cEU)|d7b!;Si$800P$1`f?ytPfNmtg9= zb@!XVgIbo<*9n1kY8p5)5Uz|%ZR;X}vn+HaqFnQlx1)@7{b(k0YIMK*!}JStjaBoq z*8B|Q=*µ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

BRO6xzp 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 Date: Sun, 19 Jul 2026 08:09:06 +0530 Subject: [PATCH 04/15] feat(intent): Add additional "thought" intent type --- apps/gui/src/components/play/InteractView.tsx | 1 + content/demo/scenarios/talking-room.json | 20 +++++++++++++++---- packages/actor/src/actor-prompt-builder.ts | 17 +++++++++++++--- .../actor/tests/actor-prompt-builder.test.ts | 2 +- packages/architect/src/architect.ts | 10 +++++----- packages/architect/src/delta.ts | 5 +++-- packages/architect/src/llm-validator.ts | 8 ++++---- packages/intent/src/intent-decoder.ts | 2 +- packages/intent/src/intent.ts | 10 ++++++++-- packages/memory/src/handoff.ts | 4 +++- packages/scenario/src/schema.ts | 2 +- packages/scenario/tests/talking-room.test.ts | 13 ++++++++---- 12 files changed, 66 insertions(+), 28 deletions(-) diff --git a/apps/gui/src/components/play/InteractView.tsx b/apps/gui/src/components/play/InteractView.tsx index 71dc0ff..e315940 100644 --- a/apps/gui/src/components/play/InteractView.tsx +++ b/apps/gui/src/components/play/InteractView.tsx @@ -23,6 +23,7 @@ function IntentTag({ }) { const labels: Record = { monologue: "thought", + thought: "thought", dialogue: "dialogue", action: "action", }; diff --git a/content/demo/scenarios/talking-room.json b/content/demo/scenarios/talking-room.json index 8a65d53..0e3c41f 100644 --- a/content/demo/scenarios/talking-room.json +++ b/content/demo/scenarios/talking-room.json @@ -73,7 +73,7 @@ "initialMemories": [ { "id": "ab3f29d2-cf11-4111-9a99-b13c126d123e", - "timestamp": "2026-07-09T07:58:00.000Z", + "timestamp": "2026-07-01T07:58:00.000Z", "locationId": "white-room", "intent": { "type": "monologue", @@ -82,6 +82,18 @@ "actorId": "7c9b83b3-8cfb-4e89-8d77-626a5757d591", "targetIds": [] } + }, + { + "id": "zz3f29d2-as11-9811-9a99-b13c126d123e", + "timestamp": "2026-07-01T09:58:00.000Z", + "locationId": "white-room", + "intent": { + "type": "action", + "originalText": "he wakes up from his sleep.", + "description": "", + "actorId": "bf3f29d2-cf11-4b11-9a99-b13c126d400e", + "targetIds": [] + } } ] }, @@ -119,12 +131,12 @@ ], "initialMemories": [ { - "id": "7c9b83b3-8cfb-4e89-8d77-626a5757d591", - "timestamp": "2026-07-09T07:58:30.000Z", + "id": "zx1f29d2-cf11-4111-9a99-b13c126d123e", + "timestamp": "2026-07-09T07:58:00.000Z", "locationId": "white-room", "intent": { "type": "action", - "originalText": "Why can't I remember anything before the research agreement. It's like my memory was erased.", + "originalText": "I wake up in an unfamiliar place.", "description": "", "actorId": "bf3f29d2-cf11-4b11-9a99-b13c126d400e", "targetIds": [] diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index 0387bee..8fbd530 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -112,7 +112,11 @@ Guidelines: } // --- Cognitive Buffer --- - const memorySection = this.buildMemorySection(entity, recentEntries, now); + const memorySection = this.buildCognitiveBufferSection( + entity, + recentEntries, + now, + ); if (memorySection) { sections.push(memorySection); } @@ -131,7 +135,7 @@ Guidelines: return sections.join("\n\n"); } - private buildMemorySection( + private buildCognitiveBufferSection( entity: Entity, entries: BufferEntry[], now: Date, @@ -147,9 +151,16 @@ Guidelines: let currentGroup: string | null = null; for (const entry of recent) { - const serialized = serializeSubjectiveBufferEntry(entry, entity); + let serialized = serializeSubjectiveBufferEntry(entry, entity); const when = naturalizeTime(now, new Date(entry.timestamp)); + if ( + entry.intent.actorId === entity.id && + entry.intent.type === "dialogue" + ) { + serialized = `You said: ${serialized}`; + } + if (when !== currentGroup) { currentGroup = when; const header = when.charAt(0).toUpperCase() + when.slice(1); diff --git a/packages/actor/tests/actor-prompt-builder.test.ts b/packages/actor/tests/actor-prompt-builder.test.ts index 757c980..772996f 100644 --- a/packages/actor/tests/actor-prompt-builder.test.ts +++ b/packages/actor/tests/actor-prompt-builder.test.ts @@ -78,7 +78,7 @@ describe("ActorPromptBuilder with Memory Ledger Integration", () => { // Check Cognitive Buffer exists expect(userContext).toContain("=== COGNITIVE BUFFER ==="); - expect(userContext).toContain("Alice greets Bob"); + expect(userContext).toContain("You said: Alice greets Bob"); // Check Memory Ledger exists expect(userContext).toContain("=== MEMORY LEDGER ==="); diff --git a/packages/architect/src/architect.ts b/packages/architect/src/architect.ts index f5514cd..5de9945 100644 --- a/packages/architect/src/architect.ts +++ b/packages/architect/src/architect.ts @@ -47,22 +47,22 @@ export class Architect { * Processes, validates, generates deltas, applies them to the world state, * and persists the changes to the database. * - * "monologue" intents are internal thoughts — they bypass validation and + * "monologue" and "thought" intents are internal thoughts — they bypass validation and * time-delta generation entirely: the clock does not advance, the world * state is not mutated or persisted. The caller is responsible for writing - * the monologue to the actor's Cognitive Buffer. + * the monologue/thought to the actor's Cognitive Buffer. */ async processIntent( worldState: WorldState, intent: Intent, ): Promise { - // 0. Monologue intents are purely internal — short-circuit before any + // 0. Monologue/thought intents are purely internal — short-circuit before any // validation or world mutation. - if (intent.type === "monologue") { + if (intent.type === "monologue" || intent.type === "thought") { return { isValid: true, reason: - "Monologue intent bypasses validation (internal thought, not perceivable).", + "Monologue/thought intent bypasses validation (internal thought, not perceivable).", timeDelta: { minutesToAdvance: 0, explanation: "Internal thought — no time elapsed.", diff --git a/packages/architect/src/delta.ts b/packages/architect/src/delta.ts index bb6666e..3b180e5 100644 --- a/packages/architect/src/delta.ts +++ b/packages/architect/src/delta.ts @@ -26,10 +26,11 @@ export class TimeDeltaGenerator implements IDeltaGenerator { explanation: "Dialogue action; 1 minute granted for quick exchange.", }; } - if (intent.type === "monologue") { + if (intent.type === "monologue" || intent.type === "thought") { return { minutesToAdvance: 0, - explanation: "Monologue action; no time advanced for internal thought.", + explanation: + "Monologue/thought action; no time advanced for internal thought.", }; } diff --git a/packages/architect/src/llm-validator.ts b/packages/architect/src/llm-validator.ts index 0262d54..3e92515 100644 --- a/packages/architect/src/llm-validator.ts +++ b/packages/architect/src/llm-validator.ts @@ -16,7 +16,7 @@ export class LLMValidator { /** * Validates an action intent against the objective world state. * - * "monologue" intents must never reach this validator — they are internal + * "monologue" and "thought" intents must never reach this validator — they are internal * thoughts that bypass validation entirely (see Architect.processIntent). * This guard exists as a defensive safeguard. */ @@ -24,12 +24,12 @@ export class LLMValidator { worldState: WorldState, intent: Intent, ): Promise { - // Defensive guard: monologue intents bypass validation. - if (intent.type === "monologue") { + // Defensive guard: monologue and thought intents bypass validation. + if (intent.type === "monologue" || intent.type === "thought") { return { isValid: true, reason: - "Monologue intents are internal thoughts and bypass validation.", + "Monologue/thought intents are internal thoughts and bypass validation.", }; } diff --git a/packages/intent/src/intent-decoder.ts b/packages/intent/src/intent-decoder.ts index 4c73b5c..739580f 100644 --- a/packages/intent/src/intent-decoder.ts +++ b/packages/intent/src/intent-decoder.ts @@ -41,7 +41,7 @@ For each intent you must: 1. Classify its type: - "dialogue": if actor speaking, talking, whispering, murmuring, etc - "action": Any physical or logical action performed in the world (e.g., moving, opening, looking). - - "monologue": An inner thought, reflection, or internal monologue/self narration. + - "monologue" (or "thought"): An inner thought, reflection, or internal monologue/self narration. 2. Extract the original text fragment from the prose that corresponds to this intent. 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") diff --git a/packages/intent/src/intent.ts b/packages/intent/src/intent.ts index 705c67a..99615f2 100644 --- a/packages/intent/src/intent.ts +++ b/packages/intent/src/intent.ts @@ -7,8 +7,14 @@ import { z } from "zod"; * - "monologue": An inner thought or internal monologue. Not perceivable by * any other entity. Bypasses the Architect/validators entirely and is * written directly to the actor's Cognitive Buffer with no outcome. + * - "thought": Equivalent/alias to "monologue". */ -export const IntentTypeSchema = z.enum(["dialogue", "action", "monologue"]); +export const IntentTypeSchema = z.enum([ + "dialogue", + "action", + "monologue", + "thought", +]); export type IntentType = z.infer; /** @@ -30,7 +36,7 @@ export const LLMIntentSchema = z.object({ /** * Entity IDs of the receiving parties (e.g., who is being spoken to, * what object is being interacted with). Always an empty array for - * "monologue" intents, since they are not perceivable by anyone. + * "monologue" and "thought" intents, since they are not perceivable by anyone. */ targetIds: z.array(z.string()), diff --git a/packages/memory/src/handoff.ts b/packages/memory/src/handoff.ts index 8eee51c..78a3172 100644 --- a/packages/memory/src/handoff.ts +++ b/packages/memory/src/handoff.ts @@ -85,7 +85,9 @@ function checkIdleDecay(bufferEntries: BufferEntry[]): boolean { // Check the last N entries const lastN = bufferEntries.slice(-N); - return lastN.every((e) => e.intent.type === "monologue"); + return lastN.every( + (e) => e.intent.type === "monologue" || e.intent.type === "thought", + ); } function checkAttributeTrigger(entity: Entity): boolean { diff --git a/packages/scenario/src/schema.ts b/packages/scenario/src/schema.ts index bbfe4ec..7215ed0 100644 --- a/packages/scenario/src/schema.ts +++ b/packages/scenario/src/schema.ts @@ -30,7 +30,7 @@ export const ScenarioMemoryEntrySchema = z.object({ timestamp: z.string(), // ISO string locationId: z.string().nullable(), intent: z.object({ - type: z.enum(["dialogue", "action", "monologue"]), + type: z.enum(["dialogue", "action", "monologue", "thought"]), originalText: z.string(), description: z.string(), selfDescription: z.string().optional(), diff --git a/packages/scenario/tests/talking-room.test.ts b/packages/scenario/tests/talking-room.test.ts index 9226fb0..f262a14 100644 --- a/packages/scenario/tests/talking-room.test.ts +++ b/packages/scenario/tests/talking-room.test.ts @@ -128,17 +128,22 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => { // 7. Assert initial pre-seeded memories const alphaMemories = bufferRepo.listForOwner(alphaId); - expect(alphaMemories).toHaveLength(1); - expect(alphaMemories[0].id).toBe("alpha-wake"); + expect(alphaMemories).toHaveLength(2); + expect(alphaMemories[0].id).toBe("ab3f29d2-cf11-4111-9a99-b13c126d123e"); expect(alphaMemories[0].intent.type).toBe("monologue"); expect(alphaMemories[0].intent.originalText).toContain("jail"); expect(alphaMemories[0].intent.description).toBe(""); + expect(alphaMemories[1].id).toBe("zz3f29d2-as11-9811-9a99-b13c126d123e"); + expect(alphaMemories[1].intent.type).toBe("action"); + expect(alphaMemories[1].intent.originalText).toContain("sleep"); + expect(alphaMemories[1].intent.description).toBe(""); + const betaMemories = bufferRepo.listForOwner(betaId); expect(betaMemories).toHaveLength(1); - expect(betaMemories[0].id).toBe("beta-wake"); + expect(betaMemories[0].id).toBe("zx1f29d2-cf11-4111-9a99-b13c126d123e"); expect(betaMemories[0].intent.type).toBe("action"); - expect(betaMemories[0].intent.originalText).toContain("agreement"); + expect(betaMemories[0].intent.originalText).toContain("unfamiliar"); expect(betaMemories[0].intent.description).toBe(""); db.close(); From 84bff926315ead2719b9a4b4b91df4c6517377f3 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 19 Jul 2026 11:01:06 +0530 Subject: [PATCH 05/15] refactor(intent): Improve intent decoder userContext structure --- content/demo/scenarios/talking-room.json | 2 +- package.json | 1 + packages/actor/src/actor-prompt-builder.ts | 4 +- packages/actor/src/actor.ts | 11 ++- packages/intent/src/intent-decoder.ts | 101 ++++++++++----------- pnpm-lock.yaml | 42 +++++++++ 6 files changed, 102 insertions(+), 59 deletions(-) diff --git a/content/demo/scenarios/talking-room.json b/content/demo/scenarios/talking-room.json index 0e3c41f..7c6f386 100644 --- a/content/demo/scenarios/talking-room.json +++ b/content/demo/scenarios/talking-room.json @@ -85,7 +85,7 @@ }, { "id": "zz3f29d2-as11-9811-9a99-b13c126d123e", - "timestamp": "2026-07-01T09:58:00.000Z", + "timestamp": "2026-07-09T07:58:00.000Z", "locationId": "white-room", "intent": { "type": "action", diff --git a/package.json b/package.json index 15ec7a7..21f3dac 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@langchain/openai": "^0.3.17", "@langchain/openrouter": "^0.4.3", "@types/node": "^20.19.43", + "compromise": "^14.16.0", "dotenv": "^17.4.2" } } diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index 8fbd530..190949b 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -76,13 +76,13 @@ Your output is a short block of narrative prose describing what your character d Guidelines: - Always write in the first person -- Only describe your character's own actions, spoken words, and internal reactions. Do NOT narrate or describe the environment or your surroundings, or other characters' actions. - Refer to other entities by the subjective names/aliases that you refer to them as. - Keep your prose vivid but concise. Write it in natural narrative order. - Not every response requires an outward action. It is perfectly valid to only think (a monologue) and do nothing perceivable. - Never speak or act on another entity's behalf. You only control your own character. - Stay strictly within what your character knows. Do not invent knowledge that doesn't exist or act on it. -- You are limited by just your memory. If your memory is limited, then that's all you can remember. If you do make stuff up then that's lying. Which is allowed, but remember that you're lying. +- You are limited by just your memory. If your memory is limited, then that's all you can remember. If you do make stuff up then that's lying. Which is allowed, but remember that you're lying +- Only describe your character's own actions, spoken words, and internal reactions. Do NOT narrate the environment or your surroundings, or other characters' actions. ". `.trim(); } diff --git a/packages/actor/src/actor.ts b/packages/actor/src/actor.ts index a7528b7..af05830 100644 --- a/packages/actor/src/actor.ts +++ b/packages/actor/src/actor.ts @@ -76,7 +76,7 @@ export class ActorAgent { constructor( llmProvider: ILLMProvider | { actor: ILLMProvider; decoder: ILLMProvider }, - bufferRepo?: BufferRepository, + private bufferRepo?: BufferRepository, ledgerRepo?: LedgerRepository, memoryLimit?: number, generator?: IActorProseGenerator, @@ -127,10 +127,19 @@ export class ActorAgent { userContext, ); + const recentEntries = this.bufferRepo + ? this.bufferRepo.listForOwner(entity.id) + : []; + const recentIntents = recentEntries + .filter((e) => e.intent.actorId !== entity.id) + .slice(-3) + .map((e) => e.intent); + const intents = await this.decoder.decode( worldState, entity.id, narrativeProse, + recentIntents, ); return { diff --git a/packages/intent/src/intent-decoder.ts b/packages/intent/src/intent-decoder.ts index 739580f..ed3584b 100644 --- a/packages/intent/src/intent-decoder.ts +++ b/packages/intent/src/intent-decoder.ts @@ -1,6 +1,6 @@ -import { WorldState } from "@omnia/core"; +import { WorldState, resolveAlias } from "@omnia/core"; import { ILLMProvider } from "@omnia/llm"; -import { IntentSequence, LLMIntentSequenceSchema } from "./intent.js"; +import { Intent, IntentSequence, LLMIntentSequenceSchema } from "./intent.js"; export class IntentDecoder { constructor(private llmProvider: ILLMProvider) {} @@ -18,20 +18,47 @@ export class IntentDecoder { worldState: WorldState, actorId: string, narrativeProse: string, + recentIntents: Intent[] = [], ): Promise { - const entityIds = Array.from(worldState.entities.keys()); const actor = worldState.getEntity(actorId); - const aliasEntries = actor ? Array.from(actor.aliases.entries()) : []; - const aliasContext = - aliasEntries.length > 0 - ? aliasEntries - .map( - ([targetId, alias]) => - `- "${alias}" refers to entity ID: "${targetId}"`, - ) - .join("\n") - : "(No known aliases)"; + // 1. Get other entities co-located in the same context + const otherEntitiesLines: string[] = []; + for (const otherEntity of worldState.entities.values()) { + if ( + otherEntity.id !== actorId && + otherEntity.locationId === actor?.locationId + ) { + const alias = actor + ? resolveAlias(actor, otherEntity.id) + : otherEntity.id; + otherEntitiesLines.push(` - Alias="${alias}" ID=${otherEntity.id}`); + } + } + const otherEntitiesContext = + otherEntitiesLines.length > 0 + ? otherEntitiesLines.join("\n") + : " (No other entities in context)"; + + // 2. Format historical context (2-3 recent intents received by the actor) + const historicalLines: string[] = []; + for (const prior of recentIntents) { + const targetIds = + prior.actorId !== actorId ? [prior.actorId] : prior.targetIds; + const targetsStr = targetIds + .map((tid) => { + const alias = actor ? resolveAlias(actor, tid) : tid; + return `(Alias="${alias}", ID="${tid}")`; + }) + .join(", "); + historicalLines.push( + ` - Content: "${prior.originalText}", Type: ${prior.type}, Target Entities: ${targetsStr || "None"}`, + ); + } + const historicalContext = + historicalLines.length > 0 + ? historicalLines.join("\n") + : " (No prior intents in context)"; const systemPrompt = ` You are the Intent Decoder for a narrative simulation engine. @@ -47,23 +74,16 @@ For each intent you must: - "description": No subject or name — a bare third-person verb phrase only (e.g. "clears their throat", "shakes their head slowly") - "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. - In case of a dialogue, the description and self Description only stores the exact words said by the entity. (e.g. "I will do that later", "Are you serious right now?") -4. Identify targetIds — the entity IDs of the receiving parties. Use the "KNOWN ENTITY IDS" mapping to resolve any subjective names,or aliases used in the prose to their correct system entity IDs. If no specific target, use an empty array. +4. Identify targetIds — the entity IDs of the receiving parties. Use the "Other entities in context" list to resolve any subjective names, aliases, or descriptions used in the prose to their correct entity IDs. If no specific target, use an empty array. 5. Identify modifiers — a list of strings representing additional qualities or modifiers extracted from the narrative prose. This includes emotions, tone of voice, speed, manner of action, or statement type (e.g., "question", "anxious", "whispering", "slowly", "quietly", "forcefully"). If no modifiers are present, use an empty array. `.trim(); const userContext = ` -=== KNOWN ENTITY IDS === -${entityIds.length > 0 ? entityIds.join(", ") : "(No entities)"} - -=== ACTOR ALIASES === -The actor refers to other entities using these subjective names/aliases: -${aliasContext} - -=== WORLD STATE === -${serializeSimplifiedWorldState(worldState)} - -=== ACTOR === -Actor ID: ${actorId} +Intent Source: ${actorId} +Other entities in context: +${otherEntitiesContext} +Historical Context: +${historicalContext} === NARRATIVE PROSE === ${narrativeProse} @@ -91,32 +111,3 @@ ${narrativeProse} }; } } - -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/pnpm-lock.yaml b/pnpm-lock.yaml index 17b33d7..802931d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,6 +284,9 @@ importers: "@types/node": specifier: ^20.19.43 version: 20.19.43 + compromise: + specifier: ^14.16.0 + version: 14.16.0 dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -4716,6 +4719,13 @@ packages: } engines: { node: ">= 18" } + compromise@14.16.0: + resolution: + { + integrity: sha512-4DFYl/Hl7sW4XWUDfx9S5vxqyYKpZDwwqrpXsQv5acdbVP+joKceIcIaLb0lhVWUpDBV0OnExk/o/dnYUwXnhQ==, + } + engines: { node: ">=12.0.0" } + conf@10.2.0: resolution: { @@ -5388,6 +5398,13 @@ packages: integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, } + efrt@2.7.0: + resolution: + { + integrity: sha512-/RInbCy1d4P6Zdfa+TMVsf/ufZVotat5hCw3QXmWtjU+3pFEOvOQ7ibo3aIxyCJw2leIeAMjmPj+1SLJiCpdrQ==, + } + engines: { node: ">=12.0.0" } + electron-to-chromium@1.5.389: resolution: { @@ -6114,6 +6131,13 @@ packages: integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, } + grad-school@0.0.5: + resolution: + { + integrity: sha512-rXunEHF9M9EkMydTBux7+IryYXEZinRk6g8OBOGDBzo/qWJjhTxy86i5q7lQYpCLHN8Sqv1XX3OIOc7ka2gtvQ==, + } + engines: { node: ">=8.0.0" } + groq-sdk@1.3.0: resolution: { @@ -8914,6 +8938,12 @@ packages: integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==, } + suffix-thumb@5.0.2: + resolution: + { + integrity: sha512-I5PWXAFKx3FYnI9a+dQMWNqTxoRt6vdBdb0O+BJ1sxXCWtSoQCusc13E58f+9p4MYx/qCnEMkD5jac6K2j3dgA==, + } + supports-color@10.2.2: resolution: { @@ -12520,6 +12550,12 @@ snapshots: common-ancestor-path@2.0.0: {} + compromise@14.16.0: + dependencies: + efrt: 2.7.0 + grad-school: 0.0.5 + suffix-thumb: 5.0.2 + conf@10.2.0: dependencies: ajv: 8.20.0 @@ -12898,6 +12934,8 @@ snapshots: ee-first@1.1.1: {} + efrt@2.7.0: {} + electron-to-chromium@1.5.389: {} emoji-regex@10.6.0: {} @@ -13379,6 +13417,8 @@ snapshots: graceful-fs@4.2.11: {} + grad-school@0.0.5: {} + groq-sdk@1.3.0: {} h3@1.15.11: @@ -15488,6 +15528,8 @@ snapshots: stylis@4.4.0: {} + suffix-thumb@5.0.2: {} + supports-color@10.2.2: {} svgo@4.0.1: From a4b620502a0f192083734b84d70ecb462e6a97d2 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 19 Jul 2026 13:13:07 +0530 Subject: [PATCH 06/15] FEAT!(voice): Implement intent hydration, dehydration system fixes: #29 --- apps/gui/package.json | 1 + apps/gui/src/components/play/InteractView.tsx | 33 ++- apps/gui/src/lib/simulation-types.ts | 4 +- .../src/lib/simulation/simulation-manager.ts | 17 +- apps/gui/src/lib/simulation/turn-executor.ts | 3 +- content/demo/scenarios/talking-room.json | 9 +- packages/actor/package.json | 1 + packages/actor/src/actor-prompt-builder.ts | 10 +- .../actor/tests/actor-prompt-builder.test.ts | 12 +- packages/actor/tsconfig.json | 3 +- packages/architect/package.json | 1 + packages/architect/src/delta.ts | 6 +- packages/architect/src/llm-validator.ts | 6 +- packages/architect/tests/architect.test.ts | 29 +-- packages/architect/tsconfig.json | 3 +- packages/intent/package.json | 1 + packages/intent/src/intent-decoder.ts | 43 ++-- packages/intent/src/intent.ts | 10 +- packages/intent/tests/intent.test.ts | 25 +- packages/intent/tsconfig.json | 6 +- packages/llm/tests/openrouter.test.ts | 4 + packages/memory/package.json | 1 + packages/memory/src/buffer.ts | 27 +- packages/memory/tests/handoff.test.ts | 27 +- packages/memory/tests/memory.test.ts | 40 +-- packages/memory/tsconfig.json | 3 +- packages/scenario/src/loader.ts | 10 +- packages/scenario/src/schema.ts | 5 +- packages/scenario/tests/scenario.test.ts | 2 +- packages/scenario/tests/talking-room.test.ts | 9 +- packages/voice/package.json | 13 + packages/voice/src/dehydration.ts | 146 +++++++++++ packages/voice/src/hydration.ts | 236 ++++++++++++++++++ packages/voice/src/index.ts | 2 + packages/voice/tsconfig.json | 9 + pnpm-lock.yaml | 24 ++ tests/integration/actor-monologue.test.ts | 17 +- tests/integration/game-loop.test.ts | 35 +-- 38 files changed, 622 insertions(+), 211 deletions(-) create mode 100644 packages/voice/package.json create mode 100644 packages/voice/src/dehydration.ts create mode 100644 packages/voice/src/hydration.ts create mode 100644 packages/voice/src/index.ts create mode 100644 packages/voice/tsconfig.json diff --git a/apps/gui/package.json b/apps/gui/package.json index bfe39ce..69f8a44 100644 --- a/apps/gui/package.json +++ b/apps/gui/package.json @@ -19,6 +19,7 @@ "@omnia/memory": "workspace:*", "@omnia/scenario": "workspace:*", "@omnia/spatial": "workspace:*", + "@omnia/voice": "workspace:*", "@radix-ui/react-dialog": "^1.1.19", "@radix-ui/react-separator": "^1.1.11", "@radix-ui/react-slot": "^1.3.0", diff --git a/apps/gui/src/components/play/InteractView.tsx b/apps/gui/src/components/play/InteractView.tsx index e315940..12c9024 100644 --- a/apps/gui/src/components/play/InteractView.tsx +++ b/apps/gui/src/components/play/InteractView.tsx @@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Spinner } from "@/components/ui/spinner"; import { cn } from "@/lib/utils"; +import { hydrate } from "@omnia/voice"; import { Alert, AlertAction, @@ -17,9 +18,13 @@ import { function IntentTag({ intent, isSelf, + playerAliases, + playerId, }: { intent: SimSnapshot["log"][number]["intents"][number]; isSelf?: boolean; + playerAliases: Record; + playerId: string; }) { const labels: Record = { monologue: "thought", @@ -35,10 +40,13 @@ function IntentTag({ outcome = intent.isValid ? " ✅" : ` ❌ (${intent.reason})`; } - const textToDisplay = - isSelf && intent.selfDescription - ? intent.selfDescription - : intent.description; + const viewerAliasesMap = new Map(Object.entries(playerAliases || {})); + const viewerEntityMock = { + id: playerId || "", + aliases: viewerAliasesMap, + }; + + const textToDisplay = hydrate(intent.content, viewerEntityMock as any); const modifiersStr = intent.modifiers && intent.modifiers.length > 0 ? ( @@ -76,10 +84,14 @@ function LogEntryCard({ entry, onShowPrompt, isPlayerCard, + playerAliases, + playerId, }: { entry: SimSnapshot["log"][number]; onShowPrompt: (entry: SimSnapshot["log"][number]) => void; isPlayerCard: boolean; + playerAliases: Record; + playerId: string; }) { const showMenu = !!(entry.rawPrompt || entry.decoderPrompt); @@ -117,7 +129,13 @@ function LogEntryCard({

{entry.intents.map((intent, i) => ( - + ))}
@@ -184,12 +202,17 @@ export function InteractView({ ); } + const playerAliases = playerEntity?.aliases || {}; + const playerId = playerEntity?.id || ""; + return ( ); })} diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index e2b6c72..3f201e0 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -1,7 +1,6 @@ export interface IntentInfo { type: string; - description: string; - selfDescription?: string; + content: string; modifiers: string[]; targetIds: string[]; isValid?: boolean; @@ -49,6 +48,7 @@ export interface EntityInfo { name: string; isPlayer: boolean; isAgent: boolean; + aliases?: Record; } export interface WaitingContext { diff --git a/apps/gui/src/lib/simulation/simulation-manager.ts b/apps/gui/src/lib/simulation/simulation-manager.ts index a3f9c96..4400075 100644 --- a/apps/gui/src/lib/simulation/simulation-manager.ts +++ b/apps/gui/src/lib/simulation/simulation-manager.ts @@ -468,6 +468,21 @@ export class SimulationManager { // --------------------------------------------------------------------------- private snapshot(session: SimSession): SimSnapshot { + const worldState = session.coreRepo.loadWorldState(session.worldInstanceId); + const hydratedEntities = session.entities.map((e) => { + const actualEntity = worldState?.getEntity(e.id); + const aliases: Record = {}; + if (actualEntity) { + for (const [targetId, alias] of actualEntity.aliases.entries()) { + aliases[targetId] = alias; + } + } + return { + ...e, + aliases, + }; + }); + return { id: session.worldInstanceId, status: session.status, @@ -475,7 +490,7 @@ export class SimulationManager { maxTurns: session.maxTurns, scenarioName: session.scenarioName, scenarioDescription: session.scenarioDescription, - entities: session.entities, + entities: hydratedEntities, log: session.log, entityIndex: session.entityIndex, waitingEntity: session.waitingEntity, diff --git a/apps/gui/src/lib/simulation/turn-executor.ts b/apps/gui/src/lib/simulation/turn-executor.ts index 825ffc0..57f6445 100644 --- a/apps/gui/src/lib/simulation/turn-executor.ts +++ b/apps/gui/src/lib/simulation/turn-executor.ts @@ -58,8 +58,7 @@ async function processIntents( intentInfos.push({ type: intent.type, - description: intent.description, - selfDescription: intent.selfDescription, + content: intent.content, modifiers: intent.modifiers || [], targetIds: intent.targetIds, isValid: outcome.isValid, diff --git a/content/demo/scenarios/talking-room.json b/content/demo/scenarios/talking-room.json index 7c6f386..7615008 100644 --- a/content/demo/scenarios/talking-room.json +++ b/content/demo/scenarios/talking-room.json @@ -77,8 +77,7 @@ "locationId": "white-room", "intent": { "type": "monologue", - "originalText": "I didn't have a choice. I would have been sent to jail if I hadn't agreed to do this experiement.", - "description": "", + "content": "I didn't have a choice. I would have been sent to jail if I hadn't agreed to do this experiment.", "actorId": "7c9b83b3-8cfb-4e89-8d77-626a5757d591", "targetIds": [] } @@ -89,8 +88,7 @@ "locationId": "white-room", "intent": { "type": "action", - "originalText": "he wakes up from his sleep.", - "description": "", + "content": "entity@bf3f29d2-cf11-4b11-9a99-b13c126d400e[I] wake up from my sleep.", "actorId": "bf3f29d2-cf11-4b11-9a99-b13c126d400e", "targetIds": [] } @@ -136,8 +134,7 @@ "locationId": "white-room", "intent": { "type": "action", - "originalText": "I wake up in an unfamiliar place.", - "description": "", + "content": "entity@bf3f29d2-cf11-4b11-9a99-b13c126d400e[I] wake up in an unfamiliar place.", "actorId": "bf3f29d2-cf11-4b11-9a99-b13c126d400e", "targetIds": [] } diff --git a/packages/actor/package.json b/packages/actor/package.json index 1a8c11c..761dc1b 100644 --- a/packages/actor/package.json +++ b/packages/actor/package.json @@ -11,6 +11,7 @@ "@omnia/intent": "workspace:*", "@omnia/llm": "workspace:*", "@omnia/memory": "workspace:*", + "@omnia/voice": "workspace:*", "zod": "^4.4.3" } } diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index 190949b..e943794 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -13,6 +13,7 @@ import { LedgerEntry, LedgerRepository, } from "@omnia/memory"; +import { hydrate } from "@omnia/voice"; /** * Zod schema for the structured response expected from the actor LLM. @@ -158,7 +159,7 @@ Guidelines: entry.intent.actorId === entity.id && entry.intent.type === "dialogue" ) { - serialized = `You said: ${serialized}`; + serialized = `I said: ${serialized}`; } if (when !== currentGroup) { @@ -250,12 +251,7 @@ Guidelines: for (const entry of recalled) { const when = naturalizeTime(now, new Date(entry.timestamp)); - let content = entry.content; - // Resolve system IDs to subjective aliases in the content - for (const targetId of entry.involvedEntityIds) { - const alias = resolveAlias(entity, targetId); - content = content.replace(new RegExp(targetId, "g"), alias); - } + let content = hydrate(entry.content, entity); if (entry.locationId) { content += ` (at ${entry.locationId})`; } diff --git a/packages/actor/tests/actor-prompt-builder.test.ts b/packages/actor/tests/actor-prompt-builder.test.ts index 772996f..437a250 100644 --- a/packages/actor/tests/actor-prompt-builder.test.ts +++ b/packages/actor/tests/actor-prompt-builder.test.ts @@ -55,8 +55,8 @@ describe("ActorPromptBuilder with Memory Ledger Integration", () => { type: "dialogue", actorId: "alice", targetIds: ["bob"], - originalText: "Hello there", - description: "Alice greets Bob", + content: "entity@alice[I] say 'Hello there' to entity@bob[Bob]", + modifiers: [], }, }); @@ -67,7 +67,7 @@ describe("ActorPromptBuilder with Memory Ledger Integration", () => { timestamp: "2024-01-08T12:00:00.000Z", // 2 days ago locationId: "tavern", involvedEntityIds: ["bob"], - content: "alice met bob at the tavern.", + content: "entity@alice[Alice] met entity@bob[bob] at the tavern.", quotes: ["I am a ranger."], importance: 9, embedding: [], @@ -78,12 +78,12 @@ describe("ActorPromptBuilder with Memory Ledger Integration", () => { // Check Cognitive Buffer exists expect(userContext).toContain("=== COGNITIVE BUFFER ==="); - expect(userContext).toContain("You said: Alice greets Bob"); + expect(userContext).toContain("I said: I say 'Hello there' to Strider"); // Check Memory Ledger exists expect(userContext).toContain("=== MEMORY LEDGER ==="); - // Bob should be resolved to Strider in the ledger content - expect(userContext).toContain("alice met Strider at the tavern."); + // Bob should be resolved to Strider, and alice to I in the ledger content + expect(userContext).toContain("I met Strider at the tavern."); expect(userContext).toContain('Quote: "I am a ranger."'); }); diff --git a/packages/actor/tsconfig.json b/packages/actor/tsconfig.json index 2ac04d2..7dd4d9a 100644 --- a/packages/actor/tsconfig.json +++ b/packages/actor/tsconfig.json @@ -9,6 +9,7 @@ { "path": "../core" }, { "path": "../intent" }, { "path": "../llm" }, - { "path": "../memory" } + { "path": "../memory" }, + { "path": "../voice" } ] } diff --git a/packages/architect/package.json b/packages/architect/package.json index 4b815a5..2bf2e71 100644 --- a/packages/architect/package.json +++ b/packages/architect/package.json @@ -10,6 +10,7 @@ "@omnia/core": "workspace:*", "@omnia/llm": "workspace:*", "@omnia/intent": "workspace:*", + "@omnia/voice": "workspace:*", "zod": "^4.4.3" } } diff --git a/packages/architect/src/delta.ts b/packages/architect/src/delta.ts index 3b180e5..7339e60 100644 --- a/packages/architect/src/delta.ts +++ b/packages/architect/src/delta.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { WorldState, serializeObjectiveWorldState } from "@omnia/core"; import { ILLMProvider } from "@omnia/llm"; import { Intent } from "@omnia/intent"; +import { hydrateObjective } from "@omnia/voice"; export const TimeDeltaSchema = z.object({ minutesToAdvance: z.number().int().nonnegative(), @@ -46,6 +47,8 @@ Return a structured JSON object containing: - "explanation": a brief explanation of why this amount of time is appropriate. `.trim(); + const objectiveContent = hydrateObjective(intent.content, worldState); + const userContext = ` === CURRENT WORLD STATE === Current Time: ${worldState.clock.get().toISOString()} @@ -55,8 +58,7 @@ ${serializeObjectiveWorldState(worldState)} === ACTION === Actor ID: ${intent.actorId} Type: ${intent.type} -Description: "${intent.description}" -Original Text: "${intent.originalText}" +Content: "${objectiveContent}" Target IDs: ${intent.targetIds.join(", ") || "(None)"} `.trim(); diff --git a/packages/architect/src/llm-validator.ts b/packages/architect/src/llm-validator.ts index 3e92515..93cb572 100644 --- a/packages/architect/src/llm-validator.ts +++ b/packages/architect/src/llm-validator.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { WorldState, serializeObjectiveWorldState } from "@omnia/core"; import { ILLMProvider } from "@omnia/llm"; import { Intent } from "@omnia/intent"; +import { hydrateObjective } from "@omnia/voice"; export const ValidationResultSchema = z.object({ isValid: z.boolean(), @@ -60,6 +61,8 @@ You must respond with a JSON object containing: - "reason": a concise explanation of why the action is allowed or denied. `.trim(); + const objectiveContent = hydrateObjective(intent.content, worldState); + const userContext = ` === CURRENT WORLD STATE === Current Time: ${worldState.clock.get().toISOString()} @@ -69,8 +72,7 @@ ${serializedWorld} === PROPOSED ACTION === Actor ID: ${intent.actorId} Type: ${intent.type} -Description: "${intent.description}" -Original Text: "${intent.originalText}" +Content: "${objectiveContent}" Target IDs: ${intent.targetIds.join(", ") || "(None)"} Decide if the proposed action is logically valid and physically possible. diff --git a/packages/architect/tests/architect.test.ts b/packages/architect/tests/architect.test.ts index a3cc481..149020f 100644 --- a/packages/architect/tests/architect.test.ts +++ b/packages/architect/tests/architect.test.ts @@ -1,5 +1,5 @@ -import { describe, test, expect } from "vitest"; import Database from "better-sqlite3"; +import { describe, test, expect } from "vitest"; import { WorldState, Entity, @@ -7,15 +7,16 @@ import { AttributeVisibility, } from "@omnia/core"; import { MockLLMProvider } from "@omnia/llm"; -import { Architect, AliasDeltaGenerator } from "@omnia/architect"; import { Intent } from "@omnia/intent"; +import { Architect, AliasDeltaGenerator } from "../src/index.js"; -describe("Architect & LLMValidator Unit Tests (Tier 1)", () => { - test("returns valid response when LLM validates intent as successful", async () => { +describe("World Architect Validation Tests (Tier 1)", () => { + test("returns valid response when LLM confirms the intent", async () => { const world = new WorldState("world-1"); const alice = new Entity("alice"); world.addEntity(alice); + // Setup mock LLM response const mockResponse = { isValid: true, reason: "Alice is in the room and the chest is unlocked.", @@ -25,9 +26,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => { const intent: Intent = { 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.", + content: "entity@alice[I] open the chest and read the scroll", actorId: "alice", targetIds: [], modifiers: [], @@ -56,9 +55,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => { const intent: Intent = { type: "action", - originalText: "unlock the gate and escape", - description: "Unlock the gate and escape", - selfDescription: "You unlock the gate and escape.", + content: "entity@bob[I] unlock the gate and escape", actorId: "bob", targetIds: [], modifiers: [], @@ -79,9 +76,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => { const intent: Intent = { type: "action", - originalText: "haunt the mansion", - description: "Haunt the mansion", - selfDescription: "You haunt the mansion.", + content: "entity@ghost[I] haunt the mansion", actorId: "ghost", targetIds: [], modifiers: [], @@ -128,9 +123,7 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", () const intent: Intent = { 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.", + content: "entity@alice[I] pick the lock of the wooden chest", actorId: "alice", targetIds: [], modifiers: [], @@ -177,9 +170,7 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", () const intent: Intent = { type: "action", - originalText: "run away", - description: "Run away", - selfDescription: "You run away.", + content: "entity@bob[I] run away", actorId: "bob", targetIds: [], modifiers: [], diff --git a/packages/architect/tsconfig.json b/packages/architect/tsconfig.json index 66a91d3..f11c151 100644 --- a/packages/architect/tsconfig.json +++ b/packages/architect/tsconfig.json @@ -8,6 +8,7 @@ "references": [ { "path": "../core" }, { "path": "../llm" }, - { "path": "../intent" } + { "path": "../intent" }, + { "path": "../voice" } ] } diff --git a/packages/intent/package.json b/packages/intent/package.json index 095cf86..3e0a9c7 100644 --- a/packages/intent/package.json +++ b/packages/intent/package.json @@ -9,6 +9,7 @@ "dependencies": { "@omnia/core": "workspace:*", "@omnia/llm": "workspace:*", + "@omnia/voice": "workspace:*", "zod": "^4.4.3" } } diff --git a/packages/intent/src/intent-decoder.ts b/packages/intent/src/intent-decoder.ts index ed3584b..857d408 100644 --- a/packages/intent/src/intent-decoder.ts +++ b/packages/intent/src/intent-decoder.ts @@ -1,5 +1,6 @@ import { WorldState, resolveAlias } from "@omnia/core"; import { ILLMProvider } from "@omnia/llm"; +import { dehydrate } from "@omnia/voice"; import { Intent, IntentSequence, LLMIntentSequenceSchema } from "./intent.js"; export class IntentDecoder { @@ -51,8 +52,10 @@ export class IntentDecoder { return `(Alias="${alias}", ID="${tid}")`; }) .join(", "); + // In the historical context, prior.content is already dehydrated, so we hydrate it for the actor's view + // Wait, we can keep it simple or just use the content. We'll show the content. historicalLines.push( - ` - Content: "${prior.originalText}", Type: ${prior.type}, Target Entities: ${targetsStr || "None"}`, + ` - Content: "${prior.content}", Type: ${prior.type}, Target Entities: ${targetsStr || "None"}`, ); } const historicalContext = @@ -67,15 +70,11 @@ Your job is to take a block of narrative prose written by an actor agent and dec For each intent you must: 1. Classify its type: - "dialogue": if actor speaking, talking, whispering, murmuring, etc - - "action": Any physical or logical action performed in the world (e.g., moving, opening, looking). - - "monologue" (or "thought"): An inner thought, reflection, or internal monologue/self narration. -2. Extract the original text fragment from the prose that corresponds to this intent. -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") - - "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. - - In case of a dialogue, the description and self Description only stores the exact words said by the entity. (e.g. "I will do that later", "Are you serious right now?") -4. Identify targetIds — the entity IDs of the receiving parties. Use the "Other entities in context" list to resolve any subjective names, aliases, or descriptions used in the prose to their correct entity IDs. If no specific target, use an empty array. -5. Identify modifiers — a list of strings representing additional qualities or modifiers extracted from the narrative prose. This includes emotions, tone of voice, speed, manner of action, or statement type (e.g., "question", "anxious", "whispering", "slowly", "quietly", "forcefully"). If no modifiers are present, use an empty array. + - "action": Any physical action performed in the world (e.g., moving, opening, looking). DO NOT CLASSIFY SPEAKING MODIFIERS AS ACTIONS + - "monologue" (or "thought"): An inner thought, reflection, or monologue/self narration. +2. Extract the original narrative text fragment from the prose that corresponds to this intent and populate it as "content". Do not paraphrase, do not convert to third person, and do not convert to second person. Keep the original text fragment exactly as written in the prose (first-person voice). +3. Identify targetIds — the entity IDs of the receiving parties. Use the "Other entities in context" list to resolve any subjective names, aliases, or descriptions used in the prose to their correct entity IDs. If no specific target, use an empty array. +4. Identify modifiers — a list of strings representing additional qualities or modifiers extracted from the narrative prose. This includes emotions, tone of voice, speed, manner of action, or statement type (e.g., "question", "anxious", "whispering", "slowly", "quietly", "forcefully"). If no modifiers are present, use an empty array. `.trim(); const userContext = ` @@ -101,10 +100,26 @@ ${narrativeProse} ); } - const fullIntents = response.data.intents.map((intent) => ({ - ...intent, - actorId, - })); + const aliasMap: Record = {}; + if (actor) { + for (const [targetId, alias] of actor.aliases.entries()) { + aliasMap[alias] = targetId; + } + } + + const fullIntents = response.data.intents.map((intent) => { + const dehydrated = dehydrate( + intent.content, + actorId, + intent.targetIds, + aliasMap, + ); + return { + ...intent, + content: dehydrated, + actorId, + }; + }); return { intents: fullIntents, diff --git a/packages/intent/src/intent.ts b/packages/intent/src/intent.ts index 99615f2..bffaf90 100644 --- a/packages/intent/src/intent.ts +++ b/packages/intent/src/intent.ts @@ -24,14 +24,8 @@ export const LLMIntentSchema = z.object({ /** The type of intent. */ type: IntentTypeSchema, - /** The original narrative text fragment this intent was extracted from. */ - originalText: z.string(), - - /** 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 dehydrated canonical content of the intent. */ + content: z.string(), /** * Entity IDs of the receiving parties (e.g., who is being spoken to, diff --git a/packages/intent/tests/intent.test.ts b/packages/intent/tests/intent.test.ts index 162b327..b9ed1b3 100644 --- a/packages/intent/tests/intent.test.ts +++ b/packages/intent/tests/intent.test.ts @@ -9,13 +9,11 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { const alice = new Entity("alice"); world.addEntity(alice); - const mockResponse: IntentSequence = { + const mockResponse = { intents: [ { type: "action", - originalText: "Alice opened the chest.", - description: "Open the wooden chest.", - selfDescription: "You open the wooden chest.", + content: "I open the wooden chest.", targetIds: [], modifiers: [], }, @@ -34,6 +32,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { expect(result.intents).toHaveLength(1); expect(result.intents[0].type).toBe("action"); expect(result.intents[0].actorId).toBe("alice"); + expect(result.intents[0].content).toContain("entity@alice[I]"); expect(result.intents[0].targetIds).toEqual([]); }); @@ -44,13 +43,11 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { world.addEntity(alice); world.addEntity(bob); - const mockResponse: IntentSequence = { + const mockResponse = { intents: [ { 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.", + content: '"Do you have the key?" I asked Bob.', targetIds: ["bob"], modifiers: [], }, @@ -68,6 +65,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { expect(result.intents).toHaveLength(1); expect(result.intents[0].type).toBe("dialogue"); + expect(result.intents[0].content).toContain("entity@alice[I]"); expect(result.intents[0].targetIds).toEqual(["bob"]); }); @@ -78,21 +76,17 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { world.addEntity(alice); world.addEntity(bob); - const mockResponse: IntentSequence = { + const mockResponse = { intents: [ { type: "dialogue", - originalText: '"Cover me," Alice whispered to Bob.', - description: "Alice whispers to Bob requesting cover.", - selfDescription: "You whisper to Bob requesting cover.", + content: '"Cover me," I whispered to Bob.', targetIds: ["bob"], modifiers: [], }, { 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.", + content: "I crept towards the door and pulled the handle.", targetIds: [], modifiers: [], }, @@ -113,6 +107,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => { expect(result.intents[0].targetIds).toEqual(["bob"]); expect(result.intents[1].type).toBe("action"); expect(result.intents[1].actorId).toBe("alice"); + expect(result.intents[1].content).toContain("entity@alice[I]"); }); test("throws on LLM failure", async () => { diff --git a/packages/intent/tsconfig.json b/packages/intent/tsconfig.json index f601e59..88fa32b 100644 --- a/packages/intent/tsconfig.json +++ b/packages/intent/tsconfig.json @@ -5,5 +5,9 @@ "outDir": "dist" }, "include": ["src"], - "references": [{ "path": "../core" }, { "path": "../llm" }] + "references": [ + { "path": "../core" }, + { "path": "../llm" }, + { "path": "../voice" } + ] } diff --git a/packages/llm/tests/openrouter.test.ts b/packages/llm/tests/openrouter.test.ts index 7f0fee8..6f941d4 100644 --- a/packages/llm/tests/openrouter.test.ts +++ b/packages/llm/tests/openrouter.test.ts @@ -127,6 +127,10 @@ describe("OpenRouterProvider Unit Tests (Tier 1)", () => { expect(provider.lastCalls[0]).toEqual({ systemPrompt: "system prompt", userContext: "user context", + response: { + name: "mocked response", + success: true, + }, usage: { inputTokens: 10, outputTokens: 5, diff --git a/packages/memory/package.json b/packages/memory/package.json index 069ced9..1406ba6 100644 --- a/packages/memory/package.json +++ b/packages/memory/package.json @@ -10,6 +10,7 @@ "@omnia/core": "workspace:*", "@omnia/intent": "workspace:*", "@omnia/llm": "workspace:*", + "@omnia/voice": "workspace:*", "zod": "^4.4.3" } } diff --git a/packages/memory/src/buffer.ts b/packages/memory/src/buffer.ts index 733419b..9bef288 100644 --- a/packages/memory/src/buffer.ts +++ b/packages/memory/src/buffer.ts @@ -1,6 +1,7 @@ import Database from "better-sqlite3"; import { Entity, resolveAlias } from "@omnia/core"; import { Intent } from "@omnia/intent"; +import { hydrate } from "@omnia/voice"; export interface BufferEntry { id: string; @@ -23,32 +24,14 @@ export function serializeSubjectiveBufferEntry( entry: BufferEntry, viewer: Entity, ): string { - const isSelf = viewer.id === entry.intent.actorId; - - 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; + let details = hydrate(entry.intent.content, viewer).trim(); + if (details.length > 0) { + details = details.charAt(0).toUpperCase() + details.slice(1); } - - 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}`; + return details; } export class BufferRepository { diff --git a/packages/memory/tests/handoff.test.ts b/packages/memory/tests/handoff.test.ts index bf0c1c2..4fe9727 100644 --- a/packages/memory/tests/handoff.test.ts +++ b/packages/memory/tests/handoff.test.ts @@ -1,20 +1,20 @@ -import { describe, test, expect } from "vitest"; import Database from "better-sqlite3"; +import { describe, test, expect } from "vitest"; import { Entity } from "@omnia/core"; import { MockLLMProvider, MockEmbeddingProvider } from "@omnia/llm"; import { BufferEntry, BufferRepository, LedgerRepository, - checkHandoffTrigger, - splitBufferForHandoff, HandoffEngine, + splitBufferForHandoff, + checkHandoffTrigger, } from "@omnia/memory"; -describe("Memory Handoff Tests (Tier 1)", () => { - const now = new Date("2026-07-07T12:00:00.000Z"); +const now = new Date("2026-07-09T08:00:00.000Z"); - test("splitBufferForHandoff correctly splits based on watermark and fresh buckets", () => { +describe("Memory Handoff Tests (Tier 1)", () => { + test("splitBufferForHandoff identifies candidate entries based on recency", () => { const entries: BufferEntry[] = []; // Add 12 older entries (older than 30 minutes) @@ -30,8 +30,7 @@ describe("Memory Handoff Tests (Tier 1)", () => { locationId: "room-1", intent: { type: "dialogue", - originalText: `Old event ${i}`, - description: `does old thing ${i}`, + content: `entity@alice[I] do old thing ${i}`, actorId: "alice", targetIds: ["bob"], }, @@ -54,8 +53,7 @@ describe("Memory Handoff Tests (Tier 1)", () => { locationId: "room-1", intent: { type: "dialogue", - originalText: `Fresh event ${idx}`, - description: `does fresh thing ${idx}`, + content: `entity@alice[I] do fresh thing ${idx}`, actorId: "alice", targetIds: ["bob"], }, @@ -84,8 +82,7 @@ describe("Memory Handoff Tests (Tier 1)", () => { locationId: "room-1", intent: { type: "dialogue", - originalText: "hello", - description: "says hello", + content: "entity@alice[I] say hello", actorId: "alice", targetIds: [], }, @@ -100,8 +97,7 @@ describe("Memory Handoff Tests (Tier 1)", () => { locationId: "room-2", intent: { type: "monologue", - originalText: "think", - description: "thinks", + content: "entity@alice[I] think", actorId: "alice", targetIds: [], }, @@ -138,8 +134,7 @@ describe("Memory Handoff Tests (Tier 1)", () => { locationId: "room-1", intent: { type: i % 2 === 0 ? "dialogue" : "action", - originalText: `Event ${i}`, - description: `does thing ${i}`, + content: `entity@alice[I] do thing ${i}`, actorId: "alice", targetIds: ["bob"], }, diff --git a/packages/memory/tests/memory.test.ts b/packages/memory/tests/memory.test.ts index 0741c23..678e985 100644 --- a/packages/memory/tests/memory.test.ts +++ b/packages/memory/tests/memory.test.ts @@ -1,24 +1,14 @@ -import { describe, test, expect } from "vitest"; import Database from "better-sqlite3"; +import { describe, test, expect } from "vitest"; import { Entity, SQLiteRepository } from "@omnia/core"; import { Intent } from "@omnia/intent"; import { BufferEntry, BufferRepository, serializeSubjectiveBufferEntry, - resolveAlias, } from "@omnia/memory"; describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { - test("resolveAlias correctly handles self and fallbacks", () => { - const viewer = new Entity("alice"); - viewer.aliases.set("bob", "the hooded figure"); - - expect(resolveAlias(viewer, "alice")).toBe("you"); - expect(resolveAlias(viewer, "bob")).toBe("the hooded figure"); - expect(resolveAlias(viewer, "charlie")).toBe("an unfamiliar figure"); - }); - test("serializes dialogue intent substituting target/actor aliases", () => { const viewer = new Entity("alice"); viewer.aliases.set("bob", "the hooded figure"); @@ -31,9 +21,8 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { locationId: "room-1", intent: { type: "dialogue", - originalText: '"Hello there," Bob said to Charlie.', - description: "says, 'Hello there' to the bartender", - selfDescription: "You say, 'Hello there' to the bartender.", + content: + "entity@bob[I] say 'Hello there' to entity@charlie[the bartender]", actorId: "bob", targetIds: ["charlie"], modifiers: [], @@ -42,7 +31,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { const result = serializeSubjectiveBufferEntry(entry, viewer); expect(result).toBe( - "The hooded figure says, 'Hello there' to the bartender", + "The hooded figure says 'Hello there' to the bartender", ); }); @@ -57,9 +46,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { locationId: "room-1", intent: { type: "action", - originalText: "Bob tried to break the latch.", - description: "attempts to break the lock latch", - selfDescription: "You attempt to break the lock latch.", + content: "entity@bob[I] attempt to break the lock latch", actorId: "bob", targetIds: [], modifiers: [], @@ -86,9 +73,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { locationId: "room-1", intent: { type: "action", - originalText: "I opened the window.", - description: "open the window", - selfDescription: "You open the window.", + content: "entity@alice[I] open the window", actorId: "alice", targetIds: [], modifiers: [], @@ -96,7 +81,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { }; const resultSelf = serializeSubjectiveBufferEntry(entrySelf, viewer); - expect(resultSelf).toBe("You open the window."); + expect(resultSelf).toBe("I open the window"); const entryUnfamiliar: BufferEntry = { id: "entry-unfamiliar", @@ -105,9 +90,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => { locationId: "room-1", intent: { type: "action", - originalText: "Someone knocked.", - description: "knocks on the door", - selfDescription: "You knock on the door.", + content: "entity@stranger-1[I] knock on the door", actorId: "stranger-1", targetIds: [], modifiers: [], @@ -136,9 +119,7 @@ describe("BufferRepository Persistence Tests (Tier 1)", () => { const intent: Intent = { type: "action", - originalText: "Alice picked up a stick.", - description: "Alice gathers a stick", - selfDescription: "You gather a stick.", + content: "entity@alice[I] gather a stick", actorId: "alice", targetIds: [], modifiers: [], @@ -196,8 +177,7 @@ describe("BufferRepository Persistence Tests (Tier 1)", () => { locationId: "forest", intent: { type: "action", - originalText: "Alice sneezed.", - description: "Alice sneezes", + content: "entity@alice[I] sneeze", actorId: "alice", targetIds: [], }, diff --git a/packages/memory/tsconfig.json b/packages/memory/tsconfig.json index c3a3b11..3a80f01 100644 --- a/packages/memory/tsconfig.json +++ b/packages/memory/tsconfig.json @@ -8,6 +8,7 @@ "references": [ { "path": "../core" }, { "path": "../intent" }, - { "path": "../llm" } + { "path": "../llm" }, + { "path": "../voice" } ] } diff --git a/packages/scenario/src/loader.ts b/packages/scenario/src/loader.ts index ff31959..e4806cf 100644 --- a/packages/scenario/src/loader.ts +++ b/packages/scenario/src/loader.ts @@ -145,8 +145,14 @@ export class ScenarioLoader { timestamp: mem.timestamp, locationId: mem.locationId, intent: { - ...mem.intent, - selfDescription: mem.intent.selfDescription ?? "", + type: mem.intent.type, + content: + mem.intent.content || + mem.intent.description || + mem.intent.originalText || + "", + actorId: mem.intent.actorId, + targetIds: mem.intent.targetIds, modifiers: mem.intent.modifiers ?? [], }, outcome: mem.outcome, diff --git a/packages/scenario/src/schema.ts b/packages/scenario/src/schema.ts index 7215ed0..c222f06 100644 --- a/packages/scenario/src/schema.ts +++ b/packages/scenario/src/schema.ts @@ -31,8 +31,9 @@ export const ScenarioMemoryEntrySchema = z.object({ locationId: z.string().nullable(), intent: z.object({ type: z.enum(["dialogue", "action", "monologue", "thought"]), - originalText: z.string(), - description: z.string(), + content: z.string().optional(), + originalText: z.string().optional(), + description: z.string().optional(), selfDescription: z.string().optional(), actorId: z.string(), targetIds: z.array(z.string()), diff --git a/packages/scenario/tests/scenario.test.ts b/packages/scenario/tests/scenario.test.ts index 2f9ea6a..6c60a60 100644 --- a/packages/scenario/tests/scenario.test.ts +++ b/packages/scenario/tests/scenario.test.ts @@ -139,7 +139,7 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => { expect(memories[0].id).toBe("mem-seed-1"); expect(memories[0].timestamp).toBe("2026-07-09T07:55:00.000Z"); expect(memories[0].locationId).toBe("lobby"); - expect(memories[0].intent.description).toBe("entered the house"); + expect(memories[0].intent.content).toBe("entered the house"); db.close(); }); diff --git a/packages/scenario/tests/talking-room.test.ts b/packages/scenario/tests/talking-room.test.ts index f262a14..7837893 100644 --- a/packages/scenario/tests/talking-room.test.ts +++ b/packages/scenario/tests/talking-room.test.ts @@ -131,20 +131,17 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => { expect(alphaMemories).toHaveLength(2); expect(alphaMemories[0].id).toBe("ab3f29d2-cf11-4111-9a99-b13c126d123e"); expect(alphaMemories[0].intent.type).toBe("monologue"); - expect(alphaMemories[0].intent.originalText).toContain("jail"); - expect(alphaMemories[0].intent.description).toBe(""); + expect(alphaMemories[0].intent.content).toContain("jail"); expect(alphaMemories[1].id).toBe("zz3f29d2-as11-9811-9a99-b13c126d123e"); expect(alphaMemories[1].intent.type).toBe("action"); - expect(alphaMemories[1].intent.originalText).toContain("sleep"); - expect(alphaMemories[1].intent.description).toBe(""); + expect(alphaMemories[1].intent.content).toContain("sleep"); const betaMemories = bufferRepo.listForOwner(betaId); expect(betaMemories).toHaveLength(1); expect(betaMemories[0].id).toBe("zx1f29d2-cf11-4111-9a99-b13c126d123e"); expect(betaMemories[0].intent.type).toBe("action"); - expect(betaMemories[0].intent.originalText).toContain("unfamiliar"); - expect(betaMemories[0].intent.description).toBe(""); + expect(betaMemories[0].intent.content).toContain("unfamiliar"); db.close(); }); diff --git a/packages/voice/package.json b/packages/voice/package.json new file mode 100644 index 0000000..1492a2b --- /dev/null +++ b/packages/voice/package.json @@ -0,0 +1,13 @@ +{ + "name": "@omnia/voice", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./dist/index.js" + }, + "dependencies": { + "@omnia/core": "workspace:*", + "compromise": "^14.14.0" + } +} diff --git a/packages/voice/src/dehydration.ts b/packages/voice/src/dehydration.ts new file mode 100644 index 0000000..9e08359 --- /dev/null +++ b/packages/voice/src/dehydration.ts @@ -0,0 +1,146 @@ +export interface Segment { + text: string; + isQuote: boolean; +} + +/** + * Splits text into quote and non-quote segments. + */ +export function splitQuotes(text: string): Segment[] { + const segments: Segment[] = []; + let current = ""; + let inQuote = false; + let quoteChar = ""; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if ((char === '"' || char === "'") && (!inQuote || char === quoteChar)) { + if (current) { + segments.push({ text: current, isQuote: inQuote }); + current = ""; + } + inQuote = !inQuote; + quoteChar = inQuote ? char : ""; + } else { + current += char; + } + } + + if (current) { + segments.push({ text: current, isQuote: inQuote }); + } + + return segments; +} + +/** + * Transforms standard narrative prose from the source actor's perspective + * into a dehydrated canonical form with entity@[original] placeholder tags. + */ +export function dehydrate( + content: string, + sourceId: string, + targetIds: string[], + aliasMap: Record, +): string { + if (!content) return ""; + + const segments = splitQuotes(content); + + const processedSegments = segments.map((seg) => { + if (seg.isQuote) { + return `"${seg.text}"`; + } + + let text = seg.text; + + // 1. Map lowercase aliases/names/IDs to IDs + const nameToId = new Map(); + + // Add target IDs and source ID themselves + nameToId.set(sourceId.toLowerCase(), sourceId); + targetIds.forEach((id) => { + nameToId.set(id.toLowerCase(), id); + }); + + // Add entries from aliasMap (mapped lowercased) + Object.entries(aliasMap).forEach(([name, id]) => { + nameToId.set(name.toLowerCase(), id); + }); + + // Sort names by length descending to match longest name first + const sortedNames = Array.from(nameToId.keys()).sort( + (a, b) => b.length - a.length, + ); + + // Track state of matched target IDs for pronoun lookback + const matchedTargetIds: string[] = []; + + // 2. Replace names and aliases with entity@[name] + sortedNames.forEach((name) => { + const id = nameToId.get(name)!; + const escapedName = name.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); + const regex = new RegExp(`\\b${escapedName}\\b`, "gi"); + + text = text.replace(regex, (matched) => { + if (id !== sourceId) { + matchedTargetIds.push(id); + } + return `entity@${id}[${matched}]`; + }); + }); + + // 3. Replace first-person pronouns with source actor tag + const firstPersonPronouns = [ + { word: "i" }, + { word: "me" }, + { word: "my" }, + { word: "myself" }, + { word: "mine" }, + { word: "we" }, + { word: "us" }, + { word: "our" }, + { word: "ours" }, + { word: "ourselves" }, + ]; + + firstPersonPronouns.forEach(({ word }) => { + const regex = new RegExp(`\\b${word}\\b`, "gi"); + text = text.replace(regex, (matched) => { + return `entity@${sourceId}[${matched}]`; + }); + }); + + // 4. Replace third-person pronouns using state lookback + const thirdPersonPronouns = [ + "he", + "him", + "his", + "himself", + "she", + "her", + "hers", + "herself", + "they", + "them", + "their", + "theirs", + "themselves", + ]; + thirdPersonPronouns.forEach((pronoun) => { + const regex = new RegExp(`\\b${pronoun}\\b`, "gi"); + text = text.replace(regex, (matched) => { + const lastTargetId = + matchedTargetIds[matchedTargetIds.length - 1] || targetIds[0]; + if (lastTargetId) { + return `entity@${lastTargetId}[${matched}]`; + } + return matched; + }); + }); + + return text; + }); + + return processedSegments.join(""); +} diff --git a/packages/voice/src/hydration.ts b/packages/voice/src/hydration.ts new file mode 100644 index 0000000..741d158 --- /dev/null +++ b/packages/voice/src/hydration.ts @@ -0,0 +1,236 @@ +import nlp from "compromise"; +import { Entity, WorldState, resolveAlias } from "@omnia/core"; +import { splitQuotes } from "./dehydration.js"; + +/** + * Hydrates a dehydrated narration text containing entity@[original] symbol tags + * into natural language from a specific viewer's perspective. + */ +export function hydrate(content: string, viewer: Entity): string { + if (!content) return ""; + const segments = splitQuotes(content); + + const processedSegments = segments.map((seg) => { + if (seg.isQuote) { + return `'${seg.text}'`; + } + + // Match entity@[original] and optionally the following space and word + const regex = /entity@([a-zA-Z0-9-]+)\[([^\]]+)\](?:\s+([a-zA-Z]+))?/g; + + const firstPersonSet = new Set([ + "i", + "me", + "my", + "myself", + "mine", + "we", + "us", + "our", + "ours", + ]); + const thirdPersonSet = new Set([ + "he", + "him", + "his", + "himself", + "she", + "her", + "hers", + "herself", + "they", + "them", + "their", + "theirs", + "themselves", + ]); + + return seg.text.replace(regex, (matchStr, id, original, followingWord) => { + const isSelf = id === viewer.id; + const lowerOriginal = original.toLowerCase(); + let resolvedSubject = ""; + let isThirdPersonSingular = false; + + if (isSelf) { + if (["his", "her", "their", "my", "its", "our"].includes(lowerOriginal)) + resolvedSubject = "my"; + else if (["hers", "theirs", "mine", "ours"].includes(lowerOriginal)) + resolvedSubject = "mine"; + else if ( + [ + "himself", + "herself", + "themselves", + "myself", + "itself", + "ourselves", + ].includes(lowerOriginal) + ) + resolvedSubject = "myself"; + else if (["he", "she", "they", "i", "we"].includes(lowerOriginal)) + resolvedSubject = "I"; + else if (["him", "her", "them", "me", "us"].includes(lowerOriginal)) + resolvedSubject = "me"; + else { + // Noun/alias mapped to self: check preceding/succeeding context + const matchIdx = seg.text.indexOf(matchStr); + const precedingText = seg.text.slice(0, matchIdx); + const prec = precedingText.trim(); + const words = prec.split(/\s+/); + const lastWord = words[words.length - 1]?.toLowerCase() || ""; + + const prepositions = [ + "to", + "with", + "for", + "at", + "by", + "from", + "in", + "on", + "about", + "between", + "of", + "under", + "over", + "behind", + "beside", + "through", + ]; + if (prepositions.includes(lastWord)) { + resolvedSubject = "me"; + } else { + resolvedSubject = "I"; + } + } + } else { + const alias = resolveAlias(viewer, id); + if (firstPersonSet.has(lowerOriginal)) { + if (["my", "our"].includes(lowerOriginal)) + resolvedSubject = `${alias}'s`; + else if (["mine", "ours"].includes(lowerOriginal)) + resolvedSubject = `${alias}'s`; + else if (["myself", "ourselves"].includes(lowerOriginal)) + resolvedSubject = "himself"; + else { + resolvedSubject = alias; + isThirdPersonSingular = true; + } + } else if (thirdPersonSet.has(lowerOriginal)) { + resolvedSubject = original; + if (["he", "she", "it"].includes(lowerOriginal)) { + isThirdPersonSingular = true; + } + } else { + resolvedSubject = alias; + isThirdPersonSingular = true; + } + } + + if (followingWord) { + if (isThirdPersonSingular) { + const conj = nlp(followingWord).verbs().conjugate()[0] as any; + if (conj && conj.Infinitive === followingWord && conj.PresentTense) { + return `${resolvedSubject} ${conj.PresentTense}`; + } + } + return `${resolvedSubject} ${followingWord}`; + } + + return resolvedSubject; + }); + }); + + return processedSegments.join(""); +} + +/** + * Hydrates a dehydrated narration text containing entity@[original] symbol tags + * into natural language from an objective world perspective. + */ +export function hydrateObjective( + content: string, + worldState: WorldState, +): string { + if (!content) return ""; + const segments = splitQuotes(content); + + const processedSegments = segments.map((seg) => { + if (seg.isQuote) { + return `'${seg.text}'`; + } + + // Match entity@[original] and optionally the following space and word + const regex = /entity@([a-zA-Z0-9-]+)\[([^\]]+)\](?:\s+([a-zA-Z]+))?/g; + + const firstPersonSet = new Set([ + "i", + "me", + "my", + "myself", + "mine", + "we", + "us", + "our", + "ours", + ]); + const thirdPersonSet = new Set([ + "he", + "him", + "his", + "himself", + "she", + "her", + "hers", + "herself", + "they", + "them", + "their", + "theirs", + "themselves", + ]); + + return seg.text.replace(regex, (matchStr, id, original, followingWord) => { + const entity = worldState.getEntity(id); + const name = entity?.attributes.get("name")?.getValue() || id; + const lowerOriginal = original.toLowerCase(); + let resolvedSubject = ""; + let isThirdPersonSingular = false; + + if (firstPersonSet.has(lowerOriginal)) { + if (["my", "our"].includes(lowerOriginal)) + resolvedSubject = `${name}'s`; + else if (["mine", "ours"].includes(lowerOriginal)) + resolvedSubject = `${name}'s`; + else if (["myself", "ourselves"].includes(lowerOriginal)) + resolvedSubject = "himself"; + else { + resolvedSubject = name; + isThirdPersonSingular = true; + } + } else if (thirdPersonSet.has(lowerOriginal)) { + resolvedSubject = original; + if (["he", "she", "it"].includes(lowerOriginal)) { + isThirdPersonSingular = true; + } + } else { + resolvedSubject = name; + isThirdPersonSingular = true; + } + + if (followingWord) { + if (isThirdPersonSingular) { + const conj = nlp(followingWord).verbs().conjugate()[0] as any; + if (conj && conj.Infinitive === followingWord && conj.PresentTense) { + return `${resolvedSubject} ${conj.PresentTense}`; + } + } + return `${resolvedSubject} ${followingWord}`; + } + + return resolvedSubject; + }); + }); + + return processedSegments.join(""); +} diff --git a/packages/voice/src/index.ts b/packages/voice/src/index.ts new file mode 100644 index 0000000..d16fb7c --- /dev/null +++ b/packages/voice/src/index.ts @@ -0,0 +1,2 @@ +export * from "./dehydration.js"; +export * from "./hydration.js"; diff --git a/packages/voice/tsconfig.json b/packages/voice/tsconfig.json new file mode 100644 index 0000000..3182c75 --- /dev/null +++ b/packages/voice/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"], + "references": [{ "path": "../core" }] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 802931d..88cd69b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -357,6 +357,9 @@ importers: "@omnia/spatial": specifier: workspace:* version: link:../../packages/spatial + "@omnia/voice": + specifier: workspace:* + version: link:../../packages/voice "@radix-ui/react-dialog": specifier: ^1.1.19 version: 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -442,6 +445,9 @@ importers: "@omnia/memory": specifier: workspace:* version: link:../memory + "@omnia/voice": + specifier: workspace:* + version: link:../voice zod: specifier: ^4.4.3 version: 4.4.3 @@ -457,6 +463,9 @@ importers: "@omnia/llm": specifier: workspace:* version: link:../llm + "@omnia/voice": + specifier: workspace:* + version: link:../voice zod: specifier: ^4.4.3 version: 4.4.3 @@ -481,6 +490,9 @@ importers: "@omnia/llm": specifier: workspace:* version: link:../llm + "@omnia/voice": + specifier: workspace:* + version: link:../voice zod: specifier: ^4.4.3 version: 4.4.3 @@ -509,6 +521,9 @@ importers: "@omnia/llm": specifier: workspace:* version: link:../llm + "@omnia/voice": + specifier: workspace:* + version: link:../voice zod: specifier: ^4.4.3 version: 4.4.3 @@ -534,6 +549,15 @@ importers: specifier: workspace:* version: link:../core + packages/voice: + dependencies: + "@omnia/core": + specifier: workspace:* + version: link:../core + compromise: + specifier: ^14.14.0 + version: 14.16.0 + web/docs: dependencies: "@astrojs/starlight": diff --git a/tests/integration/actor-monologue.test.ts b/tests/integration/actor-monologue.test.ts index 9d32d0d..2dffc44 100644 --- a/tests/integration/actor-monologue.test.ts +++ b/tests/integration/actor-monologue.test.ts @@ -56,34 +56,25 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => { }; // 2. IntentDecoder splits that prose into 3 intents. - const mockDecodedSequence: IntentSequence = { + const mockDecodedSequence = { intents: [ { 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.", + content: "I internally reflect that Bob has not noticed me.", actorId: "alice", targetIds: [], modifiers: [], }, { type: "dialogue", - originalText: '"Hey Bob," she called out softly.', - description: "Alice softly calls out to Bob.", - selfDescription: "You softly call out to Bob.", + content: '"Hey Bob," I call out softly to Bob.', actorId: "alice", targetIds: ["bob"], modifiers: [], }, { 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.", + content: "I reach for the ledger on the table.", actorId: "alice", targetIds: [], modifiers: [], diff --git a/tests/integration/game-loop.test.ts b/tests/integration/game-loop.test.ts index aedf050..422dbe9 100644 --- a/tests/integration/game-loop.test.ts +++ b/tests/integration/game-loop.test.ts @@ -1,30 +1,21 @@ import { describe, test, expect } from "vitest"; import Database from "better-sqlite3"; -import { - WorldState, - Entity, - SQLiteRepository, - AttributeVisibility, -} from "@omnia/core"; +import { WorldState, Entity, SQLiteRepository } from "@omnia/core"; import { MockLLMProvider } from "@omnia/llm"; import { IntentDecoder, IntentSequence } from "@omnia/intent"; import { Architect } from "@omnia/architect"; -describe("Omnia Integration Tests (Tier 2)", () => { - test("end-to-end intent decoding, validation, execution, and persistence", async () => { +describe("Game Loop Integration Tests (Tier 2)", () => { + test("successfully processes a sequence of dialogue and action intents", async () => { const db = new Database(":memory:"); const repo = new SQLiteRepository(db); const startTime = new Date("2026-07-06T12:00:00.000Z"); const world = new WorldState("world-abc", startTime); - world.addAttribute("location", "Dungeon Crawl", AttributeVisibility.PUBLIC); const alice = new Entity("alice", "room-1"); - alice.addAttribute("role", "rogue", AttributeVisibility.PUBLIC); - world.addEntity(alice); - const bob = new Entity("bob", "room-1"); - bob.addAttribute("role", "knight", AttributeVisibility.PUBLIC); + world.addEntity(alice); world.addEntity(bob); // Save initial state to database @@ -32,22 +23,18 @@ describe("Omnia Integration Tests (Tier 2)", () => { // Mock responses for LLM: // 1. IntentDecoder parses: `"Cover me," Alice whispered to Bob. She crept towards the door and pulled the handle.` - const mockIntentSequence: IntentSequence = { + const mockIntentSequence = { intents: [ { 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.", + content: '"Cover me," I whisper to Bob.', actorId: "alice", targetIds: ["bob"], modifiers: [], }, { 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.", + content: "I creep to the door and pull the handle.", actorId: "alice", targetIds: [], modifiers: [], @@ -135,9 +122,7 @@ describe("Omnia Integration Tests (Tier 2)", () => { // 2. Valid dialogue: speaking to bob const intent1 = { 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.", + content: "entity@alice[I] attempt to pick the lock with a hairpin.", actorId: "alice", targetIds: [], modifiers: [], @@ -145,9 +130,7 @@ describe("Omnia Integration Tests (Tier 2)", () => { const intent2 = { type: "dialogue" as const, - originalText: '"This is useless," she mutters.', - description: "Alice mutters to herself.", - selfDescription: "You mutter to yourself.", + content: '"This is useless," entity@alice[I] mutter.', actorId: "alice", targetIds: [], modifiers: [], From 5c3a79e8b6ab1ef3597dc20cdcce2e3de0b9de76 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 19 Jul 2026 13:35:19 +0530 Subject: [PATCH 07/15] fix(voice): Quote splitting based on single and double quotes --- content/demo/scenarios/talking-room.json | 2 +- packages/intent/src/intent-decoder.ts | 7 +- packages/voice/src/contractions.ts | 82 ++++++++++++++++++++++++ packages/voice/src/dehydration.ts | 3 +- packages/voice/src/index.ts | 1 + 5 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 packages/voice/src/contractions.ts diff --git a/content/demo/scenarios/talking-room.json b/content/demo/scenarios/talking-room.json index 7615008..9aa1770 100644 --- a/content/demo/scenarios/talking-room.json +++ b/content/demo/scenarios/talking-room.json @@ -88,7 +88,7 @@ "locationId": "white-room", "intent": { "type": "action", - "content": "entity@bf3f29d2-cf11-4b11-9a99-b13c126d400e[I] wake up from my sleep.", + "content": "entity@bf3f29d2-cf11-4b11-9a99-b13c126d400e wakes up from his sleep.", "actorId": "bf3f29d2-cf11-4b11-9a99-b13c126d400e", "targetIds": [] } diff --git a/packages/intent/src/intent-decoder.ts b/packages/intent/src/intent-decoder.ts index 857d408..c5319af 100644 --- a/packages/intent/src/intent-decoder.ts +++ b/packages/intent/src/intent-decoder.ts @@ -1,6 +1,6 @@ import { WorldState, resolveAlias } from "@omnia/core"; import { ILLMProvider } from "@omnia/llm"; -import { dehydrate } from "@omnia/voice"; +import { dehydrate, expandContractions } from "@omnia/voice"; import { Intent, IntentSequence, LLMIntentSequenceSchema } from "./intent.js"; export class IntentDecoder { @@ -21,6 +21,7 @@ export class IntentDecoder { narrativeProse: string, recentIntents: Intent[] = [], ): Promise { + const processedProse = expandContractions(narrativeProse); const actor = worldState.getEntity(actorId); // 1. Get other entities co-located in the same context @@ -75,6 +76,8 @@ For each intent you must: 2. Extract the original narrative text fragment from the prose that corresponds to this intent and populate it as "content". Do not paraphrase, do not convert to third person, and do not convert to second person. Keep the original text fragment exactly as written in the prose (first-person voice). 3. Identify targetIds — the entity IDs of the receiving parties. Use the "Other entities in context" list to resolve any subjective names, aliases, or descriptions used in the prose to their correct entity IDs. If no specific target, use an empty array. 4. Identify modifiers — a list of strings representing additional qualities or modifiers extracted from the narrative prose. This includes emotions, tone of voice, speed, manner of action, or statement type (e.g., "question", "anxious", "whispering", "slowly", "quietly", "forcefully"). If no modifiers are present, use an empty array. +5. For dialogue intents always use the following format for content field: + I say "" (optionally: to him/her/alias). `.trim(); const userContext = ` @@ -85,7 +88,7 @@ Historical Context: ${historicalContext} === NARRATIVE PROSE === -${narrativeProse} +${processedProse} `.trim(); const response = await this.llmProvider.generateStructuredResponse({ diff --git a/packages/voice/src/contractions.ts b/packages/voice/src/contractions.ts new file mode 100644 index 0000000..3f3f2f0 --- /dev/null +++ b/packages/voice/src/contractions.ts @@ -0,0 +1,82 @@ +import { splitQuotes } from "./dehydration.js"; + +/** + * Preprocessor that expands common contractions (e.g. "he's" -> "he is", "I'm" -> "I am") + * in non-quote segments of a text, keeping dialogue segments untouched. + */ +export function expandContractions(text: string): string { + if (!text) return ""; + + const contractionMap: Record = { + "i'm": "I am", + "you're": "you are", + "he's": "he is", + "she's": "she is", + "it's": "it is", + "we're": "we are", + "they're": "they are", + "i've": "I have", + "you've": "you have", + "we've": "we have", + "they've": "they have", + "i'd": "I would", + "you'd": "you would", + "he'd": "he would", + "she'd": "she would", + "we'd": "we would", + "they'd": "they would", + "i'll": "I will", + "you'll": "you will", + "he'll": "he will", + "she'll": "she will", + "we'll": "we will", + "they'll": "they will", + "isn't": "is not", + "aren't": "are not", + "wasn't": "was not", + "weren't": "were not", + "haven't": "have not", + "hasn't": "has not", + "hadn't": "had not", + "won't": "will not", + "wouldn't": "would not", + "don't": "do not", + "doesn't": "does not", + "didn't": "did not", + "can't": "cannot", + "couldn't": "could not", + "shouldn't": "should not", + "mightn't": "might not", + "mustn't": "must not", + }; + + const segments = splitQuotes(text); + const processed = segments.map((seg) => { + if (seg.isQuote) { + return `"${seg.text}"`; + } + + let chunk = seg.text; + Object.entries(contractionMap).forEach(([contraction, replacement]) => { + const escaped = contraction.replace("'", "'"); + const regex = new RegExp(`\\b${escaped}\\b`, "gi"); + chunk = chunk.replace(regex, (matched) => { + const isCapitalized = matched[0] === matched[0].toUpperCase(); + let finalRep = replacement; + if (isCapitalized) { + finalRep = finalRep[0].toUpperCase() + finalRep.slice(1); + } else { + if (finalRep.startsWith("I ")) { + // Keep I capitalized + } else { + finalRep = finalRep[0].toLowerCase() + finalRep.slice(1); + } + } + return finalRep; + }); + }); + return chunk; + }); + + return processed.join(""); +} diff --git a/packages/voice/src/dehydration.ts b/packages/voice/src/dehydration.ts index 9e08359..53f3557 100644 --- a/packages/voice/src/dehydration.ts +++ b/packages/voice/src/dehydration.ts @@ -14,13 +14,12 @@ export function splitQuotes(text: string): Segment[] { for (let i = 0; i < text.length; i++) { const char = text[i]; - if ((char === '"' || char === "'") && (!inQuote || char === quoteChar)) { + if (char === '"') { if (current) { segments.push({ text: current, isQuote: inQuote }); current = ""; } inQuote = !inQuote; - quoteChar = inQuote ? char : ""; } else { current += char; } diff --git a/packages/voice/src/index.ts b/packages/voice/src/index.ts index d16fb7c..629558a 100644 --- a/packages/voice/src/index.ts +++ b/packages/voice/src/index.ts @@ -1,2 +1,3 @@ export * from "./dehydration.js"; export * from "./hydration.js"; +export * from "./contractions.js"; From c2926261a1bc15008279323b2fdb684e2e5c9758 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 19 Jul 2026 16:03:20 +0530 Subject: [PATCH 08/15] refactor(content): Updated talking room to add more coherent memory --- content/demo/scenarios/talking-room.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/content/demo/scenarios/talking-room.json b/content/demo/scenarios/talking-room.json index 9aa1770..1686dfc 100644 --- a/content/demo/scenarios/talking-room.json +++ b/content/demo/scenarios/talking-room.json @@ -82,13 +82,24 @@ "targetIds": [] } }, + { + "id": "10ak29d2-as11-9811-9a99-b13c126d123e", + "timestamp": "2026-07-09T06:00:00.000Z", + "locationId": "white-room", + "intent": { + "type": "action", + "content": "entity@7c9b83b3-8cfb-4e89-8d77-626a5757d591[I] woke up today in the room and saw entity@bf3f29d2-cf11-4b11-9a99-b13c126d400e[another man]!", + "actorId": "7c9b83b3-8cfb-4e89-8d77-626a5757d591", + "targetIds": ["bf3f29d2-cf11-4b11-9a99-b13c126d400e"] + } + }, { "id": "zz3f29d2-as11-9811-9a99-b13c126d123e", "timestamp": "2026-07-09T07:58:00.000Z", "locationId": "white-room", "intent": { "type": "action", - "content": "entity@bf3f29d2-cf11-4b11-9a99-b13c126d400e wakes up from his sleep.", + "content": "entity@bf3f29d2-cf11-4b11-9a99-b13c126d400e[I] wake up from my sleep.", "actorId": "bf3f29d2-cf11-4b11-9a99-b13c126d400e", "targetIds": [] } From f8977a14c6dadd145a06d773c671872dc05b1e04 Mon Sep 17 00:00:00 2001 From: rhit-lid2 <59123825+NeoLi00@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:08:55 +0530 Subject: [PATCH 09/15] refactor(gui): Use structured logging over string parsing --- apps/gui/src/components/play/PromptModal.tsx | 42 ++++++---- apps/gui/src/lib/simulation-types.ts | 5 ++ apps/gui/src/lib/simulation/turn-executor.ts | 14 ++-- packages/actor/src/actor-prompt-builder.ts | 81 +++++++++++++------- packages/actor/src/actor.ts | 12 ++- packages/scenario/tests/talking-room.test.ts | 10 ++- 6 files changed, 112 insertions(+), 52 deletions(-) diff --git a/apps/gui/src/components/play/PromptModal.tsx b/apps/gui/src/components/play/PromptModal.tsx index eb46f93..3d2c1fe 100644 --- a/apps/gui/src/components/play/PromptModal.tsx +++ b/apps/gui/src/components/play/PromptModal.tsx @@ -29,24 +29,37 @@ export function PromptModal({ entry, onClose }: PromptModalProps) { systemPrompt: string, userContext: string, inputTokens: number, + sectionsObj?: { + worldInfo: string; + memoryLedger: string; + cognitiveBuffer: string; + }, ) => { - const recentHeader = "=== COGNITIVE BUFFER ==="; - const ledgerHeader = "=== MEMORY LEDGER ==="; - - const recentIdx = userContext.indexOf(recentHeader); - let worldStr = userContext; + let worldStr = ""; let recentStr = ""; let ledgerStr = ""; - if (recentIdx !== -1) { - worldStr = userContext.substring(0, recentIdx).trim(); - const rest = userContext.substring(recentIdx).trim(); - const ledgerIdx = rest.indexOf(ledgerHeader); - if (ledgerIdx !== -1) { - recentStr = rest.substring(0, ledgerIdx).trim(); - ledgerStr = rest.substring(ledgerIdx).trim(); - } else { - recentStr = rest; + if (sectionsObj) { + worldStr = sectionsObj.worldInfo; + recentStr = sectionsObj.cognitiveBuffer; + ledgerStr = sectionsObj.memoryLedger; + } else { + const recentHeader = "=== COGNITIVE BUFFER ==="; + const ledgerHeader = "=== MEMORY LEDGER ==="; + + const recentIdx = userContext.indexOf(recentHeader); + worldStr = userContext; + + if (recentIdx !== -1) { + worldStr = userContext.substring(0, recentIdx).trim(); + const rest = userContext.substring(recentIdx).trim(); + const ledgerIdx = rest.indexOf(ledgerHeader); + if (ledgerIdx !== -1) { + recentStr = rest.substring(0, ledgerIdx).trim(); + ledgerStr = rest.substring(ledgerIdx).trim(); + } else { + recentStr = rest; + } } } @@ -144,6 +157,7 @@ export function PromptModal({ entry, onClose }: PromptModalProps) { entry.rawPrompt.systemPrompt, entry.rawPrompt.userContext, entry.usage.inputTokens, + entry.rawPrompt.sections, ) : null; const decoderBreakdown = diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index 3f201e0..11fa8ba 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -20,6 +20,11 @@ export interface LogEntry { rawPrompt?: { systemPrompt: string; userContext: string; + sections?: { + worldInfo: string; + memoryLedger: string; + cognitiveBuffer: string; + }; }; usage?: { inputTokens: number; diff --git a/apps/gui/src/lib/simulation/turn-executor.ts b/apps/gui/src/lib/simulation/turn-executor.ts index 57f6445..e489cd4 100644 --- a/apps/gui/src/lib/simulation/turn-executor.ts +++ b/apps/gui/src/lib/simulation/turn-executor.ts @@ -165,6 +165,11 @@ export async function processNpcTurn( narrativeProse: result.narrativeProse, intents: [], timestamp: worldState.clock.get().toISOString(), + rawPrompt: { + systemPrompt: result.systemPrompt || "", + userContext: result.userContext || "", + sections: result.promptComponents, + }, }; if ( @@ -175,10 +180,6 @@ export async function processNpcTurn( session.actorProvider.lastCalls[ session.actorProvider.lastCalls.length - 1 ]; - entry.rawPrompt = { - systemPrompt: actorCall.systemPrompt, - userContext: actorCall.userContext, - }; entry.usage = actorCall.usage; } @@ -243,8 +244,9 @@ export async function executePlayerAction( intents: [], timestamp: worldState.clock.get().toISOString(), rawPrompt: { - systemPrompt: ctx.systemPrompt, - userContext: ctx.userContext, + systemPrompt: result.systemPrompt || ctx.systemPrompt, + userContext: result.userContext || ctx.userContext, + sections: result.promptComponents, }, }; diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index e943794..e94eae7 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -54,16 +54,27 @@ export class ActorPromptBuilder { private ledgerLimit = 5, ) {} + /** + * Assembles the system prompt and user context for a given entity. + */ /** * Assembles the system prompt and user context for a given entity. */ build( worldState: WorldState, entity: Entity, - ): { systemPrompt: string; userContext: string } { + ): { + systemPrompt: string; + userContext: string; + sections: { + worldInfo: string; + memoryLedger: string; + cognitiveBuffer: string; + }; + } { const systemPrompt = this.buildSystemPrompt(); - const userContext = this.buildUserContext(worldState, entity); - return { systemPrompt, userContext }; + const { userContext, sections } = this.buildUserContext(worldState, entity); + return { systemPrompt, userContext, sections }; } private buildSystemPrompt(): string { @@ -84,23 +95,28 @@ Guidelines: - Stay strictly within what your character knows. Do not invent knowledge that doesn't exist or act on it. - You are limited by just your memory. If your memory is limited, then that's all you can remember. If you do make stuff up then that's lying. Which is allowed, but remember that you're lying - Only describe your character's own actions, spoken words, and internal reactions. Do NOT narrate the environment or your surroundings, or other characters' actions. +- Be clear about who or what you are interacting with. ". `.trim(); } - private buildUserContext(worldState: WorldState, entity: Entity): string { - const sections: string[] = []; + private buildUserContext( + worldState: WorldState, + entity: Entity, + ): { + userContext: string; + sections: { + worldInfo: string; + memoryLedger: string; + cognitiveBuffer: string; + }; + } { const now = worldState.clock.get(); - // --- Subjective present time --- - sections.push( - `=== CURRENT MOMENT ===\nIt is ${now.toISOString()} right now.`, - ); - - // --- Subjective world state (self + perceived entities + co-location) --- - sections.push( - `=== THE WORLD AS YOU PERCEIVE IT ===\n${serializeSubjectiveWorldState(worldState, entity.id)}`, - ); + // --- Subjective present time & world state --- + const momentStr = `=== CURRENT MOMENT ===\nIt is ${now.toISOString()} right now.`; + const perceivedStr = `=== THE WORLD AS YOU PERCEIVE IT ===\n${serializeSubjectiveWorldState(worldState, entity.id)}`; + const worldInfo = `${momentStr}\n\n${perceivedStr}`; // Fetch recent buffer entries once let recentEntries: BufferEntry[] = []; @@ -112,16 +128,6 @@ Guidelines: } } - // --- Cognitive Buffer --- - const memorySection = this.buildCognitiveBufferSection( - entity, - recentEntries, - now, - ); - if (memorySection) { - sections.push(memorySection); - } - // --- Recalled Memory Ledger --- const ledgerSection = this.buildLedgerSection( worldState, @@ -129,11 +135,30 @@ Guidelines: recentEntries, now, ); - if (ledgerSection) { - sections.push(ledgerSection); - } + const memoryLedger = ledgerSection || ""; - return sections.join("\n\n"); + // --- Cognitive Buffer --- + const memorySection = this.buildCognitiveBufferSection( + entity, + recentEntries, + now, + ); + const cognitiveBuffer = memorySection || ""; + + // Assemble final user context + const parts: string[] = [worldInfo]; + if (memoryLedger) parts.push(memoryLedger); + if (cognitiveBuffer) parts.push(cognitiveBuffer); + const userContext = parts.join("\n\n"); + + return { + userContext, + sections: { + worldInfo, + memoryLedger, + cognitiveBuffer, + }, + }; } private buildCognitiveBufferSection( diff --git a/packages/actor/src/actor.ts b/packages/actor/src/actor.ts index af05830..80a29c5 100644 --- a/packages/actor/src/actor.ts +++ b/packages/actor/src/actor.ts @@ -54,6 +54,13 @@ export interface ActorTurnResult { narrativeProse: string; /** The decoded intent sequence (split/classified from the prose). */ intents: IntentSequence; + systemPrompt?: string; + userContext?: string; + promptComponents?: { + worldInfo: string; + memoryLedger: string; + cognitiveBuffer: string; + }; } /** @@ -116,7 +123,7 @@ export class ActorAgent { ); } - const { systemPrompt, userContext } = this.promptBuilder.build( + const { systemPrompt, userContext, sections } = this.promptBuilder.build( worldState, entity, ); @@ -145,6 +152,9 @@ export class ActorAgent { return { narrativeProse, intents, + systemPrompt, + userContext, + promptComponents: sections, }; } } diff --git a/packages/scenario/tests/talking-room.test.ts b/packages/scenario/tests/talking-room.test.ts index 7837893..88a5ed7 100644 --- a/packages/scenario/tests/talking-room.test.ts +++ b/packages/scenario/tests/talking-room.test.ts @@ -128,14 +128,18 @@ describe("Talking Room Demo Scenario Test (Tier 1)", () => { // 7. Assert initial pre-seeded memories const alphaMemories = bufferRepo.listForOwner(alphaId); - expect(alphaMemories).toHaveLength(2); + expect(alphaMemories).toHaveLength(3); expect(alphaMemories[0].id).toBe("ab3f29d2-cf11-4111-9a99-b13c126d123e"); expect(alphaMemories[0].intent.type).toBe("monologue"); expect(alphaMemories[0].intent.content).toContain("jail"); - expect(alphaMemories[1].id).toBe("zz3f29d2-as11-9811-9a99-b13c126d123e"); + expect(alphaMemories[1].id).toBe("10ak29d2-as11-9811-9a99-b13c126d123e"); expect(alphaMemories[1].intent.type).toBe("action"); - expect(alphaMemories[1].intent.content).toContain("sleep"); + expect(alphaMemories[1].intent.content).toContain("another man"); + + expect(alphaMemories[2].id).toBe("zz3f29d2-as11-9811-9a99-b13c126d123e"); + expect(alphaMemories[2].intent.type).toBe("action"); + expect(alphaMemories[2].intent.content).toContain("sleep"); const betaMemories = bufferRepo.listForOwner(betaId); expect(betaMemories).toHaveLength(1); From 1e34becec7e079c45025ba5f4e8c0adf2fa416a4 Mon Sep 17 00:00:00 2001 From: rhit-lid2 <59123825+NeoLi00@users.noreply.github.com> Date: Sun, 19 Jul 2026 16:18:15 +0530 Subject: [PATCH 10/15] refactor(gui): Unify prompt analysis components for actor, intent decoder and Handoff --- apps/gui/src/components/play/HandoffModal.tsx | 54 +- .../src/components/play/PromptAnalyzer.tsx | 172 +++++++ apps/gui/src/components/play/PromptModal.tsx | 473 +++--------------- apps/gui/src/lib/simulation-types.ts | 27 +- apps/gui/src/lib/simulation/alias-handoff.ts | 40 +- apps/gui/src/lib/simulation/turn-executor.ts | 21 + packages/actor/src/actor-prompt-builder.ts | 51 +- packages/actor/src/actor.ts | 11 +- packages/voice/src/index.ts | 12 + 9 files changed, 381 insertions(+), 480 deletions(-) create mode 100644 apps/gui/src/components/play/PromptAnalyzer.tsx diff --git a/apps/gui/src/components/play/HandoffModal.tsx b/apps/gui/src/components/play/HandoffModal.tsx index 4daa079..c8186a4 100644 --- a/apps/gui/src/components/play/HandoffModal.tsx +++ b/apps/gui/src/components/play/HandoffModal.tsx @@ -9,6 +9,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Badge } from "@/components/ui/badge"; +import { PromptAnalyzer } from "@/components/play/PromptAnalyzer"; interface HandoffModalProps { entry: SimSnapshot["log"][number]; @@ -191,25 +192,40 @@ export function HandoffModal({ entry, onClose }: HandoffModalProps) { )} {activeTab === "prompt" && entry.rawPrompt && ( -
-
-

- System Prompt -

-
-                  {entry.rawPrompt.systemPrompt}
-                
-
- -
-

- User Context (Candidates) -

-
-                  {entry.rawPrompt.userContext}
-                
-
-
+ 0 + ? entry.rawPrompt.components + : [ + { + label: "System Prompt", + type: "system", + content: entry.rawPrompt.systemPrompt || "", + }, + { + label: "User Context", + type: "world", + content: entry.rawPrompt.userContext || "", + }, + ] + } + inputTokens={entry.usage?.inputTokens || 0} + maxContext={ + entry.usage?.maxContext !== undefined + ? entry.usage.maxContext + : 32768 + } + modelName={entry.usage?.modelName} + providerInstanceName={entry.usage?.providerInstanceName} + outputLabel="LLM Output (Promoted Memory Chunks)" + outputText={ + handoffResult + ? JSON.stringify(handoffResult, null, 2) + : undefined + } + outputTokens={entry.usage?.outputTokens} + /> )} {activeTab === "output" && ( diff --git a/apps/gui/src/components/play/PromptAnalyzer.tsx b/apps/gui/src/components/play/PromptAnalyzer.tsx new file mode 100644 index 0000000..8a064a7 --- /dev/null +++ b/apps/gui/src/components/play/PromptAnalyzer.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import type { PromptComponent } from "@/lib/simulation-types"; + +interface PromptAnalyzerProps { + components: PromptComponent[]; + inputTokens: number; + maxContext?: number; + modelName?: string; + providerInstanceName?: string; + outputLabel?: string; + outputText?: string; + outputTokens?: number; +} + +export function PromptAnalyzer({ + components, + inputTokens, + maxContext = 32768, + modelName, + providerInstanceName, + outputLabel = "LLM Output", + outputText, + outputTokens, +}: PromptAnalyzerProps) { + const totalLen = components.reduce((sum, s) => sum + s.content.length, 0); + + if (totalLen === 0) { + return ( +
+ No prompt context recorded. +
+ ); + } + + const sections = components.map((s) => { + const pct = totalLen > 0 ? (s.content.length / totalLen) * 100 : 0; + return { + ...s, + pct, + tokens: Math.round((s.content.length / totalLen) * inputTokens), + }; + }); + + const usagePctOfContext = + maxContext > 0 ? (inputTokens / maxContext) * 100 : 0; + const isAbsolute = maxContext > 0 && usagePctOfContext >= 20; + + const getColorClass = (type: string) => { + switch (type) { + case "system": + return "bg-blue-500"; + case "world": + return "bg-emerald-500"; + case "events": + return "bg-purple-500"; + case "memories": + return "bg-pink-500"; + case "input": + return "bg-amber-500"; + default: + return "bg-slate-500"; + } + }; + + return ( +
+ {/* Provider Details */} + {(providerInstanceName || modelName) && ( +
+ LLM Instance:{" "} + {providerInstanceName || "Default"} + {modelName && ({modelName})} +
+ )} + + {/* Progress Bar & Breakdown */} +
+
+ Input Prompt Breakdown + + Total Input Tokens: {inputTokens} + {maxContext > 0 ? ( + + {" "} + / {maxContext} ({usagePctOfContext.toFixed(1)}% used) + + ) : ( + (infinite context) + )} + +
+ + {/* Token Bar */} +
+ {sections.map((item, idx) => { + const widthPct = isAbsolute + ? item.pct * (inputTokens / maxContext) + : item.pct; + return ( +
+ ); + })} + {isAbsolute && ( +
+ )} +
+ + {/* Accordion Components */} + + {sections.map((item, idx) => { + return ( + + +
+ + {item.label}: + + {item.tokens} tokens ( + {item.pct.toFixed(0)}%) + +
+
+ +
+                    {item.content}
+                  
+
+
+ ); + })} +
+
+ + {/* Output Section */} + {outputText && ( +
+
+ {outputLabel} + {outputTokens !== undefined && ( + + Total Output Tokens: {outputTokens} + + )} +
+
+
+              {outputText}
+            
+
+
+ )} +
+ ); +} diff --git a/apps/gui/src/components/play/PromptModal.tsx b/apps/gui/src/components/play/PromptModal.tsx index 3d2c1fe..1d91392 100644 --- a/apps/gui/src/components/play/PromptModal.tsx +++ b/apps/gui/src/components/play/PromptModal.tsx @@ -9,13 +9,8 @@ import { DialogTitle, } from "@/components/ui/dialog"; -import { - Accordion, - AccordionItem, - AccordionTrigger, - AccordionContent, -} from "@/components/ui/accordion"; import { PromptSwitcher } from "@/components/play/PromptSwitcher"; +import { PromptAnalyzer } from "@/components/play/PromptAnalyzer"; interface PromptModalProps { entry: SimSnapshot["log"][number]; @@ -25,191 +20,39 @@ interface PromptModalProps { export function PromptModal({ entry, onClose }: PromptModalProps) { const [activeTab, setActiveTab] = useState<"actor" | "decoder">("actor"); - const parseActorPrompt = ( - systemPrompt: string, - userContext: string, - inputTokens: number, - sectionsObj?: { - worldInfo: string; - memoryLedger: string; - cognitiveBuffer: string; - }, - ) => { - let worldStr = ""; - let recentStr = ""; - let ledgerStr = ""; - - if (sectionsObj) { - worldStr = sectionsObj.worldInfo; - recentStr = sectionsObj.cognitiveBuffer; - ledgerStr = sectionsObj.memoryLedger; - } else { - const recentHeader = "=== COGNITIVE BUFFER ==="; - const ledgerHeader = "=== MEMORY LEDGER ==="; - - const recentIdx = userContext.indexOf(recentHeader); - worldStr = userContext; - - if (recentIdx !== -1) { - worldStr = userContext.substring(0, recentIdx).trim(); - const rest = userContext.substring(recentIdx).trim(); - const ledgerIdx = rest.indexOf(ledgerHeader); - if (ledgerIdx !== -1) { - recentStr = rest.substring(0, ledgerIdx).trim(); - ledgerStr = rest.substring(ledgerIdx).trim(); - } else { - recentStr = rest; - } - } - } - - const sections: { label: string; type: string; content: string }[] = [ - { label: "System Prompt", type: "system", content: systemPrompt }, - { label: "World Info", type: "world", content: worldStr }, - { - label: "Cognitive Buffer", - type: "events", - content: recentStr || "(No cognitive buffer entries.)", - }, - { - label: "Memory Ledger", - type: "memories", - content: ledgerStr || "(No memory ledger entries.)", - }, - ]; - - const totalLen = sections.reduce((sum, s) => sum + s.content.length, 0); - if (totalLen === 0) return null; - - return sections.map((s) => { - const pct = (s.content.length / totalLen) * 100; - return { - ...s, - pct, - relativePct: pct, - tokens: Math.round((s.content.length / totalLen) * inputTokens), - }; - }); - }; - - const parseDecoderPrompt = ( - systemPrompt: string, - userContext: string, - inputTokens: number, - ) => { - const proseHeader = "=== NARRATIVE PROSE ==="; - const idx = userContext.indexOf(proseHeader); - - let worldStr = userContext; - let proseStr = ""; - - if (idx !== -1) { - worldStr = userContext.substring(0, idx).trim(); - proseStr = userContext.substring(idx).trim(); - } - - const sysLen = systemPrompt.length; - const worldLen = worldStr.length; - const proseLen = proseStr.length; - const totalLen = sysLen + worldLen + proseLen; - - if (totalLen === 0) return null; - - const sysPct = (sysLen / totalLen) * 100; - const worldPct = (worldLen / totalLen) * 100; - const prosePct = (proseLen / totalLen) * 100; - - const sysTokens = Math.round((sysLen / totalLen) * inputTokens); - const worldTokens = Math.round((worldLen / totalLen) * inputTokens); - const proseTokens = Math.max(0, inputTokens - sysTokens - worldTokens); - - return [ - { - label: "System Prompt", - pct: sysPct, - relativePct: sysPct, - tokens: sysTokens, - type: "system", - content: systemPrompt, - }, - { - label: "Decoder Context", - pct: worldPct, - relativePct: worldPct, - tokens: worldTokens, - type: "world", - content: worldStr, - }, - { - label: "Narrative Prose", - pct: prosePct, - relativePct: prosePct, - tokens: proseTokens, - type: "memories", - content: proseStr, - }, - ]; - }; - - const actorBreakdown = - entry.rawPrompt && entry.usage - ? parseActorPrompt( - entry.rawPrompt.systemPrompt, - entry.rawPrompt.userContext, - entry.usage.inputTokens, - entry.rawPrompt.sections, - ) - : null; - const decoderBreakdown = - entry.decoderPrompt && entry.decoderUsage - ? parseDecoderPrompt( - entry.decoderPrompt.systemPrompt, - entry.decoderPrompt.userContext, - entry.decoderUsage.inputTokens, - ) - : null; - - const actorMaxContext = - entry.usage?.maxContext !== undefined ? entry.usage.maxContext : 32768; - const actorUsedTokens = entry.usage?.inputTokens || 0; - const actorUsagePctOfContext = - actorMaxContext > 0 ? (actorUsedTokens / actorMaxContext) * 100 : 0; - const isActorAbsolute = actorMaxContext > 0 && actorUsagePctOfContext >= 20; - - const scaledActorBreakdown = actorBreakdown - ? actorBreakdown.map((item) => ({ - ...item, - pct: isActorAbsolute - ? item.relativePct * (actorUsedTokens / actorMaxContext) - : item.relativePct, - })) - : null; - - const decoderMaxContext = - entry.decoderUsage?.maxContext !== undefined - ? entry.decoderUsage.maxContext - : 32768; - const decoderUsedTokens = entry.decoderUsage?.inputTokens || 0; - const decoderUsagePctOfContext = - decoderMaxContext > 0 ? (decoderUsedTokens / decoderMaxContext) * 100 : 0; - const isDecoderAbsolute = - decoderMaxContext > 0 && decoderUsagePctOfContext >= 20; - - const scaledDecoderBreakdown = decoderBreakdown - ? decoderBreakdown.map((item) => ({ - ...item, - pct: isDecoderAbsolute - ? item.relativePct * (decoderUsedTokens / decoderMaxContext) - : item.relativePct, - })) - : null; - useEffect(() => { if (!entry.rawPrompt && entry.decoderPrompt) { setActiveTab("decoder"); } }, [entry]); + // Helper to resolve components with a fallback if none exist (for backwards-compatibility) + const getComponents = ( + promptBreakdown: any, + defaultType: "world" | "input", + ) => { + if (!promptBreakdown) return []; + if (promptBreakdown.components && promptBreakdown.components.length > 0) { + return promptBreakdown.components; + } + // Fallback: convert flat strings into components list + return [ + { + label: "System Prompt", + type: "system" as const, + content: promptBreakdown.systemPrompt || "", + }, + { + label: "User Context", + type: defaultType, + content: promptBreakdown.userContext || "", + }, + ]; + }; + + const actorComponents = getComponents(entry.rawPrompt, "world"); + const decoderComponents = getComponents(entry.decoderPrompt, "input"); + return ( !open && onClose()}> @@ -228,241 +71,37 @@ export function PromptModal({ entry, onClose }: PromptModalProps) {
{activeTab === "actor" && entry.rawPrompt && ( -
- {entry.usage ? ( -
- LLM Instance:{" "} - {entry.usage.providerInstanceName || "Default"} - {entry.usage.modelName && ( - ({entry.usage.modelName}) - )} -
- ) : ( -
- No LLM token usage (Player turn used fixed prose). -
- )} - - {scaledActorBreakdown && ( -
-
- - Input Prompt Breakdown - - - Total Input Tokens: {actorUsedTokens} - {actorMaxContext > 0 ? ( - - {" "} - / {actorMaxContext} ( - {actorUsagePctOfContext.toFixed(1)}% used) - - ) : ( - (infinite context) - )} - -
-
- {scaledActorBreakdown.map((item, idx) => { - const displayPct = - actorMaxContext > 0 - ? (item.tokens / actorMaxContext) * 100 - : item.relativePct; - return ( -
- ); - })} - {isActorAbsolute && ( -
- )} -
- - {scaledActorBreakdown.map((item, idx) => { - const displayPct = - actorMaxContext > 0 - ? (item.tokens / actorMaxContext) * 100 - : item.relativePct; - return ( - - - - {item.label}: {item.tokens} tokens - ({displayPct.toFixed(0)}%) - - -
-                              {item.content}
-                            
-
-
- ); - })} -
-
- )} - - {entry.usage && ( -
-
- LLM Output - - Total Output Tokens:{" "} - {entry.usage.outputTokens} - -
-
-
-                      {entry.narrativeProse}
-                    
-
-
- )} -
+ )} {activeTab === "decoder" && entry.decoderPrompt && ( -
- {entry.decoderUsage && ( -
- LLM Instance:{" "} - - {entry.decoderUsage.providerInstanceName || "Default"} - - {entry.decoderUsage.modelName && ( - ({entry.decoderUsage.modelName}) - )} -
- )} - - {scaledDecoderBreakdown && ( -
-
- - Input Prompt Breakdown - - - Total Input Tokens: {decoderUsedTokens} - {decoderMaxContext > 0 ? ( - - {" "} - / {decoderMaxContext} ( - {decoderUsagePctOfContext.toFixed(1)}% used) - - ) : ( - (infinite context) - )} - -
-
- {scaledDecoderBreakdown.map((item, idx) => { - const displayPct = - decoderMaxContext > 0 - ? (item.tokens / decoderMaxContext) * 100 - : item.relativePct; - return ( -
- ); - })} - {isDecoderAbsolute && ( -
- )} -
- - {scaledDecoderBreakdown.map((item, idx) => { - const displayPct = - decoderMaxContext > 0 - ? (item.tokens / decoderMaxContext) * 100 - : item.relativePct; - return ( - - - - {item.label}: {item.tokens} tokens - ({displayPct.toFixed(0)}%) - - -
-                              {item.content}
-                            
-
-
- ); - })} -
-
- )} - - {entry.decoderUsage && ( -
-
- LLM Output - - Total Output Tokens:{" "} - {entry.decoderUsage.outputTokens} - -
-
-
-                      {JSON.stringify(entry.intents, null, 2)}
-                    
-
-
- )} -
+ )}
diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index 11fa8ba..35cb8ca 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -8,6 +8,18 @@ export interface IntentInfo { minutesToAdvance?: number; } +export interface PromptComponent { + label: string; + type: "system" | "world" | "events" | "memories" | "input" | "other"; + content: string; +} + +export interface PromptBreakdown { + systemPrompt: string; + userContext: string; + components?: PromptComponent[]; +} + export interface LogEntry { turn: number; entityId: string; @@ -17,15 +29,7 @@ export interface LogEntry { timestamp: string; isHandoff?: boolean; handoffResult?: any; - rawPrompt?: { - systemPrompt: string; - userContext: string; - sections?: { - worldInfo: string; - memoryLedger: string; - cognitiveBuffer: string; - }; - }; + rawPrompt?: PromptBreakdown; usage?: { inputTokens: number; outputTokens: number; @@ -34,10 +38,7 @@ export interface LogEntry { providerInstanceName?: string; maxContext?: number; }; - decoderPrompt?: { - systemPrompt: string; - userContext: string; - }; + decoderPrompt?: PromptBreakdown; decoderUsage?: { inputTokens: number; outputTokens: number; diff --git a/apps/gui/src/lib/simulation/alias-handoff.ts b/apps/gui/src/lib/simulation/alias-handoff.ts index 36204fc..30295e5 100644 --- a/apps/gui/src/lib/simulation/alias-handoff.ts +++ b/apps/gui/src/lib/simulation/alias-handoff.ts @@ -45,6 +45,39 @@ export async function runHandoffResolution(session: SimSession): Promise { ]; const info = session.entities.find((e) => e.id === entity.id); const entityName = info?.name || entity.id; + let handoffPrompt = undefined; + if (lastCall) { + const header = "Cognitive Buffer Candidates for Handoff:"; + const userContext = lastCall.userContext; + const idx = userContext.indexOf(header); + + let contextStr = userContext; + let candidatesStr = ""; + + if (idx !== -1) { + contextStr = userContext.substring(0, idx).trim(); + candidatesStr = userContext.substring(idx).trim(); + } + + handoffPrompt = { + systemPrompt: lastCall.systemPrompt, + userContext: lastCall.userContext, + components: [ + { + label: "System Prompt", + type: "system", + content: lastCall.systemPrompt, + }, + { label: "Entity Context", type: "world", content: contextStr }, + { + label: "Cognitive Candidates", + type: "input", + content: candidatesStr, + }, + ], + }; + } + session.log.push({ turn: session.turn, entityId: entity.id, @@ -53,12 +86,7 @@ export async function runHandoffResolution(session: SimSession): Promise { intents: [], timestamp: worldState.clock.get().toISOString(), isHandoff: true, - rawPrompt: lastCall - ? { - systemPrompt: lastCall.systemPrompt, - userContext: lastCall.userContext, - } - : undefined, + rawPrompt: handoffPrompt, usage: lastCall?.usage, handoffResult: lastCall?.response, }); diff --git a/apps/gui/src/lib/simulation/turn-executor.ts b/apps/gui/src/lib/simulation/turn-executor.ts index e489cd4..60a8634 100644 --- a/apps/gui/src/lib/simulation/turn-executor.ts +++ b/apps/gui/src/lib/simulation/turn-executor.ts @@ -191,9 +191,30 @@ export async function processNpcTurn( session.decoderProvider.lastCalls[ session.decoderProvider.lastCalls.length - 1 ]; + const proseHeader = "=== NARRATIVE PROSE ==="; + const userContext = decoderCall.userContext; + const idx = userContext.indexOf(proseHeader); + + let contextStr = userContext; + let proseStr = ""; + + if (idx !== -1) { + contextStr = userContext.substring(0, idx).trim(); + proseStr = userContext.substring(idx).trim(); + } + entry.decoderPrompt = { systemPrompt: decoderCall.systemPrompt, userContext: decoderCall.userContext, + components: [ + { + label: "System Prompt", + type: "system", + content: decoderCall.systemPrompt, + }, + { label: "Decoder Context", type: "world", content: contextStr }, + { label: "Narrative Prose", type: "input", content: proseStr }, + ], }; entry.decoderUsage = decoderCall.usage; } diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index e94eae7..44707d9 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -13,7 +13,7 @@ import { LedgerEntry, LedgerRepository, } from "@omnia/memory"; -import { hydrate } from "@omnia/voice"; +import { hydrate, PromptComponent } from "@omnia/voice"; /** * Zod schema for the structured response expected from the actor LLM. @@ -60,21 +60,24 @@ export class ActorPromptBuilder { /** * Assembles the system prompt and user context for a given entity. */ + /** + * Assembles the system prompt and user context for a given entity. + */ build( worldState: WorldState, entity: Entity, ): { systemPrompt: string; userContext: string; - sections: { - worldInfo: string; - memoryLedger: string; - cognitiveBuffer: string; - }; + components: PromptComponent[]; } { const systemPrompt = this.buildSystemPrompt(); - const { userContext, sections } = this.buildUserContext(worldState, entity); - return { systemPrompt, userContext, sections }; + const { userContext, components } = this.buildUserContext( + worldState, + entity, + systemPrompt, + ); + return { systemPrompt, userContext, components }; } private buildSystemPrompt(): string { @@ -103,13 +106,10 @@ Guidelines: private buildUserContext( worldState: WorldState, entity: Entity, + systemPrompt: string, ): { userContext: string; - sections: { - worldInfo: string; - memoryLedger: string; - cognitiveBuffer: string; - }; + components: PromptComponent[]; } { const now = worldState.clock.get(); @@ -151,13 +151,28 @@ Guidelines: if (cognitiveBuffer) parts.push(cognitiveBuffer); const userContext = parts.join("\n\n"); + const components: PromptComponent[] = [ + { label: "System Prompt", type: "system", content: systemPrompt }, + { label: "World Info", type: "world", content: worldInfo }, + ]; + if (memoryLedger) { + components.push({ + label: "Memory Ledger", + type: "memories", + content: memoryLedger, + }); + } + if (cognitiveBuffer) { + components.push({ + label: "Cognitive Buffer", + type: "events", + content: cognitiveBuffer, + }); + } + return { userContext, - sections: { - worldInfo, - memoryLedger, - cognitiveBuffer, - }, + components, }; } diff --git a/packages/actor/src/actor.ts b/packages/actor/src/actor.ts index 80a29c5..c31c730 100644 --- a/packages/actor/src/actor.ts +++ b/packages/actor/src/actor.ts @@ -2,6 +2,7 @@ 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 { PromptComponent } from "@omnia/voice"; import { ActorPromptBuilder, ActorResponseSchema, @@ -56,11 +57,7 @@ export interface ActorTurnResult { intents: IntentSequence; systemPrompt?: string; userContext?: string; - promptComponents?: { - worldInfo: string; - memoryLedger: string; - cognitiveBuffer: string; - }; + promptComponents?: PromptComponent[]; } /** @@ -123,7 +120,7 @@ export class ActorAgent { ); } - const { systemPrompt, userContext, sections } = this.promptBuilder.build( + const { systemPrompt, userContext, components } = this.promptBuilder.build( worldState, entity, ); @@ -154,7 +151,7 @@ export class ActorAgent { intents, systemPrompt, userContext, - promptComponents: sections, + promptComponents: components, }; } } diff --git a/packages/voice/src/index.ts b/packages/voice/src/index.ts index 629558a..7cc66dc 100644 --- a/packages/voice/src/index.ts +++ b/packages/voice/src/index.ts @@ -1,3 +1,15 @@ export * from "./dehydration.js"; export * from "./hydration.js"; export * from "./contractions.js"; + +export interface PromptComponent { + label: string; + type: "system" | "world" | "events" | "memories" | "input" | "other"; + content: string; +} + +export interface PromptBreakdown { + systemPrompt: string; + userContext: string; + components?: PromptComponent[]; +} From ee25bf4a4c582b1e79e30da1a97f7952794f836f Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 19 Jul 2026 18:20:28 +0530 Subject: [PATCH 11/15] refactor(llm, memory): Use generic types prompt builder and prompt component for actor,intent and handoff --- apps/gui/src/lib/simulation/alias-handoff.ts | 43 ++------ apps/gui/src/lib/simulation/turn-executor.ts | 21 +++- packages/actor/src/actor-prompt-builder.ts | 3 +- packages/actor/src/actor.ts | 3 +- packages/intent/src/intent-decoder.ts | 90 +++------------- packages/intent/src/intent-prompt-builder.ts | 102 ++++++++++++++++++ packages/intent/src/intent.ts | 8 +- packages/llm/src/llm.ts | 16 +++ packages/memory/src/handoff-prompt-builder.ts | 59 ++++++++++ packages/memory/src/handoff.ts | 68 ++++++------ packages/voice/src/index.ts | 12 --- 11 files changed, 267 insertions(+), 158 deletions(-) create mode 100644 packages/intent/src/intent-prompt-builder.ts create mode 100644 packages/memory/src/handoff-prompt-builder.ts diff --git a/apps/gui/src/lib/simulation/alias-handoff.ts b/apps/gui/src/lib/simulation/alias-handoff.ts index 30295e5..22e6ebe 100644 --- a/apps/gui/src/lib/simulation/alias-handoff.ts +++ b/apps/gui/src/lib/simulation/alias-handoff.ts @@ -39,44 +39,13 @@ export async function runHandoffResolution(session: SimSession): Promise { worldState.clock.get(), ); if (ran) { + const lastResult = (handoffEngine as any).lastResult; const lastCall = session.handoffProvider.lastCalls?.[ (session.handoffProvider.lastCalls?.length || 0) - 1 ]; const info = session.entities.find((e) => e.id === entity.id); const entityName = info?.name || entity.id; - let handoffPrompt = undefined; - if (lastCall) { - const header = "Cognitive Buffer Candidates for Handoff:"; - const userContext = lastCall.userContext; - const idx = userContext.indexOf(header); - - let contextStr = userContext; - let candidatesStr = ""; - - if (idx !== -1) { - contextStr = userContext.substring(0, idx).trim(); - candidatesStr = userContext.substring(idx).trim(); - } - - handoffPrompt = { - systemPrompt: lastCall.systemPrompt, - userContext: lastCall.userContext, - components: [ - { - label: "System Prompt", - type: "system", - content: lastCall.systemPrompt, - }, - { label: "Entity Context", type: "world", content: contextStr }, - { - label: "Cognitive Candidates", - type: "input", - content: candidatesStr, - }, - ], - }; - } session.log.push({ turn: session.turn, @@ -86,9 +55,15 @@ export async function runHandoffResolution(session: SimSession): Promise { intents: [], timestamp: worldState.clock.get().toISOString(), isHandoff: true, - rawPrompt: handoffPrompt, + rawPrompt: lastResult + ? { + systemPrompt: lastResult.systemPrompt || "", + userContext: lastResult.userContext || "", + components: lastResult.promptComponents, + } + : undefined, usage: lastCall?.usage, - handoffResult: lastCall?.response, + handoffResult: lastResult?.response || lastCall?.response, }); } } diff --git a/apps/gui/src/lib/simulation/turn-executor.ts b/apps/gui/src/lib/simulation/turn-executor.ts index 60a8634..70d30e1 100644 --- a/apps/gui/src/lib/simulation/turn-executor.ts +++ b/apps/gui/src/lib/simulation/turn-executor.ts @@ -168,7 +168,7 @@ export async function processNpcTurn( rawPrompt: { systemPrompt: result.systemPrompt || "", userContext: result.userContext || "", - sections: result.promptComponents, + components: result.promptComponents, }, }; @@ -267,7 +267,7 @@ export async function executePlayerAction( rawPrompt: { systemPrompt: result.systemPrompt || ctx.systemPrompt, userContext: result.userContext || ctx.userContext, - sections: result.promptComponents, + components: result.promptComponents, }, }; @@ -279,9 +279,26 @@ export async function executePlayerAction( session.decoderProvider.lastCalls[ session.decoderProvider.lastCalls.length - 1 ]; + const proseHeader = "=== NARRATIVE PROSE ==="; + const userContext = call.userContext; + const idx = userContext.indexOf(proseHeader); + + let contextStr = userContext; + let proseStr = ""; + + if (idx !== -1) { + contextStr = userContext.substring(0, idx).trim(); + proseStr = userContext.substring(idx).trim(); + } + entry.decoderPrompt = { systemPrompt: call.systemPrompt, userContext: call.userContext, + components: [ + { label: "System Prompt", type: "system", content: call.systemPrompt }, + { label: "Decoder Context", type: "world", content: contextStr }, + { label: "Narrative Prose", type: "input", content: proseStr }, + ], }; entry.decoderUsage = call.usage; } diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index 44707d9..dff4fa9 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -13,7 +13,8 @@ import { LedgerEntry, LedgerRepository, } from "@omnia/memory"; -import { hydrate, PromptComponent } from "@omnia/voice"; +import { hydrate } from "@omnia/voice"; +import { PromptComponent } from "@omnia/llm"; /** * Zod schema for the structured response expected from the actor LLM. diff --git a/packages/actor/src/actor.ts b/packages/actor/src/actor.ts index c31c730..405007b 100644 --- a/packages/actor/src/actor.ts +++ b/packages/actor/src/actor.ts @@ -1,8 +1,7 @@ import { Entity, WorldState } from "@omnia/core"; -import { ILLMProvider } from "@omnia/llm"; +import { ILLMProvider, PromptComponent } from "@omnia/llm"; import { BufferEntry, BufferRepository, LedgerRepository } from "@omnia/memory"; import { Intent, IntentDecoder, IntentSequence } from "@omnia/intent"; -import { PromptComponent } from "@omnia/voice"; import { ActorPromptBuilder, ActorResponseSchema, diff --git a/packages/intent/src/intent-decoder.ts b/packages/intent/src/intent-decoder.ts index c5319af..b528d0a 100644 --- a/packages/intent/src/intent-decoder.ts +++ b/packages/intent/src/intent-decoder.ts @@ -1,19 +1,18 @@ -import { WorldState, resolveAlias } from "@omnia/core"; +import { WorldState } from "@omnia/core"; import { ILLMProvider } from "@omnia/llm"; import { dehydrate, expandContractions } from "@omnia/voice"; import { Intent, IntentSequence, LLMIntentSequenceSchema } from "./intent.js"; +import { IntentDecoderPromptBuilder } from "./intent-prompt-builder.js"; export class IntentDecoder { - constructor(private llmProvider: ILLMProvider) {} + private promptBuilder: IntentDecoderPromptBuilder; + + constructor(private llmProvider: ILLMProvider) { + this.promptBuilder = new IntentDecoderPromptBuilder(); + } /** * Decodes narrative prose into an ordered sequence of structured intents. - * - * Responsibilities (from docs/intents.md): - * - Split prose into multiple intents when applicable. - * - Classify each intent as "dialogue", "action", or "monologue". - * - Parse narrative text into structured JSON with minimal information loss. - * - Contextually resolve receiving parties (targets). */ async decode( worldState: WorldState, @@ -24,72 +23,12 @@ export class IntentDecoder { const processedProse = expandContractions(narrativeProse); const actor = worldState.getEntity(actorId); - // 1. Get other entities co-located in the same context - const otherEntitiesLines: string[] = []; - for (const otherEntity of worldState.entities.values()) { - if ( - otherEntity.id !== actorId && - otherEntity.locationId === actor?.locationId - ) { - const alias = actor - ? resolveAlias(actor, otherEntity.id) - : otherEntity.id; - otherEntitiesLines.push(` - Alias="${alias}" ID=${otherEntity.id}`); - } - } - const otherEntitiesContext = - otherEntitiesLines.length > 0 - ? otherEntitiesLines.join("\n") - : " (No other entities in context)"; - - // 2. Format historical context (2-3 recent intents received by the actor) - const historicalLines: string[] = []; - for (const prior of recentIntents) { - const targetIds = - prior.actorId !== actorId ? [prior.actorId] : prior.targetIds; - const targetsStr = targetIds - .map((tid) => { - const alias = actor ? resolveAlias(actor, tid) : tid; - return `(Alias="${alias}", ID="${tid}")`; - }) - .join(", "); - // In the historical context, prior.content is already dehydrated, so we hydrate it for the actor's view - // Wait, we can keep it simple or just use the content. We'll show the content. - historicalLines.push( - ` - Content: "${prior.content}", Type: ${prior.type}, Target Entities: ${targetsStr || "None"}`, - ); - } - const historicalContext = - historicalLines.length > 0 - ? historicalLines.join("\n") - : " (No prior intents in context)"; - - const systemPrompt = ` -You are the Intent Decoder for a narrative simulation engine. -Your job is to take a block of narrative prose written by an actor agent and decompose it into an ordered sequence of discrete intents. - -For each intent you must: -1. Classify its type: - - "dialogue": if actor speaking, talking, whispering, murmuring, etc - - "action": Any physical action performed in the world (e.g., moving, opening, looking). DO NOT CLASSIFY SPEAKING MODIFIERS AS ACTIONS - - "monologue" (or "thought"): An inner thought, reflection, or monologue/self narration. -2. Extract the original narrative text fragment from the prose that corresponds to this intent and populate it as "content". Do not paraphrase, do not convert to third person, and do not convert to second person. Keep the original text fragment exactly as written in the prose (first-person voice). -3. Identify targetIds — the entity IDs of the receiving parties. Use the "Other entities in context" list to resolve any subjective names, aliases, or descriptions used in the prose to their correct entity IDs. If no specific target, use an empty array. -4. Identify modifiers — a list of strings representing additional qualities or modifiers extracted from the narrative prose. This includes emotions, tone of voice, speed, manner of action, or statement type (e.g., "question", "anxious", "whispering", "slowly", "quietly", "forcefully"). If no modifiers are present, use an empty array. -5. For dialogue intents always use the following format for content field: - I say "" (optionally: to him/her/alias). -`.trim(); - - const userContext = ` -Intent Source: ${actorId} -Other entities in context: -${otherEntitiesContext} -Historical Context: -${historicalContext} - -=== NARRATIVE PROSE === -${processedProse} -`.trim(); + const { systemPrompt, userContext, components } = this.promptBuilder.build( + worldState, + actorId, + processedProse, + recentIntents, + ); const response = await this.llmProvider.generateStructuredResponse({ systemPrompt, @@ -126,6 +65,9 @@ ${processedProse} return { intents: fullIntents, + systemPrompt, + userContext, + promptComponents: components, }; } } diff --git a/packages/intent/src/intent-prompt-builder.ts b/packages/intent/src/intent-prompt-builder.ts new file mode 100644 index 0000000..972d12d --- /dev/null +++ b/packages/intent/src/intent-prompt-builder.ts @@ -0,0 +1,102 @@ +import { WorldState, resolveAlias } from "@omnia/core"; +import { PromptBreakdown, PromptComponent } from "@omnia/llm"; +import { Intent } from "./intent.js"; + +// TODO: Builder a generic interface for prompt builders in @omnia/llm: IPromptBuilder or something + +/** + * Prompt builder for the Intent Decoder. + * Separates prompt generation, structure, and component breakdowns. + */ +export class IntentDecoderPromptBuilder { + build( + worldState: WorldState, + actorId: string, + processedProse: string, + recentIntents: Intent[], + ): PromptBreakdown { + const actor = worldState.getEntity(actorId); + + // 1. Get other entities co-located in the same context + const otherEntitiesLines: string[] = []; + for (const otherEntity of worldState.entities.values()) { + if ( + otherEntity.id !== actorId && + otherEntity.locationId === actor?.locationId + ) { + const alias = actor + ? resolveAlias(actor, otherEntity.id) + : otherEntity.id; + otherEntitiesLines.push(` - Alias="${alias}" ID=${otherEntity.id}`); + } + } + const otherEntitiesContext = + otherEntitiesLines.length > 0 + ? otherEntitiesLines.join("\n") + : " (No other entities in context)"; + + // 2. Format historical context (2-3 recent intents received by the actor) + const historicalLines: string[] = []; + for (const prior of recentIntents) { + const targetIds = + prior.actorId !== actorId ? [prior.actorId] : prior.targetIds; + const targetsStr = targetIds + .map((tid) => { + const alias = actor ? resolveAlias(actor, tid) : tid; + return `(Alias="${alias}", ID="${tid}")`; + }) + .join(", "); + historicalLines.push( + ` - Content: "${prior.content}", Type: ${prior.type}, Target Entities: ${targetsStr || "None"}`, + ); + } + const historicalContext = + historicalLines.length > 0 + ? historicalLines.join("\n") + : " (No prior intents in context)"; + + const systemPrompt = ` +You are the Intent Decoder for a narrative simulation engine. +Your job is to take a block of narrative prose written by an actor agent and decompose it into an ordered sequence of discrete intents. + +For each intent you must: +1. Classify its type: + - "dialogue": if actor speaking, talking, whispering, murmuring, etc + - "action": Any physical action performed in the world (e.g., moving, opening, looking). DO NOT CLASSIFY SPEAKING MODIFIERS AS ACTIONS + - "monologue" (or "thought"): An inner thought, reflection, or monologue/self narration. +2. Extract the original narrative text fragment from the prose that corresponds to this intent and populate it as "content". Do not paraphrase, do not convert to third person, and do not convert to second person. Keep the original text fragment exactly as written in the prose (first-person voice). +3. Identify targetIds — the entity IDs of the receiving parties. Use the "Other entities in context" list to resolve any subjective names, aliases, or descriptions used in the prose to their correct entity IDs. If no specific target, use an empty array. +4. Identify modifiers — a list of strings representing additional qualities or modifiers extracted from the narrative prose. This includes emotions, tone of voice, speed, manner of action, or statement type (e.g., "question", "anxious", "whispering", "slowly", "quietly", "forcefully"). If no modifiers are present, use an empty array. +5. For dialogue intents always use the following format for content field: + I say "" (optionally: to him/her/alias). +`.trim(); + + const decoderContext = ` +Intent Source: ${actorId} +Other entities in context: +${otherEntitiesContext} +Historical Context: +${historicalContext} +`.trim(); + + const narrativeProseSection = `=== NARRATIVE PROSE ===\n${processedProse}`; + + const userContext = `${decoderContext}\n\n${narrativeProseSection}`; + + const components: PromptComponent[] = [ + { label: "System Prompt", type: "system", content: systemPrompt }, + { label: "Decoder Context", type: "world", content: decoderContext }, + { + label: "Narrative Prose", + type: "input", + content: narrativeProseSection, + }, + ]; + + return { + systemPrompt, + userContext, + components, + }; + } +} diff --git a/packages/intent/src/intent.ts b/packages/intent/src/intent.ts index bffaf90..aa43b7e 100644 --- a/packages/intent/src/intent.ts +++ b/packages/intent/src/intent.ts @@ -56,8 +56,14 @@ export const LLMIntentSequenceSchema = z.object({ * The full output of the Intent Decoder: an ordered sequence of intents * extracted from a single narrative prose block. */ +import { PromptComponent } from "@omnia/llm"; + export const IntentSequenceSchema = z.object({ intents: z.array(IntentSchema), }); -export type IntentSequence = z.infer; +export type IntentSequence = z.infer & { + systemPrompt?: string; + userContext?: string; + promptComponents?: PromptComponent[]; +}; diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index 03bb135..f470fe2 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -1,6 +1,22 @@ import { z } from "zod"; import { ProviderRegistry } from "./registry.js"; +export interface PromptComponent { + label: string; + type: "system" | "world" | "events" | "memories" | "input" | "other"; + content: string; +} + +export interface PromptBreakdown { + systemPrompt: string; + userContext: string; + components?: PromptComponent[]; +} + +export interface IPromptBuilder { + build(...args: TArgs): PromptBreakdown; +} + export interface LLMRequest { systemPrompt: string; userContext: string; diff --git a/packages/memory/src/handoff-prompt-builder.ts b/packages/memory/src/handoff-prompt-builder.ts new file mode 100644 index 0000000..879dd16 --- /dev/null +++ b/packages/memory/src/handoff-prompt-builder.ts @@ -0,0 +1,59 @@ +import { Entity } from "@omnia/core"; +import { PromptBreakdown, PromptComponent } from "@omnia/llm"; +import { BufferEntry, serializeSubjectiveBufferEntry } from "./buffer.js"; + +/** + * Prompt builder for the Handoff Engine. + * Separates prompt generation, structure, and component breakdowns. + */ +export class HandoffPromptBuilder { + build(entity: Entity, candidates: BufferEntry[], now: Date): PromptBreakdown { + const candidatesList = candidates + .map((entry) => { + const serialized = serializeSubjectiveBufferEntry(entry, entity); + return `ID: ${entry.id} | Timestamp: ${entry.timestamp} | Location: ${entry.locationId || "None"}\nContent: ${serialized}`; + }) + .join("\n---\n"); + + const systemPrompt = ` +You are the memory Handoff Engine. Your task is to process a list of Cognitive Buffer entries for an entity and select which memories to promote to the Memory Ledger, and which to forget or summarize. + +Instructions: +1. **Cluster** related consecutive buffer entries into high-level narrative beats or events (e.g. physical action and its outcome or trivial actions). Combine them into a single chunk. +2. **Write in the third-person** for the events of other entities. (eg. Alan did that. Sarah did this, etc) +2. **Write in first-person for the events that you yourself did. (eg. I did this, I did that.) +3. **verbatim Quotes**: Extract verbatim, high-salience quotes from dialogue if relevant. Do not modify or invent quotes. +4. **Determine Importance**: Assign an importance score from 1 (trivial, e.g. waking up) to 10 (life-altering, e.g. witnessing a crime). +4. Discard small body movements like looking around, sighing, etc that do not contextually hold any meaning after it is done. +5. **Involved Entities**: Identify all entity IDs involved in the memories in this chunk. +6. **Retain in Cognitive Buffer (Pinning)**: If a beat represents an unresolved high-stakes situation (e.g. a standing threat, an unanswered accusation, an ongoing chase or conflict), set "retainInBuffer" to true so it remains in the Cognitive Buffer for immediate context. Otherwise, set it to false so it is safely pruned from the Cognitive Buffer. +7. **Exclude stage business**: Glances, sighs, ambient noticing, and irrelevant sensory details should be ignored and not included in any promoted chunk. They will be forgotten. +8. **Forget by omission**: Any buffer entry ID that you do not include in any chunk's "sourceEntryIds" will be permanently deleted and forgotten. +`.trim(); + + const entityContext = ` +Subject Entity ID: ${entity.id} +Current Time: ${now.toISOString()} +`.trim(); + + const candidatesSection = `Cognitive Buffer Candidates for Handoff:\n${candidatesList}`; + + const userContext = `${entityContext}\n\n${candidatesSection}`; + + const components: PromptComponent[] = [ + { label: "System Prompt", type: "system", content: systemPrompt }, + { label: "Entity Context", type: "world", content: entityContext }, + { + label: "Cognitive Candidates", + type: "input", + content: candidatesSection, + }, + ]; + + return { + systemPrompt, + userContext, + components, + }; + } +} diff --git a/packages/memory/src/handoff.ts b/packages/memory/src/handoff.ts index 78a3172..a51ba10 100644 --- a/packages/memory/src/handoff.ts +++ b/packages/memory/src/handoff.ts @@ -6,7 +6,8 @@ import { BufferRepository, } from "./buffer.js"; import { LedgerEntry, LedgerRepository } from "./ledger.js"; -import { ILLMProvider, IEmbeddingProvider } from "@omnia/llm"; +import { ILLMProvider, IEmbeddingProvider, PromptComponent } from "@omnia/llm"; +import { HandoffPromptBuilder } from "./handoff-prompt-builder.js"; export const HandoffChunkSchema = z.object({ sourceEntryIds: z.array(z.string()), // buffer rows this chunk consumes @@ -202,55 +203,44 @@ export function splitBufferForHandoff( /** * HandoffEngine processes memory handoffs using LLM summarization and DB transactions. */ +export interface HandoffRunResult { + success: boolean; + systemPrompt?: string; + userContext?: string; + promptComponents?: PromptComponent[]; + response?: any; +} + export class HandoffEngine { + public lastResult: HandoffRunResult | null = null; + private promptBuilder: HandoffPromptBuilder; + constructor( private llmProvider: ILLMProvider, private embedProvider: IEmbeddingProvider, private bufferRepo: BufferRepository, private ledgerRepo: LedgerRepository, - ) {} + ) { + this.promptBuilder = new HandoffPromptBuilder(); + } async runHandoff( entity: Entity, bufferEntries: BufferEntry[], now: Date, ): Promise { + this.lastResult = null; const { candidates } = splitBufferForHandoff(bufferEntries, now); if (candidates.length === 0) { return false; } - const candidatesList = candidates - .map((entry) => { - const serialized = serializeSubjectiveBufferEntry(entry, entity); - return `ID: ${entry.id} | Timestamp: ${entry.timestamp} | Location: ${entry.locationId || "None"}\nContent: ${serialized}`; - }) - .join("\n---\n"); - - const systemPrompt = ` -You are the memory Handoff Engine. Your task is to process a list of Cognitive Buffer entries for an entity and select which memories to promote to the Memory Ledger, and which to forget or summarize. - -Instructions: -1. **Cluster** related consecutive buffer entries into high-level narrative beats or events (e.g. physical action and its outcome or trivial actions). Combine them into a single chunk. -2. **Write in the third-person** for the events of other entities. (eg. Alan did that. Sarah did this, etc) -2. **Write in first-person for the events that you yourself did. (eg. I did this, I did that.) -3. **verbatim Quotes**: Extract verbatim, high-salience quotes from dialogue if relevant. Do not modify or invent quotes. -4. **Determine Importance**: Assign an importance score from 1 (trivial, e.g. waking up) to 10 (life-altering, e.g. witnessing a crime). -4. Discard small body movements like looking around, sighing, etc that do not contextually hold any meaning after it is done. -5. **Involved Entities**: Identify all entity IDs involved in the memories in this chunk. -6. **Retain in Cognitive Buffer (Pinning)**: If a beat represents an unresolved high-stakes situation (e.g. a standing threat, an unanswered accusation, an ongoing chase or conflict), set "retainInBuffer" to true so it remains in the Cognitive Buffer for immediate context. Otherwise, set it to false so it is safely pruned from the Cognitive Buffer. -7. **Exclude stage business**: Glances, sighs, ambient noticing, and irrelevant sensory details should be ignored and not included in any promoted chunk. They will be forgotten. -8. **Forget by omission**: Any buffer entry ID that you do not include in any chunk's "sourceEntryIds" will be permanently deleted and forgotten. -`.trim(); - - const userContext = ` -Subject Entity ID: ${entity.id} -Current Time: ${now.toISOString()} - -Cognitive Buffer Candidates for Handoff: -${candidatesList} -`.trim(); + const { systemPrompt, userContext, components } = this.promptBuilder.build( + entity, + candidates, + now, + ); const response = await this.llmProvider.generateStructuredResponse({ systemPrompt, @@ -259,9 +249,23 @@ ${candidatesList} }); if (!response.success || !response.data) { + this.lastResult = { + success: false, + systemPrompt, + userContext, + promptComponents: components, + }; return false; } + this.lastResult = { + success: true, + systemPrompt, + userContext, + promptComponents: components, + response: response.data, + }; + const result = response.data; const db = (this.bufferRepo as any).db; diff --git a/packages/voice/src/index.ts b/packages/voice/src/index.ts index 7cc66dc..629558a 100644 --- a/packages/voice/src/index.ts +++ b/packages/voice/src/index.ts @@ -1,15 +1,3 @@ export * from "./dehydration.js"; export * from "./hydration.js"; export * from "./contractions.js"; - -export interface PromptComponent { - label: string; - type: "system" | "world" | "events" | "memories" | "input" | "other"; - content: string; -} - -export interface PromptBreakdown { - systemPrompt: string; - userContext: string; - components?: PromptComponent[]; -} From 7baf583b13cbb2bf02e5b91069710120b7887875 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 19 Jul 2026 20:31:58 +0530 Subject: [PATCH 12/15] refactor(architect): Use IPromptBuilder Interface for LLMValidator --- apps/gui/src/lib/simulation/turn-executor.ts | 75 +++++++++++++++++-- packages/actor/src/actor-prompt-builder.ts | 15 ++-- packages/architect/src/architect.ts | 2 +- packages/architect/src/index.ts | 1 + .../src/llm-validator-prompt-builder.ts | 59 +++++++++++++++ packages/architect/src/llm-validator.ts | 58 +++++--------- packages/intent/src/intent-prompt-builder.ts | 8 +- packages/memory/src/handoff-prompt-builder.ts | 6 +- 8 files changed, 164 insertions(+), 60 deletions(-) create mode 100644 packages/architect/src/llm-validator-prompt-builder.ts diff --git a/apps/gui/src/lib/simulation/turn-executor.ts b/apps/gui/src/lib/simulation/turn-executor.ts index 70d30e1..60dcb8c 100644 --- a/apps/gui/src/lib/simulation/turn-executor.ts +++ b/apps/gui/src/lib/simulation/turn-executor.ts @@ -49,10 +49,12 @@ async function processIntents( // eslint-disable-next-line @typescript-eslint/no-explicit-any worldState: any, session: SimSession, -): Promise { +): Promise<{ intentInfos: IntentInfo[]; validatorCalls: ValidatorCall[] }> { const intentInfos: IntentInfo[] = []; + const validatorCalls: ValidatorCall[] = []; - for (const intent of intents) { + for (let i = 0; i < intents.length; i++) { + const intent = intents[i]; const outcome = await session.architect.processIntent(worldState, intent); const ts = worldState.clock.get().toISOString(); @@ -66,6 +68,53 @@ async function processIntents( minutesToAdvance: outcome.timeDelta?.minutesToAdvance, }); + if ( + intent.type === "action" && + (session.architect.validator as any).lastResult + ) { + const lastResult = (session.architect.validator as any).lastResult; + let usage = undefined; + if ( + session.validatorProvider.lastCalls && + session.validatorProvider.lastCalls.length > 0 + ) { + const valCall = + session.validatorProvider.lastCalls[ + session.validatorProvider.lastCalls.length - 1 + ]; + usage = valCall.usage; + } + + validatorCalls.push({ + intentIndex: i, + intentContent: intent.content, + prompt: { + systemPrompt: lastResult.systemPrompt || "", + userContext: lastResult.userContext || "", + components: lastResult.promptComponents, + }, + response: { + isValid: outcome.isValid, + reason: outcome.reason, + }, + usage, + }); + } else { + const reason = + intent.type === "dialogue" + ? "Dialogue intents represent verbal/communication actions and are automatically valid." + : "Monologue/thought intents represent internal reflections and bypass validation."; + + validatorCalls.push({ + intentIndex: i, + intentContent: intent.content, + response: { + isValid: true, + reason: outcome.reason || reason, + }, + }); + } + const actorEntry = buildBufferEntryForIntent(intent, ts, entity.locationId); if (intent.type === "action") { actorEntry.outcome = { isValid: outcome.isValid, reason: outcome.reason }; @@ -99,7 +148,7 @@ async function processIntents( } } - return intentInfos; + return { intentInfos, validatorCalls }; } // --------------------------------------------------------------------------- @@ -219,13 +268,21 @@ export async function processNpcTurn( entry.decoderUsage = decoderCall.usage; } - entry.intents = await processIntents( + const { intentInfos, validatorCalls } = await processIntents( result.intents.intents, info.id, entity, worldState, session, ); + entry.intents = intentInfos; + entry.validatorCalls = validatorCalls; + entry.decodedIntents = result.intents.intents.map((intent) => ({ + type: intent.type, + content: intent.content, + modifiers: intent.modifiers || [], + targetIds: intent.targetIds, + })); session.log.push(entry); session.coreRepo.saveWorldState(worldState); @@ -303,13 +360,21 @@ export async function executePlayerAction( entry.decoderUsage = call.usage; } - entry.intents = await processIntents( + const { intentInfos, validatorCalls: playerValCalls } = await processIntents( result.intents.intents, ctx.entityId, entity, worldState, session, ); + entry.intents = intentInfos; + entry.validatorCalls = playerValCalls; + entry.decodedIntents = result.intents.intents.map((intent) => ({ + type: intent.type, + content: intent.content, + modifiers: intent.modifiers || [], + targetIds: intent.targetIds, + })); session.log.push(entry); session.coreRepo.saveWorldState(worldState); diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index dff4fa9..2cd0676 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -14,7 +14,7 @@ import { LedgerRepository, } from "@omnia/memory"; import { hydrate } from "@omnia/voice"; -import { PromptComponent } from "@omnia/llm"; +import { PromptComponent, IPromptBuilder, PromptBreakdown } from "@omnia/llm"; /** * Zod schema for the structured response expected from the actor LLM. @@ -38,7 +38,9 @@ export type ActorResponse = z.infer; * ACL'd to it), its own Cognitive Buffer, and the entities co-located * with it. System UUIDs are surfaced as subjective aliases. */ -export class ActorPromptBuilder { +export class ActorPromptBuilder implements IPromptBuilder< + [WorldState, Entity] +> { /** * @param bufferRepo Used to fetch the actor's Cognitive Buffer. Optional — * if absent, the memory section is omitted. @@ -64,14 +66,7 @@ export class ActorPromptBuilder { /** * Assembles the system prompt and user context for a given entity. */ - build( - worldState: WorldState, - entity: Entity, - ): { - systemPrompt: string; - userContext: string; - components: PromptComponent[]; - } { + build(worldState: WorldState, entity: Entity): PromptBreakdown { const systemPrompt = this.buildSystemPrompt(); const { userContext, components } = this.buildUserContext( worldState, diff --git a/packages/architect/src/architect.ts b/packages/architect/src/architect.ts index 5de9945..22f8331 100644 --- a/packages/architect/src/architect.ts +++ b/packages/architect/src/architect.ts @@ -9,7 +9,7 @@ export interface ProcessResult extends ValidationResult { } export class Architect { - private validator: LLMValidator; + public validator: LLMValidator; private timeDeltaGenerator: TimeDeltaGenerator; constructor( diff --git a/packages/architect/src/index.ts b/packages/architect/src/index.ts index 19e95a8..d5ad0bd 100644 --- a/packages/architect/src/index.ts +++ b/packages/architect/src/index.ts @@ -1,3 +1,4 @@ export * from "./llm-validator.js"; +export * from "./llm-validator-prompt-builder.js"; export * from "./architect.js"; export * from "./delta.js"; diff --git a/packages/architect/src/llm-validator-prompt-builder.ts b/packages/architect/src/llm-validator-prompt-builder.ts new file mode 100644 index 0000000..733a348 --- /dev/null +++ b/packages/architect/src/llm-validator-prompt-builder.ts @@ -0,0 +1,59 @@ +import { WorldState, serializeObjectiveWorldState } from "@omnia/core"; +import { Intent } from "@omnia/intent"; +import { hydrateObjective } from "@omnia/voice"; +import { PromptBreakdown, PromptComponent, IPromptBuilder } from "@omnia/llm"; + +/** + * Prompt builder for the LLM Validator (World Architect). + * Separates prompt generation, structure, and component breakdowns. + */ +export class LLMValidatorPromptBuilder implements IPromptBuilder< + [WorldState, Intent] +> { + build(worldState: WorldState, intent: Intent): PromptBreakdown { + const serializedWorld = serializeObjectiveWorldState(worldState); + + const systemPrompt = ` +You are the World Architect, a deterministic and objective judge of reality, physics, and narration for a simulation game. +Your task is to judge whether a proposed action (Intent) by an actor is physically and logically possible given the current objective state of the world. +Exempt dialogue or speech actions from validation (consider them always valid). +Enforce logical boundaries such as: +- Spatial boundaries (an actor cannot grab an object in another location unless they are there). +- Physical boundaries (an actor cannot open a locked drawer without a key or breaking it). +- State Boundaries (an actor cannot perform a task if their state doesn't allow them to do so). +- State/Attribute constraints. +- An actor can perform actions on themselves as long as it follows the boundaries stated above. + +You must respond with a JSON object containing: +- "isValid": boolean indicating if the action is possible/allowed. +- "reason": a very short explanation of why the action is allowed or denied. +`.trim(); + + const objectiveContent = hydrateObjective(intent.content, worldState); + + const worldStateSection = `=== CURRENT WORLD STATE ===\nCurrent Time: ${worldState.clock.get().toISOString()}\nEntities & Attributes:\n${serializedWorld}`; + const proposedActionSection = `=== PROPOSED ACTION ===\nActor ID: ${intent.actorId}\nType: ${intent.type}\nContent: "${objectiveContent}"\nTarget IDs: ${intent.targetIds.join(", ") || "(None)"}`; + + const userContext = `${worldStateSection}\n\n${proposedActionSection}\n\nDecide if the proposed action is logically valid and physically possible.`; + + const components: PromptComponent[] = [ + { label: "System Prompt", type: "system", content: systemPrompt }, + { + label: "Current World State", + type: "world", + content: worldStateSection, + }, + { + label: "Proposed Action", + type: "input", + content: proposedActionSection, + }, + ]; + + return { + systemPrompt, + userContext, + components, + }; + } +} diff --git a/packages/architect/src/llm-validator.ts b/packages/architect/src/llm-validator.ts index 93cb572..893c0db 100644 --- a/packages/architect/src/llm-validator.ts +++ b/packages/architect/src/llm-validator.ts @@ -1,8 +1,8 @@ import { z } from "zod"; -import { WorldState, serializeObjectiveWorldState } from "@omnia/core"; -import { ILLMProvider } from "@omnia/llm"; +import { WorldState } from "@omnia/core"; +import { ILLMProvider, PromptBreakdown } from "@omnia/llm"; import { Intent } from "@omnia/intent"; -import { hydrateObjective } from "@omnia/voice"; +import { LLMValidatorPromptBuilder } from "./llm-validator-prompt-builder.js"; export const ValidationResultSchema = z.object({ isValid: z.boolean(), @@ -12,7 +12,12 @@ export const ValidationResultSchema = z.object({ export type ValidationResult = z.infer; export class LLMValidator { - constructor(private llmProvider: ILLMProvider) {} + public lastResult: PromptBreakdown | null = null; + private promptBuilder: LLMValidatorPromptBuilder; + + constructor(private llmProvider: ILLMProvider) { + this.promptBuilder = new LLMValidatorPromptBuilder(); + } /** * Validates an action intent against the objective world state. @@ -25,6 +30,8 @@ export class LLMValidator { worldState: WorldState, intent: Intent, ): Promise { + this.lastResult = null; + // Defensive guard: monologue and thought intents bypass validation. if (intent.type === "monologue" || intent.type === "thought") { return { @@ -42,41 +49,16 @@ export class LLMValidator { }; } - // 1. Serialize the objective world state for the LLM - const serializedWorld = serializeObjectiveWorldState(worldState); + const { systemPrompt, userContext, components } = this.promptBuilder.build( + worldState, + intent, + ); - // 2. Build the prompts - const systemPrompt = ` -You are the World Architect, a deterministic and objective judge of reality, physics, and narration for a simulation game. -Your task is to judge whether a proposed action (Intent) by an actor is physically and logically possible given the current objective state of the world. -Exempt dialogue or speech actions from validation (consider them always valid). -Enforce logical boundaries such as: -- Spatial boundaries (an actor cannot grab an object in another location unless they are there). -- Physical boundaries (an actor cannot open a locked drawer without a key or breaking it). -- State Boundaries (an actor cannot perform a task if their state doesn't allow them to do so). -- State/Attribute constraints. - -You must respond with a JSON object containing: -- "isValid": boolean indicating if the action is possible/allowed. -- "reason": a concise explanation of why the action is allowed or denied. -`.trim(); - - const objectiveContent = hydrateObjective(intent.content, worldState); - - const userContext = ` -=== CURRENT WORLD STATE === -Current Time: ${worldState.clock.get().toISOString()} -Entities & Attributes: -${serializedWorld} - -=== PROPOSED ACTION === -Actor ID: ${intent.actorId} -Type: ${intent.type} -Content: "${objectiveContent}" -Target IDs: ${intent.targetIds.join(", ") || "(None)"} - -Decide if the proposed action is logically valid and physically possible. -`.trim(); + this.lastResult = { + systemPrompt, + userContext, + components, + }; // structured call via the LLM provider const response = await this.llmProvider.generateStructuredResponse({ diff --git a/packages/intent/src/intent-prompt-builder.ts b/packages/intent/src/intent-prompt-builder.ts index 972d12d..316edb7 100644 --- a/packages/intent/src/intent-prompt-builder.ts +++ b/packages/intent/src/intent-prompt-builder.ts @@ -1,14 +1,14 @@ import { WorldState, resolveAlias } from "@omnia/core"; -import { PromptBreakdown, PromptComponent } from "@omnia/llm"; +import { PromptBreakdown, PromptComponent, IPromptBuilder } from "@omnia/llm"; import { Intent } from "./intent.js"; -// TODO: Builder a generic interface for prompt builders in @omnia/llm: IPromptBuilder or something - /** * Prompt builder for the Intent Decoder. * Separates prompt generation, structure, and component breakdowns. */ -export class IntentDecoderPromptBuilder { +export class IntentDecoderPromptBuilder implements IPromptBuilder< + [WorldState, string, string, Intent[]] +> { build( worldState: WorldState, actorId: string, diff --git a/packages/memory/src/handoff-prompt-builder.ts b/packages/memory/src/handoff-prompt-builder.ts index 879dd16..3a019bc 100644 --- a/packages/memory/src/handoff-prompt-builder.ts +++ b/packages/memory/src/handoff-prompt-builder.ts @@ -1,12 +1,14 @@ import { Entity } from "@omnia/core"; -import { PromptBreakdown, PromptComponent } from "@omnia/llm"; +import { PromptBreakdown, PromptComponent, IPromptBuilder } from "@omnia/llm"; import { BufferEntry, serializeSubjectiveBufferEntry } from "./buffer.js"; /** * Prompt builder for the Handoff Engine. * Separates prompt generation, structure, and component breakdowns. */ -export class HandoffPromptBuilder { +export class HandoffPromptBuilder implements IPromptBuilder< + [Entity, BufferEntry[], Date] +> { build(entity: Entity, candidates: BufferEntry[], now: Date): PromptBreakdown { const candidatesList = candidates .map((entry) => { From 8ff1650657452e49f85066893d3ae8612f6b4a01 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 19 Jul 2026 20:32:27 +0530 Subject: [PATCH 13/15] feat(gui): Model Statistics now show Validators in the pipeline --- apps/gui/src/components/play/PromptModal.tsx | 53 ++++++++++- .../src/components/play/PromptSwitcher.tsx | 88 +++++++++++++------ apps/gui/src/lib/simulation-types.ts | 20 +++++ 3 files changed, 133 insertions(+), 28 deletions(-) diff --git a/apps/gui/src/components/play/PromptModal.tsx b/apps/gui/src/components/play/PromptModal.tsx index 1d91392..84e13a9 100644 --- a/apps/gui/src/components/play/PromptModal.tsx +++ b/apps/gui/src/components/play/PromptModal.tsx @@ -18,7 +18,7 @@ interface PromptModalProps { } export function PromptModal({ entry, onClose }: PromptModalProps) { - const [activeTab, setActiveTab] = useState<"actor" | "decoder">("actor"); + const [activeTab, setActiveTab] = useState("actor"); useEffect(() => { if (!entry.rawPrompt && entry.decoderPrompt) { @@ -53,6 +53,17 @@ export function PromptModal({ entry, onClose }: PromptModalProps) { const actorComponents = getComponents(entry.rawPrompt, "world"); const decoderComponents = getComponents(entry.decoderPrompt, "input"); + const isValidatorTab = activeTab.startsWith("validator-"); + const validatorIndex = isValidatorTab + ? parseInt(activeTab.substring("validator-".length), 10) + : -1; + const validatorCall = isValidatorTab + ? entry.validatorCalls?.find((c) => c.intentIndex === validatorIndex) + : null; + const validatorComponents = validatorCall + ? getComponents(validatorCall.prompt, "world") + : []; + return ( !open && onClose()}> @@ -67,6 +78,12 @@ export function PromptModal({ entry, onClose }: PromptModalProps) { onTabChange={setActiveTab} hasActor={!!entry.rawPrompt} hasDecoder={!!entry.decoderPrompt} + validatorCalls={ + entry.validatorCalls?.map((c) => ({ + intentIndex: c.intentIndex, + intentContent: c.intentContent, + })) || [] + } />
@@ -99,10 +116,42 @@ export function PromptModal({ entry, onClose }: PromptModalProps) { modelName={entry.decoderUsage?.modelName} providerInstanceName={entry.decoderUsage?.providerInstanceName} outputLabel="LLM Output (Decoded Intent Sequence)" - outputText={JSON.stringify(entry.intents, null, 2)} + outputText={JSON.stringify( + entry.decodedIntents || entry.intents, + null, + 2, + )} outputTokens={entry.decoderUsage?.outputTokens} /> )} + + {validatorCall && validatorCall.prompt && ( + + )} + + {validatorCall && !validatorCall.prompt && ( +
+ + Bypassed LLM Validation + +

+ {validatorCall.response.reason} +

+
+ )}
diff --git a/apps/gui/src/components/play/PromptSwitcher.tsx b/apps/gui/src/components/play/PromptSwitcher.tsx index a30da29..05ce8a6 100644 --- a/apps/gui/src/components/play/PromptSwitcher.tsx +++ b/apps/gui/src/components/play/PromptSwitcher.tsx @@ -1,10 +1,11 @@ "use client"; interface PromptSwitcherProps { - activeTab: "actor" | "decoder"; - onTabChange: (tab: "actor" | "decoder") => void; + activeTab: string; + onTabChange: (tab: any) => void; hasActor: boolean; hasDecoder: boolean; + validatorCalls?: { intentIndex: number; intentContent: string }[]; } export function PromptSwitcher({ @@ -12,32 +13,67 @@ export function PromptSwitcher({ onTabChange, hasActor, hasDecoder, + validatorCalls = [], }: PromptSwitcherProps) { return ( -
- - - +
+ {/* Primary Pipeline (Linear flow to the left) */} +
+ + + + + +
+ + {/* Branching Validator Column to the right of Intent Decoder */} + {validatorCalls.length > 0 && ( +
+ + +
+
+ {validatorCalls.map((call) => { + const tabKey = `validator-${call.intentIndex}`; + return ( + + ); + })} +
+
+
+ )}
); } diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index 35cb8ca..c1e53c2 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -20,6 +20,24 @@ export interface PromptBreakdown { components?: PromptComponent[]; } +export interface ValidatorCall { + intentIndex: number; + intentContent: string; + prompt?: PromptBreakdown; + response: { + isValid: boolean; + reason: string; + }; + usage?: { + inputTokens: number; + outputTokens: number; + totalTokens: number; + modelName?: string; + providerInstanceName?: string; + maxContext?: number; + }; +} + export interface LogEntry { turn: number; entityId: string; @@ -29,6 +47,8 @@ export interface LogEntry { timestamp: string; isHandoff?: boolean; handoffResult?: any; + decodedIntents?: IntentInfo[]; + validatorCalls?: ValidatorCall[]; rawPrompt?: PromptBreakdown; usage?: { inputTokens: number; From 01ec0621942f73cb13f7c3846585311e58ace086 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sun, 19 Jul 2026 21:55:12 +0530 Subject: [PATCH 14/15] fix(gui): Add hydration fallback in gui --- apps/gui/src/components/play/InteractView.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/gui/src/components/play/InteractView.tsx b/apps/gui/src/components/play/InteractView.tsx index 12c9024..68ae954 100644 --- a/apps/gui/src/components/play/InteractView.tsx +++ b/apps/gui/src/components/play/InteractView.tsx @@ -20,11 +20,13 @@ function IntentTag({ isSelf, playerAliases, playerId, + entities, }: { intent: SimSnapshot["log"][number]["intents"][number]; isSelf?: boolean; playerAliases: Record; playerId: string; + entities: SimSnapshot["entities"]; }) { const labels: Record = { monologue: "thought", @@ -40,7 +42,18 @@ function IntentTag({ outcome = intent.isValid ? " ✅" : ` ❌ (${intent.reason})`; } - const viewerAliasesMap = new Map(Object.entries(playerAliases || {})); + const viewerAliasesMap = new Map(); + if (entities) { + for (const ent of entities) { + viewerAliasesMap.set(ent.id, ent.name || ent.id); + } + } + if (playerAliases) { + for (const [targetId, alias] of Object.entries(playerAliases)) { + viewerAliasesMap.set(targetId, alias); + } + } + const viewerEntityMock = { id: playerId || "", aliases: viewerAliasesMap, @@ -86,12 +99,14 @@ function LogEntryCard({ isPlayerCard, playerAliases, playerId, + entities, }: { entry: SimSnapshot["log"][number]; onShowPrompt: (entry: SimSnapshot["log"][number]) => void; isPlayerCard: boolean; playerAliases: Record; playerId: string; + entities: SimSnapshot["entities"]; }) { const showMenu = !!(entry.rawPrompt || entry.decoderPrompt); @@ -135,6 +150,7 @@ function LogEntryCard({ isSelf={isPlayerCard} playerAliases={playerAliases} playerId={playerId} + entities={entities} /> ))}
@@ -213,6 +229,7 @@ export function InteractView({ isPlayerCard={entry.entityId === playerEntity?.id} playerAliases={playerAliases} playerId={playerId} + entities={snapshot.entities} /> ); })} From 8f6fe0dc28439ede5eea561fd3bcdb9db49831e2 Mon Sep 17 00:00:00 2001 From: rhit-lid2 <59123825+NeoLi00@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:18:42 +0530 Subject: [PATCH 15/15] chore: fix linting issues --- .../config/ProviderInstancesConfig.tsx | 10 +- apps/gui/src/components/play/HandoffModal.tsx | 111 ++++++++++-------- apps/gui/src/components/play/InteractView.tsx | 8 +- apps/gui/src/components/play/PromptModal.tsx | 6 +- .../src/components/play/PromptSwitcher.tsx | 2 +- apps/gui/src/components/ui/combobox.tsx | 1 - apps/gui/src/components/ui/input-group.tsx | 5 +- apps/gui/src/lib/simulation-types.ts | 9 +- apps/gui/src/lib/simulation/alias-handoff.ts | 2 +- apps/gui/src/lib/simulation/turn-executor.ts | 7 +- packages/actor/src/actor-prompt-builder.ts | 1 - packages/intent/tests/intent.test.ts | 2 +- packages/llm/src/llm.ts | 4 +- packages/llm/src/providers/mock.ts | 1 + packages/llm/tests/openai.test.ts | 1 + packages/memory/src/buffer.ts | 2 +- packages/memory/src/handoff.ts | 8 +- packages/voice/src/dehydration.ts | 6 +- packages/voice/src/hydration.ts | 10 +- tests/integration/actor-monologue.test.ts | 1 - tests/integration/game-loop.test.ts | 2 +- 21 files changed, 105 insertions(+), 94 deletions(-) diff --git a/apps/gui/src/components/config/ProviderInstancesConfig.tsx b/apps/gui/src/components/config/ProviderInstancesConfig.tsx index f405581..ad84f2b 100644 --- a/apps/gui/src/components/config/ProviderInstancesConfig.tsx +++ b/apps/gui/src/components/config/ProviderInstancesConfig.tsx @@ -43,13 +43,7 @@ import { CardTitle, CardAction, } from "@/components/ui/card"; -import { - Item, - ItemContent, - ItemGroup, - ItemTitle, - ItemDescription, -} from "@/components/ui/item"; +import { Item, ItemContent, ItemGroup, ItemTitle } from "@/components/ui/item"; import { Empty, EmptyTitle, EmptyDescription } from "@/components/ui/empty"; import { cn } from "@/lib/utils"; import { RefreshCwIcon } from "lucide-react"; @@ -200,14 +194,12 @@ export function ProviderInstancesConfig({ fetchModelsForExistingInstance(selectedInstanceId); } } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedInstanceId, instances, availableProviders]); // Re-fetch models when provider/key/endpoint changes on new instance form useEffect(() => { if (selectedInstanceId !== "new") return; fetchModelsForNewInstance(editProvider, editKey, editEndpointUrl); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [editProvider, editKey, editEndpointUrl, selectedInstanceId]); const handleProviderChange = (providerId: string | null) => { diff --git a/apps/gui/src/components/play/HandoffModal.tsx b/apps/gui/src/components/play/HandoffModal.tsx index c8186a4..55af187 100644 --- a/apps/gui/src/components/play/HandoffModal.tsx +++ b/apps/gui/src/components/play/HandoffModal.tsx @@ -126,66 +126,73 @@ export function HandoffModal({ entry, onClose }: HandoffModalProps) {

Memory Ledger Additions

- {chunks.map((chunk: any, index: number) => ( -
-
-
- {chunk.content} -
- - Importance: {chunk.importance} - -
- - {chunk.quotes && chunk.quotes.length > 0 && ( -
- {chunk.quotes.map((quote: string, qIdx: number) => ( -
“{quote}”
- ))} -
- )} - -
- {chunk.retainInBuffer ? ( + {chunks.map( + ( + chunk: { content: string; importance: number }, + index: number, + ) => ( +
+
+
+ {chunk.content} +
- Pinned in Buffer - - ) : ( - - Pruned from Buffer + Importance: {chunk.importance} +
+ + {chunk.quotes && chunk.quotes.length > 0 && ( +
+ {chunk.quotes.map((quote: string, qIdx: number) => ( +
“{quote}”
+ ))} +
)} - {chunk.involvedEntityIds && - chunk.involvedEntityIds.length > 0 && ( -
- Entities: - {chunk.involvedEntityIds.map((entId: string) => ( - - {entId} - - ))} -
+
+ {chunk.retainInBuffer ? ( + + Pinned in Buffer + + ) : ( + + Pruned from Buffer + )} + + {chunk.involvedEntityIds && + chunk.involvedEntityIds.length > 0 && ( +
+ Entities: + {chunk.involvedEntityIds.map( + (entId: string) => ( + + {entId} + + ), + )} +
+ )} +
-
- ))} + ), + )}
)}
diff --git a/apps/gui/src/components/play/InteractView.tsx b/apps/gui/src/components/play/InteractView.tsx index 68ae954..503b484 100644 --- a/apps/gui/src/components/play/InteractView.tsx +++ b/apps/gui/src/components/play/InteractView.tsx @@ -17,13 +17,11 @@ import { function IntentTag({ intent, - isSelf, playerAliases, playerId, entities, }: { intent: SimSnapshot["log"][number]["intents"][number]; - isSelf?: boolean; playerAliases: Record; playerId: string; entities: SimSnapshot["entities"]; @@ -59,7 +57,10 @@ function IntentTag({ aliases: viewerAliasesMap, }; - const textToDisplay = hydrate(intent.content, viewerEntityMock as any); + const textToDisplay = hydrate( + intent.content, + viewerEntityMock as unknown as Parameters[1], + ); const modifiersStr = intent.modifiers && intent.modifiers.length > 0 ? ( @@ -147,7 +148,6 @@ function LogEntryCard({ { if (!promptBreakdown) return []; @@ -66,7 +66,7 @@ export function PromptModal({ entry, onClose }: PromptModalProps) { return ( !open && onClose()}> - + Raw Prompts & Token Usage ({entry.entityName}) diff --git a/apps/gui/src/components/play/PromptSwitcher.tsx b/apps/gui/src/components/play/PromptSwitcher.tsx index 05ce8a6..b03677c 100644 --- a/apps/gui/src/components/play/PromptSwitcher.tsx +++ b/apps/gui/src/components/play/PromptSwitcher.tsx @@ -2,7 +2,7 @@ interface PromptSwitcherProps { activeTab: string; - onTabChange: (tab: any) => void; + onTabChange: (tab: string) => void; hasActor: boolean; hasDecoder: boolean; validatorCalls?: { intentIndex: number; intentContent: string }[]; diff --git a/apps/gui/src/components/ui/combobox.tsx b/apps/gui/src/components/ui/combobox.tsx index 52d5399..a4d0ebe 100644 --- a/apps/gui/src/components/ui/combobox.tsx +++ b/apps/gui/src/components/ui/combobox.tsx @@ -5,7 +5,6 @@ import { Combobox as ComboboxPrimitive } from "@base-ui/react"; import { CheckIcon, ChevronDownIcon, XIcon } from "lucide-react"; import { cn } from "@/lib/utils"; -import { Button } from "@/components/ui/button"; import { InputGroup, InputGroupAddon, diff --git a/apps/gui/src/components/ui/input-group.tsx b/apps/gui/src/components/ui/input-group.tsx index d005a09..2caa3ed 100644 --- a/apps/gui/src/components/ui/input-group.tsx +++ b/apps/gui/src/components/ui/input-group.tsx @@ -99,12 +99,13 @@ function InputGroupButton({ return React.cloneElement(render, { className: cn( inputGroupButtonVariants({ size }), - (render.props as any)?.className, + (render.props as Record)?.className as + string | undefined, className, ), type, ...props, - } as any); + } as Record as React.HTMLAttributes); } return ( diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts index c1e53c2..81037f3 100644 --- a/apps/gui/src/lib/simulation-types.ts +++ b/apps/gui/src/lib/simulation-types.ts @@ -38,6 +38,13 @@ export interface ValidatorCall { }; } +export interface HandoffResult { + chunks: { + content: string; + importance: number; + }[]; +} + export interface LogEntry { turn: number; entityId: string; @@ -46,7 +53,7 @@ export interface LogEntry { intents: IntentInfo[]; timestamp: string; isHandoff?: boolean; - handoffResult?: any; + handoffResult?: HandoffResult; decodedIntents?: IntentInfo[]; validatorCalls?: ValidatorCall[]; rawPrompt?: PromptBreakdown; diff --git a/apps/gui/src/lib/simulation/alias-handoff.ts b/apps/gui/src/lib/simulation/alias-handoff.ts index 22e6ebe..f80bc03 100644 --- a/apps/gui/src/lib/simulation/alias-handoff.ts +++ b/apps/gui/src/lib/simulation/alias-handoff.ts @@ -39,7 +39,7 @@ export async function runHandoffResolution(session: SimSession): Promise { worldState.clock.get(), ); if (ran) { - const lastResult = (handoffEngine as any).lastResult; + const lastResult = handoffEngine.lastResult; const lastCall = session.handoffProvider.lastCalls?.[ (session.handoffProvider.lastCalls?.length || 0) - 1 diff --git a/apps/gui/src/lib/simulation/turn-executor.ts b/apps/gui/src/lib/simulation/turn-executor.ts index 60dcb8c..edaa2b9 100644 --- a/apps/gui/src/lib/simulation/turn-executor.ts +++ b/apps/gui/src/lib/simulation/turn-executor.ts @@ -68,11 +68,8 @@ async function processIntents( minutesToAdvance: outcome.timeDelta?.minutesToAdvance, }); - if ( - intent.type === "action" && - (session.architect.validator as any).lastResult - ) { - const lastResult = (session.architect.validator as any).lastResult; + if (intent.type === "action" && session.architect.validator.lastResult) { + const lastResult = session.architect.validator.lastResult; let usage = undefined; if ( session.validatorProvider.lastCalls && diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts index 2cd0676..ab4ff82 100644 --- a/packages/actor/src/actor-prompt-builder.ts +++ b/packages/actor/src/actor-prompt-builder.ts @@ -4,7 +4,6 @@ import { WorldState, naturalizeTime, serializeSubjectiveWorldState, - resolveAlias, } from "@omnia/core"; import { BufferEntry, diff --git a/packages/intent/tests/intent.test.ts b/packages/intent/tests/intent.test.ts index b9ed1b3..88a1e88 100644 --- a/packages/intent/tests/intent.test.ts +++ b/packages/intent/tests/intent.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "vitest"; import { WorldState, Entity } from "@omnia/core"; import { MockLLMProvider } from "@omnia/llm"; -import { IntentDecoder, IntentSequence } from "@omnia/intent"; +import { IntentDecoder } from "@omnia/intent"; describe("IntentDecoder Unit Tests (Tier 1)", () => { test("decodes prose with a single action intent", async () => { diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index f470fe2..fb7b1ca 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -13,7 +13,7 @@ export interface PromptBreakdown { components?: PromptComponent[]; } -export interface IPromptBuilder { +export interface IPromptBuilder { build(...args: TArgs): PromptBreakdown; } @@ -49,7 +49,7 @@ export interface LLMCallRecord { providerInstanceName?: string; maxContext?: number; }; - response?: any; + response?: unknown; } export interface ILLMProvider { diff --git a/packages/llm/src/providers/mock.ts b/packages/llm/src/providers/mock.ts index 116cffb..8c2fde0 100644 --- a/packages/llm/src/providers/mock.ts +++ b/packages/llm/src/providers/mock.ts @@ -30,6 +30,7 @@ export class MockLLMProvider implements ILLMProvider { registerGenerative("mock", () => new MockLLMProvider([])); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars static create(inst: ModelProviderInstance): ILLMProvider { return new MockLLMProvider([]); } diff --git a/packages/llm/tests/openai.test.ts b/packages/llm/tests/openai.test.ts index b09f2a7..0a01413 100644 --- a/packages/llm/tests/openai.test.ts +++ b/packages/llm/tests/openai.test.ts @@ -66,6 +66,7 @@ vi.mock("@langchain/openai", () => { constructor(config: unknown) { this.config = config; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars embedQuery = vi.fn().mockImplementation(async (text: string) => { return [0.1, 0.2, 0.3]; }); diff --git a/packages/memory/src/buffer.ts b/packages/memory/src/buffer.ts index 9bef288..a3c981e 100644 --- a/packages/memory/src/buffer.ts +++ b/packages/memory/src/buffer.ts @@ -1,5 +1,5 @@ import Database from "better-sqlite3"; -import { Entity, resolveAlias } from "@omnia/core"; +import { Entity } from "@omnia/core"; import { Intent } from "@omnia/intent"; import { hydrate } from "@omnia/voice"; diff --git a/packages/memory/src/handoff.ts b/packages/memory/src/handoff.ts index a51ba10..a31309c 100644 --- a/packages/memory/src/handoff.ts +++ b/packages/memory/src/handoff.ts @@ -208,7 +208,7 @@ export interface HandoffRunResult { systemPrompt?: string; userContext?: string; promptComponents?: PromptComponent[]; - response?: any; + response?: unknown; } export class HandoffEngine { @@ -267,7 +267,11 @@ export class HandoffEngine { }; const result = response.data; - const db = (this.bufferRepo as any).db; + const db = ( + this.bufferRepo as unknown as { + db: { transaction: (fn: () => void) => () => void }; + } + ).db; const ledgerEntries: LedgerEntry[] = []; for (const chunk of result.chunks) { diff --git a/packages/voice/src/dehydration.ts b/packages/voice/src/dehydration.ts index 53f3557..d0de255 100644 --- a/packages/voice/src/dehydration.ts +++ b/packages/voice/src/dehydration.ts @@ -10,7 +10,6 @@ export function splitQuotes(text: string): Segment[] { const segments: Segment[] = []; let current = ""; let inQuote = false; - let quoteChar = ""; for (let i = 0; i < text.length; i++) { const char = text[i]; @@ -78,7 +77,10 @@ export function dehydrate( // 2. Replace names and aliases with entity@[name] sortedNames.forEach((name) => { const id = nameToId.get(name)!; - const escapedName = name.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); + const escapedName = name.replace( + new RegExp("[-/\\\\^$*+?.()|[\\]{}]", "g"), + "\\$&", + ); const regex = new RegExp(`\\b${escapedName}\\b`, "gi"); text = text.replace(regex, (matched) => { diff --git a/packages/voice/src/hydration.ts b/packages/voice/src/hydration.ts index 741d158..be01709 100644 --- a/packages/voice/src/hydration.ts +++ b/packages/voice/src/hydration.ts @@ -48,7 +48,7 @@ export function hydrate(content: string, viewer: Entity): string { return seg.text.replace(regex, (matchStr, id, original, followingWord) => { const isSelf = id === viewer.id; const lowerOriginal = original.toLowerCase(); - let resolvedSubject = ""; + let resolvedSubject: string; let isThirdPersonSingular = false; if (isSelf) { @@ -129,7 +129,8 @@ export function hydrate(content: string, viewer: Entity): string { if (followingWord) { if (isThirdPersonSingular) { - const conj = nlp(followingWord).verbs().conjugate()[0] as any; + const conj = nlp(followingWord).verbs().conjugate()[0] as + { Infinitive?: string; PresentTense?: string } | undefined; if (conj && conj.Infinitive === followingWord && conj.PresentTense) { return `${resolvedSubject} ${conj.PresentTense}`; } @@ -194,7 +195,7 @@ export function hydrateObjective( const entity = worldState.getEntity(id); const name = entity?.attributes.get("name")?.getValue() || id; const lowerOriginal = original.toLowerCase(); - let resolvedSubject = ""; + let resolvedSubject: string; let isThirdPersonSingular = false; if (firstPersonSet.has(lowerOriginal)) { @@ -220,7 +221,8 @@ export function hydrateObjective( if (followingWord) { if (isThirdPersonSingular) { - const conj = nlp(followingWord).verbs().conjugate()[0] as any; + const conj = nlp(followingWord).verbs().conjugate()[0] as + { Infinitive?: string; PresentTense?: string } | undefined; if (conj && conj.Infinitive === followingWord && conj.PresentTense) { return `${resolvedSubject} ${conj.PresentTense}`; } diff --git a/tests/integration/actor-monologue.test.ts b/tests/integration/actor-monologue.test.ts index 2dffc44..9f713cc 100644 --- a/tests/integration/actor-monologue.test.ts +++ b/tests/integration/actor-monologue.test.ts @@ -7,7 +7,6 @@ import { AttributeVisibility, } from "@omnia/core"; import { MockLLMProvider } from "@omnia/llm"; -import { IntentSequence } from "@omnia/intent"; import { Architect } from "@omnia/architect"; import { BufferRepository, BufferEntry } from "@omnia/memory"; import { diff --git a/tests/integration/game-loop.test.ts b/tests/integration/game-loop.test.ts index 422dbe9..852cadd 100644 --- a/tests/integration/game-loop.test.ts +++ b/tests/integration/game-loop.test.ts @@ -2,7 +2,7 @@ import { describe, test, expect } from "vitest"; import Database from "better-sqlite3"; import { WorldState, Entity, SQLiteRepository } from "@omnia/core"; import { MockLLMProvider } from "@omnia/llm"; -import { IntentDecoder, IntentSequence } from "@omnia/intent"; +import { IntentDecoder } from "@omnia/intent"; import { Architect } from "@omnia/architect"; describe("Game Loop Integration Tests (Tier 2)", () => {