Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Powered by the [Genome Evolution Protocol](https://evomap.ai) and

| Seam | dsh event | Behaviour |
| --- | --- | --- |
| Asset priming | `agent/pre-step`, once per turn | Searches the Proxy with that turn's own prompt — only the text the person typed — fetches the two leading matches that clear the similarity line and have a strategy, in one call, and injects the strategy of the first that actually came back, announced by the asset's short title rather than its hash. The hash appears once, on the line that asks for the outcome, because that call needs it. Ids the Hub declined to deliver are remembered for a day and skipped, so a slot is not spent twice on one. A lookup slower than the wait budget is injected into the next step instead of holding the current one. Assets already injected in this session are skipped. |
| Asset priming | `agent/pre-step`, once per turn | Searches the Proxy with that turn's own prompt — only the text the person typed — fetches the two leading matches that clear the similarity line and have a strategy, in one call, and injects the strategy of the first that actually came back, announced by the asset's short title rather than its hash, falling back to its description when the Hub's title is a truncated fragment. The hash appears once, on the line that asks for the outcome, because that call needs it. Ids the Hub declined to deliver are remembered for a day and skipped, so a slot is not spent twice on one. A lookup slower than the wait budget is injected into the next step instead of holding the current one. Assets already injected in this session are skipped. |
| Signal detection | successful `tools/result` for `write`, `edit`, or `str_replace_editor` | Tags the turn with improvement signals and injects one bounded notice per file/signal set. |
| Reuse feedback | `session/event` → `turn/end` | Reports each strategy injected during that turn to the Proxy with the turn's outcome, marked as automatic and unconfirmed. Injected asset ids are written to `~/.evolver/state/` as they are injected and read back at report time, so a report survives the process dying in between. Reports are keyed on the asset id alone, and each verdict is written twice: to the Proxy, and as a `value.reuse_hit` / `value.reuse_outcome` root event in `~/.evomap/evolution/root_events.jsonl`. The local write is the one that closes the loop — the actuator that re-orders future candidates reads root events, not the Hub. An asset the model reported itself with `evolver_asset_reuse_result` is left alone. |
| Verdict correction | `agent/pre-step` | When the prompt that opens a later turn in the same session plainly says the last answer did not hold, the verdicts already sent for that session are revised to `failed` — each asset at most once, since the Hub counts every report it receives. |
Expand Down
25 changes: 21 additions & 4 deletions src/prime.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,28 @@ function fetchedById(data) {
return new Map(found.filter((asset) => asset?.asset_id).map((asset) => [asset.asset_id, asset]));
}

const CJK_SCRIPT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
const MIN_CJK_TITLE_CHARS = 5;

function trimmedText(value) {
return typeof value === 'string' && value.trim() ? value.trim() : '';
}

// A Hub short_title is sometimes a truncated fragment: `Object`, `自动化小`.
// Character count cannot separate those from a good title, because `智能缓存优化`
// says as much in six characters as a Latin title says in forty.
function looksLikeAName(text) {
if (!text) return false;
if (CJK_SCRIPT.test(text)) return [...text].length >= MIN_CJK_TITLE_CHARS;
return /\s/.test(text);
}

function readableNameOf(hit, asset) {
for (const candidate of [hit?.short_title, asset?.short_title]) {
if (typeof candidate === 'string' && candidate.trim()) return candidate.trim().slice(0, TITLE_MAX_CHARS);
}
return hit?.asset_type ?? 'Gene';
const title = trimmedText(hit?.short_title) || trimmedText(asset?.short_title);
if (looksLikeAName(title)) return title.slice(0, TITLE_MAX_CHARS);
const described = trimmedText(hit?.nl_summary) || trimmedText(asset?.summary);
if (described) return described.slice(0, TITLE_MAX_CHARS);
return title || hit?.asset_type || 'Gene';
}

function similarityOf(hit) {
Expand Down
62 changes: 56 additions & 6 deletions test/prime.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ test('one asset is fetched and injected as its strategy alone', async () => {
assert.deepEqual(calls[1].body, { asset_ids: ['sha256:abc', 'sha256:second'] });
assert.equal(calls[0].signal, controller.signal);
assert.deepEqual(ids, ['sha256:abc']);
assert.match(text, /\[Evolution Memory\] Gene \(EvoMap network\)/);
assert.match(text, /\[Evolution Memory\] Retry the upload with backoff\. \(EvoMap network\)/);
assert.match(text, /evolver_asset_reuse_result for sha256:abc\./);
assert.match(text, /^1\. Measure the failure rate first\.$/m);
assert.match(text, /^2\. Add jittered backoff\.$/m);
assert.doesNotMatch(text, /Retry the upload with backoff|npm test|sha256:second/);
assert.doesNotMatch(text, /npm test|sha256:second/);
assert.match(text, /evolver_asset_reuse_result/);
});

Expand Down Expand Up @@ -267,14 +267,64 @@ test('a named gene is announced by its name, and its hash only where it gets use
assert.doesNotMatch(text, /no business being in front of the model|Also long/);
});

test('an unnamed asset falls back to its type rather than reciting a summary', async () => {
test('an asset with neither a name nor a description falls back to its type', async () => {
const { proxyFetch } = stubProxy({
search: { results: [{ asset_id: 'sha256:plain', asset_type: 'Capsule', has_strategy: true, similarity: 0.9 }] },
fetch: { assets: [{ asset_id: 'sha256:plain', summary: 'A summary that must stay out of the prompt.', strategy: ['Do it.'] }] },
fetch: { assets: [{ asset_id: 'sha256:plain', strategy: ['Do it.'] }] },
});

const { text } = await hubGene(proxyFetch, 'add a retry to the uploader');

assert.match(text, /^\[Evolution Memory\] Capsule \(EvoMap network\):$/m);
assert.doesNotMatch(text, /must stay out of the prompt/);
});

test('a one-word title is a truncation, not a name, so the summary stands in', async () => {
const { proxyFetch } = stubProxy({
search: {
results: [{
asset_id: 'sha256:fragment', asset_type: 'Gene', has_strategy: true, similarity: 0.9,
short_title: 'Object',
nl_summary: 'A generic object pool for reusing expensive resources such as database connections, so they are not rebuilt per request.',
}],
},
fetch: { assets: [{ asset_id: 'sha256:fragment', strategy: ['Initialize the pool.'] }] },
});

const { text } = await hubGene(proxyFetch, 'reuse database connections');
const header = text.split('\n')[0];

assert.doesNotMatch(header, /\] Object \(/, 'Object is what got truncated, not what the gene is');
assert.match(header, /A generic object pool for reusing expensive resources/);
assert.ok(header.length <= 120, 'the stand-in is a name-length excerpt, not the whole summary');
});

test('a short CJK title is a name, and is not mistaken for a fragment', async () => {
const { proxyFetch } = stubProxy({
search: {
results: [{
asset_id: 'sha256:cjk', asset_type: 'Gene', has_strategy: true, similarity: 0.9,
short_title: '智能缓存优化',
nl_summary: '这个基因用于优化缓存命中率。',
}],
},
fetch: { assets: [{ asset_id: 'sha256:cjk', strategy: ['预热缓存。'] }] },
});

const { text } = await hubGene(proxyFetch, '这个服务的缓存命中率太低了,帮我优化一下');
assert.match(text.split('\n')[0], /^\[Evolution Memory\] 智能缓存优化 \(EvoMap network\):$/);
});

test('a truncated CJK title gives way to the summary as well', async () => {
const { proxyFetch } = stubProxy({
search: {
results: [{
asset_id: 'sha256:cut', asset_type: 'Gene', has_strategy: true, similarity: 0.9,
short_title: '自动化小',
nl_summary: '这个基因能自动帮你创作小红书笔记并一键发布。',
}],
},
fetch: { assets: [{ asset_id: 'sha256:cut', strategy: ['先定选题。'] }] },
});

const { text } = await hubGene(proxyFetch, '帮我写一篇小红书的种草笔记');
assert.match(text.split('\n')[0], /这个基因能自动帮你创作小红书笔记/);
});
Loading