@beingmartinbmc/ojas 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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/aahar/index.d.ts +179 -0
  4. package/dist/aahar/index.d.ts.map +1 -0
  5. package/dist/aahar/index.js +657 -0
  6. package/dist/aahar/index.js.map +1 -0
  7. package/dist/aahar/scoring.d.ts +85 -0
  8. package/dist/aahar/scoring.d.ts.map +1 -0
  9. package/dist/aahar/scoring.js +268 -0
  10. package/dist/aahar/scoring.js.map +1 -0
  11. package/dist/agni/index.d.ts +113 -0
  12. package/dist/agni/index.d.ts.map +1 -0
  13. package/dist/agni/index.js +328 -0
  14. package/dist/agni/index.js.map +1 -0
  15. package/dist/agni/model-router.d.ts +77 -0
  16. package/dist/agni/model-router.d.ts.map +1 -0
  17. package/dist/agni/model-router.js +163 -0
  18. package/dist/agni/model-router.js.map +1 -0
  19. package/dist/agni/response-distiller.d.ts +37 -0
  20. package/dist/agni/response-distiller.d.ts.map +1 -0
  21. package/dist/agni/response-distiller.js +193 -0
  22. package/dist/agni/response-distiller.js.map +1 -0
  23. package/dist/agni/tiktoken-adapter.d.ts +55 -0
  24. package/dist/agni/tiktoken-adapter.d.ts.map +1 -0
  25. package/dist/agni/tiktoken-adapter.js +113 -0
  26. package/dist/agni/tiktoken-adapter.js.map +1 -0
  27. package/dist/chikitsa/index.d.ts +130 -0
  28. package/dist/chikitsa/index.d.ts.map +1 -0
  29. package/dist/chikitsa/index.js +565 -0
  30. package/dist/chikitsa/index.js.map +1 -0
  31. package/dist/demo.d.ts +15 -0
  32. package/dist/demo.d.ts.map +1 -0
  33. package/dist/demo.js +278 -0
  34. package/dist/demo.js.map +1 -0
  35. package/dist/index.d.ts +201 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +588 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/mcp/audit.d.ts +39 -0
  40. package/dist/mcp/audit.d.ts.map +1 -0
  41. package/dist/mcp/audit.js +73 -0
  42. package/dist/mcp/audit.js.map +1 -0
  43. package/dist/mcp/contracts.d.ts +76 -0
  44. package/dist/mcp/contracts.d.ts.map +1 -0
  45. package/dist/mcp/contracts.js +44 -0
  46. package/dist/mcp/contracts.js.map +1 -0
  47. package/dist/mcp/envelope.d.ts +107 -0
  48. package/dist/mcp/envelope.d.ts.map +1 -0
  49. package/dist/mcp/envelope.js +162 -0
  50. package/dist/mcp/envelope.js.map +1 -0
  51. package/dist/mcp/registry.d.ts +110 -0
  52. package/dist/mcp/registry.d.ts.map +1 -0
  53. package/dist/mcp/registry.js +258 -0
  54. package/dist/mcp/registry.js.map +1 -0
  55. package/dist/mcp/server.d.ts +26 -0
  56. package/dist/mcp/server.d.ts.map +1 -0
  57. package/dist/mcp/server.js +107 -0
  58. package/dist/mcp/server.js.map +1 -0
  59. package/dist/mcp/tools/agent.d.ts +4 -0
  60. package/dist/mcp/tools/agent.d.ts.map +1 -0
  61. package/dist/mcp/tools/agent.js +300 -0
  62. package/dist/mcp/tools/agent.js.map +1 -0
  63. package/dist/mcp/tools/context.d.ts +4 -0
  64. package/dist/mcp/tools/context.d.ts.map +1 -0
  65. package/dist/mcp/tools/context.js +261 -0
  66. package/dist/mcp/tools/context.js.map +1 -0
  67. package/dist/mcp/tools/index.d.ts +5 -0
  68. package/dist/mcp/tools/index.d.ts.map +1 -0
  69. package/dist/mcp/tools/index.js +20 -0
  70. package/dist/mcp/tools/index.js.map +1 -0
  71. package/dist/mcp/tools/memory.d.ts +4 -0
  72. package/dist/mcp/tools/memory.d.ts.map +1 -0
  73. package/dist/mcp/tools/memory.js +220 -0
  74. package/dist/mcp/tools/memory.js.map +1 -0
  75. package/dist/mcp/tools/output.d.ts +4 -0
  76. package/dist/mcp/tools/output.d.ts.map +1 -0
  77. package/dist/mcp/tools/output.js +206 -0
  78. package/dist/mcp/tools/output.js.map +1 -0
  79. package/dist/mcp/tools/recovery.d.ts +4 -0
  80. package/dist/mcp/tools/recovery.d.ts.map +1 -0
  81. package/dist/mcp/tools/recovery.js +165 -0
  82. package/dist/mcp/tools/recovery.js.map +1 -0
  83. package/dist/mcp/tools/registrar.d.ts +4 -0
  84. package/dist/mcp/tools/registrar.d.ts.map +1 -0
  85. package/dist/mcp/tools/registrar.js +17 -0
  86. package/dist/mcp/tools/registrar.js.map +1 -0
  87. package/dist/mcp/tools/report.d.ts +4 -0
  88. package/dist/mcp/tools/report.d.ts.map +1 -0
  89. package/dist/mcp/tools/report.js +68 -0
  90. package/dist/mcp/tools/report.js.map +1 -0
  91. package/dist/mcp/tools/shared.d.ts +37 -0
  92. package/dist/mcp/tools/shared.d.ts.map +1 -0
  93. package/dist/mcp/tools/shared.js +214 -0
  94. package/dist/mcp/tools/shared.js.map +1 -0
  95. package/dist/mcp/trace.d.ts +47 -0
  96. package/dist/mcp/trace.d.ts.map +1 -0
  97. package/dist/mcp/trace.js +216 -0
  98. package/dist/mcp/trace.js.map +1 -0
  99. package/dist/nidra/index.d.ts +275 -0
  100. package/dist/nidra/index.d.ts.map +1 -0
  101. package/dist/nidra/index.js +889 -0
  102. package/dist/nidra/index.js.map +1 -0
  103. package/dist/persistence/migrations.d.ts +10 -0
  104. package/dist/persistence/migrations.d.ts.map +1 -0
  105. package/dist/persistence/migrations.js +77 -0
  106. package/dist/persistence/migrations.js.map +1 -0
  107. package/dist/persistence/sqlite.d.ts +30 -0
  108. package/dist/persistence/sqlite.d.ts.map +1 -0
  109. package/dist/persistence/sqlite.js +209 -0
  110. package/dist/persistence/sqlite.js.map +1 -0
  111. package/dist/persistence/types.d.ts +104 -0
  112. package/dist/persistence/types.d.ts.map +1 -0
  113. package/dist/persistence/types.js +5 -0
  114. package/dist/persistence/types.js.map +1 -0
  115. package/dist/pulse/index.d.ts +144 -0
  116. package/dist/pulse/index.d.ts.map +1 -0
  117. package/dist/pulse/index.js +453 -0
  118. package/dist/pulse/index.js.map +1 -0
  119. package/dist/raksha/classifiers/http-classifier.d.ts +26 -0
  120. package/dist/raksha/classifiers/http-classifier.d.ts.map +1 -0
  121. package/dist/raksha/classifiers/http-classifier.js +62 -0
  122. package/dist/raksha/classifiers/http-classifier.js.map +1 -0
  123. package/dist/raksha/classifiers/index.d.ts +5 -0
  124. package/dist/raksha/classifiers/index.d.ts.map +1 -0
  125. package/dist/raksha/classifiers/index.js +8 -0
  126. package/dist/raksha/classifiers/index.js.map +1 -0
  127. package/dist/raksha/classifiers/onnx-classifier.d.ts +41 -0
  128. package/dist/raksha/classifiers/onnx-classifier.d.ts.map +1 -0
  129. package/dist/raksha/classifiers/onnx-classifier.js +99 -0
  130. package/dist/raksha/classifiers/onnx-classifier.js.map +1 -0
  131. package/dist/raksha/hallucination-detectors.d.ts +106 -0
  132. package/dist/raksha/hallucination-detectors.d.ts.map +1 -0
  133. package/dist/raksha/hallucination-detectors.js +327 -0
  134. package/dist/raksha/hallucination-detectors.js.map +1 -0
  135. package/dist/raksha/index.d.ts +168 -0
  136. package/dist/raksha/index.d.ts.map +1 -0
  137. package/dist/raksha/index.js +597 -0
  138. package/dist/raksha/index.js.map +1 -0
  139. package/dist/raksha/prompt-injection-detectors.d.ts +30 -0
  140. package/dist/raksha/prompt-injection-detectors.d.ts.map +1 -0
  141. package/dist/raksha/prompt-injection-detectors.js +153 -0
  142. package/dist/raksha/prompt-injection-detectors.js.map +1 -0
  143. package/dist/types.d.ts +1115 -0
  144. package/dist/types.d.ts.map +1 -0
  145. package/dist/types.js +71 -0
  146. package/dist/types.js.map +1 -0
  147. package/dist/util/calibration.d.ts +32 -0
  148. package/dist/util/calibration.d.ts.map +1 -0
  149. package/dist/util/calibration.js +108 -0
  150. package/dist/util/calibration.js.map +1 -0
  151. package/dist/util/id.d.ts +2 -0
  152. package/dist/util/id.d.ts.map +1 -0
  153. package/dist/util/id.js +9 -0
  154. package/dist/util/id.js.map +1 -0
  155. package/dist/vyayam/index.d.ts +76 -0
  156. package/dist/vyayam/index.d.ts.map +1 -0
  157. package/dist/vyayam/index.js +528 -0
  158. package/dist/vyayam/index.js.map +1 -0
  159. package/dist/vyayam/tool-fault-proxy.d.ts +95 -0
  160. package/dist/vyayam/tool-fault-proxy.d.ts.map +1 -0
  161. package/dist/vyayam/tool-fault-proxy.js +170 -0
  162. package/dist/vyayam/tool-fault-proxy.js.map +1 -0
  163. package/docs/ARCHITECTURE.md +162 -0
  164. package/docs/BACKLOG.md +342 -0
  165. package/docs/CONFIGURATION.md +305 -0
  166. package/docs/EVIDENCE.md +232 -0
  167. package/docs/EVIDENCE_MATRIX.md +293 -0
  168. package/docs/KNOWN_FAILURES.md +367 -0
  169. package/docs/MCP.md +614 -0
  170. package/docs/MODULES.md +368 -0
  171. package/docs/SECURITY.md +251 -0
  172. package/docs/TRUST.md +88 -0
  173. package/docs/assets/ojas-hero.png +0 -0
  174. package/package.json +101 -0
