refactor(architect): Add self description for intent decoder and refine context (#22)

This commit is contained in:
2026-07-11 09:11:49 +05:30
parent 9bdc3ca04b
commit bfff93e793
12 changed files with 106 additions and 36 deletions

View File

@@ -22,6 +22,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => {
type: "action",
originalText: "open the chest and read the scroll",
description: "Open the chest and read the scroll",
selfDescription: "You open the chest and read the scroll.",
actorId: "alice",
targetIds: [],
};
@@ -51,6 +52,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => {
type: "action",
originalText: "unlock the gate and escape",
description: "Unlock the gate and escape",
selfDescription: "You unlock the gate and escape.",
actorId: "bob",
targetIds: [],
};
@@ -72,6 +74,7 @@ describe("Architect & LLMValidator Unit Tests (Tier 1)", () => {
type: "action",
originalText: "haunt the mansion",
description: "Haunt the mansion",
selfDescription: "You haunt the mansion.",
actorId: "ghost",
targetIds: [],
};
@@ -110,6 +113,7 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
type: "action",
originalText: "pick the lock of the wooden chest",
description: "Pick the lock of the wooden chest",
selfDescription: "You pick the lock of the wooden chest.",
actorId: "alice",
targetIds: [],
};
@@ -152,6 +156,7 @@ describe("TimeDeltaGenerator & Architect.processIntent Unit Tests (Tier 1)", ()
type: "action",
originalText: "run away",
description: "Run away",
selfDescription: "You run away.",
actorId: "bob",
targetIds: [],
};

View File

@@ -1,4 +1,4 @@
import { WorldState, serializeObjectiveWorldState } from "@omnia/core";
import { WorldState } from "@omnia/core";
import { ILLMProvider } from "@omnia/llm";
import { IntentSequence, IntentSequenceSchema } from "./intent.js";
@@ -37,7 +37,9 @@ For each intent you must:
- "action": Any physical or logical action performed in the world (e.g., moving, picking up, opening, looking).
- "monologue": An inner thought, reflection, or internal monologue. This is purely internal — not spoken aloud, not perceivable by any other entity, and not a physical action. Use this for any prose depicting the character thinking, reflecting, feeling, or narrating to themselves internally.
2. Extract the original text fragment from the prose that corresponds to this intent.
3. Write a concise, structured description of the intent (what is being done or said). Include as much detail about the action as possible that was extracted from the narrative prose. Do not make up qualities.
3. Populate "description" and "selfDescription":
- "description": No subject or name — a bare third-person verb phrase only (e.g. "clears their throat", "shakes their head slowly"), so it can be composed with any observer's alias for the actor.
- "selfDescription": The same event from the actor's own perspective, second person, complete sentence starting with "You" (e.g. "You clear your throat.", "You shake your head slowly."). This is shown directly in the actor's own memory — it must never say "the actor" or refer to them in the third person.
4. Identify the actorId (the entity performing the intent — this will always be "${actorId}").
5. Identify targetIds — the entity IDs of the receiving parties. Use the "KNOWN ENTITY IDS" and "ACTOR ALIASES" mapping to resolve any subjective names, descriptions, or nicknames used in the prose to their correct system entity IDs. If no specific target, use an empty array. For "monologue" intents, targetIds must always be an empty array.
@@ -58,7 +60,7 @@ The actor refers to other entities using these subjective names/aliases:
${aliasContext}
=== WORLD STATE ===
${serializeObjectiveWorldState(worldState)}
${serializeSimplifiedWorldState(worldState)}
=== ACTOR ===
Actor ID: ${actorId}
@@ -82,3 +84,30 @@ ${narrativeProse}
return response.data;
}
}
function serializeSimplifiedWorldState(worldState: WorldState): string {
const lines: string[] = [];
lines.push("Locations:");
if (worldState.locations.size > 0) {
for (const loc of worldState.locations.values()) {
const parentId = (loc as { parentId?: string | null }).parentId;
const parentStr = parentId ? ` (Parent: ${parentId})` : "";
lines.push(` - Location [ID: ${loc.id}]${parentStr}`);
}
} else {
lines.push(" (No locations)");
}
lines.push("Entities:");
if (worldState.entities.size > 0) {
for (const entity of worldState.entities.values()) {
const locStr = entity.locationId ? ` (Location: ${entity.locationId})` : "";
lines.push(` - Entity [ID: ${entity.id}]${locStr}`);
}
} else {
lines.push(" (No entities)");
}
return lines.join("\n");
}

View File

