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/inject.d.ts
CHANGED
|
@@ -114,20 +114,42 @@ export declare function formatPromptEntriesSection(entries: readonly HarnessEntr
|
|
|
114
114
|
export declare function formatSubagentSpecsSection(entries: readonly HarnessEntry[], query?: string): string;
|
|
115
115
|
/**
|
|
116
116
|
* Gap B3: a lightweight directory of ALL non-archived entries across all
|
|
117
|
-
* kinds — one line per entry
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
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.
|
|
121
123
|
*
|
|
122
124
|
* 2026-08-22 throttle: the directory is CAPPED at {@link DEFAULT_DIRECTORY_LINES}
|
|
123
|
-
* lines
|
|
124
|
-
*
|
|
125
|
-
*
|
|
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.
|
|
126
132
|
*/
|
|
127
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;
|
|
128
136
|
export declare function formatEntriesDirectory(...kindEntries: readonly HarnessEntry[][]): string;
|
|
129
137
|
/** {@link formatEntriesDirectory} with an explicit cap (configurable). */
|
|
130
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[];
|
|
131
153
|
/**
|
|
132
154
|
* Walk the parent-session chain from `agent` upward and return the nearest
|
|
133
155
|
* session whose local store is non-empty, if any. Children inherit their
|
|
@@ -138,19 +160,23 @@ export declare function formatEntriesDirectoryCapped(maxLines: number, ...kindEn
|
|
|
138
160
|
export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, agent: AgentLike): HarnessState | undefined;
|
|
139
161
|
/**
|
|
140
162
|
* Compose the full injected block for one assembling agent: global entries
|
|
141
|
-
* 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).
|
|
142
165
|
* The optional `query` — when absent, derived from the agent's most recent
|
|
143
166
|
* direct user messages — ranks which entries fill the per-kind cap
|
|
144
167
|
* (relevance first, then recency; see {@link rankEntries}). Returns "" when
|
|
145
168
|
* nothing is injectable — the prompt renderer then drops the section, so an
|
|
146
169
|
* empty store adds zero tokens to every assembly.
|
|
147
170
|
*
|
|
148
|
-
* `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.
|
|
149
174
|
* Usage recording covers ALL kinds — memories and skills appear as directory
|
|
150
175
|
* lines, prompts/subagents as content — and is deduped per session so the
|
|
151
176
|
* counts read "how many sessions saw this", not "how many prompt builds".
|
|
152
177
|
*/
|
|
153
178
|
export declare function entriesSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, query?: string, opts?: {
|
|
154
179
|
directoryLines?: number;
|
|
180
|
+
projectKey?: string;
|
|
155
181
|
}): string;
|
|
156
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";
|
|
@@ -214,27 +215,81 @@ export function formatSubagentSpecsSection(entries, query) {
|
|
|
214
215
|
}
|
|
215
216
|
/**
|
|
216
217
|
* Gap B3: a lightweight directory of ALL non-archived entries across all
|
|
217
|
-
* kinds — one line per entry
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
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.
|
|
221
224
|
*
|
|
222
225
|
* 2026-08-22 throttle: the directory is CAPPED at {@link DEFAULT_DIRECTORY_LINES}
|
|
223
|
-
* lines
|
|
224
|
-
*
|
|
225
|
-
*
|
|
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.
|
|
226
233
|
*/
|
|
227
234
|
export const DEFAULT_DIRECTORY_LINES = 15;
|
|
228
|
-
/**
|
|
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. */
|
|
229
248
|
const CONTENT_SECTION_KINDS = 2;
|
|
230
249
|
export function formatEntriesDirectory(...kindEntries) {
|
|
231
250
|
return formatEntriesDirectoryCapped(DEFAULT_DIRECTORY_LINES, ...kindEntries);
|
|
232
251
|
}
|
|
233
252
|
/** {@link formatEntriesDirectory} with an explicit cap (configurable). */
|
|
234
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) {
|
|
235
290
|
const allEntries = kindEntries.flat().filter((e) => !isArchived(e));
|
|
236
291
|
if (allEntries.length === 0) {
|
|
237
|
-
return
|
|
292
|
+
return [];
|
|
238
293
|
}
|
|
239
294
|
// Skip the directory only when EVERY entry is already content-visible.
|
|
240
295
|
// Only the first two arrays (prompt, subagent) have curated sections —
|
|
@@ -245,19 +300,9 @@ export function formatEntriesDirectoryCapped(maxLines, ...kindEntries) {
|
|
|
245
300
|
.slice(0, CONTENT_SECTION_KINDS)
|
|
246
301
|
.reduce((sum, entries) => sum + Math.min(entries.filter((e) => !isArchived(e)).length, MAX_INJECTED_ENTRIES_PER_KIND), 0);
|
|
247
302
|
if (allEntries.length <= contentVisible) {
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
const sorted = [...allEntries].sort((a, b) => `${a.kind}:${a.id}`.localeCompare(`${b.kind}:${b.id}`));
|
|
251
|
-
const lines = ["# Continual Harness — Entry Directory", "All entries (use evolve_list for full text of any entry):"];
|
|
252
|
-
const shown = sorted.slice(0, Math.max(maxLines, 1));
|
|
253
|
-
for (const entry of shown) {
|
|
254
|
-
lines.push(`- [${entry.kind}:${entry.id}] ${entry.title}`);
|
|
255
|
-
}
|
|
256
|
-
const hidden = sorted.length - shown.length;
|
|
257
|
-
if (hidden > 0) {
|
|
258
|
-
lines.push(`- …and ${hidden} more entries (evolve_list for the full index)`);
|
|
303
|
+
return [];
|
|
259
304
|
}
|
|
260
|
-
return
|
|
305
|
+
return rankEntries(allEntries, query).slice(0, Math.max(maxLines, 1));
|
|
261
306
|
}
|
|
262
307
|
/**
|
|
263
308
|
* Walk the parent-session chain from `agent` upward and return the nearest
|
|
@@ -281,14 +326,17 @@ export function nearestLocalStateWithEntries(engine, agent) {
|
|
|
281
326
|
}
|
|
282
327
|
/**
|
|
283
328
|
* Compose the full injected block for one assembling agent: global entries
|
|
284
|
-
* 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).
|
|
285
331
|
* The optional `query` — when absent, derived from the agent's most recent
|
|
286
332
|
* direct user messages — ranks which entries fill the per-kind cap
|
|
287
333
|
* (relevance first, then recency; see {@link rankEntries}). Returns "" when
|
|
288
334
|
* nothing is injectable — the prompt renderer then drops the section, so an
|
|
289
335
|
* empty store adds zero tokens to every assembly.
|
|
290
336
|
*
|
|
291
|
-
* `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.
|
|
292
340
|
* Usage recording covers ALL kinds — memories and skills appear as directory
|
|
293
341
|
* lines, prompts/subagents as content — and is deduped per session so the
|
|
294
342
|
* counts read "how many sessions saw this", not "how many prompt builds".
|
|
@@ -298,8 +346,10 @@ export function entriesSectionText(engine, agent, query, opts) {
|
|
|
298
346
|
return "";
|
|
299
347
|
}
|
|
300
348
|
const globalState = engine.load("global", undefined);
|
|
349
|
+
const projectKey = opts?.projectKey ?? projectKeyOf(agent);
|
|
350
|
+
const projectState = projectKey ? engine.load("project", projectKey) : undefined;
|
|
301
351
|
const localState = nearestLocalStateWithEntries(engine, agent);
|
|
302
|
-
const merged =
|
|
352
|
+
const merged = mergeHarnessStates(globalState, localState, projectState ? { projectState } : undefined);
|
|
303
353
|
const promptEntries = Object.values(merged.entries.prompt);
|
|
304
354
|
const subagentEntries = Object.values(merged.entries.subagent);
|
|
305
355
|
const relevanceQuery = (query ?? recentUserText(agent)).trim();
|
|
@@ -318,18 +368,18 @@ export function entriesSectionText(engine, agent, query, opts) {
|
|
|
318
368
|
}
|
|
319
369
|
// Gap B3: lightweight directory of ALL entries (id+title, one line each).
|
|
320
370
|
// Zero-cost index so the model knows what exists and can ask for full text.
|
|
321
|
-
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);
|
|
322
377
|
// Directory-visible keys count too: a memory's injection IS its directory
|
|
323
|
-
// line. Set semantics keep content-injected entries single-counted.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const key = `${kind}:${entry.id}`;
|
|
329
|
-
if (injectedKeys.has(key) || directoryText.includes(`[${key}]`)) {
|
|
330
|
-
injectedKeys.add(key);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
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}`);
|
|
333
383
|
}
|
|
334
384
|
// Record usage durably (best-effort: failure never blocks injection).
|
|
335
385
|
// Deduped per session — see recordInjection.
|
package/lib/llm-text.d.ts
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
* and JSON parsing.
|
|
10
10
|
*/
|
|
11
11
|
import type { Context } from "@deepseek-ai/cordis";
|
|
12
|
+
import { type Message, type TokenUsage } from "@deepseek-ai/dsh-llm";
|
|
13
|
+
/** Terminal outcome of one shared direct LLM call. */
|
|
14
|
+
export type StreamTextOutcome = "success" | "max-tokens" | "error" | "aborted" | "empty";
|
|
15
|
+
/** Provider usage and terminal result observed after the stream settles. */
|
|
16
|
+
export interface StreamTextObservation {
|
|
17
|
+
usage?: TokenUsage;
|
|
18
|
+
outcome: StreamTextOutcome;
|
|
19
|
+
}
|
|
20
|
+
/** Diagnostic observer invoked before streamText returns or throws. */
|
|
21
|
+
export type StreamTextUsageObserver = (observation: StreamTextObservation) => void;
|
|
12
22
|
export interface StreamTextOptions {
|
|
13
23
|
provider: string;
|
|
14
24
|
model: string;
|
|
@@ -16,6 +26,19 @@ export interface StreamTextOptions {
|
|
|
16
26
|
prompt: string;
|
|
17
27
|
maxTokens?: number;
|
|
18
28
|
signal?: AbortSignal | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Route A session prefix: session-derived messages sent before the
|
|
31
|
+
* trailing caller message so providers with prompt caching can serve
|
|
32
|
+
* the shared context at cache-read price. Empty/omitted keeps the
|
|
33
|
+
* legacy single-message request.
|
|
34
|
+
*/
|
|
35
|
+
prefixMessages?: readonly Message[];
|
|
36
|
+
/**
|
|
37
|
+
* Observe the provider-reported usage and terminal outcome after the
|
|
38
|
+
* stream settles. The callback runs before success/error return and is
|
|
39
|
+
* diagnostic-only: throwing from it never changes the model call result.
|
|
40
|
+
*/
|
|
41
|
+
onUsage?: StreamTextUsageObserver;
|
|
19
42
|
}
|
|
20
43
|
/**
|
|
21
44
|
* Stream a single-turn text completion through `ctx.llm`. Forces
|
package/lib/llm-text.js
CHANGED
|
@@ -11,40 +11,57 @@ import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export async function streamText(ctx, opts) {
|
|
13
13
|
const assembler = new BlockAssembler();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
14
|
+
let outcome = "error";
|
|
15
|
+
try {
|
|
16
|
+
for await (const chunk of ctx.llm.stream({
|
|
17
|
+
provider: opts.provider,
|
|
18
|
+
model: opts.model,
|
|
19
|
+
system: opts.system,
|
|
20
|
+
messages: [
|
|
21
|
+
...(opts.prefixMessages ?? []),
|
|
22
|
+
createUserMessage({
|
|
23
|
+
content: [{ type: "text", text: opts.prompt }],
|
|
24
|
+
source: EVOLVE_MESSAGE_SOURCE,
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
reasoningEffort: ReasoningEffortId("off"),
|
|
28
|
+
maxTokens: opts.maxTokens ?? 8000,
|
|
29
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
30
|
+
})) {
|
|
31
|
+
assembler.push(chunk);
|
|
32
|
+
}
|
|
33
|
+
const finish = assembler.finish;
|
|
34
|
+
if (finish.kind === "error") {
|
|
35
|
+
outcome = "error";
|
|
36
|
+
throw new Error(`evolve: LLM call failed: ${finish.failure?.message ?? "unknown"}`);
|
|
37
|
+
}
|
|
38
|
+
if (finish.kind === "aborted") {
|
|
39
|
+
outcome = "aborted";
|
|
40
|
+
throw new Error("evolve: LLM call aborted");
|
|
41
|
+
}
|
|
42
|
+
if (finish.kind === "max-tokens") {
|
|
43
|
+
outcome = "max-tokens";
|
|
44
|
+
throw new Error("evolve: LLM output budget exhausted (max-tokens)");
|
|
45
|
+
}
|
|
46
|
+
const text = assembler
|
|
47
|
+
.blocks()
|
|
48
|
+
.filter((block) => block.type === "text")
|
|
49
|
+
.map((block) => block.text)
|
|
50
|
+
.join("\n");
|
|
51
|
+
if (text.length === 0) {
|
|
52
|
+
outcome = "empty";
|
|
53
|
+
throw new Error("evolve: LLM produced no text output");
|
|
54
|
+
}
|
|
55
|
+
outcome = "success";
|
|
56
|
+
return text;
|
|
29
57
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
finally {
|
|
59
|
+
try {
|
|
60
|
+
opts.onUsage?.({ ...(assembler.usage ? { usage: assembler.usage } : {}), outcome });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Usage observation is diagnostic; its failure must not alter the model call.
|
|
64
|
+
}
|
|
33
65
|
}
|
|
34
|
-
if (finish.kind === "aborted") {
|
|
35
|
-
throw new Error("evolve: LLM call aborted");
|
|
36
|
-
}
|
|
37
|
-
if (finish.kind === "max-tokens") {
|
|
38
|
-
throw new Error("evolve: LLM output budget exhausted (max-tokens)");
|
|
39
|
-
}
|
|
40
|
-
const text = assembler
|
|
41
|
-
.blocks()
|
|
42
|
-
.filter((block) => block.type === "text")
|
|
43
|
-
.map((block) => block.text)
|
|
44
|
-
.join("\n");
|
|
45
|
-
if (text.length === 0) {
|
|
46
|
-
throw new Error("evolve: LLM produced no text output");
|
|
47
|
-
}
|
|
48
|
-
return text;
|
|
49
66
|
}
|
|
50
67
|
//# sourceMappingURL=llm-text.js.map
|
package/lib/planner.d.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
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
|
-
|
|
12
|
+
import { type PrefixCacheOptions } from "./prefix-cache.js";
|
|
13
|
+
import { type TokenUsageTarget } from "./token-usage.js";
|
|
14
|
+
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
15
|
export interface PlanOptions {
|
|
14
16
|
agent: Agent;
|
|
15
17
|
state: HarnessState;
|
|
@@ -32,6 +34,16 @@ export interface PlanOptions {
|
|
|
32
34
|
global?: boolean;
|
|
33
35
|
signal?: AbortSignal;
|
|
34
36
|
maxOutputTokens?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Prefix-cache routing for the planning input. Route A prepends a
|
|
39
|
+
* session-derived message prefix and drops the auto-extracted
|
|
40
|
+
* trajectory block (an explicitly passed `trajectory` is always kept);
|
|
41
|
+
* Route B keeps the legacy flat-text input. Absent → auto-detect with
|
|
42
|
+
* the default budget.
|
|
43
|
+
*/
|
|
44
|
+
prefixCache?: PrefixCacheOptions;
|
|
45
|
+
/** Direct-call token ledger destination; the planner phase is fixed by this module. */
|
|
46
|
+
tokenUsage?: TokenUsageTarget;
|
|
35
47
|
}
|
|
36
48
|
export declare function planWithLlm(ctx: Context, options: PlanOptions): Promise<RefinementProposal>;
|
|
37
49
|
//# sourceMappingURL=planner.d.ts.map
|
package/lib/planner.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { parseProposal } from "./plan.js";
|
|
2
2
|
import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
|
|
3
|
-
import { recentUserText } from "./inject.js";
|
|
3
|
+
import { recentUserText, sessionEventsOf } from "./inject.js";
|
|
4
|
+
import { buildPrefixMessages, detectPlannerRoute, resolvePrefixCache } from "./prefix-cache.js";
|
|
4
5
|
import { skillQualityGuide } from "./skillquality.js";
|
|
5
6
|
import { streamText } from "./llm-text.js";
|
|
7
|
+
import { createTokenUsageObserver } from "./token-usage.js";
|
|
6
8
|
export const PLANNER_SYSTEM_PROMPT = `You are the /evolve continual harness subsystem.
|
|
7
9
|
|
|
8
10
|
Your job is to improve the editable continual harness state. Instead of
|
|
@@ -12,8 +14,14 @@ to reusable state: prompt notes, memories, skills, and subagent specs.
|
|
|
12
14
|
Rules:
|
|
13
15
|
- The base system prompt is immutable and MUST NOT be rewritten (never edit id "base_system_prompt").
|
|
14
16
|
- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.
|
|
15
|
-
- prompt = narrow behavioral policy addendums; memory =
|
|
16
|
-
|
|
17
|
+
- prompt = narrow behavioral policy addendums; memory = ONE FACT per entry, typed via metadata.memoryType
|
|
18
|
+
- (user = who the user is; feedback = a pitfall/correction or confirmed approach, MUST carry Why + How to apply;
|
|
19
|
+
- project = work goal/constraint not derivable from the repo; reference = URL/dashboard/ticket pointer).
|
|
20
|
+
- Memory rules: one fact per entry (title = one-line retrieval hook, content = the fact alone);
|
|
21
|
+
- never save what the repo already records (code structure, past fixes, git history, AGENTS.md) or what only
|
|
22
|
+
- matters to this conversation — save the non-obvious point instead; before creating, update the existing
|
|
23
|
+
- entry when one already covers the fact (creates carry metadata {memoryType} or they are rejected);
|
|
24
|
+
- skill = repeatable procedures (must carry a python reference {type:"python", import, callable}
|
|
17
25
|
and an arguments object); subagent = reusable delegation roles.
|
|
18
26
|
- Skill entries are authored to the DSH skill quality standard
|
|
19
27
|
(skill-creator, distilled from the official deepseek-harness 11 skills;
|
|
@@ -78,10 +86,22 @@ export async function planWithLlm(ctx, options) {
|
|
|
78
86
|
const scopeInstruction = options.global
|
|
79
87
|
? "Requested scope: global. Only propose stable cross-session lessons, durable preferences, reusable skills/subagents, or explicitly project-qualified facts."
|
|
80
88
|
: "Requested scope: local. Prefer session-scoped edits for current task progress; global entries are read-only context — do not propose update/delete for them.";
|
|
81
|
-
// Ground the plan in the caller's session:
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
89
|
+
// Ground the plan in the caller's session: Route A carries the context
|
|
90
|
+
// as a session-derived message prefix (cache-eligible on providers with
|
|
91
|
+
// prompt caching) and drops the auto-extracted trajectory block; Route B
|
|
92
|
+
// keeps the legacy flat-text block. An empty prefix falls back to B.
|
|
93
|
+
const events = sessionEventsOf(agent);
|
|
94
|
+
const routing = resolvePrefixCache(options.prefixCache);
|
|
95
|
+
const route = detectPlannerRoute(events, routing.mode);
|
|
96
|
+
let prefixMessages = [];
|
|
97
|
+
if (route === "A") {
|
|
98
|
+
prefixMessages = buildPrefixMessages(events, {
|
|
99
|
+
...(agent.options.provider ? { provider: agent.options.provider } : {}),
|
|
100
|
+
...(agent.options.model ? { model: agent.options.model } : {}),
|
|
101
|
+
maxChars: routing.maxChars,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const trajectory = prefixMessages.length > 0 ? (options.trajectory ?? "") : (options.trajectory ?? recentUserText(agent));
|
|
85
105
|
// The skill quality standard is always present: the skill-creator
|
|
86
106
|
// template facts when installed, the builtin distilled guide otherwise
|
|
87
107
|
// (~1KB — planning is low-frequency, and the standard keeps skill
|
|
@@ -105,6 +125,10 @@ export async function planWithLlm(ctx, options) {
|
|
|
105
125
|
prompt: userPrompt,
|
|
106
126
|
maxTokens: options.maxOutputTokens ?? 8000,
|
|
107
127
|
signal: options.signal,
|
|
128
|
+
...(prefixMessages.length > 0 ? { prefixMessages } : {}),
|
|
129
|
+
...(options.tokenUsage
|
|
130
|
+
? { onUsage: createTokenUsageObserver(options.tokenUsage, "planner", agent.options.provider, agent.options.model) }
|
|
131
|
+
: {}),
|
|
108
132
|
});
|
|
109
133
|
return parseProposal(text);
|
|
110
134
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefix-cache-aware planning input routing (Route A/B).
|
|
3
|
+
*
|
|
4
|
+
* The planner and the review gate re-send session context on every call as
|
|
5
|
+
* flat text. On providers with prompt caching that context is re-billed at
|
|
6
|
+
* full price each time. Route A instead prepends session-derived messages —
|
|
7
|
+
* the same evidence in structured form, eligible for cache-read pricing —
|
|
8
|
+
* and drops the redundant flat trajectory block. Route B keeps the legacy
|
|
9
|
+
* flat-text input byte-for-byte.
|
|
10
|
+
*
|
|
11
|
+
* Adapted from dsh-continual-harness (Route A/B + cache-detect): this port
|
|
12
|
+
* deliberately does NOT reproduce the host loop's byte-identical cache key
|
|
13
|
+
* (no deriveMessages/tools/sessionId plumbing on this seam), so a hit needs
|
|
14
|
+
* a provider that keys on the message-prefix content. Sources are stamped
|
|
15
|
+
* honestly and every reduction is documented below.
|
|
16
|
+
*/
|
|
17
|
+
import { type AssistantMessage, type UserMessage } from "@deepseek-ai/dsh-llm";
|
|
18
|
+
/** Planning input shape: A = session-derived message prefix, B = flat-text trajectory. */
|
|
19
|
+
export type PlannerRoute = "A" | "B";
|
|
20
|
+
/** Routing mode: auto-detect from cache evidence, always prefix, or never. */
|
|
21
|
+
export type PlannerPrefixCacheMode = "auto" | "session" | "off";
|
|
22
|
+
/** Per-call routing overrides; absent fields fall back to the resolved defaults. */
|
|
23
|
+
export interface PrefixCacheOptions {
|
|
24
|
+
mode?: PlannerPrefixCacheMode;
|
|
25
|
+
maxChars?: number;
|
|
26
|
+
}
|
|
27
|
+
/** Default session-prefix budget for one Route A input, in characters. */
|
|
28
|
+
export declare const DEFAULT_PREFIX_MAX_CHARS = 12000;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve per-call overrides against the defaults.
|
|
31
|
+
*
|
|
32
|
+
* @param opts - sparse per-call overrides.
|
|
33
|
+
* @returns the effective mode (default "auto") and char budget.
|
|
34
|
+
* @throws When maxChars is present but not a positive safe integer (fail loud on misconfiguration).
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolvePrefixCache(opts?: PrefixCacheOptions): {
|
|
37
|
+
mode: PlannerPrefixCacheMode;
|
|
38
|
+
maxChars: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* True when any assistant message in this session reported cache-read
|
|
42
|
+
* tokens — evidence the provider can serve a warm prefix.
|
|
43
|
+
*
|
|
44
|
+
* @param events - session log rows, newest last.
|
|
45
|
+
* @returns whether cache evidence exists (false on an empty/unreadable log).
|
|
46
|
+
*/
|
|
47
|
+
export declare function hasCacheEvidence(events: readonly unknown[]): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Pick the planning input route for one call.
|
|
50
|
+
*
|
|
51
|
+
* @param events - session log rows, newest last.
|
|
52
|
+
* @param mode - routing mode (default "auto").
|
|
53
|
+
* @returns "A" when the session prefix should carry the context, else "B".
|
|
54
|
+
*/
|
|
55
|
+
export declare function detectPlannerRoute(events: readonly unknown[], mode?: PlannerPrefixCacheMode): PlannerRoute;
|
|
56
|
+
/**
|
|
57
|
+
* Rebuild a tail-biased session prefix from the event log.
|
|
58
|
+
*
|
|
59
|
+
* Inclusion mirrors the trajectory grounding (only direct human `user`
|
|
60
|
+
* speech plus the model's own assistant text), so injected plugin context
|
|
61
|
+
* and tool results never echo back into the planning input. Whole messages
|
|
62
|
+
* are dropped oldest-first over budget; a single over-budget message keeps
|
|
63
|
+
* its tail.
|
|
64
|
+
*
|
|
65
|
+
* @param events - session log rows, newest last.
|
|
66
|
+
* @param opts - provider/model stamps the assistant messages (the calling
|
|
67
|
+
* agent's own route); maxChars caps the prefix (default {@link DEFAULT_PREFIX_MAX_CHARS}).
|
|
68
|
+
* @returns the prefix messages, oldest first ([] when nothing qualifies —
|
|
69
|
+
* callers fall back to Route B).
|
|
70
|
+
* @throws When assistant text exists but provider/model is missing (an
|
|
71
|
+
* assistant message cannot be honestly stamped without its route).
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildPrefixMessages(events: readonly unknown[], opts: {
|
|
74
|
+
provider?: string;
|
|
75
|
+
model?: string;
|
|
76
|
+
maxChars?: number;
|
|
77
|
+
}): (UserMessage | AssistantMessage)[];
|
|
78
|
+
//# sourceMappingURL=prefix-cache.d.ts.map
|