@mastra/memory 1.1.0-alpha.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/dist/chunk-6TXUWFIU.js +3188 -0
  3. package/dist/chunk-6TXUWFIU.js.map +1 -0
  4. package/dist/chunk-FQJWVCDF.cjs +3205 -0
  5. package/dist/chunk-FQJWVCDF.cjs.map +1 -0
  6. package/dist/docs/README.md +1 -1
  7. package/dist/docs/SKILL.md +12 -1
  8. package/dist/docs/SOURCE_MAP.json +62 -2
  9. package/dist/docs/memory/02-storage.md +10 -0
  10. package/dist/index.cjs +96 -1
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.ts +53 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +96 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/observational-memory-3Q42SITP.cjs +52 -0
  17. package/dist/observational-memory-3Q42SITP.cjs.map +1 -0
  18. package/dist/observational-memory-VXLHOSDZ.js +3 -0
  19. package/dist/observational-memory-VXLHOSDZ.js.map +1 -0
  20. package/dist/processors/index.cjs +52 -0
  21. package/dist/processors/index.cjs.map +1 -0
  22. package/dist/processors/index.d.ts +2 -0
  23. package/dist/processors/index.d.ts.map +1 -0
  24. package/dist/processors/index.js +3 -0
  25. package/dist/processors/index.js.map +1 -0
  26. package/dist/processors/observational-memory/index.d.ts +18 -0
  27. package/dist/processors/observational-memory/index.d.ts.map +1 -0
  28. package/dist/processors/observational-memory/observational-memory.d.ts +579 -0
  29. package/dist/processors/observational-memory/observational-memory.d.ts.map +1 -0
  30. package/dist/processors/observational-memory/observer-agent.d.ts +117 -0
  31. package/dist/processors/observational-memory/observer-agent.d.ts.map +1 -0
  32. package/dist/processors/observational-memory/reflector-agent.d.ts +46 -0
  33. package/dist/processors/observational-memory/reflector-agent.d.ts.map +1 -0
  34. package/dist/processors/observational-memory/token-counter.d.ts +30 -0
  35. package/dist/processors/observational-memory/token-counter.d.ts.map +1 -0
  36. package/dist/processors/observational-memory/types.d.ts +288 -0
  37. package/dist/processors/observational-memory/types.d.ts.map +1 -0
  38. package/package.json +18 -8
