dsh-continual-evolve 0.6.0 → 0.7.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 +10 -9
- package/README.zh.md +10 -9
- package/lib/auto.d.ts +6 -4
- package/lib/auto.js +9 -6
- package/lib/command.js +51 -24
- package/lib/fate.js +3 -2
- package/lib/index.d.ts +47 -47
- package/lib/index.js +1 -1
- package/lib/inject.d.ts +51 -12
- package/lib/inject.js +104 -38
- package/lib/llm-text.js +2 -1
- package/lib/message-source.d.ts +23 -0
- package/lib/message-source.js +12 -0
- package/lib/notify.d.ts +5 -4
- package/lib/notify.js +2 -1
- package/lib/planner.d.ts +1 -1
- package/lib/planner.js +8 -2
- package/lib/project.d.ts +28 -0
- package/lib/project.js +66 -0
- package/lib/service.js +5 -0
- package/lib/source.d.ts +5 -4
- package/lib/source.js +3 -2
- package/lib/state.d.ts +8 -4
- package/lib/state.js +14 -7
- package/lib/store.d.ts +6 -0
- package/lib/store.js +11 -3
- package/lib/tool.d.ts +4 -1
- package/lib/tool.js +84 -29
- package/lib/types.d.ts +14 -2
- package/lib/types.js +12 -0
- package/lib/validate.js +30 -0
- package/lib/wrapup-command.js +1 -1
- package/lib/wrapup.d.ts +2 -2
- package/lib/wrapup.js +15 -1
- package/package.json +12 -12
package/lib/state.js
CHANGED
|
@@ -20,7 +20,7 @@ export function stateFilePath(stateDir) {
|
|
|
20
20
|
return join(stateDir, "harness_state.json");
|
|
21
21
|
}
|
|
22
22
|
function normalizeScope(value, fallback) {
|
|
23
|
-
return value === "global" || value === "local" ? value : fallback;
|
|
23
|
+
return value === "global" || value === "project" || value === "local" ? value : fallback;
|
|
24
24
|
}
|
|
25
25
|
function objectRecord(value) {
|
|
26
26
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
@@ -95,24 +95,31 @@ export function loadHarnessState(stateDir, scope = "global") {
|
|
|
95
95
|
return state;
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
* Merge global and local state into the view the model sees.
|
|
99
|
-
*
|
|
100
|
-
* `
|
|
98
|
+
* Merge global, project, and local state into the view the model sees.
|
|
99
|
+
* Precedence is global < project < local: a colliding project id is
|
|
100
|
+
* prefixed `project:` so both remain addressable, and a colliding local id
|
|
101
|
+
* is prefixed `local:` (local always wins — it is the session's live
|
|
102
|
+
* staging copy). Called with two args the project layer is simply absent.
|
|
101
103
|
*/
|
|
102
|
-
export function mergeHarnessStates(globalState, localState) {
|
|
104
|
+
export function mergeHarnessStates(globalState, localState, opts) {
|
|
103
105
|
const merged = emptyHarnessState();
|
|
104
|
-
merged.schema = Math.max(globalState.schema, localState?.schema ?? 1);
|
|
106
|
+
merged.schema = Math.max(globalState.schema, opts?.projectState?.schema ?? 1, localState?.schema ?? 1);
|
|
105
107
|
for (const kind of Object.keys(merged.entries)) {
|
|
106
108
|
for (const [id, entry] of Object.entries(globalState.entries[kind])) {
|
|
107
109
|
merged.entries[kind][id] = { ...entry, scope: "global" };
|
|
108
110
|
}
|
|
111
|
+
for (const [id, entry] of Object.entries(opts?.projectState?.entries[kind] ?? {})) {
|
|
112
|
+
const scoped = { ...entry, scope: "project" };
|
|
113
|
+
const mergedId = merged.entries[kind][id] ? `project:${id}` : id;
|
|
114
|
+
merged.entries[kind][mergedId] = scoped;
|
|
115
|
+
}
|
|
109
116
|
for (const [id, entry] of Object.entries(localState?.entries[kind] ?? {})) {
|
|
110
117
|
const scoped = { ...entry, scope: "local" };
|
|
111
118
|
const mergedId = merged.entries[kind][id] ? `local:${id}` : id;
|
|
112
119
|
merged.entries[kind][mergedId] = scoped;
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
|
-
merged.refinements = [...globalState.refinements, ...(localState?.refinements ?? [])];
|
|
122
|
+
merged.refinements = [...globalState.refinements, ...(opts?.projectState?.refinements ?? []), ...(localState?.refinements ?? [])];
|
|
116
123
|
return merged;
|
|
117
124
|
}
|
|
118
125
|
/**
|
package/lib/store.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ export interface StorePaths {
|
|
|
8
8
|
/** JSONL path for applied refinement results. */
|
|
9
9
|
resultsPath: string;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve store paths for a scope. For `project` the `sessionId` parameter
|
|
13
|
+
* carries the project key (see {@link resolveProjectKey} in project.ts) —
|
|
14
|
+
* it is sanitized to a single path segment so an externally supplied key
|
|
15
|
+
* can never traverse out of `evolve/projects/`.
|
|
16
|
+
*/
|
|
11
17
|
export declare function storePaths(baseDir: string, scope: HarnessScope, sessionId?: string): StorePaths;
|
|
12
18
|
/** Snapshot the current state file before a mutation, if one exists. */
|
|
13
19
|
export declare function snapshotBefore(paths: StorePaths, refinementId: string): void;
|
package/lib/store.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Layout (self-contained under the DSH home; no dependency on session
|
|
5
5
|
* persistence internals):
|
|
6
6
|
*
|
|
7
|
-
* <dshHome>/evolve/global/harness_state.json cross-
|
|
7
|
+
* <dshHome>/evolve/global/harness_state.json cross-project store
|
|
8
8
|
* <dshHome>/evolve/global/refinements.jsonl applied results (rollback source)
|
|
9
|
-
* <dshHome>/evolve/
|
|
9
|
+
* <dshHome>/evolve/projects/<slug-hash>/... per-project store
|
|
10
|
+
* <dshHome>/evolve/local/<sessionId>/... per-session staging store
|
|
10
11
|
*
|
|
11
12
|
* Snapshot discipline is code-enforced: before any mutating apply, the
|
|
12
13
|
* pre-apply state is copied to `snapshots/<refinementId>.json`. The model has
|
|
@@ -14,10 +15,17 @@
|
|
|
14
15
|
*/
|
|
15
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
17
|
import { join } from "node:path";
|
|
18
|
+
import { sanitizeProjectKey } from "./project.js";
|
|
17
19
|
import { stateFilePath } from "./state.js";
|
|
18
20
|
export const EVOLVE_DIR = "evolve";
|
|
21
|
+
/**
|
|
22
|
+
* Resolve store paths for a scope. For `project` the `sessionId` parameter
|
|
23
|
+
* carries the project key (see {@link resolveProjectKey} in project.ts) —
|
|
24
|
+
* it is sanitized to a single path segment so an externally supplied key
|
|
25
|
+
* can never traverse out of `evolve/projects/`.
|
|
26
|
+
*/
|
|
19
27
|
export function storePaths(baseDir, scope, sessionId) {
|
|
20
|
-
const scopeDir = scope === "global" ? "global" : join("local", sessionId ?? "anonymous");
|
|
28
|
+
const scopeDir = scope === "global" ? "global" : scope === "project" ? join("projects", sanitizeProjectKey(sessionId ?? "project")) : join("local", sessionId ?? "anonymous");
|
|
21
29
|
const stateDir = join(baseDir, EVOLVE_DIR, scopeDir);
|
|
22
30
|
return {
|
|
23
31
|
stateDir,
|
package/lib/tool.d.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
import type { Context } from "@deepseek-ai/cordis";
|
|
7
7
|
import type { HarnessScope } from "./types.js";
|
|
8
8
|
import type { EvolutionEngine } from "./service.js";
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Accept the string form (`scope: "project"`) with the legacy boolean tool
|
|
11
|
+
* parameter (`global: true`) as fallback. `scope` wins when both are given.
|
|
12
|
+
*/
|
|
10
13
|
export declare function scopeOf(value: unknown, fallback: HarnessScope): HarnessScope;
|
|
11
14
|
export interface ToolGateOptions {
|
|
12
15
|
requireGlobalApproval: boolean;
|
package/lib/tool.js
CHANGED
|
@@ -1,31 +1,68 @@
|
|
|
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
|
-
const SCOPES = ["local", "global"];
|
|
9
|
-
/**
|
|
10
|
+
const SCOPES = ["local", "project", "global"];
|
|
11
|
+
/**
|
|
12
|
+
* Accept the string form (`scope: "project"`) with the legacy boolean tool
|
|
13
|
+
* parameter (`global: true`) as fallback. `scope` wins when both are given.
|
|
14
|
+
*/
|
|
10
15
|
export function scopeOf(value, fallback) {
|
|
11
|
-
|
|
16
|
+
if (value === "global" || value === true) {
|
|
17
|
+
return "global";
|
|
18
|
+
}
|
|
19
|
+
if (value === "project") {
|
|
20
|
+
return "project";
|
|
21
|
+
}
|
|
22
|
+
return fallback;
|
|
12
23
|
}
|
|
13
24
|
/** The calling agent's session id; tools always run inside an agent scope. */
|
|
14
25
|
function sessionIdOf(exec) {
|
|
15
26
|
return exec.agent?.id;
|
|
16
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The store id a scope reads/writes: session id for local, derived project
|
|
30
|
+
* key for project, undefined for global. Throws for project when the
|
|
31
|
+
* session cwd is unavailable — fail loud instead of writing local-by-mistake.
|
|
32
|
+
*/
|
|
33
|
+
function storeIdFor(scope, exec) {
|
|
34
|
+
if (scope === "local") {
|
|
35
|
+
return sessionIdOf(exec);
|
|
36
|
+
}
|
|
37
|
+
if (scope === "project") {
|
|
38
|
+
const key = projectKeyOf(exec.agent);
|
|
39
|
+
if (!key) {
|
|
40
|
+
throw new Error("project scope needs the session cwd (unavailable for this agent) — use local or global instead");
|
|
41
|
+
}
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Human-approval gate shared by global and project writes (both cross-session). */
|
|
47
|
+
function needsApproval(scope) {
|
|
48
|
+
return scope === "global" || scope === "project";
|
|
49
|
+
}
|
|
50
|
+
/** Store label used in approval prompts. */
|
|
51
|
+
function storeLabel(scope) {
|
|
52
|
+
return scope === "project" ? "本项目跨会话 store" : "跨会话全局 store";
|
|
53
|
+
}
|
|
17
54
|
function textResult(text) {
|
|
18
55
|
return { text };
|
|
19
56
|
}
|
|
20
57
|
export function registerEvolveTools(ctx, engine, opts) {
|
|
21
58
|
ctx.tools.register(defineTool({
|
|
22
59
|
name: "evolve_list",
|
|
23
|
-
description: "List the continual harness state (prompt notes, memories, skills, subagent specs) for the current session (local) or across
|
|
60
|
+
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
61
|
parameters: {
|
|
25
62
|
scope: {
|
|
26
63
|
type: "string",
|
|
27
64
|
enum: SCOPES,
|
|
28
|
-
description: "Which store to list: 'local' (default) or 'global'.",
|
|
65
|
+
description: "Which store to list: 'local' (default), 'project', or 'global'.",
|
|
29
66
|
},
|
|
30
67
|
},
|
|
31
68
|
output: {
|
|
@@ -34,7 +71,7 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
34
71
|
},
|
|
35
72
|
execute: async (args, exec) => {
|
|
36
73
|
const scope = scopeOf(args.scope, "local");
|
|
37
|
-
const state = engine.load(scope,
|
|
74
|
+
const state = engine.load(scope, storeIdFor(scope, exec));
|
|
38
75
|
const text = formatHarnessStateForPrompt(state);
|
|
39
76
|
// Append injection usage counts (gap B1).
|
|
40
77
|
const usage = loadUsage(engine.baseDir);
|
|
@@ -64,6 +101,8 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
64
101
|
skill_kind: { type: "string", enum: ["executable", "guidance"], description: "For skills: executable (python reference, default) or guidance (SKILL.md document, no reference)." },
|
|
65
102
|
reference: { type: "object", additionalProperties: true, description: "For executable skills: {type:'python', import, callable}." },
|
|
66
103
|
arguments: { type: "object", additionalProperties: true, description: "For executable skills: accepted input contract." },
|
|
104
|
+
memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Required for memory: one fact per entry (feedback/project must carry Why + How to apply)." },
|
|
105
|
+
scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
|
|
67
106
|
global: { type: "boolean", description: "Set true to write the cross-session store (requires human approval; only for durable, reusable lessons)." },
|
|
68
107
|
},
|
|
69
108
|
output: {
|
|
@@ -71,15 +110,18 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
71
110
|
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
72
111
|
},
|
|
73
112
|
execute: async (args, exec) => {
|
|
74
|
-
const scope = scopeOf(args.global, "local");
|
|
75
|
-
|
|
113
|
+
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
114
|
+
// Resolve first: a project scope without a session cwd fails
|
|
115
|
+
// here, before any human approval question is asked.
|
|
116
|
+
const storeId = storeIdFor(scope, exec);
|
|
117
|
+
if (needsApproval(scope) && opts.requireGlobalApproval) {
|
|
76
118
|
// Informed approval: surface a similarity hit against the
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
const hit = mostSimilarEntry(Object.values(
|
|
119
|
+
// target store BEFORE the human decides — the engine's
|
|
120
|
+
// write-time guard still has the final say.
|
|
121
|
+
const targetState = engine.load(scope, storeId);
|
|
122
|
+
const hit = mostSimilarEntry(Object.values(targetState.entries[args.kind]), args.title ?? "", args.content ?? "", CONFLICT_WARN_SCORE);
|
|
81
123
|
const conflictNote = hit ? ` ⚠️ ${buildConflictNotice(hit)}——建议改用 evolve_update` : "";
|
|
82
|
-
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" →
|
|
124
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → ${storeLabel(scope)}${conflictNote}`);
|
|
83
125
|
}
|
|
84
126
|
const edit = {
|
|
85
127
|
action: "create",
|
|
@@ -95,7 +137,10 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
95
137
|
edit.reference = args.reference;
|
|
96
138
|
if (args.arguments !== undefined)
|
|
97
139
|
edit.arguments = args.arguments;
|
|
98
|
-
|
|
140
|
+
if (args.kind === "memory" && args.memoryType !== undefined) {
|
|
141
|
+
edit.metadata = { [MEMORY_TYPE_KEY]: args.memoryType };
|
|
142
|
+
}
|
|
143
|
+
return textResult(applyEditsText(engine, scope, storeId, [edit], exec.agent, sessionIdOf(exec)));
|
|
99
144
|
},
|
|
100
145
|
}));
|
|
101
146
|
ctx.tools.register(defineTool({
|
|
@@ -106,6 +151,8 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
106
151
|
id: { type: "string", required: true, description: "Existing entry id." },
|
|
107
152
|
title: { type: "string" },
|
|
108
153
|
content: { type: "string" },
|
|
154
|
+
memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "For memory: (re)classify the entry's recall type." },
|
|
155
|
+
scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
|
|
109
156
|
global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
|
|
110
157
|
},
|
|
111
158
|
output: {
|
|
@@ -113,16 +160,19 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
113
160
|
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
114
161
|
},
|
|
115
162
|
execute: async (args, exec) => {
|
|
116
|
-
const scope = scopeOf(args.global, "local");
|
|
117
|
-
|
|
118
|
-
|
|
163
|
+
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
164
|
+
const storeId = storeIdFor(scope, exec);
|
|
165
|
+
if (needsApproval(scope) && opts.requireGlobalApproval) {
|
|
166
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_update ${args.kind}:${args.id} → ${storeLabel(scope)}`);
|
|
119
167
|
}
|
|
120
168
|
const edit = { action: "update", kind: args.kind, id: args.id };
|
|
121
169
|
if (args.title !== undefined)
|
|
122
170
|
edit.title = args.title;
|
|
123
171
|
if (args.content !== undefined)
|
|
124
172
|
edit.content = args.content;
|
|
125
|
-
|
|
173
|
+
if (args.memoryType !== undefined)
|
|
174
|
+
edit.metadata = { [MEMORY_TYPE_KEY]: args.memoryType };
|
|
175
|
+
return textResult(applyEditsText(engine, scope, storeId, [edit], exec.agent, sessionIdOf(exec)));
|
|
126
176
|
},
|
|
127
177
|
}));
|
|
128
178
|
ctx.tools.register(defineTool({
|
|
@@ -131,6 +181,7 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
131
181
|
parameters: {
|
|
132
182
|
kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
|
|
133
183
|
id: { type: "string", required: true },
|
|
184
|
+
scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
|
|
134
185
|
global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
|
|
135
186
|
},
|
|
136
187
|
output: {
|
|
@@ -138,12 +189,13 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
138
189
|
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
139
190
|
},
|
|
140
191
|
execute: async (args, exec) => {
|
|
141
|
-
const scope = scopeOf(args.global, "local");
|
|
142
|
-
|
|
143
|
-
|
|
192
|
+
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
193
|
+
const storeId = storeIdFor(scope, exec);
|
|
194
|
+
if (needsApproval(scope) && opts.requireGlobalApproval) {
|
|
195
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${args.id} → ${storeLabel(scope)}`);
|
|
144
196
|
}
|
|
145
197
|
const edit = { action: "delete", kind: args.kind, id: args.id };
|
|
146
|
-
return textResult(applyEditsText(engine, scope,
|
|
198
|
+
return textResult(applyEditsText(engine, scope, storeId, [edit], exec.agent, sessionIdOf(exec)));
|
|
147
199
|
},
|
|
148
200
|
}));
|
|
149
201
|
ctx.tools.register(defineTool({
|
|
@@ -151,6 +203,7 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
151
203
|
description: "Deterministically revert a previous refinement by its id (from evolve_list history or the /evolve command).",
|
|
152
204
|
parameters: {
|
|
153
205
|
refinementId: { type: "string", required: true, description: "The refinement id to roll back." },
|
|
206
|
+
scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
|
|
154
207
|
global: { type: "boolean", description: "Set true to roll back a cross-session refinement." },
|
|
155
208
|
},
|
|
156
209
|
output: {
|
|
@@ -158,14 +211,14 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
158
211
|
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
159
212
|
},
|
|
160
213
|
execute: async (args, exec) => {
|
|
161
|
-
const scope = scopeOf(args.global, "local");
|
|
162
|
-
const result = engine.rollback(scope,
|
|
214
|
+
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
215
|
+
const result = engine.rollback(scope, storeIdFor(scope, exec), args.refinementId);
|
|
163
216
|
return textResult(`Rolled back ${result.rollbackOf ?? result.id}: ${result.appliedEdits.filter((e) => e.applied).length} edit(s) reverted.`);
|
|
164
217
|
},
|
|
165
218
|
}));
|
|
166
219
|
}
|
|
167
|
-
function applyEditsText(engine, scope,
|
|
168
|
-
const result = engine.apply(scope,
|
|
220
|
+
function applyEditsText(engine, scope, storeId, edits, agent, eventSessionId) {
|
|
221
|
+
const result = engine.apply(scope, storeId, {
|
|
169
222
|
summary: "Direct tool edit",
|
|
170
223
|
rationale: "Model-invoked single edit via evolve_* tool.",
|
|
171
224
|
expectedOutcome: "Entry is created, updated, or deleted as requested.",
|
|
@@ -173,14 +226,16 @@ function applyEditsText(engine, scope, sessionId, edits, agent) {
|
|
|
173
226
|
}, agent
|
|
174
227
|
? {
|
|
175
228
|
scope,
|
|
176
|
-
...(entrySourceOf(agent,
|
|
229
|
+
...(entrySourceOf(agent, eventSessionId ?? storeId) ? { source: entrySourceOf(agent, eventSessionId ?? storeId) } : {}),
|
|
177
230
|
}
|
|
178
231
|
: { scope });
|
|
179
232
|
const applied = result.appliedEdits.filter((e) => e.applied);
|
|
180
233
|
const failed = result.appliedEdits.filter((e) => !e.applied);
|
|
181
234
|
// Gap C4: emit structured evolve_complete event for third-party consumers.
|
|
182
|
-
|
|
183
|
-
|
|
235
|
+
// The event keys on the live session (not the project store key).
|
|
236
|
+
const eventSession = eventSessionId ?? storeId;
|
|
237
|
+
if (applied.length > 0 && eventSession) {
|
|
238
|
+
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", eventSession));
|
|
184
239
|
}
|
|
185
240
|
const lines = [`refinement ${result.id}: ${applied.length} applied, ${failed.length} failed`];
|
|
186
241
|
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
|
@@ -34,7 +34,7 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
|
|
|
34
34
|
splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
|
-
const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy);
|
|
37
|
+
const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
|
|
38
38
|
if (blocked) {
|
|
39
39
|
splitSkipped.push({ key: item.key, reason: blocked });
|
|
40
40
|
continue;
|
package/lib/wrapup.d.ts
CHANGED
|
@@ -199,7 +199,7 @@ export declare function splitArchiveGuards(items: readonly WrapupItem[], candida
|
|
|
199
199
|
* too thin, no near-duplicate global entry. A blocked split is dropped (the
|
|
200
200
|
* entry still archives plain) rather than half-promoting a redundancy.
|
|
201
201
|
*/
|
|
202
|
-
export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind, policy?: PromotionPolicy): string | undefined;
|
|
202
|
+
export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind, policy?: PromotionPolicy, candidateMetadata?: Record<string, unknown>): string | undefined;
|
|
203
203
|
/**
|
|
204
204
|
* Shared proposal builders for a WHOLE promotion — used by both the
|
|
205
205
|
* `/evolve wrapup` command and the gate's local-fate dimension so the two
|
|
@@ -232,7 +232,7 @@ export declare function splitPromoteProposals(item: WrapupItem, candidate: Wrapu
|
|
|
232
232
|
* injection). Undefined when nothing applies.
|
|
233
233
|
*/
|
|
234
234
|
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\".";
|
|
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- 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
236
|
export interface AssessOptions {
|
|
237
237
|
/** Output token budget for the assessment call. */
|
|
238
238
|
maxOutputTokens?: number;
|
package/lib/wrapup.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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";
|
|
@@ -190,6 +191,13 @@ export function filterPromotable(items, globalState, candidates, policy = DEFAUL
|
|
|
190
191
|
skipped.push({ key: item.key, reason: "already covered globally" });
|
|
191
192
|
continue;
|
|
192
193
|
}
|
|
194
|
+
if (candidate.kind === "memory" && !isMemoryType(candidate.metadata[MEMORY_TYPE_KEY])) {
|
|
195
|
+
skipped.push({
|
|
196
|
+
key: item.key,
|
|
197
|
+
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`,
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
193
201
|
const scoped = projectScopedReason(`${candidate.title}\n${candidate.content}`, policy);
|
|
194
202
|
if (scoped) {
|
|
195
203
|
skipped.push({ key: item.key, reason: scoped });
|
|
@@ -271,9 +279,12 @@ export function splitArchiveGuards(items, candidates) {
|
|
|
271
279
|
* too thin, no near-duplicate global entry. A blocked split is dropped (the
|
|
272
280
|
* entry still archives plain) rather than half-promoting a redundancy.
|
|
273
281
|
*/
|
|
274
|
-
export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY) {
|
|
282
|
+
export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY, candidateMetadata) {
|
|
275
283
|
if (!item.promote)
|
|
276
284
|
return "no split payload";
|
|
285
|
+
if (kind === "memory" && !isMemoryType(candidateMetadata?.[MEMORY_TYPE_KEY])) {
|
|
286
|
+
return `split promotion needs the source entry classified (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update it first`;
|
|
287
|
+
}
|
|
277
288
|
if (globalCoverageDetected(globalState, kind, { id: "", title: item.promote.title })) {
|
|
278
289
|
return "split promotion duplicates a globally covered topic";
|
|
279
290
|
}
|
|
@@ -454,6 +465,9 @@ Rules:
|
|
|
454
465
|
usage (e.g. a safety policy that rarely triggers but is critical).
|
|
455
466
|
- Do not promote local task state, work-in-progress notes, or content tied to
|
|
456
467
|
one session's ephemeral details.
|
|
468
|
+
- Memory entries carry a recall type (user/feedback/project/reference): a
|
|
469
|
+
memory WITHOUT one cannot be promoted (the guard skips it) — verdict such
|
|
470
|
+
entries "keep" (or "archive" when they qualify) rather than "promote".
|
|
457
471
|
- Skills: only "promote" a skill entry that is a genuinely reusable procedure
|
|
458
472
|
meeting the DSH skill quality standard; one-off workflows are "archive" or
|
|
459
473
|
"keep".
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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
6
|
"repository": {
|
|
@@ -57,19 +57,19 @@
|
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
60
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6",
|
|
61
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
62
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
|
|
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
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
|
-
"@deepseek-ai/cordis": "
|
|
66
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
67
|
-
"@deepseek-ai/dsh-commands": "0.1.
|
|
68
|
-
"@deepseek-ai/dsh-home-paths": "0.1.
|
|
69
|
-
"@deepseek-ai/dsh-llm": "0.1.
|
|
70
|
-
"@deepseek-ai/dsh-
|
|
71
|
-
"@deepseek-ai/
|
|
72
|
-
"@deepseek-ai/schemastery": "^3.18.1",
|
|
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
|
+
"@deepseek-ai/schemastery": "3.18.3",
|
|
73
73
|
"@types/node": "^22.10.0",
|
|
74
74
|
"@vitest/coverage-v8": "^3.2.0",
|
|
75
75
|
"oxlint": "^0.16.0",
|