dsh-continual-evolve 0.1.0 → 0.2.0
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 +149 -19
- package/README.zh.md +64 -21
- package/lib/apply.js +2 -0
- package/lib/approval.d.ts +19 -0
- package/lib/auto.d.ts +61 -1
- package/lib/auto.js +108 -2
- package/lib/benchmark.d.ts +14 -0
- package/lib/command.js +234 -5
- package/lib/evaluate.d.ts +36 -7
- package/lib/evaluate.js +157 -43
- package/lib/fate.d.ts +126 -0
- package/lib/fate.js +338 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +18 -2
- package/lib/mount.js +5 -0
- package/lib/planner.d.ts +8 -1
- package/lib/planner.js +27 -0
- package/lib/render.js +2 -1
- package/lib/review.d.ts +1 -1
- package/lib/review.js +17 -0
- package/lib/score.d.ts +22 -4
- package/lib/score.js +48 -7
- package/lib/skill.d.ts +10 -2
- package/lib/skill.js +34 -2
- package/lib/skillquality.d.ts +81 -0
- package/lib/skillquality.js +311 -0
- package/lib/tool.js +6 -3
- package/lib/types.d.ts +31 -0
- package/lib/types.js +19 -0
- package/lib/validate.js +25 -1
- package/lib/wrapup.d.ts +210 -0
- package/lib/wrapup.js +439 -0
- package/package.json +24 -14
package/lib/wrapup.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, isArchived } from "./types.js";
|
|
3
|
+
import { extractJsonObject } from "./plan.js";
|
|
4
|
+
import { compactText } from "./render.js";
|
|
5
|
+
export function candidateKey(kind, id) {
|
|
6
|
+
return `${kind}:${id}`;
|
|
7
|
+
}
|
|
8
|
+
/** Lowercase, punctuation-stripped title used for cheap coverage matching. */
|
|
9
|
+
function normalizeKey(value) {
|
|
10
|
+
return value.toLowerCase().replace(/[^a-z0-9\u3400-\u9fff]+/g, "").trim();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Deterministic global-coverage check (STRONG signal): the global store
|
|
14
|
+
* already covers a topic when it holds a title that normalizes equal to, or
|
|
15
|
+
* (beyond a length floor) contains, the candidate's normalized title.
|
|
16
|
+
* Archived global entries count too — the topic was already judged
|
|
17
|
+
* cross-session; a local duplicate would only re-sediment it.
|
|
18
|
+
*
|
|
19
|
+
* The bare same-id case is deliberately NOT coverage: ids are slugs derived
|
|
20
|
+
* from titles, so a real collision is usually caught by the title check
|
|
21
|
+
* below. A same-id entry with a wildly different title is a weak signal — the
|
|
22
|
+
* caller routes it through {@link globalHintsFor} for the assessor to judge
|
|
23
|
+
* against the actual global title (real case: local `memory` "用户产品愿景与
|
|
24
|
+
* 收入需求(本会话)" vs global `memory` "用户画像(持续更新)").
|
|
25
|
+
*/
|
|
26
|
+
export function globalCoverageDetected(globalState, kind, entry) {
|
|
27
|
+
const records = globalState.entries[kind];
|
|
28
|
+
const title = normalizeKey(entry.title);
|
|
29
|
+
if (title.length === 0)
|
|
30
|
+
return false;
|
|
31
|
+
for (const other of Object.values(records)) {
|
|
32
|
+
const otherTitle = normalizeKey(other.title);
|
|
33
|
+
if (otherTitle.length === 0)
|
|
34
|
+
continue;
|
|
35
|
+
if (otherTitle === title)
|
|
36
|
+
return true;
|
|
37
|
+
if (title.length >= 4 && otherTitle.length >= 4 && (title.includes(otherTitle) || otherTitle.includes(title))) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The actual global entries that touch the same topic as a local candidate:
|
|
45
|
+
* same id (regardless of title — the weak collision signal that is NOT
|
|
46
|
+
* coverage on its own), equal normalized title, or title overlap. The raw ids
|
|
47
|
+
* and titles let the assessor judge enrichment against real global content
|
|
48
|
+
* (does the global copy already hold what the local one adds?) rather than a
|
|
49
|
+
* bare boolean. Bounded: a handful of best matches, never the whole store.
|
|
50
|
+
*/
|
|
51
|
+
export function globalHintsFor(globalState, kind, entry) {
|
|
52
|
+
const records = globalState.entries[kind];
|
|
53
|
+
const title = normalizeKey(entry.title);
|
|
54
|
+
const hints = [];
|
|
55
|
+
for (const other of Object.values(records)) {
|
|
56
|
+
const otherTitle = normalizeKey(other.title);
|
|
57
|
+
if (otherTitle.length === 0 && other.id !== entry.id)
|
|
58
|
+
continue;
|
|
59
|
+
const matches = other.id === entry.id ||
|
|
60
|
+
(otherTitle.length > 0 && (otherTitle === title || (title.length >= 4 && otherTitle.length >= 4 && (title.includes(otherTitle) || otherTitle.includes(title)))));
|
|
61
|
+
if (matches) {
|
|
62
|
+
hints.push({ id: other.id, title: other.title });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return hints;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The auditable local candidates of a session: every non-archived local
|
|
69
|
+
* entry that has not already been promoted (a promoted entry's lifecycle is
|
|
70
|
+
* finished — the global copy is the live one). Each carries its
|
|
71
|
+
* `coveredGlobally` flag so the assessor never wastes a promote on a topic
|
|
72
|
+
* the global store already owns.
|
|
73
|
+
*/
|
|
74
|
+
export function listLocalCandidates(state, globalState) {
|
|
75
|
+
const candidates = [];
|
|
76
|
+
for (const kind of Object.keys(state.entries)) {
|
|
77
|
+
for (const entry of Object.values(state.entries[kind])) {
|
|
78
|
+
if (entry.scope !== "local")
|
|
79
|
+
continue;
|
|
80
|
+
if (isArchived(entry))
|
|
81
|
+
continue;
|
|
82
|
+
if (typeof entry.metadata[PROMOTED_TO_KEY] === "string")
|
|
83
|
+
continue;
|
|
84
|
+
candidates.push({
|
|
85
|
+
kind,
|
|
86
|
+
id: entry.id,
|
|
87
|
+
title: entry.title,
|
|
88
|
+
content: entry.content,
|
|
89
|
+
path: entry.path,
|
|
90
|
+
version: entry.version,
|
|
91
|
+
metadata: entry.metadata,
|
|
92
|
+
coveredGlobally: globalCoverageDetected(globalState, kind, entry),
|
|
93
|
+
globalHints: globalHintsFor(globalState, kind, entry),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return candidates;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse and validate the model's assessment JSON. Defense is mechanical:
|
|
101
|
+
* keys outside the candidate list are dropped, verdicts outside the enum
|
|
102
|
+
* collapse to "keep", and candidates the model omitted default to "keep" —
|
|
103
|
+
* a malformed reply can never change an entry's fate by itself.
|
|
104
|
+
*
|
|
105
|
+
* Split promotion (verdict "archive" with a `promote` sub-object): the
|
|
106
|
+
* sub-object is accepted ONLY on archive verdicts and ONLY when both cleaned
|
|
107
|
+
* title and content are non-empty strings — a dropped/malformed sub-object
|
|
108
|
+
* silently degrades to a plain archive (the entry is never half-promoted).
|
|
109
|
+
*/
|
|
110
|
+
export function parseWrapupAssessment(text, candidates) {
|
|
111
|
+
const value = extractJsonObject(text);
|
|
112
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
113
|
+
throw new Error("wrap-up assessment JSON must be an object");
|
|
114
|
+
}
|
|
115
|
+
const record = value;
|
|
116
|
+
const allowed = new Set(candidates.map((candidate) => candidateKey(candidate.kind, candidate.id)));
|
|
117
|
+
const items = [];
|
|
118
|
+
if (Array.isArray(record["items"])) {
|
|
119
|
+
for (const raw of record["items"]) {
|
|
120
|
+
if (typeof raw !== "object" || raw === null)
|
|
121
|
+
continue;
|
|
122
|
+
const item = raw;
|
|
123
|
+
const key = typeof item["key"] === "string" ? item["key"] : "";
|
|
124
|
+
if (!allowed.has(key))
|
|
125
|
+
continue;
|
|
126
|
+
const verdict = item["verdict"] === "promote" || item["verdict"] === "archive" ? item["verdict"] : "keep";
|
|
127
|
+
const built = { key, verdict, reason: typeof item["reason"] === "string" ? item["reason"] : "" };
|
|
128
|
+
if (verdict === "archive" && typeof item["promote"] === "object" && item["promote"] !== null) {
|
|
129
|
+
const sub = item["promote"];
|
|
130
|
+
const subTitle = typeof sub["title"] === "string" ? sub["title"].trim() : "";
|
|
131
|
+
const subContent = typeof sub["content"] === "string" ? sub["content"].trim() : "";
|
|
132
|
+
if (subTitle.length > 0 && subContent.length > 0) {
|
|
133
|
+
built.promote = { title: subTitle, content: subContent };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
items.push(built);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const candidate of candidates) {
|
|
140
|
+
const key = candidateKey(candidate.kind, candidate.id);
|
|
141
|
+
if (!items.some((item) => item.key === key)) {
|
|
142
|
+
items.push({ key, verdict: "keep", reason: "not mentioned by the assessor" });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { items, rationale: typeof record["rationale"] === "string" ? record["rationale"] : "" };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Apply-time deterministic guard: re-check every promote verdict against the
|
|
149
|
+
* global store right before it lands. The LLM classification may be stale
|
|
150
|
+
* (a gate ran while assessing) or wrong; this ensures a promote never writes
|
|
151
|
+
* a duplicate global entry. Pure and unit-tested.
|
|
152
|
+
*/
|
|
153
|
+
export function filterPromotable(items, globalState, candidates) {
|
|
154
|
+
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
155
|
+
const promotable = [];
|
|
156
|
+
const skipped = [];
|
|
157
|
+
for (const item of items) {
|
|
158
|
+
if (item.verdict !== "promote")
|
|
159
|
+
continue;
|
|
160
|
+
const candidate = byKey.get(item.key);
|
|
161
|
+
if (!candidate) {
|
|
162
|
+
skipped.push({ key: item.key, reason: "not in the audited candidate list" });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (candidate.coveredGlobally || globalCoverageDetected(globalState, candidate.kind, candidate)) {
|
|
166
|
+
skipped.push({ key: item.key, reason: "already covered globally" });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
promotable.push(item);
|
|
170
|
+
}
|
|
171
|
+
return { promotable, skipped };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The symmetric archive guard. `filterPromotable` is one-directional: it
|
|
175
|
+
* stops the model from WRITING duplicate global entries, but nothing stopped
|
|
176
|
+
* an unfounded ARCHIVE from hiding content that was actually only local.
|
|
177
|
+
* Guard criteria: an archive needs user confirmation when it is NOT covered
|
|
178
|
+
* globally AND the entry carries a real distillation source (sourceSeqs /
|
|
179
|
+
* sourceSession — i.e. it was distilled from actual user messages, so it
|
|
180
|
+
* may hold reusable value). Operational/empty entries archive silently as
|
|
181
|
+
* before. Split archives (archive + promote sub-object) skip this check:
|
|
182
|
+
* their promotion already crosses a human approval gate, so the archive is
|
|
183
|
+
* the completion of an approved action, not a silent burial.
|
|
184
|
+
*/
|
|
185
|
+
export function needsArchiveReview(item, candidate) {
|
|
186
|
+
if (item.verdict !== "archive")
|
|
187
|
+
return false;
|
|
188
|
+
if (item.promote)
|
|
189
|
+
return false;
|
|
190
|
+
if (candidate.coveredGlobally)
|
|
191
|
+
return false;
|
|
192
|
+
const seqs = candidate.metadata[SOURCE_SEQS_KEY];
|
|
193
|
+
const session = candidate.metadata[SOURCE_SESSION_KEY];
|
|
194
|
+
return (Array.isArray(seqs) && seqs.length > 0) || (typeof session === "string" && session.length > 0);
|
|
195
|
+
}
|
|
196
|
+
/** Partition archive items into silent vs review-required (see needsArchiveReview). */
|
|
197
|
+
export function splitArchiveGuards(items, candidates) {
|
|
198
|
+
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
199
|
+
const silent = [];
|
|
200
|
+
const review = [];
|
|
201
|
+
for (const item of items) {
|
|
202
|
+
if (item.verdict !== "archive")
|
|
203
|
+
continue;
|
|
204
|
+
const candidate = byKey.get(item.key);
|
|
205
|
+
if (candidate && needsArchiveReview(item, candidate)) {
|
|
206
|
+
review.push(item);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
silent.push(item);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { silent, review };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Apply-time guard for a split promotion (archive + promote sub-object):
|
|
216
|
+
* the cleaned title must not duplicate a topic already covered globally. A
|
|
217
|
+
* duplicate split is dropped (the entry still archives plain) rather than
|
|
218
|
+
* half-promoting a redundancy.
|
|
219
|
+
*/
|
|
220
|
+
export function splitPromoteBlocked(item, globalState, kind) {
|
|
221
|
+
if (!item.promote)
|
|
222
|
+
return "no split payload";
|
|
223
|
+
if (globalCoverageDetected(globalState, kind, { id: "", title: item.promote.title })) {
|
|
224
|
+
return "split promotion duplicates a globally covered topic";
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Shared proposal builders for a WHOLE promotion — used by both the
|
|
230
|
+
* `/evolve wrapup` command and the gate's local-fate dimension so the two
|
|
231
|
+
* paths apply IDENTICAL edits (global create + local retirement stamp).
|
|
232
|
+
*
|
|
233
|
+
* The local stamp is a factory: the `promotedTo` id is only known after the
|
|
234
|
+
* global create lands (validation may slugify the id), so the caller applies
|
|
235
|
+
* the global proposal first and stamps the local copy with the created id.
|
|
236
|
+
*/
|
|
237
|
+
export function wholePromoteProposals(item, candidate, sessionId) {
|
|
238
|
+
const now = new Date().toISOString();
|
|
239
|
+
return {
|
|
240
|
+
global: {
|
|
241
|
+
summary: `wrapup: promote local ${item.key} to the global store`,
|
|
242
|
+
rationale: item.reason,
|
|
243
|
+
expectedOutcome: `The entry is now visible to every session via the global store (sourcedFromLocal=${sessionId}:${candidate.id}).`,
|
|
244
|
+
edits: [
|
|
245
|
+
{
|
|
246
|
+
action: "create",
|
|
247
|
+
kind: candidate.kind,
|
|
248
|
+
id: candidate.id,
|
|
249
|
+
title: candidate.title,
|
|
250
|
+
content: candidate.content,
|
|
251
|
+
path: candidate.path,
|
|
252
|
+
metadata: {
|
|
253
|
+
...candidate.metadata,
|
|
254
|
+
[SOURCED_FROM_KEY]: `${sessionId}:${candidate.id}`,
|
|
255
|
+
[PROMOTED_AT_KEY]: now,
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
localStamp: (createdId) => ({
|
|
261
|
+
summary: `wrapup: stamp local ${item.key} as promoted to ${createdId} and retire it from injection`,
|
|
262
|
+
rationale: item.reason,
|
|
263
|
+
expectedOutcome: `The local copy keeps its data but stops being injected; the global copy is the live one.`,
|
|
264
|
+
edits: [
|
|
265
|
+
{
|
|
266
|
+
action: "update",
|
|
267
|
+
kind: candidate.kind,
|
|
268
|
+
id: candidate.id,
|
|
269
|
+
title: candidate.title,
|
|
270
|
+
content: candidate.content,
|
|
271
|
+
metadata: {
|
|
272
|
+
...candidate.metadata,
|
|
273
|
+
[PROMOTED_TO_KEY]: createdId,
|
|
274
|
+
[PROMOTED_AT_KEY]: now,
|
|
275
|
+
[ARCHIVED_AT_KEY]: now,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Shared proposal builders for a SPLIT promotion (A-form): archive a mixed
|
|
284
|
+
* local entry but promote ONLY the cleaned durable part the model extracted.
|
|
285
|
+
* Same usage contract as {@link wholePromoteProposals}: apply the global
|
|
286
|
+
* create, then stamp the original local entry with the created id.
|
|
287
|
+
*/
|
|
288
|
+
export function splitPromoteProposals(item, candidate, sessionId) {
|
|
289
|
+
if (!item.promote)
|
|
290
|
+
throw new Error("split promote proposals require a promote payload");
|
|
291
|
+
const now = new Date().toISOString();
|
|
292
|
+
return {
|
|
293
|
+
global: {
|
|
294
|
+
summary: `wrapup: split — promote cleaned part of ${item.key} to the global store`,
|
|
295
|
+
rationale: item.reason,
|
|
296
|
+
expectedOutcome: `Only the durable part becomes visible globally; the snapshot half stays archived with the original.`,
|
|
297
|
+
edits: [
|
|
298
|
+
{
|
|
299
|
+
action: "create",
|
|
300
|
+
kind: candidate.kind,
|
|
301
|
+
id: candidate.id,
|
|
302
|
+
title: item.promote.title,
|
|
303
|
+
content: item.promote.content,
|
|
304
|
+
path: candidate.path,
|
|
305
|
+
metadata: {
|
|
306
|
+
...candidate.metadata,
|
|
307
|
+
[SOURCED_FROM_KEY]: `${sessionId}:${candidate.id}`,
|
|
308
|
+
[PROMOTED_AT_KEY]: now,
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
localStamp: (createdId) => ({
|
|
314
|
+
summary: `wrapup: split — archive original ${item.key}, stamped as promoted to ${createdId}`,
|
|
315
|
+
rationale: item.reason,
|
|
316
|
+
expectedOutcome: `The original leaves injection (data kept, restorable); the cleaned global copy is the live one.`,
|
|
317
|
+
edits: [
|
|
318
|
+
{
|
|
319
|
+
action: "update",
|
|
320
|
+
kind: candidate.kind,
|
|
321
|
+
id: candidate.id,
|
|
322
|
+
title: candidate.title,
|
|
323
|
+
content: candidate.content,
|
|
324
|
+
metadata: {
|
|
325
|
+
...candidate.metadata,
|
|
326
|
+
[PROMOTED_TO_KEY]: createdId,
|
|
327
|
+
[PROMOTED_AT_KEY]: now,
|
|
328
|
+
[ARCHIVED_AT_KEY]: now,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
}),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
export const WRAPUP_ASSESS_SYSTEM_PROMPT = `You are the /evolve session wrap-up assessor.
|
|
336
|
+
|
|
337
|
+
A session is ending and its local harness entries need a fate. Classify each
|
|
338
|
+
listed entry exactly once:
|
|
339
|
+
|
|
340
|
+
- "promote" — the content is a stable, durable, CROSS-SESSION reusable lesson:
|
|
341
|
+
a durable user preference, a project-level fact or convention, a reusable
|
|
342
|
+
procedure or skill. Future sessions would benefit from seeing it.
|
|
343
|
+
- "archive" — the content is session-specific task progress, one-off noise,
|
|
344
|
+
superseded or obsolete, or already covered by the global store (note
|
|
345
|
+
"covered globally" in the reason).
|
|
346
|
+
- "keep" — still actively useful to this session, or genuinely uncertain.
|
|
347
|
+
|
|
348
|
+
Rules:
|
|
349
|
+
- When an entry is marked "covered globally" in the listing, prefer "archive"
|
|
350
|
+
or "keep" over "promote" — promoting a duplicate gains nothing.
|
|
351
|
+
- Do not promote local task state, work-in-progress notes, or content tied to
|
|
352
|
+
one session's ephemeral details.
|
|
353
|
+
- Skills: only "promote" a skill entry that is a genuinely reusable procedure
|
|
354
|
+
meeting the DSH skill quality standard; one-off workflows are "archive" or
|
|
355
|
+
"keep".
|
|
356
|
+
- SPLIT PROMOTION: when an entry mixes a stable, cross-session-reusable part
|
|
357
|
+
WITH session-specific snapshot details, do NOT promote it whole. Instead
|
|
358
|
+
give verdict "archive" WITH a "promote" sub-object holding a CLEANED
|
|
359
|
+
version of only the durable part (a stable title + the persistent facts,
|
|
360
|
+
stripped of dates/states/one-off figures). Ephemeral snapshot content stays
|
|
361
|
+
out of the sub-object — it is left behind in the archive. A sub-object is
|
|
362
|
+
only meaningful on "archive" verdicts.
|
|
363
|
+
|
|
364
|
+
Return JSON only:
|
|
365
|
+
{
|
|
366
|
+
"rationale": "one or two sentences",
|
|
367
|
+
"items": [
|
|
368
|
+
{"key": "memory:foo", "verdict": "promote|archive|keep", "reason": "why"},
|
|
369
|
+
{"key": "memory:bar", "verdict": "archive", "reason": "why",
|
|
370
|
+
"promote": {"title": "cleaned stable title", "content": "cleaned durable part only"}}
|
|
371
|
+
]
|
|
372
|
+
}
|
|
373
|
+
Only keys from the provided list are allowed; any entry you omit defaults to "keep".`;
|
|
374
|
+
/**
|
|
375
|
+
* Ask the model to classify the audited local candidates. Routes through the
|
|
376
|
+
* calling agent's own provider/model (same model the session runs on), with
|
|
377
|
+
* reasoning disabled so the output budget goes to the JSON verdicts.
|
|
378
|
+
*/
|
|
379
|
+
export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
|
|
380
|
+
if (candidates.length === 0) {
|
|
381
|
+
return { items: [], rationale: "No local candidates to assess." };
|
|
382
|
+
}
|
|
383
|
+
if (!agent.options.provider || !agent.options.model) {
|
|
384
|
+
throw new Error("evolve: no provider/model route for the wrap-up assessor");
|
|
385
|
+
}
|
|
386
|
+
const candidateText = candidates
|
|
387
|
+
.map((candidate) => {
|
|
388
|
+
const key = candidateKey(candidate.kind, candidate.id);
|
|
389
|
+
const covered = candidate.coveredGlobally ? " (covered globally)" : "";
|
|
390
|
+
const hints = candidate.globalHints.length > 0
|
|
391
|
+
? ` | global≈${candidate.globalHints.map((hint) => hint.id + ":" + hint.title).join(", ")}`
|
|
392
|
+
: "";
|
|
393
|
+
return `- ${key} [${candidate.path}, v${candidate.version}] "${candidate.title}"${covered}${hints}: ${compactText(candidate.content, 220)}`;
|
|
394
|
+
})
|
|
395
|
+
.join("\n");
|
|
396
|
+
const userPrompt = [
|
|
397
|
+
`A local session is wrapping up. Classify each entry below for its fate.`,
|
|
398
|
+
`<local_entries>\n${candidateText}\n</local_entries>`,
|
|
399
|
+
"Return only JSON. Every item must reference one of the keys above.",
|
|
400
|
+
].join("\n\n");
|
|
401
|
+
const assembler = new BlockAssembler();
|
|
402
|
+
for await (const chunk of ctx.llm.stream({
|
|
403
|
+
provider: agent.options.provider,
|
|
404
|
+
model: agent.options.model,
|
|
405
|
+
system: WRAPUP_ASSESS_SYSTEM_PROMPT,
|
|
406
|
+
messages: [
|
|
407
|
+
createUserMessage({
|
|
408
|
+
content: [{ type: "text", text: userPrompt }],
|
|
409
|
+
source: { kind: "plugin", plugin: "dsh-continual-evolve" },
|
|
410
|
+
}),
|
|
411
|
+
],
|
|
412
|
+
// Force non-reasoning output so the budget lands in the JSON verdicts.
|
|
413
|
+
reasoningEffort: ReasoningEffortId("off"),
|
|
414
|
+
maxTokens: options.maxOutputTokens ?? 4096,
|
|
415
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
416
|
+
})) {
|
|
417
|
+
assembler.push(chunk);
|
|
418
|
+
}
|
|
419
|
+
const finish = assembler.finish;
|
|
420
|
+
if (finish.kind === "error") {
|
|
421
|
+
throw new Error(`evolve: wrap-up assessor call failed: ${finish.failure?.message ?? "unknown"}`);
|
|
422
|
+
}
|
|
423
|
+
if (finish.kind === "aborted") {
|
|
424
|
+
throw new Error("evolve: wrap-up assessor call aborted");
|
|
425
|
+
}
|
|
426
|
+
if (finish.kind === "max-tokens") {
|
|
427
|
+
throw new Error("evolve: wrap-up assessor output budget exhausted (max-tokens)");
|
|
428
|
+
}
|
|
429
|
+
const text = assembler
|
|
430
|
+
.blocks()
|
|
431
|
+
.filter((block) => block.type === "text")
|
|
432
|
+
.map((block) => block.text)
|
|
433
|
+
.join("\n");
|
|
434
|
+
if (text.length === 0) {
|
|
435
|
+
throw new Error("evolve: wrap-up assessor produced no text");
|
|
436
|
+
}
|
|
437
|
+
return parseWrapupAssessment(text, candidates);
|
|
438
|
+
}
|
|
439
|
+
//# sourceMappingURL=wrapup.js.map
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/ZK-Andy/dsh-continual-evolve.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/ZK-Andy/dsh-continual-evolve",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/ZK-Andy/dsh-continual-evolve/issues"
|
|
13
|
+
},
|
|
6
14
|
"type": "module",
|
|
7
15
|
"main": "lib/index.js",
|
|
8
16
|
"types": "lib/index.d.ts",
|
|
@@ -32,9 +40,20 @@
|
|
|
32
40
|
"engines": {
|
|
33
41
|
"node": "^22.19.0 || >=24.0.0"
|
|
34
42
|
},
|
|
43
|
+
"packageManager": "pnpm@11.7.0",
|
|
35
44
|
"publishConfig": {
|
|
36
45
|
"access": "public"
|
|
37
46
|
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc -p tsconfig.json",
|
|
49
|
+
"prepare": "tsc -p tsconfig.json",
|
|
50
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:watch": "vitest",
|
|
54
|
+
"lint": "oxlint src test",
|
|
55
|
+
"clean": "rm -rf lib"
|
|
56
|
+
},
|
|
38
57
|
"peerDependencies": {
|
|
39
58
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
59
|
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
|
|
@@ -43,25 +62,16 @@
|
|
|
43
62
|
},
|
|
44
63
|
"devDependencies": {
|
|
45
64
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
46
|
-
"@deepseek-ai/dsh-home-paths": "0.1.0-rc.6",
|
|
47
|
-
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
48
|
-
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
49
65
|
"@deepseek-ai/dsh-agent": "0.1.0-rc.6",
|
|
50
66
|
"@deepseek-ai/dsh-commands": "0.1.0-rc.6",
|
|
67
|
+
"@deepseek-ai/dsh-home-paths": "0.1.0-rc.6",
|
|
68
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
51
69
|
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6",
|
|
70
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
52
71
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
53
72
|
"@types/node": "^22.10.0",
|
|
54
73
|
"oxlint": "^0.16.0",
|
|
55
74
|
"typescript": "^5.9.0",
|
|
56
75
|
"vitest": "^3.2.0"
|
|
57
|
-
},
|
|
58
|
-
"scripts": {
|
|
59
|
-
"build": "tsc -p tsconfig.json",
|
|
60
|
-
"dev": "tsc -p tsconfig.json --watch",
|
|
61
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
62
|
-
"test": "vitest run",
|
|
63
|
-
"test:watch": "vitest",
|
|
64
|
-
"lint": "oxlint src test",
|
|
65
|
-
"clean": "rm -rf lib"
|
|
66
76
|
}
|
|
67
|
-
}
|
|
77
|
+
}
|