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