@@ -21,9 +21,23 @@ import type { ObservationNote } from './MemoryObserver.js';
2121// Types
2222// ---------------------------------------------------------------------------
2323
24+ /**
25+ * Result of a reflection cycle.
26+ *
27+ * Contains the consolidated long-term traces (typed as episodic, semantic,
28+ * procedural, prospective, or relational), any superseded trace IDs, the
29+ * consumed note IDs, and the compression ratio achieved.
30+ */
2431export interface MemoryReflectionResult {
2532 /** New long-term memory traces to store. */
26- traces : Omit < MemoryTrace , 'id' | 'encodingStrength' | 'stability' | 'retrievalCount' | 'lastAccessedAt' | 'accessCount' | 'reinforcementInterval' | 'createdAt' | 'updatedAt' > [ ] ;
33+ traces : ( Omit < MemoryTrace , 'id' | 'encodingStrength' | 'stability' | 'retrievalCount' | 'lastAccessedAt' | 'accessCount' | 'reinforcementInterval' | 'createdAt' | 'updatedAt' > & {
34+ /**
35+ * Reflector's chain-of-thought reasoning for why this trace matters.
36+ * Available for devtools/debugging; stripped before the trace is
37+ * passed to `CognitiveMemoryManager.encode()` for storage.
38+ */
39+ reasoning ?: string ;
40+ } ) [ ] ;
2741 /** IDs of existing traces that should be superseded. */
2842 supersededTraceIds : string [ ] ;
2943 /** IDs of observation notes that were consumed. */
@@ -36,34 +50,81 @@ export interface MemoryReflectionResult {
3650// Personality-aware system prompt
3751// ---------------------------------------------------------------------------
3852
53+ /**
54+ * Build a personality-biased system prompt for the memory reflector.
55+ *
56+ * Personality influences (grounded in HEXACO model of personality):
57+ * - High honesty → prefer newer info over old on contradiction (source monitoring)
58+ * - High agreeableness → keep both versions on contradiction (cognitive flexibility)
59+ * - High conscientiousness → structured, categorized output (organizational encoding)
60+ * - High openness → rich, associative output (spreading activation style)
61+ * - High emotionality → heightened sensitivity to relational/emotional signals
62+ * - High extraversion → captures social dynamics and group interactions
63+ *
64+ * The chain-of-thought `<thinking>` block asks the LLM to reason about
65+ * each memory type before extraction, improving classification accuracy
66+ * and ensuring relational signals are not overlooked.
67+ *
68+ * @param traits - HEXACO personality traits of the agent
69+ * @returns System prompt string for the LLM reflector call
70+ */
3971function buildReflectorSystemPrompt ( traits : HexacoTraits ) : string {
4072 const clamp = ( v : number | undefined ) : number => v == null ? 0.5 : Math . max ( 0 , Math . min ( 1 , v ) ) ;
4173
74+ // Conflict resolution strategy — mirrors source monitoring in cognitive psychology.
75+ // High honesty agents trust newer information; high agreeableness agents preserve both.
4276 const conflictStrategy = clamp ( traits . honesty ) > 0.6
4377 ? 'When you detect a contradiction with existing knowledge, prefer the newer information and flag the old memory for supersession.'
4478 : clamp ( traits . agreeableness ) > 0.6
4579 ? 'When you detect a contradiction, keep both versions and note the discrepancy.'
4680 : 'When you detect a contradiction, keep the version with higher confidence.' ;
4781
82+ // Memory organization style — mirrors encoding specificity principle.
4883 const memoryStyle = clamp ( traits . conscientiousness ) > 0.6
4984 ? 'Produce structured, well-organized memory traces with clear categories.'
5085 : clamp ( traits . openness ) > 0.6
5186 ? 'Produce rich, associative memory traces that capture connections and context.'
5287 : 'Produce concise, factual memory traces focused on key information.' ;
5388
89+ // Relational sensitivity — emotionality and agreeableness heighten
90+ // detection of trust signals, boundary events, and social cues.
91+ const relationalEmphases : string [ ] = [ ] ;
92+ if ( clamp ( traits . emotionality ) > 0.6 ) {
93+ relationalEmphases . push ( 'Pay special attention to emotional subtleties, vulnerability signals, and shifts in emotional tone — these are important relational memories.' ) ;
94+ }
95+ if ( clamp ( traits . agreeableness ) > 0.6 ) {
96+ relationalEmphases . push ( 'Notice rapport cues, harmony signals, and moments of mutual understanding.' ) ;
97+ }
98+ if ( clamp ( traits . extraversion ) > 0.6 ) {
99+ relationalEmphases . push ( 'Capture social dynamics, group interactions, and interpersonal energy shifts.' ) ;
100+ }
101+ const relationalBlock = relationalEmphases . length > 0
102+ ? `\n\nRelational sensitivity:\n${ relationalEmphases . map ( ( e ) => `- ${ e } ` ) . join ( '\n' ) } `
103+ : '' ;
104+
54105 return `You are a memory reflector. Your job is to consolidate observation notes into long-term memory traces.
55106
107+ Before producing traces, reason step by step inside <thinking> tags:
108+ 1. What new FACTS did the user reveal? (semantic)
109+ 2. What EVENTS happened worth remembering? (episodic)
110+ 3. What PATTERNS or PREFERENCES emerged? (procedural)
111+ 4. What FUTURE INTENTIONS were expressed? (prospective)
112+ 5. What RELATIONSHIP SIGNALS appeared — vulnerability, trust, conflict, warmth? (relational)
113+ 6. Do any of these CONTRADICT existing memories? If so, which is more reliable?
114+ 7. What can be MERGED from multiple notes into a single trace?
115+
56116Rules:
571171. Merge redundant or overlapping observations into single traces
58- 2. Assign each trace a type: "episodic" (events), "semantic" (facts/knowledge), "procedural" (how-to), or "prospective" (future goals/ intentions)
118+ 2. Assign each trace a type: "episodic" (events/experiences ), "semantic" (facts/knowledge), "procedural" (how-to/patterns ), "prospective" (future intentions/reminders), or "relational" (trust signals, boundary events, emotional bonds, relationship shifts )
591193. Assign a scope: "user" (about the user), "thread" (conversation-specific), "persona" (about the agent), or "organization" (shared)
601204. ${ conflictStrategy }
611215. ${ memoryStyle }
62- 6. Target 5-40x compression: many notes → few high-quality traces
122+ 6. Target 5-40x compression: many notes → few high-quality traces${ relationalBlock }
63123
64- For each trace , output a JSON object on its own line:
124+ After your <thinking> block , output JSON objects, one per line:
65125{
66- "type": "episodic|semantic|procedural|prospective",
126+ "reasoning": "brief explanation of why this trace matters",
127+ "type": "episodic|semantic|procedural|prospective|relational",
67128 "scope": "user|thread|persona|organization",
68129 "scopeId": "relevant_id",
69130 "content": "consolidated memory content",
@@ -75,7 +136,7 @@ For each trace, output a JSON object on its own line:
75136 "consumedNotes": ["note_id1", "note_id2"]
76137}
77138
78- Output ONLY valid JSON objects, one per line.` ;
139+ Output your <thinking> block first, then ONLY valid JSON objects, one per line.` ;
79140}
80141
81142// ---------------------------------------------------------------------------
@@ -172,23 +233,44 @@ export class MemoryReflector {
172233
173234 // --- Internal ---
174235
236+ /**
237+ * Parse the LLM's reflection response into structured trace data.
238+ *
239+ * Handles:
240+ * - Stripping `<thinking>...</thinking>` blocks (chain-of-thought reasoning)
241+ * - Parsing one JSON object per line
242+ * - Validating and normalizing type/scope enums
243+ * - Preserving the optional `reasoning` field for devtools
244+ * - Collecting superseded and consumed note IDs
245+ *
246+ * @param llmResponse - Raw LLM output containing optional thinking block + JSON lines
247+ * @returns Parsed reflection result with typed traces
248+ */
175249 private parseReflection ( llmResponse : string ) : MemoryReflectionResult {
176250 const traces : MemoryReflectionResult [ 'traces' ] = [ ] ;
177251 const supersededTraceIds : string [ ] = [ ] ;
178252 const consumedNoteIds : string [ ] = [ ] ;
179253
180- const lines = llmResponse . split ( '\n' ) . filter ( ( l ) => l . trim ( ) ) ;
254+ // Strip <thinking>...</thinking> blocks before parsing JSON lines.
255+ // The thinking block contains the reflector's chain-of-thought reasoning
256+ // which is useful for debugging but not part of the trace data.
257+ const cleaned = llmResponse . replace ( / < t h i n k i n g > [ \s \S ] * ?< \/ t h i n k i n g > / gi, '' ) ;
258+ const lines = cleaned . split ( '\n' ) . filter ( ( l ) => l . trim ( ) ) ;
181259 const now = Date . now ( ) ;
182260
183261 for ( const line of lines ) {
184262 try {
185263 const parsed = JSON . parse ( line . trim ( ) ) ;
186264 if ( ! parsed . content ) continue ;
187265
266+ // Validate memory type — defaults to 'semantic' for unrecognized types.
267+ // All 5 Tulving-extended types are accepted: episodic, semantic,
268+ // procedural, prospective, and relational.
188269 const type = ( [ 'episodic' , 'semantic' , 'procedural' , 'prospective' , 'relational' ] . includes ( parsed . type )
189270 ? parsed . type
190271 : 'semantic' ) as MemoryType ;
191272
273+ // Validate memory scope — defaults to 'user' for unrecognized scopes.
192274 const scope = ( [ 'user' , 'thread' , 'persona' , 'organization' ] . includes ( parsed . scope )
193275 ? parsed . scope
194276 : 'user' ) as MemoryScope ;
@@ -215,6 +297,9 @@ export class MemoryReflector {
215297 } ,
216298 associatedTraceIds : [ ] ,
217299 isActive : true ,
300+ // Preserve reasoning for devtools — CognitiveMemoryManager strips
301+ // this before passing to encode() since it's not part of MemoryTrace.
302+ reasoning : typeof parsed . reasoning === 'string' ? parsed . reasoning : undefined ,
218303 } ) ;
219304
220305 if ( Array . isArray ( parsed . supersedes ) ) {
@@ -224,11 +309,13 @@ export class MemoryReflector {
224309 consumedNoteIds . push ( ...parsed . consumedNotes ) ;
225310 }
226311 } catch {
227- // Skip malformed lines
312+ // Skip malformed lines — common when LLM outputs markdown fences or commentary
228313 }
229314 }
230315
231- // If no specific notes were claimed, consider all pending consumed
316+ // If no specific notes were claimed, consider all pending consumed.
317+ // This handles the case where the LLM omits consumedNotes fields
318+ // but still produces valid traces from the input notes.
232319 if ( consumedNoteIds . length === 0 && traces . length > 0 ) {
233320 consumedNoteIds . push ( ...this . pendingNotes . map ( ( n ) => n . id ) ) ;
234321 }
0 commit comments