FEAT!(voice): Implement intent hydration, dehydration system fixes: #29

This commit is contained in:
2026-07-19 13:13:07 +05:30
parent 84bff92631
commit a4b620502a
38 changed files with 622 additions and 211 deletions

View File

@@ -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",

View File

@@ -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<string, string>;
playerId: string;
}) {
const labels: Record<string, string> = {
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<string, string>;
playerId: string;
}) {
const showMenu = !!(entry.rawPrompt || entry.decoderPrompt);
@@ -117,7 +129,13 @@ function LogEntryCard({
</div>
<div className="flex flex-col gap-1.5 mt-2 border-t border-dotted border-border/10 pt-2">
{entry.intents.map((intent, i) => (
<IntentTag key={i} intent={intent} isSelf={isPlayerCard} />
<IntentTag
key={i}
intent={intent}
isSelf={isPlayerCard}
playerAliases={playerAliases}
playerId={playerId}
/>
))}
</div>
</div>
@@ -184,12 +202,17 @@ export function InteractView({
</Alert>
);
}
const playerAliases = playerEntity?.aliases || {};
const playerId = playerEntity?.id || "";
return (
<LogEntryCard
key={i}
entry={entry}
onShowPrompt={onShowPrompt}
isPlayerCard={entry.entityId === playerEntity?.id}
playerAliases={playerAliases}
playerId={playerId}
/>
);
})}

View File

@@ -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<string, string>;
}
export interface WaitingContext {

View File

@@ -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<string, string> = {};
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,

View File

@@ -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,

View File

@@ -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": []
}

View File

@@ -11,6 +11,7 @@
"@omnia/intent": "workspace:*",
"@omnia/llm": "workspace:*",
"@omnia/memory": "workspace:*",
"@omnia/voice": "workspace:*",
"zod": "^4.4.3"
}
}

View File

@@ -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})`;
}

View File

@@ -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."');
});

View File

@@ -9,6 +9,7 @@
{ "path": "../core" },
{ "path": "../intent" },
{ "path": "../llm" },
{ "path": "../memory" }
{ "path": "../memory" },
{ "path": "../voice" }
]
}

View File

@@ -10,6 +10,7 @@
"@omnia/core": "workspace:*",
"@omnia/llm": "workspace:*",
"@omnia/intent": "workspace:*",
"@omnia/voice": "workspace:*",
"zod": "^4.4.3"
}
}

View File

@@ -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();

View File

@@ -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.

View File

@@ -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: [],

View File

@@ -8,6 +8,7 @@
"references": [
{ "path": "../core" },
{ "path": "../llm" },
{ "path": "../intent" }
{ "path": "../intent" },
{ "path": "../voice" }
]
}

View File

@@ -9,6 +9,7 @@
"dependencies": {
"@omnia/core": "workspace:*",
"@omnia/llm": "workspace:*",
"@omnia/voice": "workspace:*",
"zod": "^4.4.3"
}
}

View File

@@ -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<string, string> = {};
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,

View File

@@ -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,

View File

@@ -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 () => {

View File

@@ -5,5 +5,9 @@
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "../core" }, { "path": "../llm" }]
"references": [
{ "path": "../core" },
{ "path": "../llm" },
{ "path": "../voice" }
]
}

View File

@@ -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,

View File

@@ -10,6 +10,7 @@
"@omnia/core": "workspace:*",
"@omnia/intent": "workspace:*",
"@omnia/llm": "workspace:*",
"@omnia/voice": "workspace:*",
"zod": "^4.4.3"
}
}

View File

@@ -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 {

View File

@@ -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"],
},

View File

@@ -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: [],
},

View File

@@ -8,6 +8,7 @@
"references": [
{ "path": "../core" },
{ "path": "../intent" },
{ "path": "../llm" }
{ "path": "../llm" },
{ "path": "../voice" }
]
}

View File

@@ -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,

View File

@@ -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()),

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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"
}
}

View File

@@ -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@<id>[original] placeholder tags.
*/
export function dehydrate(
content: string,
sourceId: string,
targetIds: string[],
aliasMap: Record<string, string>,
): 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<string, string>();
// 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@<id>[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("");
}

View File

@@ -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@<id>[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@<id>[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@<id>[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@<id>[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("");
}

View File

@@ -0,0 +1,2 @@
export * from "./dehydration.js";
export * from "./hydration.js";

View File

@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"],
"references": [{ "path": "../core" }]
}

24
pnpm-lock.yaml generated
View File

@@ -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":

View File

@@ -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: [],

View File

@@ -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: [],