feat(intent): Add additional "thought" intent type

This commit is contained in:
2026-07-19 08:09:06 +05:30
parent 1ed1edf4cf
commit 0512be647c
12 changed files with 66 additions and 28 deletions

View File

@@ -23,6 +23,7 @@ function IntentTag({
}) {
const labels: Record<string, string> = {
monologue: "thought",
thought: "thought",
dialogue: "dialogue",
action: "action",
};

View File

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

View File

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

View File

@@ -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 ===");

View File

@@ -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<ProcessResult> {
// 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.",

View File

@@ -26,10 +26,11 @@ export class TimeDeltaGenerator implements IDeltaGenerator<TimeDelta> {
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.",
};
}

View File

@@ -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<ValidationResult> {
// 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.",
};
}

View File

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

View File

@@ -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<typeof IntentTypeSchema>;
/**
@@ -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()),

View File

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

View File

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

View File

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