@hmharness/evolution 0.14.26 → 0.14.27
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/dist/insights.d.ts +23 -0
- package/dist/insights.js +85 -0
- package/package.json +1 -1
package/dist/insights.d.ts
CHANGED
|
@@ -16,3 +16,26 @@ export declare function recordInsight(home: string, insight: Insight): Promise<v
|
|
|
16
16
|
export declare function readInsights(home: string, limit?: number): Promise<Insight[]>;
|
|
17
17
|
/** Summarize recent insights for the system prompt (bounded). */
|
|
18
18
|
export declare function recentInsights(home: string, limit?: number): Promise<string>;
|
|
19
|
+
/** Task-similarity feature set: per-WORD bigrams plus single chars as
|
|
20
|
+
* tokens (2026-09-27). Cross-word bigrams that include the space turned
|
|
21
|
+
* out to be a noise factory - 'fix' and 'xxx' shared 'x ' and unrelated
|
|
22
|
+
* natural sentences scored 0.06. Words are split on non-alphanumerics;
|
|
23
|
+
* CJK text has no spaces, so a whole Chinese clause becomes one word and
|
|
24
|
+
* keeps every character bigram. Pure. */
|
|
25
|
+
export declare function bigrams(s: string): Set<string>;
|
|
26
|
+
/** Jaccard similarity of two bigram sets. Pure. */
|
|
27
|
+
export declare function jaccard(a: Set<string>, b: Set<string>): number;
|
|
28
|
+
/** Noise floor: below this similarity an insight is unrelated to the task.
|
|
29
|
+
* With per-word bigrams the highest unrelated natural-language pairs score
|
|
30
|
+
* ~0.04 (stopword overlap: 'the' contributes th/he); related tasks land at
|
|
31
|
+
* 0.5+. The floor sits between the two, with margin on both sides. */
|
|
32
|
+
export declare const INSIGHT_SIM_FLOOR = 0.05;
|
|
33
|
+
/**
|
|
34
|
+
* The K most RELEVANT insights for this task (recency as tiebreak), falling
|
|
35
|
+
* back to the K most recent when nothing clears the noise floor - so the
|
|
36
|
+
* prompt always carries some experience, exactly like before.
|
|
37
|
+
*/
|
|
38
|
+
export declare function retrieveInsights(home: string, task: string, opts?: {
|
|
39
|
+
topK?: number;
|
|
40
|
+
pool?: number;
|
|
41
|
+
}): Promise<string>;
|
package/dist/insights.js
CHANGED
|
@@ -78,3 +78,88 @@ export async function recentInsights(home, limit = 5) {
|
|
|
78
78
|
return '';
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/* ---------------- experience retrieval (2026-09-27, plan A) ----------------
|
|
82
|
+
* The injection used to be "last 5 insights" - pure recency. After 300
|
|
83
|
+
* tasks, a question similar to task #50 got five UNRELATED recent lessons
|
|
84
|
+
* instead of that one. Industry direction (Agent KB / Agent Workflow
|
|
85
|
+
* Memory, verified 2026-09-27): retrieve relevant experience per task.
|
|
86
|
+
* Zero-dependency red line -> character-bigram Jaccard (works well for CJK
|
|
87
|
+
* too), a small recency tiebreak, graceful fallback to recency order. */
|
|
88
|
+
/** Task-similarity feature set: per-WORD bigrams plus single chars as
|
|
89
|
+
* tokens (2026-09-27). Cross-word bigrams that include the space turned
|
|
90
|
+
* out to be a noise factory - 'fix' and 'xxx' shared 'x ' and unrelated
|
|
91
|
+
* natural sentences scored 0.06. Words are split on non-alphanumerics;
|
|
92
|
+
* CJK text has no spaces, so a whole Chinese clause becomes one word and
|
|
93
|
+
* keeps every character bigram. Pure. */
|
|
94
|
+
export function bigrams(s) {
|
|
95
|
+
const words = s.toLowerCase().split(/[^a-z0-9\u4e00-\u9fff]+/).filter(Boolean);
|
|
96
|
+
const out = new Set();
|
|
97
|
+
for (const w of words) {
|
|
98
|
+
if (w.length === 1) {
|
|
99
|
+
out.add(w);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (let i = 0; i < w.length - 1; i++)
|
|
103
|
+
out.add(w.slice(i, i + 2));
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** Jaccard similarity of two bigram sets. Pure. */
|
|
108
|
+
export function jaccard(a, b) {
|
|
109
|
+
if (!a.size || !b.size)
|
|
110
|
+
return 0;
|
|
111
|
+
let inter = 0;
|
|
112
|
+
for (const g of a)
|
|
113
|
+
if (b.has(g))
|
|
114
|
+
inter++;
|
|
115
|
+
return inter / (a.size + b.size - inter);
|
|
116
|
+
}
|
|
117
|
+
function formatInsight(i) {
|
|
118
|
+
return `- [${i.outcome}] ${i.task.slice(0, 60)} (turns ${i.turns}, tools ${i.toolsUsed.join(',') || 'none'})`;
|
|
119
|
+
}
|
|
120
|
+
/** Noise floor: below this similarity an insight is unrelated to the task.
|
|
121
|
+
* With per-word bigrams the highest unrelated natural-language pairs score
|
|
122
|
+
* ~0.04 (stopword overlap: 'the' contributes th/he); related tasks land at
|
|
123
|
+
* 0.5+. The floor sits between the two, with margin on both sides. */
|
|
124
|
+
export const INSIGHT_SIM_FLOOR = 0.05;
|
|
125
|
+
/**
|
|
126
|
+
* The K most RELEVANT insights for this task (recency as tiebreak), falling
|
|
127
|
+
* back to the K most recent when nothing clears the noise floor - so the
|
|
128
|
+
* prompt always carries some experience, exactly like before.
|
|
129
|
+
*/
|
|
130
|
+
export async function retrieveInsights(home, task, opts = {}) {
|
|
131
|
+
const topK = opts.topK ?? 5;
|
|
132
|
+
const pool = opts.pool ?? 500;
|
|
133
|
+
let rows = [];
|
|
134
|
+
try {
|
|
135
|
+
const text = await readFile(join(home, 'insights', 'insights.jsonl'), 'utf8');
|
|
136
|
+
rows = text.trim().split('\n').filter(Boolean)
|
|
137
|
+
.map((l) => { try {
|
|
138
|
+
return JSON.parse(l);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
} })
|
|
143
|
+
.filter((x) => x !== null)
|
|
144
|
+
.slice(-pool);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return ''; // no archive at all - same behaviour as recentInsights
|
|
148
|
+
}
|
|
149
|
+
if (rows.length === 0)
|
|
150
|
+
return '';
|
|
151
|
+
const q = bigrams(task);
|
|
152
|
+
if (q.size === 0)
|
|
153
|
+
return rows.slice(-topK).map(formatInsight).join('\n');
|
|
154
|
+
// newest first within the pool, so idx IS the recency rank
|
|
155
|
+
const newestFirst = [...rows].reverse();
|
|
156
|
+
const scored = newestFirst
|
|
157
|
+
.map((r, idx) => ({ r, s: jaccard(q, bigrams(r.task)) + (1 - idx / (newestFirst.length + 1)) * 1e-4 }))
|
|
158
|
+
.filter((x) => x.s > INSIGHT_SIM_FLOOR)
|
|
159
|
+
.sort((a, b) => b.s - a.s)
|
|
160
|
+
.slice(0, topK)
|
|
161
|
+
.map((x) => x.r);
|
|
162
|
+
if (scored.length === 0)
|
|
163
|
+
return rows.slice(-topK).map(formatInsight).join('\n');
|
|
164
|
+
return scored.map(formatInsight).join('\n');
|
|
165
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hmharness/evolution",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.27",
|
|
4
4
|
"description": "hmharness evolution subsystem: persistent memory, insight capture, skill library, and the bench that gives evolution its fitness signal. First-class kernel citizen, not a plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|