diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx
index 1b102aa..8bbb5fe 100644
--- a/apps/gui/src/components/play/PlayView.tsx
+++ b/apps/gui/src/components/play/PlayView.tsx
@@ -39,9 +39,15 @@ function IntentTag({
? intent.selfDescription
: intent.description;
+ const modifiersStr = intent.modifiers && intent.modifiers.length > 0 ? (
+
+ ({intent.modifiers.join(", ")})
+
+ ) : null;
+
return (
- [{label}] “{textToDisplay}”{outcome}
+ [{label}] “{textToDisplay}”{modifiersStr}{outcome}
{intent.minutesToAdvance ? ` [+${intent.minutesToAdvance}min]` : ""}
);
diff --git a/apps/gui/src/lib/simulation-types.ts b/apps/gui/src/lib/simulation-types.ts
index 33028fe..9cd87ce 100644
--- a/apps/gui/src/lib/simulation-types.ts
+++ b/apps/gui/src/lib/simulation-types.ts
@@ -2,6 +2,7 @@ export interface IntentInfo {
type: string;
description: string;
selfDescription?: string;
+ modifiers: string[];
targetIds: string[];
isValid?: boolean;
reason?: string;
diff --git a/apps/gui/src/lib/simulation.ts b/apps/gui/src/lib/simulation.ts
index 882239c..5c94161 100644
--- a/apps/gui/src/lib/simulation.ts
+++ b/apps/gui/src/lib/simulation.ts
@@ -386,6 +386,7 @@ class SimulationManager {
type: intent.type,
description: intent.description,
selfDescription: intent.selfDescription,
+ modifiers: intent.modifiers || [],
targetIds: intent.targetIds,
isValid: outcome.isValid,
reason: outcome.reason,
@@ -530,6 +531,7 @@ class SimulationManager {
type: intent.type,
description: intent.description,
selfDescription: intent.selfDescription,
+ modifiers: intent.modifiers || [],
targetIds: intent.targetIds,
isValid: outcome.isValid,
reason: outcome.reason,
diff --git a/content/demo/scenarios/talking-room.json b/content/demo/scenarios/talking-room.json
index ff28e58..bbec88d 100644
--- a/content/demo/scenarios/talking-room.json
+++ b/content/demo/scenarios/talking-room.json
@@ -11,12 +11,6 @@
"visibility": "PRIVATE",
"allowedEntities": []
},
- {
- "name": "observation_status",
- "value": "Active monitoring. Audio and visual feeds online.",
- "visibility": "PRIVATE",
- "allowedEntities": []
- },
{
"name": "ambient_sound",
"value": "A low, barely audible electrical hum.",
@@ -54,6 +48,11 @@
"visibility": "PRIVATE",
"allowedEntities": ["7c9b83b3-8cfb-4e89-8d77-626a5757d591"]
},
+ {
+ "name": "gender",
+ "value": "male",
+ "visibility": "PUBLIC"
+ },
{
"name": "appearance",
"value": "A tall human with short dark hair and alert eyes, standing near the center of the room.",
@@ -96,6 +95,11 @@
"visibility": "PRIVATE",
"allowedEntities": ["bf3f29d2-cf11-4b11-9a99-b13c126d400e"]
},
+ {
+ "name": "gender",
+ "value": "male",
+ "visibility": "PUBLIC"
+ },
{
"name": "appearance",
"value": "A medium-build human with long blonde hair tied back, sitting with their back pressed against the white wall.",
diff --git a/packages/actor/src/actor-prompt-builder.ts b/packages/actor/src/actor-prompt-builder.ts
index d4ac976..485fee7 100644
--- a/packages/actor/src/actor-prompt-builder.ts
+++ b/packages/actor/src/actor-prompt-builder.ts
@@ -64,16 +64,17 @@ You are an actor agent embodying a single character in a narrative simulation. Y
Your output is a short block of narrative prose describing what your character does, says, or thinks next. You may:
- Speak aloud → Other entities can hear it if they are present nearby. (Or nobody will hear it if you are alone)
- Perform a physical action → It is subject to the world's physics and logic. Do not describe the outcome of your action.
-- Think internally / reflect / feel → this is a "monologue". NO ONE else perceives it. This is what you think internally, maybe about another event or something that just happened to you.
+- Think internally / reflect / feel → this is a "monologue". NO ONE else perceives it. This is what you think internally.
Guidelines:
-- Always write in the first person (e.g., "I do this", "I say", "I think").
+- 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.
-- Stay strictly within what your character knows. Do not invent knowledge that doesn't exist or act on it.
- 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.
".
`.trim();
}
diff --git a/packages/architect/tests/architect.test.ts b/packages/architect/tests/architect.test.ts
index 119c9aa..9b07b2b 100644
--- a/packages/architect/tests/architect.test.ts
+++ b/packages/architect/tests/architect.test.ts
@@ -25,6 +25,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => {
selfDescription: "You open the chest and read the scroll.",
actorId: "alice",
targetIds: [],
+ modifiers: [],
};
const result = await architect.validateIntent(world, intent);
@@ -55,6 +56,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => {
selfDescription: "You unlock the gate and escape.",
actorId: "bob",
targetIds: [],
+ modifiers: [],
};
const result = await architect.validateIntent(world, intent);
@@ -77,6 +79,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => {
selfDescription: "You haunt the mansion.",
actorId: "ghost",
targetIds: [],
+ modifiers: [],
};
const result = await architect.validateIntent(world, intent);
@@ -116,6 +119,7 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
selfDescription: "You pick the lock of the wooden chest.",
actorId: "alice",
targetIds: [],
+ modifiers: [],
};
const result = await architect.processIntent(world, intent);
@@ -159,6 +163,7 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
selfDescription: "You run away.",
actorId: "bob",
targetIds: [],
+ modifiers: [],
};
const result = await architect.processIntent(world, intent);
diff --git a/packages/intent/src/intent-decoder.ts b/packages/intent/src/intent-decoder.ts
index d30f3a8..4c73b5c 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 { ILLMProvider } from "@omnia/llm";
-import { IntentSequence, IntentSequenceSchema } from "./intent.js";
+import { IntentSequence, LLMIntentSequenceSchema } from "./intent.js";
export class IntentDecoder {
constructor(private llmProvider: ILLMProvider) {}
@@ -23,9 +23,15 @@ export class IntentDecoder {
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)";
+ const aliasContext =
+ aliasEntries.length > 0
+ ? aliasEntries
+ .map(
+ ([targetId, alias]) =>
+ `- "${alias}" refers to entity ID: "${targetId}"`,
+ )
+ .join("\n")
+ : "(No known aliases)";
const systemPrompt = `
You are the Intent Decoder for a narrative simulation engine.
@@ -33,22 +39,16 @@ 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": Any speech, conversation, or verbal communication directed at another entity.
- - "action": Any physical or logical action performed in the world (e.g., moving, picking up, opening, looking).
- - "monologue": An inner thought, reflection, or internal monologue. This is purely internal — not spoken aloud, not perceivable by any other entity, and not a physical action. Use this for any prose depicting the character thinking, reflecting, feeling, or narrating to themselves internally.
+ - "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.
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"), so it can be composed with any observer's alias for the actor.
+ - "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.
-4. Identify the actorId (the entity performing the intent — this will always be "${actorId}").
-5. Identify targetIds — the entity IDs of the receiving parties. Use the "KNOWN ENTITY IDS" and "ACTOR ALIASES" mapping to resolve any subjective names, descriptions, or nicknames used in the prose to their correct system entity IDs. If no specific target, use an empty array. For "monologue" intents, targetIds must always be an empty array.
-
-Rules:
-- Preserve the chronological order of intents as they appear in the prose.
-- Do NOT merge unrelated actions into a single intent.
-- Dialogue and actions should be separate intents even if they happen in the same sentence.
-- If the prose contains only dialogue, return a single dialogue intent.
-- If the prose contains only a single action, return a single action intent.
+ - 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.
+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 = `
@@ -72,7 +72,7 @@ ${narrativeProse}
const response = await this.llmProvider.generateStructuredResponse({
systemPrompt,
userContext,
- schema: IntentSequenceSchema,
+ schema: LLMIntentSequenceSchema,
});
if (!response.success || !response.data) {
@@ -81,7 +81,14 @@ ${narrativeProse}
);
}
- return response.data;
+ const fullIntents = response.data.intents.map((intent) => ({
+ ...intent,
+ actorId,
+ }));
+
+ return {
+ intents: fullIntents,
+ };
}
}
@@ -102,7 +109,9 @@ function serializeSimplifiedWorldState(worldState: WorldState): string {
lines.push("Entities:");
if (worldState.entities.size > 0) {
for (const entity of worldState.entities.values()) {
- const locStr = entity.locationId ? ` (Location: ${entity.locationId})` : "";
+ const locStr = entity.locationId
+ ? ` (Location: ${entity.locationId})`
+ : "";
lines.push(` - Entity [ID: ${entity.id}]${locStr}`);
}
} else {
diff --git a/packages/intent/src/intent.ts b/packages/intent/src/intent.ts
index 2f3e6b9..dc42974 100644
--- a/packages/intent/src/intent.ts
+++ b/packages/intent/src/intent.ts
@@ -14,7 +14,7 @@ export type IntentType = z.infer;
/**
* A single decoded intent extracted from narrative prose.
*/
-export const IntentSchema = z.object({
+export const LLMIntentSchema = z.object({
/** The type of intent. */
type: IntentTypeSchema,
@@ -27,19 +27,31 @@ export const IntentSchema = z.object({
/** The same event from the actor's own perspective (second person, "You"). */
selfDescription: z.string(),
- /** The entity ID of the actor performing the intent. */
- actorId: z.string(),
-
/**
* 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.
*/
targetIds: z.array(z.string()),
+
+ /**
+ * Additional qualities or modifiers extracted from the prose (e.g., emotions,
+ * questions, speed, manner of action like 'quietly', 'whispering', 'anxiously').
+ */
+ modifiers: z.array(z.string()),
+});
+
+export const IntentSchema = LLMIntentSchema.extend({
+ /** The entity ID of the actor performing the intent. */
+ actorId: z.string(),
});
export type Intent = z.infer;
+export const LLMIntentSequenceSchema = z.object({
+ intents: z.array(LLMIntentSchema),
+});
+
/**
* The full output of the Intent Decoder: an ordered sequence of intents
* extracted from a single narrative prose block.
diff --git a/packages/intent/tests/intent.test.ts b/packages/intent/tests/intent.test.ts
index d39db8b..0879bc2 100644
--- a/packages/intent/tests/intent.test.ts
+++ b/packages/intent/tests/intent.test.ts
@@ -16,8 +16,8 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => {
originalText: "Alice opened the chest.",
description: "Open the wooden chest.",
selfDescription: "You open the wooden chest.",
- actorId: "alice",
targetIds: [],
+ modifiers: [],
},
],
};
@@ -47,8 +47,8 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => {
originalText: '"Do you have the key?" Alice asked Bob.',
description: "Alice asks Bob if he has the key.",
selfDescription: "You ask Bob if he has the key.",
- actorId: "alice",
targetIds: ["bob"],
+ modifiers: [],
},
],
};
@@ -81,16 +81,16 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => {
originalText: '"Cover me," Alice whispered to Bob.',
description: "Alice whispers to Bob requesting cover.",
selfDescription: "You whisper to Bob requesting cover.",
- actorId: "alice",
targetIds: ["bob"],
+ 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.",
- actorId: "alice",
targetIds: [],
+ modifiers: [],
},
],
};
diff --git a/packages/memory/tests/memory.test.ts b/packages/memory/tests/memory.test.ts
index 411d5e8..5e2e433 100644
--- a/packages/memory/tests/memory.test.ts
+++ b/packages/memory/tests/memory.test.ts
@@ -36,6 +36,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
selfDescription: "You say, 'Hello there' to the bartender.",
actorId: "bob",
targetIds: ["charlie"],
+ modifiers: [],
},
};
@@ -59,6 +60,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
selfDescription: "You attempt to break the lock latch.",
actorId: "bob",
targetIds: [],
+ modifiers: [],
},
outcome: {
isValid: false,
@@ -85,6 +87,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
selfDescription: "You open the window.",
actorId: "alice",
targetIds: [],
+ modifiers: [],
},
};
@@ -103,6 +106,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
selfDescription: "You knock on the door.",
actorId: "stranger-1",
targetIds: [],
+ modifiers: [],
},
};
@@ -130,6 +134,7 @@ describe("BufferRepository Persistence Tests (Tier 1)", () => {
selfDescription: "You gather a stick.",
actorId: "alice",
targetIds: [],
+ modifiers: [],
};
const entry: BufferEntry = {
diff --git a/packages/scenario/tests/scenario.test.ts b/packages/scenario/tests/scenario.test.ts
index 3cbce78..49115e4 100644
--- a/packages/scenario/tests/scenario.test.ts
+++ b/packages/scenario/tests/scenario.test.ts
@@ -60,6 +60,7 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
selfDescription: "You entered the house.",
actorId: "investigator",
targetIds: [],
+ modifiers: [],
},
},
],
diff --git a/tests/integration/actor-monologue.test.ts b/tests/integration/actor-monologue.test.ts
index da8d166..0b509ec 100644
--- a/tests/integration/actor-monologue.test.ts
+++ b/tests/integration/actor-monologue.test.ts
@@ -65,6 +65,7 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => {
selfDescription: "You internally reflect that Bob has not noticed you.",
actorId: "alice",
targetIds: [],
+ modifiers: [],
},
{
type: "dialogue",
@@ -73,6 +74,7 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => {
selfDescription: "You softly call out to Bob.",
actorId: "alice",
targetIds: ["bob"],
+ modifiers: [],
},
{
type: "action",
@@ -81,6 +83,7 @@ describe("Actor Agent + Monologue Intent Integration (Tier 2)", () => {
selfDescription: "You 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 7784db4..05b4ed0 100644
--- a/tests/integration/game-loop.test.ts
+++ b/tests/integration/game-loop.test.ts
@@ -36,6 +36,7 @@ describe("Omnia Integration Tests (Tier 2)", () => {
selfDescription: "You whisper to Bob to cover you.",
actorId: "alice",
targetIds: ["bob"],
+ modifiers: [],
},
{
type: "action",
@@ -44,6 +45,7 @@ describe("Omnia Integration Tests (Tier 2)", () => {
selfDescription: "You creep to the door and pull the handle.",
actorId: "alice",
targetIds: [],
+ modifiers: [],
},
],
};
@@ -113,6 +115,7 @@ describe("Omnia Integration Tests (Tier 2)", () => {
selfDescription: "You attempt to pick the lock with a hairpin.",
actorId: "alice",
targetIds: [],
+ modifiers: [],
};
const intent2 = {
@@ -122,6 +125,7 @@ describe("Omnia Integration Tests (Tier 2)", () => {
selfDescription: "You mutter to yourself.",
actorId: "alice",
targetIds: [],
+ modifiers: [],
};
// LLM validation / time delta mock responses: