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/llm-call.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The transport for the plugin's own LLM calls. Every subagent call
|
|
3
|
+
* (`subagentDefs.ts` definitions loaded through `subagent.ts`) ends here.
|
|
4
|
+
*
|
|
5
|
+
* Four disciplines live in one place because all four were learned the hard
|
|
6
|
+
* way, and none of them is visible at a call site:
|
|
7
|
+
*
|
|
8
|
+
* - **The route.** `agent.options` carries an *explicit* selection only. A
|
|
9
|
+
* session running on the host's default model carries nothing there, so the
|
|
10
|
+
* route falls back to the host's `agentDefaultModel` service — the same
|
|
11
|
+
* value the host Agent loop itself runs on. A caller that still has no
|
|
12
|
+
* route refuses to send instead of streaming into a provider rejection.
|
|
13
|
+
* - **The reasoning effort.** An exact provider/model advertises the effort
|
|
14
|
+
* ids it accepts, and a request naming one it does not accept is rejected
|
|
15
|
+
* by capability validation before any provider I/O. The lowest *enabled*
|
|
16
|
+
* effort the model advertises is used; a model with no reasoning metadata
|
|
17
|
+
* gets no caller-supplied effort at all. This is the pre-teardown
|
|
18
|
+
* `llm-text.ts` discipline (see the 2026-09-24 capability note), restored
|
|
19
|
+
* after the direct calls that replaced that boundary shipped without it.
|
|
20
|
+
* - **The output budget.** A fixed budget is a guess about how much a model
|
|
21
|
+
* spends before it answers, and a wrong guess cuts the answer off as
|
|
22
|
+
* `max-tokens` — which reads in the ledger as a route fault (2026-10-13).
|
|
23
|
+
* The exact model's own advertised per-request cap wins; the definition's
|
|
24
|
+
* declared budget is only the fallback for adapters that advertise none.
|
|
25
|
+
* - **The failure.** A terminal `error`/`aborted` finish carries the host's
|
|
26
|
+
* own failure object, and the stream has usually reported its usage by
|
|
27
|
+
* then. Both now ride the thrown error: keeping only the `kind` produced a
|
|
28
|
+
* ledger full of "stream finished with error", and discarding the usage
|
|
29
|
+
* left failed calls with no model and no token account at all.
|
|
30
|
+
*/
|
|
31
|
+
/** Effort ids that mean "reasoning off"; a model may advertise only these. */
|
|
32
|
+
export const CLOSED_REASONING_EFFORTS = new Set(["disabled", "off", "none"]);
|
|
33
|
+
/**
|
|
34
|
+
* Select the lowest reasoning effort an exact model advertises.
|
|
35
|
+
*
|
|
36
|
+
* An enabled effort wins over a declared closing effort: the adapter's first
|
|
37
|
+
* enabled effort is the lowest enabled level it exposes, and a closing effort
|
|
38
|
+
* is only a fallback when the model publishes no enabled level at all. No
|
|
39
|
+
* capability list means no caller-supplied effort.
|
|
40
|
+
*
|
|
41
|
+
* @param modelInfo - Exact resolved-model metadata, when the provider exposes it.
|
|
42
|
+
* @returns The provider-owned effort id to send, or `undefined` to omit the field.
|
|
43
|
+
* @throws When the host reports reasoning metadata in a shape this plugin cannot trust.
|
|
44
|
+
*/
|
|
45
|
+
export function selectLowestReasoningEffort(modelInfo) {
|
|
46
|
+
const efforts = modelInfo?.reasoning?.efforts;
|
|
47
|
+
if (efforts === undefined) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(efforts)) {
|
|
51
|
+
throw new Error("continual-evolve: invalid model reasoning metadata");
|
|
52
|
+
}
|
|
53
|
+
const ids = [];
|
|
54
|
+
for (const effort of efforts) {
|
|
55
|
+
const id = effort?.id;
|
|
56
|
+
if (typeof id !== "string") {
|
|
57
|
+
throw new Error("continual-evolve: invalid model reasoning metadata");
|
|
58
|
+
}
|
|
59
|
+
ids.push(id);
|
|
60
|
+
}
|
|
61
|
+
return ids.find((id) => !CLOSED_REASONING_EFFORTS.has(id)) ?? ids[0];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The output budget to send for one exact route: the model's own advertised
|
|
65
|
+
* per-request cap when it has one, the caller's fallback otherwise.
|
|
66
|
+
*
|
|
67
|
+
* A budget below what the model needs is not a cost saving — it is a
|
|
68
|
+
* truncation. Deriving it from the model's capability (the same source the
|
|
69
|
+
* reasoning effort comes from) is what keeps one model's answer from being
|
|
70
|
+
* cut off at a number chosen for another model's habits.
|
|
71
|
+
*
|
|
72
|
+
* @param modelInfo - Exact resolved-model metadata, when the provider exposes it.
|
|
73
|
+
* @param fallbackMaxTokens - The definition's declared budget.
|
|
74
|
+
* @returns The budget to send.
|
|
75
|
+
* @throws When the host reports a max-tokens capability this plugin cannot trust.
|
|
76
|
+
*/
|
|
77
|
+
export function selectMaxTokens(modelInfo, fallbackMaxTokens) {
|
|
78
|
+
const advertised = modelInfo?.defaultMaxTokens;
|
|
79
|
+
if (advertised === undefined) {
|
|
80
|
+
return fallbackMaxTokens;
|
|
81
|
+
}
|
|
82
|
+
if (typeof advertised !== "number" || !Number.isInteger(advertised) || advertised <= 0) {
|
|
83
|
+
throw new Error("continual-evolve: invalid model max-tokens metadata");
|
|
84
|
+
}
|
|
85
|
+
return advertised;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* One failed call, carrying what the stream observed before it failed.
|
|
89
|
+
*
|
|
90
|
+
* The model and the tokens of a *failed* call are exactly what the ledger
|
|
91
|
+
* could not answer for (2026-10-13): the route and usage existed in memory
|
|
92
|
+
* and were dropped on the way out. They ride the error now, so every failure
|
|
93
|
+
* path can account for itself the same way a success does.
|
|
94
|
+
*/
|
|
95
|
+
export class SubagentCallError extends Error {
|
|
96
|
+
/** Usage payload observed before the failure, `undefined` when the stream never reported one. */
|
|
97
|
+
usage;
|
|
98
|
+
/** The terminal finish kind, `undefined` when the stream failed before reporting one. */
|
|
99
|
+
finish;
|
|
100
|
+
/** The route the failed call ran on. */
|
|
101
|
+
route;
|
|
102
|
+
constructor(message, observed) {
|
|
103
|
+
super(message, observed.cause === undefined ? undefined : { cause: observed.cause });
|
|
104
|
+
this.name = "SubagentCallError";
|
|
105
|
+
this.usage = observed.usage;
|
|
106
|
+
this.finish = observed.finish;
|
|
107
|
+
this.route = observed.route;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The usage a failed call had already observed, for the ledger.
|
|
112
|
+
*
|
|
113
|
+
* @param error - Whatever a call threw.
|
|
114
|
+
* @returns The observed usage payload, or `undefined` when the failure carries none.
|
|
115
|
+
*/
|
|
116
|
+
export function observedUsageOf(error) {
|
|
117
|
+
return error instanceof SubagentCallError ? error.usage : undefined;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The host's default model selection, when the host exposes that service.
|
|
121
|
+
*
|
|
122
|
+
* The service is read through `ctx.get` and never joins an inject list: a host
|
|
123
|
+
* without it (or an older one) must keep working, and a failed read is simply
|
|
124
|
+
* "no default", never a mount failure.
|
|
125
|
+
*
|
|
126
|
+
* @param get - The host context's sibling-service reader, when available.
|
|
127
|
+
* @returns The default provider/model (plus effort), or `undefined` when unknown.
|
|
128
|
+
*/
|
|
129
|
+
export function defaultRouteOf(get) {
|
|
130
|
+
if (get === undefined) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
let service;
|
|
134
|
+
try {
|
|
135
|
+
service = get("agentDefaultModel");
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// An absent service is not an error here: the caller falls back to refusing.
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const currentSelection = service?.currentSelection;
|
|
142
|
+
if (typeof currentSelection !== "function") {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
return selectionOf(currentSelection.call(service));
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// A throwing service is "no default" — the refusal path stays reachable.
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the route one direct call runs on: the agent's explicit selection
|
|
155
|
+
* first, the host default second.
|
|
156
|
+
*
|
|
157
|
+
* @param agent - The host Agent the call belongs to (its `options` may be empty).
|
|
158
|
+
* @param get - The host context's sibling-service reader, when available.
|
|
159
|
+
* @returns The route to call with, or `undefined` when there is none to use.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveCallRoute(agent, get) {
|
|
162
|
+
const options = agent?.options;
|
|
163
|
+
return selectionOf(options) ?? defaultRouteOf(get);
|
|
164
|
+
}
|
|
165
|
+
/** Narrow one candidate selection object; empty strings count as absent. */
|
|
166
|
+
function selectionOf(value) {
|
|
167
|
+
if (typeof value !== "object" || value === null) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const { provider, model, reasoningEffort } = value;
|
|
171
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
if (typeof model !== "string" || model.length === 0) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return typeof reasoningEffort === "string" && reasoningEffort.length > 0
|
|
178
|
+
? { provider, model, reasoningEffort }
|
|
179
|
+
: { provider, model };
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Stream one single-turn text call on one route.
|
|
183
|
+
*
|
|
184
|
+
* A terminal `error` or `aborted` finish always throws, carrying the host's own
|
|
185
|
+
* failure text — partial text never rescues a failed call. A `max-tokens` cut
|
|
186
|
+
* is *not* thrown here: a gate answer often completes before its budget does,
|
|
187
|
+
* so the caller owns that verdict and reads `finish`.
|
|
188
|
+
*
|
|
189
|
+
* @param llm - The LLM service; `undefined` means the host exposed no route at all.
|
|
190
|
+
* @param route - The exact provider/model to call.
|
|
191
|
+
* @param sessionId - The host Agent identity providers use for request routing.
|
|
192
|
+
* @param system - The system prompt for this call.
|
|
193
|
+
* @param prompt - The user prompt for this call.
|
|
194
|
+
* @param fallbackMaxTokens - Output budget to use when the model advertises no cap of its own.
|
|
195
|
+
* @returns The assembled text, usage, finish kind, and chosen effort.
|
|
196
|
+
* @throws On a missing LLM service, capability lookup failure, terminal provider error or abort, and empty output.
|
|
197
|
+
*/
|
|
198
|
+
export async function streamOnce(llm, route, sessionId, system, prompt, fallbackMaxTokens) {
|
|
199
|
+
if (llm === undefined) {
|
|
200
|
+
throw new Error("no llm route");
|
|
201
|
+
}
|
|
202
|
+
const dials = await resolveDials(llm, route, fallbackMaxTokens);
|
|
203
|
+
let text = "";
|
|
204
|
+
let usage;
|
|
205
|
+
let finish;
|
|
206
|
+
try {
|
|
207
|
+
for await (const chunk of llm.stream({
|
|
208
|
+
provider: route.provider,
|
|
209
|
+
model: route.model,
|
|
210
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
211
|
+
system,
|
|
212
|
+
maxTokens: dials.maxTokens,
|
|
213
|
+
sessionId,
|
|
214
|
+
...(dials.reasoningEffort === undefined ? {} : { reasoningEffort: dials.reasoningEffort }),
|
|
215
|
+
})) {
|
|
216
|
+
const piece = chunk;
|
|
217
|
+
if (piece?.type === "text-delta" && typeof piece.text === "string") {
|
|
218
|
+
text += piece.text;
|
|
219
|
+
}
|
|
220
|
+
else if (piece?.type === "usage") {
|
|
221
|
+
usage = piece.usage;
|
|
222
|
+
}
|
|
223
|
+
else if (piece?.type === "finish") {
|
|
224
|
+
finish = piece.reason;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
// A provider that dies mid-stream still reported what it spent: the
|
|
230
|
+
// observation survives the failure so the ledger can show it.
|
|
231
|
+
throw new SubagentCallError(failureHeadOf(error), { usage, finish: finish?.kind, route, cause: error });
|
|
232
|
+
}
|
|
233
|
+
const finishKind = finish?.kind;
|
|
234
|
+
if (finishKind === "error" || finishKind === "aborted") {
|
|
235
|
+
throw new SubagentCallError(failureTextOf(finishKind, finish?.failure), { usage, finish: finishKind, route });
|
|
236
|
+
}
|
|
237
|
+
if (text.trim().length === 0) {
|
|
238
|
+
// Nothing usable arrived: the finish reason is the only diagnosis.
|
|
239
|
+
throw new SubagentCallError(finishKind !== undefined && finishKind !== "stop" ? `stream finished with ${finishKind}` : "empty model output", { usage, finish: finishKind, route });
|
|
240
|
+
}
|
|
241
|
+
return { text, usage, finish: finishKind, reasoningEffort: dials.reasoningEffort };
|
|
242
|
+
}
|
|
243
|
+
/** One-line head of a mid-stream throw, flattened so the ledger row stays greppable. */
|
|
244
|
+
function failureHeadOf(error) {
|
|
245
|
+
const message = error instanceof Error && error.message.length > 0 ? error.message : String(error);
|
|
246
|
+
return message.replace(/\s+/g, " ").trim();
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Resolve both dials from one capability query.
|
|
250
|
+
*
|
|
251
|
+
* The host's capability query is the only source: an explicit selection's
|
|
252
|
+
* effort belongs to the *main* conversation and must not silently raise an
|
|
253
|
+
* auxiliary call's cost, and an explicit `maxTokens` belongs to nothing here.
|
|
254
|
+
* No query (older host, test fake) means no effort and the caller's budget.
|
|
255
|
+
*/
|
|
256
|
+
async function resolveDials(llm, route, fallbackMaxTokens) {
|
|
257
|
+
if (typeof llm.resolveModelInfo !== "function") {
|
|
258
|
+
return { reasoningEffort: undefined, maxTokens: fallbackMaxTokens };
|
|
259
|
+
}
|
|
260
|
+
const modelInfo = await llm.resolveModelInfo(route.provider, route.model);
|
|
261
|
+
return {
|
|
262
|
+
reasoningEffort: selectLowestReasoningEffort(modelInfo),
|
|
263
|
+
maxTokens: selectMaxTokens(modelInfo, fallbackMaxTokens),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/** Render a terminal failure for the ledger: host code plus host message. */
|
|
267
|
+
function failureTextOf(kind, failure) {
|
|
268
|
+
const detail = failureDetailOf(failure);
|
|
269
|
+
return detail === undefined ? `stream finished with ${kind}` : `LLM call ${kind}: ${detail}`;
|
|
270
|
+
}
|
|
271
|
+
/** Flatten the host failure object to `code: message` when either is present. */
|
|
272
|
+
function failureDetailOf(failure) {
|
|
273
|
+
if (typeof failure !== "object" || failure === null) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const { code, message } = failure;
|
|
277
|
+
const head = typeof message === "string" && message.length > 0 ? message : undefined;
|
|
278
|
+
const label = typeof code === "string" && code.length > 0 ? code : undefined;
|
|
279
|
+
if (head !== undefined && label !== undefined) {
|
|
280
|
+
return `${label}: ${head}`;
|
|
281
|
+
}
|
|
282
|
+
return head ?? label;
|
|
283
|
+
}
|
|
284
|
+
//# sourceMappingURL=llm-call.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The review queue: which stored items are due for a close look.
|
|
3
|
+
*
|
|
4
|
+
* The queue is rebuilt from the store on every drain (no cursor of its own):
|
|
5
|
+
* new items first, then corrects-linked requeues, then items untouched past
|
|
6
|
+
* the staleness window. One row whose last verdict never produced an item
|
|
7
|
+
* decision is still queued — "reviewed but nothing happened" is not progress.
|
|
8
|
+
*/
|
|
9
|
+
import type { MemoryStore } from "./store.js";
|
|
10
|
+
import type { ReviewCandidate } from "./review.js";
|
|
11
|
+
/**
|
|
12
|
+
* Collect review candidates across every workspace carrying active items:
|
|
13
|
+
* new items first (newest first), then corrects-linked requeues, then stale
|
|
14
|
+
* requeues (`staleDays`, default 30). Bounded by `limit`.
|
|
15
|
+
*
|
|
16
|
+
* @param store - The central store, sole source of the queue.
|
|
17
|
+
* @param opts - Clock override (tests), staleness window, and queue bound.
|
|
18
|
+
* @returns Candidates ordered by priority.
|
|
19
|
+
*/
|
|
20
|
+
export declare function collectCandidates(store: MemoryStore, opts?: {
|
|
21
|
+
now?: number | undefined;
|
|
22
|
+
staleDays?: number | undefined;
|
|
23
|
+
limit?: number | undefined;
|
|
24
|
+
}): ReviewCandidate[];
|
|
25
|
+
//# sourceMappingURL=review-candidates.d.ts.map
|
|
@@ -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
|