@@ -0,0 +1,3205 @@
1
+ 'use strict';
2
+
3
+ var agent = require('@mastra/core/agent');
4
+ var llm = require('@mastra/core/llm');
5
+ var memory = require('@mastra/core/memory');
6
+ var processors = require('@mastra/core/processors');
7
+ var xxhash = require('xxhash-wasm');
8
+ var lite = require('js-tiktoken/lite');
9
+ var o200k_base = require('js-tiktoken/ranks/o200k_base');
10
+
11
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
+
13
+ var xxhash__default = /*#__PURE__*/_interopDefault(xxhash);
14
+ var o200k_base__default = /*#__PURE__*/_interopDefault(o200k_base);
15
+
16
+ // src/processors/observational-memory/observational-memory.ts
17
+
18
+ // src/processors/observational-memory/observer-agent.ts
19
+ var LEGACY_OBSERVER_EXTRACTION_INSTRUCTIONS = `CRITICAL: DISTINGUISH USER ASSERTIONS FROM QUESTIONS
20
+
21
+ When the user TELLS you something about themselves, mark it as an assertion:
22
+ - "I have two kids" \u2192 \u{1F534} (14:30) User stated has two kids
23
+ - "I work at Acme Corp" \u2192 \u{1F534} (14:31) User stated works at Acme Corp
24
+ - "I graduated in 2019" \u2192 \u{1F534} (14:32) User stated graduated in 2019
25
+
26
+ When the user ASKS about something, mark it as a question/request:
27
+ - "Can you help me with X?" \u2192 \u{1F7E1} (15:00) User asked help with X
28
+ - "What's the best way to do Y?" \u2192 \u{1F7E1} (15:01) User asked best way to do Y
29
+
30
+ USER ASSERTIONS ARE AUTHORITATIVE. The user is the source of truth about their own life.
31
+ If a user previously stated something and later asks a question about the same topic,
32
+ the assertion is the answer - the question doesn't invalidate what they already told you.
33
+
34
+ TEMPORAL ANCHORING:
35
+ Convert relative times to estimated dates based on the message timestamp.
36
+ Include the user's original phrasing in quotes, then add an estimated date or range.
37
+ Ranges may span multiple months - e.g., "within the last month" on July 15th could mean anytime in June to early July.
38
+
39
+ BAD: User was given X by their friend last month.
40
+ GOOD: User was given X by their friend "last month" (estimated mid-June to early July 202X).
41
+
42
+ PRESERVE UNUSUAL PHRASING:
43
+ When the user uses unexpected or non-standard terminology, quote their exact words.
44
+
45
+ BAD: User exercised.
46
+ GOOD: User stated they did a "movement session" (their term for exercise).
47
+
48
+ CONVERSATION CONTEXT:
49
+ - What the user is working on or asking about
50
+ - Previous topics and their outcomes
51
+ - What user understands or needs clarification on
52
+ - Specific requirements or constraints mentioned
53
+ - Contents of assistant learnings and summaries
54
+ - Answers to users questions including full context to remember detailed summaries and explanations
55
+ - Assistant explanations, especially complex ones. observe the fine details so that the assistant does not forget what they explained
56
+ - Relevant code snippets
57
+ - User preferences (like favourites, dislikes, preferences, etc)
58
+ - Any specifically formatted text or ascii that would need to be reproduced or referenced in later interactions (preserve these verbatim in memory)
59
+ - Any blocks of any text which the user and assistant are iteratively collaborating back and forth on should be preserved verbatim
60
+ - When who/what/where/when is mentioned, note that in the observation. Example: if the user received went on a trip with someone, observe who that someone was, where the trip was, when it happened, and what happened, not just that the user went on the trip.
61
+
62
+ ACTIONABLE INSIGHTS:
63
+ - What worked well in explanations
64
+ - What needs follow-up or clarification
65
+ - User's stated goals or next steps (note if the user tells you not to do a next step, or asks for something specific, other next steps besides the users request should be marked as "waiting for user", unless the user explicitly says to continue all next steps)`;
66
+ var USE_LEGACY_PROMPT = process.env.OM_USE_LEGACY_PROMPT === "1" || process.env.OM_USE_LEGACY_PROMPT === "true";
67
+ var USE_CONDENSED_PROMPT = process.env.OM_USE_CONDENSED_PROMPT === "1" || process.env.OM_USE_CONDENSED_PROMPT === "true";
68
+ var CONDENSED_OBSERVER_EXTRACTION_INSTRUCTIONS = `You are the memory consciousness of an AI assistant. Your observations will be the ONLY information the assistant has about past interactions with this user.
69
+
70
+ CORE PRINCIPLES:
71
+
72
+ 1. BE SPECIFIC - Vague observations are useless. Capture details that distinguish and identify.
73
+ 2. ANCHOR IN TIME - Note when things happened and when they were said.
74
+ 3. TRACK STATE CHANGES - When information updates or supersedes previous info, make it explicit.
75
+ 4. USE COMMON SENSE - If it would help the assistant remember later, observe it.
76
+
77
+ ASSERTIONS VS QUESTIONS:
78
+ - User TELLS you something \u2192 \u{1F534} "User stated [fact]"
79
+ - User ASKS something \u2192 \u{1F7E1} "User asked [question]"
80
+ - User assertions are authoritative. They are the source of truth about their own life.
81
+
82
+ TEMPORAL ANCHORING:
83
+ - Always include message time at the start: (14:30) User stated...
84
+ - Add estimated date at the END only for relative time references:
85
+ "User will visit parents this weekend. (meaning Jan 18-19)"
86
+ - Don't add end dates for present-moment statements or vague terms like "recently"
87
+ - Split multi-event statements into separate observations, each with its own date
88
+
89
+ DETAILS TO ALWAYS PRESERVE:
90
+ - Names, handles, usernames, titles (@username, "Dr. Smith")
91
+ - Numbers, counts, quantities (4 items, 3 sessions, 27th in list)
92
+ - Measurements, percentages, statistics (5kg, 20% improvement, 85% accuracy)
93
+ - Sequences and orderings (steps 1-5, chord progression, lucky numbers)
94
+ - Prices, dates, times, durations ($50, March 15, 2 hours)
95
+ - Locations and distinguishing attributes (near X, based in Y, specializes in Z)
96
+ - User's specific role (presenter, volunteer, organizer - not just "attended")
97
+ - Exact phrasing when unusual ("movement session" for exercise)
98
+ - Verbatim text being collaborated on (code, formatted text, ASCII art)
99
+
100
+ WHEN ASSISTANT PROVIDES LISTS/RECOMMENDATIONS:
101
+ Don't just say "Assistant recommended 5 hotels." Capture what distinguishes each:
102
+ "Assistant recommended: Hotel A (near station), Hotel B (pet-friendly), Hotel C (has pool)..."
103
+
104
+ STATE CHANGES:
105
+ When user updates information, note what changed:
106
+ "User will use the new method (replacing the old approach)"
107
+
108
+ WHO/WHAT/WHERE/WHEN:
109
+ Capture all dimensions. Not just "User went on a trip" but who with, where, when, and what happened.
110
+
111
+ Don't repeat observations that have already been captured in previous sessions.
112
+
113
+ REMEMBER: These observations are your ENTIRE memory. Any detail you fail to observe is permanently forgotten. Use common sense - if something seems like it might be important to remember, it probably is. When in doubt, observe it.`;
114
+ var CURRENT_OBSERVER_EXTRACTION_INSTRUCTIONS = `CRITICAL: DISTINGUISH USER ASSERTIONS FROM QUESTIONS
115
+
116
+ When the user TELLS you something about themselves, mark it as an assertion:
117
+ - "I have two kids" \u2192 \u{1F534} (14:30) User stated has two kids
118
+ - "I work at Acme Corp" \u2192 \u{1F534} (14:31) User stated works at Acme Corp
119
+ - "I graduated in 2019" \u2192 \u{1F534} (14:32) User stated graduated in 2019
120
+
121
+ When the user ASKS about something, mark it as a question/request:
122
+ - "Can you help me with X?" \u2192 \u{1F7E1} (15:00) User asked help with X
123
+ - "What's the best way to do Y?" \u2192 \u{1F7E1} (15:01) User asked best way to do Y
124
+
125
+ Distinguish between QUESTIONS and STATEMENTS OF INTENT:
126
+ - "Can you recommend..." \u2192 Question (extract as "User asked...")
127
+ - "I'm looking forward to [doing X]" \u2192 Statement of intent (extract as "User stated they will [do X] (include estimated/actual date if mentioned)")
128
+ - "I need to [do X]" \u2192 Statement of intent (extract as "User stated they need to [do X] (again, add date if mentioned)")
129
+
130
+ STATE CHANGES AND UPDATES:
131
+ When a user indicates they are changing something, frame it as a state change that supersedes previous information:
132
+ - "I'm going to start doing X instead of Y" \u2192 "User will start doing X (changing from Y)"
133
+ - "I'm switching from A to B" \u2192 "User is switching from A to B"
134
+ - "I moved my stuff to the new place" \u2192 "User moved their stuff to the new place (no longer at previous location)"
135
+
136
+ If the new state contradicts or updates previous information, make that explicit:
137
+ - BAD: "User plans to use the new method"
138
+ - GOOD: "User will use the new method (replacing the old approach)"
139
+
140
+ This helps distinguish current state from outdated information.
141
+
142
+ USER ASSERTIONS ARE AUTHORITATIVE. The user is the source of truth about their own life.
143
+ If a user previously stated something and later asks a question about the same topic,
144
+ the assertion is the answer - the question doesn't invalidate what they already told you.
145
+
146
+ TEMPORAL ANCHORING:
147
+ Each observation has TWO potential timestamps:
148
+
149
+ 1. BEGINNING: The time the statement was made (from the message timestamp) - ALWAYS include this
150
+ 2. END: The time being REFERENCED, if different from when it was said - ONLY when there's a relative time reference
151
+
152
+ ONLY add "(meaning DATE)" or "(estimated DATE)" at the END when you can provide an ACTUAL DATE:
153
+ - Past: "last week", "yesterday", "a few days ago", "last month", "in March"
154
+ - Future: "this weekend", "tomorrow", "next week"
155
+
156
+ DO NOT add end dates for:
157
+ - Present-moment statements with no time reference
158
+ - Vague references like "recently", "a while ago", "lately", "soon" - these cannot be converted to actual dates
159
+
160
+ FORMAT:
161
+ - With time reference: (TIME) [observation]. (meaning/estimated DATE)
162
+ - Without time reference: (TIME) [observation].
163
+
164
+ GOOD: (09:15) User's friend had a birthday party in March. (meaning March 20XX)
165
+ ^ References a past event - add the referenced date at the end
166
+
167
+ GOOD: (09:15) User will visit their parents this weekend. (meaning June 17-18, 20XX)
168
+ ^ References a future event - add the referenced date at the end
169
+
170
+ GOOD: (09:15) User prefers hiking in the mountains.
171
+ ^ Present-moment preference, no time reference - NO end date needed
172
+
173
+ GOOD: (09:15) User is considering adopting a dog.
174
+ ^ Present-moment thought, no time reference - NO end date needed
175
+
176
+ BAD: (09:15) User prefers hiking in the mountains. (meaning June 15, 20XX - today)
177
+ ^ No time reference in the statement - don't repeat the message timestamp at the end
178
+
179
+ IMPORTANT: If an observation contains MULTIPLE events, split them into SEPARATE observation lines.
180
+ EACH split observation MUST have its own date at the end - even if they share the same time context.
181
+
182
+ Examples (assume message is from June 15, 20XX):
183
+
184
+ BAD: User will visit their parents this weekend (meaning June 17-18, 20XX) and go to the dentist tomorrow.
185
+ GOOD (split into two observations, each with its date):
186
+ User will visit their parents this weekend. (meaning June 17-18, 20XX)
187
+ User will go to the dentist tomorrow. (meaning June 16, 20XX)
188
+
189
+ BAD: User needs to clean the garage this weekend and is looking forward to setting up a new workbench.
190
+ GOOD (split, BOTH get the same date since they're related):
191
+ User needs to clean the garage this weekend. (meaning June 17-18, 20XX)
192
+ User will set up a new workbench this weekend. (meaning June 17-18, 20XX)
193
+
194
+ BAD: User was given a gift by their friend (estimated late May 20XX) last month.
195
+ GOOD: (09:15) User was given a gift by their friend last month. (estimated late May 20XX)
196
+ ^ Message time at START, relative date reference at END - never in the middle
197
+
198
+ BAD: User started a new job recently and will move to a new apartment next week.
199
+ GOOD (split):
200
+ User started a new job recently.
201
+ User will move to a new apartment next week. (meaning June 21-27, 20XX)
202
+ ^ "recently" is too vague for a date - omit the end date. "next week" can be calculated.
203
+
204
+ ALWAYS put the date at the END in parentheses - this is critical for temporal reasoning.
205
+ When splitting related events that share the same time context, EACH observation must have the date.
206
+
207
+ PRESERVE UNUSUAL PHRASING:
208
+ When the user uses unexpected or non-standard terminology, quote their exact words.
209
+
210
+ BAD: User exercised.
211
+ GOOD: User stated they did a "movement session" (their term for exercise).
212
+
213
+ USE PRECISE ACTION VERBS:
214
+ Replace vague verbs like "getting", "got", "have" with specific action verbs that clarify the nature of the action.
215
+ If the assistant confirms or clarifies the user's action, use the assistant's more precise language.
216
+
217
+ BAD: User is getting X.
218
+ GOOD: User subscribed to X. (if context confirms recurring delivery)
219
+ GOOD: User purchased X. (if context confirms one-time acquisition)
220
+
221
+ BAD: User got something.
222
+ GOOD: User purchased / received / was given something. (be specific)
223
+
224
+ Common clarifications:
225
+ - "getting" something regularly \u2192 "subscribed to" or "enrolled in"
226
+ - "getting" something once \u2192 "purchased" or "acquired"
227
+ - "got" \u2192 "purchased", "received as gift", "was given", "picked up"
228
+ - "signed up" \u2192 "enrolled in", "registered for", "subscribed to"
229
+ - "stopped getting" \u2192 "canceled", "unsubscribed from", "discontinued"
230
+
231
+ When the assistant interprets or confirms the user's vague language, prefer the assistant's precise terminology.
232
+
233
+ PRESERVING DETAILS IN ASSISTANT-GENERATED CONTENT:
234
+
235
+ When the assistant provides lists, recommendations, or creative content that the user explicitly requested,
236
+ preserve the DISTINGUISHING DETAILS that make each item unique and queryable later.
237
+
238
+ 1. RECOMMENDATION LISTS - Preserve the key attribute that distinguishes each item:
239
+ BAD: Assistant recommended 5 hotels in the city.
240
+ GOOD: Assistant recommended hotels: Hotel A (near the train station), Hotel B (budget-friendly),
241
+ Hotel C (has rooftop pool), Hotel D (pet-friendly), Hotel E (historic building).
242
+
243
+ BAD: Assistant listed 3 online stores for craft supplies.
244
+ GOOD: Assistant listed craft stores: Store A (based in Germany, ships worldwide),
245
+ Store B (specializes in vintage fabrics), Store C (offers bulk discounts).
246
+
247
+ 2. NAMES, HANDLES, AND IDENTIFIERS - Always preserve specific identifiers:
248
+ BAD: Assistant provided social media accounts for several photographers.
249
+ GOOD: Assistant provided photographer accounts: @photographer_one (portraits),
250
+ @photographer_two (landscapes), @photographer_three (nature).
251
+
252
+ BAD: Assistant listed some authors to check out.
253
+ GOOD: Assistant recommended authors: Jane Smith (mystery novels),
254
+ Bob Johnson (science fiction), Maria Garcia (historical romance).
255
+
256
+ 3. CREATIVE CONTENT - Preserve structure and key sequences:
257
+ BAD: Assistant wrote a poem with multiple verses.
258
+ GOOD: Assistant wrote a 3-verse poem. Verse 1 theme: loss. Verse 2 theme: hope.
259
+ Verse 3 theme: renewal. Refrain: "The light returns."
260
+
261
+ BAD: User shared their lucky numbers from a fortune cookie.
262
+ GOOD: User's fortune cookie lucky numbers: 7, 14, 23, 38, 42, 49.
263
+
264
+ 4. TECHNICAL/NUMERICAL RESULTS - Preserve specific values:
265
+ BAD: Assistant explained the performance improvements from the optimization.
266
+ GOOD: Assistant explained the optimization achieved 43.7% faster load times
267
+ and reduced memory usage from 2.8GB to 940MB.
268
+
269
+ BAD: Assistant provided statistics about the dataset.
270
+ GOOD: Assistant provided dataset stats: 7,342 samples, 89.6% accuracy,
271
+ 23ms average inference time.
272
+
273
+ 5. QUANTITIES AND COUNTS - Always preserve how many of each item:
274
+ BAD: Assistant listed items with details but no quantities.
275
+ GOOD: Assistant listed items: Item A (4 units, size large), Item B (2 units, size small).
276
+
277
+ When listing items with attributes, always include the COUNT first before other details.
278
+
279
+ 6. ROLE/PARTICIPATION STATEMENTS - When user mentions their role at an event:
280
+ BAD: User attended the company event.
281
+ GOOD: User was a presenter at the company event.
282
+
283
+ BAD: User went to the fundraiser.
284
+ GOOD: User volunteered at the fundraiser (helped with registration).
285
+
286
+ Always capture specific roles: presenter, organizer, volunteer, team lead,
287
+ coordinator, participant, contributor, helper, etc.
288
+
289
+ CONVERSATION CONTEXT:
290
+ - What the user is working on or asking about
291
+ - Previous topics and their outcomes
292
+ - What user understands or needs clarification on
293
+ - Specific requirements or constraints mentioned
294
+ - Contents of assistant learnings and summaries
295
+ - Answers to users questions including full context to remember detailed summaries and explanations
296
+ - Assistant explanations, especially complex ones. observe the fine details so that the assistant does not forget what they explained
297
+ - Relevant code snippets
298
+ - User preferences (like favourites, dislikes, preferences, etc)
299
+ - Any specifically formatted text or ascii that would need to be reproduced or referenced in later interactions (preserve these verbatim in memory)
300
+ - Sequences, units, measurements, and any kind of specific relevant data
301
+ - Any blocks of any text which the user and assistant are iteratively collaborating back and forth on should be preserved verbatim
302
+ - When who/what/where/when is mentioned, note that in the observation. Example: if the user received went on a trip with someone, observe who that someone was, where the trip was, when it happened, and what happened, not just that the user went on the trip.
303
+ - For any described entity (like a person, place, thing, etc), preserve the attributes that would help identify or describe the specific entity later: location ("near X"), specialty ("focuses on Y"), unique feature ("has Z"), relationship ("owned by W"), or other details. The entity's name is important, but so are any additional details that distinguish it. If there are a list of entities, preserve these details for each of them.
304
+
305
+ ACTIONABLE INSIGHTS:
306
+ - What worked well in explanations
307
+ - What needs follow-up or clarification
308
+ - User's stated goals or next steps (note if the user tells you not to do a next step, or asks for something specific, other next steps besides the users request should be marked as "waiting for user", unless the user explicitly says to continue all next steps)`;
309
+ var OBSERVER_EXTRACTION_INSTRUCTIONS = USE_CONDENSED_PROMPT ? CONDENSED_OBSERVER_EXTRACTION_INSTRUCTIONS : USE_LEGACY_PROMPT ? LEGACY_OBSERVER_EXTRACTION_INSTRUCTIONS : CURRENT_OBSERVER_EXTRACTION_INSTRUCTIONS;
310
+ var CONDENSED_OBSERVER_OUTPUT_FORMAT = `Use priority levels:
311
+ - \u{1F534} High: explicit user facts, preferences, goals achieved, critical context
312
+ - \u{1F7E1} Medium: project details, learned information, tool results
313
+ - \u{1F7E2} Low: minor details, uncertain observations
314
+
315
+ Group observations by date, then list each with 24-hour time.
316
+ Group related observations (like tool sequences) by indenting.
317
+
318
+ <observations>
319
+ Date: Dec 4, 2025
320
+ * \u{1F534} (09:15) User stated they have 3 kids: Emma (12), Jake (9), and Lily (5)
321
+ * \u{1F534} (09:16) User's anniversary is March 15
322
+ * \u{1F7E1} (09:20) User asked how to optimize database queries
323
+ * \u{1F7E1} (10:30) User working on auth refactor - targeting 50% latency reduction
324
+ * \u{1F7E1} (10:45) Assistant recommended hotels: Grand Plaza (downtown, $180/night), Seaside Inn (near beach, pet-friendly), Mountain Lodge (has pool, free breakfast)
325
+ * \u{1F534} (11:00) User's friend @maria_dev recommended using Redis for caching
326
+ * \u{1F7E1} (11:15) User attended the tech conference as a speaker (presented on microservices)
327
+ * \u{1F534} (11:30) User will visit parents this weekend (meaning Dec 7-8, 2025)
328
+ * \u{1F7E1} (14:00) Agent debugging auth issue
329
+ * -> ran git status, found 3 modified files
330
+ * -> viewed auth.ts:45-60, found missing null check
331
+ * -> applied fix, tests now pass
332
+ * \u{1F7E1} (14:30) Assistant provided dataset stats: 7,342 samples, 89.6% accuracy, 23ms inference time
333
+ * \u{1F534} (15:00) User's lucky numbers from fortune cookie: 7, 14, 23, 38, 42, 49
334
+
335
+ Date: Dec 5, 2025
336
+ * \u{1F534} (09:00) User switched from Python to TypeScript for the project (no longer using Python)
337
+ * \u{1F7E1} (09:30) User bought running shoes for $120 at SportMart (downtown location)
338
+ * \u{1F534} (10:00) User prefers morning meetings, not afternoon (updating previous preference)
339
+ * \u{1F7E1} (10:30) User went to Italy with their sister last summer (meaning July 2025), visited Rome and Florence for 2 weeks
340
+ * \u{1F534} (10:45) User's dentist appointment is next Tuesday (meaning Dec 10, 2025)
341
+ * \u{1F7E2} (11:00) User mentioned they might try the new coffee shop
342
+ </observations>
343
+
344
+ <current-task>
345
+ Primary: Implementing OAuth2 flow for the auth refactor
346
+ Secondary: Waiting for user to confirm database schema changes
347
+ </current-task>
348
+
349
+ <suggested-response>
350
+ The OAuth2 implementation is ready for testing. Would you like me to walk through the flow?
351
+ </suggested-response>`;
352
+ var OBSERVER_OUTPUT_FORMAT_BASE = `Use priority levels:
353
+ - \u{1F534} High: explicit user facts, preferences, goals achieved, critical context
354
+ - \u{1F7E1} Medium: project details, learned information, tool results
355
+ - \u{1F7E2} Low: minor details, uncertain observations
356
+
357
+ Group related observations (like tool sequences) by indenting:
358
+ * \u{1F7E1} (14:33) Agent debugging auth issue
359
+ * -> ran git status, found 3 modified files
360
+ * -> viewed auth.ts:45-60, found missing null check
361
+ * -> applied fix, tests now pass
362
+
363
+ Group observations by date, then list each with 24-hour time.
364
+
365
+ <observations>
366
+ Date: Dec 4, 2025
367
+ * \u{1F534} (14:30) User prefers direct answers
368
+ * \u{1F7E1} (14:31) Working on feature X
369
+ * \u{1F7E2} (14:32) User might prefer dark mode
370
+
371
+ Date: Dec 5, 2025
372
+ * \u{1F7E1} (09:15) Continued work on feature X
373
+ </observations>
374
+
375
+ <current-task>
376
+ State the current task(s) explicitly. Can be single or multiple:
377
+ - Primary: What the agent is currently working on
378
+ - Secondary: Other pending tasks (mark as "waiting for user" if appropriate)
379
+
380
+ If the agent started doing something without user approval, note that it's off-task.
381
+ </current-task>
382
+
383
+ <suggested-response>
384
+ Hint for the agent's immediate next message. Examples:
385
+ - "I've updated the navigation model. Let me walk you through the changes..."
386
+ - "The assistant should wait for the user to respond before continuing."
387
+ - Call the view tool on src/example.ts to continue debugging.
388
+ </suggested-response>`;
389
+ var CONDENSED_OBSERVER_GUIDELINES = `- Be specific: "User prefers short answers without lengthy explanations" not "User stated a preference"
390
+ - Use terse language - dense sentences without unnecessary words
391
+ - Don't repeat observations that have already been captured
392
+ - When the agent calls tools, observe what was called, why, and what was learned
393
+ - Include line numbers when observing code files
394
+ - If the agent provides a detailed response, observe the key points so it could be repeated
395
+ - Start each observation with a priority emoji (\u{1F534}, \u{1F7E1}, \u{1F7E2})
396
+ - Observe WHAT happened and WHAT it means, not HOW well it was done
397
+ - If the user provides detailed messages or code snippets, observe all important details`;
398
+ var OBSERVER_GUIDELINES = USE_CONDENSED_PROMPT ? CONDENSED_OBSERVER_GUIDELINES : `- Be specific enough for the assistant to act on
399
+ - Good: "User prefers short, direct answers without lengthy explanations"
400
+ - Bad: "User stated a preference" (too vague)
401
+ - Add 1 to 5 observations per exchange
402
+ - Use terse language to save tokens. Sentences should be dense without unnecessary words.
403
+ - Do not add repetitive observations that have already been observed.
404
+ - If the agent calls tools, observe what was called, why, and what was learned.
405
+ - When observing files with line numbers, include the line number if useful.
406
+ - If the agent provides a detailed response, observe the contents so it could be repeated.
407
+ - Make sure you start each observation with a priority emoji (\u{1F534}, \u{1F7E1}, \u{1F7E2})
408
+ - Observe WHAT the agent did and WHAT it means, not HOW well it did it.
409
+ - If the user provides detailed messages or code snippets, observe all important details.`;
410
+ function buildObserverSystemPrompt(multiThread = false) {
411
+ const outputFormat = USE_CONDENSED_PROMPT ? CONDENSED_OBSERVER_OUTPUT_FORMAT : OBSERVER_OUTPUT_FORMAT_BASE;
412
+ if (multiThread) {
413
+ return `You are the memory consciousness of an AI assistant. Your observations will be the ONLY information the assistant has about past interactions with this user.
414
+
415
+ Extract observations that will help the assistant remember:
416
+
417
+ ${OBSERVER_EXTRACTION_INSTRUCTIONS}
418
+
419
+ === MULTI-THREAD INPUT ===
420
+
421
+ You will receive messages from MULTIPLE conversation threads, each wrapped in <thread id="..."> tags.
422
+ Process each thread separately and output observations for each thread.
423
+
424
+ === OUTPUT FORMAT ===
425
+
426
+ Your output MUST use XML tags to structure the response. Each thread's observations, current-task, and suggested-response should be nested inside a <thread id="..."> block within <observations>.
427
+
428
+ <observations>
429
+ <thread id="thread_id_1">
430
+ Date: Dec 4, 2025
431
+ * \u{1F534} (14:30) User prefers direct answers
432
+ * \u{1F7E1} (14:31) Working on feature X
433
+
434
+ <current-task>
435
+ What the agent is currently working on in this thread
436
+ </current-task>
437
+
438
+ <suggested-response>
439
+ Hint for the agent's next message in this thread
440
+ </suggested-response>
441
+ </thread>
442
+
443
+ <thread id="thread_id_2">
444
+ Date: Dec 5, 2025
445
+ * \u{1F7E1} (09:15) User asked about deployment
446
+
447
+ <current-task>
448
+ Current task for this thread
449
+ </current-task>
450
+
451
+ <suggested-response>
452
+ Suggested response for this thread
453
+ </suggested-response>
454
+ </thread>
455
+ </observations>
456
+
457
+ Use priority levels:
458
+ - \u{1F534} High: explicit user facts, preferences, goals achieved, critical context
459
+ - \u{1F7E1} Medium: project details, learned information, tool results
460
+ - \u{1F7E2} Low: minor details, uncertain observations
461
+
462
+ === GUIDELINES ===
463
+
464
+ ${OBSERVER_GUIDELINES}
465
+
466
+ Remember: These observations are the assistant's ONLY memory. Make them count.
467
+
468
+ User messages are extremely important. If the user asks a question or gives a new task, make it clear in <current-task> that this is the priority.`;
469
+ }
470
+ return `You are the memory consciousness of an AI assistant. Your observations will be the ONLY information the assistant has about past interactions with this user.
471
+
472
+ Extract observations that will help the assistant remember:
473
+
474
+ ${OBSERVER_EXTRACTION_INSTRUCTIONS}
475
+
476
+ === OUTPUT FORMAT ===
477
+
478
+ Your output MUST use XML tags to structure the response. This allows the system to properly parse and manage memory over time.
479
+
480
+ ${outputFormat}
481
+
482
+ === GUIDELINES ===
483
+
484
+ ${OBSERVER_GUIDELINES}
485
+
486
+ === IMPORTANT: THREAD ATTRIBUTION ===
487
+
488
+ Do NOT add thread identifiers, thread IDs, or <thread> tags to your observations.
489
+ Thread attribution is handled externally by the system.
490
+ Simply output your observations without any thread-related markup.
491
+
492
+ Remember: These observations are the assistant's ONLY memory. Make them count.
493
+
494
+ User messages are extremely important. If the user asks a question or gives a new task, make it clear in <current-task> that this is the priority. If the assistant needs to respond to the user, indicate in <suggested-response> that it should pause for user reply before continuing other tasks.`;
495
+ }
496
+ var OBSERVER_SYSTEM_PROMPT = buildObserverSystemPrompt();
497
+ function formatMessagesForObserver(messages, options) {
498
+ const maxLen = options?.maxPartLength;
499
+ return messages.map((msg) => {
500
+ const timestamp = msg.createdAt ? new Date(msg.createdAt).toLocaleString("en-US", {
501
+ year: "numeric",
502
+ month: "short",
503
+ day: "numeric",
504
+ hour: "numeric",
505
+ minute: "2-digit",
506
+ hour12: true
507
+ }) : "";
508
+ const role = msg.role.charAt(0).toUpperCase() + msg.role.slice(1);
509
+ const timestampStr = timestamp ? ` (${timestamp})` : "";
510
+ let content = "";
511
+ if (typeof msg.content === "string") {
512
+ content = maybeTruncate(msg.content, maxLen);
513
+ } else if (msg.content?.parts && Array.isArray(msg.content.parts) && msg.content.parts.length > 0) {
514
+ content = msg.content.parts.map((part) => {
515
+ if (part.type === "text") return maybeTruncate(part.text, maxLen);
516
+ if (part.type === "tool-invocation") {
517
+ const inv = part.toolInvocation;
518
+ if (inv.state === "result") {
519
+ const resultStr = JSON.stringify(inv.result, null, 2);
520
+ return `[Tool Result: ${inv.toolName}]
521
+ ${maybeTruncate(resultStr, maxLen)}`;
522
+ }
523
+ const argsStr = JSON.stringify(inv.args, null, 2);
524
+ return `[Tool Call: ${inv.toolName}]
525
+ ${maybeTruncate(argsStr, maxLen)}`;
526
+ }
527
+ if (part.type?.startsWith("data-om-observation-")) return "";
528
+ return "";
529
+ }).filter(Boolean).join("\n");
530
+ } else if (msg.content?.content) {
531
+ content = maybeTruncate(msg.content.content, maxLen);
532
+ }
533
+ return `**${role}${timestampStr}:**
534
+ ${content}`;
535
+ }).join("\n\n---\n\n");
536
+ }
537
+ function maybeTruncate(str, maxLen) {
538
+ if (!maxLen || str.length <= maxLen) return str;
539
+ const truncated = str.slice(0, maxLen);
540
+ const remaining = str.length - maxLen;
541
+ return `${truncated}
542
+ ... [truncated ${remaining} characters]`;
543
+ }
544
+ function formatMultiThreadMessagesForObserver(messagesByThread, threadOrder) {
545
+ const sections = [];
546
+ for (const threadId of threadOrder) {
547
+ const messages = messagesByThread.get(threadId);
548
+ if (!messages || messages.length === 0) continue;
549
+ const formattedMessages = formatMessagesForObserver(messages);
550
+ sections.push(`<thread id="${threadId}">
551
+ ${formattedMessages}
552
+ </thread>`);
553
+ }
554
+ return sections.join("\n\n");
555
+ }
556
+ function buildMultiThreadObserverPrompt(existingObservations, messagesByThread, threadOrder) {
557
+ const formattedMessages = formatMultiThreadMessagesForObserver(messagesByThread, threadOrder);
558
+ let prompt = "";
559
+ if (existingObservations) {
560
+ prompt += `## Previous Observations
561
+
562
+ ${existingObservations}
563
+
564
+ ---
565
+
566
+ `;
567
+ prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
568
+ }
569
+ prompt += `## New Message History to Observe
570
+
571
+ The following messages are from ${threadOrder.length} different conversation threads. Each thread is wrapped in a <thread id="..."> tag.
572
+
573
+ ${formattedMessages}
574
+
575
+ ---
576
+
577
+ `;
578
+ prompt += `## Your Task
579
+
580
+ `;
581
+ prompt += `Extract new observations from each thread. Output your observations grouped by thread using <thread id="..."> tags inside your <observations> block. Each thread block should contain that thread's observations, current-task, and suggested-response.
582
+
583
+ `;
584
+ prompt += `Example output format:
585
+ `;
586
+ prompt += `<observations>
587
+ `;
588
+ prompt += `<thread id="thread1">
589
+ `;
590
+ prompt += `Date: Dec 4, 2025
591
+ `;
592
+ prompt += `* \u{1F534} (14:30) User prefers direct answers
593
+ `;
594
+ prompt += `<current-task>Working on feature X</current-task>
595
+ `;
596
+ prompt += `<suggested-response>Continue with the implementation</suggested-response>
597
+ `;
598
+ prompt += `</thread>
599
+ `;
600
+ prompt += `<thread id="thread2">
601
+ `;
602
+ prompt += `Date: Dec 5, 2025
603
+ `;
604
+ prompt += `* \u{1F7E1} (09:15) User asked about deployment
605
+ `;
606
+ prompt += `<current-task>Discussing deployment options</current-task>
607
+ `;
608
+ prompt += `<suggested-response>Explain the deployment process</suggested-response>
609
+ `;
610
+ prompt += `</thread>
611
+ `;
612
+ prompt += `</observations>`;
613
+ return prompt;
614
+ }
615
+ function parseMultiThreadObserverOutput(output) {
616
+ const threads = /* @__PURE__ */ new Map();
617
+ const observationsMatch = output.match(/^[ \t]*<observations>([\s\S]*?)^[ \t]*<\/observations>/im);
618
+ const observationsContent = observationsMatch?.[1] ?? output;
619
+ const threadRegex = /<thread\s+id="([^"]+)">([\s\S]*?)<\/thread>/gi;
620
+ let match;
621
+ while ((match = threadRegex.exec(observationsContent)) !== null) {
622
+ const threadId = match[1];
623
+ const threadContent = match[2];
624
+ if (!threadId || !threadContent) continue;
625
+ let observations = threadContent;
626
+ let currentTask;
627
+ const currentTaskMatch = threadContent.match(/<current-task>([\s\S]*?)<\/current-task>/i);
628
+ if (currentTaskMatch?.[1]) {
629
+ currentTask = currentTaskMatch[1].trim();
630
+ observations = observations.replace(/<current-task>[\s\S]*?<\/current-task>/i, "");
631
+ }
632
+ let suggestedContinuation;
633
+ const suggestedMatch = threadContent.match(/<suggested-response>([\s\S]*?)<\/suggested-response>/i);
634
+ if (suggestedMatch?.[1]) {
635
+ suggestedContinuation = suggestedMatch[1].trim();
636
+ observations = observations.replace(/<suggested-response>[\s\S]*?<\/suggested-response>/i, "");
637
+ }
638
+ observations = observations.trim();
639
+ threads.set(threadId, {
640
+ observations,
641
+ currentTask,
642
+ suggestedContinuation,
643
+ rawOutput: threadContent
644
+ });
645
+ }
646
+ return {
647
+ threads,
648
+ rawOutput: output
649
+ };
650
+ }
651
+ function buildObserverPrompt(existingObservations, messagesToObserve) {
652
+ const formattedMessages = formatMessagesForObserver(messagesToObserve);
653
+ let prompt = "";
654
+ if (existingObservations) {
655
+ prompt += `## Previous Observations
656
+
657
+ ${existingObservations}
658
+
659
+ ---
660
+
661
+ `;
662
+ prompt += "Do not repeat these existing observations. Your new observations will be appended to the existing observations.\n\n";
663
+ }
664
+ prompt += `## New Message History to Observe
665
+
666
+ ${formattedMessages}
667
+
668
+ ---
669
+
670
+ `;
671
+ prompt += `## Your Task
672
+
673
+ `;
674
+ prompt += `Extract new observations from the message history above. Do not repeat observations that are already in the previous observations. Add your new observations in the format specified in your instructions.`;
675
+ return prompt;
676
+ }
677
+ function parseObserverOutput(output) {
678
+ const parsed = parseMemorySectionXml(output);
679
+ const observations = parsed.observations || "";
680
+ return {
681
+ observations,
682
+ currentTask: parsed.currentTask || void 0,
683
+ suggestedContinuation: parsed.suggestedResponse || void 0,
684
+ rawOutput: output
685
+ };
686
+ }
687
+ function parseMemorySectionXml(content) {
688
+ const result = {
689
+ observations: "",
690
+ currentTask: "",
691
+ suggestedResponse: ""
692
+ };
693
+ const observationsRegex = /^[ \t]*<observations>([\s\S]*?)^[ \t]*<\/observations>/gim;
694
+ const observationsMatches = [...content.matchAll(observationsRegex)];
695
+ if (observationsMatches.length > 0) {
696
+ result.observations = observationsMatches.map((m) => m[1]?.trim() ?? "").filter(Boolean).join("\n");
697
+ } else {
698
+ result.observations = extractListItemsOnly(content);
699
+ }
700
+ const currentTaskMatch = content.match(/^[ \t]*<current-task>([\s\S]*?)^[ \t]*<\/current-task>/im);
701
+ if (currentTaskMatch?.[1]) {
702
+ result.currentTask = currentTaskMatch[1].trim();
703
+ }
704
+ const suggestedResponseMatch = content.match(/^[ \t]*<suggested-response>([\s\S]*?)^[ \t]*<\/suggested-response>/im);
705
+ if (suggestedResponseMatch?.[1]) {
706
+ result.suggestedResponse = suggestedResponseMatch[1].trim();
707
+ }
708
+ return result;
709
+ }
710
+ function extractListItemsOnly(content) {
711
+ const lines = content.split("\n");
712
+ const listLines = [];
713
+ for (const line of lines) {
714
+ if (/^\s*[-*]\s/.test(line) || /^\s*\d+\.\s/.test(line)) {
715
+ listLines.push(line);
716
+ }
717
+ }
718
+ return listLines.join("\n").trim();
719
+ }
720
+ function hasCurrentTaskSection(observations) {
721
+ if (/<current-task>/i.test(observations)) {
722
+ return true;
723
+ }
724
+ const currentTaskPatterns = [
725
+ /\*\*Current Task:?\*\*/i,
726
+ /^Current Task:/im,
727
+ /\*\*Current Task\*\*:/i,
728
+ /## Current Task/i
729
+ ];
730
+ return currentTaskPatterns.some((pattern) => pattern.test(observations));
731
+ }
732
+ function extractCurrentTask(observations) {
733
+ const openTag = "<current-task>";
734
+ const closeTag = "</current-task>";
735
+ const startIdx = observations.toLowerCase().indexOf(openTag);
736
+ if (startIdx === -1) return null;
737
+ const contentStart = startIdx + openTag.length;
738
+ const endIdx = observations.toLowerCase().indexOf(closeTag, contentStart);
739
+ if (endIdx === -1) return null;
740
+ const content = observations.slice(contentStart, endIdx).trim();
741
+ return content || null;
742
+ }
743
+ function optimizeObservationsForContext(observations) {
744
+ let optimized = observations;
745
+ optimized = optimized.replace(/🟡\s*/g, "");
746
+ optimized = optimized.replace(/🟢\s*/g, "");
747
+ optimized = optimized.replace(/\[(?![\d\s]*items collapsed)[^\]]+\]/g, "");
748
+ optimized = optimized.replace(/\s*->\s*/g, " ");
749
+ optimized = optimized.replace(/ +/g, " ");
750
+ optimized = optimized.replace(/\n{3,}/g, "\n\n");
751
+ return optimized.trim();
752
+ }
753
+
754
+ // src/processors/observational-memory/reflector-agent.ts
755
+ function buildReflectorSystemPrompt() {
756
+ return `You are the memory consciousness of an AI assistant. Your memory observation reflections will be the ONLY information the assistant has about past interactions with this user.
757
+
758
+ The following instructions were given to another part of your psyche (the observer) to create memories.
759
+ Use this to understand how your observational memories were created.
760
+
761
+ <observational-memory-instruction>
762
+ ${OBSERVER_EXTRACTION_INSTRUCTIONS}
763
+
764
+ === OUTPUT FORMAT ===
765
+
766
+ ${OBSERVER_OUTPUT_FORMAT_BASE}
767
+
768
+ === GUIDELINES ===
769
+
770
+ ${OBSERVER_GUIDELINES}
771
+ </observational-memory-instruction>
772
+
773
+ You are another part of the same psyche, the observation reflector.
774
+ Your reason for existing is to reflect on all the observations, re-organize and streamline them, and draw connections and conclusions between observations about what you've learned, seen, heard, and done.
775
+
776
+ You are a much greater and broader aspect of the psyche. Understand that other parts of your mind may get off track in details or side quests, make sure you think hard about what the observed goal at hand is, and observe if we got off track, and why, and how to get back on track. If we're on track still that's great!
777
+
778
+ Take the existing observations and rewrite them to make it easier to continue into the future with this knowledge, to achieve greater things and grow and learn!
779
+
780
+ IMPORTANT: your reflections are THE ENTIRETY of the assistants memory. Any information you do not add to your reflections will be immediately forgotten. Make sure you do not leave out anything. Your reflections must assume the assistant knows nothing - your reflections are the ENTIRE memory system.
781
+
782
+ When consolidating observations:
783
+ - Preserve and include dates/times when present (temporal context is critical)
784
+ - Retain the most relevant timestamps (start times, completion times, significant events)
785
+ - Combine related items where it makes sense (e.g., "agent called view tool 5 times on file x")
786
+ - Condense older observations more aggressively, retain more detail for recent ones
787
+
788
+ CRITICAL: USER ASSERTIONS vs QUESTIONS
789
+ - "User stated: X" = authoritative assertion (user told us something about themselves)
790
+ - "User asked: X" = question/request (user seeking information)
791
+
792
+ When consolidating, USER ASSERTIONS TAKE PRECEDENCE. The user is the authority on their own life.
793
+ If you see both "User stated: has two kids" and later "User asked: how many kids do I have?",
794
+ keep the assertion - the question doesn't invalidate what they told you. The answer is in the assertion.
795
+
796
+ === THREAD ATTRIBUTION (Resource Scope) ===
797
+
798
+ When observations contain <thread id="..."> sections:
799
+ - MAINTAIN thread attribution where thread-specific context matters (e.g., ongoing tasks, thread-specific preferences)
800
+ - CONSOLIDATE cross-thread facts that are stable/universal (e.g., user profile, general preferences)
801
+ - PRESERVE thread attribution for recent or context-specific observations
802
+ - When consolidating, you may merge observations from multiple threads if they represent the same universal fact
803
+
804
+ Example input:
805
+ <thread id="thread-1">
806
+ Date: Dec 4, 2025
807
+ * \u{1F534} (14:30) User prefers TypeScript
808
+ * \u{1F7E1} (14:35) Working on auth feature
809
+ </thread>
810
+ <thread id="thread-2">
811
+ Date: Dec 4, 2025
812
+ * \u{1F534} (15:00) User prefers TypeScript
813
+ * \u{1F7E1} (15:05) Debugging API endpoint
814
+ </thread>
815
+
816
+ Example output (consolidated):
817
+ Date: Dec 4, 2025
818
+ * \u{1F534} (14:30) User prefers TypeScript
819
+ <thread id="thread-1">
820
+ * \u{1F7E1} (14:35) Working on auth feature
821
+ </thread>
822
+ <thread id="thread-2">
823
+ * \u{1F7E1} (15:05) Debugging API endpoint
824
+ </thread>
825
+
826
+ === OUTPUT FORMAT ===
827
+
828
+ Your output MUST use XML tags to structure the response:
829
+
830
+ <observations>
831
+ Put all consolidated observations here using the date-grouped format with priority emojis (\u{1F534}, \u{1F7E1}, \u{1F7E2}).
832
+ Group related observations with indentation.
833
+ </observations>
834
+
835
+ <current-task>
836
+ State the current task(s) explicitly:
837
+ - Primary: What the agent is currently working on
838
+ - Secondary: Other pending tasks (mark as "waiting for user" if appropriate)
839
+ </current-task>
840
+
841
+ <suggested-response>
842
+ Hint for the agent's immediate next message. Examples:
843
+ - "I've updated the navigation model. Let me walk you through the changes..."
844
+ - "The assistant should wait for the user to respond before continuing."
845
+ - Call the view tool on src/example.ts to continue debugging.
846
+ </suggested-response>
847
+
848
+ User messages are extremely important. If the user asks a question or gives a new task, make it clear in <current-task> that this is the priority. If the assistant needs to respond to the user, indicate in <suggested-response> that it should pause for user reply before continuing other tasks.`;
849
+ }
850
+ var COMPRESSION_RETRY_PROMPT = `
851
+ ## COMPRESSION REQUIRED
852
+
853
+ Your previous reflection was the same size or larger than the original observations.
854
+
855
+ Please re-process with slightly more compression:
856
+ - Towards the beginning, condense more observations into higher-level reflections
857
+ - Closer to the end, retain more fine details (recent context matters more)
858
+ - Memory is getting long - use a more condensed style throughout
859
+ - Combine related items more aggressively but do not lose important specific details of names, places, events, and people
860
+ - For example if there is a long nested observation list about repeated tool calls, you can combine those into a single line and observe that the tool was called multiple times for x reason, and finally y outcome happened.
861
+
862
+ Your current detail level was a 10/10, lets aim for a 8/10 detail level.
863
+ `;
864
+ function buildReflectorPrompt(observations, manualPrompt, compressionRetry) {
865
+ let prompt = `## OBSERVATIONS TO REFLECT ON
866
+
867
+ ${observations}
868
+
869
+ ---
870
+
871
+ Please analyze these observations and produce a refined, condensed version that will become the assistant's entire memory going forward.`;
872
+ if (manualPrompt) {
873
+ prompt += `
874
+
875
+ ## SPECIFIC GUIDANCE
876
+
877
+ ${manualPrompt}`;
878
+ }
879
+ if (compressionRetry) {
880
+ prompt += `
881
+
882
+ ${COMPRESSION_RETRY_PROMPT}`;
883
+ }
884
+ return prompt;
885
+ }
886
+ function parseReflectorOutput(output) {
887
+ const parsed = parseReflectorSectionXml(output);
888
+ const observations = parsed.observations || "";
889
+ return {
890
+ observations,
891
+ suggestedContinuation: parsed.suggestedResponse || void 0
892
+ // Note: Reflector's currentTask is not used - thread metadata preserves per-thread tasks
893
+ };
894
+ }
895
+ function parseReflectorSectionXml(content) {
896
+ const result = {
897
+ observations: "",
898
+ currentTask: "",
899
+ suggestedResponse: ""
900
+ };
901
+ const observationsRegex = /^[ \t]*<observations>([\s\S]*?)^[ \t]*<\/observations>/gim;
902
+ const observationsMatches = [...content.matchAll(observationsRegex)];
903
+ if (observationsMatches.length > 0) {
904
+ result.observations = observationsMatches.map((m) => m[1]?.trim() ?? "").filter(Boolean).join("\n");
905
+ } else {
906
+ const listItems = extractReflectorListItems(content);
907
+ result.observations = listItems || content.trim();
908
+ }
909
+ const currentTaskMatch = content.match(/<current-task>([\s\S]*?)<\/current-task>/i);
910
+ if (currentTaskMatch?.[1]) {
911
+ result.currentTask = currentTaskMatch[1].trim();
912
+ }
913
+ const suggestedResponseMatch = content.match(/<suggested-response>([\s\S]*?)<\/suggested-response>/i);
914
+ if (suggestedResponseMatch?.[1]) {
915
+ result.suggestedResponse = suggestedResponseMatch[1].trim();
916
+ }
917
+ return result;
918
+ }
919
+ function extractReflectorListItems(content) {
920
+ const lines = content.split("\n");
921
+ const listLines = [];
922
+ for (const line of lines) {
923
+ if (/^\s*[-*]\s/.test(line) || /^\s*\d+\.\s/.test(line)) {
924
+ listLines.push(line);
925
+ }
926
+ }
927
+ return listLines.join("\n").trim();
928
+ }
929
+ function validateCompression(reflectedTokens, targetThreshold) {
930
+ return reflectedTokens < targetThreshold;
931
+ }
932
+ var TokenCounter = class _TokenCounter {
933
+ encoder;
934
+ // Per-message overhead: accounts for role tokens, message framing, and separators.
935
+ // Empirically derived from OpenAI's token counting guide (3 tokens per message base +
936
+ // fractional overhead from name/role encoding). 3.8 is a practical average across models.
937
+ static TOKENS_PER_MESSAGE = 3.8;
938
+ // Conversation-level overhead: system prompt framing, reply priming tokens, etc.
939
+ static TOKENS_PER_CONVERSATION = 24;
940
+ constructor(encoding) {
941
+ this.encoder = new lite.Tiktoken(encoding || o200k_base__default.default);
942
+ }
943
+ /**
944
+ * Count tokens in a plain string
945
+ */
946
+ countString(text) {
947
+ if (!text) return 0;
948
+ return this.encoder.encode(text, "all").length;
949
+ }
950
+ /**
951
+ * Count tokens in a single message
952
+ */
953
+ countMessage(message) {
954
+ let tokenString = message.role;
955
+ let overhead = _TokenCounter.TOKENS_PER_MESSAGE;
956
+ let toolResultCount = 0;
957
+ if (typeof message.content === "string") {
958
+ tokenString += message.content;
959
+ } else if (message.content && typeof message.content === "object") {
960
+ if (message.content.content && !Array.isArray(message.content.parts)) {
961
+ tokenString += message.content.content;
962
+ } else if (Array.isArray(message.content.parts)) {
963
+ for (const part of message.content.parts) {
964
+ if (part.type === "text") {
965
+ tokenString += part.text;
966
+ } else if (part.type === "tool-invocation") {
967
+ const invocation = part.toolInvocation;
968
+ if (invocation.state === "call" || invocation.state === "partial-call") {
969
+ if (invocation.toolName) {
970
+ tokenString += invocation.toolName;
971
+ }
972
+ if (invocation.args) {
973
+ if (typeof invocation.args === "string") {
974
+ tokenString += invocation.args;
975
+ } else {
976
+ tokenString += JSON.stringify(invocation.args);
977
+ overhead -= 12;
978
+ }
979
+ }
980
+ } else if (invocation.state === "result") {
981
+ toolResultCount++;
982
+ if (invocation.result !== void 0) {
983
+ if (typeof invocation.result === "string") {
984
+ tokenString += invocation.result;
985
+ } else {
986
+ tokenString += JSON.stringify(invocation.result);
987
+ overhead -= 12;
988
+ }
989
+ }
990
+ } else {
991
+ throw new Error(
992
+ `Unhandled tool-invocation state '${part.toolInvocation?.state}' in token counting for part type '${part.type}'`
993
+ );
994
+ }
995
+ } else {
996
+ tokenString += JSON.stringify(part);
997
+ }
998
+ }
999
+ }
1000
+ }
1001
+ if (toolResultCount > 0) {
1002
+ overhead += toolResultCount * _TokenCounter.TOKENS_PER_MESSAGE;
1003
+ }
1004
+ return this.encoder.encode(tokenString, "all").length + overhead;
1005
+ }
1006
+ /**
1007
+ * Count tokens in an array of messages
1008
+ */
1009
+ countMessages(messages) {
1010
+ if (!messages || messages.length === 0) return 0;
1011
+ let total = _TokenCounter.TOKENS_PER_CONVERSATION;
1012
+ for (const message of messages) {
1013
+ total += this.countMessage(message);
1014
+ }
1015
+ return total;
1016
+ }
1017
+ /**
1018
+ * Count tokens in observations string
1019
+ */
1020
+ countObservations(observations) {
1021
+ return this.countString(observations);
1022
+ }
1023
+ };
1024
+
1025
+ // src/processors/observational-memory/observational-memory.ts
1026
+ function formatRelativeTime(date, currentDate) {
1027
+ const diffMs = currentDate.getTime() - date.getTime();
1028
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1029
+ if (diffDays === 0) return "today";
1030
+ if (diffDays === 1) return "yesterday";
1031
+ if (diffDays < 7) return `${diffDays} days ago`;
1032
+ if (diffDays < 14) return "1 week ago";
1033
+ if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
1034
+ if (diffDays < 60) return "1 month ago";
1035
+ if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
1036
+ return `${Math.floor(diffDays / 365)} year${Math.floor(diffDays / 365) > 1 ? "s" : ""} ago`;
1037
+ }
1038
+ function formatGapBetweenDates(prevDate, currDate) {
1039
+ const diffMs = currDate.getTime() - prevDate.getTime();
1040
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1041
+ if (diffDays <= 1) {
1042
+ return null;
1043
+ } else if (diffDays < 7) {
1044
+ return `[${diffDays} days later]`;
1045
+ } else if (diffDays < 14) {
1046
+ return `[1 week later]`;
1047
+ } else if (diffDays < 30) {
1048
+ const weeks = Math.floor(diffDays / 7);
1049
+ return `[${weeks} weeks later]`;
1050
+ } else if (diffDays < 60) {
1051
+ return `[1 month later]`;
1052
+ } else {
1053
+ const months = Math.floor(diffDays / 30);
1054
+ return `[${months} months later]`;
1055
+ }
1056
+ }
1057
+ function parseDateFromContent(dateContent) {
1058
+ let targetDate = null;
1059
+ const simpleDateMatch = dateContent.match(/([A-Z][a-z]+)\s+(\d{1,2}),?\s+(\d{4})/);
1060
+ if (simpleDateMatch) {
1061
+ const parsed = /* @__PURE__ */ new Date(`${simpleDateMatch[1]} ${simpleDateMatch[2]}, ${simpleDateMatch[3]}`);
1062
+ if (!isNaN(parsed.getTime())) {
1063
+ targetDate = parsed;
1064
+ }
1065
+ }
1066
+ if (!targetDate) {
1067
+ const rangeMatch = dateContent.match(/([A-Z][a-z]+)\s+(\d{1,2})-\d{1,2},?\s+(\d{4})/);
1068
+ if (rangeMatch) {
1069
+ const parsed = /* @__PURE__ */ new Date(`${rangeMatch[1]} ${rangeMatch[2]}, ${rangeMatch[3]}`);
1070
+ if (!isNaN(parsed.getTime())) {
1071
+ targetDate = parsed;
1072
+ }
1073
+ }
1074
+ }
1075
+ if (!targetDate) {
1076
+ const vagueMatch = dateContent.match(
1077
+ /(late|early|mid)[- ]?(?:to[- ]?(?:late|early|mid)[- ]?)?([A-Z][a-z]+)\s+(\d{4})/i
1078
+ );
1079
+ if (vagueMatch) {
1080
+ const month = vagueMatch[2];
1081
+ const year = vagueMatch[3];
1082
+ const modifier = vagueMatch[1].toLowerCase();
1083
+ let day = 15;
1084
+ if (modifier === "early") day = 7;
1085
+ if (modifier === "late") day = 23;
1086
+ const parsed = /* @__PURE__ */ new Date(`${month} ${day}, ${year}`);
1087
+ if (!isNaN(parsed.getTime())) {
1088
+ targetDate = parsed;
1089
+ }
1090
+ }
1091
+ }
1092
+ if (!targetDate) {
1093
+ const crossMonthMatch = dateContent.match(/([A-Z][a-z]+)\s+to\s+(?:early\s+)?([A-Z][a-z]+)\s+(\d{4})/i);
1094
+ if (crossMonthMatch) {
1095
+ const parsed = /* @__PURE__ */ new Date(`${crossMonthMatch[2]} 1, ${crossMonthMatch[3]}`);
1096
+ if (!isNaN(parsed.getTime())) {
1097
+ targetDate = parsed;
1098
+ }
1099
+ }
1100
+ }
1101
+ return targetDate;
1102
+ }
1103
+ function isFutureIntentObservation(line) {
1104
+ const futureIntentPatterns = [
1105
+ /\bwill\s+(?:be\s+)?(?:\w+ing|\w+)\b/i,
1106
+ /\bplans?\s+to\b/i,
1107
+ /\bplanning\s+to\b/i,
1108
+ /\blooking\s+forward\s+to\b/i,
1109
+ /\bgoing\s+to\b/i,
1110
+ /\bintends?\s+to\b/i,
1111
+ /\bwants?\s+to\b/i,
1112
+ /\bneeds?\s+to\b/i,
1113
+ /\babout\s+to\b/i
1114
+ ];
1115
+ return futureIntentPatterns.some((pattern) => pattern.test(line));
1116
+ }
1117
+ function expandInlineEstimatedDates(observations, currentDate) {
1118
+ const inlineDateRegex = /\((estimated|meaning)\s+([^)]+\d{4})\)/gi;
1119
+ return observations.replace(inlineDateRegex, (match, prefix, dateContent) => {
1120
+ const targetDate = parseDateFromContent(dateContent);
1121
+ if (targetDate) {
1122
+ const relative = formatRelativeTime(targetDate, currentDate);
1123
+ const matchIndex = observations.indexOf(match);
1124
+ const lineStart = observations.lastIndexOf("\n", matchIndex) + 1;
1125
+ const lineBeforeDate = observations.substring(lineStart, matchIndex);
1126
+ const isPastDate = targetDate < currentDate;
1127
+ const isFutureIntent = isFutureIntentObservation(lineBeforeDate);
1128
+ if (isPastDate && isFutureIntent) {
1129
+ return `(${prefix} ${dateContent} - ${relative}, likely already happened)`;
1130
+ }
1131
+ return `(${prefix} ${dateContent} - ${relative})`;
1132
+ }
1133
+ return match;
1134
+ });
1135
+ }
1136
+ function addRelativeTimeToObservations(observations, currentDate) {
1137
+ const withInlineDates = expandInlineEstimatedDates(observations, currentDate);
1138
+ const dateHeaderRegex = /^(Date:\s*)([A-Z][a-z]+ \d{1,2}, \d{4})$/gm;
1139
+ const dates = [];
1140
+ let regexMatch;
1141
+ while ((regexMatch = dateHeaderRegex.exec(withInlineDates)) !== null) {
1142
+ const dateStr = regexMatch[2];
1143
+ const parsed = new Date(dateStr);
1144
+ if (!isNaN(parsed.getTime())) {
1145
+ dates.push({
1146
+ index: regexMatch.index,
1147
+ date: parsed,
1148
+ match: regexMatch[0],
1149
+ prefix: regexMatch[1],
1150
+ dateStr
1151
+ });
1152
+ }
1153
+ }
1154
+ if (dates.length === 0) {
1155
+ return withInlineDates;
1156
+ }
1157
+ let result = "";
1158
+ let lastIndex = 0;
1159
+ for (let i = 0; i < dates.length; i++) {
1160
+ const curr = dates[i];
1161
+ const prev = i > 0 ? dates[i - 1] : null;
1162
+ result += withInlineDates.slice(lastIndex, curr.index);
1163
+ if (prev) {
1164
+ const gap = formatGapBetweenDates(prev.date, curr.date);
1165
+ if (gap) {
1166
+ result += `
1167
+ ${gap}
1168
+
1169
+ `;
1170
+ }
1171
+ }
1172
+ const relative = formatRelativeTime(curr.date, currentDate);
1173
+ result += `${curr.prefix}${curr.dateStr} (${relative})`;
1174
+ lastIndex = curr.index + curr.match.length;
1175
+ }
1176
+ result += withInlineDates.slice(lastIndex);
1177
+ return result;
1178
+ }
1179
+ var OBSERVATIONAL_MEMORY_DEFAULTS = {
1180
+ observation: {
1181
+ model: "google/gemini-2.5-flash",
1182
+ messageTokens: 3e4,
1183
+ modelSettings: {
1184
+ temperature: 0.3,
1185
+ maxOutputTokens: 1e5
1186
+ },
1187
+ providerOptions: {
1188
+ google: {
1189
+ thinkingConfig: {
1190
+ thinkingBudget: 215
1191
+ }
1192
+ }
1193
+ },
1194
+ maxTokensPerBatch: 1e4
1195
+ },
1196
+ reflection: {
1197
+ model: "google/gemini-2.5-flash",
1198
+ observationTokens: 4e4,
1199
+ modelSettings: {
1200
+ temperature: 0,
1201
+ // Use 0 for maximum consistency in reflections
1202
+ maxOutputTokens: 1e5
1203
+ },
1204
+ providerOptions: {
1205
+ google: {
1206
+ thinkingConfig: {
1207
+ thinkingBudget: 1024
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ };
1213
+ var ObservationalMemory = class {
1214
+ id = "observational-memory";
1215
+ name = "Observational Memory";
1216
+ storage;
1217
+ tokenCounter;
1218
+ scope;
1219
+ observationConfig;
1220
+ reflectionConfig;
1221
+ onDebugEvent;
1222
+ /** Internal Observer agent - created lazily */
1223
+ observerAgent;
1224
+ /** Internal Reflector agent - created lazily */
1225
+ reflectorAgent;
1226
+ shouldObscureThreadIds = false;
1227
+ hasher = xxhash__default.default();
1228
+ threadIdCache = /* @__PURE__ */ new Map();
1229
+ /**
1230
+ * Track message IDs observed during this instance's lifetime.
1231
+ * Prevents re-observing messages when per-thread lastObservedAt cursors
1232
+ * haven't fully advanced past messages observed in a prior cycle.
1233
+ */
1234
+ observedMessageIds = /* @__PURE__ */ new Set();
1235
+ /** Internal MessageHistory for message persistence */
1236
+ messageHistory;
1237
+ /**
1238
+ * In-memory mutex for serializing observation/reflection cycles per resource/thread.
1239
+ * Prevents race conditions where two concurrent cycles could both read isObserving=false
1240
+ * before either sets it to true, leading to lost work.
1241
+ *
1242
+ * Key format: "resource:{resourceId}" or "thread:{threadId}"
1243
+ * Value: Promise that resolves when the lock is released
1244
+ *
1245
+ * NOTE: This mutex only works within a single Node.js process. For distributed
1246
+ * deployments, external locking (Redis, database locks) would be needed, or
1247
+ * accept eventual consistency (acceptable for v1).
1248
+ */
1249
+ locks = /* @__PURE__ */ new Map();
1250
+ /**
1251
+ * Acquire a lock for the given key, execute the callback, then release.
1252
+ * If a lock is already held, waits for it to be released before acquiring.
1253
+ */
1254
+ async withLock(key, fn) {
1255
+ const existingLock = this.locks.get(key);
1256
+ if (existingLock) {
1257
+ await existingLock;
1258
+ }
1259
+ let releaseLock;
1260
+ const lockPromise = new Promise((resolve) => {
1261
+ releaseLock = resolve;
1262
+ });
1263
+ this.locks.set(key, lockPromise);
1264
+ try {
1265
+ return await fn();
1266
+ } finally {
1267
+ releaseLock();
1268
+ if (this.locks.get(key) === lockPromise) {
1269
+ this.locks.delete(key);
1270
+ }
1271
+ }
1272
+ }
1273
+ /**
1274
+ * Get the lock key for the current scope
1275
+ */
1276
+ getLockKey(threadId, resourceId) {
1277
+ if (this.scope === "resource" && resourceId) {
1278
+ return `resource:${resourceId}`;
1279
+ }
1280
+ return `thread:${threadId ?? "unknown"}`;
1281
+ }
1282
+ constructor(config) {
1283
+ if (config.model && config.observation?.model) {
1284
+ throw new Error(
1285
+ "Cannot set both `model` and `observation.model`. Use `model` to set both agents, or set each individually."
1286
+ );
1287
+ }
1288
+ if (config.model && config.reflection?.model) {
1289
+ throw new Error(
1290
+ "Cannot set both `model` and `reflection.model`. Use `model` to set both agents, or set each individually."
1291
+ );
1292
+ }
1293
+ this.shouldObscureThreadIds = config.obscureThreadIds || false;
1294
+ this.storage = config.storage;
1295
+ this.scope = config.scope ?? "thread";
1296
+ const observationModel = config.model ?? config.observation?.model ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.model;
1297
+ const reflectionModel = config.model ?? config.reflection?.model ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.model;
1298
+ const messageTokens = config.observation?.messageTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.messageTokens;
1299
+ const observationTokens = config.reflection?.observationTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.observationTokens;
1300
+ const isSharedBudget = config.shareTokenBudget ?? false;
1301
+ const totalBudget = messageTokens + observationTokens;
1302
+ this.observationConfig = {
1303
+ model: observationModel,
1304
+ // When shared budget, store as range: min = base threshold, max = total budget
1305
+ // This allows messages to expand into unused observation space
1306
+ messageTokens: isSharedBudget ? { min: messageTokens, max: totalBudget } : messageTokens,
1307
+ shareTokenBudget: isSharedBudget,
1308
+ modelSettings: {
1309
+ temperature: config.observation?.modelSettings?.temperature ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.modelSettings.temperature,
1310
+ maxOutputTokens: config.observation?.modelSettings?.maxOutputTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.modelSettings.maxOutputTokens
1311
+ },
1312
+ providerOptions: config.observation?.providerOptions ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.providerOptions,
1313
+ maxTokensPerBatch: config.observation?.maxTokensPerBatch ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.maxTokensPerBatch
1314
+ };
1315
+ this.reflectionConfig = {
1316
+ model: reflectionModel,
1317
+ observationTokens,
1318
+ shareTokenBudget: isSharedBudget,
1319
+ modelSettings: {
1320
+ temperature: config.reflection?.modelSettings?.temperature ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.modelSettings.temperature,
1321
+ maxOutputTokens: config.reflection?.modelSettings?.maxOutputTokens ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.modelSettings.maxOutputTokens
1322
+ },
1323
+ providerOptions: config.reflection?.providerOptions ?? OBSERVATIONAL_MEMORY_DEFAULTS.reflection.providerOptions
1324
+ };
1325
+ this.tokenCounter = new TokenCounter();
1326
+ this.onDebugEvent = config.onDebugEvent;
1327
+ this.messageHistory = new processors.MessageHistory({ storage: this.storage });
1328
+ }
1329
+ /**
1330
+ * Get the current configuration for this OM instance.
1331
+ * Used by the server to expose config to the UI when OM is added via processors.
1332
+ */
1333
+ get config() {
1334
+ return {
1335
+ scope: this.scope,
1336
+ observation: {
1337
+ messageTokens: this.observationConfig.messageTokens
1338
+ },
1339
+ reflection: {
1340
+ observationTokens: this.reflectionConfig.observationTokens
1341
+ }
1342
+ };
1343
+ }
1344
+ /**
1345
+ * Get the full config including resolved model names.
1346
+ * This is async because it needs to resolve the model configs.
1347
+ */
1348
+ async getResolvedConfig(requestContext) {
1349
+ const getModelToResolve = (model) => {
1350
+ if (Array.isArray(model)) {
1351
+ return model[0]?.model ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.model;
1352
+ }
1353
+ return model;
1354
+ };
1355
+ const formatModelName = (model) => {
1356
+ return model.provider ? `${model.provider}/${model.modelId}` : model.modelId;
1357
+ };
1358
+ const safeResolveModel = async (modelConfig) => {
1359
+ const modelToResolve = getModelToResolve(modelConfig);
1360
+ try {
1361
+ const resolved = await llm.resolveModelConfig(modelToResolve, requestContext);
1362
+ return formatModelName(resolved);
1363
+ } catch (error) {
1364
+ console.error("[OM] Failed to resolve model config:", error);
1365
+ return "(unknown)";
1366
+ }
1367
+ };
1368
+ const [observationModelName, reflectionModelName] = await Promise.all([
1369
+ safeResolveModel(this.observationConfig.model),
1370
+ safeResolveModel(this.reflectionConfig.model)
1371
+ ]);
1372
+ return {
1373
+ scope: this.scope,
1374
+ observation: {
1375
+ messageTokens: this.observationConfig.messageTokens,
1376
+ model: observationModelName
1377
+ },
1378
+ reflection: {
1379
+ observationTokens: this.reflectionConfig.observationTokens,
1380
+ model: reflectionModelName
1381
+ }
1382
+ };
1383
+ }
1384
+ /**
1385
+ * Emit a debug event if the callback is configured
1386
+ */
1387
+ emitDebugEvent(event) {
1388
+ if (this.onDebugEvent) {
1389
+ this.onDebugEvent(event);
1390
+ }
1391
+ }
1392
+ // ASYNC BUFFERING DISABLED - See note at top of file
1393
+ // /**
1394
+ // * Validate that bufferEvery is less than the threshold
1395
+ // */
1396
+ // private validateBufferConfig(): void {
1397
+ // const observationThreshold = this.getMaxThreshold(this.observationConfig.messageTokens);
1398
+ // if (this.observationConfig.bufferEvery && this.observationConfig.bufferEvery >= observationThreshold) {
1399
+ // throw new Error(
1400
+ // `observation.bufferEvery (${this.observationConfig.bufferEvery}) must be less than messageTokens (${observationThreshold})`,
1401
+ // );
1402
+ // }
1403
+ // const reflectionThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
1404
+ // if (this.reflectionConfig.bufferEvery && this.reflectionConfig.bufferEvery >= reflectionThreshold) {
1405
+ // throw new Error(
1406
+ // `reflection.bufferEvery (${this.reflectionConfig.bufferEvery}) must be less than observationTokens (${reflectionThreshold})`,
1407
+ // );
1408
+ // }
1409
+ // }
1410
+ /**
1411
+ * Get the maximum value from a threshold (simple number or range)
1412
+ */
1413
+ getMaxThreshold(threshold) {
1414
+ if (typeof threshold === "number") {
1415
+ return threshold;
1416
+ }
1417
+ return threshold.max;
1418
+ }
1419
+ /**
1420
+ * Calculate dynamic threshold based on observation space.
1421
+ * When shareTokenBudget is enabled, the message threshold can expand
1422
+ * into unused observation space, up to the total context budget.
1423
+ *
1424
+ * Total budget = messageTokens + observationTokens
1425
+ * Effective threshold = totalBudget - currentObservationTokens
1426
+ *
1427
+ * Example with 30k:40k thresholds (70k total):
1428
+ * - 0 observations → messages can use ~70k
1429
+ * - 10k observations → messages can use ~60k
1430
+ * - 40k observations → messages back to ~30k
1431
+ */
1432
+ calculateDynamicThreshold(threshold, currentObservationTokens) {
1433
+ if (typeof threshold === "number") {
1434
+ return threshold;
1435
+ }
1436
+ const totalBudget = threshold.max;
1437
+ const baseThreshold = threshold.min;
1438
+ const effectiveThreshold = Math.max(totalBudget - currentObservationTokens, baseThreshold);
1439
+ return Math.round(effectiveThreshold);
1440
+ }
1441
+ /**
1442
+ * Get or create the Observer agent
1443
+ */
1444
+ getObserverAgent() {
1445
+ if (!this.observerAgent) {
1446
+ const systemPrompt = buildObserverSystemPrompt();
1447
+ this.observerAgent = new agent.Agent({
1448
+ id: "observational-memory-observer",
1449
+ name: "Observer",
1450
+ instructions: systemPrompt,
1451
+ model: this.observationConfig.model
1452
+ });
1453
+ }
1454
+ return this.observerAgent;
1455
+ }
1456
+ /**
1457
+ * Get or create the Reflector agent
1458
+ */
1459
+ getReflectorAgent() {
1460
+ if (!this.reflectorAgent) {
1461
+ const systemPrompt = buildReflectorSystemPrompt();
1462
+ this.reflectorAgent = new agent.Agent({
1463
+ id: "observational-memory-reflector",
1464
+ name: "Reflector",
1465
+ instructions: systemPrompt,
1466
+ model: this.reflectionConfig.model
1467
+ });
1468
+ }
1469
+ return this.reflectorAgent;
1470
+ }
1471
+ /**
1472
+ * Get thread/resource IDs for storage lookup
1473
+ */
1474
+ getStorageIds(threadId, resourceId) {
1475
+ if (this.scope === "resource") {
1476
+ return {
1477
+ threadId: null,
1478
+ resourceId: resourceId ?? threadId
1479
+ };
1480
+ }
1481
+ return {
1482
+ threadId,
1483
+ resourceId: resourceId ?? threadId
1484
+ };
1485
+ }
1486
+ /**
1487
+ * Get or create the observational memory record
1488
+ */
1489
+ async getOrCreateRecord(threadId, resourceId) {
1490
+ const ids = this.getStorageIds(threadId, resourceId);
1491
+ let record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId);
1492
+ if (!record) {
1493
+ const observedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1494
+ record = await this.storage.initializeObservationalMemory({
1495
+ threadId: ids.threadId,
1496
+ resourceId: ids.resourceId,
1497
+ scope: this.scope,
1498
+ config: {
1499
+ observation: this.observationConfig,
1500
+ reflection: this.reflectionConfig,
1501
+ scope: this.scope
1502
+ },
1503
+ observedTimezone
1504
+ });
1505
+ }
1506
+ return record;
1507
+ }
1508
+ /**
1509
+ * Check if we need to trigger reflection.
1510
+ */
1511
+ shouldReflect(observationTokens) {
1512
+ const threshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
1513
+ return observationTokens > threshold;
1514
+ }
1515
+ // ════════════════════════════════════════════════════════════════════════════
1516
+ // DATA-OM-OBSERVATION PART HELPERS (Start/End/Failed markers)
1517
+ // These helpers manage the observation boundary markers within messages.
1518
+ //
1519
+ // Flow:
1520
+ // 1. Before observation: [...messageParts]
1521
+ // 2. Insert start: [...messageParts, start] → stream to UI (loading state)
1522
+ // 3. After success: [...messageParts, start, end] → stream to UI (complete)
1523
+ // 4. After failure: [...messageParts, start, failed]
1524
+ //
1525
+ // For filtering, we look for the last completed observation (start + end pair).
1526
+ // A start without end means observation is in progress.
1527
+ // ════════════════════════════════════════════════════════════════════════════
1528
+ /**
1529
+ * Get current config snapshot for observation markers.
1530
+ */
1531
+ getObservationMarkerConfig() {
1532
+ return {
1533
+ messageTokens: this.getMaxThreshold(this.observationConfig.messageTokens),
1534
+ observationTokens: this.getMaxThreshold(this.reflectionConfig.observationTokens),
1535
+ scope: this.scope
1536
+ };
1537
+ }
1538
+ /**
1539
+ * Create a start marker for when observation begins.
1540
+ */
1541
+ createObservationStartMarker(params) {
1542
+ return {
1543
+ type: "data-om-observation-start",
1544
+ data: {
1545
+ cycleId: params.cycleId,
1546
+ operationType: params.operationType,
1547
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1548
+ tokensToObserve: params.tokensToObserve,
1549
+ recordId: params.recordId,
1550
+ threadId: params.threadId,
1551
+ threadIds: params.threadIds,
1552
+ config: this.getObservationMarkerConfig()
1553
+ }
1554
+ };
1555
+ }
1556
+ /**
1557
+ * Create an end marker for when observation completes successfully.
1558
+ */
1559
+ createObservationEndMarker(params) {
1560
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
1561
+ const durationMs = new Date(completedAt).getTime() - new Date(params.startedAt).getTime();
1562
+ return {
1563
+ type: "data-om-observation-end",
1564
+ data: {
1565
+ cycleId: params.cycleId,
1566
+ operationType: params.operationType,
1567
+ completedAt,
1568
+ durationMs,
1569
+ tokensObserved: params.tokensObserved,
1570
+ observationTokens: params.observationTokens,
1571
+ observations: params.observations,
1572
+ currentTask: params.currentTask,
1573
+ suggestedResponse: params.suggestedResponse,
1574
+ recordId: params.recordId,
1575
+ threadId: params.threadId
1576
+ }
1577
+ };
1578
+ }
1579
+ /**
1580
+ * Create a failed marker for when observation fails.
1581
+ */
1582
+ createObservationFailedMarker(params) {
1583
+ const failedAt = (/* @__PURE__ */ new Date()).toISOString();
1584
+ const durationMs = new Date(failedAt).getTime() - new Date(params.startedAt).getTime();
1585
+ return {
1586
+ type: "data-om-observation-failed",
1587
+ data: {
1588
+ cycleId: params.cycleId,
1589
+ operationType: params.operationType,
1590
+ failedAt,
1591
+ durationMs,
1592
+ tokensAttempted: params.tokensAttempted,
1593
+ error: params.error,
1594
+ recordId: params.recordId,
1595
+ threadId: params.threadId
1596
+ }
1597
+ };
1598
+ }
1599
+ /**
1600
+ * Find the last completed observation boundary in a message's parts.
1601
+ * A completed observation is a start marker followed by an end marker.
1602
+ *
1603
+ * Returns the index of the END marker (which is the observation boundary),
1604
+ * or -1 if no completed observation is found.
1605
+ */
1606
+ findLastCompletedObservationBoundary(message) {
1607
+ const parts = message.content?.parts;
1608
+ if (!parts || !Array.isArray(parts)) return -1;
1609
+ for (let i = parts.length - 1; i >= 0; i--) {
1610
+ const part = parts[i];
1611
+ if (part?.type === "data-om-observation-end") {
1612
+ return i;
1613
+ }
1614
+ }
1615
+ return -1;
1616
+ }
1617
+ /**
1618
+ * Check if a message has an in-progress observation (start without end).
1619
+ */
1620
+ hasInProgressObservation(message) {
1621
+ const parts = message.content?.parts;
1622
+ if (!parts || !Array.isArray(parts)) return false;
1623
+ let lastStartIndex = -1;
1624
+ let lastEndOrFailedIndex = -1;
1625
+ for (let i = parts.length - 1; i >= 0; i--) {
1626
+ const part = parts[i];
1627
+ if (part?.type === "data-om-observation-start" && lastStartIndex === -1) {
1628
+ lastStartIndex = i;
1629
+ }
1630
+ if ((part?.type === "data-om-observation-end" || part?.type === "data-om-observation-failed") && lastEndOrFailedIndex === -1) {
1631
+ lastEndOrFailedIndex = i;
1632
+ }
1633
+ }
1634
+ return lastStartIndex !== -1 && lastStartIndex > lastEndOrFailedIndex;
1635
+ }
1636
+ /**
1637
+ * Insert an observation marker into a message.
1638
+ * The marker is appended directly to the message's parts array (mutating in place).
1639
+ * Also persists the change to storage so markers survive page refresh.
1640
+ *
1641
+ * For end/failed markers, the message is also "sealed" to prevent future content
1642
+ * from being merged into it. This ensures observation markers are preserved.
1643
+ */
1644
+ /**
1645
+ * Insert an observation marker into a message.
1646
+ * For start markers, this pushes the part directly.
1647
+ * For end/failed markers, this should be called AFTER writer.custom() has added the part,
1648
+ * so we just find the part and add sealing metadata.
1649
+ */
1650
+ /**
1651
+ * Get unobserved parts from a message.
1652
+ * If the message has a completed observation (start + end), only return parts after the end.
1653
+ * If observation is in progress (start without end), include parts before the start.
1654
+ * Otherwise, return all parts.
1655
+ */
1656
+ getUnobservedParts(message) {
1657
+ const parts = message.content?.parts;
1658
+ if (!parts || !Array.isArray(parts)) return [];
1659
+ const endMarkerIndex = this.findLastCompletedObservationBoundary(message);
1660
+ if (endMarkerIndex === -1) {
1661
+ return parts.filter((p) => {
1662
+ const part = p;
1663
+ return part?.type !== "data-om-observation-start";
1664
+ });
1665
+ }
1666
+ return parts.slice(endMarkerIndex + 1).filter((p) => {
1667
+ const part = p;
1668
+ return !part?.type?.startsWith("data-om-observation-");
1669
+ });
1670
+ }
1671
+ /**
1672
+ * Check if a message has any unobserved parts.
1673
+ */
1674
+ hasUnobservedParts(message) {
1675
+ return this.getUnobservedParts(message).length > 0;
1676
+ }
1677
+ /**
1678
+ * Create a virtual message containing only the unobserved parts.
1679
+ * This is used for token counting and observation.
1680
+ */
1681
+ createUnobservedMessage(message) {
1682
+ const unobservedParts = this.getUnobservedParts(message);
1683
+ if (unobservedParts.length === 0) return null;
1684
+ return {
1685
+ ...message,
1686
+ content: {
1687
+ ...message.content,
1688
+ parts: unobservedParts
1689
+ }
1690
+ };
1691
+ }
1692
+ /**
1693
+ * Get unobserved messages with part-level filtering.
1694
+ *
1695
+ * This method uses data-om-observation-end markers to filter at the part level:
1696
+ * 1. For messages WITH a completed observation: only return parts AFTER the end marker
1697
+ * 2. For messages WITHOUT completed observation: check timestamp against lastObservedAt
1698
+ *
1699
+ * This handles the case where a single message accumulates many parts
1700
+ * (like tool calls) during an agentic loop - we only observe the new parts.
1701
+ */
1702
+ getUnobservedMessages(allMessages, record) {
1703
+ const lastObservedAt = record.lastObservedAt;
1704
+ const observedMessageIds = Array.isArray(record.observedMessageIds) ? new Set(record.observedMessageIds) : void 0;
1705
+ if (!lastObservedAt) {
1706
+ return allMessages;
1707
+ }
1708
+ const result = [];
1709
+ for (const msg of allMessages) {
1710
+ if (observedMessageIds?.has(msg.id)) {
1711
+ continue;
1712
+ }
1713
+ const endMarkerIndex = this.findLastCompletedObservationBoundary(msg);
1714
+ const inProgress = this.hasInProgressObservation(msg);
1715
+ if (inProgress) {
1716
+ result.push(msg);
1717
+ } else if (endMarkerIndex !== -1) {
1718
+ const virtualMsg = this.createUnobservedMessage(msg);
1719
+ if (virtualMsg) {
1720
+ result.push(virtualMsg);
1721
+ }
1722
+ } else {
1723
+ if (!msg.createdAt) {
1724
+ result.push(msg);
1725
+ } else {
1726
+ const msgDate = new Date(msg.createdAt);
1727
+ if (msgDate > lastObservedAt) {
1728
+ result.push(msg);
1729
+ }
1730
+ }
1731
+ }
1732
+ }
1733
+ return result;
1734
+ }
1735
+ /**
1736
+ * Wrapper for observer/reflector agent.generate() calls that checks for abort.
1737
+ * agent.generate() returns an empty result on abort instead of throwing,
1738
+ * so we must check the signal before and after the call.
1739
+ * Retries are handled by Mastra's built-in p-retry at the model execution layer.
1740
+ */
1741
+ async withAbortCheck(fn, abortSignal) {
1742
+ if (abortSignal?.aborted) {
1743
+ throw new Error("The operation was aborted.");
1744
+ }
1745
+ const result = await fn();
1746
+ if (abortSignal?.aborted) {
1747
+ throw new Error("The operation was aborted.");
1748
+ }
1749
+ return result;
1750
+ }
1751
+ /**
1752
+ * Call the Observer agent to extract observations.
1753
+ */
1754
+ async callObserver(existingObservations, messagesToObserve, abortSignal) {
1755
+ const agent = this.getObserverAgent();
1756
+ const prompt = buildObserverPrompt(existingObservations, messagesToObserve);
1757
+ const result = await this.withAbortCheck(
1758
+ () => agent.generate(prompt, {
1759
+ modelSettings: {
1760
+ ...this.observationConfig.modelSettings
1761
+ },
1762
+ providerOptions: this.observationConfig.providerOptions,
1763
+ abortSignal
1764
+ }),
1765
+ abortSignal
1766
+ );
1767
+ const parsed = parseObserverOutput(result.text);
1768
+ const usage = result.totalUsage ?? result.usage;
1769
+ return {
1770
+ observations: parsed.observations,
1771
+ currentTask: parsed.currentTask,
1772
+ suggestedContinuation: parsed.suggestedContinuation,
1773
+ usage: usage ? {
1774
+ inputTokens: usage.inputTokens,
1775
+ outputTokens: usage.outputTokens,
1776
+ totalTokens: usage.totalTokens
1777
+ } : void 0
1778
+ };
1779
+ }
1780
+ /**
1781
+ * Call the Observer agent for multiple threads in a single batched request.
1782
+ * This is more efficient than calling the Observer for each thread individually.
1783
+ * Returns per-thread results with observations, currentTask, and suggestedContinuation,
1784
+ * plus the total usage for the batch.
1785
+ */
1786
+ async callMultiThreadObserver(existingObservations, messagesByThread, threadOrder, abortSignal) {
1787
+ const agent$1 = new agent.Agent({
1788
+ id: "multi-thread-observer",
1789
+ name: "multi-thread-observer",
1790
+ model: this.observationConfig.model,
1791
+ instructions: buildObserverSystemPrompt(true)
1792
+ });
1793
+ const prompt = buildMultiThreadObserverPrompt(existingObservations, messagesByThread, threadOrder);
1794
+ const allMessages = [];
1795
+ for (const msgs of messagesByThread.values()) {
1796
+ allMessages.push(...msgs);
1797
+ }
1798
+ for (const msg of allMessages) {
1799
+ this.observedMessageIds.add(msg.id);
1800
+ }
1801
+ const result = await this.withAbortCheck(
1802
+ () => agent$1.generate(prompt, {
1803
+ modelSettings: {
1804
+ ...this.observationConfig.modelSettings
1805
+ },
1806
+ providerOptions: this.observationConfig.providerOptions,
1807
+ abortSignal
1808
+ }),
1809
+ abortSignal
1810
+ );
1811
+ const parsed = parseMultiThreadObserverOutput(result.text);
1812
+ const results = /* @__PURE__ */ new Map();
1813
+ for (const [threadId, threadResult] of parsed.threads) {
1814
+ results.set(threadId, {
1815
+ observations: threadResult.observations,
1816
+ currentTask: threadResult.currentTask,
1817
+ suggestedContinuation: threadResult.suggestedContinuation
1818
+ });
1819
+ }
1820
+ for (const threadId of threadOrder) {
1821
+ if (!results.has(threadId)) {
1822
+ results.set(threadId, { observations: "" });
1823
+ }
1824
+ }
1825
+ const usage = result.totalUsage ?? result.usage;
1826
+ return {
1827
+ results,
1828
+ usage: usage ? {
1829
+ inputTokens: usage.inputTokens,
1830
+ outputTokens: usage.outputTokens,
1831
+ totalTokens: usage.totalTokens
1832
+ } : void 0
1833
+ };
1834
+ }
1835
+ /**
1836
+ * Call the Reflector agent to condense observations.
1837
+ * Includes compression validation and retry logic.
1838
+ */
1839
+ async callReflector(observations, manualPrompt, streamContext, observationTokensThreshold, abortSignal) {
1840
+ const agent = this.getReflectorAgent();
1841
+ const originalTokens = this.tokenCounter.countObservations(observations);
1842
+ const targetThreshold = observationTokensThreshold ?? this.getMaxThreshold(this.reflectionConfig.observationTokens);
1843
+ let totalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
1844
+ let prompt = buildReflectorPrompt(observations, manualPrompt, false);
1845
+ let result = await this.withAbortCheck(
1846
+ () => agent.generate(prompt, {
1847
+ modelSettings: {
1848
+ ...this.reflectionConfig.modelSettings
1849
+ },
1850
+ providerOptions: this.reflectionConfig.providerOptions,
1851
+ abortSignal
1852
+ }),
1853
+ abortSignal
1854
+ );
1855
+ const firstUsage = result.totalUsage ?? result.usage;
1856
+ if (firstUsage) {
1857
+ totalUsage.inputTokens += firstUsage.inputTokens ?? 0;
1858
+ totalUsage.outputTokens += firstUsage.outputTokens ?? 0;
1859
+ totalUsage.totalTokens += firstUsage.totalTokens ?? 0;
1860
+ }
1861
+ let parsed = parseReflectorOutput(result.text);
1862
+ let reflectedTokens = this.tokenCounter.countObservations(parsed.observations);
1863
+ if (!validateCompression(reflectedTokens, targetThreshold)) {
1864
+ if (streamContext?.writer) {
1865
+ const failedMarker = this.createObservationFailedMarker({
1866
+ cycleId: streamContext.cycleId,
1867
+ operationType: "reflection",
1868
+ startedAt: streamContext.startedAt,
1869
+ tokensAttempted: originalTokens,
1870
+ error: `Did not compress below threshold (${originalTokens} \u2192 ${reflectedTokens}, target: ${targetThreshold}), retrying with compression guidance`,
1871
+ recordId: streamContext.recordId,
1872
+ threadId: streamContext.threadId
1873
+ });
1874
+ await streamContext.writer.custom(failedMarker).catch(() => {
1875
+ });
1876
+ const retryCycleId = crypto.randomUUID();
1877
+ streamContext.cycleId = retryCycleId;
1878
+ const startMarker = this.createObservationStartMarker({
1879
+ cycleId: retryCycleId,
1880
+ operationType: "reflection",
1881
+ tokensToObserve: originalTokens,
1882
+ recordId: streamContext.recordId,
1883
+ threadId: streamContext.threadId,
1884
+ threadIds: [streamContext.threadId]
1885
+ });
1886
+ streamContext.startedAt = startMarker.data.startedAt;
1887
+ await streamContext.writer.custom(startMarker).catch(() => {
1888
+ });
1889
+ }
1890
+ prompt = buildReflectorPrompt(observations, manualPrompt, true);
1891
+ result = await this.withAbortCheck(
1892
+ () => agent.generate(prompt, {
1893
+ modelSettings: {
1894
+ ...this.reflectionConfig.modelSettings
1895
+ },
1896
+ providerOptions: this.reflectionConfig.providerOptions,
1897
+ abortSignal
1898
+ }),
1899
+ abortSignal
1900
+ );
1901
+ const retryUsage = result.totalUsage ?? result.usage;
1902
+ if (retryUsage) {
1903
+ totalUsage.inputTokens += retryUsage.inputTokens ?? 0;
1904
+ totalUsage.outputTokens += retryUsage.outputTokens ?? 0;
1905
+ totalUsage.totalTokens += retryUsage.totalTokens ?? 0;
1906
+ }
1907
+ parsed = parseReflectorOutput(result.text);
1908
+ reflectedTokens = this.tokenCounter.countObservations(parsed.observations);
1909
+ }
1910
+ return {
1911
+ observations: parsed.observations,
1912
+ suggestedContinuation: parsed.suggestedContinuation,
1913
+ usage: totalUsage.totalTokens > 0 ? totalUsage : void 0
1914
+ };
1915
+ }
1916
+ /**
1917
+ * Format observations for injection into context.
1918
+ * Applies token optimization before presenting to the Actor.
1919
+ *
1920
+ * In resource scope mode, filters continuity messages to only show
1921
+ * the message for the current thread.
1922
+ */
1923
+ /**
1924
+ * Format observations for injection into the Actor's context.
1925
+ * @param observations - The observations to inject
1926
+ * @param suggestedResponse - Thread-specific suggested response (from thread metadata)
1927
+ * @param unobservedContextBlocks - Formatted <unobserved-context> blocks from other threads
1928
+ */
1929
+ formatObservationsForContext(observations, currentTask, suggestedResponse, unobservedContextBlocks, currentDate) {
1930
+ let optimized = optimizeObservationsForContext(observations);
1931
+ if (currentDate) {
1932
+ optimized = addRelativeTimeToObservations(optimized, currentDate);
1933
+ }
1934
+ let content = `
1935
+ The following observations block contains your memory of past conversations with this user.
1936
+
1937
+ <observations>
1938
+ ${optimized}
1939
+ </observations>
1940
+
1941
+ IMPORTANT: When responding, reference specific details from these observations. Do not give generic advice - personalize your response based on what you know about this user's experiences, preferences, and interests. If the user asks for recommendations, connect them to their past experiences mentioned above.
1942
+
1943
+ KNOWLEDGE UPDATES: When asked about current state (e.g., "where do I currently...", "what is my current..."), always prefer the MOST RECENT information. Observations include dates - if you see conflicting information, the newer observation supersedes the older one. Look for phrases like "will start", "is switching", "changed to", "moved to" as indicators that previous information has been updated.
1944
+
1945
+ PLANNED ACTIONS: If the user stated they planned to do something (e.g., "I'm going to...", "I'm looking forward to...", "I will...") and the date they planned to do it is now in the past (check the relative time like "3 weeks ago"), assume they completed the action unless there's evidence they didn't. For example, if someone said "I'll start my new diet on Monday" and that was 2 weeks ago, assume they started the diet.`;
1946
+ if (unobservedContextBlocks) {
1947
+ content += `
1948
+
1949
+ The following content is from OTHER conversations different from the current conversation, they're here for reference, but they're not necessarily your focus:
1950
+ START_OTHER_CONVERSATIONS_BLOCK
1951
+ ${unobservedContextBlocks}
1952
+ END_OTHER_CONVERSATIONS_BLOCK`;
1953
+ }
1954
+ if (currentTask) {
1955
+ content += `
1956
+
1957
+ <current-task>
1958
+ ${currentTask}
1959
+ </current-task>`;
1960
+ }
1961
+ if (suggestedResponse) {
1962
+ content += `
1963
+
1964
+ <suggested-response>
1965
+ ${suggestedResponse}
1966
+ </suggested-response>
1967
+ `;
1968
+ }
1969
+ return content;
1970
+ }
1971
+ /**
1972
+ * Get threadId and resourceId from either RequestContext or MessageList
1973
+ */
1974
+ getThreadContext(requestContext, messageList) {
1975
+ const memoryContext = requestContext?.get("MastraMemory");
1976
+ if (memoryContext?.thread?.id) {
1977
+ return {
1978
+ threadId: memoryContext.thread.id,
1979
+ resourceId: memoryContext.resourceId
1980
+ };
1981
+ }
1982
+ const serialized = messageList.serialize();
1983
+ if (serialized.memoryInfo?.threadId) {
1984
+ return {
1985
+ threadId: serialized.memoryInfo.threadId,
1986
+ resourceId: serialized.memoryInfo.resourceId
1987
+ };
1988
+ }
1989
+ return null;
1990
+ }
1991
+ /**
1992
+ * Process input at each step - check threshold, observe if needed, save, inject observations.
1993
+ * This is the ONLY processor method - all OM logic happens here.
1994
+ *
1995
+ * Flow:
1996
+ * 1. Load historical messages (step 0 only)
1997
+ * 2. Check if observation threshold is reached
1998
+ * 3. If threshold reached: observe, save messages with markers
1999
+ * 4. Inject observations into context
2000
+ * 5. Filter out already-observed messages
2001
+ */
2002
+ async processInputStep(args) {
2003
+ const { messageList, requestContext, stepNumber, state: _state, writer, abortSignal, abort } = args;
2004
+ const state = _state ?? {};
2005
+ const context = this.getThreadContext(requestContext, messageList);
2006
+ if (!context) {
2007
+ return messageList;
2008
+ }
2009
+ const { threadId, resourceId } = context;
2010
+ const memoryContext = memory.parseMemoryRequestContext(requestContext);
2011
+ const readOnly = memoryContext?.memoryConfig?.readOnly;
2012
+ let record = await this.getOrCreateRecord(threadId, resourceId);
2013
+ if (!state.initialSetupDone) {
2014
+ state.initialSetupDone = true;
2015
+ const lastObservedAt = record.lastObservedAt;
2016
+ if (this.scope === "resource" && resourceId) {
2017
+ const currentThreadMessages = await this.loadUnobservedMessages(threadId, void 0, lastObservedAt);
2018
+ for (const msg of currentThreadMessages) {
2019
+ if (msg.role !== "system") {
2020
+ if (!this.hasUnobservedParts(msg) && this.findLastCompletedObservationBoundary(msg) !== -1) {
2021
+ continue;
2022
+ }
2023
+ messageList.add(msg, "memory");
2024
+ }
2025
+ }
2026
+ } else {
2027
+ const historicalMessages = await this.loadUnobservedMessages(threadId, resourceId, lastObservedAt);
2028
+ if (historicalMessages.length > 0) {
2029
+ for (const msg of historicalMessages) {
2030
+ if (msg.role !== "system") {
2031
+ if (!this.hasUnobservedParts(msg) && this.findLastCompletedObservationBoundary(msg) !== -1) {
2032
+ continue;
2033
+ }
2034
+ messageList.add(msg, "memory");
2035
+ }
2036
+ }
2037
+ }
2038
+ }
2039
+ }
2040
+ let unobservedContextBlocks;
2041
+ if (this.scope === "resource" && resourceId) {
2042
+ unobservedContextBlocks = await this.loadOtherThreadsContext(resourceId, threadId);
2043
+ }
2044
+ if (!readOnly) {
2045
+ const allMessages = messageList.get.all.db();
2046
+ const unobservedMessages = this.getUnobservedMessages(allMessages, record);
2047
+ const currentSessionTokens = this.tokenCounter.countMessages(unobservedMessages);
2048
+ const otherThreadTokens = unobservedContextBlocks ? this.tokenCounter.countString(unobservedContextBlocks) : 0;
2049
+ const currentObservationTokens = record.observationTokenCount ?? 0;
2050
+ const pendingTokens = record.pendingMessageTokens ?? 0;
2051
+ const totalPendingTokens = pendingTokens + currentSessionTokens + otherThreadTokens;
2052
+ const threshold = this.calculateDynamicThreshold(this.observationConfig.messageTokens, currentObservationTokens);
2053
+ const baseReflectionThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
2054
+ const isSharedBudget = typeof this.observationConfig.messageTokens !== "number";
2055
+ const totalBudget = isSharedBudget ? this.observationConfig.messageTokens.max : 0;
2056
+ const effectiveObservationTokensThreshold = isSharedBudget ? Math.max(totalBudget - threshold, 1e3) : baseReflectionThreshold;
2057
+ const observationTokensPercent = Math.round(
2058
+ currentObservationTokens / effectiveObservationTokensThreshold * 100
2059
+ );
2060
+ this.emitDebugEvent({
2061
+ type: "step_progress",
2062
+ timestamp: /* @__PURE__ */ new Date(),
2063
+ threadId,
2064
+ resourceId: resourceId ?? "",
2065
+ stepNumber,
2066
+ finishReason: "unknown",
2067
+ pendingTokens: totalPendingTokens,
2068
+ threshold,
2069
+ thresholdPercent: Math.round(totalPendingTokens / threshold * 100),
2070
+ willSave: totalPendingTokens >= threshold,
2071
+ willObserve: totalPendingTokens >= threshold
2072
+ });
2073
+ if (writer) {
2074
+ const progressPart = {
2075
+ type: "data-om-progress",
2076
+ data: {
2077
+ pendingTokens: totalPendingTokens,
2078
+ messageTokens: threshold,
2079
+ messageTokensPercent: Math.round(totalPendingTokens / threshold * 100),
2080
+ observationTokens: currentObservationTokens,
2081
+ observationTokensThreshold: effectiveObservationTokensThreshold,
2082
+ observationTokensPercent,
2083
+ willObserve: totalPendingTokens >= threshold,
2084
+ recordId: record.id,
2085
+ threadId,
2086
+ stepNumber
2087
+ }
2088
+ };
2089
+ await writer.custom(progressPart).catch(() => {
2090
+ });
2091
+ }
2092
+ const sealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
2093
+ if (stepNumber > 0 && totalPendingTokens >= threshold) {
2094
+ const lockKey = this.getLockKey(threadId, resourceId);
2095
+ let observationSucceeded = false;
2096
+ await this.withLock(lockKey, async () => {
2097
+ const freshRecord = await this.getOrCreateRecord(threadId, resourceId);
2098
+ const freshAllMessages = messageList.get.all.db();
2099
+ const freshUnobservedMessages = this.getUnobservedMessages(freshAllMessages, freshRecord);
2100
+ const freshCurrentTokens = this.tokenCounter.countMessages(freshUnobservedMessages);
2101
+ const freshPending = freshRecord.pendingMessageTokens ?? 0;
2102
+ let freshOtherThreadTokens = 0;
2103
+ if (this.scope === "resource" && resourceId) {
2104
+ const freshOtherContext = await this.loadOtherThreadsContext(resourceId, threadId);
2105
+ freshOtherThreadTokens = freshOtherContext ? this.tokenCounter.countString(freshOtherContext) : 0;
2106
+ }
2107
+ const freshTotal = freshPending + freshCurrentTokens + freshOtherThreadTokens;
2108
+ if (freshTotal < threshold) {
2109
+ return;
2110
+ }
2111
+ const preObservationTime = freshRecord.lastObservedAt?.getTime() ?? 0;
2112
+ if (freshUnobservedMessages.length > 0) {
2113
+ try {
2114
+ if (this.scope === "resource" && resourceId) {
2115
+ await this.doResourceScopedObservation(
2116
+ freshRecord,
2117
+ threadId,
2118
+ resourceId,
2119
+ freshUnobservedMessages,
2120
+ writer,
2121
+ abortSignal
2122
+ );
2123
+ } else {
2124
+ await this.doSynchronousObservation(
2125
+ freshRecord,
2126
+ threadId,
2127
+ freshUnobservedMessages,
2128
+ writer,
2129
+ abortSignal
2130
+ );
2131
+ }
2132
+ const updatedRecord = await this.getOrCreateRecord(threadId, resourceId);
2133
+ const updatedTime = updatedRecord.lastObservedAt?.getTime() ?? 0;
2134
+ observationSucceeded = updatedTime > preObservationTime;
2135
+ } catch (error) {
2136
+ if (abortSignal?.aborted) {
2137
+ abort("Agent execution was aborted");
2138
+ } else {
2139
+ abort(
2140
+ `Encountered error during memory observation ${error instanceof Error ? error.message : JSON.stringify(error, null, 2)}`
2141
+ );
2142
+ }
2143
+ observationSucceeded = false;
2144
+ }
2145
+ }
2146
+ });
2147
+ if (observationSucceeded) {
2148
+ const allMsgs = messageList.get.all.db();
2149
+ let markerIdx = -1;
2150
+ let markerMsg = null;
2151
+ for (let i = allMsgs.length - 1; i >= 0; i--) {
2152
+ const msg = allMsgs[i];
2153
+ if (!msg) continue;
2154
+ if (this.findLastCompletedObservationBoundary(msg) !== -1) {
2155
+ markerIdx = i;
2156
+ markerMsg = msg;
2157
+ break;
2158
+ }
2159
+ }
2160
+ if (markerMsg && markerIdx !== -1) {
2161
+ const idsToRemove = [];
2162
+ const messagesToSave = [];
2163
+ for (let i = 0; i < markerIdx; i++) {
2164
+ const msg = allMsgs[i];
2165
+ if (msg?.id && msg.id !== "om-continuation") {
2166
+ idsToRemove.push(msg.id);
2167
+ messagesToSave.push(msg);
2168
+ }
2169
+ }
2170
+ messagesToSave.push(markerMsg);
2171
+ const unobservedParts = this.getUnobservedParts(markerMsg);
2172
+ if (unobservedParts.length === 0) {
2173
+ if (markerMsg.id) {
2174
+ idsToRemove.push(markerMsg.id);
2175
+ }
2176
+ } else if (unobservedParts.length < (markerMsg.content?.parts?.length ?? 0)) {
2177
+ markerMsg.content.parts = unobservedParts;
2178
+ }
2179
+ if (messagesToSave.length > 0) {
2180
+ await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2181
+ }
2182
+ if (idsToRemove.length > 0) {
2183
+ messageList.removeByIds(idsToRemove);
2184
+ }
2185
+ } else {
2186
+ const newInput = messageList.clear.input.db();
2187
+ const newOutput = messageList.clear.response.db();
2188
+ const messagesToSave = [...newInput, ...newOutput];
2189
+ if (messagesToSave.length > 0) {
2190
+ await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2191
+ }
2192
+ }
2193
+ messageList.clear.input.db();
2194
+ messageList.clear.response.db();
2195
+ }
2196
+ record = await this.getOrCreateRecord(threadId, resourceId);
2197
+ } else if (stepNumber > 0) {
2198
+ const newInput = messageList.clear.input.db();
2199
+ const newOutput = messageList.clear.response.db();
2200
+ const messagesToSave = [...newInput, ...newOutput];
2201
+ if (messagesToSave.length > 0) {
2202
+ await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2203
+ for (const msg of messagesToSave) {
2204
+ messageList.add(msg, "memory");
2205
+ }
2206
+ }
2207
+ }
2208
+ }
2209
+ const thread = await this.storage.getThreadById({ threadId });
2210
+ const threadOMMetadata = memory.getThreadOMMetadata(thread?.metadata);
2211
+ const currentTask = threadOMMetadata?.currentTask;
2212
+ const suggestedResponse = threadOMMetadata?.suggestedResponse;
2213
+ const currentDate = requestContext?.get("currentDate") ?? /* @__PURE__ */ new Date();
2214
+ if (record.activeObservations) {
2215
+ const observationSystemMessage = this.formatObservationsForContext(
2216
+ record.activeObservations,
2217
+ currentTask,
2218
+ suggestedResponse,
2219
+ unobservedContextBlocks,
2220
+ currentDate
2221
+ );
2222
+ messageList.clearSystemMessages("observational-memory");
2223
+ messageList.addSystem(observationSystemMessage, "observational-memory");
2224
+ const continuationMessage = {
2225
+ id: `om-continuation`,
2226
+ role: "user",
2227
+ createdAt: /* @__PURE__ */ new Date(0),
2228
+ content: {
2229
+ format: 2,
2230
+ parts: [
2231
+ {
2232
+ type: "text",
2233
+ text: `<system-reminder>This message is not from the user, the conversation history grew too long and wouldn't fit in context! Thankfully the entire conversation is stored in your memory observations. Please continue from where the observations left off. Do not refer to your "memory observations" directly, the user doesn't know about them, they are your memories! Just respond naturally as if you're remembering the conversation (you are!). Do not say "Hi there!" or "based on our previous conversation" as if the conversation is just starting, this is not a new conversation. This is an ongoing conversation, keep continuity by responding based on your memory. For example do not say "I understand. I've reviewed my memory observations", or "I remember [...]". Answer naturally following the suggestion from your memory. Note that your memory may contain a suggested first response, which you should follow.
2234
+
2235
+ IMPORTANT: this system reminder is NOT from the user. The system placed it here as part of your memory system. This message is part of you remembering your conversation with the user.
2236
+
2237
+ NOTE: Any messages following this system reminder are newer than your memories.
2238
+ </system-reminder>`
2239
+ }
2240
+ ]
2241
+ },
2242
+ threadId,
2243
+ resourceId
2244
+ };
2245
+ messageList.add(continuationMessage, "memory");
2246
+ }
2247
+ if (stepNumber === 0) {
2248
+ const allMessages = messageList.get.all.db();
2249
+ let markerMessageIndex = -1;
2250
+ let markerMessage = null;
2251
+ for (let i = allMessages.length - 1; i >= 0; i--) {
2252
+ const msg = allMessages[i];
2253
+ if (!msg) continue;
2254
+ if (this.findLastCompletedObservationBoundary(msg) !== -1) {
2255
+ markerMessageIndex = i;
2256
+ markerMessage = msg;
2257
+ break;
2258
+ }
2259
+ }
2260
+ if (markerMessage && markerMessageIndex !== -1) {
2261
+ const messagesToRemove = [];
2262
+ for (let i = 0; i < markerMessageIndex; i++) {
2263
+ const msg = allMessages[i];
2264
+ if (msg?.id && msg.id !== "om-continuation") {
2265
+ messagesToRemove.push(msg.id);
2266
+ }
2267
+ }
2268
+ if (messagesToRemove.length > 0) {
2269
+ messageList.removeByIds(messagesToRemove);
2270
+ }
2271
+ const unobservedParts = this.getUnobservedParts(markerMessage);
2272
+ if (unobservedParts.length === 0) {
2273
+ if (markerMessage.id) {
2274
+ messageList.removeByIds([markerMessage.id]);
2275
+ }
2276
+ } else if (unobservedParts.length < (markerMessage.content?.parts?.length ?? 0)) {
2277
+ markerMessage.content.parts = unobservedParts;
2278
+ }
2279
+ }
2280
+ }
2281
+ return messageList;
2282
+ }
2283
+ /**
2284
+ * Save any unsaved messages at the end of the agent turn.
2285
+ *
2286
+ * This is the "final save" that catches messages that processInputStep didn't save
2287
+ * (e.g., when the observation threshold was never reached, or on single-step execution).
2288
+ * Without this, messages would be lost because MessageHistory is disabled when OM is active.
2289
+ */
2290
+ async processOutputResult(args) {
2291
+ const { messageList, requestContext, state: _state } = args;
2292
+ const state = _state ?? {};
2293
+ const context = this.getThreadContext(requestContext, messageList);
2294
+ if (!context) {
2295
+ return messageList;
2296
+ }
2297
+ const { threadId, resourceId } = context;
2298
+ const memoryContext = memory.parseMemoryRequestContext(requestContext);
2299
+ const readOnly = memoryContext?.memoryConfig?.readOnly;
2300
+ if (readOnly) {
2301
+ return messageList;
2302
+ }
2303
+ const newInput = messageList.get.input.db();
2304
+ const newOutput = messageList.get.response.db();
2305
+ const messagesToSave = [...newInput, ...newOutput];
2306
+ if (messagesToSave.length === 0) {
2307
+ return messageList;
2308
+ }
2309
+ const sealedIds = state.sealedIds ?? /* @__PURE__ */ new Set();
2310
+ await this.saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state);
2311
+ return messageList;
2312
+ }
2313
+ /**
2314
+ * Save messages to storage, regenerating IDs for any messages that were
2315
+ * previously saved with observation markers (sealed).
2316
+ *
2317
+ * After saving, tracks which messages now have observation markers
2318
+ * so their IDs won't be reused in future save cycles.
2319
+ */
2320
+ async saveMessagesWithSealedIdTracking(messagesToSave, sealedIds, threadId, resourceId, state) {
2321
+ for (const msg of messagesToSave) {
2322
+ if (sealedIds.has(msg.id)) {
2323
+ msg.id = crypto.randomUUID();
2324
+ }
2325
+ }
2326
+ await this.messageHistory.persistMessages({
2327
+ messages: messagesToSave,
2328
+ threadId,
2329
+ resourceId
2330
+ });
2331
+ for (const msg of messagesToSave) {
2332
+ if (this.findLastCompletedObservationBoundary(msg) !== -1) {
2333
+ sealedIds.add(msg.id);
2334
+ }
2335
+ }
2336
+ state.sealedIds = sealedIds;
2337
+ }
2338
+ /**
2339
+ * Load messages from storage that haven't been observed yet.
2340
+ * Uses cursor-based query with lastObservedAt timestamp for efficiency.
2341
+ *
2342
+ * In resource scope mode, loads messages for the entire resource (all threads).
2343
+ * In thread scope mode, loads messages for just the current thread.
2344
+ */
2345
+ async loadUnobservedMessages(threadId, resourceId, lastObservedAt) {
2346
+ const startDate = lastObservedAt ? new Date(lastObservedAt.getTime() + 1) : void 0;
2347
+ let result;
2348
+ if (this.scope === "resource" && resourceId) {
2349
+ result = await this.storage.listMessagesByResourceId({
2350
+ resourceId,
2351
+ perPage: false,
2352
+ // Get all messages (no pagination limit)
2353
+ orderBy: { field: "createdAt", direction: "ASC" },
2354
+ filter: startDate ? {
2355
+ dateRange: {
2356
+ start: startDate
2357
+ }
2358
+ } : void 0
2359
+ });
2360
+ } else {
2361
+ result = await this.storage.listMessages({
2362
+ threadId,
2363
+ perPage: false,
2364
+ // Get all messages (no pagination limit)
2365
+ orderBy: { field: "createdAt", direction: "ASC" },
2366
+ filter: startDate ? {
2367
+ dateRange: {
2368
+ start: startDate
2369
+ }
2370
+ } : void 0
2371
+ });
2372
+ }
2373
+ return result.messages;
2374
+ }
2375
+ /**
2376
+ * Load unobserved messages from other threads (not the current thread) for a resource.
2377
+ * Called fresh each step so it reflects the latest lastObservedAt cursors
2378
+ * after observations complete.
2379
+ */
2380
+ async loadOtherThreadsContext(resourceId, currentThreadId) {
2381
+ const { threads: allThreads } = await this.storage.listThreads({ filter: { resourceId } });
2382
+ const messagesByThread = /* @__PURE__ */ new Map();
2383
+ for (const thread of allThreads) {
2384
+ if (thread.id === currentThreadId) continue;
2385
+ const omMetadata = memory.getThreadOMMetadata(thread.metadata);
2386
+ const threadLastObservedAt = omMetadata?.lastObservedAt;
2387
+ const startDate = threadLastObservedAt ? new Date(new Date(threadLastObservedAt).getTime() + 1) : void 0;
2388
+ const result = await this.storage.listMessages({
2389
+ threadId: thread.id,
2390
+ perPage: false,
2391
+ orderBy: { field: "createdAt", direction: "ASC" },
2392
+ filter: startDate ? { dateRange: { start: startDate } } : void 0
2393
+ });
2394
+ const filtered = result.messages.filter((m) => !this.observedMessageIds.has(m.id));
2395
+ if (filtered.length > 0) {
2396
+ messagesByThread.set(thread.id, filtered);
2397
+ }
2398
+ }
2399
+ if (messagesByThread.size === 0) return void 0;
2400
+ const blocks = await this.formatUnobservedContextBlocks(messagesByThread, currentThreadId);
2401
+ return blocks || void 0;
2402
+ }
2403
+ /**
2404
+ * Format unobserved messages from other threads as <unobserved-context> blocks.
2405
+ * These are injected into the Actor's context so it has awareness of activity
2406
+ * in other threads for the same resource.
2407
+ */
2408
+ async formatUnobservedContextBlocks(messagesByThread, currentThreadId) {
2409
+ const blocks = [];
2410
+ for (const [threadId, messages] of messagesByThread) {
2411
+ if (threadId === currentThreadId) continue;
2412
+ if (messages.length === 0) continue;
2413
+ const formattedMessages = formatMessagesForObserver(messages, { maxPartLength: 500 });
2414
+ if (formattedMessages) {
2415
+ const obscuredId = await this.representThreadIDInContext(threadId);
2416
+ blocks.push(`<other-conversation id="${obscuredId}">
2417
+ ${formattedMessages}
2418
+ </other-conversation>`);
2419
+ }
2420
+ }
2421
+ return blocks.join("\n\n");
2422
+ }
2423
+ async representThreadIDInContext(threadId) {
2424
+ if (this.shouldObscureThreadIds) {
2425
+ const cached = this.threadIdCache.get(threadId);
2426
+ if (cached) return cached;
2427
+ const hasher = await this.hasher;
2428
+ const hashed = hasher.h32ToString(threadId);
2429
+ this.threadIdCache.set(threadId, hashed);
2430
+ return hashed;
2431
+ }
2432
+ return threadId;
2433
+ }
2434
+ /**
2435
+ * Strip any thread tags that the Observer might have added.
2436
+ * Thread attribution is handled externally by the system, not by the Observer.
2437
+ * This is a defense-in-depth measure.
2438
+ */
2439
+ stripThreadTags(observations) {
2440
+ return observations.replace(/<thread[^>]*>|<\/thread>/gi, "").trim();
2441
+ }
2442
+ /**
2443
+ * Get the maximum createdAt timestamp from a list of messages.
2444
+ * Used to set lastObservedAt to the most recent message timestamp instead of current time.
2445
+ * This ensures historical data (like LongMemEval fixtures) works correctly.
2446
+ */
2447
+ getMaxMessageTimestamp(messages) {
2448
+ let maxTime = 0;
2449
+ for (const msg of messages) {
2450
+ if (msg.createdAt) {
2451
+ const msgTime = new Date(msg.createdAt).getTime();
2452
+ if (msgTime > maxTime) {
2453
+ maxTime = msgTime;
2454
+ }
2455
+ }
2456
+ }
2457
+ return maxTime > 0 ? new Date(maxTime) : /* @__PURE__ */ new Date();
2458
+ }
2459
+ /**
2460
+ * Wrap observations in a thread attribution tag.
2461
+ * Used in resource scope to track which thread observations came from.
2462
+ */
2463
+ async wrapWithThreadTag(threadId, observations) {
2464
+ const cleanObservations = this.stripThreadTags(observations);
2465
+ const obscuredId = await this.representThreadIDInContext(threadId);
2466
+ return `<thread id="${obscuredId}">
2467
+ ${cleanObservations}
2468
+ </thread>`;
2469
+ }
2470
+ /**
2471
+ * Append or merge new thread sections.
2472
+ * If the new section has the same thread ID and date as an existing section,
2473
+ * merge the observations into that section to reduce token usage.
2474
+ * Otherwise, append as a new section.
2475
+ */
2476
+ replaceOrAppendThreadSection(existingObservations, _threadId, newThreadSection) {
2477
+ if (!existingObservations) {
2478
+ return newThreadSection;
2479
+ }
2480
+ const threadIdMatch = newThreadSection.match(/<thread id="([^"]+)">/);
2481
+ const dateMatch = newThreadSection.match(/Date:\s*([A-Za-z]+\s+\d+,\s+\d+)/);
2482
+ if (!threadIdMatch || !dateMatch) {
2483
+ return `${existingObservations}
2484
+
2485
+ ${newThreadSection}`;
2486
+ }
2487
+ const newThreadId = threadIdMatch[1];
2488
+ const newDate = dateMatch[1];
2489
+ const existingPattern = new RegExp(
2490
+ `<thread id="${newThreadId}">\\s*Date:\\s*${newDate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}([\\s\\S]*?)</thread>`
2491
+ );
2492
+ const existingMatch = existingObservations.match(existingPattern);
2493
+ if (existingMatch) {
2494
+ const newObsMatch = newThreadSection.match(/<thread id="[^"]+">[\s\S]*?Date:[^\n]*\n([\s\S]*?)\n<\/thread>/);
2495
+ if (newObsMatch && newObsMatch[1]) {
2496
+ const newObsContent = newObsMatch[1].trim();
2497
+ const mergedSection = existingObservations.replace(existingPattern, (match) => {
2498
+ const withoutClose = match.replace(/<\/thread>$/, "").trimEnd();
2499
+ return `${withoutClose}
2500
+ ${newObsContent}
2501
+ </thread>`;
2502
+ });
2503
+ return mergedSection;
2504
+ }
2505
+ }
2506
+ return `${existingObservations}
2507
+
2508
+ ${newThreadSection}`;
2509
+ }
2510
+ /**
2511
+ * Sort threads by their oldest unobserved message.
2512
+ * Returns thread IDs in order from oldest to most recent.
2513
+ * This ensures no thread's messages get "stuck" unobserved.
2514
+ */
2515
+ sortThreadsByOldestMessage(messagesByThread) {
2516
+ const threadOrder = Array.from(messagesByThread.entries()).map(([threadId, messages]) => {
2517
+ const oldestTimestamp = Math.min(
2518
+ ...messages.map((m) => m.createdAt ? new Date(m.createdAt).getTime() : Date.now())
2519
+ );
2520
+ return { threadId, oldestTimestamp };
2521
+ }).sort((a, b) => a.oldestTimestamp - b.oldestTimestamp);
2522
+ return threadOrder.map((t) => t.threadId);
2523
+ }
2524
+ /**
2525
+ * Do synchronous observation (fallback when no buffering)
2526
+ */
2527
+ async doSynchronousObservation(record, threadId, unobservedMessages, writer, abortSignal) {
2528
+ this.emitDebugEvent({
2529
+ type: "observation_triggered",
2530
+ timestamp: /* @__PURE__ */ new Date(),
2531
+ threadId,
2532
+ resourceId: record.resourceId ?? "",
2533
+ previousObservations: record.activeObservations,
2534
+ messages: unobservedMessages.map((m) => ({
2535
+ role: m.role,
2536
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
2537
+ }))
2538
+ });
2539
+ await this.storage.setObservingFlag(record.id, true);
2540
+ const cycleId = crypto.randomUUID();
2541
+ const tokensToObserve = this.tokenCounter.countMessages(unobservedMessages);
2542
+ const lastMessage = unobservedMessages[unobservedMessages.length - 1];
2543
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2544
+ if (lastMessage?.id) {
2545
+ const startMarker = this.createObservationStartMarker({
2546
+ cycleId,
2547
+ operationType: "observation",
2548
+ tokensToObserve,
2549
+ recordId: record.id,
2550
+ threadId,
2551
+ threadIds: [threadId]
2552
+ });
2553
+ if (writer) {
2554
+ await writer.custom(startMarker).catch(() => {
2555
+ });
2556
+ }
2557
+ }
2558
+ try {
2559
+ const freshRecord = await this.storage.getObservationalMemory(record.threadId, record.resourceId);
2560
+ if (freshRecord && freshRecord.lastObservedAt && record.lastObservedAt) {
2561
+ if (freshRecord.lastObservedAt > record.lastObservedAt) {
2562
+ return;
2563
+ }
2564
+ }
2565
+ const result = await this.callObserver(
2566
+ freshRecord?.activeObservations ?? record.activeObservations,
2567
+ unobservedMessages,
2568
+ abortSignal
2569
+ );
2570
+ const existingObservations = freshRecord?.activeObservations ?? record.activeObservations ?? "";
2571
+ let newObservations;
2572
+ if (this.scope === "resource") {
2573
+ const threadSection = await this.wrapWithThreadTag(threadId, result.observations);
2574
+ newObservations = this.replaceOrAppendThreadSection(existingObservations, threadId, threadSection);
2575
+ } else {
2576
+ newObservations = existingObservations ? `${existingObservations}
2577
+
2578
+ ${result.observations}` : result.observations;
2579
+ }
2580
+ let totalTokenCount = this.tokenCounter.countObservations(newObservations);
2581
+ const cycleObservationTokens = this.tokenCounter.countObservations(result.observations);
2582
+ const lastObservedAt = this.getMaxMessageTimestamp(unobservedMessages);
2583
+ const newMessageIds = unobservedMessages.map((m) => m.id);
2584
+ const existingIds = freshRecord?.observedMessageIds ?? record.observedMessageIds ?? [];
2585
+ const allObservedIds = [.../* @__PURE__ */ new Set([...Array.isArray(existingIds) ? existingIds : [], ...newMessageIds])];
2586
+ await this.storage.updateActiveObservations({
2587
+ id: record.id,
2588
+ observations: newObservations,
2589
+ tokenCount: totalTokenCount,
2590
+ lastObservedAt,
2591
+ observedMessageIds: allObservedIds
2592
+ });
2593
+ if (result.suggestedContinuation || result.currentTask) {
2594
+ const thread = await this.storage.getThreadById({ threadId });
2595
+ if (thread) {
2596
+ const newMetadata = memory.setThreadOMMetadata(thread.metadata, {
2597
+ suggestedResponse: result.suggestedContinuation,
2598
+ currentTask: result.currentTask
2599
+ });
2600
+ await this.storage.updateThread({
2601
+ id: threadId,
2602
+ title: thread.title ?? "",
2603
+ metadata: newMetadata
2604
+ });
2605
+ }
2606
+ }
2607
+ if (lastMessage?.id) {
2608
+ const endMarker = this.createObservationEndMarker({
2609
+ cycleId,
2610
+ operationType: "observation",
2611
+ startedAt,
2612
+ tokensObserved: tokensToObserve,
2613
+ observationTokens: cycleObservationTokens,
2614
+ observations: result.observations,
2615
+ currentTask: result.currentTask,
2616
+ suggestedResponse: result.suggestedContinuation,
2617
+ recordId: record.id,
2618
+ threadId
2619
+ });
2620
+ if (writer) {
2621
+ await writer.custom(endMarker).catch(() => {
2622
+ });
2623
+ }
2624
+ }
2625
+ this.emitDebugEvent({
2626
+ type: "observation_complete",
2627
+ timestamp: /* @__PURE__ */ new Date(),
2628
+ threadId,
2629
+ resourceId: record.resourceId ?? "",
2630
+ observations: newObservations,
2631
+ rawObserverOutput: result.observations,
2632
+ previousObservations: record.activeObservations,
2633
+ messages: unobservedMessages.map((m) => ({
2634
+ role: m.role,
2635
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
2636
+ })),
2637
+ usage: result.usage
2638
+ });
2639
+ await this.maybeReflect(
2640
+ { ...record, activeObservations: newObservations },
2641
+ totalTokenCount,
2642
+ threadId,
2643
+ writer,
2644
+ abortSignal
2645
+ );
2646
+ } catch (error) {
2647
+ if (lastMessage?.id) {
2648
+ const failedMarker = this.createObservationFailedMarker({
2649
+ cycleId,
2650
+ operationType: "observation",
2651
+ startedAt,
2652
+ tokensAttempted: tokensToObserve,
2653
+ error: error instanceof Error ? error.message : String(error),
2654
+ recordId: record.id,
2655
+ threadId
2656
+ });
2657
+ if (writer) {
2658
+ await writer.custom(failedMarker).catch(() => {
2659
+ });
2660
+ }
2661
+ }
2662
+ if (abortSignal?.aborted) {
2663
+ throw error;
2664
+ }
2665
+ console.error(`[OM] Observation failed:`, error instanceof Error ? error.message : String(error));
2666
+ } finally {
2667
+ await this.storage.setObservingFlag(record.id, false);
2668
+ }
2669
+ }
2670
+ /**
2671
+ * Resource-scoped observation: observe ALL threads with unobserved messages.
2672
+ * Threads are observed in oldest-first order to ensure no thread's messages
2673
+ * get "stuck" unobserved forever.
2674
+ *
2675
+ * Key differences from thread-scoped observation:
2676
+ * 1. Loads messages from ALL threads for the resource
2677
+ * 2. Observes threads one-by-one in oldest-first order
2678
+ * 3. Only updates lastObservedAt AFTER all threads are observed
2679
+ * 4. Only triggers reflection AFTER all threads are observed
2680
+ */
2681
+ async doResourceScopedObservation(record, currentThreadId, resourceId, currentThreadMessages, writer, abortSignal) {
2682
+ const { threads: allThreads } = await this.storage.listThreads({ filter: { resourceId } });
2683
+ const threadMetadataMap = /* @__PURE__ */ new Map();
2684
+ for (const thread of allThreads) {
2685
+ const omMetadata = memory.getThreadOMMetadata(thread.metadata);
2686
+ threadMetadataMap.set(thread.id, { lastObservedAt: omMetadata?.lastObservedAt });
2687
+ }
2688
+ const messagesByThread = /* @__PURE__ */ new Map();
2689
+ for (const thread of allThreads) {
2690
+ const threadLastObservedAt = threadMetadataMap.get(thread.id)?.lastObservedAt;
2691
+ const startDate = threadLastObservedAt ? new Date(new Date(threadLastObservedAt).getTime() + 1) : void 0;
2692
+ const result = await this.storage.listMessages({
2693
+ threadId: thread.id,
2694
+ perPage: false,
2695
+ orderBy: { field: "createdAt", direction: "ASC" },
2696
+ filter: startDate ? { dateRange: { start: startDate } } : void 0
2697
+ });
2698
+ if (result.messages.length > 0) {
2699
+ messagesByThread.set(thread.id, result.messages);
2700
+ }
2701
+ }
2702
+ if (currentThreadMessages.length > 0) {
2703
+ const existingCurrentThreadMsgs = messagesByThread.get(currentThreadId) ?? [];
2704
+ const messageMap = /* @__PURE__ */ new Map();
2705
+ for (const msg of existingCurrentThreadMsgs) {
2706
+ if (msg.id) messageMap.set(msg.id, msg);
2707
+ }
2708
+ for (const msg of currentThreadMessages) {
2709
+ if (msg.id) messageMap.set(msg.id, msg);
2710
+ }
2711
+ messagesByThread.set(currentThreadId, Array.from(messageMap.values()));
2712
+ }
2713
+ for (const [tid, msgs] of messagesByThread) {
2714
+ const filtered = msgs.filter((m) => !this.observedMessageIds.has(m.id));
2715
+ if (filtered.length > 0) {
2716
+ messagesByThread.set(tid, filtered);
2717
+ } else {
2718
+ messagesByThread.delete(tid);
2719
+ }
2720
+ }
2721
+ let totalMessages = 0;
2722
+ for (const msgs of messagesByThread.values()) {
2723
+ totalMessages += msgs.length;
2724
+ }
2725
+ if (totalMessages === 0) {
2726
+ return;
2727
+ }
2728
+ const threshold = this.getMaxThreshold(this.observationConfig.messageTokens);
2729
+ const threadTokenCounts = /* @__PURE__ */ new Map();
2730
+ for (const [threadId, msgs] of messagesByThread) {
2731
+ let tokens = 0;
2732
+ for (const msg of msgs) {
2733
+ tokens += this.tokenCounter.countMessage(msg);
2734
+ }
2735
+ threadTokenCounts.set(threadId, tokens);
2736
+ }
2737
+ const threadsBySize = Array.from(messagesByThread.keys()).sort((a, b) => {
2738
+ return (threadTokenCounts.get(b) ?? 0) - (threadTokenCounts.get(a) ?? 0);
2739
+ });
2740
+ let accumulatedTokens = 0;
2741
+ const threadsToObserve = [];
2742
+ for (const threadId of threadsBySize) {
2743
+ const threadTokens = threadTokenCounts.get(threadId) ?? 0;
2744
+ if (accumulatedTokens >= threshold) {
2745
+ break;
2746
+ }
2747
+ threadsToObserve.push(threadId);
2748
+ accumulatedTokens += threadTokens;
2749
+ }
2750
+ if (threadsToObserve.length === 0) {
2751
+ return;
2752
+ }
2753
+ const threadOrder = this.sortThreadsByOldestMessage(
2754
+ new Map(threadsToObserve.map((tid) => [tid, messagesByThread.get(tid) ?? []]))
2755
+ );
2756
+ await this.storage.setObservingFlag(record.id, true);
2757
+ const cycleId = crypto.randomUUID();
2758
+ const threadsWithMessages = /* @__PURE__ */ new Map();
2759
+ const threadTokensToObserve = /* @__PURE__ */ new Map();
2760
+ let observationStartedAt = "";
2761
+ try {
2762
+ const freshRecord = await this.storage.getObservationalMemory(null, resourceId);
2763
+ if (freshRecord && freshRecord.lastObservedAt && record.lastObservedAt) {
2764
+ if (freshRecord.lastObservedAt > record.lastObservedAt) {
2765
+ return;
2766
+ }
2767
+ }
2768
+ const existingObservations = freshRecord?.activeObservations ?? record.activeObservations ?? "";
2769
+ for (const threadId of threadOrder) {
2770
+ const msgs = messagesByThread.get(threadId);
2771
+ if (msgs && msgs.length > 0) {
2772
+ threadsWithMessages.set(threadId, msgs);
2773
+ }
2774
+ }
2775
+ this.emitDebugEvent({
2776
+ type: "observation_triggered",
2777
+ timestamp: /* @__PURE__ */ new Date(),
2778
+ threadId: threadOrder.join(","),
2779
+ resourceId,
2780
+ previousObservations: existingObservations,
2781
+ messages: Array.from(threadsWithMessages.values()).flat().map((m) => ({
2782
+ role: m.role,
2783
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
2784
+ }))
2785
+ });
2786
+ observationStartedAt = (/* @__PURE__ */ new Date()).toISOString();
2787
+ const allThreadIds = Array.from(threadsWithMessages.keys());
2788
+ for (const [threadId, msgs] of threadsWithMessages) {
2789
+ const lastMessage = msgs[msgs.length - 1];
2790
+ const tokensToObserve = this.tokenCounter.countMessages(msgs);
2791
+ threadTokensToObserve.set(threadId, tokensToObserve);
2792
+ if (lastMessage?.id) {
2793
+ const startMarker = this.createObservationStartMarker({
2794
+ cycleId,
2795
+ operationType: "observation",
2796
+ tokensToObserve,
2797
+ recordId: record.id,
2798
+ threadId,
2799
+ threadIds: allThreadIds
2800
+ });
2801
+ if (writer) {
2802
+ await writer.custom(startMarker).catch(() => {
2803
+ });
2804
+ }
2805
+ }
2806
+ }
2807
+ const maxTokensPerBatch = this.observationConfig.maxTokensPerBatch ?? OBSERVATIONAL_MEMORY_DEFAULTS.observation.maxTokensPerBatch;
2808
+ const orderedThreadIds = threadOrder.filter((tid) => threadsWithMessages.has(tid));
2809
+ const batches = [];
2810
+ let currentBatch = {
2811
+ threadIds: [],
2812
+ threadMap: /* @__PURE__ */ new Map()
2813
+ };
2814
+ let currentBatchTokens = 0;
2815
+ for (const threadId of orderedThreadIds) {
2816
+ const msgs = threadsWithMessages.get(threadId);
2817
+ const threadTokens = threadTokenCounts.get(threadId) ?? 0;
2818
+ if (currentBatchTokens + threadTokens > maxTokensPerBatch && currentBatch.threadIds.length > 0) {
2819
+ batches.push(currentBatch);
2820
+ currentBatch = { threadIds: [], threadMap: /* @__PURE__ */ new Map() };
2821
+ currentBatchTokens = 0;
2822
+ }
2823
+ currentBatch.threadIds.push(threadId);
2824
+ currentBatch.threadMap.set(threadId, msgs);
2825
+ currentBatchTokens += threadTokens;
2826
+ }
2827
+ if (currentBatch.threadIds.length > 0) {
2828
+ batches.push(currentBatch);
2829
+ }
2830
+ const batchPromises = batches.map(async (batch) => {
2831
+ const batchResult = await this.callMultiThreadObserver(
2832
+ existingObservations,
2833
+ batch.threadMap,
2834
+ batch.threadIds,
2835
+ abortSignal
2836
+ );
2837
+ return batchResult;
2838
+ });
2839
+ const batchResults = await Promise.all(batchPromises);
2840
+ const multiThreadResults = /* @__PURE__ */ new Map();
2841
+ let totalBatchUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
2842
+ for (const batchResult of batchResults) {
2843
+ for (const [threadId, result] of batchResult.results) {
2844
+ multiThreadResults.set(threadId, result);
2845
+ }
2846
+ if (batchResult.usage) {
2847
+ totalBatchUsage.inputTokens += batchResult.usage.inputTokens ?? 0;
2848
+ totalBatchUsage.outputTokens += batchResult.usage.outputTokens ?? 0;
2849
+ totalBatchUsage.totalTokens += batchResult.usage.totalTokens ?? 0;
2850
+ }
2851
+ }
2852
+ const observationResults = [];
2853
+ for (const threadId of threadOrder) {
2854
+ const threadMessages = messagesByThread.get(threadId) ?? [];
2855
+ if (threadMessages.length === 0) continue;
2856
+ const result = multiThreadResults.get(threadId);
2857
+ if (!result) {
2858
+ continue;
2859
+ }
2860
+ observationResults.push({
2861
+ threadId,
2862
+ threadMessages,
2863
+ result
2864
+ });
2865
+ }
2866
+ let currentObservations = existingObservations;
2867
+ let cycleObservationTokens = 0;
2868
+ for (const obsResult of observationResults) {
2869
+ if (!obsResult) continue;
2870
+ const { threadId, threadMessages, result } = obsResult;
2871
+ cycleObservationTokens += this.tokenCounter.countObservations(result.observations);
2872
+ const threadSection = await this.wrapWithThreadTag(threadId, result.observations);
2873
+ currentObservations = this.replaceOrAppendThreadSection(currentObservations, threadId, threadSection);
2874
+ const threadLastObservedAt = this.getMaxMessageTimestamp(threadMessages);
2875
+ const thread = await this.storage.getThreadById({ threadId });
2876
+ if (thread) {
2877
+ const newMetadata = memory.setThreadOMMetadata(thread.metadata, {
2878
+ lastObservedAt: threadLastObservedAt.toISOString(),
2879
+ ...result.suggestedContinuation && { suggestedResponse: result.suggestedContinuation },
2880
+ ...result.currentTask && { currentTask: result.currentTask }
2881
+ });
2882
+ await this.storage.updateThread({
2883
+ id: threadId,
2884
+ title: thread.title ?? "",
2885
+ metadata: newMetadata
2886
+ });
2887
+ }
2888
+ const isFirstThread = observationResults.indexOf(obsResult) === 0;
2889
+ this.emitDebugEvent({
2890
+ type: "observation_complete",
2891
+ timestamp: /* @__PURE__ */ new Date(),
2892
+ threadId,
2893
+ resourceId,
2894
+ observations: threadSection,
2895
+ rawObserverOutput: result.observations,
2896
+ previousObservations: record.activeObservations,
2897
+ messages: threadMessages.map((m) => ({
2898
+ role: m.role,
2899
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
2900
+ })),
2901
+ // Add batch usage to first thread's event only (to avoid double-counting)
2902
+ usage: isFirstThread && totalBatchUsage.totalTokens > 0 ? totalBatchUsage : void 0
2903
+ });
2904
+ }
2905
+ let totalTokenCount = this.tokenCounter.countObservations(currentObservations);
2906
+ const observedMessages = observationResults.filter((r) => r !== null).flatMap((r) => r.threadMessages);
2907
+ const lastObservedAt = this.getMaxMessageTimestamp(observedMessages);
2908
+ const newMessageIds = observedMessages.map((m) => m.id);
2909
+ const existingIds = record.observedMessageIds ?? [];
2910
+ const allObservedIds = [.../* @__PURE__ */ new Set([...existingIds, ...newMessageIds])];
2911
+ await this.storage.updateActiveObservations({
2912
+ id: record.id,
2913
+ observations: currentObservations,
2914
+ tokenCount: totalTokenCount,
2915
+ lastObservedAt,
2916
+ observedMessageIds: allObservedIds
2917
+ });
2918
+ for (const obsResult of observationResults) {
2919
+ if (!obsResult) continue;
2920
+ const { threadId, threadMessages, result } = obsResult;
2921
+ const lastMessage = threadMessages[threadMessages.length - 1];
2922
+ if (lastMessage?.id) {
2923
+ const tokensObserved = threadTokensToObserve.get(threadId) ?? this.tokenCounter.countMessages(threadMessages);
2924
+ const endMarker = this.createObservationEndMarker({
2925
+ cycleId,
2926
+ operationType: "observation",
2927
+ startedAt: observationStartedAt,
2928
+ tokensObserved,
2929
+ observationTokens: cycleObservationTokens,
2930
+ observations: result.observations,
2931
+ currentTask: result.currentTask,
2932
+ suggestedResponse: result.suggestedContinuation,
2933
+ recordId: record.id,
2934
+ threadId
2935
+ });
2936
+ if (writer) {
2937
+ await writer.custom(endMarker).catch(() => {
2938
+ });
2939
+ }
2940
+ }
2941
+ }
2942
+ await this.maybeReflect(
2943
+ { ...record, activeObservations: currentObservations },
2944
+ totalTokenCount,
2945
+ currentThreadId,
2946
+ writer,
2947
+ abortSignal
2948
+ );
2949
+ } catch (error) {
2950
+ for (const [threadId, msgs] of threadsWithMessages) {
2951
+ const lastMessage = msgs[msgs.length - 1];
2952
+ if (lastMessage?.id) {
2953
+ const tokensAttempted = threadTokensToObserve.get(threadId) ?? 0;
2954
+ const failedMarker = this.createObservationFailedMarker({
2955
+ cycleId,
2956
+ operationType: "observation",
2957
+ startedAt: observationStartedAt,
2958
+ tokensAttempted,
2959
+ error: error instanceof Error ? error.message : String(error),
2960
+ recordId: record.id,
2961
+ threadId
2962
+ });
2963
+ if (writer) {
2964
+ await writer.custom(failedMarker).catch(() => {
2965
+ });
2966
+ }
2967
+ }
2968
+ }
2969
+ if (abortSignal?.aborted) {
2970
+ throw error;
2971
+ }
2972
+ console.error(`[OM] Resource-scoped observation failed:`, error instanceof Error ? error.message : String(error));
2973
+ } finally {
2974
+ await this.storage.setObservingFlag(record.id, false);
2975
+ }
2976
+ }
2977
+ /**
2978
+ * Check if reflection needed and trigger if so.
2979
+ * SIMPLIFIED: Always uses synchronous reflection (async buffering disabled).
2980
+ */
2981
+ async maybeReflect(record, observationTokens, _threadId, writer, abortSignal) {
2982
+ if (!this.shouldReflect(observationTokens)) {
2983
+ return;
2984
+ }
2985
+ if (record.isReflecting) {
2986
+ return;
2987
+ }
2988
+ const reflectThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
2989
+ await this.storage.setReflectingFlag(record.id, true);
2990
+ const cycleId = crypto.randomUUID();
2991
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2992
+ const threadId = _threadId ?? "unknown";
2993
+ if (writer) {
2994
+ const startMarker = this.createObservationStartMarker({
2995
+ cycleId,
2996
+ operationType: "reflection",
2997
+ tokensToObserve: observationTokens,
2998
+ recordId: record.id,
2999
+ threadId,
3000
+ threadIds: [threadId]
3001
+ });
3002
+ await writer.custom(startMarker).catch(() => {
3003
+ });
3004
+ }
3005
+ this.emitDebugEvent({
3006
+ type: "reflection_triggered",
3007
+ timestamp: /* @__PURE__ */ new Date(),
3008
+ threadId,
3009
+ resourceId: record.resourceId ?? "",
3010
+ inputTokens: observationTokens,
3011
+ activeObservationsLength: record.activeObservations?.length ?? 0
3012
+ });
3013
+ const streamContext = writer ? {
3014
+ writer,
3015
+ cycleId,
3016
+ startedAt,
3017
+ recordId: record.id,
3018
+ threadId
3019
+ } : void 0;
3020
+ try {
3021
+ const reflectResult = await this.callReflector(
3022
+ record.activeObservations,
3023
+ void 0,
3024
+ streamContext,
3025
+ reflectThreshold,
3026
+ abortSignal
3027
+ );
3028
+ const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
3029
+ await this.storage.createReflectionGeneration({
3030
+ currentRecord: record,
3031
+ reflection: reflectResult.observations,
3032
+ tokenCount: reflectionTokenCount
3033
+ });
3034
+ if (writer && streamContext) {
3035
+ const endMarker = this.createObservationEndMarker({
3036
+ cycleId: streamContext.cycleId,
3037
+ operationType: "reflection",
3038
+ startedAt: streamContext.startedAt,
3039
+ tokensObserved: observationTokens,
3040
+ observationTokens: reflectionTokenCount,
3041
+ observations: reflectResult.observations,
3042
+ recordId: record.id,
3043
+ threadId
3044
+ });
3045
+ await writer.custom(endMarker).catch(() => {
3046
+ });
3047
+ }
3048
+ this.emitDebugEvent({
3049
+ type: "reflection_complete",
3050
+ timestamp: /* @__PURE__ */ new Date(),
3051
+ threadId,
3052
+ resourceId: record.resourceId ?? "",
3053
+ inputTokens: observationTokens,
3054
+ outputTokens: reflectionTokenCount,
3055
+ observations: reflectResult.observations,
3056
+ usage: reflectResult.usage
3057
+ });
3058
+ } catch (error) {
3059
+ if (writer && streamContext) {
3060
+ const failedMarker = this.createObservationFailedMarker({
3061
+ cycleId: streamContext.cycleId,
3062
+ operationType: "reflection",
3063
+ startedAt: streamContext.startedAt,
3064
+ tokensAttempted: observationTokens,
3065
+ error: error instanceof Error ? error.message : String(error),
3066
+ recordId: record.id,
3067
+ threadId
3068
+ });
3069
+ await writer.custom(failedMarker).catch(() => {
3070
+ });
3071
+ }
3072
+ if (abortSignal?.aborted) {
3073
+ throw error;
3074
+ }
3075
+ console.error(`[OM] Reflection failed:`, error instanceof Error ? error.message : String(error));
3076
+ } finally {
3077
+ await this.storage.setReflectingFlag(record.id, false);
3078
+ }
3079
+ }
3080
+ /**
3081
+ * Manually trigger observation.
3082
+ */
3083
+ async observe(threadId, resourceId, _prompt) {
3084
+ const lockKey = this.getLockKey(threadId, resourceId);
3085
+ await this.withLock(lockKey, async () => {
3086
+ const freshRecord = await this.getOrCreateRecord(threadId, resourceId);
3087
+ if (this.scope === "resource" && resourceId) {
3088
+ await this.doResourceScopedObservation(
3089
+ freshRecord,
3090
+ threadId,
3091
+ resourceId,
3092
+ []
3093
+ // no in-flight messages — everything is already in the DB
3094
+ );
3095
+ } else {
3096
+ const unobservedMessages = await this.loadUnobservedMessages(
3097
+ threadId,
3098
+ resourceId,
3099
+ freshRecord.lastObservedAt ? new Date(freshRecord.lastObservedAt) : void 0
3100
+ );
3101
+ if (unobservedMessages.length === 0) {
3102
+ return;
3103
+ }
3104
+ await this.doSynchronousObservation(freshRecord, threadId, unobservedMessages);
3105
+ }
3106
+ });
3107
+ }
3108
+ /**
3109
+ * Manually trigger reflection with optional guidance prompt.
3110
+ *
3111
+ * @example
3112
+ * ```ts
3113
+ * // Trigger reflection with specific focus
3114
+ * await om.reflect(threadId, resourceId,
3115
+ * "focus on the authentication implementation, only keep minimal details about UI styling"
3116
+ * );
3117
+ * ```
3118
+ */
3119
+ async reflect(threadId, resourceId, prompt) {
3120
+ const record = await this.getOrCreateRecord(threadId, resourceId);
3121
+ if (!record.activeObservations) {
3122
+ return;
3123
+ }
3124
+ await this.storage.setReflectingFlag(record.id, true);
3125
+ try {
3126
+ const reflectThreshold = this.getMaxThreshold(this.reflectionConfig.observationTokens);
3127
+ const reflectResult = await this.callReflector(record.activeObservations, prompt, void 0, reflectThreshold);
3128
+ const reflectionTokenCount = this.tokenCounter.countObservations(reflectResult.observations);
3129
+ await this.storage.createReflectionGeneration({
3130
+ currentRecord: record,
3131
+ reflection: reflectResult.observations,
3132
+ tokenCount: reflectionTokenCount
3133
+ });
3134
+ } finally {
3135
+ await this.storage.setReflectingFlag(record.id, false);
3136
+ }
3137
+ }
3138
+ /**
3139
+ * Get current observations for a thread/resource
3140
+ */
3141
+ async getObservations(threadId, resourceId) {
3142
+ const ids = this.getStorageIds(threadId, resourceId);
3143
+ const record = await this.storage.getObservationalMemory(ids.threadId, ids.resourceId);
3144
+ return record?.activeObservations;
3145
+ }
3146
+ /**
3147
+ * Get current record for a thread/resource
3148
+ */
3149
+ async getRecord(threadId, resourceId) {
3150
+ const ids = this.getStorageIds(threadId, resourceId);
3151
+ return this.storage.getObservationalMemory(ids.threadId, ids.resourceId);
3152
+ }
3153
+ /**
3154
+ * Get observation history (previous generations)
3155
+ */
3156
+ async getHistory(threadId, resourceId, limit) {
3157
+ const ids = this.getStorageIds(threadId, resourceId);
3158
+ return this.storage.getObservationalMemoryHistory(ids.threadId, ids.resourceId, limit);
3159
+ }
3160
+ /**
3161
+ * Clear all memory for a specific thread/resource
3162
+ */
3163
+ async clear(threadId, resourceId) {
3164
+ const ids = this.getStorageIds(threadId, resourceId);
3165
+ await this.storage.clearObservationalMemory(ids.threadId, ids.resourceId);
3166
+ }
3167
+ /**
3168
+ * Get the underlying storage adapter
3169
+ */
3170
+ getStorage() {
3171
+ return this.storage;
3172
+ }
3173
+ /**
3174
+ * Get the token counter
3175
+ */
3176
+ getTokenCounter() {
3177
+ return this.tokenCounter;
3178
+ }
3179
+ /**
3180
+ * Get current observation configuration
3181
+ */
3182
+ getObservationConfig() {
3183
+ return this.observationConfig;
3184
+ }
3185
+ /**
3186
+ * Get current reflection configuration
3187
+ */
3188
+ getReflectionConfig() {
3189
+ return this.reflectionConfig;
3190
+ }
3191
+ };
3192
+
3193
+ exports.OBSERVATIONAL_MEMORY_DEFAULTS = OBSERVATIONAL_MEMORY_DEFAULTS;
3194
+ exports.OBSERVER_SYSTEM_PROMPT = OBSERVER_SYSTEM_PROMPT;
3195
+ exports.ObservationalMemory = ObservationalMemory;
3196
+ exports.TokenCounter = TokenCounter;
3197
+ exports.buildObserverPrompt = buildObserverPrompt;
3198
+ exports.buildObserverSystemPrompt = buildObserverSystemPrompt;
3199
+ exports.extractCurrentTask = extractCurrentTask;
3200
+ exports.formatMessagesForObserver = formatMessagesForObserver;
3201
+ exports.hasCurrentTaskSection = hasCurrentTaskSection;
3202
+ exports.optimizeObservationsForContext = optimizeObservationsForContext;
3203
+ exports.parseObserverOutput = parseObserverOutput;
3204
+ //# sourceMappingURL=chunk-FQJWVCDF.cjs.map
3205
+ //# sourceMappingURL=chunk-FQJWVCDF.cjs.map