@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.
- package/CHANGELOG.md +95 -0
- package/dist/chunk-6TXUWFIU.js +3188 -0
- package/dist/chunk-6TXUWFIU.js.map +1 -0
- package/dist/chunk-FQJWVCDF.cjs +3205 -0
- package/dist/chunk-FQJWVCDF.cjs.map +1 -0
- package/dist/docs/README.md +1 -1
- package/dist/docs/SKILL.md +12 -1
- package/dist/docs/SOURCE_MAP.json +62 -2
- package/dist/docs/memory/02-storage.md +10 -0
- package/dist/index.cjs +96 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +53 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +96 -1
- package/dist/index.js.map +1 -1
- package/dist/observational-memory-3Q42SITP.cjs +52 -0
- package/dist/observational-memory-3Q42SITP.cjs.map +1 -0
- package/dist/observational-memory-VXLHOSDZ.js +3 -0
- package/dist/observational-memory-VXLHOSDZ.js.map +1 -0
- package/dist/processors/index.cjs +52 -0
- package/dist/processors/index.cjs.map +1 -0
- package/dist/processors/index.d.ts +2 -0
- package/dist/processors/index.d.ts.map +1 -0
- package/dist/processors/index.js +3 -0
- package/dist/processors/index.js.map +1 -0
- package/dist/processors/observational-memory/index.d.ts +18 -0
- package/dist/processors/observational-memory/index.d.ts.map +1 -0
- package/dist/processors/observational-memory/observational-memory.d.ts +579 -0
- package/dist/processors/observational-memory/observational-memory.d.ts.map +1 -0
- package/dist/processors/observational-memory/observer-agent.d.ts +117 -0
- package/dist/processors/observational-memory/observer-agent.d.ts.map +1 -0
- package/dist/processors/observational-memory/reflector-agent.d.ts +46 -0
- package/dist/processors/observational-memory/reflector-agent.d.ts.map +1 -0
- package/dist/processors/observational-memory/token-counter.d.ts +30 -0
- package/dist/processors/observational-memory/token-counter.d.ts.map +1 -0
- package/dist/processors/observational-memory/types.d.ts +288 -0
- package/dist/processors/observational-memory/types.d.ts.map +1 -0
- 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
|