dsh-continual-evolve 0.2.0 → 0.4.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/README.md +83 -371
- package/README.zh.md +84 -235
- package/lib/apply.js +8 -2
- package/lib/approval.d.ts +6 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +55 -4
- package/lib/auto.js +61 -5
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +333 -0
- package/lib/benchmark.d.ts +70 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.d.ts +3 -0
- package/lib/command.js +62 -441
- package/lib/evaluate.d.ts +7 -0
- package/lib/evaluate.js +22 -7
- package/lib/evolve-event.d.ts +38 -0
- package/lib/evolve-event.js +49 -0
- package/lib/failures.d.ts +39 -0
- package/lib/failures.js +170 -0
- package/lib/fate.d.ts +5 -2
- package/lib/fate.js +13 -8
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +51 -25
- package/lib/index.js +33 -1
- package/lib/inject.d.ts +24 -1
- package/lib/inject.js +93 -5
- package/lib/llm-text.d.ts +30 -0
- package/lib/llm-text.js +49 -0
- package/lib/mount-command.d.ts +10 -0
- package/lib/mount-command.js +48 -0
- package/lib/plan.js +5 -0
- package/lib/planner.d.ts +1 -1
- package/lib/planner.js +13 -39
- package/lib/promotion.d.ts +62 -0
- package/lib/promotion.js +102 -0
- package/lib/render.d.ts +1 -3
- package/lib/render.js +0 -4
- package/lib/review.d.ts +4 -1
- package/lib/review.js +10 -38
- package/lib/rollback.d.ts +1 -3
- package/lib/rollback.js +0 -8
- package/lib/score.d.ts +15 -0
- package/lib/score.js +74 -5
- package/lib/service.d.ts +2 -2
- package/lib/service.js +7 -3
- package/lib/skill-render.d.ts +23 -0
- package/lib/skill-render.js +68 -0
- package/lib/skill.d.ts +2 -5
- package/lib/skill.js +2 -29
- package/lib/skillquality.d.ts +1 -2
- package/lib/skillquality.js +2 -2
- package/lib/state.js +6 -1
- package/lib/store.d.ts +1 -3
- package/lib/store.js +0 -7
- package/lib/tool.js +22 -1
- package/lib/types.d.ts +8 -0
- package/lib/usage.d.ts +45 -0
- package/lib/usage.js +115 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +26 -1
- package/lib/wrapup-command.d.ts +9 -0
- package/lib/wrapup-command.js +212 -0
- package/lib/wrapup.d.ts +29 -15
- package/lib/wrapup.js +69 -42
- package/package.json +10 -8
package/lib/wrapup.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
2
1
|
import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, isArchived } from "./types.js";
|
|
3
2
|
import { extractJsonObject } from "./plan.js";
|
|
4
3
|
import { compactText } from "./render.js";
|
|
4
|
+
import { streamText } from "./llm-text.js";
|
|
5
|
+
import { getUsageCount, loadUsage } from "./usage.js";
|
|
6
|
+
import { recencyScore } from "./inject.js";
|
|
7
|
+
import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason } from "./promotion.js";
|
|
5
8
|
export function candidateKey(kind, id) {
|
|
6
9
|
return `${kind}:${id}`;
|
|
7
10
|
}
|
|
@@ -71,7 +74,11 @@ export function globalHintsFor(globalState, kind, entry) {
|
|
|
71
74
|
* `coveredGlobally` flag so the assessor never wastes a promote on a topic
|
|
72
75
|
* the global store already owns.
|
|
73
76
|
*/
|
|
74
|
-
|
|
77
|
+
/** Staleness threshold: entries with recency below this AND zero usage are stale. */
|
|
78
|
+
const STALE_RECENCY_THRESHOLD = 0.1;
|
|
79
|
+
export function listLocalCandidates(state, globalState, baseDir) {
|
|
80
|
+
const usage = baseDir ? loadUsage(baseDir) : undefined;
|
|
81
|
+
const now = Date.now();
|
|
75
82
|
const candidates = [];
|
|
76
83
|
for (const kind of Object.keys(state.entries)) {
|
|
77
84
|
for (const entry of Object.values(state.entries[kind])) {
|
|
@@ -81,6 +88,8 @@ export function listLocalCandidates(state, globalState) {
|
|
|
81
88
|
continue;
|
|
82
89
|
if (typeof entry.metadata[PROMOTED_TO_KEY] === "string")
|
|
83
90
|
continue;
|
|
91
|
+
const injectionCount = usage ? getUsageCount(usage, kind, entry.id) : 0;
|
|
92
|
+
const stale = injectionCount === 0 && recencyScore(entry, now) < STALE_RECENCY_THRESHOLD;
|
|
84
93
|
candidates.push({
|
|
85
94
|
kind,
|
|
86
95
|
id: entry.id,
|
|
@@ -91,6 +100,8 @@ export function listLocalCandidates(state, globalState) {
|
|
|
91
100
|
metadata: entry.metadata,
|
|
92
101
|
coveredGlobally: globalCoverageDetected(globalState, kind, entry),
|
|
93
102
|
globalHints: globalHintsFor(globalState, kind, entry),
|
|
103
|
+
injectionCount,
|
|
104
|
+
stale,
|
|
94
105
|
});
|
|
95
106
|
}
|
|
96
107
|
}
|
|
@@ -148,9 +159,16 @@ export function parseWrapupAssessment(text, candidates) {
|
|
|
148
159
|
* Apply-time deterministic guard: re-check every promote verdict against the
|
|
149
160
|
* global store right before it lands. The LLM classification may be stale
|
|
150
161
|
* (a gate ran while assessing) or wrong; this ensures a promote never writes
|
|
151
|
-
* a duplicate global entry. Pure and unit-tested.
|
|
162
|
+
* a duplicate, project-scoped, or too-thin global entry. Pure and unit-tested.
|
|
163
|
+
*
|
|
164
|
+
* Guards (2026-08-22 promotion policy):
|
|
165
|
+
* - audited candidate list + title coverage (pre-existing),
|
|
166
|
+
* - project-scoped content markers (absolute paths / session ids) — the
|
|
167
|
+
* global store is shared across projects and must stay portable,
|
|
168
|
+
* - thin content below the policy floor (framing outweighs the fact),
|
|
169
|
+
* - near-duplicate of an existing global entry by content overlap.
|
|
152
170
|
*/
|
|
153
|
-
export function filterPromotable(items, globalState, candidates) {
|
|
171
|
+
export function filterPromotable(items, globalState, candidates, policy = DEFAULT_PROMOTION_POLICY) {
|
|
154
172
|
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
155
173
|
const promotable = [];
|
|
156
174
|
const skipped = [];
|
|
@@ -166,6 +184,26 @@ export function filterPromotable(items, globalState, candidates) {
|
|
|
166
184
|
skipped.push({ key: item.key, reason: "already covered globally" });
|
|
167
185
|
continue;
|
|
168
186
|
}
|
|
187
|
+
const scoped = projectScopedReason(`${candidate.title}\n${candidate.content}`, policy);
|
|
188
|
+
if (scoped) {
|
|
189
|
+
skipped.push({ key: item.key, reason: scoped });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (candidate.content.length < policy.minPromoteChars) {
|
|
193
|
+
skipped.push({
|
|
194
|
+
key: item.key,
|
|
195
|
+
reason: `too thin to promote (${candidate.content.length} < ${policy.minPromoteChars} chars) — keep local or merge`,
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const similar = mostSimilarGlobalEntry(globalState, candidate.kind, candidate.title, candidate.content, policy);
|
|
200
|
+
if (similar) {
|
|
201
|
+
skipped.push({
|
|
202
|
+
key: item.key,
|
|
203
|
+
reason: `near-duplicate of global ${candidate.kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)}) — update that entry instead`,
|
|
204
|
+
});
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
169
207
|
promotable.push(item);
|
|
170
208
|
}
|
|
171
209
|
return { promotable, skipped };
|
|
@@ -213,16 +251,27 @@ export function splitArchiveGuards(items, candidates) {
|
|
|
213
251
|
}
|
|
214
252
|
/**
|
|
215
253
|
* Apply-time guard for a split promotion (archive + promote sub-object):
|
|
216
|
-
* the cleaned
|
|
217
|
-
*
|
|
218
|
-
*
|
|
254
|
+
* the cleaned payload must pass the same promotion policy as a whole
|
|
255
|
+
* promote — no global coverage duplicate, no project-scoped content, not
|
|
256
|
+
* too thin, no near-duplicate global entry. A blocked split is dropped (the
|
|
257
|
+
* entry still archives plain) rather than half-promoting a redundancy.
|
|
219
258
|
*/
|
|
220
|
-
export function splitPromoteBlocked(item, globalState, kind) {
|
|
259
|
+
export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY) {
|
|
221
260
|
if (!item.promote)
|
|
222
261
|
return "no split payload";
|
|
223
262
|
if (globalCoverageDetected(globalState, kind, { id: "", title: item.promote.title })) {
|
|
224
263
|
return "split promotion duplicates a globally covered topic";
|
|
225
264
|
}
|
|
265
|
+
const scoped = projectScopedReason(`${item.promote.title}\n${item.promote.content}`, policy);
|
|
266
|
+
if (scoped)
|
|
267
|
+
return `split promotion is ${scoped}`;
|
|
268
|
+
if (item.promote.content.length < policy.minPromoteChars) {
|
|
269
|
+
return `split promotion too thin (${item.promote.content.length} < ${policy.minPromoteChars} chars)`;
|
|
270
|
+
}
|
|
271
|
+
const similar = mostSimilarGlobalEntry(globalState, kind, item.promote.title, item.promote.content, policy);
|
|
272
|
+
if (similar) {
|
|
273
|
+
return `split promotion near-duplicates global ${kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)})`;
|
|
274
|
+
}
|
|
226
275
|
return undefined;
|
|
227
276
|
}
|
|
228
277
|
/**
|
|
@@ -342,12 +391,17 @@ listed entry exactly once:
|
|
|
342
391
|
procedure or skill. Future sessions would benefit from seeing it.
|
|
343
392
|
- "archive" — the content is session-specific task progress, one-off noise,
|
|
344
393
|
superseded or obsolete, or already covered by the global store (note
|
|
345
|
-
"covered globally" in the reason)
|
|
394
|
+
"covered globally" in the reason), or stale (old + never injected — note
|
|
395
|
+
"stale (injectionCount=0, recency low)" in the reason).
|
|
346
396
|
- "keep" — still actively useful to this session, or genuinely uncertain.
|
|
347
397
|
|
|
348
398
|
Rules:
|
|
349
399
|
- When an entry is marked "covered globally" in the listing, prefer "archive"
|
|
350
400
|
or "keep" over "promote" — promoting a duplicate gains nothing.
|
|
401
|
+
- When an entry is marked "stale" (injectionCount=0 and low recency), prefer
|
|
402
|
+
"archive" — the entry has never been used and is old, so it is unlikely to
|
|
403
|
+
be needed again. Only "keep" if the content is clearly valuable despite low
|
|
404
|
+
usage (e.g. a safety policy that rarely triggers but is critical).
|
|
351
405
|
- Do not promote local task state, work-in-progress notes, or content tied to
|
|
352
406
|
one session's ephemeral details.
|
|
353
407
|
- Skills: only "promote" a skill entry that is a genuinely reusable procedure
|
|
@@ -387,10 +441,11 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
|
|
|
387
441
|
.map((candidate) => {
|
|
388
442
|
const key = candidateKey(candidate.kind, candidate.id);
|
|
389
443
|
const covered = candidate.coveredGlobally ? " (covered globally)" : "";
|
|
444
|
+
const stale = candidate.stale ? ` (stale: injectionCount=${candidate.injectionCount}, recency low)` : "";
|
|
390
445
|
const hints = candidate.globalHints.length > 0
|
|
391
446
|
? ` | global≈${candidate.globalHints.map((hint) => hint.id + ":" + hint.title).join(", ")}`
|
|
392
447
|
: "";
|
|
393
|
-
return `- ${key} [${candidate.path}, v${candidate.version}] "${candidate.title}"${covered}${hints}: ${compactText(candidate.content, 220)}`;
|
|
448
|
+
return `- ${key} [${candidate.path}, v${candidate.version}] "${candidate.title}"${covered}${stale}${hints}: ${compactText(candidate.content, 220)}`;
|
|
394
449
|
})
|
|
395
450
|
.join("\n");
|
|
396
451
|
const userPrompt = [
|
|
@@ -398,42 +453,14 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
|
|
|
398
453
|
`<local_entries>\n${candidateText}\n</local_entries>`,
|
|
399
454
|
"Return only JSON. Every item must reference one of the keys above.",
|
|
400
455
|
].join("\n\n");
|
|
401
|
-
const
|
|
402
|
-
for await (const chunk of ctx.llm.stream({
|
|
456
|
+
const text = await streamText(ctx, {
|
|
403
457
|
provider: agent.options.provider,
|
|
404
458
|
model: agent.options.model,
|
|
405
459
|
system: WRAPUP_ASSESS_SYSTEM_PROMPT,
|
|
406
|
-
|
|
407
|
-
createUserMessage({
|
|
408
|
-
content: [{ type: "text", text: userPrompt }],
|
|
409
|
-
source: { kind: "plugin", plugin: "dsh-continual-evolve" },
|
|
410
|
-
}),
|
|
411
|
-
],
|
|
412
|
-
// Force non-reasoning output so the budget lands in the JSON verdicts.
|
|
413
|
-
reasoningEffort: ReasoningEffortId("off"),
|
|
460
|
+
prompt: userPrompt,
|
|
414
461
|
maxTokens: options.maxOutputTokens ?? 4096,
|
|
415
|
-
|
|
416
|
-
})
|
|
417
|
-
assembler.push(chunk);
|
|
418
|
-
}
|
|
419
|
-
const finish = assembler.finish;
|
|
420
|
-
if (finish.kind === "error") {
|
|
421
|
-
throw new Error(`evolve: wrap-up assessor call failed: ${finish.failure?.message ?? "unknown"}`);
|
|
422
|
-
}
|
|
423
|
-
if (finish.kind === "aborted") {
|
|
424
|
-
throw new Error("evolve: wrap-up assessor call aborted");
|
|
425
|
-
}
|
|
426
|
-
if (finish.kind === "max-tokens") {
|
|
427
|
-
throw new Error("evolve: wrap-up assessor output budget exhausted (max-tokens)");
|
|
428
|
-
}
|
|
429
|
-
const text = assembler
|
|
430
|
-
.blocks()
|
|
431
|
-
.filter((block) => block.type === "text")
|
|
432
|
-
.map((block) => block.text)
|
|
433
|
-
.join("\n");
|
|
434
|
-
if (text.length === 0) {
|
|
435
|
-
throw new Error("evolve: wrap-up assessor produced no text");
|
|
436
|
-
}
|
|
462
|
+
signal: options.signal,
|
|
463
|
+
});
|
|
437
464
|
return parseWrapupAssessment(text, candidates);
|
|
438
465
|
}
|
|
439
466
|
//# sourceMappingURL=wrapup.js.map
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/ZK-Andy/dsh-continual-evolve.git"
|
|
8
|
+
"url": "git+https://github.com/ZK-Andy/dsh-continual-evolve.git"
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/ZK-Andy/dsh-continual-evolve",
|
|
11
11
|
"bugs": {
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"dev": "tsc -p tsconfig.json --watch",
|
|
51
51
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
52
|
"test": "vitest run",
|
|
53
|
+
"test:coverage": "vitest run --coverage",
|
|
53
54
|
"test:watch": "vitest",
|
|
54
55
|
"lint": "oxlint src test",
|
|
55
56
|
"clean": "rm -rf lib"
|
|
@@ -62,14 +63,15 @@
|
|
|
62
63
|
},
|
|
63
64
|
"devDependencies": {
|
|
64
65
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
65
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
66
|
-
"@deepseek-ai/dsh-commands": "0.1.
|
|
67
|
-
"@deepseek-ai/dsh-home-paths": "0.1.
|
|
68
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
69
|
-
"@deepseek-ai/dsh-system-prompt": "0.1.
|
|
70
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
66
|
+
"@deepseek-ai/dsh-agent": "0.1.1-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-commands": "0.1.1-rc.2",
|
|
68
|
+
"@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
|
|
69
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
70
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
|
|
71
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.2",
|
|
71
72
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
72
73
|
"@types/node": "^22.10.0",
|
|
74
|
+
"@vitest/coverage-v8": "^3.2.0",
|
|
73
75
|
"oxlint": "^0.16.0",
|
|
74
76
|
"typescript": "^5.9.0",
|
|
75
77
|
"vitest": "^3.2.0"
|