entroly-wasm 0.19.4 → 0.19.5
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/js/server.js +100 -1
- package/js/value_tracker.js +202 -47
- package/package.json +1 -1
- package/pkg/entroly_wasm_bg.wasm +0 -0
package/js/server.js
CHANGED
|
@@ -10,6 +10,7 @@ const { CheckpointManager, persistIndex, loadIndex } = require('./checkpoint');
|
|
|
10
10
|
const { autoIndex, startIncrementalWatcher } = require('./auto_index');
|
|
11
11
|
const { startAutotuneDaemon, FeedbackJournal, TaskProfileOptimizer } = require('./autotune');
|
|
12
12
|
const { VaultManager } = require('./vault');
|
|
13
|
+
const { getTracker } = require('./value_tracker');
|
|
13
14
|
const { EpistemicRouter, BeliefCompiler, VerificationEngine, ChangePipeline, FlowOrchestrator, compileDocs, exportTrainingData } = require('./cogops');
|
|
14
15
|
const { WorkspaceChangeListener } = require('./workspace');
|
|
15
16
|
const { SkillEngine } = require('./skills');
|
|
@@ -26,6 +27,11 @@ class EntrolyMCPServer {
|
|
|
26
27
|
this.engine = new WasmEntrolyEngine();
|
|
27
28
|
this.turnCounter = 0;
|
|
28
29
|
|
|
30
|
+
// Shared cross-runtime telemetry sink — SAME file/schema/dir as the
|
|
31
|
+
// Python tracker, so npm users' value shows up on the same dashboard
|
|
32
|
+
// as pip/MCP/SDK users. Fail-open: never let telemetry break a tool.
|
|
33
|
+
try { this.valueTracker = getTracker(); } catch (_) { this.valueTracker = null; }
|
|
34
|
+
|
|
29
35
|
// Feedback journal — persists episodes for cross-session autotune
|
|
30
36
|
this.feedbackJournal = new FeedbackJournal(this.config.checkpointDir);
|
|
31
37
|
// Task-conditioned weight profiles (novel: per-task-type optimization)
|
|
@@ -112,6 +118,8 @@ class EntrolyMCPServer {
|
|
|
112
118
|
// ── Analysis Tools (2) ──
|
|
113
119
|
{ name: 'repo_file_map', description: 'Return the canonical Entroly file map across Python, Rust core, and WASM repos with ownership roles.', inputSchema: { type: 'object', properties: { format: { type: 'string', description: 'Output format: markdown or json', default: 'markdown' } } } },
|
|
114
120
|
{ name: 'prefetch_related', description: 'Predict which files the LLM will need next based on co-access patterns and dependency graph.', inputSchema: { type: 'object', properties: { file_path: { type: 'string', description: 'Current file being worked on' }, source_content: { type: 'string', description: 'Content of current file for import analysis', default: '' }, language: { type: 'string', description: 'Programming language', default: '' } }, required: ['file_path'] } },
|
|
121
|
+
// ── Hallucination Detection Tool ──
|
|
122
|
+
{ name: 'verify_response', description: 'Verify an AI-generated response for hallucination. Computes entity coverage gap, hedging curvature, and entropy consistency. Returns fused_risk [0.0=safe, 1.0=hallucinated], verdict (pass/warn/flag), and flagged claims. All local — zero LLM calls.', inputSchema: { type: 'object', properties: { response: { type: 'string', description: 'The AI-generated text to verify' }, context: { type: 'string', description: 'Source context provided to the AI', default: '' }, prompt: { type: 'string', description: 'Original user prompt', default: '' } }, required: ['response'] } },
|
|
115
123
|
];
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -130,6 +138,18 @@ class EntrolyMCPServer {
|
|
|
130
138
|
const profile = this.taskProfiles.applyToEngine(this.engine, query);
|
|
131
139
|
const result = this.engine.optimize(budget, query);
|
|
132
140
|
result._taskProfile = { taskType: profile.taskType, confidence: profile.confidence };
|
|
141
|
+
// Record value to the shared sink (mirrors entroly/server.py).
|
|
142
|
+
try {
|
|
143
|
+
const ts = (result && result.tokens_saved) || 0;
|
|
144
|
+
if (this.valueTracker && ts > 0) {
|
|
145
|
+
this.valueTracker.record({
|
|
146
|
+
tokensSaved: ts,
|
|
147
|
+
model: (result && result.model) || '',
|
|
148
|
+
duplicates: (result && result.duplicates_caught) || 0,
|
|
149
|
+
optimized: true,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
} catch (_) { /* never fail optimization for telemetry */ }
|
|
133
153
|
const state = this.engine.export_state();
|
|
134
154
|
this._lastOptCtx = { weights: { w_r: state.w_recency, w_f: state.w_frequency, w_s: state.w_semantic, w_e: state.w_entropy }, selectedSources: (result.selected || []).map(s => s.source).filter(Boolean), selectedCount: result.selected_count || 0, tokenBudget: budget, query, turn: this.turnCounter };
|
|
135
155
|
if (this.checkpointMgr.shouldAutoCheckpoint()) { try { persistIndex(this.engine, this.indexPath); this.checkpointMgr.save({ engine_state: state, turn: this.turnCounter }); } catch {} }
|
|
@@ -181,7 +201,21 @@ class EntrolyMCPServer {
|
|
|
181
201
|
case 'entroly_dashboard': {
|
|
182
202
|
const stats = this.engine.stats();
|
|
183
203
|
const explanation = this.engine.explain_selection();
|
|
184
|
-
|
|
204
|
+
// npm users live in their MCP client (no Python dashboard), so
|
|
205
|
+
// surface the persisted cross-session value here directly.
|
|
206
|
+
let value = null;
|
|
207
|
+
try {
|
|
208
|
+
if (this.valueTracker) {
|
|
209
|
+
const tr = this.valueTracker.getTrends();
|
|
210
|
+
value = {
|
|
211
|
+
lifetime: tr.lifetime,
|
|
212
|
+
today: (tr.daily.slice(-1)[0]) || null,
|
|
213
|
+
recent_activity: tr.activity.slice(0, 10),
|
|
214
|
+
data_source: 'shared telemetry file (cross-runtime)',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
} catch (_) { /* fail-open */ }
|
|
218
|
+
return { stats, explanation, value, turn: this.turnCounter };
|
|
185
219
|
}
|
|
186
220
|
|
|
187
221
|
// ── CogOps Epistemic Tools ──
|
|
@@ -327,6 +361,71 @@ class EntrolyMCPServer {
|
|
|
327
361
|
return { file: args.file_path, language: args.language || 'unknown', predicted_files: imports.slice(0, 10), dep_graph: depStats, engine: 'javascript' };
|
|
328
362
|
}
|
|
329
363
|
|
|
364
|
+
// ── Hallucination Detection ──
|
|
365
|
+
case 'verify_response': {
|
|
366
|
+
const resp = args.response || '';
|
|
367
|
+
const ctx = args.context || '';
|
|
368
|
+
const prm = args.prompt || '';
|
|
369
|
+
|
|
370
|
+
// Entity coverage gap: extract identifiers from context vs response
|
|
371
|
+
const ctxEntities = new Set((ctx + ' ' + prm).match(/[a-zA-Z_][a-zA-Z0-9_.]{2,}/g) || []);
|
|
372
|
+
const respEntities = (resp.match(/[a-zA-Z_][a-zA-Z0-9_.]{2,}/g) || []);
|
|
373
|
+
const respUnique = [...new Set(respEntities)];
|
|
374
|
+
let grounded = 0;
|
|
375
|
+
const flaggedClaims = [];
|
|
376
|
+
for (const ent of respUnique) {
|
|
377
|
+
if (ctxEntities.has(ent) || ent.length <= 3) {
|
|
378
|
+
grounded++;
|
|
379
|
+
} else {
|
|
380
|
+
flaggedClaims.push({ claim: ent, score: 1.0 });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const entityGap = respUnique.length > 0 ? 1.0 - (grounded / respUnique.length) : 0;
|
|
384
|
+
|
|
385
|
+
// ECE-like: hedging language curvature
|
|
386
|
+
const hedgeWords = ['might', 'could', 'possibly', 'perhaps', 'maybe', 'uncertain', 'likely', 'probably', 'appears to', 'seems'];
|
|
387
|
+
const wordCount = resp.split(/\s+/).length;
|
|
388
|
+
let hedgeCount = 0;
|
|
389
|
+
for (const h of hedgeWords) { hedgeCount += (resp.toLowerCase().match(new RegExp(h, 'g')) || []).length; }
|
|
390
|
+
const hedgeCurvature = Math.min(1.0, hedgeCount / Math.max(wordCount, 1) * 20);
|
|
391
|
+
|
|
392
|
+
// EPR-like: entropy consistency (char-bigram)
|
|
393
|
+
const bigrams = {};
|
|
394
|
+
for (let i = 0; i < resp.length - 1; i++) {
|
|
395
|
+
const bg = resp.slice(i, i + 2);
|
|
396
|
+
bigrams[bg] = (bigrams[bg] || 0) + 1;
|
|
397
|
+
}
|
|
398
|
+
const total = Object.values(bigrams).reduce((a, b) => a + b, 0) || 1;
|
|
399
|
+
let entropy = 0;
|
|
400
|
+
for (const c of Object.values(bigrams)) {
|
|
401
|
+
const p = c / total;
|
|
402
|
+
if (p > 0) entropy -= p * Math.log2(p);
|
|
403
|
+
}
|
|
404
|
+
// Normalize: high entropy is normal for text, low entropy is suspicious
|
|
405
|
+
const maxEntropy = Math.log2(Math.min(Object.keys(bigrams).length, 500)) || 1;
|
|
406
|
+
const eprRisk = Math.max(0, 1.0 - entropy / maxEntropy);
|
|
407
|
+
|
|
408
|
+
// Fuse (same weights as proxy)
|
|
409
|
+
const fused = Math.max(0, Math.min(1.0,
|
|
410
|
+
0.80 * entityGap + 0.08 * hedgeCurvature + 0.07 * eprRisk + 0.05 * 0
|
|
411
|
+
));
|
|
412
|
+
|
|
413
|
+
let verdict, recommendation;
|
|
414
|
+
if (fused < 0.15) { verdict = 'pass'; recommendation = 'Accept — response appears well-grounded'; }
|
|
415
|
+
else if (fused < 0.40) { verdict = 'warn'; recommendation = 'Review — some claims may not be grounded'; }
|
|
416
|
+
else { verdict = 'flag'; recommendation = 'Reject or rephrase — high hallucination risk'; }
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
fused_risk: Math.round(fused * 10000) / 10000,
|
|
420
|
+
verdict,
|
|
421
|
+
recommendation,
|
|
422
|
+
witness: { entity_coverage_gap: Math.round(entityGap * 10000) / 10000, total_entities: respUnique.length, grounded_count: grounded },
|
|
423
|
+
ece: { hedging_curvature: Math.round(hedgeCurvature * 10000) / 10000, hedge_count: hedgeCount },
|
|
424
|
+
epr: { entropy_risk: Math.round(eprRisk * 10000) / 10000, bigram_entropy: Math.round(entropy * 100) / 100 },
|
|
425
|
+
flagged_claims: flaggedClaims.slice(0, 10),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
330
429
|
default:
|
|
331
430
|
throw new Error(`Unknown tool: ${name}`);
|
|
332
431
|
}
|
package/js/value_tracker.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ValueTracker — JS port of entroly/value_tracker.py
|
|
2
|
+
* ValueTracker — JS port of entroly/value_tracker.py (schema v3)
|
|
3
3
|
*
|
|
4
4
|
* Persistent, lifetime-savings accounting with the self-funded evolution
|
|
5
5
|
* budget invariant:
|
|
6
6
|
*
|
|
7
7
|
* C_spent(t) ≤ τ · S(t) (τ = 5%)
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* CROSS-RUNTIME CONTRACT: this writes the SAME file, SAME directory and
|
|
10
|
+
* SAME JSON schema as the Python tracker so that one shared dashboard
|
|
11
|
+
* shows value no matter whether the user installed via pip (MCP/proxy),
|
|
12
|
+
* the SDK, or npm (this wasm runtime). The directory resolution and the
|
|
13
|
+
* UTC date-key formats below MUST stay byte-identical to
|
|
14
|
+
* entroly/value_tracker.py — diverging them silently re-creates the
|
|
15
|
+
* "blank dashboard for npm users" bug.
|
|
16
|
+
*
|
|
17
|
+
* Atomic-write safe; survives process restarts.
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
'use strict';
|
|
@@ -18,35 +25,22 @@ const os = require('os');
|
|
|
18
25
|
|
|
19
26
|
const EVOLUTION_TAX_RATE = 0.05;
|
|
20
27
|
const FILE_NAME = 'value_tracker.json';
|
|
28
|
+
const ACTIVITY_NAME = 'activity.jsonl';
|
|
29
|
+
const SCHEMA_VERSION = 3;
|
|
30
|
+
const MAX_DAILY = 90, MAX_WEEKLY = 52, MAX_MONTHLY = 24, MAX_ACTIVITY = 200;
|
|
21
31
|
|
|
22
32
|
// Per-model $/1M tokens — kept in sync with entroly/value_tracker.py (×1000).
|
|
23
33
|
const COST_PER_M = {
|
|
24
34
|
default: 3.0,
|
|
25
|
-
|
|
26
|
-
'gpt-
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'o1-mini': 3.0,
|
|
33
|
-
'o3': 10.0,
|
|
34
|
-
'o3-mini': 1.1,
|
|
35
|
-
'o4-mini': 1.1,
|
|
36
|
-
// Anthropic
|
|
37
|
-
'claude-opus-4': 15.0,
|
|
38
|
-
'claude-sonnet-4': 3.0,
|
|
39
|
-
'claude-haiku-4': 0.8,
|
|
40
|
-
'claude-3-5-sonnet': 3.0,
|
|
41
|
-
'claude-3-5-haiku': 0.8,
|
|
42
|
-
// Google
|
|
43
|
-
'gemini-2.5-pro': 1.25,
|
|
44
|
-
'gemini-2.5-flash': 0.075,
|
|
45
|
-
'gemini-1.5-pro': 1.25,
|
|
46
|
-
'gemini-1.5-flash': 0.075,
|
|
35
|
+
'gpt-4o': 2.5, 'gpt-4o-mini': 0.15, 'gpt-4-turbo': 10.0, 'gpt-4': 30.0,
|
|
36
|
+
'gpt-3.5-turbo': 0.5, 'o1': 15.0, 'o1-mini': 3.0, 'o3': 10.0,
|
|
37
|
+
'o3-mini': 1.1, 'o4-mini': 1.1,
|
|
38
|
+
'claude-opus-4': 15.0, 'claude-sonnet-4': 3.0, 'claude-haiku-4': 0.8,
|
|
39
|
+
'claude-3-5-sonnet': 3.0, 'claude-3-5-haiku': 0.8,
|
|
40
|
+
'gemini-2.5-pro': 1.25, 'gemini-2.5-flash': 0.075,
|
|
41
|
+
'gemini-1.5-pro': 1.25, 'gemini-1.5-flash': 0.075,
|
|
47
42
|
};
|
|
48
43
|
|
|
49
|
-
// Old→new name aliases so 'claude-3-opus' hits the same rate as Python.
|
|
50
44
|
const MODEL_ALIASES = {
|
|
51
45
|
'claude-3-opus': 'claude-opus-4',
|
|
52
46
|
'claude-3-sonnet': 'claude-sonnet-4',
|
|
@@ -60,11 +54,10 @@ function estimateCost(tokens, model = '') {
|
|
|
60
54
|
for (const [alias, canonical] of Object.entries(MODEL_ALIASES)) {
|
|
61
55
|
if (m.startsWith(alias)) { m = canonical + m.slice(alias.length); break; }
|
|
62
56
|
}
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const keys = Object.keys(COST_PER_M).filter(k => k !== 'default')
|
|
58
|
+
.sort((a, b) => b.length - a.length);
|
|
65
59
|
const key = (m && keys.find(k => m.startsWith(k)));
|
|
66
60
|
if (!key && model) {
|
|
67
|
-
// One-time warning per unknown model — stderr, doesn't pollute stdout.
|
|
68
61
|
if (!estimateCost._warned) estimateCost._warned = new Set();
|
|
69
62
|
if (!estimateCost._warned.has(model)) {
|
|
70
63
|
estimateCost._warned.add(model);
|
|
@@ -74,6 +67,29 @@ function estimateCost(tokens, model = '') {
|
|
|
74
67
|
return (tokens / 1_000_000) * COST_PER_M[key || 'default'];
|
|
75
68
|
}
|
|
76
69
|
|
|
70
|
+
// ── UTC date keys — MUST match Python time.gmtime()-based keys ──────────
|
|
71
|
+
|
|
72
|
+
function _pad(n) { return String(n).padStart(2, '0'); }
|
|
73
|
+
|
|
74
|
+
function _dayKey(d = new Date()) {
|
|
75
|
+
return `${d.getUTCFullYear()}-${_pad(d.getUTCMonth() + 1)}-${_pad(d.getUTCDate())}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _monthKey(d = new Date()) {
|
|
79
|
+
return `${d.getUTCFullYear()}-${_pad(d.getUTCMonth() + 1)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _weekKey(d = new Date()) {
|
|
83
|
+
// ISO-8601 week, matching Python datetime.date.isocalendar().
|
|
84
|
+
const t = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
85
|
+
const day = t.getUTCDay() || 7; // Mon=1..Sun=7
|
|
86
|
+
t.setUTCDate(t.getUTCDate() + 4 - day); // nearest Thursday
|
|
87
|
+
const isoYear = t.getUTCFullYear();
|
|
88
|
+
const yearStart = new Date(Date.UTC(isoYear, 0, 1));
|
|
89
|
+
const week = Math.ceil((((t - yearStart) / 86400000) + 1) / 7);
|
|
90
|
+
return `${isoYear}-W${_pad(week)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
77
93
|
function atomicWrite(filePath, content) {
|
|
78
94
|
const tmp = filePath + '.tmp-' + process.pid;
|
|
79
95
|
fs.writeFileSync(tmp, content, 'utf8');
|
|
@@ -82,10 +98,16 @@ function atomicWrite(filePath, content) {
|
|
|
82
98
|
|
|
83
99
|
class ValueTracker {
|
|
84
100
|
constructor(dataDir = null) {
|
|
85
|
-
|
|
101
|
+
// Honor ENTROLY_DIR exactly like Python's _default_dir(); else
|
|
102
|
+
// ~/.entroly. NOT cwd/.entroly — that was the npm/py path split.
|
|
103
|
+
this._dir = dataDir
|
|
104
|
+
|| process.env.ENTROLY_DIR
|
|
105
|
+
|| path.join(os.homedir(), '.entroly');
|
|
86
106
|
fs.mkdirSync(this._dir, { recursive: true });
|
|
87
107
|
this._path = path.join(this._dir, FILE_NAME);
|
|
88
|
-
this.
|
|
108
|
+
this._activityPath = path.join(this._dir, ACTIVITY_NAME);
|
|
109
|
+
this._data = this._migrate(this._load());
|
|
110
|
+
this._activity = this._loadActivity();
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
_load() {
|
|
@@ -101,36 +123,143 @@ class ValueTracker {
|
|
|
101
123
|
_defaults() {
|
|
102
124
|
const now = Date.now() / 1000;
|
|
103
125
|
return {
|
|
104
|
-
version:
|
|
126
|
+
version: SCHEMA_VERSION,
|
|
105
127
|
lifetime: {
|
|
106
|
-
tokens_saved: 0,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
last_seen: now,
|
|
111
|
-
evolution_spent_usd: 0.0,
|
|
112
|
-
evolution_attempts: 0,
|
|
128
|
+
tokens_saved: 0, cost_saved_usd: 0.0,
|
|
129
|
+
requests_optimized: 0, requests_total: 0, duplicates_caught: 0,
|
|
130
|
+
first_seen: now, last_seen: now,
|
|
131
|
+
evolution_spent_usd: 0.0, evolution_attempts: 0,
|
|
113
132
|
evolution_successes: 0,
|
|
133
|
+
hallucinations_blocked: 0, routing_saved_usd: 0.0,
|
|
134
|
+
routing_decisions: 0,
|
|
114
135
|
},
|
|
136
|
+
daily: {}, weekly: {}, monthly: {},
|
|
115
137
|
};
|
|
116
138
|
}
|
|
117
139
|
|
|
140
|
+
_migrate(data) {
|
|
141
|
+
// Backward-compatible forward migration (v2 → v3): backfill any
|
|
142
|
+
// missing keys/buckets without touching existing counters.
|
|
143
|
+
const base = this._defaults();
|
|
144
|
+
data.lifetime = data.lifetime || {};
|
|
145
|
+
for (const [k, v] of Object.entries(base.lifetime)) {
|
|
146
|
+
if (!(k in data.lifetime)) data.lifetime[k] = v;
|
|
147
|
+
}
|
|
148
|
+
for (const b of ['daily', 'weekly', 'monthly']) {
|
|
149
|
+
if (!data[b] || typeof data[b] !== 'object') data[b] = {};
|
|
150
|
+
}
|
|
151
|
+
data.version = SCHEMA_VERSION;
|
|
152
|
+
return data;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_loadActivity() {
|
|
156
|
+
if (!fs.existsSync(this._activityPath)) return [];
|
|
157
|
+
try {
|
|
158
|
+
const out = [];
|
|
159
|
+
for (const line of fs.readFileSync(this._activityPath, 'utf8').split('\n')) {
|
|
160
|
+
const s = line.trim();
|
|
161
|
+
if (!s) continue;
|
|
162
|
+
try { out.push(JSON.parse(s)); } catch (_) { /* skip */ }
|
|
163
|
+
}
|
|
164
|
+
return out.slice(-MAX_ACTIVITY);
|
|
165
|
+
} catch (_) { return []; }
|
|
166
|
+
}
|
|
167
|
+
|
|
118
168
|
_save() {
|
|
119
169
|
try { atomicWrite(this._path, JSON.stringify(this._data, null, 2)); }
|
|
120
170
|
catch (_) { /* best-effort */ }
|
|
121
171
|
}
|
|
122
172
|
|
|
123
|
-
|
|
173
|
+
_saveActivity() {
|
|
174
|
+
try {
|
|
175
|
+
this._activity = this._activity.slice(-MAX_ACTIVITY);
|
|
176
|
+
const content = this._activity.map(e => JSON.stringify(e)).join('\n')
|
|
177
|
+
+ (this._activity.length ? '\n' : '');
|
|
178
|
+
atomicWrite(this._activityPath, content);
|
|
179
|
+
} catch (_) { /* best-effort */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_bump(bucketName, key, tokens, cost) {
|
|
183
|
+
const bucket = this._data[bucketName] || (this._data[bucketName] = {});
|
|
184
|
+
if (!bucket[key]) bucket[key] = { tokens_saved: 0, cost_saved: 0.0, requests: 0 };
|
|
185
|
+
bucket[key].tokens_saved += tokens;
|
|
186
|
+
bucket[key].cost_saved = +(bucket[key].cost_saved + cost).toFixed(6);
|
|
187
|
+
bucket[key].requests += 1;
|
|
188
|
+
const limit = { daily: MAX_DAILY, weekly: MAX_WEEKLY, monthly: MAX_MONTHLY }[bucketName];
|
|
189
|
+
const ks = Object.keys(bucket);
|
|
190
|
+
if (ks.length > limit) {
|
|
191
|
+
ks.sort();
|
|
192
|
+
for (const old of ks.slice(0, ks.length - limit)) delete bucket[old];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
record({ tokensSaved = 0, model = '', duplicates = 0, optimized = true } = {}) {
|
|
124
197
|
const cost = estimateCost(tokensSaved, model);
|
|
198
|
+
const now = new Date();
|
|
125
199
|
const lt = this._data.lifetime;
|
|
126
200
|
lt.tokens_saved += tokensSaved;
|
|
127
201
|
lt.cost_saved_usd = +(lt.cost_saved_usd + cost).toFixed(6);
|
|
202
|
+
lt.requests_total = (lt.requests_total || 0) + 1;
|
|
128
203
|
if (optimized) lt.requests_optimized += 1;
|
|
204
|
+
lt.duplicates_caught = (lt.duplicates_caught || 0) + duplicates;
|
|
129
205
|
lt.last_seen = Date.now() / 1000;
|
|
206
|
+
this._bump('daily', _dayKey(now), tokensSaved, cost);
|
|
207
|
+
this._bump('weekly', _weekKey(now), tokensSaved, cost);
|
|
208
|
+
this._bump('monthly', _monthKey(now), tokensSaved, cost);
|
|
130
209
|
this._save();
|
|
210
|
+
this._activity.push({
|
|
211
|
+
ts: +(Date.now() / 1000).toFixed(3),
|
|
212
|
+
kind: 'optimize',
|
|
213
|
+
summary: `Optimized request: saved ${tokensSaved.toLocaleString()} tokens`
|
|
214
|
+
+ (model ? ` (${model})` : ''),
|
|
215
|
+
tokens_saved: tokensSaved,
|
|
216
|
+
cost_saved_usd: +cost.toFixed(6),
|
|
217
|
+
model: model || '',
|
|
218
|
+
duplicates,
|
|
219
|
+
source: 'npm',
|
|
220
|
+
});
|
|
221
|
+
this._saveActivity();
|
|
131
222
|
return { tokensSaved, costSaved: cost };
|
|
132
223
|
}
|
|
133
224
|
|
|
225
|
+
recordEvent(kind, summary, opts = {}) {
|
|
226
|
+
try {
|
|
227
|
+
const row = {
|
|
228
|
+
ts: +(Date.now() / 1000).toFixed(3),
|
|
229
|
+
kind: String(kind),
|
|
230
|
+
summary: String(summary).slice(0, 240),
|
|
231
|
+
source: opts.source || 'npm',
|
|
232
|
+
};
|
|
233
|
+
if (opts.tokensSaved) row.tokens_saved = opts.tokensSaved | 0;
|
|
234
|
+
if (opts.costSavedUsd) row.cost_saved_usd = +Number(opts.costSavedUsd).toFixed(6);
|
|
235
|
+
if (opts.model) row.model = opts.model;
|
|
236
|
+
this._activity.push(row);
|
|
237
|
+
this._saveActivity();
|
|
238
|
+
} catch (_) { /* fail-open */ }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
recordHallucinationBlocked(n = 1, detail = '') {
|
|
242
|
+
try {
|
|
243
|
+
this._data.lifetime.hallucinations_blocked =
|
|
244
|
+
(this._data.lifetime.hallucinations_blocked || 0) + (n | 0);
|
|
245
|
+
this._save();
|
|
246
|
+
this.recordEvent('hallucination',
|
|
247
|
+
detail || `Blocked ${n} unsupported claim(s)`, { source: 'witness' });
|
|
248
|
+
} catch (_) { /* fail-open */ }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
recordRoutingSaving(costSavedUsd, chosenModel = '', detail = '') {
|
|
252
|
+
try {
|
|
253
|
+
const lt = this._data.lifetime;
|
|
254
|
+
lt.routing_saved_usd = +((lt.routing_saved_usd || 0) + Number(costSavedUsd)).toFixed(6);
|
|
255
|
+
lt.routing_decisions = (lt.routing_decisions || 0) + 1;
|
|
256
|
+
this._save();
|
|
257
|
+
this.recordEvent('routing',
|
|
258
|
+
detail || `Routed to ${chosenModel || 'cheaper model'}`,
|
|
259
|
+
{ source: 'ravs', costSavedUsd, model: chosenModel });
|
|
260
|
+
} catch (_) { /* fail-open */ }
|
|
261
|
+
}
|
|
262
|
+
|
|
134
263
|
getEvolutionBudget() {
|
|
135
264
|
const lt = this._data.lifetime;
|
|
136
265
|
const lifetimeSaved = lt.cost_saved_usd || 0;
|
|
@@ -152,14 +281,9 @@ class ValueTracker {
|
|
|
152
281
|
const currentSpent = lt.evolution_spent_usd || 0;
|
|
153
282
|
const totalEarned = lifetimeSaved * EVOLUTION_TAX_RATE;
|
|
154
283
|
const available = totalEarned - currentSpent;
|
|
155
|
-
|
|
156
284
|
if (costUsd > available + 0.001) {
|
|
157
|
-
return {
|
|
158
|
-
status: 'rejected',
|
|
159
|
-
remainingUsd: +Math.max(0, available).toFixed(6),
|
|
160
|
-
};
|
|
285
|
+
return { status: 'rejected', remainingUsd: +Math.max(0, available).toFixed(6) };
|
|
161
286
|
}
|
|
162
|
-
|
|
163
287
|
lt.evolution_spent_usd = +(currentSpent + costUsd).toFixed(6);
|
|
164
288
|
lt.evolution_attempts = (lt.evolution_attempts || 0) + 1;
|
|
165
289
|
if (success) lt.evolution_successes = (lt.evolution_successes || 0) + 1;
|
|
@@ -170,12 +294,43 @@ class ValueTracker {
|
|
|
170
294
|
};
|
|
171
295
|
}
|
|
172
296
|
|
|
297
|
+
// ── Read APIs for the entroly_dashboard MCP tool ───────────────────────
|
|
298
|
+
|
|
299
|
+
_sortedBucket(name, lastN) {
|
|
300
|
+
const b = this._data[name] || {};
|
|
301
|
+
return Object.keys(b).sort().slice(-lastN).map(k => ({ key: k, ...b[k] }));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getTrends() {
|
|
305
|
+
return {
|
|
306
|
+
lifetime: { ...this._data.lifetime },
|
|
307
|
+
daily: this._sortedBucket('daily', 30),
|
|
308
|
+
weekly: this._sortedBucket('weekly', 12),
|
|
309
|
+
monthly: this._sortedBucket('monthly', 12),
|
|
310
|
+
activity: this._activity.slice(-50).reverse(),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
getActivity(lastN = 50) {
|
|
315
|
+
return this._activity.slice(-lastN).reverse();
|
|
316
|
+
}
|
|
317
|
+
|
|
173
318
|
stats() {
|
|
174
319
|
return {
|
|
175
320
|
lifetime: { ...this._data.lifetime },
|
|
176
321
|
budget: this.getEvolutionBudget(),
|
|
322
|
+
activity: this.getActivity(20),
|
|
177
323
|
};
|
|
178
324
|
}
|
|
179
325
|
}
|
|
180
326
|
|
|
181
|
-
|
|
327
|
+
// Module-level singleton, mirroring Python get_tracker().
|
|
328
|
+
let _tracker = null;
|
|
329
|
+
function getTracker() {
|
|
330
|
+
if (!_tracker) _tracker = new ValueTracker();
|
|
331
|
+
return _tracker;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
ValueTracker, EVOLUTION_TAX_RATE, estimateCost, getTracker,
|
|
336
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "entroly-wasm",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.5",
|
|
4
4
|
"description": "Information-theoretic context optimization for AI coding agents. Zero-friction, pure WebAssembly - no Python dependency.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "pkg/entroly_wasm.d.ts",
|
package/pkg/entroly_wasm_bg.wasm
CHANGED
|
Binary file
|