@dory-agentic/dory-agentic-sdk 0.2.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/LICENSE +21 -0
- package/README.md +331 -0
- package/dist/agent/createLfsAgent.d.ts +27 -0
- package/dist/agent/createLfsAgent.d.ts.map +1 -0
- package/dist/agent/createLfsAgent.js +51 -0
- package/dist/agent/loopConfig.d.ts +6 -0
- package/dist/agent/loopConfig.d.ts.map +1 -0
- package/dist/agent/loopConfig.js +5 -0
- package/dist/agent/runLfsTurn.d.ts +10 -0
- package/dist/agent/runLfsTurn.d.ts.map +1 -0
- package/dist/agent/runLfsTurn.js +20 -0
- package/dist/history/dorycodeAdapter.d.ts +16 -0
- package/dist/history/dorycodeAdapter.d.ts.map +1 -0
- package/dist/history/dorycodeAdapter.js +18 -0
- package/dist/history/formatForModel.d.ts +19 -0
- package/dist/history/formatForModel.d.ts.map +1 -0
- package/dist/history/formatForModel.js +22 -0
- package/dist/history/lanes.d.ts +3 -0
- package/dist/history/lanes.d.ts.map +1 -0
- package/dist/history/lanes.js +3 -0
- package/dist/history/messageManifest.d.ts +45 -0
- package/dist/history/messageManifest.d.ts.map +1 -0
- package/dist/history/messageManifest.js +231 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/lfs/adaptiveCoefficients.d.ts +13 -0
- package/dist/lfs/adaptiveCoefficients.d.ts.map +1 -0
- package/dist/lfs/adaptiveCoefficients.js +16 -0
- package/dist/lfs/attenuationCompiler.d.ts +16 -0
- package/dist/lfs/attenuationCompiler.d.ts.map +1 -0
- package/dist/lfs/attenuationCompiler.js +123 -0
- package/dist/lfs/chunkManager.d.ts +6 -0
- package/dist/lfs/chunkManager.d.ts.map +1 -0
- package/dist/lfs/chunkManager.js +142 -0
- package/dist/lfs/config.d.ts +23 -0
- package/dist/lfs/config.d.ts.map +1 -0
- package/dist/lfs/config.js +34 -0
- package/dist/lfs/contextAdapter.d.ts +5 -0
- package/dist/lfs/contextAdapter.d.ts.map +1 -0
- package/dist/lfs/contextAdapter.js +161 -0
- package/dist/lfs/memoryEngine.d.ts +108 -0
- package/dist/lfs/memoryEngine.d.ts.map +1 -0
- package/dist/lfs/memoryEngine.js +322 -0
- package/dist/lfs/memoryParser.d.ts +121 -0
- package/dist/lfs/memoryParser.d.ts.map +1 -0
- package/dist/lfs/memoryParser.js +743 -0
- package/dist/lfs/paritySwapper.d.ts +20 -0
- package/dist/lfs/paritySwapper.d.ts.map +1 -0
- package/dist/lfs/paritySwapper.js +317 -0
- package/dist/lfs/preflightRouter.d.ts +14 -0
- package/dist/lfs/preflightRouter.d.ts.map +1 -0
- package/dist/lfs/preflightRouter.js +24 -0
- package/dist/lfs/runtimeConfig.d.ts +31 -0
- package/dist/lfs/runtimeConfig.d.ts.map +1 -0
- package/dist/lfs/runtimeConfig.js +40 -0
- package/dist/lfs/sessionStore.d.ts +14 -0
- package/dist/lfs/sessionStore.d.ts.map +1 -0
- package/dist/lfs/sessionStore.js +52 -0
- package/dist/lfs/tokenCounter.d.ts +90 -0
- package/dist/lfs/tokenCounter.d.ts.map +1 -0
- package/dist/lfs/tokenCounter.js +26 -0
- package/dist/lfs/types.d.ts +44 -0
- package/dist/lfs/types.d.ts.map +1 -0
- package/dist/lfs/types.js +1 -0
- package/dist/lfs/vectorDb.d.ts +19 -0
- package/dist/lfs/vectorDb.d.ts.map +1 -0
- package/dist/lfs/vectorDb.js +158 -0
- package/dist/mcp/client.d.ts +23 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +60 -0
- package/dist/mcp/config.d.ts +10 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +72 -0
- package/dist/providers/ollama.d.ts +2 -0
- package/dist/providers/ollama.d.ts.map +1 -0
- package/dist/providers/ollama.js +7 -0
- package/dist/providers/registry.d.ts +5 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +9 -0
- package/dist/tools/registry.d.ts +14 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +26 -0
- package/dist/tools/remind.d.ts +15 -0
- package/dist/tools/remind.d.ts.map +1 -0
- package/dist/tools/remind.js +53 -0
- package/dist/tools/skillLoader.d.ts +12 -0
- package/dist/tools/skillLoader.d.ts.map +1 -0
- package/dist/tools/skillLoader.js +38 -0
- package/package.json +66 -0
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import { buildMsgNumberToIdMap, formatScorableIdListForPrompt, isScorableMessage, resolveMessageId, } from "../history/messageManifest";
|
|
2
|
+
export { collectScorableMessageIds } from "../history/messageManifest";
|
|
3
|
+
import { queryVectorCache } from "./vectorDb";
|
|
4
|
+
/** All non-archived rows with IDs (for prompt listing). */
|
|
5
|
+
export function collectActiveMessageIds(history) {
|
|
6
|
+
return history
|
|
7
|
+
.filter((m) => !m.isArchived && m.role !== 'system' && m.id)
|
|
8
|
+
.map((m) => m.id);
|
|
9
|
+
}
|
|
10
|
+
/** Map agent output keys to exact IDs from history (fixes msg_1 vs msg_01; never by list position). */
|
|
11
|
+
export function canonicalizeMessageIds(weights, activeIds) {
|
|
12
|
+
const numberMap = buildMsgNumberToIdMap(activeIds);
|
|
13
|
+
const invalidIds = [];
|
|
14
|
+
const canonical = {};
|
|
15
|
+
for (const [rawId, val] of Object.entries(weights)) {
|
|
16
|
+
if (val <= 0)
|
|
17
|
+
continue;
|
|
18
|
+
const match = resolveMessageId(rawId, activeIds, numberMap);
|
|
19
|
+
if (match) {
|
|
20
|
+
canonical[match] = (canonical[match] ?? 0) + val;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
invalidIds.push(rawId);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { canonical, invalidIds };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Merge agent relevance % with referenced_message_ids.
|
|
30
|
+
* Cited IDs with no score get a fair share so used context is never left at 0%.
|
|
31
|
+
*/
|
|
32
|
+
export function mergeRelevanceWithReferences(agentPercents, referencedIds, activeIds) {
|
|
33
|
+
const merged = { ...agentPercents };
|
|
34
|
+
const cited = referencedIds.filter((id) => activeIds.has(id));
|
|
35
|
+
if (cited.length === 0)
|
|
36
|
+
return merged;
|
|
37
|
+
const citedMissing = cited.filter((id) => !(merged[id] > 0));
|
|
38
|
+
if (citedMissing.length === 0)
|
|
39
|
+
return merged;
|
|
40
|
+
const existingSum = Object.values(merged).reduce((a, b) => a + b, 0);
|
|
41
|
+
const reserveForCited = citedMissing.length * 5;
|
|
42
|
+
const scale = existingSum > 0 && existingSum + reserveForCited > 100
|
|
43
|
+
? (100 - reserveForCited) / existingSum
|
|
44
|
+
: 1;
|
|
45
|
+
if (scale < 1) {
|
|
46
|
+
for (const id of Object.keys(merged)) {
|
|
47
|
+
merged[id] = Number((merged[id] * scale).toFixed(2));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const perMissing = citedMissing.length > 0
|
|
51
|
+
? Number(((100 - Object.values(merged).reduce((a, b) => a + b, 0)) /
|
|
52
|
+
citedMissing.length).toFixed(2))
|
|
53
|
+
: 0;
|
|
54
|
+
for (const id of citedMissing) {
|
|
55
|
+
merged[id] = Math.max(merged[id] ?? 0, perMissing);
|
|
56
|
+
}
|
|
57
|
+
return merged;
|
|
58
|
+
}
|
|
59
|
+
/** Shared instructions for optimized-lane relevance % (server converts to credits). */
|
|
60
|
+
export function buildRelevanceScoringInstructions(params) {
|
|
61
|
+
const idList = formatScorableIdListForPrompt(params.history);
|
|
62
|
+
const hasChunks = params.history.some(m => m.chunks && m.chunks.length > 0);
|
|
63
|
+
if (hasChunks) {
|
|
64
|
+
return `Context pressure is active. Review the active memory manifest. Return an explicit evaluation dictionary inside your |||RATIO:{}||| block scoring the functional importance of historical chunk IDs to your current task on a strict scale from 0.0 (Unrelated noise) to 1.0 (Core critical asset).
|
|
65
|
+
|
|
66
|
+
Scorable active chunk IDs:
|
|
67
|
+
${idList}`;
|
|
68
|
+
}
|
|
69
|
+
return `--- MEMORY SCORING (after you finish "response") ---
|
|
70
|
+
1. Write your full answer in "response" first.
|
|
71
|
+
2. Set "referenced_message_ids" to every ID you actually used (subset of scorable IDs below).
|
|
72
|
+
3. Set "credit_allocation" to relevance PERCENTAGES (0–100, sum ~100) for same-topic messages that helped produce THIS answer.
|
|
73
|
+
- Keys MUST be exact Message IDs from conversation rows (msg_01, msg_02, …) — NOT conversation # position alone.
|
|
74
|
+
- Chat history is oldest → newest; msg_01 is the first user message unless your thread started differently.
|
|
75
|
+
- Do NOT score Internal trace or Tool result rows.
|
|
76
|
+
- Percentages are NOT credits — the server maps % → ${params.budget} credits for this turn.
|
|
77
|
+
|
|
78
|
+
"context_shift": "continue" | "new_topic" (unrelated → credit_allocation: {}).
|
|
79
|
+
|
|
80
|
+
Scorable messages (same order as [Message ID: …] rows in history above):
|
|
81
|
+
${idList}`;
|
|
82
|
+
}
|
|
83
|
+
/** Extract |||ALLOCATION:{...}||| from any text (response body or full JSON stream). */
|
|
84
|
+
export function extractAllocation(content) {
|
|
85
|
+
if (!content)
|
|
86
|
+
return {};
|
|
87
|
+
const patterns = [
|
|
88
|
+
/\|\|\|RATIO:\s*([\s\S]*?)\s*\|\|\|/,
|
|
89
|
+
/RATIO:\s*(\{[\s\S]*?\})(?:\s*\|\|\||\s*$)/,
|
|
90
|
+
/\|\|\|ALLOCATION:\s*([\s\S]*?)\s*\|\|\|/,
|
|
91
|
+
/ALLOCATION:\s*(\{[\s\S]*?\})(?:\s*\|\|\||\s*$)/,
|
|
92
|
+
];
|
|
93
|
+
for (const pattern of patterns) {
|
|
94
|
+
const match = content.match(pattern);
|
|
95
|
+
if (!match)
|
|
96
|
+
continue;
|
|
97
|
+
const parsed = parseAllocationJson(match[1].trim());
|
|
98
|
+
if (Object.keys(parsed).length > 0)
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
function parseAllocationJson(raw) {
|
|
104
|
+
const attempts = [raw, raw.replace(/\\"/g, '"')];
|
|
105
|
+
for (const attempt of attempts) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(attempt);
|
|
108
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
109
|
+
return normalizeAllocation(parsed);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// try next
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Fallback: "msg_01": 25 style without valid JSON
|
|
117
|
+
const loose = {};
|
|
118
|
+
const idMatches = raw.matchAll(/"?((?:msg|slice)_\d+)"?\s*:\s*(\d+(?:\.\d+)?)/g);
|
|
119
|
+
for (const m of idMatches) {
|
|
120
|
+
const val = Number(m[2]);
|
|
121
|
+
if (!isNaN(val) && val > 0)
|
|
122
|
+
loose[m[1]] = val;
|
|
123
|
+
}
|
|
124
|
+
return loose;
|
|
125
|
+
}
|
|
126
|
+
function normalizeAllocation(obj) {
|
|
127
|
+
const allocation = {};
|
|
128
|
+
for (const key in obj) {
|
|
129
|
+
const val = Number(obj[key]);
|
|
130
|
+
if (!isNaN(val) && val > 0)
|
|
131
|
+
allocation[key] = val;
|
|
132
|
+
}
|
|
133
|
+
return allocation;
|
|
134
|
+
}
|
|
135
|
+
export function extractReferencedIds(content) {
|
|
136
|
+
if (!content)
|
|
137
|
+
return [];
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(content);
|
|
140
|
+
if (Array.isArray(parsed.referenced_message_ids)) {
|
|
141
|
+
return parsed.referenced_message_ids.filter((id) => typeof id === 'string');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// partial stream
|
|
146
|
+
}
|
|
147
|
+
const match = content.match(/"referenced_message_ids"\s*:\s*\[([\s\S]*?)\]/);
|
|
148
|
+
if (!match)
|
|
149
|
+
return [];
|
|
150
|
+
const ids = match[1].match(/"((?:msg|slice)_\d+)"/g);
|
|
151
|
+
return ids ? ids.map((id) => id.replace(/"/g, '')) : [];
|
|
152
|
+
}
|
|
153
|
+
function extractCreditAllocationFromParsed(parsed) {
|
|
154
|
+
const raw = parsed.credit_allocation ?? parsed.relevance_percent ?? parsed.relevance_percentages;
|
|
155
|
+
if (raw && typeof raw === 'object') {
|
|
156
|
+
return normalizeAllocation(raw);
|
|
157
|
+
}
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
function extractContextShift(parsed) {
|
|
161
|
+
const raw = parsed.context_shift;
|
|
162
|
+
if (raw === 'new_topic' || raw === 'continue')
|
|
163
|
+
return raw;
|
|
164
|
+
return 'continue';
|
|
165
|
+
}
|
|
166
|
+
/** Map normalized 0–100% shares to credits that sum to budget (2 decimal places). */
|
|
167
|
+
export function percentMapToCredits(normalizedPercents, budget) {
|
|
168
|
+
const entries = Object.entries(normalizedPercents).filter(([, p]) => p > 0);
|
|
169
|
+
if (entries.length === 0 || budget <= 0)
|
|
170
|
+
return {};
|
|
171
|
+
const credits = {};
|
|
172
|
+
let allocated = 0;
|
|
173
|
+
entries.forEach(([id, pct], index) => {
|
|
174
|
+
if (index === entries.length - 1) {
|
|
175
|
+
credits[id] = Math.max(0, Number((budget - allocated).toFixed(2)));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const c = Number(((pct / 100) * budget).toFixed(2));
|
|
179
|
+
credits[id] = c;
|
|
180
|
+
allocated += c;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return credits;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Agent sends relevance as percentages (0–100) per message ID.
|
|
187
|
+
* Values are normalized to sum to 100%, then converted to credits in the backend.
|
|
188
|
+
*/
|
|
189
|
+
export function normalizePercentagesToBudget(agentPercents, budget, activeIds) {
|
|
190
|
+
const invalidIds = [];
|
|
191
|
+
const rawPercents = {};
|
|
192
|
+
for (const id in agentPercents) {
|
|
193
|
+
if (agentPercents[id] <= 0)
|
|
194
|
+
continue;
|
|
195
|
+
if (!activeIds.has(id)) {
|
|
196
|
+
invalidIds.push(id);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
rawPercents[id] = agentPercents[id];
|
|
200
|
+
}
|
|
201
|
+
const rawSum = Object.values(rawPercents).reduce((a, b) => a + b, 0);
|
|
202
|
+
if (rawSum <= 0 || budget <= 0) {
|
|
203
|
+
return {
|
|
204
|
+
rawPercents,
|
|
205
|
+
normalizedPercents: {},
|
|
206
|
+
credits: {},
|
|
207
|
+
rawSum,
|
|
208
|
+
invalidIds,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const normalizedPercents = {};
|
|
212
|
+
for (const [id, p] of Object.entries(rawPercents)) {
|
|
213
|
+
normalizedPercents[id] = Number(((p / rawSum) * 100).toFixed(2));
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
rawPercents,
|
|
217
|
+
normalizedPercents,
|
|
218
|
+
credits: percentMapToCredits(normalizedPercents, budget),
|
|
219
|
+
rawSum,
|
|
220
|
+
invalidIds,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/** Distribute integer credits so they sum exactly to budget using relevance weights. */
|
|
224
|
+
export function normalizeRelevanceToBudget(weights, budget, activeIds) {
|
|
225
|
+
const valid = {};
|
|
226
|
+
let totalWeight = 0;
|
|
227
|
+
for (const id in weights) {
|
|
228
|
+
if (activeIds.has(id) && weights[id] > 0) {
|
|
229
|
+
valid[id] = weights[id];
|
|
230
|
+
totalWeight += weights[id];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (totalWeight <= 0 || budget <= 0)
|
|
234
|
+
return {};
|
|
235
|
+
const entries = Object.entries(valid);
|
|
236
|
+
const allocation = {};
|
|
237
|
+
let allocatedSum = 0;
|
|
238
|
+
entries.forEach(([id, weight], index) => {
|
|
239
|
+
if (index === entries.length - 1) {
|
|
240
|
+
allocation[id] = Math.max(0, budget - allocatedSum);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const share = Math.round((weight / totalWeight) * budget);
|
|
244
|
+
allocation[id] = share;
|
|
245
|
+
allocatedSum += share;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return allocation;
|
|
249
|
+
}
|
|
250
|
+
export function parsePartialJson(jsonStr, isMmuActive) {
|
|
251
|
+
if (isMmuActive === false) {
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(jsonStr);
|
|
254
|
+
const response = typeof parsed.response === 'string' ? parsed.response : (Object.entries(parsed)
|
|
255
|
+
.filter(([k]) => k !== 'thinking')
|
|
256
|
+
.map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`)
|
|
257
|
+
.join('\n'));
|
|
258
|
+
return {
|
|
259
|
+
thinking: typeof parsed.thinking === 'string' ? parsed.thinking : '',
|
|
260
|
+
response,
|
|
261
|
+
referencedIds: [],
|
|
262
|
+
creditAllocation: {},
|
|
263
|
+
contextShift: 'continue',
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// partial stream
|
|
268
|
+
}
|
|
269
|
+
let thinking = '';
|
|
270
|
+
let response = '';
|
|
271
|
+
const thinkingMatch = jsonStr.match(/"thinking"\s*:\s*"(|.*?[^\\])"/);
|
|
272
|
+
if (thinkingMatch) {
|
|
273
|
+
thinking = thinkingMatch[1];
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const thinkingStartMatch = jsonStr.match(/"thinking"\s*:\s*"([\s\S]*)/);
|
|
277
|
+
if (thinkingStartMatch)
|
|
278
|
+
thinking = thinkingStartMatch[1];
|
|
279
|
+
}
|
|
280
|
+
const responseMatch = jsonStr.match(/"response"\s*:\s*"(|.*?[^\\])"/);
|
|
281
|
+
if (responseMatch) {
|
|
282
|
+
response = responseMatch[1];
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const responseStartMatch = jsonStr.match(/"response"\s*:\s*"([\s\S]*)/);
|
|
286
|
+
if (responseStartMatch)
|
|
287
|
+
response = responseStartMatch[1];
|
|
288
|
+
}
|
|
289
|
+
const unescapeStr = (str) => str
|
|
290
|
+
.replace(/\\"/g, '"')
|
|
291
|
+
.replace(/\\n/g, '\n')
|
|
292
|
+
.replace(/\\t/g, '\t')
|
|
293
|
+
.replace(/\\\\/g, '\\');
|
|
294
|
+
return {
|
|
295
|
+
thinking: unescapeStr(thinking),
|
|
296
|
+
response: unescapeStr(response),
|
|
297
|
+
referencedIds: [],
|
|
298
|
+
creditAllocation: {},
|
|
299
|
+
contextShift: 'continue',
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(jsonStr);
|
|
304
|
+
const response = typeof parsed.response === 'string' ? parsed.response : (Object.entries(parsed)
|
|
305
|
+
.filter(([k]) => k !== 'thinking' && k !== 'credit_allocation' && k !== 'referenced_message_ids' && k !== 'context_shift')
|
|
306
|
+
.map(([k, v]) => `${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`)
|
|
307
|
+
.join('\n'));
|
|
308
|
+
const fromField = extractCreditAllocationFromParsed(parsed);
|
|
309
|
+
const fromResponse = extractAllocation(response);
|
|
310
|
+
const creditAllocation = Object.keys(fromField).length > 0 ? fromField : fromResponse;
|
|
311
|
+
return {
|
|
312
|
+
thinking: typeof parsed.thinking === 'string' ? parsed.thinking : '',
|
|
313
|
+
response,
|
|
314
|
+
referencedIds: Array.isArray(parsed.referenced_message_ids)
|
|
315
|
+
? parsed.referenced_message_ids.filter((id) => typeof id === 'string')
|
|
316
|
+
: [],
|
|
317
|
+
creditAllocation,
|
|
318
|
+
contextShift: extractContextShift(parsed),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// partial stream — continue below
|
|
323
|
+
}
|
|
324
|
+
let thinking = '';
|
|
325
|
+
let response = '';
|
|
326
|
+
let referencedIds = [];
|
|
327
|
+
const thinkingMatch = jsonStr.match(/"thinking"\s*:\s*"(|.*?[^\\])"/);
|
|
328
|
+
if (thinkingMatch) {
|
|
329
|
+
thinking = thinkingMatch[1];
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const thinkingStartMatch = jsonStr.match(/"thinking"\s*:\s*"([\s\S]*)/);
|
|
333
|
+
if (thinkingStartMatch)
|
|
334
|
+
thinking = thinkingStartMatch[1];
|
|
335
|
+
}
|
|
336
|
+
const responseMatch = jsonStr.match(/"response"\s*:\s*"(|.*?[^\\])"/);
|
|
337
|
+
if (responseMatch) {
|
|
338
|
+
response = responseMatch[1];
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const responseStartMatch = jsonStr.match(/"response"\s*:\s*"([\s\S]*)/);
|
|
342
|
+
if (responseStartMatch)
|
|
343
|
+
response = responseStartMatch[1];
|
|
344
|
+
}
|
|
345
|
+
referencedIds = extractReferencedIds(jsonStr);
|
|
346
|
+
const unescapeStr = (str) => str
|
|
347
|
+
.replace(/\\"/g, '"')
|
|
348
|
+
.replace(/\\n/g, '\n')
|
|
349
|
+
.replace(/\\t/g, '\t')
|
|
350
|
+
.replace(/\\\\/g, '\\');
|
|
351
|
+
const unescapedResponse = unescapeStr(response);
|
|
352
|
+
const creditFromPartial = extractCreditAllocationFromParsed(tryParseObject(jsonStr) ?? {});
|
|
353
|
+
const partialObj = tryParseObject(jsonStr) ?? {};
|
|
354
|
+
let contextShift = 'continue';
|
|
355
|
+
if (partialObj.context_shift === 'new_topic' || partialObj.context_shift === 'continue') {
|
|
356
|
+
contextShift = partialObj.context_shift;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
thinking: unescapeStr(thinking),
|
|
360
|
+
response: unescapedResponse,
|
|
361
|
+
referencedIds,
|
|
362
|
+
creditAllocation: Object.keys(creditFromPartial).length > 0
|
|
363
|
+
? creditFromPartial
|
|
364
|
+
: extractAllocation(unescapedResponse),
|
|
365
|
+
contextShift,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function tryParseObject(jsonStr) {
|
|
369
|
+
try {
|
|
370
|
+
const parsed = JSON.parse(jsonStr);
|
|
371
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
372
|
+
? parsed
|
|
373
|
+
: null;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
/** Search credit_allocation JSON field, response |||ALLOCATION||| block, then stream buffer. */
|
|
380
|
+
export function extractAllocationFromSources(responseText, streamBuffer, parsedCreditAllocation) {
|
|
381
|
+
if (parsedCreditAllocation && Object.keys(parsedCreditAllocation).length > 0) {
|
|
382
|
+
return parsedCreditAllocation;
|
|
383
|
+
}
|
|
384
|
+
const fromResponse = extractAllocation(responseText);
|
|
385
|
+
if (Object.keys(fromResponse).length > 0)
|
|
386
|
+
return fromResponse;
|
|
387
|
+
const parsedBuffer = tryParseObject(streamBuffer);
|
|
388
|
+
if (parsedBuffer) {
|
|
389
|
+
const fromField = extractCreditAllocationFromParsed(parsedBuffer);
|
|
390
|
+
if (Object.keys(fromField).length > 0)
|
|
391
|
+
return fromField;
|
|
392
|
+
}
|
|
393
|
+
const fromBuffer = extractAllocation(streamBuffer);
|
|
394
|
+
if (Object.keys(fromBuffer).length > 0)
|
|
395
|
+
return fromBuffer;
|
|
396
|
+
// Partial stream: credit_allocation object before closing brace
|
|
397
|
+
const partialField = streamBuffer.match(/"credit_allocation"\s*:\s*(\{[\s\S]*?\})/);
|
|
398
|
+
if (partialField) {
|
|
399
|
+
const parsed = parseAllocationJson(partialField[1]);
|
|
400
|
+
if (Object.keys(parsed).length > 0)
|
|
401
|
+
return parsed;
|
|
402
|
+
}
|
|
403
|
+
return {};
|
|
404
|
+
}
|
|
405
|
+
export function sanitizeContent(content) {
|
|
406
|
+
let sanitized = content.replace(/\|\|\|ALLOCATION:[\s\S]*?\|\|\|/g, '');
|
|
407
|
+
sanitized = sanitized.replace(/\|\|\|(?:ALLOCATION:[\s\S]*)?$/, '');
|
|
408
|
+
return sanitized.trim();
|
|
409
|
+
}
|
|
410
|
+
export function deriveMemoryStatus(id, referencedIds, creditsApplied, forgetScore, decayApplied, forceDecayingIds) {
|
|
411
|
+
if (forceDecayingIds?.has(id) && decayApplied > 0)
|
|
412
|
+
return 'DECAYING';
|
|
413
|
+
if (creditsApplied > 0)
|
|
414
|
+
return 'SHIELDED';
|
|
415
|
+
if (referencedIds.includes(id))
|
|
416
|
+
return 'CITED';
|
|
417
|
+
if (forgetScore >= 1.0)
|
|
418
|
+
return 'EVICTED';
|
|
419
|
+
if (forgetScore >= 0.75)
|
|
420
|
+
return 'DANGER';
|
|
421
|
+
if (decayApplied > 0)
|
|
422
|
+
return 'DECAYING';
|
|
423
|
+
return 'FRESH';
|
|
424
|
+
}
|
|
425
|
+
export function buildTurnMemorySnapshot(params) {
|
|
426
|
+
const { turnBudget, contextShift, creditsConsumed, agentAllocation, effectiveAllocation, usedFallback, scaledAllocation, scale, totalRequested, referencedIds, messages, estimateTokens, forceDecayingIds, } = params;
|
|
427
|
+
const rows = messages
|
|
428
|
+
.filter((m) => isScorableMessage(m))
|
|
429
|
+
.map((m) => {
|
|
430
|
+
const tokens = m.tokenCount ?? estimateTokens(m.content);
|
|
431
|
+
const forgetScore = m.forgetScore ?? 0;
|
|
432
|
+
const decayApplied = m.decayApplied ?? 0;
|
|
433
|
+
const agentPercentRaw = agentAllocation[m.id] ?? 0;
|
|
434
|
+
const agentPercentNorm = effectiveAllocation[m.id] ?? 0;
|
|
435
|
+
const creditsApplied = scaledAllocation[m.id] ?? 0;
|
|
436
|
+
const budgetSharePercent = agentPercentNorm;
|
|
437
|
+
return {
|
|
438
|
+
id: m.id,
|
|
439
|
+
role: m.role,
|
|
440
|
+
tokens,
|
|
441
|
+
forgetScore,
|
|
442
|
+
decayApplied,
|
|
443
|
+
creditsRequested: agentPercentRaw,
|
|
444
|
+
creditsApplied,
|
|
445
|
+
relevanceWeight: agentPercentRaw,
|
|
446
|
+
agentPercentNormalized: agentPercentNorm,
|
|
447
|
+
budgetSharePercent,
|
|
448
|
+
status: deriveMemoryStatus(m.id, referencedIds, creditsApplied, forgetScore, decayApplied, forceDecayingIds),
|
|
449
|
+
};
|
|
450
|
+
});
|
|
451
|
+
return {
|
|
452
|
+
turnBudget,
|
|
453
|
+
contextShift,
|
|
454
|
+
creditsConsumed,
|
|
455
|
+
agentAllocation,
|
|
456
|
+
effectiveAllocation,
|
|
457
|
+
usedFallback,
|
|
458
|
+
scaledAllocation,
|
|
459
|
+
totalRequested,
|
|
460
|
+
scaleApplied: scale,
|
|
461
|
+
invalidAgentIds: params.invalidAgentIds ?? [],
|
|
462
|
+
rows,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/** Turn agent relevance percentages into final credits for this round. */
|
|
466
|
+
export function resolveTurnCredits(params) {
|
|
467
|
+
const { contextShift, agentWeights, referencedIds, budget, activeIds, activeTurnId } = params;
|
|
468
|
+
if (contextShift === 'new_topic' || budget <= 0) {
|
|
469
|
+
return {
|
|
470
|
+
agentAllocation: {},
|
|
471
|
+
effectiveAllocation: {},
|
|
472
|
+
scaledAllocation: {},
|
|
473
|
+
scale: 1,
|
|
474
|
+
totalRequested: 0,
|
|
475
|
+
usedFallback: false,
|
|
476
|
+
creditsConsumed: 0,
|
|
477
|
+
invalidAgentIds: [],
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const { canonical, invalidIds: canonInvalid } = canonicalizeMessageIds(agentWeights, activeIds);
|
|
481
|
+
const numberMap = buildMsgNumberToIdMap(activeIds);
|
|
482
|
+
const citedCanonical = referencedIds
|
|
483
|
+
.map((id) => resolveMessageId(id, activeIds, numberMap))
|
|
484
|
+
.filter((id) => id != null);
|
|
485
|
+
const invalidIds = [...canonInvalid];
|
|
486
|
+
// Exclude activeTurnId and referencedIds (citedCanonical) from receiving credits
|
|
487
|
+
const creditEligibleWeights = {};
|
|
488
|
+
for (const [id, weight] of Object.entries(canonical)) {
|
|
489
|
+
if (id !== activeTurnId && !citedCanonical.includes(id)) {
|
|
490
|
+
creditEligibleWeights[id] = weight;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const { normalizedPercents, credits, rawSum, invalidIds: normInvalid, } = normalizePercentagesToBudget(creditEligibleWeights, budget, activeIds);
|
|
494
|
+
invalidIds.push(...normInvalid);
|
|
495
|
+
if (rawSum > 0) {
|
|
496
|
+
const consumed = Object.values(credits).reduce((a, b) => a + b, 0);
|
|
497
|
+
return {
|
|
498
|
+
agentAllocation: canonical,
|
|
499
|
+
effectiveAllocation: normalizedPercents,
|
|
500
|
+
scaledAllocation: credits,
|
|
501
|
+
scale: 1,
|
|
502
|
+
totalRequested: rawSum,
|
|
503
|
+
usedFallback: false,
|
|
504
|
+
creditsConsumed: consumed,
|
|
505
|
+
invalidAgentIds: invalidIds,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
agentAllocation: {},
|
|
510
|
+
effectiveAllocation: {},
|
|
511
|
+
scaledAllocation: {},
|
|
512
|
+
scale: 1,
|
|
513
|
+
totalRequested: 0,
|
|
514
|
+
usedFallback: false,
|
|
515
|
+
creditsConsumed: 0,
|
|
516
|
+
invalidAgentIds: invalidIds,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
export function scaleAllocation(allocation, budget, activeIds) {
|
|
520
|
+
const validAllocation = {};
|
|
521
|
+
let totalRequested = 0;
|
|
522
|
+
for (const id in allocation) {
|
|
523
|
+
if (activeIds.has(id)) {
|
|
524
|
+
validAllocation[id] = allocation[id];
|
|
525
|
+
totalRequested += allocation[id];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
let scale = 1;
|
|
529
|
+
if (totalRequested > budget && budget > 0) {
|
|
530
|
+
scale = budget / totalRequested;
|
|
531
|
+
}
|
|
532
|
+
const scaledAllocation = {};
|
|
533
|
+
for (const id in validAllocation) {
|
|
534
|
+
scaledAllocation[id] = Number((validAllocation[id] * scale).toFixed(2));
|
|
535
|
+
}
|
|
536
|
+
return { validAllocation, scaledAllocation, scale, totalRequested };
|
|
537
|
+
}
|
|
538
|
+
export function extractHydrationBeaconVariables(messages, query) {
|
|
539
|
+
let pgPool = '';
|
|
540
|
+
let redisSentinel = '';
|
|
541
|
+
let otelCollector = '';
|
|
542
|
+
let maxOldSpace = '';
|
|
543
|
+
for (const m of messages) {
|
|
544
|
+
const textSources = [];
|
|
545
|
+
if (m.content) {
|
|
546
|
+
textSources.push(m.content);
|
|
547
|
+
}
|
|
548
|
+
if (m.chunks) {
|
|
549
|
+
for (const chunk of m.chunks) {
|
|
550
|
+
if (chunk.content) {
|
|
551
|
+
textSources.push(chunk.content);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
for (const text of textSources) {
|
|
556
|
+
// PG_POOL_SIZE
|
|
557
|
+
const pgMatch = text.match(/PG_POOL_SIZE\s*=\s*(\d+)/i);
|
|
558
|
+
if (pgMatch && !pgPool)
|
|
559
|
+
pgPool = pgMatch[1];
|
|
560
|
+
// Redis sentinel
|
|
561
|
+
const redisMatch = text.match(/sentinel-\d+\.redis\.internal:\d+/i) || text.match(/sentinel-01\.redis\.internal/i);
|
|
562
|
+
if (redisMatch && !redisSentinel)
|
|
563
|
+
redisSentinel = 'sentinel-01.redis.internal:26379';
|
|
564
|
+
// OTEL image or port
|
|
565
|
+
if (text.toLowerCase().includes('opentelemetry') || text.toLowerCase().includes('otel')) {
|
|
566
|
+
const imgMatch = text.match(/(otel\/opentelemetry-collector-contrib:\d+\.\d+\.\d+)/i);
|
|
567
|
+
if (imgMatch && !otelCollector)
|
|
568
|
+
otelCollector = imgMatch[1];
|
|
569
|
+
}
|
|
570
|
+
// max-old-space-size
|
|
571
|
+
const spaceMatch = text.match(/--max-old-space-size\s*=\s*(\d+)/i);
|
|
572
|
+
if (spaceMatch && !maxOldSpace)
|
|
573
|
+
maxOldSpace = spaceMatch[1];
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const lines = [];
|
|
577
|
+
if (pgPool)
|
|
578
|
+
lines.push(`- PG_POOL_SIZE: ${pgPool} (Original parameter specified by user)`);
|
|
579
|
+
if (redisSentinel)
|
|
580
|
+
lines.push(`- Redis Sentinel Node: ${redisSentinel}`);
|
|
581
|
+
if (otelCollector)
|
|
582
|
+
lines.push(`- OTEL Collector Image: ${otelCollector}`);
|
|
583
|
+
if (maxOldSpace)
|
|
584
|
+
lines.push(`- Node.js Heap Parameter: --max-old-space-size=${maxOldSpace}`);
|
|
585
|
+
let result = '';
|
|
586
|
+
if (lines.length > 0) {
|
|
587
|
+
result += `[🚨 SYSTEM HYDRATION BEACON]
|
|
588
|
+
The user is recalling archived session configurations. The following exact parameters have been extracted from Long-Term Memory (disk backups) and pinned for accuracy:
|
|
589
|
+
${lines.join('\n')}
|
|
590
|
+
DO NOT guess, assume, or substitute these parameters with generic defaults.`;
|
|
591
|
+
}
|
|
592
|
+
// Handle archived frequency counts if query asks for counts/frequencies
|
|
593
|
+
if (query) {
|
|
594
|
+
const queryLower = query.toLowerCase();
|
|
595
|
+
const needsFreq = queryLower.includes('frequent') ||
|
|
596
|
+
queryLower.includes('frequency') ||
|
|
597
|
+
queryLower.includes('count') ||
|
|
598
|
+
queryLower.includes('occurrences') ||
|
|
599
|
+
queryLower.includes('how many');
|
|
600
|
+
if (needsFreq) {
|
|
601
|
+
const stopWords = new Set([
|
|
602
|
+
'the', 'and', 'of', 'to', 'in', 'is', 'was', 'we', 'for', 'with', 'a', 'an', 'on', 'at',
|
|
603
|
+
'by', 'this', 'that', 'it', 'from', 'as', 'are', 'were', 'or', 'but', 'not', 'your', 'our',
|
|
604
|
+
'this', 'these', 'those', 'they', 'them', 'their', 'he', 'she', 'him', 'her', 'his', 'its',
|
|
605
|
+
'have', 'has', 'had', 'do', 'does', 'did', 'been', 'being', 'be', 'will', 'would', 'should',
|
|
606
|
+
'can', 'could', 'about', 'out', 'up', 'down', 'into', 'over', 'under', 'again', 'further',
|
|
607
|
+
'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each',
|
|
608
|
+
'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'only', 'own', 'same', 'so',
|
|
609
|
+
'than', 'too', 'very', 'just', 'now', 'during', 'stock', 'count', 'verified', 'fresh',
|
|
610
|
+
'batch', 'logged', 'inventory', 'database', 'shipping', 'container', 'arrived', 'organic',
|
|
611
|
+
'crates', 'ready', 'customs', 'inspection', 'warehouse', 'logs', 'scanned', 'warehouse',
|
|
612
|
+
'inventory', 'storage', 'systems', 'management', 'system', 'documentation', 'dump',
|
|
613
|
+
'read', 'carefully', 'document', 'intake', 'complete', 'acknowledge', 'outline',
|
|
614
|
+
'typical', 'microservice', 'architecture', 'trace', 'validation', 'receipt', 'sequential',
|
|
615
|
+
'runtime', 'configuration', 'commands', 'executed', 'variables', 'scoping', 'rules',
|
|
616
|
+
'analysis', 'analyze', 'frequent', 'frequency', 'occurrences', 'occurrence', 'sentences',
|
|
617
|
+
'below', 'scan', 'word', 'name', 'along', 'respond', 'only', 'json', 'format', 'fruit',
|
|
618
|
+
'fruitname', 'list'
|
|
619
|
+
]);
|
|
620
|
+
const freqMap = {};
|
|
621
|
+
for (const m of messages) {
|
|
622
|
+
if (m.chunks) {
|
|
623
|
+
for (const chunk of m.chunks) {
|
|
624
|
+
if (chunk.content) {
|
|
625
|
+
const words = chunk.content.toLowerCase().match(/\b[a-z]{3,}\b/g) || [];
|
|
626
|
+
for (const w of words) {
|
|
627
|
+
if (!stopWords.has(w)) {
|
|
628
|
+
freqMap[w] = (freqMap[w] ?? 0) + 1;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
const sorted = Object.entries(freqMap)
|
|
636
|
+
.filter(([, count]) => count >= 2)
|
|
637
|
+
.sort((a, b) => b[1] - a[1])
|
|
638
|
+
.slice(0, 15);
|
|
639
|
+
if (sorted.length > 0) {
|
|
640
|
+
const frequencySummary = `[🚨 GLOBAL FREQUENCY SUMMARY]
|
|
641
|
+
The following frequency counts were aggregated from the scanned document:
|
|
642
|
+
${sorted.map(([word, count]) => `- ${word}: ${count}`).join('\n')}`;
|
|
643
|
+
if (result) {
|
|
644
|
+
result += '\n\n' + frequencySummary;
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
result = frequencySummary;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return result;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Pre-flight speculative hydration scanner.
|
|
656
|
+
* Scans the user query for keywords that suggest a recall of archived data,
|
|
657
|
+
* then returns the list of long-term memory IDs to hydrate.
|
|
658
|
+
*
|
|
659
|
+
* This runs client-side *before* the LLM call to eliminate the "double query"
|
|
660
|
+
* penalty (where the model would otherwise need to call remind() then re-run).
|
|
661
|
+
*/
|
|
662
|
+
export async function preFlightSpeculativeScan(userPrompt, longTermMemory) {
|
|
663
|
+
if (Object.keys(longTermMemory).length === 0)
|
|
664
|
+
return [];
|
|
665
|
+
const queryLower = userPrompt.toLowerCase();
|
|
666
|
+
// 0. High-speed surgical check for Phase 1 / initial setup config summary queries
|
|
667
|
+
if (queryLower.includes('phase 1') || queryLower.includes('infrastructure configuration') || queryLower.includes('infrastructure baseline')) {
|
|
668
|
+
const phase1Ids = ['msg_01', 'msg_02', 'msg_03', 'msg_04', 'msg_05', 'msg_06', 'msg_07', 'msg_08', 'msg_09', 'msg_10'];
|
|
669
|
+
const targets = phase1Ids.filter((id) => longTermMemory[id] !== undefined);
|
|
670
|
+
if (targets.length > 0) {
|
|
671
|
+
return targets;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// 1. Try vector cache search first
|
|
675
|
+
try {
|
|
676
|
+
const vectorMatches = await queryVectorCache(userPrompt, 5);
|
|
677
|
+
const validMatches = vectorMatches.filter((id) => longTermMemory[id] !== undefined);
|
|
678
|
+
if (validMatches.length > 0) {
|
|
679
|
+
return validMatches.slice(0, 5);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
// Vector search failed, proceed to keyword scanner
|
|
684
|
+
}
|
|
685
|
+
// 2. Keyword scanner fallback
|
|
686
|
+
// Technical recall patterns — keywords from DevOps logs, configs, etc.
|
|
687
|
+
const recallPatterns = [
|
|
688
|
+
/\bpg_pool\b/i,
|
|
689
|
+
/\bconnection\s+drops?\b/i,
|
|
690
|
+
/\bdocker[\s-]?compose\b/i,
|
|
691
|
+
/\botel\b/i,
|
|
692
|
+
/\bmemory\s+limits?\b/i,
|
|
693
|
+
/\bget\s+back\s+to\b/i,
|
|
694
|
+
/\brecall\b/i,
|
|
695
|
+
/\bearlier\b/i,
|
|
696
|
+
/\bbeginning\b/i,
|
|
697
|
+
/\bprevious\b/i,
|
|
698
|
+
/\bsentinel\b/i,
|
|
699
|
+
/\bmax[\s-]old[\s-]space\b/i,
|
|
700
|
+
/\bheap\b/i,
|
|
701
|
+
/\bcollector\b/i,
|
|
702
|
+
];
|
|
703
|
+
const isRecallQuery = recallPatterns.some((p) => p.test(queryLower));
|
|
704
|
+
if (!isRecallQuery)
|
|
705
|
+
return [];
|
|
706
|
+
const matchedIds = [];
|
|
707
|
+
for (const [id, msg] of Object.entries(longTermMemory)) {
|
|
708
|
+
const contentLower = msg.content.toLowerCase();
|
|
709
|
+
const summaryLower = (msg.archiveSummary || '').toLowerCase();
|
|
710
|
+
// Check direct substring matches for key technical terms
|
|
711
|
+
const hasMatch = ((queryLower.includes('pool') && (contentLower.includes('pool') || summaryLower.includes('pool'))) ||
|
|
712
|
+
(queryLower.includes('sentinel') && (contentLower.includes('sentinel') || summaryLower.includes('sentinel'))) ||
|
|
713
|
+
(queryLower.includes('redis') && (contentLower.includes('redis') || summaryLower.includes('redis'))) ||
|
|
714
|
+
(queryLower.includes('otel') && (contentLower.includes('otel') || summaryLower.includes('otel'))) ||
|
|
715
|
+
(queryLower.includes('collector') && (contentLower.includes('collector') || summaryLower.includes('collector'))) ||
|
|
716
|
+
(queryLower.includes('max-old-space') && (contentLower.includes('max-old-space') || summaryLower.includes('max-old-space'))) ||
|
|
717
|
+
(queryLower.includes('old-space') && (contentLower.includes('old-space') || summaryLower.includes('old-space'))) ||
|
|
718
|
+
(queryLower.includes('heap') && (contentLower.includes('heap') || summaryLower.includes('heap'))) ||
|
|
719
|
+
(queryLower.includes('memory') && (contentLower.includes('memory') || summaryLower.includes('memory'))) ||
|
|
720
|
+
(queryLower.includes('kafka') && (contentLower.includes('kafka') || summaryLower.includes('kafka'))) ||
|
|
721
|
+
(queryLower.includes('sasl') && (contentLower.includes('sasl') || summaryLower.includes('sasl'))));
|
|
722
|
+
if (hasMatch) {
|
|
723
|
+
matchedIds.push(id);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// Fallback: if no surgical matches found but it IS a recall query, hydrate the oldest turns (Phase 1 configs!)
|
|
727
|
+
if (matchedIds.length === 0) {
|
|
728
|
+
const phase1Ids = ['msg_01', 'msg_02', 'msg_03', 'msg_04', 'msg_05', 'msg_06', 'msg_07', 'msg_08', 'msg_09', 'msg_10'];
|
|
729
|
+
return phase1Ids.filter(id => longTermMemory[id] !== undefined).slice(0, 5);
|
|
730
|
+
}
|
|
731
|
+
return matchedIds.slice(0, 5);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Build an attention anchor beacon from hydrated long-term memory messages.
|
|
735
|
+
* Combines speculative hydration with the extraction of exact variable values
|
|
736
|
+
* to produce a pinned system block that guides the LLM's attention to the
|
|
737
|
+
* correct parameters.
|
|
738
|
+
*
|
|
739
|
+
* Returns empty string if no variables were found.
|
|
740
|
+
*/
|
|
741
|
+
export function buildAttentionAnchorBeacon(hydratedMsgs) {
|
|
742
|
+
return extractHydrationBeaconVariables(hydratedMsgs);
|
|
743
|
+
}
|