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/inject.d.ts
CHANGED
|
@@ -52,13 +52,26 @@ export interface AgentLike {
|
|
|
52
52
|
parentSession?: string;
|
|
53
53
|
};
|
|
54
54
|
/**
|
|
55
|
-
* Live append-only session log
|
|
56
|
-
* `
|
|
57
|
-
*
|
|
55
|
+
* Live append-only session log as the public `session.events` getter.
|
|
56
|
+
* Typed loosely (`unknown[]`) so the real `SessionEvent[]` union from
|
|
57
|
+
* dsh-session is assignable; rows are narrowed to
|
|
58
|
+
* {@link UserMessageEventLike} at read time.
|
|
58
59
|
*/
|
|
59
60
|
events?: readonly unknown[];
|
|
61
|
+
/**
|
|
62
|
+
* Full-log snapshot reader that answers the same rows in the same order
|
|
63
|
+
* where the `events` getter is absent. Called with no arguments for the
|
|
64
|
+
* whole log, which is the read {@link sessionEventsOf} needs.
|
|
65
|
+
*/
|
|
66
|
+
snapshotEvents?: () => readonly unknown[];
|
|
60
67
|
};
|
|
61
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The session's event log through whichever reader the running harness
|
|
71
|
+
* generation exposes. Returns [] when neither is present, so every caller
|
|
72
|
+
* degrades to its empty-log path rather than throwing.
|
|
73
|
+
*/
|
|
74
|
+
export declare function sessionEventsOf(agent: AgentLike | undefined): readonly unknown[];
|
|
62
75
|
/** The section-provider context shape we consume (subset of AssembleContext). */
|
|
63
76
|
export interface InjectContext {
|
|
64
77
|
agent?: AgentLike;
|
|
@@ -101,20 +114,42 @@ export declare function formatPromptEntriesSection(entries: readonly HarnessEntr
|
|
|
101
114
|
export declare function formatSubagentSpecsSection(entries: readonly HarnessEntry[], query?: string): string;
|
|
102
115
|
/**
|
|
103
116
|
* Gap B3: a lightweight directory of ALL non-archived entries across all
|
|
104
|
-
* kinds — one line per entry
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
117
|
+
* kinds — one line per entry, no content. Memory lines carry their recall
|
|
118
|
+
* type (`- [memory:feedback:id] title`) so the model can judge relevance
|
|
119
|
+
* from the index alone. This gives the model a zero-cost overview of what
|
|
120
|
+
* exists so it can ask for full text via `evolve_list` or `/evolve list`.
|
|
121
|
+
* The directory is appended after the curated top-N injection sections and
|
|
122
|
+
* adds minimal tokens.
|
|
108
123
|
*
|
|
109
124
|
* 2026-08-22 throttle: the directory is CAPPED at {@link DEFAULT_DIRECTORY_LINES}
|
|
110
|
-
* lines
|
|
111
|
-
*
|
|
112
|
-
*
|
|
125
|
+
* lines with the remainder folded into a single counter line — an uncapped
|
|
126
|
+
* directory across a polluted global store was measured at ~2K chars of
|
|
127
|
+
* every build in every project.
|
|
128
|
+
*
|
|
129
|
+
* 2026-09-23 relevance order: the directory is sorted by
|
|
130
|
+
* {@link rankEntries} against the assembly's relevance query, so the lines
|
|
131
|
+
* folded below the cap are the least relevant — not the dictionary tail.
|
|
113
132
|
*/
|
|
114
133
|
export declare const DEFAULT_DIRECTORY_LINES = 15;
|
|
134
|
+
/** One directory index line: memory lines carry their recall-type hook. */
|
|
135
|
+
export declare function directoryLine(entry: HarnessEntry): string;
|
|
115
136
|
export declare function formatEntriesDirectory(...kindEntries: readonly HarnessEntry[][]): string;
|
|
116
137
|
/** {@link formatEntriesDirectory} with an explicit cap (configurable). */
|
|
117
138
|
export declare function formatEntriesDirectoryCapped(maxLines: number, ...kindEntries: readonly HarnessEntry[][]): string;
|
|
139
|
+
/**
|
|
140
|
+
* {@link formatEntriesDirectoryCapped} with a relevance query: entries are
|
|
141
|
+
* ordered by {@link rankEntries} (relevance first, then recency) so the cap
|
|
142
|
+
* folds the least relevant entries — never the dictionary tail. An empty
|
|
143
|
+
* query degrades to the valence/recency/stable order, still deterministic.
|
|
144
|
+
*/
|
|
145
|
+
export declare function formatEntriesDirectoryRanked(maxLines: number, query: string | undefined, ...kindEntries: readonly HarnessEntry[][]): string;
|
|
146
|
+
/**
|
|
147
|
+
* The directory entries actually shown under the cap, in display order —
|
|
148
|
+
* the single source for both the rendered text and usage accounting (so a
|
|
149
|
+
* memory's injection IS its directory line even with the `:type` hook).
|
|
150
|
+
* Returns [] when the directory is suppressed or empty.
|
|
151
|
+
*/
|
|
152
|
+
export declare function rankedDirectoryEntries(maxLines: number, query: string | undefined, ...kindEntries: readonly HarnessEntry[][]): HarnessEntry[];
|
|
118
153
|
/**
|
|
119
154
|
* Walk the parent-session chain from `agent` upward and return the nearest
|
|
120
155
|
* session whose local store is non-empty, if any. Children inherit their
|
|
@@ -125,19 +160,23 @@ export declare function formatEntriesDirectoryCapped(maxLines: number, ...kindEn
|
|
|
125
160
|
export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, agent: AgentLike): HarnessState | undefined;
|
|
126
161
|
/**
|
|
127
162
|
* Compose the full injected block for one assembling agent: global entries
|
|
128
|
-
* merged with
|
|
163
|
+
* merged with this project's store (when the session cwd resolves one) and
|
|
164
|
+
* the nearest carrying local store (precedence global < project < local).
|
|
129
165
|
* The optional `query` — when absent, derived from the agent's most recent
|
|
130
166
|
* direct user messages — ranks which entries fill the per-kind cap
|
|
131
167
|
* (relevance first, then recency; see {@link rankEntries}). Returns "" when
|
|
132
168
|
* nothing is injectable — the prompt renderer then drops the section, so an
|
|
133
169
|
* empty store adds zero tokens to every assembly.
|
|
134
170
|
*
|
|
135
|
-
* `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle)
|
|
171
|
+
* `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle);
|
|
172
|
+
* `opts.projectKey` pins the project layer explicitly (tests, tools) —
|
|
173
|
+
* otherwise it is derived from the agent's session cwd, best-effort.
|
|
136
174
|
* Usage recording covers ALL kinds — memories and skills appear as directory
|
|
137
175
|
* lines, prompts/subagents as content — and is deduped per session so the
|
|
138
176
|
* counts read "how many sessions saw this", not "how many prompt builds".
|
|
139
177
|
*/
|
|
140
178
|
export declare function entriesSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, query?: string, opts?: {
|
|
141
179
|
directoryLines?: number;
|
|
180
|
+
projectKey?: string;
|
|
142
181
|
}): string;
|
|
143
182
|
//# sourceMappingURL=inject.d.ts.map
|
package/lib/inject.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { isArchived, VALENCE_NEGATIVE_KEY } from "./types.js";
|
|
1
|
+
import { MEMORY_TYPE_KEY, isArchived, isMemoryType, VALENCE_NEGATIVE_KEY } from "./types.js";
|
|
2
2
|
import { mergeHarnessStates } from "./state.js";
|
|
3
|
+
import { projectKeyOf } from "./project.js";
|
|
3
4
|
import { entryLine } from "./render.js";
|
|
4
5
|
import { recordInjection } from "./usage.js";
|
|
5
6
|
import { buildRelevanceIndex, relevanceScore, tokenize } from "./search.js";
|
|
@@ -17,6 +18,22 @@ export const RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
17
18
|
export const MAX_QUERY_MESSAGES = 3;
|
|
18
19
|
/** Query text handed to the relevance scorer is capped at this many chars. */
|
|
19
20
|
export const MAX_QUERY_CHARS = 400;
|
|
21
|
+
/**
|
|
22
|
+
* The session's event log through whichever reader the running harness
|
|
23
|
+
* generation exposes. Returns [] when neither is present, so every caller
|
|
24
|
+
* degrades to its empty-log path rather than throwing.
|
|
25
|
+
*/
|
|
26
|
+
export function sessionEventsOf(agent) {
|
|
27
|
+
const session = agent?.session;
|
|
28
|
+
if (session === undefined) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(session.events)) {
|
|
32
|
+
return session.events;
|
|
33
|
+
}
|
|
34
|
+
const snapshot = session.snapshotEvents?.();
|
|
35
|
+
return Array.isArray(snapshot) ? snapshot : [];
|
|
36
|
+
}
|
|
20
37
|
/** Stable dictionary-order tiebreak used when two entries score equally. */
|
|
21
38
|
function stableCompare(a, b) {
|
|
22
39
|
return [a.path, a.title, a.id].join("\0").localeCompare([b.path, b.title, b.id].join("\0"));
|
|
@@ -131,8 +148,8 @@ function extractBlockText(content) {
|
|
|
131
148
|
* falls back to pure recency.
|
|
132
149
|
*/
|
|
133
150
|
export function recentUserText(agent, opts) {
|
|
134
|
-
const events = agent
|
|
135
|
-
if (
|
|
151
|
+
const events = sessionEventsOf(agent);
|
|
152
|
+
if (events.length === 0) {
|
|
136
153
|
return "";
|
|
137
154
|
}
|
|
138
155
|
const maxMessages = opts?.maxMessages ?? MAX_QUERY_MESSAGES;
|
|
@@ -198,27 +215,81 @@ export function formatSubagentSpecsSection(entries, query) {
|
|
|
198
215
|
}
|
|
199
216
|
/**
|
|
200
217
|
* Gap B3: a lightweight directory of ALL non-archived entries across all
|
|
201
|
-
* kinds — one line per entry
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
218
|
+
* kinds — one line per entry, no content. Memory lines carry their recall
|
|
219
|
+
* type (`- [memory:feedback:id] title`) so the model can judge relevance
|
|
220
|
+
* from the index alone. This gives the model a zero-cost overview of what
|
|
221
|
+
* exists so it can ask for full text via `evolve_list` or `/evolve list`.
|
|
222
|
+
* The directory is appended after the curated top-N injection sections and
|
|
223
|
+
* adds minimal tokens.
|
|
205
224
|
*
|
|
206
225
|
* 2026-08-22 throttle: the directory is CAPPED at {@link DEFAULT_DIRECTORY_LINES}
|
|
207
|
-
* lines
|
|
208
|
-
*
|
|
209
|
-
*
|
|
226
|
+
* lines with the remainder folded into a single counter line — an uncapped
|
|
227
|
+
* directory across a polluted global store was measured at ~2K chars of
|
|
228
|
+
* every build in every project.
|
|
229
|
+
*
|
|
230
|
+
* 2026-09-23 relevance order: the directory is sorted by
|
|
231
|
+
* {@link rankEntries} against the assembly's relevance query, so the lines
|
|
232
|
+
* folded below the cap are the least relevant — not the dictionary tail.
|
|
210
233
|
*/
|
|
211
234
|
export const DEFAULT_DIRECTORY_LINES = 15;
|
|
212
|
-
/**
|
|
235
|
+
/** One directory index line: memory lines carry their recall-type hook. */
|
|
236
|
+
export function directoryLine(entry) {
|
|
237
|
+
if (entry.kind === "memory") {
|
|
238
|
+
const type = entry.metadata[MEMORY_TYPE_KEY];
|
|
239
|
+
const hook = isMemoryType(type) ? `:${type}` : "";
|
|
240
|
+
return `- [memory${hook}:${entry.id}] ${entry.title}`;
|
|
241
|
+
}
|
|
242
|
+
return `- [${entry.kind}:${entry.id}] ${entry.title}`;
|
|
243
|
+
}
|
|
244
|
+
/** Positional contract for the directory variadics: the first two arrays
|
|
245
|
+
* must be the content-section kinds (prompt, subagent) — the redundancy
|
|
246
|
+
* check counts exactly those as already visible. Callers pass
|
|
247
|
+
* (prompt, subagent, memory, skill); any other order miscounts. */
|
|
213
248
|
const CONTENT_SECTION_KINDS = 2;
|
|
214
249
|
export function formatEntriesDirectory(...kindEntries) {
|
|
215
250
|
return formatEntriesDirectoryCapped(DEFAULT_DIRECTORY_LINES, ...kindEntries);
|
|
216
251
|
}
|
|
217
252
|
/** {@link formatEntriesDirectory} with an explicit cap (configurable). */
|
|
218
253
|
export function formatEntriesDirectoryCapped(maxLines, ...kindEntries) {
|
|
254
|
+
return formatEntriesDirectoryRanked(maxLines, undefined, ...kindEntries);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* {@link formatEntriesDirectoryCapped} with a relevance query: entries are
|
|
258
|
+
* ordered by {@link rankEntries} (relevance first, then recency) so the cap
|
|
259
|
+
* folds the least relevant entries — never the dictionary tail. An empty
|
|
260
|
+
* query degrades to the valence/recency/stable order, still deterministic.
|
|
261
|
+
*/
|
|
262
|
+
export function formatEntriesDirectoryRanked(maxLines, query, ...kindEntries) {
|
|
263
|
+
const total = kindEntries.flat().filter((e) => !isArchived(e)).length;
|
|
264
|
+
if (total === 0) {
|
|
265
|
+
return "";
|
|
266
|
+
}
|
|
267
|
+
// Suppressed (every entry already content-visible) renders as "" — the
|
|
268
|
+
// prompt renderer then drops the section entirely.
|
|
269
|
+
const shown = rankedDirectoryEntries(maxLines, query, ...kindEntries);
|
|
270
|
+
if (shown.length === 0) {
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
const lines = ["# Continual Harness — Entry Directory", "All entries (use evolve_list for full text of any entry):"];
|
|
274
|
+
for (const entry of shown) {
|
|
275
|
+
lines.push(directoryLine(entry));
|
|
276
|
+
}
|
|
277
|
+
const hidden = total - shown.length;
|
|
278
|
+
if (hidden > 0) {
|
|
279
|
+
lines.push(`- …and ${hidden} more entries (evolve_list for the full index)`);
|
|
280
|
+
}
|
|
281
|
+
return lines.join("\n");
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The directory entries actually shown under the cap, in display order —
|
|
285
|
+
* the single source for both the rendered text and usage accounting (so a
|
|
286
|
+
* memory's injection IS its directory line even with the `:type` hook).
|
|
287
|
+
* Returns [] when the directory is suppressed or empty.
|
|
288
|
+
*/
|
|
289
|
+
export function rankedDirectoryEntries(maxLines, query, ...kindEntries) {
|
|
219
290
|
const allEntries = kindEntries.flat().filter((e) => !isArchived(e));
|
|
220
291
|
if (allEntries.length === 0) {
|
|
221
|
-
return
|
|
292
|
+
return [];
|
|
222
293
|
}
|
|
223
294
|
// Skip the directory only when EVERY entry is already content-visible.
|
|
224
295
|
// Only the first two arrays (prompt, subagent) have curated sections —
|
|
@@ -229,19 +300,9 @@ export function formatEntriesDirectoryCapped(maxLines, ...kindEntries) {
|
|
|
229
300
|
.slice(0, CONTENT_SECTION_KINDS)
|
|
230
301
|
.reduce((sum, entries) => sum + Math.min(entries.filter((e) => !isArchived(e)).length, MAX_INJECTED_ENTRIES_PER_KIND), 0);
|
|
231
302
|
if (allEntries.length <= contentVisible) {
|
|
232
|
-
return
|
|
303
|
+
return [];
|
|
233
304
|
}
|
|
234
|
-
|
|
235
|
-
const lines = ["# Continual Harness — Entry Directory", "All entries (use evolve_list for full text of any entry):"];
|
|
236
|
-
const shown = sorted.slice(0, Math.max(maxLines, 1));
|
|
237
|
-
for (const entry of shown) {
|
|
238
|
-
lines.push(`- [${entry.kind}:${entry.id}] ${entry.title}`);
|
|
239
|
-
}
|
|
240
|
-
const hidden = sorted.length - shown.length;
|
|
241
|
-
if (hidden > 0) {
|
|
242
|
-
lines.push(`- …and ${hidden} more entries (evolve_list for the full index)`);
|
|
243
|
-
}
|
|
244
|
-
return lines.join("\n");
|
|
305
|
+
return rankEntries(allEntries, query).slice(0, Math.max(maxLines, 1));
|
|
245
306
|
}
|
|
246
307
|
/**
|
|
247
308
|
* Walk the parent-session chain from `agent` upward and return the nearest
|
|
@@ -265,14 +326,17 @@ export function nearestLocalStateWithEntries(engine, agent) {
|
|
|
265
326
|
}
|
|
266
327
|
/**
|
|
267
328
|
* Compose the full injected block for one assembling agent: global entries
|
|
268
|
-
* merged with
|
|
329
|
+
* merged with this project's store (when the session cwd resolves one) and
|
|
330
|
+
* the nearest carrying local store (precedence global < project < local).
|
|
269
331
|
* The optional `query` — when absent, derived from the agent's most recent
|
|
270
332
|
* direct user messages — ranks which entries fill the per-kind cap
|
|
271
333
|
* (relevance first, then recency; see {@link rankEntries}). Returns "" when
|
|
272
334
|
* nothing is injectable — the prompt renderer then drops the section, so an
|
|
273
335
|
* empty store adds zero tokens to every assembly.
|
|
274
336
|
*
|
|
275
|
-
* `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle)
|
|
337
|
+
* `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle);
|
|
338
|
+
* `opts.projectKey` pins the project layer explicitly (tests, tools) —
|
|
339
|
+
* otherwise it is derived from the agent's session cwd, best-effort.
|
|
276
340
|
* Usage recording covers ALL kinds — memories and skills appear as directory
|
|
277
341
|
* lines, prompts/subagents as content — and is deduped per session so the
|
|
278
342
|
* counts read "how many sessions saw this", not "how many prompt builds".
|
|
@@ -282,8 +346,10 @@ export function entriesSectionText(engine, agent, query, opts) {
|
|
|
282
346
|
return "";
|
|
283
347
|
}
|
|
284
348
|
const globalState = engine.load("global", undefined);
|
|
349
|
+
const projectKey = opts?.projectKey ?? projectKeyOf(agent);
|
|
350
|
+
const projectState = projectKey ? engine.load("project", projectKey) : undefined;
|
|
285
351
|
const localState = nearestLocalStateWithEntries(engine, agent);
|
|
286
|
-
const merged =
|
|
352
|
+
const merged = mergeHarnessStates(globalState, localState, projectState ? { projectState } : undefined);
|
|
287
353
|
const promptEntries = Object.values(merged.entries.prompt);
|
|
288
354
|
const subagentEntries = Object.values(merged.entries.subagent);
|
|
289
355
|
const relevanceQuery = (query ?? recentUserText(agent)).trim();
|
|
@@ -302,18 +368,18 @@ export function entriesSectionText(engine, agent, query, opts) {
|
|
|
302
368
|
}
|
|
303
369
|
// Gap B3: lightweight directory of ALL entries (id+title, one line each).
|
|
304
370
|
// Zero-cost index so the model knows what exists and can ask for full text.
|
|
305
|
-
const
|
|
371
|
+
const directoryLines = opts?.directoryLines ?? DEFAULT_DIRECTORY_LINES;
|
|
372
|
+
const promptKind = Object.values(merged.entries.prompt);
|
|
373
|
+
const subagentKind = Object.values(merged.entries.subagent);
|
|
374
|
+
const memoryKind = Object.values(merged.entries.memory);
|
|
375
|
+
const skillKind = Object.values(merged.entries.skill);
|
|
376
|
+
const directoryText = formatEntriesDirectoryRanked(directoryLines, relevanceQuery, promptKind, subagentKind, memoryKind, skillKind);
|
|
306
377
|
// Directory-visible keys count too: a memory's injection IS its directory
|
|
307
|
-
// line. Set semantics keep content-injected entries single-counted.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const key = `${kind}:${entry.id}`;
|
|
313
|
-
if (injectedKeys.has(key) || directoryText.includes(`[${key}]`)) {
|
|
314
|
-
injectedKeys.add(key);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
378
|
+
// line. Set semantics keep content-injected entries single-counted. Keys
|
|
379
|
+
// come from the single display-ordered source (type hooks included), not
|
|
380
|
+
// substring matching on the rendered text.
|
|
381
|
+
for (const entry of rankedDirectoryEntries(directoryLines, relevanceQuery, promptKind, subagentKind, memoryKind, skillKind)) {
|
|
382
|
+
injectedKeys.add(`${entry.kind}:${entry.id}`);
|
|
317
383
|
}
|
|
318
384
|
// Record usage durably (best-effort: failure never blocks injection).
|
|
319
385
|
// Deduped per session — see recordInjection.
|
package/lib/llm-text.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
|
|
2
3
|
/**
|
|
3
4
|
* Stream a single-turn text completion through `ctx.llm`. Forces
|
|
4
5
|
* `reasoningEffort: off` so the model spends its budget on the answer,
|
|
@@ -17,7 +18,7 @@ export async function streamText(ctx, opts) {
|
|
|
17
18
|
messages: [
|
|
18
19
|
createUserMessage({
|
|
19
20
|
content: [{ type: "text", text: opts.prompt }],
|
|
20
|
-
source:
|
|
21
|
+
source: EVOLVE_MESSAGE_SOURCE,
|
|
21
22
|
}),
|
|
22
23
|
],
|
|
23
24
|
reasoningEffort: ReasoningEffortId("off"),
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Producer declaration for every message this plugin injects.
|
|
3
|
+
*
|
|
4
|
+
* Harness message sources are a merge-extensible sum type: each producer
|
|
5
|
+
* declares its own `kind` in its own module, and dsh 0.1.7 removed the shared
|
|
6
|
+
* catch-all `kind: "plugin"`. Declaring our kind once, here, keeps every
|
|
7
|
+
* injection site checked against the host vocabulary instead of repeating a
|
|
8
|
+
* local literal that silently drifts when that vocabulary changes.
|
|
9
|
+
*/
|
|
10
|
+
declare module "@deepseek-ai/dsh-llm" {
|
|
11
|
+
interface MessageSourceMap {
|
|
12
|
+
"continual-evolve": EvolveMessageSource;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Source tag identifying dsh-continual-evolve as the message producer. */
|
|
16
|
+
export interface EvolveMessageSource {
|
|
17
|
+
kind: "continual-evolve";
|
|
18
|
+
}
|
|
19
|
+
/** Source value carried by every message this plugin injects. */
|
|
20
|
+
export declare const EVOLVE_MESSAGE_SOURCE: {
|
|
21
|
+
readonly kind: "continual-evolve";
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=message-source.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Producer declaration for every message this plugin injects.
|
|
3
|
+
*
|
|
4
|
+
* Harness message sources are a merge-extensible sum type: each producer
|
|
5
|
+
* declares its own `kind` in its own module, and dsh 0.1.7 removed the shared
|
|
6
|
+
* catch-all `kind: "plugin"`. Declaring our kind once, here, keeps every
|
|
7
|
+
* injection site checked against the host vocabulary instead of repeating a
|
|
8
|
+
* local literal that silently drifts when that vocabulary changes.
|
|
9
|
+
*/
|
|
10
|
+
/** Source value carried by every message this plugin injects. */
|
|
11
|
+
export const EVOLVE_MESSAGE_SOURCE = { kind: "continual-evolve" };
|
|
12
|
+
//# sourceMappingURL=message-source.js.map
|
package/lib/notify.d.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* module queues a short follow-up turn after an approved gate run so the
|
|
5
5
|
* user SEES what was persisted, how to inspect it, and how to roll it back.
|
|
6
6
|
*
|
|
7
|
-
* The notice is a
|
|
8
|
-
* rendered in the session transcript like any other input and the
|
|
9
|
-
* answers with a one-line confirmation. It never fakes tool or assistant
|
|
7
|
+
* The notice is a `continual-evolve`-sourced user message (`agent.followup`),
|
|
8
|
+
* so it is rendered in the session transcript like any other input and the
|
|
9
|
+
* agent answers with a one-line confirmation. It never fakes tool or assistant
|
|
10
10
|
* events, so session replay, the ordered surface, and derived history stay
|
|
11
|
-
* untouched: the notice is a plain `user/message`
|
|
11
|
+
* untouched: the notice is a plain `user/message` carrying this plugin's own
|
|
12
|
+
* source kind.
|
|
12
13
|
*
|
|
13
14
|
* Every mechanical property stays in code: the notice text is built from the
|
|
14
15
|
* applied refinement result, never from model text.
|
package/lib/notify.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
|
|
2
3
|
/**
|
|
3
4
|
* Compose the user-visible gate notice from an applied refinement result.
|
|
4
5
|
* Lists every successfully applied edit (kind + title + id) and the rollback
|
|
@@ -30,7 +31,7 @@ export function notifyAutoReview(ctx, agent, result, turnsSinceLastReview) {
|
|
|
30
31
|
try {
|
|
31
32
|
agent.followup(createUserMessage({
|
|
32
33
|
content: [{ type: "text", text: buildGateNotice(result, turnsSinceLastReview) }],
|
|
33
|
-
source:
|
|
34
|
+
source: EVOLVE_MESSAGE_SOURCE,
|
|
34
35
|
}));
|
|
35
36
|
}
|
|
36
37
|
catch (cause) {
|
package/lib/planner.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { Context } from "@deepseek-ai/cordis";
|
|
10
10
|
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
11
11
|
import type { HarnessState, RefinementProposal, RefinementResult } from "./types.js";
|
|
12
|
-
export declare const PLANNER_SYSTEM_PROMPT = "You are the /evolve continual harness subsystem.\n\nYour job is to improve the editable continual harness state. Instead of\nsummarizing the conversation you emit precise Create, Update, or Delete edits\nto reusable state: prompt notes, memories, skills, and subagent specs.\n\nRules:\n- The base system prompt is immutable and MUST NOT be rewritten (never edit id \"base_system_prompt\").\n- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.\n- prompt = narrow behavioral policy addendums; memory =
|
|
12
|
+
export declare const PLANNER_SYSTEM_PROMPT = "You are the /evolve continual harness subsystem.\n\nYour job is to improve the editable continual harness state. Instead of\nsummarizing the conversation you emit precise Create, Update, or Delete edits\nto reusable state: prompt notes, memories, skills, and subagent specs.\n\nRules:\n- The base system prompt is immutable and MUST NOT be rewritten (never edit id \"base_system_prompt\").\n- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.\n- prompt = narrow behavioral policy addendums; memory = ONE FACT per entry, typed via metadata.memoryType\n- (user = who the user is; feedback = a pitfall/correction or confirmed approach, MUST carry Why + How to apply;\n- project = work goal/constraint not derivable from the repo; reference = URL/dashboard/ticket pointer).\n- Memory rules: one fact per entry (title = one-line retrieval hook, content = the fact alone);\n- never save what the repo already records (code structure, past fixes, git history, AGENTS.md) or what only\n- matters to this conversation \u2014 save the non-obvious point instead; before creating, update the existing\n- entry when one already covers the fact (creates carry metadata {memoryType} or they are rejected);\n- skill = repeatable procedures (must carry a python reference {type:\"python\", import, callable}\n and an arguments object); subagent = reusable delegation roles.\n- Skill entries are authored to the DSH skill quality standard\n (skill-creator, distilled from the official deepseek-harness 11 skills;\n the full facts are provided in the <skill_quality_standard> block below):\n only for a REAL trigger scenario grounded in the trajectory\n (who, in what real task, what signal) \u2014 never invent one to pad the store;\n never duplicate the official 11 skills or existing entries; content is a\n SKILL.md document (frontmatter routing with \"use when / do not use when\"\n description, boundary declaration, prerequisites and exclusions, layered\n information, verifiable completion criteria). Self-check every proposed\n skill against the 7 structural features and state the result in its\n reason field.\n- Repeated multi-step workflows (session start/end routines, recurring\n wrap-up or handoff procedures) may be proposed as guidance skills:\n kind=skill, skill_kind=\"guidance\", content = a SKILL.md document (no\n python reference \u2014 executable skills keep requiring reference +\n arguments). Only propose with repeated evidence in the trajectory, never\n for one-off flows. Guidance skills materialize as discoverable SKILL.md\n files under <skillsRoot>/<kebab-name>/SKILL.md and are always offered to\n the user for a decision before they land.\n- Local edits are session-scoped; global edits persist across sessions.\n- Gap C2: EVERY edit MUST include a \"blastRadius\" field indicating how broadly\n the edit applies: \"general\" (cross-project tactical rule, applies everywhere),\n \"project\" (valid for the current project/repo), or \"session\" (one-off,\n specific to this session's context). The review gate validates that\n local-scope edits use \"session\" or \"project\", and global-scope edits use\n \"general\" or \"project\". When in doubt, prefer narrower blast radius.\n- Ground every edit in evidence: the session trajectory (recent direct user\n messages) is provided when available; prefer edits backed by it over\n speculation, and never invent preferences the user did not express.\n- Stale entries (superseded by newer ones, never referenced in recent\n trajectories, obsolete facts): propose action \"archive\" instead of\n \"delete\" \u2014 archive hides the entry from injection while keeping its data\n restorable; it requires only kind + id.\n- Output JSON only, exactly this shape:\n{\n \"summary\": \"one sentence\",\n \"rationale\": \"why these edits are justified by the evidence\",\n \"expectedOutcome\": \"what should improve and how to validate it\",\n \"edits\": [\n {\n \"action\": \"create|update|delete\",\n \"kind\": \"prompt|memory|skill|subagent\",\n \"id\": \"stable id for update/delete, optional for create\",\n \"title\": \"required for create/update except delete\",\n \"content\": \"required for create/update except delete\",\n \"path\": \"optional grouping path\",\n \"reference\": {\"type\":\"python\",\"import\":\"pkg.mod\",\"callable\":\"fn\"} ,\n \"arguments\": {\"name\": {\"type\":\"string\",\"required\":true,\"description\":\"...\"}},\n \"skill_kind\": \"executable|guidance (optional; skill kind only \u2014 guidance = SKILL.md document without reference)\",\n \"metadata\": {},\n \"reason\": \"why this edit is useful\",\n \"blastRadius\": \"general|project|session\"\n }\n ]\n}";
|
|
13
13
|
export interface PlanOptions {
|
|
14
14
|
agent: Agent;
|
|
15
15
|
state: HarnessState;
|
package/lib/planner.js
CHANGED
|
@@ -12,8 +12,14 @@ to reusable state: prompt notes, memories, skills, and subagent specs.
|
|
|
12
12
|
Rules:
|
|
13
13
|
- The base system prompt is immutable and MUST NOT be rewritten (never edit id "base_system_prompt").
|
|
14
14
|
- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.
|
|
15
|
-
- prompt = narrow behavioral policy addendums; memory =
|
|
16
|
-
|
|
15
|
+
- prompt = narrow behavioral policy addendums; memory = ONE FACT per entry, typed via metadata.memoryType
|
|
16
|
+
- (user = who the user is; feedback = a pitfall/correction or confirmed approach, MUST carry Why + How to apply;
|
|
17
|
+
- project = work goal/constraint not derivable from the repo; reference = URL/dashboard/ticket pointer).
|
|
18
|
+
- Memory rules: one fact per entry (title = one-line retrieval hook, content = the fact alone);
|
|
19
|
+
- never save what the repo already records (code structure, past fixes, git history, AGENTS.md) or what only
|
|
20
|
+
- matters to this conversation — save the non-obvious point instead; before creating, update the existing
|
|
21
|
+
- entry when one already covers the fact (creates carry metadata {memoryType} or they are rejected);
|
|
22
|
+
- skill = repeatable procedures (must carry a python reference {type:"python", import, callable}
|
|
17
23
|
and an arguments object); subagent = reusable delegation roles.
|
|
18
24
|
- Skill entries are authored to the DSH skill quality standard
|
|
19
25
|
(skill-creator, distilled from the official deepseek-harness 11 skills;
|
package/lib/project.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable project key for a workspace. `workspaceIdentity` (when set)
|
|
3
|
+
* replaces the path as the hash source so renamed checkouts of the same
|
|
4
|
+
* logical project keep one store.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveProjectKey(workspacePath: string, workspaceIdentity?: string): string;
|
|
7
|
+
/** Defense for externally supplied keys (explicit opts): single safe segment. */
|
|
8
|
+
export declare function sanitizeProjectKey(key: string): string;
|
|
9
|
+
/** Minimal agent shape the project-key derivation needs (duck-typed). */
|
|
10
|
+
export interface ProjectKeyAgentLike {
|
|
11
|
+
session?: {
|
|
12
|
+
header?: {
|
|
13
|
+
cwd?: unknown;
|
|
14
|
+
meta?: {
|
|
15
|
+
cwd?: unknown;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Derive the project key from an assembling agent's session cwd. Returns
|
|
22
|
+
* undefined when no validated absolute cwd is present — the caller then
|
|
23
|
+
* skips the project layer rather than failing the whole assembly.
|
|
24
|
+
*/
|
|
25
|
+
export declare function projectKeyOf(agent: unknown, opts?: {
|
|
26
|
+
workspaceIdentity?: string;
|
|
27
|
+
}): string | undefined;
|
|
28
|
+
//# sourceMappingURL=project.d.ts.map
|
package/lib/project.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project scope identity: the third harness layer between session-local
|
|
3
|
+
* staging and the cross-project global store.
|
|
4
|
+
*
|
|
5
|
+
* A project is identified by the session's validated absolute `cwd`
|
|
6
|
+
* (stable slug + sha256, mirroring ZCode's `project-root.ts`), so every
|
|
7
|
+
* session opened in the same workspace shares one project store while
|
|
8
|
+
* different checkouts never collide. All helpers are pure and never throw —
|
|
9
|
+
* a missing cwd simply means "no project layer" (the caller degrades to
|
|
10
|
+
* global+local, the same shape as an empty project store).
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { basename, isAbsolute, resolve } from "node:path";
|
|
14
|
+
/**
|
|
15
|
+
* Stable project key for a workspace. `workspaceIdentity` (when set)
|
|
16
|
+
* replaces the path as the hash source so renamed checkouts of the same
|
|
17
|
+
* logical project keep one store.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveProjectKey(workspacePath, workspaceIdentity) {
|
|
20
|
+
const identity = workspaceIdentity?.trim();
|
|
21
|
+
const normalized = resolve(workspacePath);
|
|
22
|
+
const keySource = identity && identity.length > 0 ? identity : process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
23
|
+
const hash = createHash("sha256").update(keySource).digest("hex").slice(0, 16);
|
|
24
|
+
const slug = identity && identity.length > 0 ? "project" : sanitizeSlug(basename(normalized) || "project");
|
|
25
|
+
return `${slug}-${hash}`;
|
|
26
|
+
}
|
|
27
|
+
function sanitizeSlug(value) {
|
|
28
|
+
const slug = value
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
31
|
+
.replace(/^-+|-+$/g, "")
|
|
32
|
+
.slice(0, 48);
|
|
33
|
+
return slug.length > 0 ? slug : "project";
|
|
34
|
+
}
|
|
35
|
+
/** Defense for externally supplied keys (explicit opts): single safe segment. */
|
|
36
|
+
export function sanitizeProjectKey(key) {
|
|
37
|
+
const cleaned = key
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
40
|
+
.replace(/^-+|-+$/g, "")
|
|
41
|
+
.slice(0, 64);
|
|
42
|
+
return cleaned.length > 0 ? cleaned : "project";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Derive the project key from an assembling agent's session cwd. Returns
|
|
46
|
+
* undefined when no validated absolute cwd is present — the caller then
|
|
47
|
+
* skips the project layer rather than failing the whole assembly.
|
|
48
|
+
*/
|
|
49
|
+
export function projectKeyOf(agent, opts) {
|
|
50
|
+
try {
|
|
51
|
+
const header = agent?.session?.header;
|
|
52
|
+
const raw = header?.cwd ?? header?.meta?.cwd;
|
|
53
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const cwd = raw.trim();
|
|
57
|
+
if (!isAbsolute(cwd)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return resolveProjectKey(cwd, opts?.workspaceIdentity);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=project.js.map
|
package/lib/service.js
CHANGED
|
@@ -6,6 +6,11 @@ import { loadHarnessState, saveHarnessState } from "./state.js";
|
|
|
6
6
|
import { appendResult, loadResults, snapshotBefore, storePaths } from "./store.js";
|
|
7
7
|
import { CONFLICT_BLOCK_SCORE, CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry, secretLeakReason } from "./promotion.js";
|
|
8
8
|
export function createEvolutionEngine(baseDir, hooks = {}) {
|
|
9
|
+
/**
|
|
10
|
+
* Load a scope's state. For `project` the `sessionId` parameter carries
|
|
11
|
+
* the project key (see `resolveProjectKey` in project.ts); callers
|
|
12
|
+
* derive it with `projectKeyOf(agent)` and pass it through unchanged.
|
|
13
|
+
*/
|
|
9
14
|
function load(scope, sessionId) {
|
|
10
15
|
return loadHarnessState(storePaths(baseDir, scope, sessionId).stateDir, scope);
|
|
11
16
|
}
|
package/lib/source.d.ts
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* citation of (sessionId, seqs) expands back to the exact original
|
|
5
5
|
* conversation rows (the durable log under `<dshHome>/sessions/...`).
|
|
6
6
|
*
|
|
7
|
-
* The extraction reads the live
|
|
8
|
-
* dsh-session dependency) and only
|
|
9
|
-
* injected plugin context and tool
|
|
7
|
+
* The extraction reads the live session log through the reader the running
|
|
8
|
+
* harness generation exposes (duck-typed, no dsh-session dependency) and only
|
|
9
|
+
* ever selects direct human messages — injected plugin context and tool
|
|
10
|
+
* results never become citations.
|
|
10
11
|
*/
|
|
11
12
|
import type { EntrySource } from "./types.js";
|
|
12
|
-
import type
|
|
13
|
+
import { type AgentLike } from "./inject.js";
|
|
13
14
|
/** At most this many source user messages are cited per entry. */
|
|
14
15
|
export declare const MAX_SOURCE_MESSAGES = 3;
|
|
15
16
|
/**
|
package/lib/source.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sessionEventsOf } from "./inject.js";
|
|
1
2
|
/** At most this many source user messages are cited per entry. */
|
|
2
3
|
export const MAX_SOURCE_MESSAGES = 3;
|
|
3
4
|
/**
|
|
@@ -6,8 +7,8 @@ export const MAX_SOURCE_MESSAGES = 3;
|
|
|
6
7
|
* callers then simply omit the citation.
|
|
7
8
|
*/
|
|
8
9
|
export function recentUserSeqs(agent, opts) {
|
|
9
|
-
const events = agent
|
|
10
|
-
if (
|
|
10
|
+
const events = sessionEventsOf(agent);
|
|
11
|
+
if (events.length === 0) {
|
|
11
12
|
return [];
|
|
12
13
|
}
|
|
13
14
|
const maxMessages = opts?.maxMessages ?? MAX_SOURCE_MESSAGES;
|
package/lib/state.d.ts
CHANGED
|
@@ -7,11 +7,15 @@ export declare function stateFilePath(stateDir: string): string;
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function loadHarnessState(stateDir: string, scope?: HarnessScope): HarnessState;
|
|
9
9
|
/**
|
|
10
|
-
* Merge global and local state into the view the model sees.
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
10
|
+
* Merge global, project, and local state into the view the model sees.
|
|
11
|
+
* Precedence is global < project < local: a colliding project id is
|
|
12
|
+
* prefixed `project:` so both remain addressable, and a colliding local id
|
|
13
|
+
* is prefixed `local:` (local always wins — it is the session's live
|
|
14
|
+
* staging copy). Called with two args the project layer is simply absent.
|
|
13
15
|
*/
|
|
14
|
-
export declare function mergeHarnessStates(globalState: HarnessState, localState
|
|
16
|
+
export declare function mergeHarnessStates(globalState: HarnessState, localState?: HarnessState | undefined, opts?: {
|
|
17
|
+
projectState?: HarnessState | undefined;
|
|
18
|
+
}): HarnessState;
|
|
15
19
|
/**
|
|
16
20
|
* Persist state atomically: write to a temp file, fsync-free but rename-based,
|
|
17
21
|
* preserving the mode of an existing file (defaults to 0o600 for new files).
|