@lmzhen/dsh-evolution-review 0.1.0-rc.9 → 0.2.0-rc.1
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/lib/index.js +274 -71
- package/lib/types/index.d.ts +33 -1
- package/package.json +20 -18
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import {
|
|
4
|
-
import { PROMPT_BUNDLE, advanceReview, foldTurn, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
|
|
3
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_USER_CHAR_LIMIT, PROMPT_BUNDLE, SkillLibrary, advanceReview, evolutionIoAdapter, foldTurn, resolveOrigins, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
|
|
5
5
|
import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
|
|
6
6
|
//#region lib/types/redact.js
|
|
7
7
|
/**
|
|
@@ -42,28 +42,34 @@ const inject = ["agents", "tools"];
|
|
|
42
42
|
const Config = z.object({
|
|
43
43
|
reviewEnabled: z.boolean().default(true),
|
|
44
44
|
reviewMode: z.string().default("subagent"),
|
|
45
|
-
memoryInterval: z.number().default(
|
|
46
|
-
skillInterval: z.number().default(
|
|
47
|
-
reviewToolAllow: z.array(z.string()).default([
|
|
48
|
-
"skill",
|
|
49
|
-
"skill_search",
|
|
50
|
-
"skill_load"
|
|
51
|
-
]),
|
|
45
|
+
memoryInterval: z.number().default(DEFAULT_REVIEW_MEMORY_INTERVAL),
|
|
46
|
+
skillInterval: z.number().default(DEFAULT_REVIEW_SKILL_INTERVAL),
|
|
47
|
+
reviewToolAllow: z.array(z.string()).default(["skill"]),
|
|
52
48
|
reviewTimeoutMs: z.number().default(12e4),
|
|
53
49
|
executionTimeoutMs: z.number().default(3e4),
|
|
54
50
|
reviewContextMessages: z.number().default(60),
|
|
55
51
|
reviewMessageChars: z.number().default(2e3),
|
|
56
52
|
reviewMaxDepth: z.number().default(0),
|
|
57
|
-
reviewProvider: z.string()
|
|
53
|
+
reviewProvider: z.string(),
|
|
54
|
+
skillReviewTrigger: z.string().default(DEFAULT_SKILL_REVIEW_TRIGGER),
|
|
55
|
+
skillReviewCompletionMinToolCalls: z.number().default(DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS)
|
|
58
56
|
});
|
|
59
57
|
function apply(ctx, rawConfig) {
|
|
60
58
|
if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
|
|
61
59
|
const config = rawConfig;
|
|
62
60
|
const turnStarts = /* @__PURE__ */ new Map();
|
|
61
|
+
const cumulativeToolCalls = /* @__PURE__ */ new Map();
|
|
62
|
+
const completionInjected = /* @__PURE__ */ new Set();
|
|
63
63
|
const policy = () => ctx.get("evolutionPolicy")?.get();
|
|
64
64
|
ctx.on("session/event", (session, event) => {
|
|
65
65
|
if (event.type === "turn/start") turnStarts.set(session.id, session.seq - 1);
|
|
66
66
|
if (event.type !== "turn/end") return;
|
|
67
|
+
if (turnStarts.size >= COUNTER_SWEEP_THRESHOLD || cumulativeToolCalls.size >= COUNTER_SWEEP_THRESHOLD || completionInjected.size >= COUNTER_SWEEP_THRESHOLD) {
|
|
68
|
+
const isAlive = (id) => ctx.agents.get(id) !== void 0;
|
|
69
|
+
sweepDeadSessionEntries(turnStarts, isAlive);
|
|
70
|
+
sweepDeadSessionEntries(cumulativeToolCalls, isAlive);
|
|
71
|
+
sweepDeadSessionEntries(completionInjected, isAlive);
|
|
72
|
+
}
|
|
67
73
|
onTurnEnd(session, event);
|
|
68
74
|
});
|
|
69
75
|
async function onTurnEnd(session, event) {
|
|
@@ -88,17 +94,52 @@ function apply(ctx, rawConfig) {
|
|
|
88
94
|
substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? 500
|
|
89
95
|
});
|
|
90
96
|
await stateService?.saveReviewState(session.id, state);
|
|
91
|
-
|
|
92
|
-
|
|
97
|
+
const cumulative = (cumulativeToolCalls.get(session.id) ?? 0) + signal.toolCalls;
|
|
98
|
+
cumulativeToolCalls.set(session.id, cumulative);
|
|
99
|
+
if (kind) {
|
|
100
|
+
ctx.emit("evolution/review-scheduled", {
|
|
101
|
+
sessionId: session.id,
|
|
102
|
+
kind,
|
|
103
|
+
toolCalls: signal.toolCalls,
|
|
104
|
+
userChars: signal.userChars,
|
|
105
|
+
assistantChars: signal.assistantChars
|
|
106
|
+
});
|
|
107
|
+
if (!await trySubagentReview(session, agent, kind, signal)) agent.inject(createUserMessage({
|
|
108
|
+
content: [{
|
|
109
|
+
type: "text",
|
|
110
|
+
text: reviewPrompt(kind)
|
|
111
|
+
}],
|
|
112
|
+
source: {
|
|
113
|
+
kind: "plugin",
|
|
114
|
+
plugin: "dsh-evolution-review",
|
|
115
|
+
form: "notice",
|
|
116
|
+
summary: "auto-review"
|
|
117
|
+
}
|
|
118
|
+
}));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const trigger = config.skillReviewTrigger;
|
|
122
|
+
if (trigger !== "completion" && trigger !== "both") return;
|
|
123
|
+
if (completionInjected.has(session.id)) return;
|
|
124
|
+
if (!shouldCompletionReview(event.data.reason, cumulative, config.skillReviewCompletionMinToolCalls)) return;
|
|
125
|
+
completionInjected.add(session.id);
|
|
126
|
+
ctx.emit("evolution/review-scheduled", {
|
|
127
|
+
sessionId: session.id,
|
|
128
|
+
kind: "skill",
|
|
129
|
+
toolCalls: signal.toolCalls,
|
|
130
|
+
userChars: signal.userChars,
|
|
131
|
+
assistantChars: signal.assistantChars
|
|
132
|
+
});
|
|
133
|
+
agent.inject(createUserMessage({
|
|
93
134
|
content: [{
|
|
94
135
|
type: "text",
|
|
95
|
-
text:
|
|
136
|
+
text: COMPLETION_SKILL_REVIEW_PROMPT
|
|
96
137
|
}],
|
|
97
138
|
source: {
|
|
98
139
|
kind: "plugin",
|
|
99
140
|
plugin: "dsh-evolution-review",
|
|
100
141
|
form: "notice",
|
|
101
|
-
summary: "
|
|
142
|
+
summary: "completion review"
|
|
102
143
|
}
|
|
103
144
|
}));
|
|
104
145
|
}
|
|
@@ -122,7 +163,7 @@ function apply(ctx, rawConfig) {
|
|
|
122
163
|
signal: AbortSignal.timeout(config.reviewTimeoutMs),
|
|
123
164
|
maxDepth: config.reviewMaxDepth,
|
|
124
165
|
agentOptions,
|
|
125
|
-
persona: reviewPrompt(kind),
|
|
166
|
+
persona: reviewPrompt(kind, "plan"),
|
|
126
167
|
toolFilter: { allow: [...config.reviewToolAllow] },
|
|
127
168
|
outputSchema: {
|
|
128
169
|
type: "object",
|
|
@@ -140,51 +181,64 @@ function apply(ctx, rawConfig) {
|
|
|
140
181
|
}
|
|
141
182
|
}
|
|
142
183
|
});
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
try {
|
|
185
|
+
const result = await run.result;
|
|
186
|
+
if (!result.structured) return true;
|
|
187
|
+
const childReads = run.localAgent ? collectReadSkillNames(run.localAgent.session) : /* @__PURE__ */ new Set();
|
|
188
|
+
const snapshot = policy();
|
|
189
|
+
const plan = result.structured;
|
|
190
|
+
const policyFingerprint = fingerprintPolicy(snapshot);
|
|
191
|
+
const validation = validateEvolutionPlan(plan, {
|
|
192
|
+
sessionSeq: session.seq - 1,
|
|
193
|
+
maxOpsPerPlan: snapshot?.maxOpsPerPlan ?? DEFAULT_MAX_OPS_PER_PLAN,
|
|
194
|
+
protectedSkillNames: new Set(snapshot?.protectedSkillNames ?? []),
|
|
195
|
+
maxMemoryChars: snapshot?.memoryChars ?? DEFAULT_MEMORY_CHAR_LIMIT,
|
|
196
|
+
maxUserChars: snapshot?.userChars ?? DEFAULT_USER_CHAR_LIMIT,
|
|
197
|
+
maxSkillContentChars: snapshot?.skillContentChars ?? DEFAULT_SKILL_CONTENT_CHARS
|
|
198
|
+
});
|
|
199
|
+
const acceptedSkillOps = validation.accepted.skillOps ?? [];
|
|
200
|
+
const skippedUnread = filterUnreadSkillOps(acceptedSkillOps, new Set([...collectReadSkillNames(session), ...childReads]));
|
|
201
|
+
const actions = await executePlan(validation.accepted);
|
|
202
|
+
const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
|
|
203
|
+
ctx.emit("evolution/plan-applied", {
|
|
204
|
+
sessionId: session.id,
|
|
205
|
+
planId: randomUUID(),
|
|
206
|
+
policyFingerprint,
|
|
207
|
+
memoryApplied: actions.filter((action) => action.startsWith("Memory")).length,
|
|
208
|
+
skillApplied: actions.filter((action) => action.startsWith("Skill ")).length,
|
|
209
|
+
rejectedOps: validation.rejected.length + skippedUnread,
|
|
210
|
+
evidenceQuotes,
|
|
211
|
+
estimatedInputChars: reviewText.length
|
|
212
|
+
});
|
|
213
|
+
if (actions.length > 0) agent.inject(createUserMessage({
|
|
214
|
+
content: [{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: `💾 Self-improvement review: ${actions.join(" · ")}`
|
|
217
|
+
}],
|
|
218
|
+
source: {
|
|
219
|
+
kind: "plugin",
|
|
220
|
+
plugin: "dsh-evolution-review",
|
|
221
|
+
form: "notice",
|
|
222
|
+
summary: "self-improvement review"
|
|
223
|
+
}
|
|
224
|
+
}));
|
|
225
|
+
return true;
|
|
226
|
+
} finally {
|
|
227
|
+
try {
|
|
228
|
+
await run.dispose();
|
|
229
|
+
} catch (disposeError) {
|
|
230
|
+
ctx.logger.warn(`dsh-evolution-review: subagent dispose failed: ${disposeError instanceof Error ? disposeError.message : String(disposeError)}`);
|
|
178
231
|
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
ctx.logger.warn(`dsh-evolution-review: subagent review failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
182
235
|
return false;
|
|
183
236
|
}
|
|
184
237
|
}
|
|
185
|
-
async function executePlan(plan
|
|
238
|
+
async function executePlan(plan) {
|
|
186
239
|
const memory = ctx.get("memory");
|
|
187
240
|
const approval = ctx.get("evolutionApproval");
|
|
241
|
+
const origins = resolveOrigins(void 0, true);
|
|
188
242
|
const actions = [];
|
|
189
243
|
for (const op of plan.memoryOps ?? []) {
|
|
190
244
|
if (!Array.isArray(op.evidence) || op.evidence.length === 0) continue;
|
|
@@ -202,46 +256,177 @@ function apply(ctx, rawConfig) {
|
|
|
202
256
|
...op,
|
|
203
257
|
evidence: op.evidence
|
|
204
258
|
};
|
|
205
|
-
|
|
259
|
+
const runnerArgs = {
|
|
206
260
|
operation: args,
|
|
207
|
-
origin:
|
|
208
|
-
}
|
|
261
|
+
origin: origins.library
|
|
262
|
+
};
|
|
263
|
+
if ((approval ? await runApproved("skill", `skill ${op.action ?? "patch"} ${op.name}`, runnerArgs, runnerArgs) : await executeSkillDirect(args))?.ok) actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
|
|
209
264
|
}
|
|
210
265
|
return actions;
|
|
211
266
|
async function runApproved(kind, summary, stored, runnerArgs) {
|
|
212
267
|
if (!approval) return void 0;
|
|
268
|
+
if (approval.isEnabled === true && !approval.hasRunner(kind)) {
|
|
269
|
+
ctx.logger.warn(`dsh-evolution-review: approval enabled but no replay runner registered for kind "${kind}" - skipping write (${summary})`);
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
message: `Approval is enabled but no replay runner is registered for kind "${kind}"; write skipped (mount the tool that provides it, or disable approval).`
|
|
273
|
+
};
|
|
274
|
+
}
|
|
213
275
|
const decision = await approval.request({
|
|
214
276
|
kind,
|
|
215
277
|
summary,
|
|
216
278
|
args: stored,
|
|
217
|
-
origin:
|
|
279
|
+
origin: origins.approval
|
|
218
280
|
});
|
|
219
281
|
if (decision.action === "staged") return {
|
|
220
282
|
ok: false,
|
|
221
283
|
message: decision.message
|
|
222
284
|
};
|
|
285
|
+
if (approval.isEnabled === false) return await runnerDirect(kind, runnerArgs);
|
|
223
286
|
return await approval.run(kind, runnerArgs);
|
|
224
287
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
288
|
+
/** Direct execution for the approval-disabled case (parallel to executeSkillDirect). */
|
|
289
|
+
async function runnerDirect(kind, args) {
|
|
290
|
+
if (kind === "memory") {
|
|
291
|
+
const memory = ctx.get("memory");
|
|
292
|
+
const op = args;
|
|
293
|
+
if (!memory?.applyBatch) return void 0;
|
|
294
|
+
return await memory.applyBatch(op.target === "user" ? "user" : "memory", [{
|
|
295
|
+
action: op.action ?? "add",
|
|
296
|
+
facts: op.facts ?? op.content,
|
|
297
|
+
old_text: op.old_text
|
|
298
|
+
}]);
|
|
299
|
+
}
|
|
300
|
+
const wrapped = args ?? {};
|
|
301
|
+
if (!wrapped.operation) return void 0;
|
|
302
|
+
return await executeSkillDirect(wrapped.operation);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Approval-disabled path: execute the skill op through SkillLibrary with an
|
|
306
|
+
* EXPLICIT background_review origin. Going through ctx.tools.execute would
|
|
307
|
+
* make tool-skill-manage infer origin from the parent agent's header
|
|
308
|
+
* (not 'subagent'), silently escaping the .hermes-managed marker and the
|
|
309
|
+
* pinned write guard.
|
|
310
|
+
*/
|
|
311
|
+
async function executeSkillDirect(skillArgs) {
|
|
312
|
+
const io = ctx.get("evolutionIo");
|
|
313
|
+
if (!io) return {
|
|
233
314
|
ok: false,
|
|
234
|
-
message: "
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
315
|
+
message: "evolution-io service not mounted"
|
|
316
|
+
};
|
|
317
|
+
const library = new SkillLibrary(void 0, evolutionIoAdapter(() => io.provider()), void 0, (event) => {
|
|
318
|
+
ctx.emit("evolution/skill-mutated", event);
|
|
319
|
+
});
|
|
320
|
+
const op = skillArgs;
|
|
321
|
+
const name = op.name ?? "";
|
|
322
|
+
const origin = origins.library;
|
|
323
|
+
if (op.action === "create") {
|
|
324
|
+
const created = await library.create(name, op.content ?? "", origin);
|
|
325
|
+
if (created.ok) await ctx.get("skillUsage")?.markAgentCreated?.(name);
|
|
326
|
+
return created;
|
|
327
|
+
}
|
|
328
|
+
if (op.action === "edit" || op.action === "update") return await library.update(name, op.content ?? "", origin);
|
|
329
|
+
if (op.action === "patch") return await library.patch(name, op.old_string ?? "", op.new_string ?? "", op.file_path ?? "", false, origin);
|
|
330
|
+
if (op.action === "delete") {
|
|
331
|
+
const into = (op.absorbed_into ?? "").trim();
|
|
332
|
+
if (!into || !await library.read(into)) return {
|
|
333
|
+
ok: false,
|
|
334
|
+
message: "delete requires an existing absorbed_into target"
|
|
335
|
+
};
|
|
336
|
+
const archived = await library.archive(name, { absorbedInto: into });
|
|
337
|
+
if (archived.ok) await ctx.get("skillUsage")?.markArchived?.(name);
|
|
338
|
+
return archived;
|
|
339
|
+
}
|
|
340
|
+
if (op.action === "write_file") return await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? op.content ?? "", origin);
|
|
341
|
+
if (op.action === "remove_file") return await library.removeSupportFile(name, op.file_path ?? "", origin);
|
|
342
|
+
if (op.action === "restructure") {
|
|
343
|
+
const moves = (op.restructure ?? []).filter((move) => move !== null).map((move) => ({
|
|
344
|
+
heading: move.heading ?? "",
|
|
345
|
+
toFile: move.to_file ?? ""
|
|
346
|
+
}));
|
|
347
|
+
return await library.restructure(name, moves, origin);
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
message: `Unknown skill action "${op.action ?? ""}"`
|
|
238
352
|
};
|
|
239
353
|
}
|
|
240
354
|
}
|
|
241
355
|
ctx.effect(() => () => {
|
|
242
356
|
turnStarts.clear();
|
|
357
|
+
cumulativeToolCalls.clear();
|
|
358
|
+
completionInjected.clear();
|
|
243
359
|
}, "dsh-evolution-review.cleanup");
|
|
244
360
|
}
|
|
361
|
+
/** Completion-channel decision: task finished normally AND the session is proven long. */
|
|
362
|
+
function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
|
|
363
|
+
return reason?.kind === "completed" && sessionToolCalls >= minToolCalls;
|
|
364
|
+
}
|
|
365
|
+
/** Skill names this session loaded (read-before-write source for the background review). */
|
|
366
|
+
function collectReadSkillNames(session) {
|
|
367
|
+
const names = /* @__PURE__ */ new Set();
|
|
368
|
+
for (const event of session.events) {
|
|
369
|
+
if (event.type !== "tool/call") continue;
|
|
370
|
+
if (event.data.name !== "skill" && event.data.name !== "skill_load") continue;
|
|
371
|
+
const raw = event.data.arguments;
|
|
372
|
+
let parsed = {};
|
|
373
|
+
if (typeof raw === "string") try {
|
|
374
|
+
parsed = JSON.parse(raw);
|
|
375
|
+
} catch {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
else parsed = raw ?? {};
|
|
379
|
+
const name = typeof parsed.name === "string" ? parsed.name : typeof parsed.skill === "string" ? parsed.skill : "";
|
|
380
|
+
if (name) names.add(name);
|
|
381
|
+
}
|
|
382
|
+
return names;
|
|
383
|
+
}
|
|
384
|
+
/** Map/set size that triggers a dead-session counter sweep (bounded, not a hard cap). */
|
|
385
|
+
const COUNTER_SWEEP_THRESHOLD = 128;
|
|
386
|
+
/**
|
|
387
|
+
* Remove every entry whose session is no longer live (rc.42 audit P1-10):
|
|
388
|
+
* `turnStarts` / `cumulativeToolCalls` / `completionInjected` are keyed by
|
|
389
|
+
* SessionId with no platform session-end hook to prune against, so they grew
|
|
390
|
+
* unbounded over a long-lived host. Works for maps and sets; returns the
|
|
391
|
+
* number of removed entries.
|
|
392
|
+
*/
|
|
393
|
+
function sweepDeadSessionEntries(entries, isAlive) {
|
|
394
|
+
let removed = 0;
|
|
395
|
+
for (const id of [...entries.keys()]) if (!isAlive(id)) {
|
|
396
|
+
entries.delete(id);
|
|
397
|
+
removed += 1;
|
|
398
|
+
}
|
|
399
|
+
return removed;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Drop mutating ops whose target was not read this session, in place.
|
|
403
|
+
* Create is exempt (no read required to author a new skill). Covers the same
|
|
404
|
+
* mutating surface Hermes guards (edit/patch/write_file/remove_file), so a
|
|
405
|
+
* background review cannot blind-touch support files or edits of skills it
|
|
406
|
+
* never loaded. Returns the count of dropped ops so the plan event can report
|
|
407
|
+
* them as rejected.
|
|
408
|
+
*/
|
|
409
|
+
function filterUnreadSkillOps(ops, readNames) {
|
|
410
|
+
const READ_REQUIRED = [
|
|
411
|
+
"edit",
|
|
412
|
+
"update",
|
|
413
|
+
"patch",
|
|
414
|
+
"delete",
|
|
415
|
+
"write_file",
|
|
416
|
+
"remove_file",
|
|
417
|
+
"restructure"
|
|
418
|
+
];
|
|
419
|
+
let dropped = 0;
|
|
420
|
+
for (let index = ops.length - 1; index >= 0; index -= 1) {
|
|
421
|
+
const op = ops[index];
|
|
422
|
+
if (!op) continue;
|
|
423
|
+
if (op.action !== void 0 && READ_REQUIRED.includes(op.action) && op.name && !readNames.has(op.name)) {
|
|
424
|
+
ops.splice(index, 1);
|
|
425
|
+
dropped += 1;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return dropped;
|
|
429
|
+
}
|
|
245
430
|
function fingerprintPolicy(snapshot) {
|
|
246
431
|
try {
|
|
247
432
|
return createHash("sha256").update(JSON.stringify(snapshot)).digest("hex").slice(0, 12);
|
|
@@ -256,13 +441,31 @@ function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars)
|
|
|
256
441
|
const text = message.content.map((block) => block.type === "text" ? block.text : "").join(" ").trim();
|
|
257
442
|
if (text) messages.push(`${message.role.toUpperCase()}: ${text.slice(0, maxMessageChars)}`);
|
|
258
443
|
}
|
|
444
|
+
const toolLines = [];
|
|
445
|
+
const events = session.events;
|
|
446
|
+
for (let index = events.length - 1; index >= 0 && toolLines.length < 12; index -= 1) {
|
|
447
|
+
const event = events[index];
|
|
448
|
+
if (event?.type === "tool/call") {
|
|
449
|
+
const data = event.data;
|
|
450
|
+
const argsRaw = typeof data?.arguments === "string" ? data.arguments : JSON.stringify(data?.arguments ?? {});
|
|
451
|
+
toolLines.push(`[call] ${data?.name ?? "?"} ${argsRaw.slice(0, 500)}`);
|
|
452
|
+
} else if (event?.type === "tool/result") {
|
|
453
|
+
const data = event.data;
|
|
454
|
+
const output = typeof data?.output === "string" ? data.output : "";
|
|
455
|
+
const failure = data?.error ? " [ERROR]" : "";
|
|
456
|
+
toolLines.push(`[result]${failure} ${output.slice(0, 500)}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
toolLines.reverse();
|
|
259
460
|
return [
|
|
260
461
|
`Review kind: ${kind}`,
|
|
261
462
|
`Signals: ${signal.toolCalls} tool calls, ${signal.userChars} user chars, ${signal.assistantChars} assistant chars.`,
|
|
463
|
+
`Recent tool activity (${toolLines.length}):`,
|
|
464
|
+
...toolLines,
|
|
262
465
|
"Return ONLY the structured JSON plan. Evidence is mandatory for every op.",
|
|
263
466
|
"",
|
|
264
467
|
...messages
|
|
265
468
|
].join("\n");
|
|
266
469
|
}
|
|
267
470
|
//#endregion
|
|
268
|
-
export { Config, apply, inject, name };
|
|
471
|
+
export { Config, apply, filterUnreadSkillOps, inject, name, shouldCompletionReview, sweepDeadSessionEntries };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -11,7 +11,11 @@ export interface Config {
|
|
|
11
11
|
reviewMode?: string;
|
|
12
12
|
memoryInterval?: number;
|
|
13
13
|
skillInterval?: number;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Tools the one-shot review subagent may use. Only actually-existing tools
|
|
16
|
+
* may be listed (the DSH tool catalog has `skill`; the `skill_search` /
|
|
17
|
+
* `skill_load` discovery pair does not exist on this platform).
|
|
18
|
+
*/
|
|
15
19
|
reviewToolAllow?: string[];
|
|
16
20
|
reviewTimeoutMs?: number;
|
|
17
21
|
executionTimeoutMs?: number;
|
|
@@ -20,7 +24,35 @@ export interface Config {
|
|
|
20
24
|
reviewMaxDepth?: number;
|
|
21
25
|
/** LLM provider for review subagents. Omit to inherit the deployment default route. */
|
|
22
26
|
reviewProvider?: string;
|
|
27
|
+
/** Skill-review trigger: cadence (interval) | completion (once after a proven-long task) | both. */
|
|
28
|
+
skillReviewTrigger?: string;
|
|
29
|
+
/** Cumulative session tool calls before a session counts as proven-long for the completion channel. */
|
|
30
|
+
skillReviewCompletionMinToolCalls?: number;
|
|
23
31
|
}
|
|
24
32
|
export declare const Config: z<Config>;
|
|
25
33
|
export declare function apply(ctx: Context, rawConfig: Config): void;
|
|
34
|
+
/** Completion-channel decision: task finished normally AND the session is proven long. */
|
|
35
|
+
export declare function shouldCompletionReview(reason: {
|
|
36
|
+
kind?: string;
|
|
37
|
+
} | undefined, sessionToolCalls: number, minToolCalls: number): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Remove every entry whose session is no longer live (rc.42 audit P1-10):
|
|
40
|
+
* `turnStarts` / `cumulativeToolCalls` / `completionInjected` are keyed by
|
|
41
|
+
* SessionId with no platform session-end hook to prune against, so they grew
|
|
42
|
+
* unbounded over a long-lived host. Works for maps and sets; returns the
|
|
43
|
+
* number of removed entries.
|
|
44
|
+
*/
|
|
45
|
+
export declare function sweepDeadSessionEntries<K>(entries: Map<K, unknown> | Set<K>, isAlive: (id: K) => boolean): number;
|
|
46
|
+
/**
|
|
47
|
+
* Drop mutating ops whose target was not read this session, in place.
|
|
48
|
+
* Create is exempt (no read required to author a new skill). Covers the same
|
|
49
|
+
* mutating surface Hermes guards (edit/patch/write_file/remove_file), so a
|
|
50
|
+
* background review cannot blind-touch support files or edits of skills it
|
|
51
|
+
* never loaded. Returns the count of dropped ops so the plan event can report
|
|
52
|
+
* them as rejected.
|
|
53
|
+
*/
|
|
54
|
+
export declare function filterUnreadSkillOps(ops: Array<{
|
|
55
|
+
action?: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
}>, readNames: ReadonlySet<string>): number;
|
|
26
58
|
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-review",
|
|
3
3
|
"description": "Background review orchestration (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0-rc.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -33,27 +33,29 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
36
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
37
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.
|
|
36
|
+
"@lmzhen/dsh-evolution-core": "^0.2.0-rc.1",
|
|
37
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.2.0-rc.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
41
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
42
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
43
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
44
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
45
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
46
|
-
"@lmzhen/dsh-evolution-state": "^0.
|
|
41
|
+
"@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
|
|
42
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
46
|
+
"@lmzhen/dsh-evolution-state": "^0.2.0-rc.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
50
|
-
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.
|
|
51
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
53
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@
|
|
56
|
-
"@
|
|
57
|
-
"@lmzhen/dsh-evolution-
|
|
49
|
+
"@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.1-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.1-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
|
|
57
|
+
"@lmzhen/dsh-evolution-core": "^0.2.0-rc.1",
|
|
58
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.2.0-rc.1",
|
|
59
|
+
"@lmzhen/dsh-evolution-state": "^0.2.0-rc.1"
|
|
58
60
|
}
|
|
59
61
|
}
|