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
package/lib/review.js
CHANGED
|
@@ -6,148 +6,35 @@
|
|
|
6
6
|
* only know keep/rewrite/quarantine) and oversized rewrites degrade to
|
|
7
7
|
* quarantine instead of landing.
|
|
8
8
|
*
|
|
9
|
+
* This module owns the batch: the hard daily cap, the rollup, and the
|
|
10
|
+
* scheduler; the queue it drains is `review-candidates.ts`, and the per-item
|
|
11
|
+
* walk itself is a responsibility chain (`review-ring.ts`) the batch assembles
|
|
12
|
+
* once per run — gate → evolve → landing — so the order is visible in one
|
|
13
|
+
* place and each step is one link.
|
|
14
|
+
*
|
|
9
15
|
* This track is independent from the extraction pipeline by construction:
|
|
10
16
|
* own prompts (`review-prompt.ts`), own budget (`review_daily`), own ledger
|
|
11
17
|
* (`review_log`), no cursor, no increment. It only shares the store's write
|
|
12
18
|
* gates (`applyProposals` / `applySkillProposals` / `quarantine`).
|
|
13
19
|
*/
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
20
|
+
import { runChain } from "./chain.js";
|
|
21
|
+
import { resolveCallRoute } from "./llm-call.js";
|
|
16
22
|
import { cwdOf } from "./memory-section.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* Output budget for one cheap-gate call: a verdict JSON **plus** the reasoning
|
|
22
|
-
* a thinking model spends before emitting it. A 512-token budget cut the gate
|
|
23
|
-
* off on reasoning-model calls (2026-10-13) and the cut was ledgered as a
|
|
24
|
-
* route failure, so the per-item failure rate read as a network problem.
|
|
25
|
-
*/
|
|
26
|
-
export const GATE_MAX_TOKENS = 1024;
|
|
27
|
-
/** Output budget for the evolve pass: a verdict plus full replacement text. */
|
|
28
|
-
export const EVOLVE_MAX_TOKENS = 4096;
|
|
23
|
+
import { collectCandidates } from "./review-candidates.js";
|
|
24
|
+
import { reviewChain } from "./review-ring.js";
|
|
25
|
+
import { subagentLoaderFor } from "./subagent.js";
|
|
26
|
+
import { zeroUsage } from "./token-usage.js";
|
|
29
27
|
/** Defaults for the review settings the host may override — and edit live. */
|
|
30
28
|
export const REVIEW_DEFAULT_DRY_RUN = true;
|
|
31
29
|
export const REVIEW_DEFAULT_DAILY_BUDGET = 100;
|
|
32
30
|
export const REVIEW_DEFAULT_BATCH_SIZE = 10;
|
|
33
31
|
export const REVIEW_DEFAULT_STALE_DAYS = 30;
|
|
34
|
-
/** Relative body change: |new-old| / max(old, 1). Above the cap → quarantine. */
|
|
35
|
-
export function rewriteMagnitude(oldBody, newBody) {
|
|
36
|
-
return Math.abs(newBody.length - oldBody.length) / Math.max(oldBody.length, 1);
|
|
37
|
-
}
|
|
38
32
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const now = opts.now ?? Date.now();
|
|
45
|
-
const staleMs = (opts.staleDays ?? 30) * 24 * 60 * 60 * 1000;
|
|
46
|
-
const limit = opts.limit ?? 100;
|
|
47
|
-
const fresh = [];
|
|
48
|
-
const requeued = [];
|
|
49
|
-
const stale = [];
|
|
50
|
-
// One push per enumerated row: store primary keys make (workspace, kind,
|
|
51
|
-
// id) unique, so no dedup set is needed — a duplicate here would mean a
|
|
52
|
-
// store invariant broke, and failing loud beats silently merging.
|
|
53
|
-
const push = (candidate) => {
|
|
54
|
-
(candidate.priority === 0 ? fresh : candidate.priority === 1 ? requeued : stale).push(candidate);
|
|
55
|
-
};
|
|
56
|
-
const partitions = new Set();
|
|
57
|
-
for (const summary of store.listWorkspaces()) {
|
|
58
|
-
partitions.add(summary.workspaceId);
|
|
59
|
-
}
|
|
60
|
-
for (const summary of store.listSkillWorkspaces()) {
|
|
61
|
-
partitions.add(summary.workspaceId);
|
|
62
|
-
}
|
|
63
|
-
partitions.add("");
|
|
64
|
-
for (const workspaceId of partitions) {
|
|
65
|
-
const correctsTargets = new Set();
|
|
66
|
-
const memories = store.listAll(workspaceId);
|
|
67
|
-
for (const record of memories) {
|
|
68
|
-
if (record.corrects !== null && record.corrects.length > 0) {
|
|
69
|
-
correctsTargets.add(record.corrects);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
for (const record of memories) {
|
|
73
|
-
if (record.status !== "active") {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
const state = store.reviewStateOf(workspaceId, "memory", record.id);
|
|
77
|
-
const updatedAt = Date.parse(record.updatedAt) || 0;
|
|
78
|
-
if (state === undefined) {
|
|
79
|
-
push({ workspaceId, kind: "memory", id: record.id, priority: 0, title: record.title, description: record.description, body: record.body, memoryType: record.type });
|
|
80
|
-
}
|
|
81
|
-
else if (correctsTargets.has(record.id)) {
|
|
82
|
-
push({ workspaceId, kind: "memory", id: record.id, priority: 1, title: record.title, description: record.description, body: record.body, memoryType: record.type });
|
|
83
|
-
}
|
|
84
|
-
else if (now - updatedAt >= staleMs) {
|
|
85
|
-
push({ workspaceId, kind: "memory", id: record.id, priority: 2, title: record.title, description: record.description, body: record.body, memoryType: record.type });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const projectSeen = new Set();
|
|
89
|
-
const skillsOf = (partition, scope) => {
|
|
90
|
-
for (const record of store.listAllSkills(partition)) {
|
|
91
|
-
if (record.status !== "active" || projectSeen.has(record.id)) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
projectSeen.add(record.id);
|
|
95
|
-
const state = store.reviewStateOf(partition, "skill", record.id);
|
|
96
|
-
const updatedAt = Date.parse(record.updatedAt) || 0;
|
|
97
|
-
const candidate = {
|
|
98
|
-
workspaceId: partition, kind: "skill", id: record.id,
|
|
99
|
-
priority: state === undefined ? 0 : 2,
|
|
100
|
-
title: record.title, description: record.description, body: record.body, scope,
|
|
101
|
-
};
|
|
102
|
-
if (state === undefined || now - updatedAt >= staleMs) {
|
|
103
|
-
push(candidate);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
if (workspaceId !== "") {
|
|
108
|
-
skillsOf(workspaceId, "project");
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// Global skills ride along once (project rows shadow them per id).
|
|
112
|
-
skillsGlobal(store, now, staleMs, push);
|
|
113
|
-
const byPriority = (a, b) => (a.priority - b.priority);
|
|
114
|
-
return [...fresh, ...requeued, ...stale].sort(byPriority).slice(0, limit);
|
|
115
|
-
}
|
|
116
|
-
function skillsGlobal(store, now, staleMs, push) {
|
|
117
|
-
const projectIds = new Set();
|
|
118
|
-
for (const summary of store.listSkillWorkspaces()) {
|
|
119
|
-
if (summary.workspaceId === "") {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
for (const record of store.listAllSkills(summary.workspaceId)) {
|
|
123
|
-
// Archived rows shadow nothing (readWithScope shows the global
|
|
124
|
-
// row once the project row archives after a move-to-global).
|
|
125
|
-
if (record.status === "active") {
|
|
126
|
-
projectIds.add(record.id);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
for (const record of store.listAllSkills("")) {
|
|
131
|
-
if (record.status !== "active" || projectIds.has(record.id)) {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
const state = store.reviewStateOf("", "skill", record.id);
|
|
135
|
-
const updatedAt = Date.parse(record.updatedAt) || 0;
|
|
136
|
-
if (state === undefined || now - updatedAt >= staleMs) {
|
|
137
|
-
push({
|
|
138
|
-
workspaceId: "", kind: "skill", id: record.id,
|
|
139
|
-
priority: state === undefined ? 0 : 2,
|
|
140
|
-
title: record.title, description: record.description, body: record.body, scope: "global",
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Run one review batch: cheap gate per item, evolve pass on approval,
|
|
147
|
-
* safeguards before landing, one `item-review` row per terminal path, one
|
|
148
|
-
* daily merge at the end. Never throws: LLM and parse failures degrade to
|
|
149
|
-
* skip rows (the run keeps going), exactly like extraction's fail-ledgered
|
|
150
|
-
* discipline.
|
|
33
|
+
* Run one review batch: assemble the per-item chain once, then walk each
|
|
34
|
+
* candidate through it — cheap gate, evolve pass on approval, safeguards
|
|
35
|
+
* before landing, one `item-review` row per terminal path, one daily merge at
|
|
36
|
+
* the end. LLM and parse failures degrade to skip rows inside the chain (the
|
|
37
|
+
* run keeps going), exactly like extraction's fail-ledgered discipline.
|
|
151
38
|
*/
|
|
152
39
|
export async function runReviewBatch(deps, candidates, opts) {
|
|
153
40
|
const summary = { reviewed: 0, approved: 0, applied: 0, dropped: 0, backlog: 0, cheapToken: 0, evolveToken: 0 };
|
|
@@ -159,7 +46,10 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
159
46
|
const route = opts.provider === undefined || opts.model === undefined
|
|
160
47
|
? undefined
|
|
161
48
|
: { provider: opts.provider, model: opts.model };
|
|
162
|
-
const
|
|
49
|
+
const llm = deps.llm;
|
|
50
|
+
// The environment class: one loader and one chain for the whole run, in the
|
|
51
|
+
// order the ring declares. Without a route there is no ring to walk.
|
|
52
|
+
const ring = route === undefined || llm === undefined ? undefined : assembleRing({ llm, route, sessionId: opts.sessionId });
|
|
163
53
|
let position = 0;
|
|
164
54
|
for (const candidate of batch) {
|
|
165
55
|
const remaining = deps.store.consumeReviewBudget(opts.day, opts.budget);
|
|
@@ -183,7 +73,7 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
183
73
|
else {
|
|
184
74
|
requeued += 1;
|
|
185
75
|
}
|
|
186
|
-
if (
|
|
76
|
+
if (route === undefined || ring === undefined) {
|
|
187
77
|
deps.store.logReview({
|
|
188
78
|
runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
|
|
189
79
|
verdict: "skip", reason: "no-route", applied: 0,
|
|
@@ -192,7 +82,11 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
192
82
|
position += 1;
|
|
193
83
|
continue;
|
|
194
84
|
}
|
|
195
|
-
|
|
85
|
+
const item = {
|
|
86
|
+
deps, loader: ring.loader, route, runId, candidate, opts, summary, skipReasons,
|
|
87
|
+
gate: { usage: zeroUsage(), raw: undefined },
|
|
88
|
+
};
|
|
89
|
+
await runChain(ring.links, item);
|
|
196
90
|
position += 1;
|
|
197
91
|
}
|
|
198
92
|
summary.backlog = Math.max(0, candidates.length - summary.reviewed - summary.dropped);
|
|
@@ -204,191 +98,15 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
204
98
|
});
|
|
205
99
|
return summary;
|
|
206
100
|
}
|
|
207
|
-
async function reviewOne(deps, runId, candidate, opts, route, summary, skipReasons) {
|
|
208
|
-
const base = {
|
|
209
|
-
runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
|
|
210
|
-
};
|
|
211
|
-
// ── Cheap gate ──────────────────────────────────────────────
|
|
212
|
-
let gateUsage = zeroUsage();
|
|
213
|
-
let gateRaw;
|
|
214
|
-
try {
|
|
215
|
-
const answer = await streamOnce(deps.llm, route, opts.sessionId, REVIEW_CHEAP_SYSTEM_PROMPT, gatePrompt(candidate), GATE_MAX_TOKENS);
|
|
216
|
-
gateRaw = answer.usage;
|
|
217
|
-
const gate = parseGateAnswer(answer.text);
|
|
218
|
-
if (gate === null) {
|
|
219
|
-
// A cut gate is a failure only when no complete verdict came
|
|
220
|
-
// through: the JSON usually lands before the budget runs out, so
|
|
221
|
-
// parse first and blame the finish reason only if it did not.
|
|
222
|
-
if (answer.finish !== undefined && answer.finish !== "stop") {
|
|
223
|
-
throw new Error(`stream finished with ${answer.finish}`);
|
|
224
|
-
}
|
|
225
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: "gate-parse-failed", cheapUsage: gateRaw, applied: 0 });
|
|
226
|
-
skipReasons.push("gate-parse-failed");
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (!gate.worthReview) {
|
|
230
|
-
summary.cheapToken += totalOf(gateRaw);
|
|
231
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: gate.reason, cheapUsage: gateRaw, applied: 0 });
|
|
232
|
-
skipReasons.push("cheap-gate");
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
summary.cheapToken += totalOf(gateRaw);
|
|
236
|
-
gateUsage = normalizedOf(gateRaw);
|
|
237
|
-
}
|
|
238
|
-
catch (error) {
|
|
239
|
-
// The error text rides the row: a bare `gate-llm-failed` hid a ~50%
|
|
240
|
-
// per-item failure rate in production (2026-10-12) with no way to
|
|
241
|
-
// tell transient route faults from systematic ones.
|
|
242
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: `gate-llm-failed: ${failureHead(error)}`, applied: 0 });
|
|
243
|
-
skipReasons.push("gate-llm-failed");
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
summary.approved += 1;
|
|
247
|
-
// ── Evolve pass ─────────────────────────────────────────────
|
|
248
|
-
const neighbors = neighborHooks(deps.store, candidate);
|
|
249
|
-
const extra = candidate.kind === "memory" && candidate.memoryType === "feedback"
|
|
250
|
-
? "注意:feedback 类正文必须带 Why 与 How to apply 两行。"
|
|
251
|
-
: undefined;
|
|
252
|
-
let evolveRaw;
|
|
253
|
-
try {
|
|
254
|
-
const system = candidate.kind === "memory" ? REVIEW_MEMORY_SYSTEM_PROMPT : REVIEW_SKILL_SYSTEM_PROMPT;
|
|
255
|
-
const answer = await streamOnce(deps.llm, route, opts.sessionId, system, evolvePrompt({ id: candidate.id, title: candidate.title, description: candidate.description, body: candidate.body, extra, scope: candidate.scope }, neighbors), EVOLVE_MAX_TOKENS);
|
|
256
|
-
evolveRaw = answer.usage;
|
|
257
|
-
summary.evolveToken += totalOf(evolveRaw);
|
|
258
|
-
// The evolve pass carries full replacement text: a cut stream may hold
|
|
259
|
-
// half a body, so it never becomes a verdict.
|
|
260
|
-
if (answer.finish !== undefined && answer.finish !== "stop") {
|
|
261
|
-
throw new Error(`stream finished with ${answer.finish}`);
|
|
262
|
-
}
|
|
263
|
-
const evolved = parseEvolveAnswer(answer.text);
|
|
264
|
-
if (evolved === null) {
|
|
265
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: "evolve-parse-failed", cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
|
|
266
|
-
skipReasons.push("evolve-parse-failed");
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
if (evolved.verdict === "keep") {
|
|
270
|
-
deps.store.logReview({ ...base, verdict: "keep", reason: evolved.reason, cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (evolved.verdict === "quarantine") {
|
|
274
|
-
landQuarantine(deps, base, candidate, opts, evolved.reason, gateUsage, evolveRaw, proposedOf(evolved), summary);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
// ── Rewrite: magnitude cap first, dry-run second, gates last ──
|
|
278
|
-
const nextBody = evolved.body ?? candidate.body;
|
|
279
|
-
if (rewriteMagnitude(candidate.body, nextBody) > REWRITE_MAGNITUDE_CAP) {
|
|
280
|
-
landQuarantine(deps, base, candidate, opts, `幅度超限转隔离:${evolved.reason}`, gateUsage, evolveRaw, proposedOf(evolved), summary);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (opts.dryRun) {
|
|
284
|
-
deps.store.logReview({ ...base, verdict: "rewrite", reason: `试运行未落盘:${evolved.reason}`, proposed: proposedOf(evolved), cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
landRewrite(deps, base, candidate, opts, evolved, gateUsage, evolveRaw, summary);
|
|
288
|
-
}
|
|
289
|
-
catch (error) {
|
|
290
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: `evolve-llm-failed: ${failureHead(error)}`, cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
|
|
291
|
-
skipReasons.push("evolve-llm-failed");
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* The rewrite payload a non-landing row keeps: exactly the fields the model
|
|
296
|
-
* supplied, or `undefined` when it proposed no text at all.
|
|
297
|
-
*/
|
|
298
|
-
function proposedOf(evolved) {
|
|
299
|
-
const payload = {};
|
|
300
|
-
if (evolved.title !== undefined) {
|
|
301
|
-
payload.title = evolved.title;
|
|
302
|
-
}
|
|
303
|
-
if (evolved.description !== undefined) {
|
|
304
|
-
payload.description = evolved.description;
|
|
305
|
-
}
|
|
306
|
-
if (evolved.body !== undefined) {
|
|
307
|
-
payload.body = evolved.body;
|
|
308
|
-
}
|
|
309
|
-
if (evolved.corrects !== undefined) {
|
|
310
|
-
payload.corrects = evolved.corrects;
|
|
311
|
-
}
|
|
312
|
-
return Object.keys(payload).length === 0 ? undefined : payload;
|
|
313
|
-
}
|
|
314
|
-
function neighborHooks(store, candidate) {
|
|
315
|
-
try {
|
|
316
|
-
if (candidate.kind === "memory") {
|
|
317
|
-
const query = candidate.title.slice(0, 60);
|
|
318
|
-
const hits = query.length >= 3 ? store.search(candidate.workspaceId, query, 5) : store.list(candidate.workspaceId).slice(0, 5);
|
|
319
|
-
return hits.filter((hit) => hit.id !== candidate.id).slice(0, 4).map((hit) => ({ id: hit.id, title: hit.title, description: hit.description }));
|
|
320
|
-
}
|
|
321
|
-
const hits = store.searchSkills(candidate.workspaceId, candidate.title.slice(0, 60), 5);
|
|
322
|
-
return hits.filter((hit) => hit.id !== candidate.id).slice(0, 4).map((hit) => ({ id: hit.id, title: hit.title, description: hit.description }));
|
|
323
|
-
}
|
|
324
|
-
catch {
|
|
325
|
-
return [];
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
function landQuarantine(deps, base, candidate, opts, reason, gateUsage, evolveRaw, proposed, summary) {
|
|
329
|
-
const prior = deps.store.reviewStateOf(candidate.workspaceId, candidate.kind, candidate.id)?.evolveCount ?? 0;
|
|
330
|
-
if (opts.dryRun) {
|
|
331
|
-
deps.store.logReview({ ...base, verdict: "quarantine", reason: `试运行未隔离:${reason}`, proposed, cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0, evolveCount: prior });
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
const ok = deps.store.quarantine(candidate.workspaceId, candidate.kind, candidate.id);
|
|
335
|
-
deps.store.logReview({
|
|
336
|
-
...base, verdict: "quarantine", reason, cheapUsage: gateUsage, evolveUsage: evolveRaw,
|
|
337
|
-
applied: ok ? 1 : 0, evolveCount: ok ? prior + 1 : prior,
|
|
338
|
-
});
|
|
339
|
-
if (ok) {
|
|
340
|
-
summary.applied += 1;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
function landRewrite(deps, base, candidate, opts, evolved, gateUsage, evolveRaw, summary) {
|
|
344
|
-
const prior = deps.store.reviewStateOf(candidate.workspaceId, candidate.kind, candidate.id)?.evolveCount ?? 0;
|
|
345
|
-
const ctx = { workspaceId: candidate.workspaceId, trigger: "item-review", sessionId: opts.sessionId, runId: base.runId, model: opts.model, decision: evolved.reason };
|
|
346
|
-
if (candidate.kind === "memory") {
|
|
347
|
-
const [outcome] = deps.store.applyProposals(ctx, [{
|
|
348
|
-
action: "update", id: candidate.id,
|
|
349
|
-
title: evolved.title, description: evolved.description, body: evolved.body,
|
|
350
|
-
corrects: evolved.corrects,
|
|
351
|
-
}]);
|
|
352
|
-
const ok = outcome !== undefined && outcome.ok;
|
|
353
|
-
deps.store.logReview({
|
|
354
|
-
...base, verdict: "rewrite", reason: ok ? evolved.reason : `门禁拒绝:${outcome?.reason ?? "未知原因"}`,
|
|
355
|
-
cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: ok ? 1 : 0, evolveCount: ok ? prior + 1 : prior,
|
|
356
|
-
});
|
|
357
|
-
if (ok) {
|
|
358
|
-
summary.applied += 1;
|
|
359
|
-
}
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
const { outcomes } = deps.store.applySkillProposals(ctx, [{
|
|
363
|
-
action: "update", scope: candidate.scope ?? "project", id: candidate.id,
|
|
364
|
-
title: evolved.title, description: evolved.description, body: evolved.body,
|
|
365
|
-
}]);
|
|
366
|
-
const outcome = outcomes[0];
|
|
367
|
-
const ok = outcome !== undefined && outcome.ok;
|
|
368
|
-
deps.store.logReview({
|
|
369
|
-
...base, verdict: "rewrite", reason: ok ? evolved.reason : `门禁拒绝:${outcome?.reason ?? "未知原因"}`,
|
|
370
|
-
cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: ok ? 1 : 0, evolveCount: ok ? prior + 1 : prior,
|
|
371
|
-
});
|
|
372
|
-
if (ok) {
|
|
373
|
-
summary.applied += 1;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
function totalOf(raw) {
|
|
377
|
-
return normalizeUsage(raw)?.total ?? 0;
|
|
378
|
-
}
|
|
379
|
-
/** One-line error head for ledger rows (fail-loud shape, capped length). */
|
|
380
|
-
export function failureHead(error, max = 120) {
|
|
381
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
382
|
-
const head = message.split("\n", 1)[0] ?? "";
|
|
383
|
-
return head.length > max ? `${head.slice(0, max)}…` : head;
|
|
384
|
-
}
|
|
385
|
-
function normalizedOf(raw) {
|
|
386
|
-
return normalizeUsage(raw) ?? zeroUsage();
|
|
387
|
-
}
|
|
388
101
|
/** Current UTC day `YYYY-MM-DD` for the hard-cap account. */
|
|
389
102
|
export function todayUtc() {
|
|
390
103
|
return new Date().toISOString().slice(0, 10);
|
|
391
104
|
}
|
|
105
|
+
/** Assemble one run's ring (simple factory: one context, one bound loader, one chain). */
|
|
106
|
+
function assembleRing(call) {
|
|
107
|
+
const loader = subagentLoaderFor(call);
|
|
108
|
+
return { loader, links: reviewChain() };
|
|
109
|
+
}
|
|
392
110
|
/**
|
|
393
111
|
* Wire the review triggers onto the host context. Global single-flight with
|
|
394
112
|
* a minimum interval: turn boundaries arm the pending workspace, compaction
|
package/lib/store.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export interface ReviewLogEntry {
|
|
|
57
57
|
itemId: string;
|
|
58
58
|
verdict: "keep" | "rewrite" | "quarantine" | "skip";
|
|
59
59
|
reason?: string | undefined;
|
|
60
|
+
/** The model this verdict ran on; `undefined` only when the run had no route at all. */
|
|
61
|
+
model?: string | undefined;
|
|
60
62
|
/** Normalized usage JSON of the cheap gate call, `undefined` when skipped before any call. */
|
|
61
63
|
cheapUsage?: unknown;
|
|
62
64
|
/** Normalized usage JSON of the evolve call, `undefined` unless rewritten. */
|
package/lib/store.js
CHANGED
|
@@ -131,7 +131,8 @@ CREATE TABLE IF NOT EXISTS memory_usage (
|
|
|
131
131
|
PRIMARY KEY (workspace_id, memory_id)
|
|
132
132
|
) STRICT;
|
|
133
133
|
-- Per-item content-review verdicts (the item-review trigger's account):
|
|
134
|
-
-- one row per reviewed item per run
|
|
134
|
+
-- one row per reviewed item per run, with the model it ran on so a failure
|
|
135
|
+
-- rate can be attributed instead of guessed at. Pruned with the ledgers.
|
|
135
136
|
CREATE TABLE IF NOT EXISTS review_log (
|
|
136
137
|
run_id TEXT, ts TEXT NOT NULL, workspace_id TEXT,
|
|
137
138
|
item_kind TEXT NOT NULL CHECK (item_kind IN ('memory','skill')),
|
|
@@ -139,6 +140,7 @@ CREATE TABLE IF NOT EXISTS review_log (
|
|
|
139
140
|
verdict TEXT NOT NULL CHECK (verdict IN ('keep','rewrite','quarantine','skip')),
|
|
140
141
|
reason TEXT,
|
|
141
142
|
proposed TEXT,
|
|
143
|
+
model TEXT,
|
|
142
144
|
cheap_usage TEXT, evolve_usage TEXT,
|
|
143
145
|
applied INTEGER NOT NULL DEFAULT 0,
|
|
144
146
|
evolve_count INTEGER NOT NULL DEFAULT 0
|
|
@@ -313,6 +315,7 @@ export async function openMemoryStore(explicitPath) {
|
|
|
313
315
|
ensureExtractionCheapUsage(database);
|
|
314
316
|
ensureExtractionCheapError(database);
|
|
315
317
|
ensureReviewProposed(database);
|
|
318
|
+
ensureReviewModel(database);
|
|
316
319
|
const stmts = {
|
|
317
320
|
get: database.prepare("SELECT * FROM memories WHERE workspace_id = ? AND id = ?"),
|
|
318
321
|
list: database.prepare(`SELECT * FROM memories WHERE workspace_id = ? AND status = 'active' ${LIST_ORDER}`),
|
|
@@ -367,8 +370,8 @@ export async function openMemoryStore(explicitPath) {
|
|
|
367
370
|
memUseGet: database.prepare("SELECT use_count, last_used_at FROM memory_usage WHERE workspace_id = ? AND memory_id = ?"),
|
|
368
371
|
memUseUpsert: database.prepare("INSERT INTO memory_usage (workspace_id, memory_id, use_count, last_used_at) VALUES (?, ?, 1, ?) " +
|
|
369
372
|
"ON CONFLICT (workspace_id, memory_id) DO UPDATE SET use_count = use_count + 1, last_used_at = excluded.last_used_at"),
|
|
370
|
-
reviewLog: database.prepare("INSERT INTO review_log (run_id, ts, workspace_id, item_kind, item_id, verdict, reason, proposed, cheap_usage, evolve_usage, applied, evolve_count) " +
|
|
371
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
373
|
+
reviewLog: database.prepare("INSERT INTO review_log (run_id, ts, workspace_id, item_kind, item_id, verdict, reason, proposed, model, cheap_usage, evolve_usage, applied, evolve_count) " +
|
|
374
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
372
375
|
reviewLast: database.prepare(`SELECT ts, applied FROM review_log WHERE workspace_id IS ? AND item_kind = ? AND item_id = ? AND NOT ${INCOMPLETE_REVIEW_SQL} ORDER BY rowid DESC LIMIT 1`),
|
|
373
376
|
reviewEvolved: database.prepare("SELECT COUNT(*) AS n FROM review_log WHERE workspace_id IS ? AND item_kind = ? AND item_id = ? AND applied = 1"),
|
|
374
377
|
reviewPrune: database.prepare("DELETE FROM review_log WHERE ts < ?"),
|
|
@@ -913,7 +916,7 @@ export async function openMemoryStore(explicitPath) {
|
|
|
913
916
|
}
|
|
914
917
|
},
|
|
915
918
|
logReview(entry) {
|
|
916
|
-
stmts.reviewLog.run(entry.runId ?? null, new Date().toISOString(), entry.workspaceId ?? null, entry.itemKind, entry.itemId, entry.verdict, entry.reason ?? null, entry.proposed === undefined ? null : JSON.stringify(entry.proposed), entry.cheapUsage === undefined ? null : JSON.stringify(entry.cheapUsage), entry.evolveUsage === undefined ? null : JSON.stringify(entry.evolveUsage), entry.applied ?? 0, entry.evolveCount ?? 0);
|
|
919
|
+
stmts.reviewLog.run(entry.runId ?? null, new Date().toISOString(), entry.workspaceId ?? null, entry.itemKind, entry.itemId, entry.verdict, entry.reason ?? null, entry.proposed === undefined ? null : JSON.stringify(entry.proposed), entry.model ?? null, entry.cheapUsage === undefined ? null : JSON.stringify(entry.cheapUsage), entry.evolveUsage === undefined ? null : JSON.stringify(entry.evolveUsage), entry.applied ?? 0, entry.evolveCount ?? 0);
|
|
917
920
|
},
|
|
918
921
|
consumeReviewBudget(day, budget) {
|
|
919
922
|
stmts.dailyUpsert.run(day, budget);
|
|
@@ -1038,6 +1041,18 @@ function ensureReviewProposed(database) {
|
|
|
1038
1041
|
}
|
|
1039
1042
|
database.exec("ALTER TABLE review_log ADD COLUMN proposed TEXT");
|
|
1040
1043
|
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Add the review ledger `model` column to a store created before verdict rows
|
|
1046
|
+
* recorded the model they ran on. Same single-migration discipline as the
|
|
1047
|
+
* columns above.
|
|
1048
|
+
*/
|
|
1049
|
+
function ensureReviewModel(database) {
|
|
1050
|
+
const columns = database.prepare("SELECT name FROM pragma_table_info('review_log')").all();
|
|
1051
|
+
if (columns.some((column) => column.name === "model")) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
database.exec("ALTER TABLE review_log ADD COLUMN model TEXT");
|
|
1055
|
+
}
|
|
1041
1056
|
/** Parse the daily skip-counts map; corrupt payloads degrade to empty. */
|
|
1042
1057
|
function parseCounts(raw) {
|
|
1043
1058
|
if (raw === null) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every subagent this plugin runs, declared in one table.
|
|
3
|
+
*
|
|
4
|
+
* A definition carries the job's identity, its whole instruction (system
|
|
5
|
+
* prompt), its output-budget fallback and its answer parser. Feature code
|
|
6
|
+
* loads definitions from here and never writes a prompt or a budget inline;
|
|
7
|
+
* adding a job means adding a row, and a job with no row cannot be called.
|
|
8
|
+
*
|
|
9
|
+
* The four review jobs are separate definitions on purpose: the memory rubric
|
|
10
|
+
* requires a memory to survive having its repo name stripped, while a project
|
|
11
|
+
* skill is *supposed* to be repo-bound, so one prompt cannot serve both.
|
|
12
|
+
*/
|
|
13
|
+
import { type CheapVerdict, type ParsedAnswer } from "./extraction-prompt.js";
|
|
14
|
+
import { type EvolveVerdict, type GateVerdict } from "./review-prompt.js";
|
|
15
|
+
import type { SubagentDefinition } from "./subagent.js";
|
|
16
|
+
/**
|
|
17
|
+
* Output-budget fallbacks, used only for adapters that advertise no
|
|
18
|
+
* per-request cap of their own. A gate verdict is one JSON object plus the
|
|
19
|
+
* reasoning a thinking model spends before emitting it; the evolve/proposal
|
|
20
|
+
* calls additionally carry full replacement bodies.
|
|
21
|
+
*/
|
|
22
|
+
export declare const GATE_MAX_TOKENS = 1024;
|
|
23
|
+
export declare const EVOLVE_MAX_TOKENS = 4096;
|
|
24
|
+
export declare const EXTRACTION_MAX_TOKENS = 4096;
|
|
25
|
+
/** The memory cheap gate: is this entry worth a close look at all? */
|
|
26
|
+
export declare const reviewGateMemory: SubagentDefinition<GateVerdict | null>;
|
|
27
|
+
/** The skill cheap gate: same question, scope-relative home rules. */
|
|
28
|
+
export declare const reviewGateSkill: SubagentDefinition<GateVerdict | null>;
|
|
29
|
+
/** The memory evolve pass: five-dimension judgment plus the proposed rewrite. */
|
|
30
|
+
export declare const reviewEvolveMemory: SubagentDefinition<EvolveVerdict | null>;
|
|
31
|
+
/** The skill evolve pass: scope-relative reuse, executability, duplication. */
|
|
32
|
+
export declare const reviewEvolveSkill: SubagentDefinition<EvolveVerdict | null>;
|
|
33
|
+
/** The extraction pre-gate: does this increment hold anything worth proposing? */
|
|
34
|
+
export declare const extractionGate: SubagentDefinition<CheapVerdict | null>;
|
|
35
|
+
/** The extraction proposal call: the run's one expensive call. */
|
|
36
|
+
export declare const extractionMain: SubagentDefinition<ParsedAnswer>;
|
|
37
|
+
/** Every definition, for the "which jobs exist" read and the id-uniqueness check. */
|
|
38
|
+
export declare const SUBAGENT_DEFINITIONS: readonly SubagentDefinition<unknown>[];
|
|
39
|
+
/** The gate definition one review candidate loads, by item kind. */
|
|
40
|
+
export declare const REVIEW_GATE_BY_KIND: Readonly<Record<"memory" | "skill", SubagentDefinition<GateVerdict | null>>>;
|
|
41
|
+
/** The evolve definition one review candidate loads, by item kind. */
|
|
42
|
+
export declare const REVIEW_EVOLVE_BY_KIND: Readonly<Record<"memory" | "skill", SubagentDefinition<EvolveVerdict | null>>>;
|
|
43
|
+
//# sourceMappingURL=subagent-defs.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every subagent this plugin runs, declared in one table.
|
|
3
|
+
*
|
|
4
|
+
* A definition carries the job's identity, its whole instruction (system
|
|
5
|
+
* prompt), its output-budget fallback and its answer parser. Feature code
|
|
6
|
+
* loads definitions from here and never writes a prompt or a budget inline;
|
|
7
|
+
* adding a job means adding a row, and a job with no row cannot be called.
|
|
8
|
+
*
|
|
9
|
+
* The four review jobs are separate definitions on purpose: the memory rubric
|
|
10
|
+
* requires a memory to survive having its repo name stripped, while a project
|
|
11
|
+
* skill is *supposed* to be repo-bound, so one prompt cannot serve both.
|
|
12
|
+
*/
|
|
13
|
+
import { EXTRACTION_CHEAP_SYSTEM_PROMPT, EXTRACTION_SYSTEM_PROMPT, parseCheapAnswer, parseExtractionAnswer, } from "./extraction-prompt.js";
|
|
14
|
+
import { parseEvolveAnswer, parseGateAnswer, REVIEW_MEMORY_CHEAP_SYSTEM_PROMPT, REVIEW_MEMORY_SYSTEM_PROMPT, REVIEW_SKILL_CHEAP_SYSTEM_PROMPT, REVIEW_SKILL_SYSTEM_PROMPT, } from "./review-prompt.js";
|
|
15
|
+
/**
|
|
16
|
+
* Output-budget fallbacks, used only for adapters that advertise no
|
|
17
|
+
* per-request cap of their own. A gate verdict is one JSON object plus the
|
|
18
|
+
* reasoning a thinking model spends before emitting it; the evolve/proposal
|
|
19
|
+
* calls additionally carry full replacement bodies.
|
|
20
|
+
*/
|
|
21
|
+
export const GATE_MAX_TOKENS = 1024;
|
|
22
|
+
export const EVOLVE_MAX_TOKENS = 4096;
|
|
23
|
+
export const EXTRACTION_MAX_TOKENS = 4096;
|
|
24
|
+
/** The memory cheap gate: is this entry worth a close look at all? */
|
|
25
|
+
export const reviewGateMemory = {
|
|
26
|
+
id: "review-gate-memory",
|
|
27
|
+
system: REVIEW_MEMORY_CHEAP_SYSTEM_PROMPT,
|
|
28
|
+
maxTokens: GATE_MAX_TOKENS,
|
|
29
|
+
parse: (text) => parseGateAnswer(text),
|
|
30
|
+
};
|
|
31
|
+
/** The skill cheap gate: same question, scope-relative home rules. */
|
|
32
|
+
export const reviewGateSkill = {
|
|
33
|
+
id: "review-gate-skill",
|
|
34
|
+
system: REVIEW_SKILL_CHEAP_SYSTEM_PROMPT,
|
|
35
|
+
maxTokens: GATE_MAX_TOKENS,
|
|
36
|
+
parse: (text) => parseGateAnswer(text),
|
|
37
|
+
};
|
|
38
|
+
/** The memory evolve pass: five-dimension judgment plus the proposed rewrite. */
|
|
39
|
+
export const reviewEvolveMemory = {
|
|
40
|
+
id: "review-evolve-memory",
|
|
41
|
+
system: REVIEW_MEMORY_SYSTEM_PROMPT,
|
|
42
|
+
maxTokens: EVOLVE_MAX_TOKENS,
|
|
43
|
+
parse: (text) => parseEvolveAnswer(text),
|
|
44
|
+
};
|
|
45
|
+
/** The skill evolve pass: scope-relative reuse, executability, duplication. */
|
|
46
|
+
export const reviewEvolveSkill = {
|
|
47
|
+
id: "review-evolve-skill",
|
|
48
|
+
system: REVIEW_SKILL_SYSTEM_PROMPT,
|
|
49
|
+
maxTokens: EVOLVE_MAX_TOKENS,
|
|
50
|
+
parse: (text) => parseEvolveAnswer(text),
|
|
51
|
+
};
|
|
52
|
+
/** The extraction pre-gate: does this increment hold anything worth proposing? */
|
|
53
|
+
export const extractionGate = {
|
|
54
|
+
id: "extraction-gate",
|
|
55
|
+
system: EXTRACTION_CHEAP_SYSTEM_PROMPT,
|
|
56
|
+
maxTokens: GATE_MAX_TOKENS,
|
|
57
|
+
parse: (text) => parseCheapAnswer(text),
|
|
58
|
+
};
|
|
59
|
+
/** The extraction proposal call: the run's one expensive call. */
|
|
60
|
+
export const extractionMain = {
|
|
61
|
+
id: "extraction-main",
|
|
62
|
+
system: EXTRACTION_SYSTEM_PROMPT,
|
|
63
|
+
maxTokens: EXTRACTION_MAX_TOKENS,
|
|
64
|
+
parse: (text) => parseExtractionAnswer(text),
|
|
65
|
+
};
|
|
66
|
+
/** Every definition, for the "which jobs exist" read and the id-uniqueness check. */
|
|
67
|
+
export const SUBAGENT_DEFINITIONS = [
|
|
68
|
+
reviewGateMemory,
|
|
69
|
+
reviewGateSkill,
|
|
70
|
+
reviewEvolveMemory,
|
|
71
|
+
reviewEvolveSkill,
|
|
72
|
+
extractionGate,
|
|
73
|
+
extractionMain,
|
|
74
|
+
];
|
|
75
|
+
/** The gate definition one review candidate loads, by item kind. */
|
|
76
|
+
export const REVIEW_GATE_BY_KIND = {
|
|
77
|
+
memory: reviewGateMemory,
|
|
78
|
+
skill: reviewGateSkill,
|
|
79
|
+
};
|
|
80
|
+
/** The evolve definition one review candidate loads, by item kind. */
|
|
81
|
+
export const REVIEW_EVOLVE_BY_KIND = {
|
|
82
|
+
memory: reviewEvolveMemory,
|
|
83
|
+
skill: reviewEvolveSkill,
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=subagent-defs.js.map
|