@lmzhen/dsh-evolution-review 0.3.68 → 0.3.69
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/lib/index.js +119 -14
- package/package.json +10 -10
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
-
import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, MAX_TIMER_DELAY_MS, PROMPT_BUNDLE, SkillLibrary, advanceReview, assertSkillsRootAliasRetired, clampedNumber, contentHash, evolutionIoAdapter, foldTurn, redactSecrets, resolveOrigins, resolveRootConfig, resolveSkillsRoot, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
|
|
4
|
+
import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, MAX_TIMER_DELAY_MS, PROMPT_BUNDLE, SkillLibrary, advanceReview, assertSkillsRootAliasRetired, clampedNumber, contentHash, evolutionIoAdapter, foldTurn, redactSecrets, resolveOrigins, resolveRootConfig, resolveSkillsRoot, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
|
|
5
5
|
import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
|
|
6
6
|
//#region lib/types/index.js
|
|
7
7
|
/**
|
|
@@ -71,6 +71,12 @@ function clampReviewConfig(rawConfig, ctx) {
|
|
|
71
71
|
if (clamped.length > 0) ctx.logger.warn(`dsh-evolution-review: ${clamped.join(", ")} provided an invalid value; falling back to the default`);
|
|
72
72
|
return config;
|
|
73
73
|
}
|
|
74
|
+
/** v30 REV-04/REV-02: read the policy snapshot off the (optional) policy
|
|
75
|
+
* service through an `unknown` boundary — the Context augmentation types the
|
|
76
|
+
* getter non-optionally, but at runtime the row can be absent. */
|
|
77
|
+
function policySnapshotOf(source) {
|
|
78
|
+
return source?.get?.();
|
|
79
|
+
}
|
|
74
80
|
function apply(ctx, rawConfig = {}) {
|
|
75
81
|
if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
|
|
76
82
|
const config = clampReviewConfig(rawConfig, ctx);
|
|
@@ -325,6 +331,26 @@ function apply(ctx, rawConfig = {}) {
|
|
|
325
331
|
ctx.effect(() => () => {
|
|
326
332
|
deferredFallbackReviews.length = 0;
|
|
327
333
|
}, "dsh-evolution-review.deferred-drain");
|
|
334
|
+
/** v32 REV-06(b): content hash of every tree skill at REVIEW SCHEDULE time.
|
|
335
|
+
* The subagent's read-time hash is not plumbed through the plan, so this
|
|
336
|
+
* pre-run snapshot is what the execute-time staleness check can honestly
|
|
337
|
+
* compare against: a skill whose hash changed while the review ran was
|
|
338
|
+
* touched concurrently, and the plan (authored against the old state) is
|
|
339
|
+
* refused as stale. */
|
|
340
|
+
async function treeSkillHashes() {
|
|
341
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
342
|
+
const io = ctx.get("evolutionIo");
|
|
343
|
+
if (!io) return hashes;
|
|
344
|
+
const library = new SkillLibrary(resolveSkillsRoot({ root: rootConfig.root }), evolutionIoAdapter(() => io.provider()));
|
|
345
|
+
for (const summary of await library.list().catch((error) => {
|
|
346
|
+
ctx.logger.warn(`dsh-evolution-review: skill tree scan failed (${error instanceof Error ? error.message : String(error)}) — pre-run staleness hashes are unavailable; full-content updates will not be drift-checked this review`);
|
|
347
|
+
return [];
|
|
348
|
+
})) {
|
|
349
|
+
const text = await library.read(summary.name).catch(() => null);
|
|
350
|
+
if (text !== null) hashes.set(summary.name, contentHash(text));
|
|
351
|
+
}
|
|
352
|
+
return hashes;
|
|
353
|
+
}
|
|
328
354
|
async function trySubagentReview(session, agent, kind, signal) {
|
|
329
355
|
if ((policy()?.reviewMode ?? config.reviewMode) === "inject") return false;
|
|
330
356
|
const subagents = ctx.get("subagents");
|
|
@@ -352,6 +378,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
352
378
|
const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars));
|
|
353
379
|
const agentOptions = { model };
|
|
354
380
|
if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
|
|
381
|
+
const preRunHashes = await treeSkillHashes();
|
|
355
382
|
const run = await subagents.start("spawn", {
|
|
356
383
|
label: "dsh-evolution-review",
|
|
357
384
|
prompt: [{
|
|
@@ -413,7 +440,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
413
440
|
const landed = [];
|
|
414
441
|
let executed;
|
|
415
442
|
try {
|
|
416
|
-
executed = await withTimeout(executePlan(validation.accepted, session, (action) => landed.push(action)), config.reviewTimeoutMs, "review plan execution");
|
|
443
|
+
executed = await withTimeout(executePlan(validation.accepted, session, (action) => landed.push(action), preRunHashes), config.reviewTimeoutMs, "review plan execution");
|
|
417
444
|
} catch (error) {
|
|
418
445
|
emitApplied({
|
|
419
446
|
actions: landed,
|
|
@@ -424,7 +451,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
424
451
|
const actions = executed.actions;
|
|
425
452
|
if (actions.length > 0) {
|
|
426
453
|
const applied = actions.join(" · ");
|
|
427
|
-
const
|
|
454
|
+
const failedNote = executed.failedOps.length > 0 ? ` 失败 ${executed.failedOps.length} 个:${executed.failedOps.join(";")}。` : "";
|
|
455
|
+
const note = executed.ok ? "" : `\n部分操作失败。${failedNote}以上操作已应用,请勿重复执行。`;
|
|
428
456
|
try {
|
|
429
457
|
deliverMessage(agent, `💾 Self-improvement review: ${applied}${note}`, "self-improvement review");
|
|
430
458
|
} catch (injectError) {
|
|
@@ -486,7 +514,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
486
514
|
}
|
|
487
515
|
}
|
|
488
516
|
}
|
|
489
|
-
async function executePlan(plan, session, onLanded) {
|
|
517
|
+
async function executePlan(plan, session, onLanded, preRunHashes) {
|
|
490
518
|
const sessionId = session?.id;
|
|
491
519
|
const memory = ctx.get("memory");
|
|
492
520
|
const approval = ctx.get("evolutionApproval");
|
|
@@ -535,6 +563,21 @@ function apply(ctx, rawConfig = {}) {
|
|
|
535
563
|
const stageCurrent = await hashLibrary.read(args.name).catch(() => null);
|
|
536
564
|
if (stageCurrent !== null) args.staged_from_sha256 = contentHash(stageCurrent);
|
|
537
565
|
}
|
|
566
|
+
if ((args.action === "write_file" || args.action === "remove_file") && hashLibrary && typeof args.name === "string" && args.name !== "" && typeof args.file_path === "string" && args.file_path !== "") {
|
|
567
|
+
const stageFile = await hashLibrary.readSupportFile(args.name, args.file_path).catch(() => void 0);
|
|
568
|
+
if (stageFile !== void 0) args.staged_from_sha256 = stageFile === null ? "absent" : contentHash(stageFile);
|
|
569
|
+
}
|
|
570
|
+
const opName = typeof args.name === "string" ? args.name : "";
|
|
571
|
+
const hashChecked = (args.action === "update" || args.action === "edit") && opName !== "" && preRunHashes?.has(opName) === true;
|
|
572
|
+
if (hashChecked) {
|
|
573
|
+
const live = await hashLibrary?.read(opName).catch(() => null) ?? null;
|
|
574
|
+
const preRun = preRunHashes.get(opName);
|
|
575
|
+
if (live === null || preRun !== void 0 && contentHash(live) !== preRun) {
|
|
576
|
+
ok = false;
|
|
577
|
+
failedOps.push(`skill ${args.action} ${args.name}: the skill changed while this review ran — update refused as stale; produce a fresh plan`);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
538
581
|
const runnerArgs = {
|
|
539
582
|
operation: args,
|
|
540
583
|
origin: origins.library
|
|
@@ -543,6 +586,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
543
586
|
if (result?.ok) {
|
|
544
587
|
actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
|
|
545
588
|
onLanded?.(`Skill ${op.name} ${op.action ?? "patch"}`);
|
|
589
|
+
if (hashChecked) {
|
|
590
|
+
const landed = await hashLibrary?.read(opName).catch(() => null) ?? null;
|
|
591
|
+
if (landed !== null) preRunHashes.set(opName, contentHash(landed));
|
|
592
|
+
}
|
|
546
593
|
} else {
|
|
547
594
|
ok = false;
|
|
548
595
|
failedOps.push(`skill ${op.action ?? "patch"} ${op.name}: ${result?.message ?? "service unavailable"}`);
|
|
@@ -617,19 +664,55 @@ function apply(ctx, rawConfig = {}) {
|
|
|
617
664
|
ok: false,
|
|
618
665
|
message: "evolution-io service not mounted"
|
|
619
666
|
};
|
|
620
|
-
const
|
|
667
|
+
const policySnapshot = policySnapshotOf(ctx.get("evolutionPolicy"));
|
|
668
|
+
const library = new SkillLibrary(resolveSkillsRoot({ root: rootConfig.root }), evolutionIoAdapter(() => io.provider()), {
|
|
669
|
+
...DEFAULT_SKILL_LIMITS,
|
|
670
|
+
maxSkillContentChars: policySnapshot?.skillContentChars ?? DEFAULT_SKILL_LIMITS.maxSkillContentChars
|
|
671
|
+
}, (event) => {
|
|
621
672
|
ctx.emit("evolution/skill-mutated", event);
|
|
622
673
|
});
|
|
623
674
|
const op = skillArgs;
|
|
624
675
|
const name = op.name ?? "";
|
|
625
676
|
const origin = origins.library;
|
|
677
|
+
const protectedNames = policySnapshot?.protectedSkillNames;
|
|
678
|
+
if (origin !== "foreground" && protectedNames?.includes(name) && op.action !== "create") return {
|
|
679
|
+
ok: false,
|
|
680
|
+
message: `Skill "${name}" is protected by the current policy (protectedSkillNames); autonomous writes are refused.`
|
|
681
|
+
};
|
|
682
|
+
if (op.action === "write_file" || op.action === "remove_file") {
|
|
683
|
+
const expected = op.staged_from_sha256;
|
|
684
|
+
if (typeof expected === "string" && expected !== "") {
|
|
685
|
+
const current = await library.readSupportFile(name, op.file_path ?? "").catch(() => void 0);
|
|
686
|
+
const actual = current === void 0 || current === null ? "absent" : contentHash(current);
|
|
687
|
+
if (current === void 0 || actual !== expected) return {
|
|
688
|
+
ok: false,
|
|
689
|
+
message: `Support file "${op.file_path ?? ""}" of "${name}" changed since this plan was produced — the staged file operation was refused as stale. Re-read the skill tree and produce a fresh plan.`
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
626
693
|
if (op.action === "create") {
|
|
627
694
|
const created = await library.create(name, op.content ?? "", origin);
|
|
628
695
|
if (created.ok) await ctx.get("skillUsage")?.markAgentCreated?.(name);
|
|
629
696
|
return created;
|
|
630
697
|
}
|
|
631
|
-
if (op.action === "edit" || op.action === "update")
|
|
632
|
-
|
|
698
|
+
if (op.action === "edit" || op.action === "update") {
|
|
699
|
+
const expected = op.staged_from_sha256;
|
|
700
|
+
if (typeof expected === "string" && expected !== "") {
|
|
701
|
+
const current = await library.read(name).catch(() => null);
|
|
702
|
+
if (current !== null && contentHash(current) !== expected) return {
|
|
703
|
+
ok: false,
|
|
704
|
+
message: `Skill "${name}" changed since this plan was produced — the full-content update was refused as stale. Re-read the skill and produce a fresh plan.`
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
const updated = await library.update(name, op.content ?? "", origin);
|
|
708
|
+
if (updated.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
|
|
709
|
+
return updated;
|
|
710
|
+
}
|
|
711
|
+
if (op.action === "patch") {
|
|
712
|
+
const patched = await library.patch(name, op.old_string ?? "", op.new_string ?? "", op.file_path ?? "", false, origin);
|
|
713
|
+
if (patched.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
|
|
714
|
+
return patched;
|
|
715
|
+
}
|
|
633
716
|
if (op.action === "delete") {
|
|
634
717
|
const into = (op.absorbed_into ?? "").trim();
|
|
635
718
|
if (!into || !await library.read(into)) return {
|
|
@@ -640,8 +723,21 @@ function apply(ctx, rawConfig = {}) {
|
|
|
640
723
|
if (archived.ok) await ctx.get("skillUsage")?.markArchived?.(name);
|
|
641
724
|
return archived;
|
|
642
725
|
}
|
|
643
|
-
if (op.action === "write_file"
|
|
644
|
-
|
|
726
|
+
if (op.action === "write_file" || op.action === "remove_file") {
|
|
727
|
+
const expected = op.staged_from_sha256;
|
|
728
|
+
if (typeof expected === "string" && expected !== "") {
|
|
729
|
+
const current = await library.readSupportFile(name, op.file_path ?? "").catch(() => void 0);
|
|
730
|
+
const actual = current === void 0 || current === null ? "absent" : contentHash(current);
|
|
731
|
+
if (current === void 0 || actual !== expected) return {
|
|
732
|
+
ok: false,
|
|
733
|
+
message: `Support file "${op.file_path ?? ""}" of "${name}" changed since this plan was produced — the staged file operation was refused as stale. Re-read the skill tree and produce a fresh plan.`
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
if (op.action === "write_file") return await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? "", origin);
|
|
737
|
+
const removedSupport = await library.removeSupportFile(name, op.file_path ?? "", origin);
|
|
738
|
+
if (removedSupport.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
|
|
739
|
+
return removedSupport;
|
|
740
|
+
}
|
|
645
741
|
if (op.action === "restructure") {
|
|
646
742
|
const moves = (op.restructure ?? []).filter((move) => move !== null).map((move) => ({
|
|
647
743
|
heading: move.heading ?? "",
|
|
@@ -676,9 +772,9 @@ function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
|
|
|
676
772
|
* per-skill read action (its `list`/`review` are whole-library), so a specific
|
|
677
773
|
* skill read through it cannot be tracked — see README Known Limitations. */
|
|
678
774
|
function collectReadSkillNames(session) {
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
775
|
+
const callNames = /* @__PURE__ */ new Map();
|
|
776
|
+
const okCallIds = /* @__PURE__ */ new Set();
|
|
777
|
+
for (const event of session.events) if (event.type === "tool/call") {
|
|
682
778
|
if (event.data.name !== "skill") continue;
|
|
683
779
|
const raw = event.data.arguments;
|
|
684
780
|
let parsed = {};
|
|
@@ -687,11 +783,20 @@ function collectReadSkillNames(session) {
|
|
|
687
783
|
} catch {
|
|
688
784
|
continue;
|
|
689
785
|
}
|
|
690
|
-
else parsed = raw
|
|
786
|
+
else parsed = raw;
|
|
691
787
|
const parsedObj = typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
692
788
|
const name = typeof parsedObj.name === "string" ? parsedObj.name : typeof parsedObj.skill === "string" ? parsedObj.skill : "";
|
|
693
|
-
if (name)
|
|
789
|
+
if (name) callNames.set(event.data.callId, name);
|
|
790
|
+
} else if (event.type === "tool/result") {
|
|
791
|
+
const blocks = event.data?.message?.content;
|
|
792
|
+
if (!Array.isArray(blocks)) continue;
|
|
793
|
+
for (const block of blocks) {
|
|
794
|
+
const typed = block;
|
|
795
|
+
if (typed.type === "tool-result" && typed.isError !== true && typeof typed.toolCallId === "string") okCallIds.add(typed.toolCallId);
|
|
796
|
+
}
|
|
694
797
|
}
|
|
798
|
+
const names = /* @__PURE__ */ new Set();
|
|
799
|
+
for (const [callId, name] of callNames) if (okCallIds.has(callId)) names.add(name);
|
|
695
800
|
return names;
|
|
696
801
|
}
|
|
697
802
|
/** Map/set size that triggers a dead-session counter sweep (bounded, not a hard cap). */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-review",
|
|
3
3
|
"description": "Background review orchestration (community build)",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.69",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
34
|
-
"@lmzhen/dsh-evolution-approval": "^0.3.
|
|
35
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
36
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.3.
|
|
34
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.69",
|
|
35
|
+
"@lmzhen/dsh-evolution-core": "^0.3.69",
|
|
36
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.3.69"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
43
43
|
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
44
44
|
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
45
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
46
|
-
"@lmzhen/dsh-evolution-policy": "^0.3.
|
|
45
|
+
"@lmzhen/dsh-evolution-state": "^0.3.69",
|
|
46
|
+
"@lmzhen/dsh-evolution-policy": "^0.3.69"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
|
|
55
55
|
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.1-rc.2",
|
|
56
56
|
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
57
|
-
"@lmzhen/dsh-evolution-approval": "^0.3.
|
|
58
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
59
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.3.
|
|
60
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
57
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.69",
|
|
58
|
+
"@lmzhen/dsh-evolution-core": "^0.3.69",
|
|
59
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.3.69",
|
|
60
|
+
"@lmzhen/dsh-evolution-state": "^0.3.69"
|
|
61
61
|
}
|
|
62
62
|
}
|