@@ -24,6 +24,9 @@ export const IntentSchema = z.object({
/** A concise, structured description of the intent's action or dialogue. */
description: z.string(),
/** The same event from the actor's own perspective (second person, "You"). */
selfDescription: z.string(),
/** The entity ID of the actor performing the intent. */
actorId: z.string(),

View File

@@ -15,6 +15,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => {
type: "action",
originalText: "Alice opened the chest.",
description: "Open the wooden chest.",
selfDescription: "You open the wooden chest.",
actorId: "alice",
targetIds: [],
},
@@ -45,6 +46,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => {
type: "dialogue",
originalText: '"Do you have the key?" Alice asked Bob.',
description: "Alice asks Bob if he has the key.",
selfDescription: "You ask Bob if he has the key.",
actorId: "alice",
targetIds: ["bob"],
},
@@ -78,6 +80,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => {
type: "dialogue",
originalText: '"Cover me," Alice whispered to Bob.',
description: "Alice whispers to Bob requesting cover.",
selfDescription: "You whisper to Bob requesting cover.",
actorId: "alice",
targetIds: ["bob"],
},
@@ -85,6 +88,7 @@ describe("IntentDecoder Unit Tests (Tier 1)", () => {
type: "action",
originalText: "She crept towards the door and pulled the handle.",
description: "Creep towards the door and pull the handle.",
selfDescription: "You creep towards the door and pull the handle.",
actorId: "alice",
targetIds: [],
},

View File

@@ -25,27 +25,28 @@ export function serializeSubjectiveBufferEntry(
entry: BufferEntry,
viewer: Entity,
): string {
const actorAlias = resolveAlias(viewer, entry.intent.actorId);
const isSelf = viewer.id === entry.intent.actorId;
const targetAliases = entry.intent.targetIds.map((tid) =>
resolveAlias(viewer, tid),
);
let details: string;
const content = entry.intent.description.trim() || entry.intent.originalText.trim();
if (entry.intent.type === "dialogue") {
details = `spoke to ${targetAliases.join(", ") || "someone"}: "${content}"`;
} else if (entry.intent.type === "monologue") {
details = `thought: "${content}"`;
} else {
details = content;
if (entry.outcome) {
if (isSelf) {
let details = (entry.intent.selfDescription || entry.intent.description || entry.intent.originalText).trim();
if (details.length > 0) {
details = details.charAt(0).toUpperCase() + details.slice(1);
}
if (entry.intent.type === "action" && entry.outcome) {
details += ` (Outcome: ${entry.outcome.isValid ? "Succeeded" : `Failed - ${entry.outcome.reason}`})`;
}
return details;
}
return `${actorAlias} ${details}`;
const actorAlias = resolveAlias(viewer, entry.intent.actorId);
const subjectStr = actorAlias.charAt(0).toUpperCase() + actorAlias.slice(1);
let details = (entry.intent.description || entry.intent.originalText).trim();
if (entry.intent.type === "action" && entry.outcome) {
details += ` (Outcome: ${entry.outcome.isValid ? "Succeeded" : `Failed - ${entry.outcome.reason}`})`;
}
return `${subjectStr} ${details}`;
}
export class BufferRepository {

View File

@@ -32,14 +32,15 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
intent: {
type: "dialogue",
originalText: '"Hello there," Bob said to Charlie.',
description: "Bob greets Charlie",
description: "says, 'Hello there' to the bartender",
selfDescription: "You say, 'Hello there' to the bartender.",
actorId: "bob",
targetIds: ["charlie"],
},
};
const result = serializeSubjectiveBufferEntry(entry, viewer);
expect(result).toBe('the hooded figure spoke to the bartender: "Bob greets Charlie"');
expect(result).toBe("The hooded figure says, 'Hello there' to the bartender");
});
test("serializes action intent with outcome details", () => {
@@ -54,7 +55,8 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
intent: {
type: "action",
originalText: "Bob tried to break the latch.",
description: "Bob attempts to break the lock latch",
description: "attempts to break the lock latch",
selfDescription: "You attempt to break the lock latch.",
actorId: "bob",
targetIds: [],
},
@@ -65,7 +67,7 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
};
const result = serializeSubjectiveBufferEntry(entry, viewer);
expect(result).toBe('the hooded figure Bob attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)');
expect(result).toBe('The hooded figure attempts to break the lock latch (Outcome: Failed - The lock is made of reinforced steel.)');
});
test("serializes self-reference and unfamiliar actors", () => {
@@ -80,13 +82,14 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
type: "action",
originalText: "I opened the window.",
description: "open the window",
selfDescription: "You open the window.",
actorId: "alice",
targetIds: [],
},
};
const resultSelf = serializeSubjectiveBufferEntry(entrySelf, viewer);
expect(resultSelf).toBe("you open the window");
expect(resultSelf).toBe("You open the window.");
const entryUnfamiliar: BufferEntry = {
id: "entry-unfamiliar",
@@ -96,14 +99,15 @@ describe("Subjective Buffer Entry Serializer Tests (Tier 1)", () => {
intent: {
type: "action",
originalText: "Someone knocked.",
description: "knock on the door",
description: "knocks on the door",
selfDescription: "You knock on the door.",
actorId: "stranger-1",
targetIds: [],
},
};
const resultUnfamiliar = serializeSubjectiveBufferEntry(entryUnfamiliar, viewer);
expect(resultUnfamiliar).toBe("an unfamiliar figure knock on the door");
expect(resultUnfamiliar).toBe("An unfamiliar figure knocks on the door");
});
});
@@ -123,6 +127,7 @@ describe("BufferRepository Persistence Tests (Tier 1)", () => {
type: "action",
originalText: "Alice picked up a stick.",
description: "Alice gathers a stick",
selfDescription: "You gather a stick.",
actorId: "alice",
targetIds: [],
};

View File

@@ -57,6 +57,7 @@ describe("Scenario Validation & Schema Tests (Tier 1)", () => {
type: "action",
originalText: "I entered the foyer.",
description: "entered the house",
selfDescription: "You entered the house.",
actorId: "investigator",
targetIds: [],
},