entroly-wasm 0.9.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/js/distill.js ADDED
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Response Distillation — Output-Side Token Optimization (JavaScript)
3
+ * ====================================================================
4
+ *
5
+ * Strips filler, pleasantries, hedging, and meta-commentary from LLM
6
+ * responses while preserving all code blocks and technical content.
7
+ *
8
+ * Grounded in:
9
+ * - Selective Context (Li et al., EMNLP 2023) — self-information scoring
10
+ * - TRIM (arXiv 2025) — omit inferable words
11
+ *
12
+ * Usage:
13
+ * const { distillResponse } = require('entroly-wasm/js/distill');
14
+ * const { text, originalTokens, compressedTokens } = distillResponse(llmOutput);
15
+ *
16
+ * @module distill
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ // ── Filler patterns: high-frequency, zero-information phrases ──
22
+
23
+ const FILLER_PATTERNS = [
24
+ // Pleasantries (zero technical content)
25
+ [/^(?:Sure!?\s*|Of course!?\s*|Absolutely!?\s*|Great question!?\s*|Happy to help!?\s*|I'd be happy to help\.?\s*|Let me help you with that\.?\s*|No problem!?\s*|Certainly!?\s*|You're welcome!?\s*)/i, ''],
26
+ // Preamble ("I'm going to...")
27
+ [/^(?:Let me |I'll |I will |I'm going to |I can |I need to )(?:take a look|look at|review|examine|analyze|check|investigate)\b[^.]*\.\s*/i, ''],
28
+ // Meta-commentary ("Here's what I found")
29
+ [/^(?:Here(?:'s| is) (?:what|how|the|my|a|an)\b[^:]*:\s*)/i, ''],
30
+ // Hedging (near-zero information)
31
+ [/\b(?:I think |I believe |It seems like |It looks like |It appears that |As far as I can tell,?\s*|From what I can see,?\s*|If I understand correctly,?\s*)/gi, ''],
32
+ // Filler transitions
33
+ [/^(?:Now,?\s*|Next,?\s*|Then,?\s*|After that,?\s*|Moving on,?\s*|With that said,?\s*|That being said,?\s*|Having said that,?\s*)(?:let(?:'s| us) )/i, ''],
34
+ // Closing pleasantries
35
+ [/(?:Let me know if (?:you (?:have|need)|there(?:'s| is)|that)\b[^.!]*[.!]?\s*$)/i, ''],
36
+ [/(?:Feel free to (?:ask|reach out|let me know)\b[^.!]*[.!]?\s*$)/i, ''],
37
+ [/(?:Hope (?:this|that) helps!?\s*$)/i, ''],
38
+ [/(?:Is there anything else\b[^.?!]*[.?!]?\s*$)/i, ''],
39
+ // Redundant acknowledgments
40
+ [/^(?:I see\.|I understand\.|Got it\.|Right\.|Okay\.)\s*/i, ''],
41
+ // Verbose connectors
42
+ [/\bIn order to\b/i, 'To'],
43
+ [/\bDue to the fact that\b/i, 'Because'],
44
+ [/\bAt this point in time\b/i, 'Now'],
45
+ [/\bFor the purpose of\b/i, 'For'],
46
+ [/\bIn the event that\b/i, 'If'],
47
+ [/\bWith regard to\b/i, 'About'],
48
+ [/\bIt is important to note that\b/i, 'Note:'],
49
+ [/\bAs (?:mentioned|noted|stated) (?:earlier|above|previously),?\s*/i, ''],
50
+ ];
51
+
52
+ // Lines that are pure filler (entire line is fluff)
53
+ const PURE_FILLER = /^(?:Sure(?:,| thing).*|I(?:'d| would) (?:be happy|love) to.*|Let me (?:know|explain|walk you through).*|(?:Here(?:'s| is) (?:a|the) (?:summary|breakdown|overview|explanation).*)|(?:To (?:summarize|recap|sum up):?.*)|(?:In (?:summary|conclusion):?.*))$/i;
54
+
55
+ /**
56
+ * Apply response distillation to LLM output text.
57
+ *
58
+ * @param {string} text - Raw LLM response text
59
+ * @param {Object} [options]
60
+ * @param {'lite'|'full'|'ultra'} [options.mode='full'] - Compression intensity
61
+ * @returns {{ text: string, originalTokens: number, compressedTokens: number }}
62
+ */
63
+ function distillResponse(text, options = {}) {
64
+ const mode = options.mode || 'full';
65
+
66
+ if (!text || text.length < 50) {
67
+ const count = text ? text.split(/\s+/).filter(Boolean).length : 0;
68
+ return { text: text || '', originalTokens: count, compressedTokens: count };
69
+ }
70
+
71
+ const originalTokens = text.split(/\s+/).filter(Boolean).length;
72
+
73
+ // Split into code blocks and prose — NEVER touch code blocks
74
+ const parts = text.split(/(```[\s\S]*?```)/);
75
+ const compressed = parts.map(part => {
76
+ if (part.startsWith('```')) return part;
77
+ return _compressProse(part, mode);
78
+ });
79
+
80
+ let result = compressed.join('');
81
+ result = result.replace(/\n{3,}/g, '\n\n').trim();
82
+
83
+ const compressedTokens = result ? result.split(/\s+/).filter(Boolean).length : 0;
84
+
85
+ return { text: result, originalTokens, compressedTokens };
86
+ }
87
+
88
+ /**
89
+ * Compress a prose section (not inside a code block).
90
+ * @private
91
+ */
92
+ function _compressProse(text, mode) {
93
+ const lines = text.split('\n');
94
+ const output = [];
95
+
96
+ for (const line of lines) {
97
+ const stripped = line.trim();
98
+
99
+ // Skip pure filler lines
100
+ if (stripped && PURE_FILLER.test(stripped)) continue;
101
+
102
+ let compressed = stripped;
103
+
104
+ if (mode === 'full' || mode === 'ultra') {
105
+ for (const [pattern, replacement] of FILLER_PATTERNS) {
106
+ compressed = compressed.replace(pattern, replacement);
107
+ }
108
+ } else if (mode === 'lite') {
109
+ // Lite: only first 4 patterns (pleasantries + preamble)
110
+ for (const [pattern, replacement] of FILLER_PATTERNS.slice(0, 4)) {
111
+ compressed = compressed.replace(pattern, replacement);
112
+ }
113
+ }
114
+
115
+ // Ultra: also strip articles and filler words
116
+ if (mode === 'ultra') {
117
+ compressed = compressed.replace(/\b(?:the|a|an|just|simply|basically|essentially)\s+/g, '');
118
+ compressed = compressed.replace(/\s{2,}/g, ' ');
119
+ }
120
+
121
+ if (compressed.trim()) {
122
+ const leading = line.length - line.trimStart().length;
123
+ output.push(' '.repeat(leading) + compressed.trim());
124
+ } else if (!stripped) {
125
+ output.push('');
126
+ }
127
+ }
128
+
129
+ return output.join('\n');
130
+ }
131
+
132
+ /**
133
+ * Distill a single SSE content delta for streaming responses.
134
+ *
135
+ * @param {string} chunk - SSE content delta text
136
+ * @param {Object} [options]
137
+ * @param {'lite'|'full'|'ultra'} [options.mode='full']
138
+ * @returns {string} Distilled chunk
139
+ */
140
+ function distillSSEChunk(chunk, options = {}) {
141
+ const mode = options.mode || 'full';
142
+
143
+ if (!chunk || chunk.length < 10) return chunk;
144
+ if (chunk.includes('```') || chunk.startsWith(' ')) return chunk;
145
+
146
+ let result = chunk;
147
+ const patternsToApply = FILLER_PATTERNS.slice(0, 8);
148
+ for (const [pattern, replacement] of patternsToApply) {
149
+ result = result.replace(pattern, replacement);
150
+ }
151
+
152
+ return result;
153
+ }
154
+
155
+ module.exports = { distillResponse, distillSSEChunk };
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Evolution Daemon — Zero-Token Autonomous Self-Improvement (JavaScript)
3
+ * ======================================================================
4
+ *
5
+ * Background daemon that orchestrates:
6
+ * 1. Dreaming Loop — idle-time weight optimization via self-play
7
+ * 2. Federation — contribute + merge global weights via GitHub
8
+ *
9
+ * Mirrors the Python EvolutionDaemon but adapted for Node.js event loop.
10
+ *
11
+ * @module evolution_daemon
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const { FeedbackJournal, optimize, WEIGHT_KEYS, DEFAULTS } = require('./autotune');
17
+
18
+ // ── Dreaming Loop ──
19
+
20
+ const IDLE_THRESHOLD_MS = 60000; // 60s idle before dreaming
21
+ const DREAM_PERTURBATION = 0.08; // Weight perturbation magnitude
22
+ const DREAM_MAX_TRIALS = 5; // Trials per dream cycle
23
+
24
+ class DreamingLoop {
25
+ /**
26
+ * @param {FeedbackJournal} journal
27
+ * @param {Object} [options]
28
+ * @param {number} [options.idleThresholdMs]
29
+ */
30
+ constructor(journal, options = {}) {
31
+ this._journal = journal;
32
+ this._idleThreshold = options.idleThresholdMs || IDLE_THRESHOLD_MS;
33
+ this._lastActivity = Date.now();
34
+ this._stats = { cycles: 0, improvements: 0, totalTrials: 0 };
35
+ }
36
+
37
+ /** Record user activity — resets idle timer. */
38
+ recordActivity() {
39
+ this._lastActivity = Date.now();
40
+ }
41
+
42
+ /** Check if system has been idle long enough to dream. */
43
+ shouldDream() {
44
+ return (Date.now() - this._lastActivity) >= this._idleThreshold;
45
+ }
46
+
47
+ /**
48
+ * Run one dream cycle: perturb weights, evaluate, keep improvements.
49
+ *
50
+ * Uses counterfactual self-play:
51
+ * 1. Load best weights from journal history
52
+ * 2. Generate random perturbations
53
+ * 3. Score each variant against journal episodes
54
+ * 4. If any beats current → adopt it
55
+ *
56
+ * @param {Object} currentWeights - Current {w_r, w_f, w_s, w_e}
57
+ * @returns {{ status: string, improvements: number, bestScore: number }}
58
+ */
59
+ runDreamCycle(currentWeights) {
60
+ const episodes = this._journal.load();
61
+ if (episodes.length < 5) {
62
+ return { status: 'insufficient_data', improvements: 0, bestScore: 0 };
63
+ }
64
+
65
+ this._stats.cycles++;
66
+ let bestWeights = { ...currentWeights };
67
+ let bestScore = this._scoreWeights(currentWeights, episodes);
68
+ let improvements = 0;
69
+
70
+ for (let trial = 0; trial < DREAM_MAX_TRIALS; trial++) {
71
+ this._stats.totalTrials++;
72
+
73
+ // Generate perturbation (Gaussian)
74
+ const candidate = {};
75
+ for (const k of WEIGHT_KEYS) {
76
+ const noise = (Math.random() + Math.random() + Math.random() - 1.5) * DREAM_PERTURBATION;
77
+ candidate[k] = Math.max(0.05, Math.min(0.80, (bestWeights[k] || 0.25) + noise));
78
+ }
79
+ // Normalize to sum=1
80
+ const sum = WEIGHT_KEYS.reduce((s, k) => s + candidate[k], 0);
81
+ for (const k of WEIGHT_KEYS) candidate[k] = Math.round(candidate[k] / sum * 10000) / 10000;
82
+
83
+ const score = this._scoreWeights(candidate, episodes);
84
+ if (score > bestScore) {
85
+ bestScore = score;
86
+ bestWeights = { ...candidate };
87
+ improvements++;
88
+ }
89
+ }
90
+
91
+ if (improvements > 0) {
92
+ this._stats.improvements += improvements;
93
+ }
94
+
95
+ return {
96
+ status: 'completed',
97
+ improvements,
98
+ bestScore: Math.round(bestScore * 1000) / 1000,
99
+ bestWeights,
100
+ trials: DREAM_MAX_TRIALS,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Score a weight configuration against historical episodes.
106
+ * Simulates reward: higher reward episodes → higher agreement with weights.
107
+ * @private
108
+ */
109
+ _scoreWeights(weights, episodes) {
110
+ let totalScore = 0;
111
+ let count = 0;
112
+
113
+ for (const ep of episodes) {
114
+ if (!ep.w || ep.r === undefined) continue;
115
+
116
+ // Cosine similarity between candidate weights and episode's weights
117
+ const epW = ep.w;
118
+ let dot = 0, normA = 0, normB = 0;
119
+ for (const k of WEIGHT_KEYS) {
120
+ const a = weights[k] || 0.25;
121
+ const b = epW[k] || epW[k.replace('w_', 'w_')] || 0.25;
122
+ dot += a * b;
123
+ normA += a * a;
124
+ normB += b * b;
125
+ }
126
+ const similarity = dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-8);
127
+
128
+ // Reward-weighted: positive episodes attract, negative repel
129
+ totalScore += ep.r * similarity;
130
+ count++;
131
+ }
132
+
133
+ return count > 0 ? totalScore / count : 0;
134
+ }
135
+
136
+ get stats() { return { ...this._stats }; }
137
+ }
138
+
139
+
140
+ // ── Evolution Daemon ──
141
+
142
+ class EvolutionDaemon {
143
+ /**
144
+ * @param {Object} options
145
+ * @param {string} options.checkpointDir - Directory for state
146
+ * @param {Object} [options.engine] - WASM engine instance
147
+ * @param {number} [options.pollIntervalMs=30000]
148
+ */
149
+ constructor(options = {}) {
150
+ this._checkpointDir = options.checkpointDir || '.entroly';
151
+ this._engine = options.engine || null;
152
+ this._pollInterval = options.pollIntervalMs || 30000;
153
+
154
+ // Journal
155
+ this._journal = new FeedbackJournal(this._checkpointDir);
156
+
157
+ // Dreaming
158
+ this._dreaming = new DreamingLoop(this._journal);
159
+
160
+ // Federation (lazy loaded)
161
+ this._federation = null;
162
+ this._transport = null;
163
+ this._federationCycleCounter = 0;
164
+
165
+ // State
166
+ this._timer = null;
167
+ this._stats = {
168
+ dreamCycles: 0,
169
+ federationContributions: 0,
170
+ federationMerges: 0,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Initialize federation (call after construction).
176
+ */
177
+ async initFederation() {
178
+ try {
179
+ const { FederationClient, GitHubTransport } = require('./federation');
180
+ this._federation = new FederationClient({ dataDir: this._checkpointDir });
181
+
182
+ if (this._federation.enabled) {
183
+ this._transport = new GitHubTransport();
184
+
185
+ // Sync on startup
186
+ try {
187
+ const packets = await this._transport.pull(48);
188
+ let saved = 0;
189
+ for (const packet of packets) {
190
+ if (this._federation.saveContribution(packet)) saved++;
191
+ }
192
+ if (saved > 0) {
193
+ this._federation.mergeGlobal();
194
+ this._stats.federationMerges++;
195
+ }
196
+ } catch {}
197
+ }
198
+ } catch {}
199
+ }
200
+
201
+ /** Record user activity — resets dreaming idle timer. */
202
+ recordActivity() {
203
+ this._dreaming.recordActivity();
204
+ }
205
+
206
+ /**
207
+ * Start the daemon (non-blocking, uses setInterval).
208
+ */
209
+ start() {
210
+ if (this._timer) return;
211
+
212
+ this._timer = setInterval(() => {
213
+ try {
214
+ this.runOnce();
215
+ } catch (e) {
216
+ console.error(`[evolution] Error: ${e.message}`);
217
+ }
218
+ }, this._pollInterval);
219
+
220
+ // Make timer non-blocking (don't prevent process exit)
221
+ if (this._timer.unref) this._timer.unref();
222
+
223
+ console.error('[evolution] Daemon started');
224
+ }
225
+
226
+ /** Stop the daemon. */
227
+ stop() {
228
+ if (this._timer) {
229
+ clearInterval(this._timer);
230
+ this._timer = null;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Execute one daemon cycle.
236
+ */
237
+ runOnce() {
238
+ const result = { dream: null, federation: null };
239
+
240
+ // ── Phase 1: Dreaming ──
241
+ if (this._dreaming.shouldDream()) {
242
+ const currentWeights = this._getCurrentWeights();
243
+ const dreamResult = this._dreaming.runDreamCycle(currentWeights);
244
+ result.dream = dreamResult;
245
+
246
+ if (dreamResult.status === 'completed') {
247
+ this._stats.dreamCycles++;
248
+
249
+ // Apply improved weights to engine
250
+ if (dreamResult.improvements > 0 && dreamResult.bestWeights && this._engine) {
251
+ const w = dreamResult.bestWeights;
252
+ try {
253
+ this._engine.set_weights(w.w_r, w.w_f, w.w_s, w.w_e);
254
+ } catch {}
255
+ }
256
+
257
+ // ── Phase 2: Federation ──
258
+ if (dreamResult.improvements > 0 && this._federation && this._federation.enabled) {
259
+ this._federationCycleCounter++;
260
+ this._handleFederation(dreamResult);
261
+ }
262
+ }
263
+ }
264
+
265
+ return result;
266
+ }
267
+
268
+ /** @private */
269
+ _getCurrentWeights() {
270
+ // Try to read from engine, fall back to defaults
271
+ if (this._engine && typeof this._engine.get_weights === 'function') {
272
+ try {
273
+ const w = this._engine.get_weights();
274
+ return {
275
+ w_r: w.recency ?? DEFAULTS.w_r,
276
+ w_f: w.frequency ?? DEFAULTS.w_f,
277
+ w_s: w.semantic_sim ?? DEFAULTS.w_s,
278
+ w_e: w.entropy ?? DEFAULTS.w_e,
279
+ };
280
+ } catch {}
281
+ }
282
+ return { ...DEFAULTS };
283
+ }
284
+
285
+ /** @private */
286
+ _handleFederation(dreamResult) {
287
+ if (!this._federation || !this._transport) return;
288
+
289
+ try {
290
+ // Contribute improved weights
291
+ const w = dreamResult.bestWeights;
292
+ const packet = this._federation.contribute(
293
+ 'js_project', // archetype
294
+ w,
295
+ this._journal.count(),
296
+ Math.min(1.0, this._journal.count() / 20),
297
+ );
298
+
299
+ if (packet && this._transport.canWrite) {
300
+ // Async push — fire and forget
301
+ this._transport.push(packet).then(ok => {
302
+ if (ok) this._stats.federationContributions++;
303
+ }).catch(() => {});
304
+ }
305
+
306
+ // Merge global every 10 cycles
307
+ if (this._federationCycleCounter % 10 === 0) {
308
+ this._transport.pull(48).then(packets => {
309
+ let saved = 0;
310
+ for (const p of packets) {
311
+ if (this._federation.saveContribution(p)) saved++;
312
+ }
313
+ if (saved > 0) {
314
+ this._federation.mergeGlobal();
315
+ this._stats.federationMerges++;
316
+
317
+ // Apply merged weights
318
+ const global = this._federation.getGlobalWeights('js_project');
319
+ if (global && this._engine) {
320
+ const gw = global.weights;
321
+ try {
322
+ this._engine.set_weights(gw.w_r, gw.w_f, gw.w_s, gw.w_e);
323
+ } catch {}
324
+ }
325
+ }
326
+ }).catch(() => {});
327
+ }
328
+ } catch {}
329
+ }
330
+
331
+ get stats() { return { ...this._stats, dreaming: this._dreaming.stats }; }
332
+ }
333
+
334
+
335
+ module.exports = { EvolutionDaemon, DreamingLoop };