@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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/agent/createLfsAgent.d.ts +27 -0
  4. package/dist/agent/createLfsAgent.d.ts.map +1 -0
  5. package/dist/agent/createLfsAgent.js +51 -0
  6. package/dist/agent/loopConfig.d.ts +6 -0
  7. package/dist/agent/loopConfig.d.ts.map +1 -0
  8. package/dist/agent/loopConfig.js +5 -0
  9. package/dist/agent/runLfsTurn.d.ts +10 -0
  10. package/dist/agent/runLfsTurn.d.ts.map +1 -0
  11. package/dist/agent/runLfsTurn.js +20 -0
  12. package/dist/history/dorycodeAdapter.d.ts +16 -0
  13. package/dist/history/dorycodeAdapter.d.ts.map +1 -0
  14. package/dist/history/dorycodeAdapter.js +18 -0
  15. package/dist/history/formatForModel.d.ts +19 -0
  16. package/dist/history/formatForModel.d.ts.map +1 -0
  17. package/dist/history/formatForModel.js +22 -0
  18. package/dist/history/lanes.d.ts +3 -0
  19. package/dist/history/lanes.d.ts.map +1 -0
  20. package/dist/history/lanes.js +3 -0
  21. package/dist/history/messageManifest.d.ts +45 -0
  22. package/dist/history/messageManifest.d.ts.map +1 -0
  23. package/dist/history/messageManifest.js +231 -0
  24. package/dist/index.d.ts +33 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +22 -0
  27. package/dist/lfs/adaptiveCoefficients.d.ts +13 -0
  28. package/dist/lfs/adaptiveCoefficients.d.ts.map +1 -0
  29. package/dist/lfs/adaptiveCoefficients.js +16 -0
  30. package/dist/lfs/attenuationCompiler.d.ts +16 -0
  31. package/dist/lfs/attenuationCompiler.d.ts.map +1 -0
  32. package/dist/lfs/attenuationCompiler.js +123 -0
  33. package/dist/lfs/chunkManager.d.ts +6 -0
  34. package/dist/lfs/chunkManager.d.ts.map +1 -0
  35. package/dist/lfs/chunkManager.js +142 -0
  36. package/dist/lfs/config.d.ts +23 -0
  37. package/dist/lfs/config.d.ts.map +1 -0
  38. package/dist/lfs/config.js +34 -0
  39. package/dist/lfs/contextAdapter.d.ts +5 -0
  40. package/dist/lfs/contextAdapter.d.ts.map +1 -0
  41. package/dist/lfs/contextAdapter.js +161 -0
  42. package/dist/lfs/memoryEngine.d.ts +108 -0
  43. package/dist/lfs/memoryEngine.d.ts.map +1 -0
  44. package/dist/lfs/memoryEngine.js +322 -0
  45. package/dist/lfs/memoryParser.d.ts +121 -0
  46. package/dist/lfs/memoryParser.d.ts.map +1 -0
  47. package/dist/lfs/memoryParser.js +743 -0
  48. package/dist/lfs/paritySwapper.d.ts +20 -0
  49. package/dist/lfs/paritySwapper.d.ts.map +1 -0
  50. package/dist/lfs/paritySwapper.js +317 -0
  51. package/dist/lfs/preflightRouter.d.ts +14 -0
  52. package/dist/lfs/preflightRouter.d.ts.map +1 -0
  53. package/dist/lfs/preflightRouter.js +24 -0
  54. package/dist/lfs/runtimeConfig.d.ts +31 -0
  55. package/dist/lfs/runtimeConfig.d.ts.map +1 -0
  56. package/dist/lfs/runtimeConfig.js +40 -0
  57. package/dist/lfs/sessionStore.d.ts +14 -0
  58. package/dist/lfs/sessionStore.d.ts.map +1 -0
  59. package/dist/lfs/sessionStore.js +52 -0
  60. package/dist/lfs/tokenCounter.d.ts +90 -0
  61. package/dist/lfs/tokenCounter.d.ts.map +1 -0
  62. package/dist/lfs/tokenCounter.js +26 -0
  63. package/dist/lfs/types.d.ts +44 -0
  64. package/dist/lfs/types.d.ts.map +1 -0
  65. package/dist/lfs/types.js +1 -0
  66. package/dist/lfs/vectorDb.d.ts +19 -0
  67. package/dist/lfs/vectorDb.d.ts.map +1 -0
  68. package/dist/lfs/vectorDb.js +158 -0
  69. package/dist/mcp/client.d.ts +23 -0
  70. package/dist/mcp/client.d.ts.map +1 -0
  71. package/dist/mcp/client.js +60 -0
  72. package/dist/mcp/config.d.ts +10 -0
  73. package/dist/mcp/config.d.ts.map +1 -0
  74. package/dist/mcp/config.js +72 -0
  75. package/dist/providers/ollama.d.ts +2 -0
  76. package/dist/providers/ollama.d.ts.map +1 -0
  77. package/dist/providers/ollama.js +7 -0
  78. package/dist/providers/registry.d.ts +5 -0
  79. package/dist/providers/registry.d.ts.map +1 -0
  80. package/dist/providers/registry.js +9 -0
  81. package/dist/tools/registry.d.ts +14 -0
  82. package/dist/tools/registry.d.ts.map +1 -0
  83. package/dist/tools/registry.js +26 -0
  84. package/dist/tools/remind.d.ts +15 -0
  85. package/dist/tools/remind.d.ts.map +1 -0
  86. package/dist/tools/remind.js +53 -0
  87. package/dist/tools/skillLoader.d.ts +12 -0
  88. package/dist/tools/skillLoader.d.ts.map +1 -0
  89. package/dist/tools/skillLoader.js +38 -0
  90. 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
+ }