dsh-continual-evolve 0.15.11 → 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 -7
- package/README.zh.md +13 -7
- 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.d.ts +9 -5
- package/lib/extraction.js +31 -256
- package/lib/index.js +40 -3
- package/lib/llm-call.d.ts +168 -0
- package/lib/llm-call.js +284 -0
- 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 +15 -33
- package/lib/review.js +48 -359
- 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,145 +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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* a thinking model spends before emitting it. A 512-token budget cut the gate
|
|
22
|
-
* off on reasoning-model calls (2026-10-13) and the cut was ledgered as a
|
|
23
|
-
* route failure, so the per-item failure rate read as a network problem.
|
|
24
|
-
*/
|
|
25
|
-
export const GATE_MAX_TOKENS = 1024;
|
|
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";
|
|
26
27
|
/** Defaults for the review settings the host may override — and edit live. */
|
|
27
28
|
export const REVIEW_DEFAULT_DRY_RUN = true;
|
|
28
29
|
export const REVIEW_DEFAULT_DAILY_BUDGET = 100;
|
|
29
30
|
export const REVIEW_DEFAULT_BATCH_SIZE = 10;
|
|
30
31
|
export const REVIEW_DEFAULT_STALE_DAYS = 30;
|
|
31
|
-
/** Relative body change: |new-old| / max(old, 1). Above the cap → quarantine. */
|
|
32
|
-
export function rewriteMagnitude(oldBody, newBody) {
|
|
33
|
-
return Math.abs(newBody.length - oldBody.length) / Math.max(oldBody.length, 1);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Collect review candidates across every workspace carrying active items:
|
|
37
|
-
* new items first (newest first), then corrects-linked requeues, then stale
|
|
38
|
-
* requeues (30 days untouched). bounded by `limit`.
|
|
39
|
-
*/
|
|
40
|
-
export function collectCandidates(store, opts = {}) {
|
|
41
|
-
const now = opts.now ?? Date.now();
|
|
42
|
-
const staleMs = (opts.staleDays ?? 30) * 24 * 60 * 60 * 1000;
|
|
43
|
-
const limit = opts.limit ?? 100;
|
|
44
|
-
const fresh = [];
|
|
45
|
-
const requeued = [];
|
|
46
|
-
const stale = [];
|
|
47
|
-
// One push per enumerated row: store primary keys make (workspace, kind,
|
|
48
|
-
// id) unique, so no dedup set is needed — a duplicate here would mean a
|
|
49
|
-
// store invariant broke, and failing loud beats silently merging.
|
|
50
|
-
const push = (candidate) => {
|
|
51
|
-
(candidate.priority === 0 ? fresh : candidate.priority === 1 ? requeued : stale).push(candidate);
|
|
52
|
-
};
|
|
53
|
-
const partitions = new Set();
|
|
54
|
-
for (const summary of store.listWorkspaces()) {
|
|
55
|
-
partitions.add(summary.workspaceId);
|
|
56
|
-
}
|
|
57
|
-
for (const summary of store.listSkillWorkspaces()) {
|
|
58
|
-
partitions.add(summary.workspaceId);
|
|
59
|
-
}
|
|
60
|
-
partitions.add("");
|
|
61
|
-
for (const workspaceId of partitions) {
|
|
62
|
-
const correctsTargets = new Set();
|
|
63
|
-
const memories = store.listAll(workspaceId);
|
|
64
|
-
for (const record of memories) {
|
|
65
|
-
if (record.corrects !== null && record.corrects.length > 0) {
|
|
66
|
-
correctsTargets.add(record.corrects);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
for (const record of memories) {
|
|
70
|
-
if (record.status !== "active") {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const state = store.reviewStateOf(workspaceId, "memory", record.id);
|
|
74
|
-
const updatedAt = Date.parse(record.updatedAt) || 0;
|
|
75
|
-
if (state === undefined) {
|
|
76
|
-
push({ workspaceId, kind: "memory", id: record.id, priority: 0, title: record.title, description: record.description, body: record.body, memoryType: record.type });
|
|
77
|
-
}
|
|
78
|
-
else if (correctsTargets.has(record.id)) {
|
|
79
|
-
push({ workspaceId, kind: "memory", id: record.id, priority: 1, title: record.title, description: record.description, body: record.body, memoryType: record.type });
|
|
80
|
-
}
|
|
81
|
-
else if (now - updatedAt >= staleMs) {
|
|
82
|
-
push({ workspaceId, kind: "memory", id: record.id, priority: 2, title: record.title, description: record.description, body: record.body, memoryType: record.type });
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const projectSeen = new Set();
|
|
86
|
-
const skillsOf = (partition, scope) => {
|
|
87
|
-
for (const record of store.listAllSkills(partition)) {
|
|
88
|
-
if (record.status !== "active" || projectSeen.has(record.id)) {
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
projectSeen.add(record.id);
|
|
92
|
-
const state = store.reviewStateOf(partition, "skill", record.id);
|
|
93
|
-
const updatedAt = Date.parse(record.updatedAt) || 0;
|
|
94
|
-
const candidate = {
|
|
95
|
-
workspaceId: partition, kind: "skill", id: record.id,
|
|
96
|
-
priority: state === undefined ? 0 : 2,
|
|
97
|
-
title: record.title, description: record.description, body: record.body, scope,
|
|
98
|
-
};
|
|
99
|
-
if (state === undefined || now - updatedAt >= staleMs) {
|
|
100
|
-
push(candidate);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
if (workspaceId !== "") {
|
|
105
|
-
skillsOf(workspaceId, "project");
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// Global skills ride along once (project rows shadow them per id).
|
|
109
|
-
skillsGlobal(store, now, staleMs, push);
|
|
110
|
-
const byPriority = (a, b) => (a.priority - b.priority);
|
|
111
|
-
return [...fresh, ...requeued, ...stale].sort(byPriority).slice(0, limit);
|
|
112
|
-
}
|
|
113
|
-
function skillsGlobal(store, now, staleMs, push) {
|
|
114
|
-
const projectIds = new Set();
|
|
115
|
-
for (const summary of store.listSkillWorkspaces()) {
|
|
116
|
-
if (summary.workspaceId === "") {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
for (const record of store.listAllSkills(summary.workspaceId)) {
|
|
120
|
-
// Archived rows shadow nothing (readWithScope shows the global
|
|
121
|
-
// row once the project row archives after a move-to-global).
|
|
122
|
-
if (record.status === "active") {
|
|
123
|
-
projectIds.add(record.id);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
for (const record of store.listAllSkills("")) {
|
|
128
|
-
if (record.status !== "active" || projectIds.has(record.id)) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const state = store.reviewStateOf("", "skill", record.id);
|
|
132
|
-
const updatedAt = Date.parse(record.updatedAt) || 0;
|
|
133
|
-
if (state === undefined || now - updatedAt >= staleMs) {
|
|
134
|
-
push({
|
|
135
|
-
workspaceId: "", kind: "skill", id: record.id,
|
|
136
|
-
priority: state === undefined ? 0 : 2,
|
|
137
|
-
title: record.title, description: record.description, body: record.body, scope: "global",
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
32
|
/**
|
|
143
|
-
* Run one review batch:
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* 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.
|
|
148
38
|
*/
|
|
149
39
|
export async function runReviewBatch(deps, candidates, opts) {
|
|
150
40
|
const summary = { reviewed: 0, approved: 0, applied: 0, dropped: 0, backlog: 0, cheapToken: 0, evolveToken: 0 };
|
|
@@ -153,7 +43,13 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
153
43
|
const runId = opts.runId ?? `review:${opts.day}:${Date.now()}`;
|
|
154
44
|
let newItems = 0;
|
|
155
45
|
let requeued = 0;
|
|
156
|
-
const
|
|
46
|
+
const route = opts.provider === undefined || opts.model === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: { provider: opts.provider, model: opts.model };
|
|
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 });
|
|
157
53
|
let position = 0;
|
|
158
54
|
for (const candidate of batch) {
|
|
159
55
|
const remaining = deps.store.consumeReviewBudget(opts.day, opts.budget);
|
|
@@ -177,7 +73,7 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
177
73
|
else {
|
|
178
74
|
requeued += 1;
|
|
179
75
|
}
|
|
180
|
-
if (
|
|
76
|
+
if (route === undefined || ring === undefined) {
|
|
181
77
|
deps.store.logReview({
|
|
182
78
|
runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
|
|
183
79
|
verdict: "skip", reason: "no-route", applied: 0,
|
|
@@ -186,7 +82,11 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
186
82
|
position += 1;
|
|
187
83
|
continue;
|
|
188
84
|
}
|
|
189
|
-
|
|
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);
|
|
190
90
|
position += 1;
|
|
191
91
|
}
|
|
192
92
|
summary.backlog = Math.max(0, candidates.length - summary.reviewed - summary.dropped);
|
|
@@ -198,227 +98,15 @@ export async function runReviewBatch(deps, candidates, opts) {
|
|
|
198
98
|
});
|
|
199
99
|
return summary;
|
|
200
100
|
}
|
|
201
|
-
async function reviewOne(deps, runId, candidate, opts, summary, skipReasons) {
|
|
202
|
-
const base = {
|
|
203
|
-
runId, workspaceId: candidate.workspaceId, itemKind: candidate.kind, itemId: candidate.id,
|
|
204
|
-
};
|
|
205
|
-
// ── Cheap gate ──────────────────────────────────────────────
|
|
206
|
-
let gateUsage = zeroUsage();
|
|
207
|
-
let gateRaw;
|
|
208
|
-
try {
|
|
209
|
-
const answer = await streamOnce(deps.llm, opts, REVIEW_CHEAP_SYSTEM_PROMPT, gatePrompt(candidate), GATE_MAX_TOKENS);
|
|
210
|
-
gateRaw = answer.usage;
|
|
211
|
-
const gate = parseGateAnswer(answer.text);
|
|
212
|
-
if (gate === null) {
|
|
213
|
-
// A cut gate is a failure only when no complete verdict came
|
|
214
|
-
// through: the JSON usually lands before the budget runs out, so
|
|
215
|
-
// parse first and blame the finish reason only if it did not.
|
|
216
|
-
if (answer.finish !== undefined && answer.finish !== "stop") {
|
|
217
|
-
throw new Error(`stream finished with ${answer.finish}`);
|
|
218
|
-
}
|
|
219
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: "gate-parse-failed", cheapUsage: gateRaw, applied: 0 });
|
|
220
|
-
skipReasons.push("gate-parse-failed");
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (!gate.worthReview) {
|
|
224
|
-
summary.cheapToken += totalOf(gateRaw);
|
|
225
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: gate.reason, cheapUsage: gateRaw, applied: 0 });
|
|
226
|
-
skipReasons.push("cheap-gate");
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
summary.cheapToken += totalOf(gateRaw);
|
|
230
|
-
gateUsage = normalizedOf(gateRaw);
|
|
231
|
-
}
|
|
232
|
-
catch (error) {
|
|
233
|
-
// The error text rides the row: a bare `gate-llm-failed` hid a ~50%
|
|
234
|
-
// per-item failure rate in production (2026-10-12) with no way to
|
|
235
|
-
// tell transient route faults from systematic ones.
|
|
236
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: `gate-llm-failed: ${failureHead(error)}`, applied: 0 });
|
|
237
|
-
skipReasons.push("gate-llm-failed");
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
summary.approved += 1;
|
|
241
|
-
// ── Evolve pass ─────────────────────────────────────────────
|
|
242
|
-
const neighbors = neighborHooks(deps.store, candidate);
|
|
243
|
-
const extra = candidate.kind === "memory" && candidate.memoryType === "feedback"
|
|
244
|
-
? "注意:feedback 类正文必须带 Why 与 How to apply 两行。"
|
|
245
|
-
: undefined;
|
|
246
|
-
let evolveRaw;
|
|
247
|
-
try {
|
|
248
|
-
const system = candidate.kind === "memory" ? REVIEW_MEMORY_SYSTEM_PROMPT : REVIEW_SKILL_SYSTEM_PROMPT;
|
|
249
|
-
const answer = await streamOnce(deps.llm, opts, system, evolvePrompt({ id: candidate.id, title: candidate.title, description: candidate.description, body: candidate.body, extra, scope: candidate.scope }, neighbors), 4096);
|
|
250
|
-
evolveRaw = answer.usage;
|
|
251
|
-
summary.evolveToken += totalOf(evolveRaw);
|
|
252
|
-
// The evolve pass carries full replacement text: a cut stream may hold
|
|
253
|
-
// half a body, so it never becomes a verdict.
|
|
254
|
-
if (answer.finish !== undefined && answer.finish !== "stop") {
|
|
255
|
-
throw new Error(`stream finished with ${answer.finish}`);
|
|
256
|
-
}
|
|
257
|
-
const evolved = parseEvolveAnswer(answer.text);
|
|
258
|
-
if (evolved === null) {
|
|
259
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: "evolve-parse-failed", cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
|
|
260
|
-
skipReasons.push("evolve-parse-failed");
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (evolved.verdict === "keep") {
|
|
264
|
-
deps.store.logReview({ ...base, verdict: "keep", reason: evolved.reason, cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (evolved.verdict === "quarantine") {
|
|
268
|
-
landQuarantine(deps, base, candidate, opts, evolved.reason, gateUsage, evolveRaw, proposedOf(evolved), summary);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
// ── Rewrite: magnitude cap first, dry-run second, gates last ──
|
|
272
|
-
const nextBody = evolved.body ?? candidate.body;
|
|
273
|
-
if (rewriteMagnitude(candidate.body, nextBody) > REWRITE_MAGNITUDE_CAP) {
|
|
274
|
-
landQuarantine(deps, base, candidate, opts, `幅度超限转隔离:${evolved.reason}`, gateUsage, evolveRaw, proposedOf(evolved), summary);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (opts.dryRun) {
|
|
278
|
-
deps.store.logReview({ ...base, verdict: "rewrite", reason: `试运行未落盘:${evolved.reason}`, proposed: proposedOf(evolved), cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
landRewrite(deps, base, candidate, opts, evolved, gateUsage, evolveRaw, summary);
|
|
282
|
-
}
|
|
283
|
-
catch (error) {
|
|
284
|
-
deps.store.logReview({ ...base, verdict: "skip", reason: `evolve-llm-failed: ${failureHead(error)}`, cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0 });
|
|
285
|
-
skipReasons.push("evolve-llm-failed");
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* The rewrite payload a non-landing row keeps: exactly the fields the model
|
|
290
|
-
* supplied, or `undefined` when it proposed no text at all.
|
|
291
|
-
*/
|
|
292
|
-
function proposedOf(evolved) {
|
|
293
|
-
const payload = {};
|
|
294
|
-
if (evolved.title !== undefined) {
|
|
295
|
-
payload.title = evolved.title;
|
|
296
|
-
}
|
|
297
|
-
if (evolved.description !== undefined) {
|
|
298
|
-
payload.description = evolved.description;
|
|
299
|
-
}
|
|
300
|
-
if (evolved.body !== undefined) {
|
|
301
|
-
payload.body = evolved.body;
|
|
302
|
-
}
|
|
303
|
-
if (evolved.corrects !== undefined) {
|
|
304
|
-
payload.corrects = evolved.corrects;
|
|
305
|
-
}
|
|
306
|
-
return Object.keys(payload).length === 0 ? undefined : payload;
|
|
307
|
-
}
|
|
308
|
-
function neighborHooks(store, candidate) {
|
|
309
|
-
try {
|
|
310
|
-
if (candidate.kind === "memory") {
|
|
311
|
-
const query = candidate.title.slice(0, 60);
|
|
312
|
-
const hits = query.length >= 3 ? store.search(candidate.workspaceId, query, 5) : store.list(candidate.workspaceId).slice(0, 5);
|
|
313
|
-
return hits.filter((hit) => hit.id !== candidate.id).slice(0, 4).map((hit) => ({ id: hit.id, title: hit.title, description: hit.description }));
|
|
314
|
-
}
|
|
315
|
-
const hits = store.searchSkills(candidate.workspaceId, candidate.title.slice(0, 60), 5);
|
|
316
|
-
return hits.filter((hit) => hit.id !== candidate.id).slice(0, 4).map((hit) => ({ id: hit.id, title: hit.title, description: hit.description }));
|
|
317
|
-
}
|
|
318
|
-
catch {
|
|
319
|
-
return [];
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
function landQuarantine(deps, base, candidate, opts, reason, gateUsage, evolveRaw, proposed, summary) {
|
|
323
|
-
const prior = deps.store.reviewStateOf(candidate.workspaceId, candidate.kind, candidate.id)?.evolveCount ?? 0;
|
|
324
|
-
if (opts.dryRun) {
|
|
325
|
-
deps.store.logReview({ ...base, verdict: "quarantine", reason: `试运行未隔离:${reason}`, proposed, cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: 0, evolveCount: prior });
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
const ok = deps.store.quarantine(candidate.workspaceId, candidate.kind, candidate.id);
|
|
329
|
-
deps.store.logReview({
|
|
330
|
-
...base, verdict: "quarantine", reason, cheapUsage: gateUsage, evolveUsage: evolveRaw,
|
|
331
|
-
applied: ok ? 1 : 0, evolveCount: ok ? prior + 1 : prior,
|
|
332
|
-
});
|
|
333
|
-
if (ok) {
|
|
334
|
-
summary.applied += 1;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
function landRewrite(deps, base, candidate, opts, evolved, gateUsage, evolveRaw, summary) {
|
|
338
|
-
const prior = deps.store.reviewStateOf(candidate.workspaceId, candidate.kind, candidate.id)?.evolveCount ?? 0;
|
|
339
|
-
const ctx = { workspaceId: candidate.workspaceId, trigger: "item-review", sessionId: opts.sessionId, runId: base.runId, model: opts.model, decision: evolved.reason };
|
|
340
|
-
if (candidate.kind === "memory") {
|
|
341
|
-
const [outcome] = deps.store.applyProposals(ctx, [{
|
|
342
|
-
action: "update", id: candidate.id,
|
|
343
|
-
title: evolved.title, description: evolved.description, body: evolved.body,
|
|
344
|
-
corrects: evolved.corrects,
|
|
345
|
-
}]);
|
|
346
|
-
const ok = outcome !== undefined && outcome.ok;
|
|
347
|
-
deps.store.logReview({
|
|
348
|
-
...base, verdict: "rewrite", reason: ok ? evolved.reason : `门禁拒绝:${outcome?.reason ?? "未知原因"}`,
|
|
349
|
-
cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: ok ? 1 : 0, evolveCount: ok ? prior + 1 : prior,
|
|
350
|
-
});
|
|
351
|
-
if (ok) {
|
|
352
|
-
summary.applied += 1;
|
|
353
|
-
}
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
const { outcomes } = deps.store.applySkillProposals(ctx, [{
|
|
357
|
-
action: "update", scope: candidate.scope ?? "project", id: candidate.id,
|
|
358
|
-
title: evolved.title, description: evolved.description, body: evolved.body,
|
|
359
|
-
}]);
|
|
360
|
-
const outcome = outcomes[0];
|
|
361
|
-
const ok = outcome !== undefined && outcome.ok;
|
|
362
|
-
deps.store.logReview({
|
|
363
|
-
...base, verdict: "rewrite", reason: ok ? evolved.reason : `门禁拒绝:${outcome?.reason ?? "未知原因"}`,
|
|
364
|
-
cheapUsage: gateUsage, evolveUsage: evolveRaw, applied: ok ? 1 : 0, evolveCount: ok ? prior + 1 : prior,
|
|
365
|
-
});
|
|
366
|
-
if (ok) {
|
|
367
|
-
summary.applied += 1;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
function totalOf(raw) {
|
|
371
|
-
return normalizeUsage(raw)?.total ?? 0;
|
|
372
|
-
}
|
|
373
|
-
/** One-line error head for ledger rows (fail-loud shape, capped length). */
|
|
374
|
-
export function failureHead(error, max = 120) {
|
|
375
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
376
|
-
const head = message.split("\n", 1)[0] ?? "";
|
|
377
|
-
return head.length > max ? `${head.slice(0, max)}…` : head;
|
|
378
|
-
}
|
|
379
|
-
function normalizedOf(raw) {
|
|
380
|
-
return normalizeUsage(raw) ?? zeroUsage();
|
|
381
|
-
}
|
|
382
|
-
/** One streaming text call with the host session identity forwarded. */
|
|
383
|
-
async function streamOnce(llm, opts, system, prompt, maxTokens) {
|
|
384
|
-
if (llm === undefined) {
|
|
385
|
-
throw new Error("no llm route");
|
|
386
|
-
}
|
|
387
|
-
let text = "";
|
|
388
|
-
let usage;
|
|
389
|
-
let finish;
|
|
390
|
-
for await (const chunk of llm.stream({
|
|
391
|
-
provider: opts.provider,
|
|
392
|
-
model: opts.model,
|
|
393
|
-
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
394
|
-
system,
|
|
395
|
-
maxTokens,
|
|
396
|
-
sessionId: opts.sessionId,
|
|
397
|
-
})) {
|
|
398
|
-
const piece = chunk;
|
|
399
|
-
if (piece?.type === "text-delta" && typeof piece.text === "string") {
|
|
400
|
-
text += piece.text;
|
|
401
|
-
}
|
|
402
|
-
else if (piece?.type === "usage") {
|
|
403
|
-
usage = piece.usage;
|
|
404
|
-
}
|
|
405
|
-
else if (piece?.type === "finish") {
|
|
406
|
-
finish = piece.reason;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
const finishKind = finish?.kind;
|
|
410
|
-
if (text.trim().length === 0) {
|
|
411
|
-
// Nothing usable arrived: the finish reason is the only diagnosis.
|
|
412
|
-
throw new Error(finishKind !== undefined && finishKind !== "stop" ? `stream finished with ${finishKind}` : "empty model output");
|
|
413
|
-
}
|
|
414
|
-
// The caller owns the truncation verdict: a gate answer often completes
|
|
415
|
-
// before its budget does, a replacement body never does.
|
|
416
|
-
return { text, usage, finish: finishKind };
|
|
417
|
-
}
|
|
418
101
|
/** Current UTC day `YYYY-MM-DD` for the hard-cap account. */
|
|
419
102
|
export function todayUtc() {
|
|
420
103
|
return new Date().toISOString().slice(0, 10);
|
|
421
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
|
+
}
|
|
422
110
|
/**
|
|
423
111
|
* Wire the review triggers onto the host context. Global single-flight with
|
|
424
112
|
* a minimum interval: turn boundaries arm the pending workspace, compaction
|
|
@@ -484,20 +172,21 @@ export function createReviewScheduler(host, deps, config) {
|
|
|
484
172
|
if (typeof id !== "string" || id.length === 0) {
|
|
485
173
|
return;
|
|
486
174
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
175
|
+
// A drain needs a live route. A session running on the host default
|
|
176
|
+
// model carries no explicit selection, so the default-model service
|
|
177
|
+
// fills it in; with neither, the boundary is only remembered and the
|
|
178
|
+
// next turn boundary reviews it. This is what keeps a review call
|
|
179
|
+
// from being sent with an empty provider or a dead session identity
|
|
180
|
+
// (both of which the provider answers with a bare stream error).
|
|
181
|
+
// Absent resolver (older host, tests) still reads the agent's explicit
|
|
182
|
+
// selection: the fallback is the legacy behavior, never "no route".
|
|
183
|
+
const route = deps.resolveRoute?.(agent) ?? resolveCallRoute(agent, undefined);
|
|
184
|
+
if (route === undefined) {
|
|
185
|
+
pendingWorkspace = cwdOf(agent);
|
|
494
186
|
return;
|
|
495
187
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
listeners.push(host.on("agent/disposed", () => {
|
|
499
|
-
drain(pendingWorkspace);
|
|
500
|
-
pendingWorkspace = undefined;
|
|
188
|
+
lastRoute = { provider: route.provider, model: route.model, sessionId: id };
|
|
189
|
+
drain(cwdOf(agent));
|
|
501
190
|
}));
|
|
502
191
|
const armed = config.read();
|
|
503
192
|
host.logger("continual-evolve").info(`review scheduler armed (daily cap ${armed.dailyBudget}, dry-run ${armed.dryRun ? "on" : "off"})`);
|
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
|