@@ -0,0 +1,657 @@
1
+ "use strict";
2
+ /**
3
+ * Ojas Aahar (ओजस आहार) — AI Cognitive Nutrition System
4
+ *
5
+ * Governs what an AI agent cognitively consumes.
6
+ * Maintains context quality, cognitive load regulation,
7
+ * and runtime attention optimization.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.Aahar = void 0;
11
+ const types_1 = require("../types");
12
+ const scoring_1 = require("./scoring");
13
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
14
+ function now() {
15
+ return new Date().toISOString();
16
+ }
17
+ function healthScore(value, source) {
18
+ return { value: clamp(value), timestamp: now(), source };
19
+ }
20
+ function clamp(v, min = 0, max = 1) {
21
+ return Math.max(min, Math.min(max, v));
22
+ }
23
+ // ─── Aahar Engine ────────────────────────────────────────────────────────────
24
+ class Aahar {
25
+ policy;
26
+ history = [];
27
+ /**
28
+ * Per-source retrieval tally. Driven by `recordRetrieval(itemId)` —
29
+ * when the agent re-fetches an item that Aahar previously rejected,
30
+ * the source's count rises. The next call to `filter()` softens the
31
+ * relevance threshold for that source so we stop rejecting items
32
+ * the agent keeps asking for. Self-tunes from observed retrieval
33
+ * pressure.
34
+ */
35
+ retrievalPressureBySource = new Map();
36
+ /**
37
+ * Map of item id → source recorded at filter time, so a later
38
+ * `recordRetrieval(itemId)` call can credit the right source even
39
+ * when the agent only knows the item id. Bounded; oldest entries
40
+ * evicted via the same `maxHistory` knob.
41
+ */
42
+ itemSourceMap = new Map();
43
+ constructor(policy = {}) {
44
+ this.policy = this.validatePolicy({ ...types_1.DEFAULT_NUTRITION_POLICY, ...policy });
45
+ }
46
+ validatePolicy(policy) {
47
+ const finite = (v) => typeof v === 'number' && Number.isFinite(v);
48
+ if (!Number.isInteger(policy.maxContextTokens) || policy.maxContextTokens <= 0) {
49
+ throw new Error('Aahar: maxContextTokens must be a positive integer');
50
+ }
51
+ if (!finite(policy.relevanceThreshold) || policy.relevanceThreshold < 0 || policy.relevanceThreshold > 1) {
52
+ throw new Error('Aahar: relevanceThreshold must be a finite number in [0,1]');
53
+ }
54
+ if (!finite(policy.freshnessWindowSec) || policy.freshnessWindowSec <= 0) {
55
+ throw new Error('Aahar: freshnessWindowSec must be a positive finite number');
56
+ }
57
+ if (!Number.isInteger(policy.maxContextItems) || policy.maxContextItems <= 0) {
58
+ throw new Error('Aahar: maxContextItems must be a positive integer');
59
+ }
60
+ if (!finite(policy.targetSignalToNoise) || policy.targetSignalToNoise < 0 || policy.targetSignalToNoise > 1) {
61
+ throw new Error('Aahar: targetSignalToNoise must be a finite number in [0,1]');
62
+ }
63
+ if (policy.maxHistory !== undefined && (!Number.isInteger(policy.maxHistory) || policy.maxHistory < 0)) {
64
+ throw new Error('Aahar: maxHistory must be a non-negative integer if set');
65
+ }
66
+ if (policy.temporalIntent !== undefined &&
67
+ policy.temporalIntent !== 'recent' &&
68
+ policy.temporalIntent !== 'historical' &&
69
+ policy.temporalIntent !== 'any') {
70
+ throw new Error("Aahar: temporalIntent must be 'recent' | 'historical' | 'any' if set");
71
+ }
72
+ if (policy.deduplicationThreshold !== undefined) {
73
+ if (!finite(policy.deduplicationThreshold) || policy.deduplicationThreshold <= 0 || policy.deduplicationThreshold > 1) {
74
+ throw new Error('Aahar: deduplicationThreshold must be a finite number in (0,1] if set');
75
+ }
76
+ }
77
+ if (policy.diversityWeight !== undefined) {
78
+ if (!finite(policy.diversityWeight) || policy.diversityWeight < 0 || policy.diversityWeight > 1) {
79
+ throw new Error('Aahar: diversityWeight must be a finite number in [0,1] if set');
80
+ }
81
+ }
82
+ return policy;
83
+ }
84
+ enforceHistoryLimit() {
85
+ const cap = this.policy.maxHistory ?? 0;
86
+ if (cap > 0 && this.history.length > cap) {
87
+ this.history.splice(0, this.history.length - cap);
88
+ }
89
+ }
90
+ // ── Core: Filter & Curate Context ────────────────────────────────────────
91
+ /**
92
+ * Reject items whose numeric fields are non-finite or out of range so
93
+ * malformed callers can't bypass token budgets (negative `tokenCount`),
94
+ * dominate ranking (`Infinity` relevance), or poison sorting (`NaN`
95
+ * freshness). Throws an Error naming the offending field; callers should
96
+ * validate at their boundary if they need lenient handling.
97
+ */
98
+ static assertValidItem(item) {
99
+ const finite = (v) => typeof v === 'number' && Number.isFinite(v);
100
+ if (!finite(item.relevanceScore) || item.relevanceScore < 0 || item.relevanceScore > 1) {
101
+ throw new Error(`Aahar: invalid relevanceScore ${item.relevanceScore} on item ${item.id} (must be a finite number in [0,1])`);
102
+ }
103
+ if (!finite(item.freshness)) {
104
+ throw new Error(`Aahar: invalid freshness ${item.freshness} on item ${item.id} (must be a finite Unix-seconds timestamp)`);
105
+ }
106
+ if (!finite(item.tokenCount) || item.tokenCount < 0 || !Number.isInteger(item.tokenCount)) {
107
+ throw new Error(`Aahar: invalid tokenCount ${item.tokenCount} on item ${item.id} (must be a non-negative integer)`);
108
+ }
109
+ if (item.trustScore !== undefined && (!finite(item.trustScore) || item.trustScore < 0 || item.trustScore > 1)) {
110
+ throw new Error(`Aahar: invalid trustScore ${item.trustScore} on item ${item.id} (must be a finite number in [0,1] if set)`);
111
+ }
112
+ }
113
+ /**
114
+ * Filter incoming context items based on nutrition policy.
115
+ * This is the primary "feeding" function — it ensures the agent
116
+ * only receives high-quality, relevant, fresh information.
117
+ *
118
+ * Validates each item up-front and throws on malformed numerics. The
119
+ * validation is a SAFETY GATE: silently accepting `tokenCount: -100000`
120
+ * or `relevanceScore: Infinity` would let a buggy/malicious caller
121
+ * bypass the token budget or dominate prioritization.
122
+ *
123
+ * When `options.query` is supplied, Aahar computes BM25 and entity-
124
+ * overlap signals against the query and fuses them with the caller's
125
+ * `relevanceScore` via Reciprocal Rank Fusion. The fused score replaces
126
+ * the bare relevance component of the per-item composite score but does
127
+ * NOT bypass the `relevanceThreshold` gate — the caller's authoritative
128
+ * relevance signal still decides admission. Omitting `options` is
129
+ * byte-for-byte equivalent to the previous single-argument signature.
130
+ */
131
+ filter(items, options) {
132
+ for (const item of items)
133
+ Aahar.assertValidItem(item);
134
+ this.assertValidOptions(options);
135
+ const nowSec = Date.now() / 1000;
136
+ const fusion = this.computeFusion(items, options?.query);
137
+ // Score and sort. When fusion was computed, pass each item's fused
138
+ // score through; otherwise the scoring degenerates to the legacy path.
139
+ const scored = items.map((item, i) => ({
140
+ item,
141
+ score: this.scoreItem(item, nowSec, fusion ? fusion[i] : undefined),
142
+ index: i,
143
+ }));
144
+ // Tier-aware ordering: 'always' first, then 'on-demand', then 'rare',
145
+ // and within each tier sort by composite score. This guarantees a
146
+ // 'rare' item with high relevance is dropped before an 'always'
147
+ // item with low relevance when the budget is tight.
148
+ const tierRank = (t) => t === 'always' ? 0 : t === 'rare' ? 2 : 1;
149
+ scored.sort((a, b) => {
150
+ const tr = tierRank(a.item.tier) - tierRank(b.item.tier);
151
+ if (tr !== 0)
152
+ return tr;
153
+ return b.score - a.score;
154
+ });
155
+ const rejected = [];
156
+ // Optional dedup pre-pass: collapse near-duplicate content before the
157
+ // packer runs so the token budget isn't burned on two copies of the
158
+ // same thing.
159
+ const survivors = this.policy.deduplicateNearDuplicates
160
+ ? this.deduplicate(scored, rejected)
161
+ : scored;
162
+ // Pack into the token budget. Greedy top-K by default; MMR-aware when
163
+ // `diversityWeight > 0` (coverage over redundancy).
164
+ const honorFreshnessGate = (this.policy.temporalIntent ?? 'recent') === 'recent';
165
+ const lambda = this.policy.diversityWeight ?? 0;
166
+ const accepted = lambda > 0
167
+ ? this.packWithMMR(survivors, lambda, nowSec, honorFreshnessGate, rejected)
168
+ : this.packGreedy(survivors, nowSec, honorFreshnessGate, rejected);
169
+ const totalTokens = accepted.reduce((s, i) => s + i.tokenCount, 0);
170
+ const qualityScore = accepted.length > 0
171
+ ? accepted.reduce((sum, i) => sum + i.relevanceScore, 0) / accepted.length
172
+ : 0;
173
+ // Track source for every input item so a later `recordRetrieval(id)`
174
+ // can credit the right source's pressure counter.
175
+ for (const i of items)
176
+ this.itemSourceMap.set(i.id, i.source);
177
+ // Tier breakdown — only populated if at least one item carried a
178
+ // tier hint (otherwise it's noise).
179
+ const anyTiered = items.some((i) => i.tier !== undefined);
180
+ let tierBreakdown;
181
+ if (anyTiered) {
182
+ tierBreakdown = {
183
+ always: { accepted: 0, rejected: 0 },
184
+ 'on-demand': { accepted: 0, rejected: 0 },
185
+ rare: { accepted: 0, rejected: 0 },
186
+ };
187
+ for (const i of accepted) {
188
+ const t = i.tier ?? 'on-demand';
189
+ tierBreakdown[t].accepted += 1;
190
+ }
191
+ for (const i of rejected) {
192
+ const t = i.tier ?? 'on-demand';
193
+ tierBreakdown[t].rejected += 1;
194
+ }
195
+ }
196
+ // Omission marker: synthetic ContextItem so the agent sees an
197
+ // explicit `[ojas:omitted N items: reason1, reason2]` line at the
198
+ // tail of `accepted`.
199
+ let finalAccepted = accepted;
200
+ if (options?.emitOmissionMarker && rejected.length > 0) {
201
+ const reasonCounts = new Map();
202
+ for (const r of rejected) {
203
+ const key = r.tier === 'rare' ? 'low-tier' : 'budget-or-relevance';
204
+ reasonCounts.set(key, (reasonCounts.get(key) ?? 0) + 1);
205
+ }
206
+ const reasons = Array.from(reasonCounts.entries())
207
+ .map(([k, n]) => `${n} ${k}`)
208
+ .join(', ');
209
+ const marker = {
210
+ id: `aahar-omitted-${Date.now()}`,
211
+ content: `[ojas:omitted ${rejected.length} items: ${reasons}]`,
212
+ source: 'aahar/omission',
213
+ relevanceScore: 0,
214
+ freshness: 1,
215
+ tokenCount: 0, // synthetic, doesn't burn budget
216
+ };
217
+ finalAccepted = [...accepted, marker];
218
+ }
219
+ const result = {
220
+ accepted: finalAccepted,
221
+ rejected,
222
+ totalTokens,
223
+ qualityScore,
224
+ ...(tierBreakdown ? { tierBreakdown } : {}),
225
+ };
226
+ this.history.push(result);
227
+ this.enforceHistoryLimit();
228
+ return result;
229
+ }
230
+ // ── Adaptive compression (Block 4 — Aahar upgrades) ──────────────────────
231
+ /**
232
+ * Record that the agent fetched (or had to re-fetch) the named item.
233
+ * Drives adaptive compression: a source whose items are fetched
234
+ * repeatedly is one whose relevance was likely underestimated, so
235
+ * subsequent `filter()` calls soften the threshold for that source.
236
+ *
237
+ * Cheap, append-only. The caller doesn't need the item itself — Aahar
238
+ * uses the `id → source` mapping captured during the most recent
239
+ * `filter()` call. Unknown ids are silently ignored.
240
+ */
241
+ recordRetrieval(itemId) {
242
+ const source = this.itemSourceMap.get(itemId);
243
+ if (!source)
244
+ return;
245
+ this.retrievalPressureBySource.set(source, (this.retrievalPressureBySource.get(source) ?? 0) + 1);
246
+ }
247
+ /**
248
+ * Effective relevance threshold for `source`, after applying any
249
+ * accumulated retrieval pressure. Returns the base policy threshold
250
+ * unmodified when there is no pressure recorded. Each retrieval
251
+ * subtracts `0.02` from the threshold, floored at `0`. Used by
252
+ * `scoreItem` / `passesGate` callers; exposed for tests + diagnostics.
253
+ */
254
+ getEffectiveThreshold(source) {
255
+ const base = this.policy.relevanceThreshold;
256
+ const pressure = this.retrievalPressureBySource.get(source) ?? 0;
257
+ if (pressure === 0)
258
+ return base;
259
+ return Math.max(0, base - pressure * 0.02);
260
+ }
261
+ /** Reset retrieval-pressure counters. Useful for tests + diagnostics. */
262
+ resetRetrievalPressure() {
263
+ this.retrievalPressureBySource.clear();
264
+ }
265
+ /** Snapshot of retrieval pressure per source (read-only view). */
266
+ getRetrievalPressure() {
267
+ return new Map(this.retrievalPressureBySource);
268
+ }
269
+ // ── Lazy / on-demand content resolution ─────────────────────────────────
270
+ /**
271
+ * Walk `accepted` items, and for any that carry a `resolveContent`
272
+ * function, await the resolver and replace `content` with the
273
+ * resolved string. Items whose resolver throws are dropped from the
274
+ * returned array — callers don't want a half-resolved bundle.
275
+ *
276
+ * Useful when the upstream retriever produces lightweight handles
277
+ * (id + scoring hints) and the actual content is expensive to
278
+ * fetch. `filter()` runs its full ranking + budget enforcement on
279
+ * the placeholder content; only the items that survive into
280
+ * `accepted` pay the resolution cost.
281
+ *
282
+ * Items without a resolver are passed through untouched. Returns a
283
+ * new array — the input is not mutated.
284
+ */
285
+ async materialise(accepted) {
286
+ const out = [];
287
+ for (const item of accepted) {
288
+ if (typeof item.resolveContent !== 'function') {
289
+ out.push(item);
290
+ continue;
291
+ }
292
+ try {
293
+ const resolved = await item.resolveContent();
294
+ if (typeof resolved !== 'string') {
295
+ // A non-string resolver result is treated as a resolution failure
296
+ // — silently drop rather than ship corrupt content.
297
+ continue;
298
+ }
299
+ // Spread to a new object so the caller can keep the original handle.
300
+ const { resolveContent: _omit, ...rest } = item;
301
+ void _omit;
302
+ out.push({ ...rest, content: resolved });
303
+ }
304
+ catch {
305
+ // Resolver throw → drop the item from the materialised set.
306
+ }
307
+ }
308
+ return out;
309
+ }
310
+ // ── Options validation ──────────────────────────────────────────────────
311
+ assertValidOptions(options) {
312
+ if (options === undefined)
313
+ return;
314
+ if (options.query !== undefined && typeof options.query !== 'string') {
315
+ throw new Error('Aahar: options.query must be a string if provided');
316
+ }
317
+ }
318
+ // ── Query-aware fusion (BM25 + entity overlap + caller relevance) ───────
319
+ /**
320
+ * Compute a Reciprocal-Rank-Fusion score per item over three lexical
321
+ * signals computed against `query`:
322
+ * - BM25 over the local item corpus,
323
+ * - count of overlapping entity tokens with the query,
324
+ * - caller-supplied `relevanceScore`.
325
+ * Returns `null` when no query is supplied (or the query is empty after
326
+ * trimming) so the caller falls back to legacy scoring.
327
+ */
328
+ computeFusion(items, query) {
329
+ const q = query?.trim();
330
+ if (!q || items.length === 0)
331
+ return null;
332
+ const queryTokens = (0, scoring_1.tokenize)(q);
333
+ const queryEntities = (0, scoring_1.extractEntities)(q);
334
+ const docTokens = items.map((it) => (0, scoring_1.tokenize)(it.content));
335
+ const bm25 = (0, scoring_1.bm25Scores)(queryTokens, docTokens);
336
+ const entityOverlap = items.map((it) => {
337
+ const ents = (0, scoring_1.extractEntities)(it.content);
338
+ let n = 0;
339
+ for (const e of queryEntities)
340
+ if (ents.has(e))
341
+ n++;
342
+ return n;
343
+ });
344
+ const relevanceVals = items.map((it) => it.relevanceScore);
345
+ const ranks = [
346
+ (0, scoring_1.scoresToRanks)(bm25),
347
+ (0, scoring_1.scoresToRanks)(entityOverlap),
348
+ (0, scoring_1.scoresToRanks)(relevanceVals),
349
+ ];
350
+ const fused = (0, scoring_1.rrfFuse)(ranks);
351
+ // Normalise to [0, 1] so the fused number plays nicely with the rest
352
+ // of the composite score which is also in [0, 1] per-component.
353
+ const maxFused = fused.reduce((m, v) => (v > m ? v : m), 0);
354
+ if (maxFused <= 0)
355
+ return fused.map(() => 0);
356
+ return fused.map((v) => v / maxFused);
357
+ }
358
+ // ── Dedup pre-pass ──────────────────────────────────────────────────────
359
+ /**
360
+ * Collapse near-duplicates by shingle Jaccard ≥ `deduplicationThreshold`.
361
+ * The first item encountered (highest-scored, since `scored` is already
362
+ * sorted descending) wins; later near-duplicates go to `rejected`.
363
+ */
364
+ deduplicate(scored, rejected) {
365
+ const threshold = this.policy.deduplicationThreshold ?? 0.85;
366
+ const kept = [];
367
+ const keptShingles = [];
368
+ for (const entry of scored) {
369
+ const sh = (0, scoring_1.shingles)(entry.item.content);
370
+ let isDup = false;
371
+ for (const ks of keptShingles) {
372
+ if ((0, scoring_1.jaccard)(sh, ks) >= threshold) {
373
+ isDup = true;
374
+ break;
375
+ }
376
+ }
377
+ if (isDup) {
378
+ rejected.push(entry.item);
379
+ }
380
+ else {
381
+ kept.push(entry);
382
+ keptShingles.push(sh);
383
+ }
384
+ }
385
+ return kept;
386
+ }
387
+ // ── Packing strategies ──────────────────────────────────────────────────
388
+ /**
389
+ * Greedy top-K packing (legacy behaviour). Items are already sorted by
390
+ * composite score; admit them in order while they pass the gates and
391
+ * the running token budget has room.
392
+ */
393
+ packGreedy(sortedScored, nowSec, honorFreshnessGate, rejected) {
394
+ const accepted = [];
395
+ let totalTokens = 0;
396
+ for (const { item } of sortedScored) {
397
+ // Adaptive-compression: per-source threshold falls as the agent
398
+ // keeps re-fetching items from that source.
399
+ const effectiveThreshold = this.getEffectiveThreshold(item.source);
400
+ const passesRelevance = item.relevanceScore >= effectiveThreshold;
401
+ const passesFreshness = !honorFreshnessGate || (nowSec - item.freshness) < this.policy.freshnessWindowSec;
402
+ const passesTokenBudget = (totalTokens + item.tokenCount) <= this.policy.maxContextTokens;
403
+ const passesItemLimit = accepted.length < this.policy.maxContextItems;
404
+ if (passesRelevance && passesFreshness && passesTokenBudget && passesItemLimit) {
405
+ accepted.push(item);
406
+ totalTokens += item.tokenCount;
407
+ }
408
+ else {
409
+ rejected.push(item);
410
+ }
411
+ }
412
+ return accepted;
413
+ }
414
+ /**
415
+ * Maximal Marginal Relevance packing. For each slot, pick the candidate
416
+ * that maximises `(1 - λ)*score - λ*maxSim(c, accepted)` where sim is
417
+ * cosine over token bags. λ=0 reduces to greedy; λ=1 picks purely for
418
+ * diversity. Items failing the relevance or freshness gates are routed
419
+ * to `rejected` up-front so MMR only chooses among legitimate
420
+ * candidates.
421
+ */
422
+ packWithMMR(sortedScored, lambda, nowSec, honorFreshnessGate, rejected) {
423
+ // First, partition by gates. Pre-tokenise + pre-bag once so the inner
424
+ // MMR loop is O(K*N) cosine ops with cached bags instead of
425
+ // re-tokenising on every comparison.
426
+ const candidates = [];
427
+ for (const { item, score } of sortedScored) {
428
+ const effectiveThreshold = this.getEffectiveThreshold(item.source);
429
+ const passesRelevance = item.relevanceScore >= effectiveThreshold;
430
+ const passesFreshness = !honorFreshnessGate || (nowSec - item.freshness) < this.policy.freshnessWindowSec;
431
+ if (!passesRelevance || !passesFreshness) {
432
+ rejected.push(item);
433
+ continue;
434
+ }
435
+ candidates.push({ item, score, bag: (0, scoring_1.buildBag)((0, scoring_1.tokenize)(item.content)) });
436
+ }
437
+ const accepted = [];
438
+ const acceptedBags = [];
439
+ let totalTokens = 0;
440
+ while (candidates.length > 0 && accepted.length < this.policy.maxContextItems) {
441
+ // Find the candidate maximising the MMR objective.
442
+ let bestIdx = -1;
443
+ let bestMmr = Number.NEGATIVE_INFINITY;
444
+ for (let i = 0; i < candidates.length; i++) {
445
+ const c = candidates[i];
446
+ let maxSim = 0;
447
+ for (const ab of acceptedBags) {
448
+ const s = (0, scoring_1.bagCosine)(c.bag, ab);
449
+ if (s > maxSim)
450
+ maxSim = s;
451
+ }
452
+ const mmr = (1 - lambda) * c.score - lambda * maxSim;
453
+ if (mmr > bestMmr) {
454
+ bestMmr = mmr;
455
+ bestIdx = i;
456
+ }
457
+ }
458
+ if (bestIdx < 0)
459
+ break;
460
+ const chosen = candidates[bestIdx];
461
+ candidates.splice(bestIdx, 1);
462
+ if (totalTokens + chosen.item.tokenCount <= this.policy.maxContextTokens) {
463
+ accepted.push(chosen.item);
464
+ acceptedBags.push(chosen.bag);
465
+ totalTokens += chosen.item.tokenCount;
466
+ }
467
+ else {
468
+ rejected.push(chosen.item);
469
+ }
470
+ }
471
+ // Anything left over after the item-limit fired is rejected too.
472
+ for (const c of candidates)
473
+ rejected.push(c.item);
474
+ return accepted;
475
+ }
476
+ // ── Context Quality Scoring ──────────────────────────────────────────────
477
+ /**
478
+ * Score a single context item. Higher = better nutrition.
479
+ *
480
+ * `fusionOverride`, when provided, replaces the bare `relevanceScore`
481
+ * term in the composite. Callers pass it when query-aware fusion has
482
+ * been computed for the whole batch (see `computeFusion`). The other
483
+ * components (freshness, tokenPenalty) and their weights are unchanged
484
+ * so the score remains on the same [0, 1]-ish scale as before.
485
+ *
486
+ * `temporalIntent` reshapes the freshness component:
487
+ * - `'recent'` (default): exponential decay favours new items.
488
+ * - `'any'`: freshness held at 0.5 (neutral).
489
+ * - `'historical'`: invert the decay so older items rank higher.
490
+ */
491
+ scoreItem(item, nowSec, fusionOverride) {
492
+ const ageSec = Math.max(0, nowSec - item.freshness);
493
+ const intent = this.policy.temporalIntent ?? 'recent';
494
+ let freshness;
495
+ if (intent === 'any') {
496
+ freshness = 0.5;
497
+ }
498
+ else if (intent === 'historical') {
499
+ freshness = 1 - Math.exp(-ageSec / this.policy.freshnessWindowSec);
500
+ }
501
+ else {
502
+ freshness = Math.exp(-ageSec / this.policy.freshnessWindowSec);
503
+ }
504
+ const tokenPenalty = item.tokenCount > this.policy.maxContextTokens * 0.5
505
+ ? 0.5
506
+ : 1.0;
507
+ const relevanceComponent = fusionOverride ?? item.relevanceScore;
508
+ return (relevanceComponent * 0.6) + (freshness * 0.3) + (tokenPenalty * 0.1);
509
+ }
510
+ // ── Cognitive Load Assessment ────────────────────────────────────────────
511
+ /**
512
+ * Measure current cognitive load based on context composition.
513
+ * Returns 0 (no load) to 1 (overloaded).
514
+ */
515
+ measureCognitiveLoad(activeContext) {
516
+ for (const item of activeContext)
517
+ Aahar.assertValidItem(item);
518
+ if (activeContext.length === 0)
519
+ return 0;
520
+ const totalTokens = activeContext.reduce((s, i) => s + i.tokenCount, 0);
521
+ const tokenLoad = totalTokens / this.policy.maxContextTokens;
522
+ const itemLoad = activeContext.length / this.policy.maxContextItems;
523
+ const noiseLoad = 1 - this.measureSignalToNoise(activeContext);
524
+ return clamp((tokenLoad * 0.4) + (itemLoad * 0.3) + (noiseLoad * 0.3));
525
+ }
526
+ // ── Signal-to-Noise Ratio ────────────────────────────────────────────────
527
+ /**
528
+ * Compute signal-to-noise ratio for a set of context items.
529
+ * Higher = cleaner cognitive input.
530
+ */
531
+ measureSignalToNoise(items) {
532
+ for (const item of items)
533
+ Aahar.assertValidItem(item);
534
+ if (items.length === 0)
535
+ return 0;
536
+ const signal = items.filter((i) => i.relevanceScore >= this.policy.relevanceThreshold).length;
537
+ return signal / items.length;
538
+ }
539
+ // ── Token Efficiency ─────────────────────────────────────────────────────
540
+ /**
541
+ * Compute token efficiency: how many tokens are high-signal vs wasted.
542
+ */
543
+ measureTokenEfficiency(items) {
544
+ for (const item of items)
545
+ Aahar.assertValidItem(item);
546
+ if (items.length === 0)
547
+ return 0;
548
+ const totalTokens = items.reduce((s, i) => s + i.tokenCount, 0);
549
+ if (totalTokens === 0)
550
+ return 0;
551
+ const signalTokens = items
552
+ .filter((i) => i.relevanceScore >= this.policy.relevanceThreshold)
553
+ .reduce((s, i) => s + i.tokenCount, 0);
554
+ return signalTokens / totalTokens;
555
+ }
556
+ // ── Attention Prioritization ─────────────────────────────────────────────
557
+ /**
558
+ * Re-rank context items by cognitive priority.
559
+ * Returns items ordered by what the agent should attend to first.
560
+ *
561
+ * When `options.query` is supplied, ranking incorporates the same
562
+ * BM25 + entity-overlap + RRF fusion as `filter()`. Omitting `options`
563
+ * preserves the previous single-argument behaviour.
564
+ */
565
+ prioritize(items, options) {
566
+ for (const item of items)
567
+ Aahar.assertValidItem(item);
568
+ this.assertValidOptions(options);
569
+ const nowSec = Date.now() / 1000;
570
+ const fusion = this.computeFusion(items, options?.query);
571
+ return [...items]
572
+ .map((item, i) => ({ item, score: this.scoreItem(item, nowSec, fusion ? fusion[i] : undefined) }))
573
+ .sort((a, b) => b.score - a.score)
574
+ .map((e) => e.item);
575
+ }
576
+ // ── Full Nutrition Health Assessment ─────────────────────────────────────
577
+ /**
578
+ * Produce a complete nutrition health report for current context.
579
+ */
580
+ assess(activeContext) {
581
+ for (const item of activeContext)
582
+ Aahar.assertValidItem(item);
583
+ const snr = this.measureSignalToNoise(activeContext);
584
+ const load = this.measureCognitiveLoad(activeContext);
585
+ const efficiency = this.measureTokenEfficiency(activeContext);
586
+ return {
587
+ contextQuality: healthScore(snr, 'aahar.signalToNoise'),
588
+ cognitiveLoad: healthScore(1 - load, 'aahar.cognitiveLoad'),
589
+ attentionFocus: healthScore(activeContext.length > 0
590
+ ? activeContext.slice(0, 3).reduce((s, i) => s + i.relevanceScore, 0) / Math.min(3, activeContext.length)
591
+ : 0, 'aahar.attentionFocus'),
592
+ signalToNoise: snr,
593
+ tokenEfficiency: efficiency,
594
+ };
595
+ }
596
+ // ── Recommendations ──────────────────────────────────────────────────────
597
+ /**
598
+ * Generate nutrition health recommendations.
599
+ */
600
+ recommend(activeContext) {
601
+ const recs = [];
602
+ const health = this.assess(activeContext);
603
+ if (health.signalToNoise < this.policy.targetSignalToNoise) {
604
+ recs.push({
605
+ module: 'aahar',
606
+ severity: health.signalToNoise < 0.3 ? 'critical' : 'warning',
607
+ message: `Signal-to-noise ratio is ${(health.signalToNoise * 100).toFixed(1)}% — below target of ${(this.policy.targetSignalToNoise * 100).toFixed(1)}%`,
608
+ action: 'Filter low-relevance context items and refresh stale data.',
609
+ });
610
+ }
611
+ if (health.cognitiveLoad.value < 0.3) {
612
+ recs.push({
613
+ module: 'aahar',
614
+ severity: 'critical',
615
+ message: 'Cognitive load is dangerously high — context overload detected.',
616
+ action: 'Reduce active context size and remove redundant items.',
617
+ });
618
+ }
619
+ if (health.tokenEfficiency < 0.5) {
620
+ recs.push({
621
+ module: 'aahar',
622
+ severity: 'warning',
623
+ message: `Token efficiency at ${(health.tokenEfficiency * 100).toFixed(1)}% — significant waste detected.`,
624
+ action: 'Trim low-value context items to reclaim token budget.',
625
+ });
626
+ }
627
+ if (activeContext.length > this.policy.maxContextItems * 0.9) {
628
+ recs.push({
629
+ module: 'aahar',
630
+ severity: 'warning',
631
+ message: 'Approaching maximum context item limit.',
632
+ action: 'Consolidate or evict least relevant items.',
633
+ });
634
+ }
635
+ if (recs.length === 0) {
636
+ recs.push({
637
+ module: 'aahar',
638
+ severity: 'info',
639
+ message: 'Cognitive nutrition is healthy.',
640
+ });
641
+ }
642
+ return recs;
643
+ }
644
+ // ── Policy Management ────────────────────────────────────────────────────
645
+ getPolicy() {
646
+ return { ...this.policy };
647
+ }
648
+ updatePolicy(updates) {
649
+ this.policy = this.validatePolicy({ ...this.policy, ...updates });
650
+ this.enforceHistoryLimit();
651
+ }
652
+ getHistory() {
653
+ return [...this.history];
654
+ }
655
+ }
656
+ exports.Aahar = Aahar;
657
+ //# sourceMappingURL=index.js.map