dsh-continual-evolve 0.6.1 → 0.7.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/README.md +19 -11
- package/README.zh.md +19 -11
- package/lib/apply.js +17 -9
- package/lib/auto.d.ts +19 -4
- package/lib/auto.js +61 -8
- package/lib/command.d.ts +6 -0
- package/lib/command.js +190 -27
- package/lib/evolve-event.d.ts +4 -2
- package/lib/evolve-event.js +12 -2
- package/lib/fate.js +8 -2
- package/lib/index.d.ts +56 -0
- package/lib/index.js +29 -2
- package/lib/inject.d.ts +35 -9
- package/lib/inject.js +86 -36
- package/lib/llm-text.d.ts +23 -0
- package/lib/llm-text.js +50 -33
- package/lib/planner.d.ts +13 -1
- package/lib/planner.js +31 -7
- package/lib/prefix-cache.d.ts +78 -0
- package/lib/prefix-cache.js +168 -0
- package/lib/project.d.ts +28 -0
- package/lib/project.js +66 -0
- package/lib/review.d.ts +11 -0
- package/lib/review.js +23 -2
- package/lib/runtime.d.ts +24 -0
- package/lib/runtime.js +83 -0
- package/lib/service.d.ts +11 -1
- package/lib/service.js +24 -3
- package/lib/state.d.ts +8 -4
- package/lib/state.js +14 -7
- package/lib/store.d.ts +44 -0
- package/lib/store.js +99 -4
- package/lib/token-usage.d.ts +68 -0
- package/lib/token-usage.js +211 -0
- package/lib/tool.d.ts +13 -1
- package/lib/tool.js +118 -32
- package/lib/types.d.ts +14 -2
- package/lib/types.js +12 -0
- package/lib/validate.js +30 -0
- package/lib/wrapup-command.js +11 -2
- package/lib/wrapup.d.ts +7 -2
- package/lib/wrapup.js +21 -1
- package/package.json +10 -10
package/lib/tool.js
CHANGED
|
@@ -1,31 +1,93 @@
|
|
|
1
1
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { MEMORY_TYPE_KEY } from "./types.js";
|
|
3
|
+
import { projectKeyOf } from "./project.js";
|
|
2
4
|
import { formatHarnessStateForPrompt } from "./render.js";
|
|
3
5
|
import { requireGlobalApproval } from "./approval.js";
|
|
4
6
|
import { CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry } from "./promotion.js";
|
|
5
7
|
import { entrySourceOf } from "./source.js";
|
|
6
8
|
import { getUsageCount, loadUsage } from "./usage.js";
|
|
7
9
|
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
import { DEFAULT_REVIEWS_RETAIN } from "./store.js";
|
|
11
|
+
const SCOPES = ["local", "project", "global"];
|
|
12
|
+
/**
|
|
13
|
+
* Accept the string form (`scope: "project"`) with the legacy boolean tool
|
|
14
|
+
* parameter (`global: true`) as fallback. `scope` wins when both are given.
|
|
15
|
+
*/
|
|
10
16
|
export function scopeOf(value, fallback) {
|
|
11
|
-
|
|
17
|
+
if (value === "global" || value === true) {
|
|
18
|
+
return "global";
|
|
19
|
+
}
|
|
20
|
+
if (value === "project") {
|
|
21
|
+
return "project";
|
|
22
|
+
}
|
|
23
|
+
return fallback;
|
|
12
24
|
}
|
|
13
25
|
/** The calling agent's session id; tools always run inside an agent scope. */
|
|
14
26
|
function sessionIdOf(exec) {
|
|
15
27
|
return exec.agent?.id;
|
|
16
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Collect the target ids for an evolve_delete call: legacy single `id`
|
|
31
|
+
* plus the batch `ids` array, de-duplicated in first-seen order. Throws
|
|
32
|
+
* loudly when neither carries an id — a silent no-op delete would look
|
|
33
|
+
* like success while deleting nothing.
|
|
34
|
+
*
|
|
35
|
+
* @throws when both `id` and `ids` are absent or empty.
|
|
36
|
+
*/
|
|
37
|
+
export function collectDeleteIds(id, ids) {
|
|
38
|
+
const collected = [];
|
|
39
|
+
if (typeof id === "string" && id.length > 0)
|
|
40
|
+
collected.push(id);
|
|
41
|
+
if (Array.isArray(ids)) {
|
|
42
|
+
for (const item of ids) {
|
|
43
|
+
if (typeof item === "string" && item.length > 0 && !collected.includes(item)) {
|
|
44
|
+
collected.push(item);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (collected.length === 0) {
|
|
49
|
+
throw new Error("evolve_delete requires id or a non-empty ids array");
|
|
50
|
+
}
|
|
51
|
+
return collected;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The store id a scope reads/writes: session id for local, derived project
|
|
55
|
+
* key for project, undefined for global. Throws for project when the
|
|
56
|
+
* session cwd is unavailable — fail loud instead of writing local-by-mistake.
|
|
57
|
+
*/
|
|
58
|
+
function storeIdFor(scope, exec) {
|
|
59
|
+
if (scope === "local") {
|
|
60
|
+
return sessionIdOf(exec);
|
|
61
|
+
}
|
|
62
|
+
if (scope === "project") {
|
|
63
|
+
const key = projectKeyOf(exec.agent);
|
|
64
|
+
if (!key) {
|
|
65
|
+
throw new Error("project scope needs the session cwd (unavailable for this agent) — use local or global instead");
|
|
66
|
+
}
|
|
67
|
+
return key;
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Human-approval gate shared by global and project writes (both cross-session). */
|
|
72
|
+
function needsApproval(scope) {
|
|
73
|
+
return scope === "global" || scope === "project";
|
|
74
|
+
}
|
|
75
|
+
/** Store label used in approval prompts. */
|
|
76
|
+
function storeLabel(scope) {
|
|
77
|
+
return scope === "project" ? "本项目跨会话 store" : "跨会话全局 store";
|
|
78
|
+
}
|
|
17
79
|
function textResult(text) {
|
|
18
80
|
return { text };
|
|
19
81
|
}
|
|
20
82
|
export function registerEvolveTools(ctx, engine, opts) {
|
|
21
83
|
ctx.tools.register(defineTool({
|
|
22
84
|
name: "evolve_list",
|
|
23
|
-
description: "List the continual harness state (prompt notes, memories, skills, subagent specs) for the current session (local) or across
|
|
85
|
+
description: "List the continual harness state (prompt notes, memories, skills, subagent specs) for the current session (local), the current project (project), or across projects (global).",
|
|
24
86
|
parameters: {
|
|
25
87
|
scope: {
|
|
26
88
|
type: "string",
|
|
27
89
|
enum: SCOPES,
|
|
28
|
-
description: "Which store to list: 'local' (default) or 'global'.",
|
|
90
|
+
description: "Which store to list: 'local' (default), 'project', or 'global'.",
|
|
29
91
|
},
|
|
30
92
|
},
|
|
31
93
|
output: {
|
|
@@ -34,7 +96,7 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
34
96
|
},
|
|
35
97
|
execute: async (args, exec) => {
|
|
36
98
|
const scope = scopeOf(args.scope, "local");
|
|
37
|
-
const state = engine.load(scope,
|
|
99
|
+
const state = engine.load(scope, storeIdFor(scope, exec));
|
|
38
100
|
const text = formatHarnessStateForPrompt(state);
|
|
39
101
|
// Append injection usage counts (gap B1).
|
|
40
102
|
const usage = loadUsage(engine.baseDir);
|
|
@@ -64,6 +126,8 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
64
126
|
skill_kind: { type: "string", enum: ["executable", "guidance"], description: "For skills: executable (python reference, default) or guidance (SKILL.md document, no reference)." },
|
|
65
127
|
reference: { type: "object", additionalProperties: true, description: "For executable skills: {type:'python', import, callable}." },
|
|
66
128
|
arguments: { type: "object", additionalProperties: true, description: "For executable skills: accepted input contract." },
|
|
129
|
+
memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Required for memory: one fact per entry (feedback/project must carry Why + How to apply)." },
|
|
130
|
+
scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
|
|
67
131
|
global: { type: "boolean", description: "Set true to write the cross-session store (requires human approval; only for durable, reusable lessons)." },
|
|
68
132
|
},
|
|
69
133
|
output: {
|
|
@@ -71,15 +135,18 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
71
135
|
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
72
136
|
},
|
|
73
137
|
execute: async (args, exec) => {
|
|
74
|
-
const scope = scopeOf(args.global, "local");
|
|
75
|
-
|
|
138
|
+
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
139
|
+
// Resolve first: a project scope without a session cwd fails
|
|
140
|
+
// here, before any human approval question is asked.
|
|
141
|
+
const storeId = storeIdFor(scope, exec);
|
|
142
|
+
if (needsApproval(scope) && opts.requireGlobalApproval) {
|
|
76
143
|
// Informed approval: surface a similarity hit against the
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
const hit = mostSimilarEntry(Object.values(
|
|
144
|
+
// target store BEFORE the human decides — the engine's
|
|
145
|
+
// write-time guard still has the final say.
|
|
146
|
+
const targetState = engine.load(scope, storeId);
|
|
147
|
+
const hit = mostSimilarEntry(Object.values(targetState.entries[args.kind]), args.title ?? "", args.content ?? "", CONFLICT_WARN_SCORE);
|
|
81
148
|
const conflictNote = hit ? ` ⚠️ ${buildConflictNotice(hit)}——建议改用 evolve_update` : "";
|
|
82
|
-
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" →
|
|
149
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → ${storeLabel(scope)}${conflictNote}`);
|
|
83
150
|
}
|
|
84
151
|
const edit = {
|
|
85
152
|
action: "create",
|
|
@@ -95,7 +162,10 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
95
162
|
edit.reference = args.reference;
|
|
96
163
|
if (args.arguments !== undefined)
|
|
97
164
|
edit.arguments = args.arguments;
|
|
98
|
-
|
|
165
|
+
if (args.kind === "memory" && args.memoryType !== undefined) {
|
|
166
|
+
edit.metadata = { [MEMORY_TYPE_KEY]: args.memoryType };
|
|
167
|
+
}
|
|
168
|
+
return textResult(applyEditsText(engine, scope, storeId, [edit], exec.agent, sessionIdOf(exec)));
|
|
99
169
|
},
|
|
100
170
|
}));
|
|
101
171
|
ctx.tools.register(defineTool({
|
|
@@ -106,6 +176,8 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
106
176
|
id: { type: "string", required: true, description: "Existing entry id." },
|
|
107
177
|
title: { type: "string" },
|
|
108
178
|
content: { type: "string" },
|
|
179
|
+
memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "For memory: (re)classify the entry's recall type." },
|
|
180
|
+
scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
|
|
109
181
|
global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
|
|
110
182
|
},
|
|
111
183
|
output: {
|
|
@@ -113,24 +185,33 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
113
185
|
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
114
186
|
},
|
|
115
187
|
execute: async (args, exec) => {
|
|
116
|
-
const scope = scopeOf(args.global, "local");
|
|
117
|
-
|
|
118
|
-
|
|
188
|
+
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
189
|
+
const storeId = storeIdFor(scope, exec);
|
|
190
|
+
if (needsApproval(scope) && opts.requireGlobalApproval) {
|
|
191
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_update ${args.kind}:${args.id} → ${storeLabel(scope)}`);
|
|
119
192
|
}
|
|
120
193
|
const edit = { action: "update", kind: args.kind, id: args.id };
|
|
121
194
|
if (args.title !== undefined)
|
|
122
195
|
edit.title = args.title;
|
|
123
196
|
if (args.content !== undefined)
|
|
124
197
|
edit.content = args.content;
|
|
125
|
-
|
|
198
|
+
if (args.memoryType !== undefined)
|
|
199
|
+
edit.metadata = { [MEMORY_TYPE_KEY]: args.memoryType };
|
|
200
|
+
return textResult(applyEditsText(engine, scope, storeId, [edit], exec.agent, sessionIdOf(exec)));
|
|
126
201
|
},
|
|
127
202
|
}));
|
|
128
203
|
ctx.tools.register(defineTool({
|
|
129
204
|
name: "evolve_delete",
|
|
130
|
-
description: "Delete
|
|
205
|
+
description: "Delete harness entries by id. Pass `id` for one entry or `ids` for several — a batch lands as one refinement behind one approval. Literal ids win: entries whose stored id itself carries a scope-like prefix (e.g. a legacy `local:…` id) are addressed verbatim.",
|
|
131
206
|
parameters: {
|
|
132
207
|
kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
|
|
133
|
-
id: { type: "string",
|
|
208
|
+
id: { type: "string", description: "Existing entry id (single delete; combine with ids or use ids for a batch)." },
|
|
209
|
+
ids: {
|
|
210
|
+
type: "array",
|
|
211
|
+
items: { type: "string" },
|
|
212
|
+
description: "Existing entry ids for one batch delete (one refinement, one approval).",
|
|
213
|
+
},
|
|
214
|
+
scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
|
|
134
215
|
global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
|
|
135
216
|
},
|
|
136
217
|
output: {
|
|
@@ -138,12 +219,14 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
138
219
|
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
139
220
|
},
|
|
140
221
|
execute: async (args, exec) => {
|
|
141
|
-
const scope = scopeOf(args.global, "local");
|
|
142
|
-
|
|
143
|
-
|
|
222
|
+
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
223
|
+
const storeId = storeIdFor(scope, exec);
|
|
224
|
+
const ids = collectDeleteIds(args.id, args.ids);
|
|
225
|
+
if (needsApproval(scope) && opts.requireGlobalApproval) {
|
|
226
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${ids.length > 1 ? `${ids.length} entries (${ids.join(", ")})` : ids[0]} → ${storeLabel(scope)}`);
|
|
144
227
|
}
|
|
145
|
-
const
|
|
146
|
-
return textResult(applyEditsText(engine, scope,
|
|
228
|
+
const edits = ids.map((id) => ({ action: "delete", kind: args.kind, id }));
|
|
229
|
+
return textResult(applyEditsText(engine, scope, storeId, edits, exec.agent, sessionIdOf(exec)));
|
|
147
230
|
},
|
|
148
231
|
}));
|
|
149
232
|
ctx.tools.register(defineTool({
|
|
@@ -151,6 +234,7 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
151
234
|
description: "Deterministically revert a previous refinement by its id (from evolve_list history or the /evolve command).",
|
|
152
235
|
parameters: {
|
|
153
236
|
refinementId: { type: "string", required: true, description: "The refinement id to roll back." },
|
|
237
|
+
scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
|
|
154
238
|
global: { type: "boolean", description: "Set true to roll back a cross-session refinement." },
|
|
155
239
|
},
|
|
156
240
|
output: {
|
|
@@ -158,14 +242,14 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
158
242
|
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
159
243
|
},
|
|
160
244
|
execute: async (args, exec) => {
|
|
161
|
-
const scope = scopeOf(args.global, "local");
|
|
162
|
-
const result = engine.rollback(scope,
|
|
245
|
+
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
246
|
+
const result = engine.rollback(scope, storeIdFor(scope, exec), args.refinementId);
|
|
163
247
|
return textResult(`Rolled back ${result.rollbackOf ?? result.id}: ${result.appliedEdits.filter((e) => e.applied).length} edit(s) reverted.`);
|
|
164
248
|
},
|
|
165
249
|
}));
|
|
166
250
|
}
|
|
167
|
-
function applyEditsText(engine, scope,
|
|
168
|
-
const result = engine.apply(scope,
|
|
251
|
+
function applyEditsText(engine, scope, storeId, edits, agent, eventSessionId) {
|
|
252
|
+
const result = engine.apply(scope, storeId, {
|
|
169
253
|
summary: "Direct tool edit",
|
|
170
254
|
rationale: "Model-invoked single edit via evolve_* tool.",
|
|
171
255
|
expectedOutcome: "Entry is created, updated, or deleted as requested.",
|
|
@@ -173,14 +257,16 @@ function applyEditsText(engine, scope, sessionId, edits, agent) {
|
|
|
173
257
|
}, agent
|
|
174
258
|
? {
|
|
175
259
|
scope,
|
|
176
|
-
...(entrySourceOf(agent,
|
|
260
|
+
...(entrySourceOf(agent, eventSessionId ?? storeId) ? { source: entrySourceOf(agent, eventSessionId ?? storeId) } : {}),
|
|
177
261
|
}
|
|
178
262
|
: { scope });
|
|
179
263
|
const applied = result.appliedEdits.filter((e) => e.applied);
|
|
180
264
|
const failed = result.appliedEdits.filter((e) => !e.applied);
|
|
181
265
|
// Gap C4: emit structured evolve_complete event for third-party consumers.
|
|
182
|
-
|
|
183
|
-
|
|
266
|
+
// The event keys on the live session (not the project store key).
|
|
267
|
+
const eventSession = eventSessionId ?? storeId;
|
|
268
|
+
if (applied.length > 0 && eventSession) {
|
|
269
|
+
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", eventSession), engine.retention?.reviews ?? DEFAULT_REVIEWS_RETAIN);
|
|
184
270
|
}
|
|
185
271
|
const lines = [`refinement ${result.id}: ${applied.length} applied, ${failed.length} failed`];
|
|
186
272
|
for (const e of applied) {
|
package/lib/types.d.ts
CHANGED
|
@@ -20,8 +20,20 @@ export type RefinementKind = "prompt" | "memory" | "skill" | "subagent";
|
|
|
20
20
|
export type SkillKind = "executable" | "guidance";
|
|
21
21
|
/** How an entry changes. */
|
|
22
22
|
export type RefinementAction = "create" | "update" | "delete" | "archive";
|
|
23
|
-
/** Where an entry lives: session
|
|
24
|
-
export type HarnessScope = "local" | "global";
|
|
23
|
+
/** Where an entry lives: session staging, one project, or cross-project. */
|
|
24
|
+
export type HarnessScope = "local" | "project" | "global";
|
|
25
|
+
/**
|
|
26
|
+
* Memory-entry shape discipline (one fact per entry, typed like ZCode's
|
|
27
|
+
* memory frontmatter): every `memory` create must stamp one of these in
|
|
28
|
+
* `metadata[MEMORY_TYPE_KEY]` — the recall hook the directory shows.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MEMORY_TYPE_KEY = "memoryType";
|
|
31
|
+
/** Closed memory-type set: user | feedback | project | reference. */
|
|
32
|
+
export declare const MEMORY_TYPES: readonly ["user", "feedback", "project", "reference"];
|
|
33
|
+
/** One of {@link MEMORY_TYPES}. */
|
|
34
|
+
export type MemoryRecallType = (typeof MEMORY_TYPES)[number];
|
|
35
|
+
/** True when the value is a closed-set memory type. */
|
|
36
|
+
export declare function isMemoryType(value: unknown): value is MemoryRecallType;
|
|
25
37
|
/**
|
|
26
38
|
* Metadata key recording which session an entry's content was distilled from
|
|
27
39
|
* (trajectory citation; see {@link EntrySource}).
|
package/lib/types.js
CHANGED
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
* code here is an original implementation, written for the DeepSeek Harness
|
|
9
9
|
* plugin surface.
|
|
10
10
|
*/
|
|
11
|
+
/**
|
|
12
|
+
* Memory-entry shape discipline (one fact per entry, typed like ZCode's
|
|
13
|
+
* memory frontmatter): every `memory` create must stamp one of these in
|
|
14
|
+
* `metadata[MEMORY_TYPE_KEY]` — the recall hook the directory shows.
|
|
15
|
+
*/
|
|
16
|
+
export const MEMORY_TYPE_KEY = "memoryType";
|
|
17
|
+
/** Closed memory-type set: user | feedback | project | reference. */
|
|
18
|
+
export const MEMORY_TYPES = ["user", "feedback", "project", "reference"];
|
|
19
|
+
/** True when the value is a closed-set memory type. */
|
|
20
|
+
export function isMemoryType(value) {
|
|
21
|
+
return typeof value === "string" && MEMORY_TYPES.includes(value);
|
|
22
|
+
}
|
|
11
23
|
/**
|
|
12
24
|
* Metadata key recording which session an entry's content was distilled from
|
|
13
25
|
* (trajectory citation; see {@link EntrySource}).
|
package/lib/validate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
|
|
1
2
|
import { validateSkillEntryContent } from "./skillquality.js";
|
|
2
3
|
const ACTIONS = new Set(["create", "update", "delete", "archive"]);
|
|
3
4
|
const KINDS = new Set(["prompt", "memory", "skill", "subagent"]);
|
|
@@ -18,6 +19,9 @@ export function validateBlastRadiusScope(scope, blastRadius) {
|
|
|
18
19
|
if (scope === "global" && blastRadius === "session") {
|
|
19
20
|
return "global-scope edit must declare blastRadius \"general\" or \"project\" (\"session\" contradicts cross-session persistence)";
|
|
20
21
|
}
|
|
22
|
+
if (scope === "project" && blastRadius === "session") {
|
|
23
|
+
return "project-scope edit must declare blastRadius \"project\" or \"general\" (\"session\" contradicts cross-session persistence)";
|
|
24
|
+
}
|
|
21
25
|
return undefined;
|
|
22
26
|
}
|
|
23
27
|
/**
|
|
@@ -71,6 +75,32 @@ export function validateEdit(edit, computedId, scope, before) {
|
|
|
71
75
|
edit.arguments === undefined) {
|
|
72
76
|
return "update carries no changes";
|
|
73
77
|
}
|
|
78
|
+
if (edit.kind === "memory" && (edit.action === "create" || edit.action === "update")) {
|
|
79
|
+
// One-fact typed memories: every memory create declares its recall
|
|
80
|
+
// type; feedback/project (pitfalls, conventions) additionally carry
|
|
81
|
+
// the Why + How-to-apply structure so the fact is reusable, not just
|
|
82
|
+
// recorded. Updates are partial — only carried fields are checked,
|
|
83
|
+
// resolved against the persisted entry.
|
|
84
|
+
const carriedType = edit.metadata?.[MEMORY_TYPE_KEY];
|
|
85
|
+
if (edit.action === "create" && !isMemoryType(carriedType)) {
|
|
86
|
+
return `create memory requires metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference (one fact per entry; update the existing entry instead of adding a duplicate)`;
|
|
87
|
+
}
|
|
88
|
+
if (carriedType !== undefined && !isMemoryType(carriedType)) {
|
|
89
|
+
return `invalid metadata.${MEMORY_TYPE_KEY} ${JSON.stringify(carriedType)} (one of user|feedback|project|reference)`;
|
|
90
|
+
}
|
|
91
|
+
const effectiveType = isMemoryType(carriedType)
|
|
92
|
+
? carriedType
|
|
93
|
+
: edit.action === "update"
|
|
94
|
+
? before?.metadata?.[MEMORY_TYPE_KEY]
|
|
95
|
+
: undefined;
|
|
96
|
+
if ((effectiveType === "feedback" || effectiveType === "project") &&
|
|
97
|
+
(edit.action === "create" || edit.content !== undefined)) {
|
|
98
|
+
const content = edit.content ?? "";
|
|
99
|
+
if (!/why/i.test(content) || !/how/i.test(content)) {
|
|
100
|
+
return `${effectiveType} memory content must carry a Why and a How to apply section (the fact plus why it matters and how to apply it next time)`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
74
104
|
if (edit.action === "create" && edit.kind === "skill") {
|
|
75
105
|
// Guidance skills are SKILL.md documents: no python reference (a
|
|
76
106
|
// reference on a guidance skill would be an invented contract) and
|
package/lib/wrapup-command.js
CHANGED
|
@@ -13,7 +13,16 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
|
|
|
13
13
|
return success(`(nothing to wrap up: ${sessionId}'s local store has no active, un-promoted entries — use /evolve list to inspect it)`);
|
|
14
14
|
}
|
|
15
15
|
// 1. Classify: the model judges each audited candidate's fate.
|
|
16
|
-
const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, {
|
|
16
|
+
const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, {
|
|
17
|
+
signal: invocation.signal,
|
|
18
|
+
tokenUsage: {
|
|
19
|
+
baseDir: engine.baseDir,
|
|
20
|
+
sessionId,
|
|
21
|
+
retain: engine.retention.tokenUsage,
|
|
22
|
+
onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
23
|
+
},
|
|
24
|
+
tokenUsagePhase: "wrapup",
|
|
25
|
+
});
|
|
17
26
|
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
18
27
|
// 2. Partition by action. Deterministic guards re-check the LIVE global
|
|
19
28
|
// store right before anything lands (state may have changed mid-call).
|
|
@@ -34,7 +43,7 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
|
|
|
34
43
|
splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
|
|
35
44
|
continue;
|
|
36
45
|
}
|
|
37
|
-
const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy);
|
|
46
|
+
const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
|
|
38
47
|
if (blocked) {
|
|
39
48
|
splitSkipped.push({ key: item.key, reason: blocked });
|
|
40
49
|
continue;
|
package/lib/wrapup.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { Context } from "@deepseek-ai/cordis";
|
|
22
22
|
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
23
23
|
import type { HarnessEntry, HarnessState, RefinementKind, RefinementProposal } from "./types.js";
|
|
24
|
+
import { type TokenUsageTarget } from "./token-usage.js";
|
|
24
25
|
import { type PromotionPolicy } from "./promotion.js";
|
|
25
26
|
/** What should happen to one local entry at session end. */
|
|
26
27
|
export type WrapupVerdict = "promote" | "archive" | "keep";
|
|
@@ -199,7 +200,7 @@ export declare function splitArchiveGuards(items: readonly WrapupItem[], candida
|
|
|
199
200
|
* too thin, no near-duplicate global entry. A blocked split is dropped (the
|
|
200
201
|
* entry still archives plain) rather than half-promoting a redundancy.
|
|
201
202
|
*/
|
|
202
|
-
export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind, policy?: PromotionPolicy): string | undefined;
|
|
203
|
+
export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind, policy?: PromotionPolicy, candidateMetadata?: Record<string, unknown>): string | undefined;
|
|
203
204
|
/**
|
|
204
205
|
* Shared proposal builders for a WHOLE promotion — used by both the
|
|
205
206
|
* `/evolve wrapup` command and the gate's local-fate dimension so the two
|
|
@@ -232,12 +233,16 @@ export declare function splitPromoteProposals(item: WrapupItem, candidate: Wrapu
|
|
|
232
233
|
* injection). Undefined when nothing applies.
|
|
233
234
|
*/
|
|
234
235
|
export declare function valenceStampProposal(item: WrapupItem, candidate: WrapupCandidate): RefinementProposal | undefined;
|
|
235
|
-
export declare const WRAPUP_ASSESS_SYSTEM_PROMPT = "You are the /evolve session wrap-up assessor.\nA session is ending and its local harness entries need a fate. Classify each\nlisted entry exactly once:\n\n- \"promote\" \u2014 the content is a stable, durable, CROSS-SESSION reusable lesson:\n a durable user preference, a project-level fact or convention, a reusable\n procedure or skill. Future sessions would benefit from seeing it.\n- \"archive\" \u2014 the content is session-specific task progress, one-off noise,\n superseded or obsolete, or already covered by the global store (note\n \"covered globally\" in the reason), or stale (old + never injected \u2014 note\n \"stale (injectionCount=0, recency low)\" in the reason), or CONTRADICTED \u2014\n newer evidence in the trajectory or the global store disproves or overrides\n the entry's content (note what contradicts it in the reason).\n- \"keep\" \u2014 still actively useful to this session, or genuinely uncertain.\n\nRules:\n- CONTRADICTED: when evidence contradicts an entry, set \"contradicted\": true\n on that item (besides the verdict). Entries marked \"(contradicted N\u00D7\n before)\" were contradicted in earlier assessments \u2014 prefer \"archive\" for\n them unless you have concrete evidence the content is valid again.\n- When an entry is marked \"covered globally\" in the listing, prefer \"archive\"\n or \"keep\" over \"promote\" \u2014 promoting a duplicate gains nothing.\n- When an entry is marked \"stale\" (injectionCount=0 and low recency), prefer\n \"archive\" \u2014 the entry has never been used and is old, so it is unlikely to\n be needed again. Only \"keep\" if the content is clearly valuable despite low\n usage (e.g. a safety policy that rarely triggers but is critical).\n- Do not promote local task state, work-in-progress notes, or content tied to\n one session's ephemeral details.\n- Skills: only \"promote\" a skill entry that is a genuinely reusable procedure\n meeting the DSH skill quality standard; one-off workflows are \"archive\" or\n \"keep\".\n- SPLIT PROMOTION: when an entry mixes a stable, cross-session-reusable part\n WITH session-specific snapshot details, do NOT promote it whole. Instead\n give verdict \"archive\" WITH a \"promote\" sub-object holding a CLEANED\n version of only the durable part (a stable title + the persistent facts,\n stripped of dates/states/one-off figures). Ephemeral snapshot content stays\n out of the sub-object \u2014 it is left behind in the archive. A sub-object is\n only meaningful on \"archive\" verdicts.\n\nReturn JSON only:\n{\n \"rationale\": \"one or two sentences\",\n \"items\": [\n {\"key\": \"memory:foo\", \"verdict\": \"promote|archive|keep\", \"reason\": \"why\"},\n {\"key\": \"memory:bar\", \"verdict\": \"archive\", \"reason\": \"why\", \"contradicted\": true,\n \"promote\": {\"title\": \"cleaned stable title\", \"content\": \"cleaned durable part only\"}}\n ]\n}\nOnly keys from the provided list are allowed; any entry you omit defaults to \"keep\".";
|
|
236
|
+
export declare const WRAPUP_ASSESS_SYSTEM_PROMPT = "You are the /evolve session wrap-up assessor.\nA session is ending and its local harness entries need a fate. Classify each\nlisted entry exactly once:\n\n- \"promote\" \u2014 the content is a stable, durable, CROSS-SESSION reusable lesson:\n a durable user preference, a project-level fact or convention, a reusable\n procedure or skill. Future sessions would benefit from seeing it.\n- \"archive\" \u2014 the content is session-specific task progress, one-off noise,\n superseded or obsolete, or already covered by the global store (note\n \"covered globally\" in the reason), or stale (old + never injected \u2014 note\n \"stale (injectionCount=0, recency low)\" in the reason), or CONTRADICTED \u2014\n newer evidence in the trajectory or the global store disproves or overrides\n the entry's content (note what contradicts it in the reason).\n- \"keep\" \u2014 still actively useful to this session, or genuinely uncertain.\n\nRules:\n- CONTRADICTED: when evidence contradicts an entry, set \"contradicted\": true\n on that item (besides the verdict). Entries marked \"(contradicted N\u00D7\n before)\" were contradicted in earlier assessments \u2014 prefer \"archive\" for\n them unless you have concrete evidence the content is valid again.\n- When an entry is marked \"covered globally\" in the listing, prefer \"archive\"\n or \"keep\" over \"promote\" \u2014 promoting a duplicate gains nothing.\n- When an entry is marked \"stale\" (injectionCount=0 and low recency), prefer\n \"archive\" \u2014 the entry has never been used and is old, so it is unlikely to\n be needed again. Only \"keep\" if the content is clearly valuable despite low\n usage (e.g. a safety policy that rarely triggers but is critical).\n- Do not promote local task state, work-in-progress notes, or content tied to\n one session's ephemeral details.\n- Memory entries carry a recall type (user/feedback/project/reference): a\n memory WITHOUT one cannot be promoted (the guard skips it) \u2014 verdict such\n entries \"keep\" (or \"archive\" when they qualify) rather than \"promote\".\n- Skills: only \"promote\" a skill entry that is a genuinely reusable procedure\n meeting the DSH skill quality standard; one-off workflows are \"archive\" or\n \"keep\".\n- SPLIT PROMOTION: when an entry mixes a stable, cross-session-reusable part\n WITH session-specific snapshot details, do NOT promote it whole. Instead\n give verdict \"archive\" WITH a \"promote\" sub-object holding a CLEANED\n version of only the durable part (a stable title + the persistent facts,\n stripped of dates/states/one-off figures). Ephemeral snapshot content stays\n out of the sub-object \u2014 it is left behind in the archive. A sub-object is\n only meaningful on \"archive\" verdicts.\n\nReturn JSON only:\n{\n \"rationale\": \"one or two sentences\",\n \"items\": [\n {\"key\": \"memory:foo\", \"verdict\": \"promote|archive|keep\", \"reason\": \"why\"},\n {\"key\": \"memory:bar\", \"verdict\": \"archive\", \"reason\": \"why\", \"contradicted\": true,\n \"promote\": {\"title\": \"cleaned stable title\", \"content\": \"cleaned durable part only\"}}\n ]\n}\nOnly keys from the provided list are allowed; any entry you omit defaults to \"keep\".";
|
|
236
237
|
export interface AssessOptions {
|
|
237
238
|
/** Output token budget for the assessment call. */
|
|
238
239
|
maxOutputTokens?: number;
|
|
239
240
|
/** Abort signal forwarded to the model call. */
|
|
240
241
|
signal?: AbortSignal;
|
|
242
|
+
/** Direct-call token ledger destination; phase is selected by the caller. */
|
|
243
|
+
tokenUsage?: TokenUsageTarget;
|
|
244
|
+
/** Ledger phase for this classifier call (manual wrapup or automatic fate). */
|
|
245
|
+
tokenUsagePhase?: "wrapup" | "fate";
|
|
241
246
|
}
|
|
242
247
|
/**
|
|
243
248
|
* Ask the model to classify the audited local candidates. Routes through the
|
package/lib/wrapup.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, VALENCE_NEGATIVE_KEY, isArchived } from "./types.js";
|
|
2
|
+
import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
|
|
2
3
|
import { extractJsonObject } from "./plan.js";
|
|
3
4
|
import { compactText } from "./render.js";
|
|
4
5
|
import { streamText } from "./llm-text.js";
|
|
5
6
|
import { getUsageCount, loadUsage } from "./usage.js";
|
|
7
|
+
import { createTokenUsageObserver } from "./token-usage.js";
|
|
6
8
|
import { recencyScore } from "./inject.js";
|
|
7
9
|
import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason, secretLeakReason } from "./promotion.js";
|
|
8
10
|
export function candidateKey(kind, id) {
|
|
@@ -190,6 +192,13 @@ export function filterPromotable(items, globalState, candidates, policy = DEFAUL
|
|
|
190
192
|
skipped.push({ key: item.key, reason: "already covered globally" });
|
|
191
193
|
continue;
|
|
192
194
|
}
|
|
195
|
+
if (candidate.kind === "memory" && !isMemoryType(candidate.metadata[MEMORY_TYPE_KEY])) {
|
|
196
|
+
skipped.push({
|
|
197
|
+
key: item.key,
|
|
198
|
+
reason: `memory has no recall type (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update the local entry to classify it first, then promote`,
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
193
202
|
const scoped = projectScopedReason(`${candidate.title}\n${candidate.content}`, policy);
|
|
194
203
|
if (scoped) {
|
|
195
204
|
skipped.push({ key: item.key, reason: scoped });
|
|
@@ -271,9 +280,12 @@ export function splitArchiveGuards(items, candidates) {
|
|
|
271
280
|
* too thin, no near-duplicate global entry. A blocked split is dropped (the
|
|
272
281
|
* entry still archives plain) rather than half-promoting a redundancy.
|
|
273
282
|
*/
|
|
274
|
-
export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY) {
|
|
283
|
+
export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY, candidateMetadata) {
|
|
275
284
|
if (!item.promote)
|
|
276
285
|
return "no split payload";
|
|
286
|
+
if (kind === "memory" && !isMemoryType(candidateMetadata?.[MEMORY_TYPE_KEY])) {
|
|
287
|
+
return `split promotion needs the source entry classified (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update it first`;
|
|
288
|
+
}
|
|
277
289
|
if (globalCoverageDetected(globalState, kind, { id: "", title: item.promote.title })) {
|
|
278
290
|
return "split promotion duplicates a globally covered topic";
|
|
279
291
|
}
|
|
@@ -454,6 +466,9 @@ Rules:
|
|
|
454
466
|
usage (e.g. a safety policy that rarely triggers but is critical).
|
|
455
467
|
- Do not promote local task state, work-in-progress notes, or content tied to
|
|
456
468
|
one session's ephemeral details.
|
|
469
|
+
- Memory entries carry a recall type (user/feedback/project/reference): a
|
|
470
|
+
memory WITHOUT one cannot be promoted (the guard skips it) — verdict such
|
|
471
|
+
entries "keep" (or "archive" when they qualify) rather than "promote".
|
|
457
472
|
- Skills: only "promote" a skill entry that is a genuinely reusable procedure
|
|
458
473
|
meeting the DSH skill quality standard; one-off workflows are "archive" or
|
|
459
474
|
"keep".
|
|
@@ -511,6 +526,11 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
|
|
|
511
526
|
prompt: userPrompt,
|
|
512
527
|
maxTokens: options.maxOutputTokens ?? 4096,
|
|
513
528
|
signal: options.signal,
|
|
529
|
+
...(options.tokenUsage
|
|
530
|
+
? {
|
|
531
|
+
onUsage: createTokenUsageObserver(options.tokenUsage, options.tokenUsagePhase ?? "wrapup", agent.options.provider, agent.options.model),
|
|
532
|
+
}
|
|
533
|
+
: {}),
|
|
514
534
|
});
|
|
515
535
|
return parseWrapupAssessment(text, candidates);
|
|
516
536
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
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
6
|
"repository": {
|
|
@@ -57,18 +57,18 @@
|
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
60
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.7-
|
|
61
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.7-
|
|
62
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.7-
|
|
60
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.7-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.7-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.7-rc.1",
|
|
63
63
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@deepseek-ai/cordis": "4.0.
|
|
67
|
-
"@deepseek-ai/dsh-agent": "0.1.7-
|
|
68
|
-
"@deepseek-ai/dsh-commands": "0.1.7-
|
|
69
|
-
"@deepseek-ai/dsh-home-paths": "0.1.7-
|
|
70
|
-
"@deepseek-ai/dsh-llm": "0.1.7-
|
|
71
|
-
"@deepseek-ai/dsh-tools": "0.1.7-
|
|
66
|
+
"@deepseek-ai/cordis": "4.0.4",
|
|
67
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-commands": "0.1.7-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-home-paths": "0.1.7-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-llm": "0.1.7-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-tools": "0.1.7-rc.1",
|
|
72
72
|
"@deepseek-ai/schemastery": "3.18.3",
|
|
73
73
|
"@types/node": "^22.10.0",
|
|
74
74
|
"@vitest/coverage-v8": "^3.2.0",
|