@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.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/aahar/index.d.ts +179 -0
- package/dist/aahar/index.d.ts.map +1 -0
- package/dist/aahar/index.js +657 -0
- package/dist/aahar/index.js.map +1 -0
- package/dist/aahar/scoring.d.ts +85 -0
- package/dist/aahar/scoring.d.ts.map +1 -0
- package/dist/aahar/scoring.js +268 -0
- package/dist/aahar/scoring.js.map +1 -0
- package/dist/agni/index.d.ts +113 -0
- package/dist/agni/index.d.ts.map +1 -0
- package/dist/agni/index.js +328 -0
- package/dist/agni/index.js.map +1 -0
- package/dist/agni/model-router.d.ts +77 -0
- package/dist/agni/model-router.d.ts.map +1 -0
- package/dist/agni/model-router.js +163 -0
- package/dist/agni/model-router.js.map +1 -0
- package/dist/agni/response-distiller.d.ts +37 -0
- package/dist/agni/response-distiller.d.ts.map +1 -0
- package/dist/agni/response-distiller.js +193 -0
- package/dist/agni/response-distiller.js.map +1 -0
- package/dist/agni/tiktoken-adapter.d.ts +55 -0
- package/dist/agni/tiktoken-adapter.d.ts.map +1 -0
- package/dist/agni/tiktoken-adapter.js +113 -0
- package/dist/agni/tiktoken-adapter.js.map +1 -0
- package/dist/chikitsa/index.d.ts +130 -0
- package/dist/chikitsa/index.d.ts.map +1 -0
- package/dist/chikitsa/index.js +565 -0
- package/dist/chikitsa/index.js.map +1 -0
- package/dist/demo.d.ts +15 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +278 -0
- package/dist/demo.js.map +1 -0
- package/dist/index.d.ts +201 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +588 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/audit.d.ts +39 -0
- package/dist/mcp/audit.d.ts.map +1 -0
- package/dist/mcp/audit.js +73 -0
- package/dist/mcp/audit.js.map +1 -0
- package/dist/mcp/contracts.d.ts +76 -0
- package/dist/mcp/contracts.d.ts.map +1 -0
- package/dist/mcp/contracts.js +44 -0
- package/dist/mcp/contracts.js.map +1 -0
- package/dist/mcp/envelope.d.ts +107 -0
- package/dist/mcp/envelope.d.ts.map +1 -0
- package/dist/mcp/envelope.js +162 -0
- package/dist/mcp/envelope.js.map +1 -0
- package/dist/mcp/registry.d.ts +110 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +258 -0
- package/dist/mcp/registry.js.map +1 -0
- package/dist/mcp/server.d.ts +26 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +107 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/agent.d.ts +4 -0
- package/dist/mcp/tools/agent.d.ts.map +1 -0
- package/dist/mcp/tools/agent.js +300 -0
- package/dist/mcp/tools/agent.js.map +1 -0
- package/dist/mcp/tools/context.d.ts +4 -0
- package/dist/mcp/tools/context.d.ts.map +1 -0
- package/dist/mcp/tools/context.js +261 -0
- package/dist/mcp/tools/context.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +5 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/index.js +20 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/memory.d.ts +4 -0
- package/dist/mcp/tools/memory.d.ts.map +1 -0
- package/dist/mcp/tools/memory.js +220 -0
- package/dist/mcp/tools/memory.js.map +1 -0
- package/dist/mcp/tools/output.d.ts +4 -0
- package/dist/mcp/tools/output.d.ts.map +1 -0
- package/dist/mcp/tools/output.js +206 -0
- package/dist/mcp/tools/output.js.map +1 -0
- package/dist/mcp/tools/recovery.d.ts +4 -0
- package/dist/mcp/tools/recovery.d.ts.map +1 -0
- package/dist/mcp/tools/recovery.js +165 -0
- package/dist/mcp/tools/recovery.js.map +1 -0
- package/dist/mcp/tools/registrar.d.ts +4 -0
- package/dist/mcp/tools/registrar.d.ts.map +1 -0
- package/dist/mcp/tools/registrar.js +17 -0
- package/dist/mcp/tools/registrar.js.map +1 -0
- package/dist/mcp/tools/report.d.ts +4 -0
- package/dist/mcp/tools/report.d.ts.map +1 -0
- package/dist/mcp/tools/report.js +68 -0
- package/dist/mcp/tools/report.js.map +1 -0
- package/dist/mcp/tools/shared.d.ts +37 -0
- package/dist/mcp/tools/shared.d.ts.map +1 -0
- package/dist/mcp/tools/shared.js +214 -0
- package/dist/mcp/tools/shared.js.map +1 -0
- package/dist/mcp/trace.d.ts +47 -0
- package/dist/mcp/trace.d.ts.map +1 -0
- package/dist/mcp/trace.js +216 -0
- package/dist/mcp/trace.js.map +1 -0
- package/dist/nidra/index.d.ts +275 -0
- package/dist/nidra/index.d.ts.map +1 -0
- package/dist/nidra/index.js +889 -0
- package/dist/nidra/index.js.map +1 -0
- package/dist/persistence/migrations.d.ts +10 -0
- package/dist/persistence/migrations.d.ts.map +1 -0
- package/dist/persistence/migrations.js +77 -0
- package/dist/persistence/migrations.js.map +1 -0
- package/dist/persistence/sqlite.d.ts +30 -0
- package/dist/persistence/sqlite.d.ts.map +1 -0
- package/dist/persistence/sqlite.js +209 -0
- package/dist/persistence/sqlite.js.map +1 -0
- package/dist/persistence/types.d.ts +104 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +5 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/pulse/index.d.ts +144 -0
- package/dist/pulse/index.d.ts.map +1 -0
- package/dist/pulse/index.js +453 -0
- package/dist/pulse/index.js.map +1 -0
- package/dist/raksha/classifiers/http-classifier.d.ts +26 -0
- package/dist/raksha/classifiers/http-classifier.d.ts.map +1 -0
- package/dist/raksha/classifiers/http-classifier.js +62 -0
- package/dist/raksha/classifiers/http-classifier.js.map +1 -0
- package/dist/raksha/classifiers/index.d.ts +5 -0
- package/dist/raksha/classifiers/index.d.ts.map +1 -0
- package/dist/raksha/classifiers/index.js +8 -0
- package/dist/raksha/classifiers/index.js.map +1 -0
- package/dist/raksha/classifiers/onnx-classifier.d.ts +41 -0
- package/dist/raksha/classifiers/onnx-classifier.d.ts.map +1 -0
- package/dist/raksha/classifiers/onnx-classifier.js +99 -0
- package/dist/raksha/classifiers/onnx-classifier.js.map +1 -0
- package/dist/raksha/hallucination-detectors.d.ts +106 -0
- package/dist/raksha/hallucination-detectors.d.ts.map +1 -0
- package/dist/raksha/hallucination-detectors.js +327 -0
- package/dist/raksha/hallucination-detectors.js.map +1 -0
- package/dist/raksha/index.d.ts +168 -0
- package/dist/raksha/index.d.ts.map +1 -0
- package/dist/raksha/index.js +597 -0
- package/dist/raksha/index.js.map +1 -0
- package/dist/raksha/prompt-injection-detectors.d.ts +30 -0
- package/dist/raksha/prompt-injection-detectors.d.ts.map +1 -0
- package/dist/raksha/prompt-injection-detectors.js +153 -0
- package/dist/raksha/prompt-injection-detectors.js.map +1 -0
- package/dist/types.d.ts +1115 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +71 -0
- package/dist/types.js.map +1 -0
- package/dist/util/calibration.d.ts +32 -0
- package/dist/util/calibration.d.ts.map +1 -0
- package/dist/util/calibration.js +108 -0
- package/dist/util/calibration.js.map +1 -0
- package/dist/util/id.d.ts +2 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +9 -0
- package/dist/util/id.js.map +1 -0
- package/dist/vyayam/index.d.ts +76 -0
- package/dist/vyayam/index.d.ts.map +1 -0
- package/dist/vyayam/index.js +528 -0
- package/dist/vyayam/index.js.map +1 -0
- package/dist/vyayam/tool-fault-proxy.d.ts +95 -0
- package/dist/vyayam/tool-fault-proxy.d.ts.map +1 -0
- package/dist/vyayam/tool-fault-proxy.js +170 -0
- package/dist/vyayam/tool-fault-proxy.js.map +1 -0
- package/docs/ARCHITECTURE.md +162 -0
- package/docs/BACKLOG.md +342 -0
- package/docs/CONFIGURATION.md +305 -0
- package/docs/EVIDENCE.md +232 -0
- package/docs/EVIDENCE_MATRIX.md +293 -0
- package/docs/KNOWN_FAILURES.md +367 -0
- package/docs/MCP.md +614 -0
- package/docs/MODULES.md +368 -0
- package/docs/SECURITY.md +251 -0
- package/docs/TRUST.md +88 -0
- package/docs/assets/ojas-hero.png +0 -0
- 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
|