dsh-continual-evolve 0.15.12 → 0.15.13
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 +13 -8
- package/README.zh.md +13 -8
- package/lib/chain.d.ts +30 -0
- package/lib/chain.js +29 -0
- package/lib/extraction-ring.d.ts +20 -0
- package/lib/extraction-ring.js +299 -0
- package/lib/extraction.js +26 -224
- package/lib/llm-call.d.ts +62 -10
- package/lib/llm-call.js +114 -36
- package/lib/review-candidates.d.ts +25 -0
- package/lib/review-candidates.js +113 -0
- package/lib/review-prompt.d.ts +16 -2
- package/lib/review-prompt.js +19 -4
- package/lib/review-ring.d.ts +52 -0
- package/lib/review-ring.js +257 -0
- package/lib/review.d.ts +6 -31
- package/lib/review.js +32 -314
- package/lib/store.d.ts +2 -0
- package/lib/store.js +19 -4
- package/lib/subagent-defs.d.ts +43 -0
- package/lib/subagent-defs.js +85 -0
- package/lib/subagent.d.ts +81 -0
- package/lib/subagent.js +52 -0
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect review candidates across every workspace carrying active items:
|
|
3
|
+
* new items first (newest first), then corrects-linked requeues, then stale
|
|
4
|
+
* requeues (`staleDays`, default 30). Bounded by `limit`.
|
|
5
|
+
*
|
|
6
|
+
* @param store - The central store, sole source of the queue.
|
|
7
|
+
* @param opts - Clock override (tests), staleness window, and queue bound.
|
|
8
|
+
* @returns Candidates ordered by priority.
|
|
9
|
+
*/
|
|
10
|
+
export function collectCandidates(store, opts = {}) {
|
|
11
|
+
const now = opts.now ?? Date.now();
|
|
12
|
+
const staleMs = (opts.staleDays ?? 30) * 24 * 60 * 60 * 1000;
|
|
13
|
+
const limit = opts.limit ?? 100;
|
|
14
|
+
const fresh = [];
|
|
15
|
+
const requeued = [];
|
|
16
|
+
const stale = [];
|
|
17
|
+
// One push per enumerated row: store primary keys make (workspace, kind,
|
|
18
|
+
// id) unique, so no dedup set is needed — a duplicate here would mean a
|
|
19
|
+
// store invariant broke, and failing loud beats silently merging.
|
|
20
|
+
const push = (candidate) => {
|
|
21
|
+
(candidate.priority === 0 ? fresh : candidate.priority === 1 ? requeued : stale).push(candidate);
|
|
22
|
+
};
|
|
23
|
+
const partitions = new Set();
|
|
24
|
+
for (const summary of store.listWorkspaces()) {
|
|
25
|
+
partitions.add(summary.workspaceId);
|
|
26
|
+
}
|
|
27
|
+
for (const summary of store.listSkillWorkspaces()) {
|
|
28
|
+
partitions.add(summary.workspaceId);
|
|
29
|
+
}
|
|
30
|
+
partitions.add("");
|
|
31
|
+
for (const workspaceId of partitions) {
|
|
32
|
+
const correctsTargets = new Set();
|
|
33
|
+
const memories = store.listAll(workspaceId);
|
|
34
|
+
for (const record of memories) {
|
|
35
|
+
if (record.corrects !== null && record.corrects.length > 0) {
|
|
36
|
+
correctsTargets.add(record.corrects);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
for (const record of memories) {
|
|
40
|
+
if (record.status !== "active") {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const state = store.reviewStateOf(workspaceId, "memory", record.id);
|
|
44
|
+
const updatedAt = Date.parse(record.updatedAt) || 0;
|
|
45
|
+
if (state === undefined) {
|
|
46
|
+
push({ workspaceId, kind: "memory", id: record.id, priority: 0, title: record.title, description: record.description, body: record.body, memoryType: record.type });
|
|
47
|
+
}
|
|
48
|
+
else if (correctsTargets.has(record.id)) {
|
|
49
|
+
push({ workspaceId, kind: "memory", id: record.id, priority: 1, title: record.title, description: record.description, body: record.body, memoryType: record.type });
|
|
50
|
+
}
|
|
51
|
+
else if (now - updatedAt >= staleMs) {
|
|
52
|
+
push({ workspaceId, kind: "memory", id: record.id, priority: 2, title: record.title, description: record.description, body: record.body, memoryType: record.type });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const projectSeen = new Set();
|
|
56
|
+
const skillsOf = (partition, scope) => {
|
|
57
|
+
for (const record of store.listAllSkills(partition)) {
|
|
58
|
+
if (record.status !== "active" || projectSeen.has(record.id)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
projectSeen.add(record.id);
|
|
62
|
+
const state = store.reviewStateOf(partition, "skill", record.id);
|
|
63
|
+
const updatedAt = Date.parse(record.updatedAt) || 0;
|
|
64
|
+
const candidate = {
|
|
65
|
+
workspaceId: partition, kind: "skill", id: record.id,
|
|
66
|
+
priority: state === undefined ? 0 : 2,
|
|
67
|
+
title: record.title, description: record.description, body: record.body, scope,
|
|
68
|
+
};
|
|
69
|
+
if (state === undefined || now - updatedAt >= staleMs) {
|
|
70
|
+
push(candidate);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
if (workspaceId !== "") {
|
|
75
|
+
skillsOf(workspaceId, "project");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Global skills ride along once (project rows shadow them per id).
|
|
79
|
+
skillsGlobal(store, now, staleMs, push);
|
|
80
|
+
const byPriority = (a, b) => (a.priority - b.priority);
|
|
81
|
+
return [...fresh, ...requeued, ...stale].sort(byPriority).slice(0, limit);
|
|
82
|
+
}
|
|
83
|
+
/** The global partition's skills, minus every id an active project row shadows. */
|
|
84
|
+
function skillsGlobal(store, now, staleMs, push) {
|
|
85
|
+
const projectIds = new Set();
|
|
86
|
+
for (const summary of store.listSkillWorkspaces()) {
|
|
87
|
+
if (summary.workspaceId === "") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const record of store.listAllSkills(summary.workspaceId)) {
|
|
91
|
+
// Archived rows shadow nothing (readWithScope shows the global
|
|
92
|
+
// row once the project row archives after a move-to-global).
|
|
93
|
+
if (record.status === "active") {
|
|
94
|
+
projectIds.add(record.id);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const record of store.listAllSkills("")) {
|
|
99
|
+
if (record.status !== "active" || projectIds.has(record.id)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const state = store.reviewStateOf("", "skill", record.id);
|
|
103
|
+
const updatedAt = Date.parse(record.updatedAt) || 0;
|
|
104
|
+
if (state === undefined || now - updatedAt >= staleMs) {
|
|
105
|
+
push({
|
|
106
|
+
workspaceId: "", kind: "skill", id: record.id,
|
|
107
|
+
priority: state === undefined ? 0 : 2,
|
|
108
|
+
title: record.title, description: record.description, body: record.body, scope: "global",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=review-candidates.js.map
|
package/lib/review-prompt.d.ts
CHANGED
|
@@ -10,8 +10,23 @@
|
|
|
10
10
|
* body executability, reusability, non-duplicate). The prompts are
|
|
11
11
|
* deliberately behavior-blind: there are no task datasets here (unlike
|
|
12
12
|
* Hermes GEPA), only the item text plus its provenance.
|
|
13
|
+
*
|
|
14
|
+
* Each of the four review jobs has its own gate/evolve prompt pair, and the
|
|
15
|
+
* memory and skill rubrics contradict each other on home-ownership by design
|
|
16
|
+
* (a memory must survive having its repo name stripped; a project skill must
|
|
17
|
+
* not). Sharing one prompt between them is what made the review ring judge
|
|
18
|
+
* skills by the memory rubric.
|
|
19
|
+
*/
|
|
20
|
+
export declare const REVIEW_MEMORY_CHEAP_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u95E8\u3002\u5148\u8FC7\u5F52\u5C5E\u4E09\u6D4B\uFF1A\u2460\u4E3B\u8BED\u662F\u7528\u6237/\u73AF\u5883\u8FD8\u662F\u67D0\u4ED3\u5E93\u7684\u67D0\u7248\u673A\u5236\uFF08\u7248\u672C\u53F7\u3001\u9608\u503C\u3001\u5F00\u5173\u540D\u51FA\u73B0\u5373\u540E\u8005\uFF09\uFF1B\u2461\u62B9\u6389\u4ED3\u5E93\u540D\u7248\u672C\u53F7\u673A\u5236\u540D\u540E\u53E5\u5B50\u8FD8\u6210\u7ACB\u5417\uFF1B\u2462\u4ED3\u5E93\u4E8B\u5B9E\u4F4F ADR/docs\uFF0C\u8BB0\u5FC6\u53EA\u6536\u8DDF\u4EBA\u8D70\u7684\u3002\u5F52\u5C5E\u4E0D\u8FC7\u76F4\u63A5\u5224 false \u5E76\u6CE8\u660E\u653E\u9519\u5BB6\u3002\u518D\u5224\u503C\u4E0D\u503C\u5F97\u7EC6\u770B\uFF1A\u6709\u660E\u786E\u4E8B\u5B9E\u3001\u505A\u6CD5\u3001\u504F\u597D\u6216\u53EF\u590D\u7528\u89C4\u7A0B\u5C31\u6279\uFF1B\u7A7A\u8BDD\u3001\u7EAF\u8FC7\u7A0B\u8BB0\u5F55\u3001\u4E00\u6B21\u6027\u7684\u77AC\u65F6\u8F93\u51FA\u3001\u5F53\u8F6E\u5DF2\u53EF\u63A8\u5BFC\u7684\u4E00\u5F8B\u62D2\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"worthReview\": true|false, \"reason\": \"\u4E00\u53E5\u8BDD\"}\u3002";
|
|
21
|
+
/**
|
|
22
|
+
* The skill cheap gate: the memory gate's rubric inverted on home-ownership.
|
|
23
|
+
* A skill bound to one repo's paths, gates and workflow cards is a *project
|
|
24
|
+
* skill* — its home test is the project, not the world — so the gate that
|
|
25
|
+
* fronted skills with the memory rubric rejected them for being exactly what
|
|
26
|
+
* they are. Every judgment here is scope-relative, and the scope rides the
|
|
27
|
+
* user prompt.
|
|
13
28
|
*/
|
|
14
|
-
export declare const
|
|
29
|
+
export declare const REVIEW_SKILL_CHEAP_SYSTEM_PROMPT = "\u4F60\u662F\u6280\u80FD\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u95E8\u3002\u5148\u770B\u5F52\u5C5E\uFF08prompt \u91CC\u6807\u4E86 scope\uFF09\uFF1A\u7ED1\u5B9A\u67D0\u4ED3\u5E93\u7684\u8DEF\u5F84/\u95E8\u7981/\u6D41\u7A0B**\u4E0D\u662F**\u7F3A\u9677\u2014\u2014\u4F4F\u9879\u76EE\u5206\u533A\uFF08scope=project\uFF09\u7684\u6280\u80FD\u672C\u6765\u5C31\u53EA\u5728\u8BE5\u9879\u76EE\u5185\u751F\u6548\uFF0C\u7981\u7528\u300C\u8DE8\u9879\u76EE\u4E0D\u53EF\u590D\u7528\u300D\u300C\u62B9\u6389\u4ED3\u5E93\u540D\u4E0D\u6210\u7ACB\u300D\u5F53\u7406\u7531\u53BB\u62D2\u5B83\uFF1B\u53EA\u6709\u300Cscope=global \u5374\u7ED1\u5B9A\u67D0\u4ED3\u5E93\u300D\u624D\u7B97\u653E\u9519\u5BB6\uFF0C\u76F4\u63A5\u5224 false \u5E76\u6CE8\u660E\u3002\u518D\u5224\u503C\u4E0D\u503C\u5F97\u7EC6\u770B\uFF1Adescription \u8BA9\u4EBA\u5728\u8BE5\u7528\u65F6\u60F3\u5F97\u8D77\u3001\u6B63\u6587\u6B65\u9AA4\u53EF\u6267\u884C\u3001\u6709\u53EF\u590D\u7528\u7684\u89C4\u7A0B\u5C31\u6279\uFF1B\u7A7A\u8BDD\u3001\u4E00\u6B21\u6027\u8F93\u51FA\u3001\u7EAF\u8FC7\u7A0B\u8BB0\u5F55\u3001\u4E0E\u65E2\u6709\u6280\u80FD\u91CD\u590D\u7684\u4E00\u5F8B\u62D2\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"worthReview\": true|false, \"reason\": \"\u4E00\u53E5\u8BDD\"}\u3002";
|
|
15
30
|
/** Memory rubric: accurate, usable, reusable, non-duplicate, fresh, right-home. */
|
|
16
31
|
export declare const REVIEW_MEMORY_SYSTEM_PROMPT = "\u4F60\u662F\u8BB0\u5FC6\u5E93\u7684\u5185\u5BB9\u8BC4\u5BA1\u3002\u5148\u8FC7\u5F52\u5C5E\u4E09\u6D4B\uFF08\u4E3B\u8BED\u662F\u7528\u6237/\u73AF\u5883\u8FD8\u662F\u67D0\u4ED3\u5E93\u7684\u67D0\u7248\u673A\u5236\uFF1B\u62B9\u6389\u4ED3\u5E93\u540D\u7248\u672C\u53F7\u673A\u5236\u540D\u540E\u8FD8\u6210\u7ACB\u5417\uFF1B\u4ED3\u5E93\u4E8B\u5B9E\u4F4F ADR/docs\uFF09\uFF1A\u5F52\u5C5E\u4E0D\u8FC7\u76F4\u63A5 quarantine \u5E76\u6CE8\u660E\u653E\u9519\u5BB6\u3002\u518D\u6309\u4E94\u7EF4\u5224\uFF1A\u2460\u51C6\u786E\uFF08\u4E8B\u5B9E\u53EF\u4FE1\u3001\u65E0\u5E7B\u89C9\uFF09\uFF1B\u2461\u53EF\u7528\uFF08Why/How \u5177\u4F53\u3001\u53EF\u6267\u884C\uFF0C\u53CD\u9988\u7C7B\u5FC5\u987B\u5E26 Why \u4E0E How to apply\uFF09\uFF1B\u2462\u53EF\u590D\u7528\uFF08\u672A\u6765\u4F1A\u8BDD\u7528\u5F97\u4E0A\uFF09\uFF1B\u2463\u4E0D\u91CD\u590D\uFF08\u4E0E\u5019\u9009\u65E7\u6761\u6BD4\uFF0C\u6709\u91CD\u590D\u5C31\u6307\u51FA\u6765\uFF09\uFF1B\u2464\u4E0D\u8FC7\u65F6\uFF08\u672A\u88AB\u7EA0\u6B63\u94FE\u53D6\u4EE3\uFF09\u3002\u53EA\u8F93\u51FA JSON\uFF1A{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"\u4E00\u53E5\u8BDD\", \"title\"?: \"\u6539\u5199\u540E\u6807\u9898\", \"description\"?: \"\u6539\u5199\u540E\u94A9\u5B50\", \"body\"?: \"\u6539\u5199\u540E\u6B63\u6587\u5168\u6587\", \"corrects\"?: \"\u88AB\u53D6\u4EE3\u7684\u540C\u533A\u6761\u76EEid\"}\u3002verdict \u8BF4\u660E\uFF1Akeep=\u4E0D\u52A8\uFF1Brewrite=\u7ED9\u51FA\u6539\u5199\u540E\u5168\u6587\u4E09\u4EF6\uFF1Bquarantine=\u6D89\u5BC6/\u6709\u5BB3/\u5B8C\u5168\u8FC7\u65F6/\u653E\u9519\u5BB6\u3002\u7981\u5220\u9664\uFF0C\u7981\u7F16\u9020\u5F15\u7528\u3002";
|
|
17
32
|
/**
|
|
@@ -49,7 +64,6 @@ export declare function evolvePrompt(entry: {
|
|
|
49
64
|
title: string;
|
|
50
65
|
description: string;
|
|
51
66
|
body: string;
|
|
52
|
-
extra?: string | undefined;
|
|
53
67
|
scope?: "project" | "global" | undefined;
|
|
54
68
|
}, neighbors: readonly {
|
|
55
69
|
id: string;
|
package/lib/review-prompt.js
CHANGED
|
@@ -10,8 +10,23 @@
|
|
|
10
10
|
* body executability, reusability, non-duplicate). The prompts are
|
|
11
11
|
* deliberately behavior-blind: there are no task datasets here (unlike
|
|
12
12
|
* Hermes GEPA), only the item text plus its provenance.
|
|
13
|
+
*
|
|
14
|
+
* Each of the four review jobs has its own gate/evolve prompt pair, and the
|
|
15
|
+
* memory and skill rubrics contradict each other on home-ownership by design
|
|
16
|
+
* (a memory must survive having its repo name stripped; a project skill must
|
|
17
|
+
* not). Sharing one prompt between them is what made the review ring judge
|
|
18
|
+
* skills by the memory rubric.
|
|
19
|
+
*/
|
|
20
|
+
export const REVIEW_MEMORY_CHEAP_SYSTEM_PROMPT = "你是记忆库的内容评审门。先过归属三测:①主语是用户/环境还是某仓库的某版机制(版本号、阈值、开关名出现即后者);②抹掉仓库名版本号机制名后句子还成立吗;③仓库事实住 ADR/docs,记忆只收跟人走的。归属不过直接判 false 并注明放错家。再判值不值得细看:有明确事实、做法、偏好或可复用规程就批;空话、纯过程记录、一次性的瞬时输出、当轮已可推导的一律拒。只输出 JSON:{\"worthReview\": true|false, \"reason\": \"一句话\"}。";
|
|
21
|
+
/**
|
|
22
|
+
* The skill cheap gate: the memory gate's rubric inverted on home-ownership.
|
|
23
|
+
* A skill bound to one repo's paths, gates and workflow cards is a *project
|
|
24
|
+
* skill* — its home test is the project, not the world — so the gate that
|
|
25
|
+
* fronted skills with the memory rubric rejected them for being exactly what
|
|
26
|
+
* they are. Every judgment here is scope-relative, and the scope rides the
|
|
27
|
+
* user prompt.
|
|
13
28
|
*/
|
|
14
|
-
export const
|
|
29
|
+
export const REVIEW_SKILL_CHEAP_SYSTEM_PROMPT = "你是技能库的内容评审门。先看归属(prompt 里标了 scope):绑定某仓库的路径/门禁/流程**不是**缺陷——住项目分区(scope=project)的技能本来就只在该项目内生效,禁用「跨项目不可复用」「抹掉仓库名不成立」当理由去拒它;只有「scope=global 却绑定某仓库」才算放错家,直接判 false 并注明。再判值不值得细看:description 让人在该用时想得起、正文步骤可执行、有可复用的规程就批;空话、一次性输出、纯过程记录、与既有技能重复的一律拒。只输出 JSON:{\"worthReview\": true|false, \"reason\": \"一句话\"}。";
|
|
15
30
|
/** Memory rubric: accurate, usable, reusable, non-duplicate, fresh, right-home. */
|
|
16
31
|
export const REVIEW_MEMORY_SYSTEM_PROMPT = "你是记忆库的内容评审。先过归属三测(主语是用户/环境还是某仓库的某版机制;抹掉仓库名版本号机制名后还成立吗;仓库事实住 ADR/docs):归属不过直接 quarantine 并注明放错家。再按五维判:①准确(事实可信、无幻觉);②可用(Why/How 具体、可执行,反馈类必须带 Why 与 How to apply);③可复用(未来会话用得上);④不重复(与候选旧条比,有重复就指出来);⑤不过时(未被纠正链取代)。只输出 JSON:{\"verdict\": \"keep|rewrite|quarantine\", \"reason\": \"一句话\", \"title\"?: \"改写后标题\", \"description\"?: \"改写后钩子\", \"body\"?: \"改写后正文全文\", \"corrects\"?: \"被取代的同区条目id\"}。verdict 说明:keep=不动;rewrite=给出改写后全文三件;quarantine=涉密/有害/完全过时/放错家。禁删除,禁编造引用。";
|
|
17
32
|
/**
|
|
@@ -37,10 +52,10 @@ export function evolvePrompt(entry, neighbors) {
|
|
|
37
52
|
if (entry.scope !== undefined) {
|
|
38
53
|
lines.push(`scope=${entry.scope}`);
|
|
39
54
|
}
|
|
55
|
+
// Nothing rides after `全文:` — a rubric reminder adjacent to the item
|
|
56
|
+
// text gets copied back as if it were content (2026-10-14, memory
|
|
57
|
+
// `codegraph`). The rubric lives in the system prompt only.
|
|
40
58
|
lines.push(`title=${entry.title}`, `钩子=${entry.description}`, "全文:", entry.body);
|
|
41
|
-
if (entry.extra !== undefined && entry.extra.length > 0) {
|
|
42
|
-
lines.push("", entry.extra);
|
|
43
|
-
}
|
|
44
59
|
if (neighbors.length > 0) {
|
|
45
60
|
lines.push("", "同类候选(判重复用):");
|
|
46
61
|
for (const neighbor of neighbors) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review ring, assembled as a responsibility chain: cheap gate → evolve
|
|
3
|
+
* pass → landing. Each link owns exactly one step and hands the item on until
|
|
4
|
+
* a link terminates the walk; the batch runner (the environment class) is the
|
|
5
|
+
* only place that knows the order.
|
|
6
|
+
*
|
|
7
|
+
* Every terminal path lands exactly one `item-review` row, and every row now
|
|
8
|
+
* carries the model it ran on — a failure with no route on it cannot be
|
|
9
|
+
* attributed, which is what left the gate's failure rate unreadable.
|
|
10
|
+
*/
|
|
11
|
+
import { type ChainLink } from "./chain.js";
|
|
12
|
+
import { type ModelSelection } from "./llm-call.js";
|
|
13
|
+
import { type EvolveVerdict } from "./review-prompt.js";
|
|
14
|
+
import { type SubagentLoader } from "./subagent.js";
|
|
15
|
+
import { type UsageTotals } from "./token-usage.js";
|
|
16
|
+
import type { ReviewCandidate, ReviewDeps, ReviewRunOptions, ReviewRunSummary } from "./review.js";
|
|
17
|
+
/** A rewrite lands only when the body-length ratio stays within this bound. */
|
|
18
|
+
export declare const REWRITE_MAGNITUDE_CAP = 0.3;
|
|
19
|
+
/** One item's walk through the ring: the context every link reads and fills in. */
|
|
20
|
+
export interface ReviewItem {
|
|
21
|
+
readonly deps: ReviewDeps;
|
|
22
|
+
/** The loader bound to this run's route and session — the only LLM entry point. */
|
|
23
|
+
readonly loader: SubagentLoader;
|
|
24
|
+
/** The route this run reviews on, for every row's `model` column. */
|
|
25
|
+
readonly route: ModelSelection;
|
|
26
|
+
readonly runId: string;
|
|
27
|
+
readonly candidate: ReviewCandidate;
|
|
28
|
+
readonly opts: ReviewRunOptions;
|
|
29
|
+
readonly summary: ReviewRunSummary;
|
|
30
|
+
readonly skipReasons: string[];
|
|
31
|
+
/** Gate facts: the normalized totals later rows carry, and the raw payload rows keep. */
|
|
32
|
+
gate: {
|
|
33
|
+
usage: UsageTotals;
|
|
34
|
+
raw: unknown;
|
|
35
|
+
};
|
|
36
|
+
/** The evolve verdict, filled by the evolve link. */
|
|
37
|
+
evolved?: EvolveVerdict;
|
|
38
|
+
/** The evolve call's raw usage payload; `undefined` when it never reported one. */
|
|
39
|
+
evolveUsage?: unknown;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Assemble the review chain: the environment class owns the order, the links
|
|
43
|
+
* own their step, and the item carries the run's loader and route.
|
|
44
|
+
*
|
|
45
|
+
* @returns The chain, in order, ending in the landing link.
|
|
46
|
+
*/
|
|
47
|
+
export declare function reviewChain(): readonly ChainLink<ReviewItem>[];
|
|
48
|
+
/** One-line error head for ledger rows (fail-loud shape, capped length). */
|
|
49
|
+
export declare function failureHead(error: unknown, max?: number): string;
|
|
50
|
+
/** Relative body change: |new-old| / max(old, 1). Above the cap → quarantine. */
|
|
51
|
+
export declare function rewriteMagnitude(oldBody: string, newBody: string): number;
|
|
52
|
+
//# sourceMappingURL=review-ring.d.ts.map
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { observedUsageOf } from "./llm-call.js";
|
|
2
|
+
import { evolvePrompt, gatePrompt } from "./review-prompt.js";
|
|
3
|
+
import { REVIEW_EVOLVE_BY_KIND, REVIEW_GATE_BY_KIND } from "./subagent-defs.js";
|
|
4
|
+
import { cutFailureOf } from "./subagent.js";
|
|
5
|
+
import { normalizeUsage, zeroUsage } from "./token-usage.js";
|
|
6
|
+
/** A rewrite lands only when the body-length ratio stays within this bound. */
|
|
7
|
+
export const REWRITE_MAGNITUDE_CAP = 0.3;
|
|
8
|
+
/**
|
|
9
|
+
* Assemble the review chain: the environment class owns the order, the links
|
|
10
|
+
* own their step, and the item carries the run's loader and route.
|
|
11
|
+
*
|
|
12
|
+
* @returns The chain, in order, ending in the landing link.
|
|
13
|
+
*/
|
|
14
|
+
export function reviewChain() {
|
|
15
|
+
return [gateLink(), evolveLink(), landLink()];
|
|
16
|
+
}
|
|
17
|
+
/** Cheap gate: decide whether the item deserves the expensive pass at all. */
|
|
18
|
+
function gateLink() {
|
|
19
|
+
return {
|
|
20
|
+
name: "review-gate",
|
|
21
|
+
async handle(item) {
|
|
22
|
+
const { deps, candidate } = item;
|
|
23
|
+
const base = rowBaseOf(item);
|
|
24
|
+
try {
|
|
25
|
+
const answer = await item.loader.run(REVIEW_GATE_BY_KIND[candidate.kind], gatePrompt(candidate));
|
|
26
|
+
const verdict = answer.answer;
|
|
27
|
+
if (verdict === null) {
|
|
28
|
+
// A cut gate is a failure only when no complete verdict came
|
|
29
|
+
// through: the JSON usually lands before the budget runs out, so
|
|
30
|
+
// parse first and blame the finish reason only if it did not.
|
|
31
|
+
if (answer.finish !== undefined && answer.finish !== "stop") {
|
|
32
|
+
throw cutFailureOf(answer, item.route);
|
|
33
|
+
}
|
|
34
|
+
deps.store.logReview({ ...base, verdict: "skip", reason: "gate-parse-failed", cheapUsage: answer.usage, applied: 0 });
|
|
35
|
+
item.skipReasons.push("gate-parse-failed");
|
|
36
|
+
return "handled";
|
|
37
|
+
}
|
|
38
|
+
if (!verdict.worthReview) {
|
|
39
|
+
item.summary.cheapToken += totalOf(answer.usage);
|
|
40
|
+
deps.store.logReview({ ...base, verdict: "skip", reason: verdict.reason, cheapUsage: answer.usage, applied: 0 });
|
|
41
|
+
item.skipReasons.push("cheap-gate");
|
|
42
|
+
return "handled";
|
|
43
|
+
}
|
|
44
|
+
item.summary.cheapToken += totalOf(answer.usage);
|
|
45
|
+
item.gate = { usage: normalizedOf(answer.usage), raw: answer.usage };
|
|
46
|
+
// Approved counts the gate's decision, not the outcome: the
|
|
47
|
+
// evolve leg may still fail and ledger its own skip row.
|
|
48
|
+
item.summary.approved += 1;
|
|
49
|
+
return "next";
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// The error text rides the row: a bare `gate-llm-failed` hid a ~50%
|
|
53
|
+
// per-item failure rate in production (2026-10-12) with no way to
|
|
54
|
+
// tell transient route faults from systematic ones — and the usage
|
|
55
|
+
// the call already reported rides it too, or a failed call burns
|
|
56
|
+
// tokens that no account shows.
|
|
57
|
+
deps.store.logReview({
|
|
58
|
+
...base, verdict: "skip", reason: `gate-llm-failed: ${failureHead(error)}`,
|
|
59
|
+
cheapUsage: observedUsageOf(error), applied: 0,
|
|
60
|
+
});
|
|
61
|
+
item.skipReasons.push("gate-llm-failed");
|
|
62
|
+
return "handled";
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** The evolve pass: judge the item and produce a verdict, or ledger why not. */
|
|
68
|
+
function evolveLink() {
|
|
69
|
+
return {
|
|
70
|
+
name: "review-evolve",
|
|
71
|
+
async handle(item) {
|
|
72
|
+
const { deps, candidate } = item;
|
|
73
|
+
const base = rowBaseOf(item);
|
|
74
|
+
try {
|
|
75
|
+
const definition = REVIEW_EVOLVE_BY_KIND[candidate.kind];
|
|
76
|
+
const neighbors = neighborHooks(deps.store, candidate);
|
|
77
|
+
const answer = await item.loader.run(definition, evolvePrompt({ id: candidate.id, title: candidate.title, description: candidate.description, body: candidate.body, scope: candidate.scope }, neighbors));
|
|
78
|
+
item.evolveUsage = answer.usage;
|
|
79
|
+
item.summary.evolveToken += totalOf(answer.usage);
|
|
80
|
+
// The evolve pass carries full replacement text: a cut stream may hold
|
|
81
|
+
// half a body, so it never becomes a verdict.
|
|
82
|
+
if (answer.finish !== undefined && answer.finish !== "stop") {
|
|
83
|
+
throw cutFailureOf(answer, item.route);
|
|
84
|
+
}
|
|
85
|
+
if (answer.answer === null) {
|
|
86
|
+
deps.store.logReview({
|
|
87
|
+
...base, verdict: "skip", reason: "evolve-parse-failed",
|
|
88
|
+
cheapUsage: item.gate.usage, evolveUsage: item.evolveUsage, applied: 0,
|
|
89
|
+
});
|
|
90
|
+
item.skipReasons.push("evolve-parse-failed");
|
|
91
|
+
return "handled";
|
|
92
|
+
}
|
|
93
|
+
item.evolved = answer.answer;
|
|
94
|
+
return "next";
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
deps.store.logReview({
|
|
98
|
+
...base, verdict: "skip", reason: `evolve-llm-failed: ${failureHead(error)}`,
|
|
99
|
+
cheapUsage: item.gate.usage, evolveUsage: observedUsageOf(error) ?? item.evolveUsage, applied: 0,
|
|
100
|
+
});
|
|
101
|
+
item.skipReasons.push("evolve-llm-failed");
|
|
102
|
+
return "handled";
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Landing: turn one verdict into a row, and (outside dry-run) into a change. */
|
|
108
|
+
function landLink() {
|
|
109
|
+
return {
|
|
110
|
+
name: "review-land",
|
|
111
|
+
handle(item) {
|
|
112
|
+
const evolved = item.evolved;
|
|
113
|
+
if (evolved === undefined) {
|
|
114
|
+
// A landing link reached without a verdict is a wiring bug: fail
|
|
115
|
+
// loud instead of writing a row with nothing behind it.
|
|
116
|
+
throw new Error(`review landing reached without a verdict for ${item.candidate.kind} ${item.candidate.id}`);
|
|
117
|
+
}
|
|
118
|
+
if (evolved.verdict === "keep") {
|
|
119
|
+
item.deps.store.logReview({
|
|
120
|
+
...rowBaseOf(item), verdict: "keep", reason: evolved.reason,
|
|
121
|
+
cheapUsage: item.gate.usage, evolveUsage: item.evolveUsage, applied: 0,
|
|
122
|
+
});
|
|
123
|
+
return "handled";
|
|
124
|
+
}
|
|
125
|
+
if (evolved.verdict === "quarantine") {
|
|
126
|
+
landQuarantine(item, evolved, evolved.reason);
|
|
127
|
+
return "handled";
|
|
128
|
+
}
|
|
129
|
+
// Rewrite: magnitude cap first, dry-run second, gates last.
|
|
130
|
+
const nextBody = evolved.body ?? item.candidate.body;
|
|
131
|
+
if (rewriteMagnitude(item.candidate.body, nextBody) > REWRITE_MAGNITUDE_CAP) {
|
|
132
|
+
landQuarantine(item, evolved, `幅度超限转隔离:${evolved.reason}`);
|
|
133
|
+
return "handled";
|
|
134
|
+
}
|
|
135
|
+
if (item.opts.dryRun) {
|
|
136
|
+
item.deps.store.logReview({
|
|
137
|
+
...rowBaseOf(item), verdict: "rewrite", reason: `试运行未落盘:${evolved.reason}`,
|
|
138
|
+
proposed: proposedOf(evolved), cheapUsage: item.gate.usage, evolveUsage: item.evolveUsage, applied: 0,
|
|
139
|
+
});
|
|
140
|
+
return "handled";
|
|
141
|
+
}
|
|
142
|
+
landRewrite(item, evolved);
|
|
143
|
+
return "handled";
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** Quarantine (or, in trial mode, the row it would have written). */
|
|
148
|
+
function landQuarantine(item, evolved, reason) {
|
|
149
|
+
const { deps, candidate, opts, summary } = item;
|
|
150
|
+
const prior = evolveCountOf(deps.store, candidate);
|
|
151
|
+
if (opts.dryRun) {
|
|
152
|
+
deps.store.logReview({
|
|
153
|
+
...rowBaseOf(item), verdict: "quarantine", reason: `试运行未隔离:${reason}`, proposed: proposedOf(evolved),
|
|
154
|
+
cheapUsage: item.gate.usage, evolveUsage: item.evolveUsage, applied: 0, evolveCount: prior,
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const ok = deps.store.quarantine(candidate.workspaceId, candidate.kind, candidate.id);
|
|
159
|
+
deps.store.logReview({
|
|
160
|
+
...rowBaseOf(item), verdict: "quarantine", reason,
|
|
161
|
+
cheapUsage: item.gate.usage, evolveUsage: item.evolveUsage,
|
|
162
|
+
applied: ok ? 1 : 0, evolveCount: ok ? prior + 1 : prior,
|
|
163
|
+
});
|
|
164
|
+
if (ok) {
|
|
165
|
+
summary.applied += 1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Apply one rewrite through the store's write gates. */
|
|
169
|
+
function landRewrite(item, evolved) {
|
|
170
|
+
const { deps, candidate, opts, summary } = item;
|
|
171
|
+
const prior = evolveCountOf(deps.store, candidate);
|
|
172
|
+
const ctx = {
|
|
173
|
+
workspaceId: candidate.workspaceId, trigger: "item-review",
|
|
174
|
+
sessionId: opts.sessionId, runId: item.runId, model: opts.model, decision: evolved.reason,
|
|
175
|
+
};
|
|
176
|
+
const outcome = candidate.kind === "memory"
|
|
177
|
+
? deps.store.applyProposals(ctx, [{
|
|
178
|
+
action: "update", id: candidate.id,
|
|
179
|
+
title: evolved.title, description: evolved.description, body: evolved.body, corrects: evolved.corrects,
|
|
180
|
+
}])[0]
|
|
181
|
+
: deps.store.applySkillProposals(ctx, [{
|
|
182
|
+
action: "update", scope: candidate.scope ?? "project", id: candidate.id,
|
|
183
|
+
title: evolved.title, description: evolved.description, body: evolved.body,
|
|
184
|
+
}]).outcomes[0];
|
|
185
|
+
const ok = outcome !== undefined && outcome.ok;
|
|
186
|
+
deps.store.logReview({
|
|
187
|
+
...rowBaseOf(item), verdict: "rewrite", reason: ok ? evolved.reason : `门禁拒绝:${outcome?.reason ?? "未知原因"}`,
|
|
188
|
+
cheapUsage: item.gate.usage, evolveUsage: item.evolveUsage, applied: ok ? 1 : 0, evolveCount: ok ? prior + 1 : prior,
|
|
189
|
+
});
|
|
190
|
+
if (ok) {
|
|
191
|
+
summary.applied += 1;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function rowBaseOf(item) {
|
|
195
|
+
return {
|
|
196
|
+
runId: item.runId,
|
|
197
|
+
workspaceId: item.candidate.workspaceId,
|
|
198
|
+
itemKind: item.candidate.kind,
|
|
199
|
+
itemId: item.candidate.id,
|
|
200
|
+
model: item.route.model,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function evolveCountOf(store, candidate) {
|
|
204
|
+
return store.reviewStateOf(candidate.workspaceId, candidate.kind, candidate.id)?.evolveCount ?? 0;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The rewrite payload a non-landing row keeps: exactly the fields the model
|
|
208
|
+
* supplied, or `undefined` when it proposed no text at all.
|
|
209
|
+
*/
|
|
210
|
+
function proposedOf(evolved) {
|
|
211
|
+
const payload = {};
|
|
212
|
+
if (evolved.title !== undefined) {
|
|
213
|
+
payload.title = evolved.title;
|
|
214
|
+
}
|
|
215
|
+
if (evolved.description !== undefined) {
|
|
216
|
+
payload.description = evolved.description;
|
|
217
|
+
}
|
|
218
|
+
if (evolved.body !== undefined) {
|
|
219
|
+
payload.body = evolved.body;
|
|
220
|
+
}
|
|
221
|
+
if (evolved.corrects !== undefined) {
|
|
222
|
+
payload.corrects = evolved.corrects;
|
|
223
|
+
}
|
|
224
|
+
return Object.keys(payload).length === 0 ? undefined : payload;
|
|
225
|
+
}
|
|
226
|
+
/** Similar existing items (hooks only) for the model's duplication check. */
|
|
227
|
+
function neighborHooks(store, candidate) {
|
|
228
|
+
try {
|
|
229
|
+
if (candidate.kind === "memory") {
|
|
230
|
+
const query = candidate.title.slice(0, 60);
|
|
231
|
+
const hits = query.length >= 3 ? store.search(candidate.workspaceId, query, 5) : store.list(candidate.workspaceId).slice(0, 5);
|
|
232
|
+
return hits.filter((hit) => hit.id !== candidate.id).slice(0, 4).map((hit) => ({ id: hit.id, title: hit.title, description: hit.description }));
|
|
233
|
+
}
|
|
234
|
+
const hits = store.searchSkills(candidate.workspaceId, candidate.title.slice(0, 60), 5);
|
|
235
|
+
return hits.filter((hit) => hit.id !== candidate.id).slice(0, 4).map((hit) => ({ id: hit.id, title: hit.title, description: hit.description }));
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function totalOf(raw) {
|
|
242
|
+
return normalizeUsage(raw)?.total ?? 0;
|
|
243
|
+
}
|
|
244
|
+
/** One-line error head for ledger rows (fail-loud shape, capped length). */
|
|
245
|
+
export function failureHead(error, max = 120) {
|
|
246
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
247
|
+
const head = message.split("\n", 1)[0] ?? "";
|
|
248
|
+
return head.length > max ? `${head.slice(0, max)}…` : head;
|
|
249
|
+
}
|
|
250
|
+
/** Relative body change: |new-old| / max(old, 1). Above the cap → quarantine. */
|
|
251
|
+
export function rewriteMagnitude(oldBody, newBody) {
|
|
252
|
+
return Math.abs(newBody.length - oldBody.length) / Math.max(oldBody.length, 1);
|
|
253
|
+
}
|
|
254
|
+
function normalizedOf(raw) {
|
|
255
|
+
return normalizeUsage(raw) ?? zeroUsage();
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=review-ring.js.map
|
package/lib/review.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { MemoryStore } from "./store.js";
|
|
2
1
|
import { type LlmRuntime, type RouteResolver } from "./llm-call.js";
|
|
2
|
+
import type { MemoryStore } from "./store.js";
|
|
3
3
|
/** The LLM face review calls, aliased so each scheduler can name its own. */
|
|
4
4
|
export type ReviewLlm = LlmRuntime;
|
|
5
5
|
export interface ReviewDeps {
|
|
@@ -51,44 +51,19 @@ export interface ReviewRunSummary {
|
|
|
51
51
|
cheapToken: number;
|
|
52
52
|
evolveToken: number;
|
|
53
53
|
}
|
|
54
|
-
/** A rewrite lands only when the body-length ratio stays within this bound. */
|
|
55
|
-
export declare const REWRITE_MAGNITUDE_CAP = 0.3;
|
|
56
|
-
/**
|
|
57
|
-
* Output budget for one cheap-gate call: a verdict JSON **plus** the reasoning
|
|
58
|
-
* a thinking model spends before emitting it. A 512-token budget cut the gate
|
|
59
|
-
* off on reasoning-model calls (2026-10-13) and the cut was ledgered as a
|
|
60
|
-
* route failure, so the per-item failure rate read as a network problem.
|
|
61
|
-
*/
|
|
62
|
-
export declare const GATE_MAX_TOKENS = 1024;
|
|
63
|
-
/** Output budget for the evolve pass: a verdict plus full replacement text. */
|
|
64
|
-
export declare const EVOLVE_MAX_TOKENS = 4096;
|
|
65
54
|
/** Defaults for the review settings the host may override — and edit live. */
|
|
66
55
|
export declare const REVIEW_DEFAULT_DRY_RUN = true;
|
|
67
56
|
export declare const REVIEW_DEFAULT_DAILY_BUDGET = 100;
|
|
68
57
|
export declare const REVIEW_DEFAULT_BATCH_SIZE = 10;
|
|
69
58
|
export declare const REVIEW_DEFAULT_STALE_DAYS = 30;
|
|
70
|
-
/** Relative body change: |new-old| / max(old, 1). Above the cap → quarantine. */
|
|
71
|
-
export declare function rewriteMagnitude(oldBody: string, newBody: string): number;
|
|
72
|
-
/**
|
|
73
|
-
* Collect review candidates across every workspace carrying active items:
|
|
74
|
-
* new items first (newest first), then corrects-linked requeues, then stale
|
|
75
|
-
* requeues (30 days untouched). bounded by `limit`.
|
|
76
|
-
*/
|
|
77
|
-
export declare function collectCandidates(store: MemoryStore, opts?: {
|
|
78
|
-
now?: number | undefined;
|
|
79
|
-
staleDays?: number | undefined;
|
|
80
|
-
limit?: number | undefined;
|
|
81
|
-
}): ReviewCandidate[];
|
|
82
59
|
/**
|
|
83
|
-
* Run one review batch:
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* discipline.
|
|
60
|
+
* Run one review batch: assemble the per-item chain once, then walk each
|
|
61
|
+
* candidate through it — cheap gate, evolve pass on approval, safeguards
|
|
62
|
+
* before landing, one `item-review` row per terminal path, one daily merge at
|
|
63
|
+
* the end. LLM and parse failures degrade to skip rows inside the chain (the
|
|
64
|
+
* run keeps going), exactly like extraction's fail-ledgered discipline.
|
|
88
65
|
*/
|
|
89
66
|
export declare function runReviewBatch(deps: ReviewDeps, candidates: readonly ReviewCandidate[], opts: ReviewRunOptions): Promise<ReviewRunSummary>;
|
|
90
|
-
/** One-line error head for ledger rows (fail-loud shape, capped length). */
|
|
91
|
-
export declare function failureHead(error: unknown, max?: number): string;
|
|
92
67
|
/** Current UTC day `YYYY-MM-DD` for the hard-cap account. */
|
|
93
68
|
export declare function todayUtc(): string;
|
|
94
69
|
/** The event wiring surface (duck-typed cordis context, mirrors extraction). */
|