dsh-continual-evolve 0.10.6 → 0.12.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.
Files changed (126) hide show
  1. package/README.md +35 -122
  2. package/README.zh.md +26 -97
  3. package/cordis.patch.yml +2 -2
  4. package/lib/index.d.ts +44 -241
  5. package/lib/index.js +30 -253
  6. package/lib/memory-guide.d.ts +13 -14
  7. package/lib/memory-guide.js +32 -21
  8. package/lib/memory-section.d.ts +77 -0
  9. package/lib/memory-section.js +189 -0
  10. package/package.json +2 -17
  11. package/lib/apply.d.ts +0 -22
  12. package/lib/apply.js +0 -157
  13. package/lib/approval.d.ts +0 -72
  14. package/lib/approval.js +0 -127
  15. package/lib/auto.d.ts +0 -236
  16. package/lib/auto.js +0 -773
  17. package/lib/autocase.d.ts +0 -38
  18. package/lib/autocase.js +0 -76
  19. package/lib/benchmark-command.d.ts +0 -9
  20. package/lib/benchmark-command.js +0 -357
  21. package/lib/benchmark.d.ts +0 -151
  22. package/lib/benchmark.js +0 -250
  23. package/lib/command.d.ts +0 -49
  24. package/lib/command.js +0 -568
  25. package/lib/consolidate.d.ts +0 -90
  26. package/lib/consolidate.js +0 -180
  27. package/lib/copy.d.ts +0 -74
  28. package/lib/copy.js +0 -160
  29. package/lib/declines.d.ts +0 -101
  30. package/lib/declines.js +0 -175
  31. package/lib/evaluate.d.ts +0 -74
  32. package/lib/evaluate.js +0 -271
  33. package/lib/evolve-event.d.ts +0 -40
  34. package/lib/evolve-event.js +0 -59
  35. package/lib/failures.d.ts +0 -42
  36. package/lib/failures.js +0 -171
  37. package/lib/fate.d.ts +0 -130
  38. package/lib/fate.js +0 -350
  39. package/lib/goal-command.d.ts +0 -7
  40. package/lib/goal-command.js +0 -37
  41. package/lib/goal.d.ts +0 -72
  42. package/lib/goal.js +0 -72
  43. package/lib/inject.d.ts +0 -196
  44. package/lib/inject.js +0 -412
  45. package/lib/llm-text.d.ts +0 -104
  46. package/lib/llm-text.js +0 -140
  47. package/lib/logfile.d.ts +0 -71
  48. package/lib/logfile.js +0 -159
  49. package/lib/memory-agent.d.ts +0 -129
  50. package/lib/memory-agent.js +0 -568
  51. package/lib/memory-benchmark.d.ts +0 -76
  52. package/lib/memory-benchmark.js +0 -170
  53. package/lib/memory-index.d.ts +0 -114
  54. package/lib/memory-index.js +0 -197
  55. package/lib/message-source.d.ts +0 -23
  56. package/lib/message-source.js +0 -12
  57. package/lib/mount-command.d.ts +0 -10
  58. package/lib/mount-command.js +0 -48
  59. package/lib/mount.d.ts +0 -42
  60. package/lib/mount.js +0 -212
  61. package/lib/notify.d.ts +0 -59
  62. package/lib/notify.js +0 -97
  63. package/lib/plan.d.ts +0 -16
  64. package/lib/plan.js +0 -126
  65. package/lib/planner.d.ts +0 -61
  66. package/lib/planner.js +0 -144
  67. package/lib/pool.d.ts +0 -7
  68. package/lib/pool.js +0 -25
  69. package/lib/prefix-cache.d.ts +0 -78
  70. package/lib/prefix-cache.js +0 -168
  71. package/lib/project.d.ts +0 -28
  72. package/lib/project.js +0 -66
  73. package/lib/projection.d.ts +0 -22
  74. package/lib/projection.js +0 -112
  75. package/lib/promotion.d.ts +0 -94
  76. package/lib/promotion.js +0 -171
  77. package/lib/recall.d.ts +0 -85
  78. package/lib/recall.js +0 -142
  79. package/lib/record-language.d.ts +0 -85
  80. package/lib/record-language.js +0 -152
  81. package/lib/render.d.ts +0 -13
  82. package/lib/render.js +0 -80
  83. package/lib/review-scheduler.d.ts +0 -38
  84. package/lib/review-scheduler.js +0 -191
  85. package/lib/review.d.ts +0 -60
  86. package/lib/review.js +0 -145
  87. package/lib/rollback.d.ts +0 -9
  88. package/lib/rollback.js +0 -68
  89. package/lib/rubric.d.ts +0 -29
  90. package/lib/rubric.js +0 -119
  91. package/lib/runtime.d.ts +0 -24
  92. package/lib/runtime.js +0 -87
  93. package/lib/score.d.ts +0 -64
  94. package/lib/score.js +0 -191
  95. package/lib/search.d.ts +0 -83
  96. package/lib/search.js +0 -136
  97. package/lib/service.d.ts +0 -49
  98. package/lib/service.js +0 -180
  99. package/lib/skill-render.d.ts +0 -23
  100. package/lib/skill-render.js +0 -69
  101. package/lib/skill.d.ts +0 -15
  102. package/lib/skill.js +0 -80
  103. package/lib/skillquality.d.ts +0 -85
  104. package/lib/skillquality.js +0 -320
  105. package/lib/source.d.ts +0 -30
  106. package/lib/source.js +0 -43
  107. package/lib/state.d.ts +0 -26
  108. package/lib/state.js +0 -151
  109. package/lib/store.d.ts +0 -62
  110. package/lib/store.js +0 -164
  111. package/lib/token-usage.d.ts +0 -68
  112. package/lib/token-usage.js +0 -211
  113. package/lib/tool.d.ts +0 -34
  114. package/lib/tool.js +0 -320
  115. package/lib/turn-snapshot.d.ts +0 -72
  116. package/lib/turn-snapshot.js +0 -238
  117. package/lib/types.d.ts +0 -208
  118. package/lib/types.js +0 -127
  119. package/lib/usage.d.ts +0 -36
  120. package/lib/usage.js +0 -98
  121. package/lib/validate.d.ts +0 -28
  122. package/lib/validate.js +0 -181
  123. package/lib/wrapup-command.d.ts +0 -10
  124. package/lib/wrapup-command.js +0 -243
  125. package/lib/wrapup.d.ts +0 -260
  126. package/lib/wrapup.js +0 -539
package/lib/inject.js DELETED
@@ -1,412 +0,0 @@
1
- import { MEMORY_TYPE_KEY, isArchived, isMemoryType, VALENCE_NEGATIVE_KEY } from "./types.js";
2
- import { mergeHarnessStates } from "./state.js";
3
- import { projectKeyOf } from "./project.js";
4
- import { entryLine } from "./render.js";
5
- import { recordInjection } from "./usage.js";
6
- import { buildRelevanceIndex, relevanceScore, tokenize } from "./search.js";
7
- /** CJK-bigram tokenizer re-exported for ranking consumers (see search.ts). */
8
- export { tokenize };
9
- /** Prompt sections render at most this many entries per kind. */
10
- export const MAX_INJECTED_ENTRIES_PER_KIND = 6;
11
- /** Per-entry content budget inside the injected block (matches render.ts). */
12
- export const MAX_INJECTED_CONTENT_LENGTH = 180;
13
- /** How many `parentSession` hops a child walks to inherit entries. */
14
- export const MAX_PARENT_CHAIN_DEPTH = 8;
15
- /** Recency half-life for the injection ranking: an entry this old scores 0. */
16
- export const RECENCY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1000;
17
- /** At most this many recent user messages feed the relevance query. */
18
- export const MAX_QUERY_MESSAGES = 3;
19
- /** Query text handed to the relevance scorer is capped at this many chars. */
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
- }
37
- /** Stable dictionary-order tiebreak used when two entries score equally. */
38
- function stableCompare(a, b) {
39
- return [a.path, a.title, a.id].join("\0").localeCompare([b.path, b.title, b.id].join("\0"));
40
- }
41
- /**
42
- * Normalized recency in [0, 1]: 1 when the entry was just updated, decaying
43
- * linearly to 0 after {@link RECENCY_HALF_LIFE_MS}. Unparseable timestamps
44
- * score 0 (never preferred over a timestamped entry).
45
- */
46
- export function recencyScore(entry, now) {
47
- const updatedAt = Date.parse(entry.updated_at);
48
- if (Number.isNaN(updatedAt)) {
49
- return 0;
50
- }
51
- const age = now - updatedAt;
52
- if (age <= 0) {
53
- return 1;
54
- }
55
- return Math.max(0, 1 - age / RECENCY_HALF_LIFE_MS);
56
- }
57
- /**
58
- * Negative-valence counter (P1 效价反馈): entries contradicted by later
59
- * assessments sink below clean ones at equal relevance/recency — the
60
- * behavioral analog of a confidence penalty (pi-continuous-learning's
61
- * contradicted −0.15), without inventing a new score axis.
62
- */
63
- function negativeValence(entry) {
64
- const value = entry.metadata[VALENCE_NEGATIVE_KEY];
65
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
66
- }
67
- /**
68
- * Rank entries for injection, best first. With no query the ranking is
69
- * negative-valence first (contradicted entries last), then pure recency
70
- * (newest first). With a query, entries are scored once against a per-call
71
- * BM25 index (CJK bigrams; field-weighted title ×2 — see search.ts): any
72
- * entry with a positive score (≥1 matched token) outranks every hit-less
73
- * entry (score exactly 0), scores decide the order among relevant entries,
74
- * the negative-valence counter breaks remaining ties (contradicted entries
75
- * sink), recency breaks remaining ties, and the stable dictionary order is
76
- * the final tiebreak, so the result is deterministic. The input is never
77
- * mutated.
78
- */
79
- export function rankEntries(entries, query, now = Date.now()) {
80
- const q = (query ?? "").trim();
81
- if (q.length === 0) {
82
- return [...entries].sort((a, b) => {
83
- const valenceDelta = negativeValence(a) - negativeValence(b);
84
- if (valenceDelta !== 0) {
85
- return valenceDelta;
86
- }
87
- const recencyDelta = recencyScore(b, now) - recencyScore(a, now);
88
- if (recencyDelta !== 0) {
89
- return recencyDelta;
90
- }
91
- return stableCompare(a, b);
92
- });
93
- }
94
- // Precompute scores once: the old comparator re-tokenized both sides on
95
- // every comparison (O(n log n) tokenizations); one index + one score per
96
- // entry turns the pass into table lookups.
97
- const index = buildRelevanceIndex(entries);
98
- const scores = new Map(entries.map((entry) => [entry, relevanceScore(index, entry, q)]));
99
- return [...entries].sort((a, b) => {
100
- const relevanceDelta = (scores.get(b) ?? 0) - (scores.get(a) ?? 0);
101
- if (relevanceDelta !== 0) {
102
- return relevanceDelta;
103
- }
104
- const valenceDelta = negativeValence(a) - negativeValence(b);
105
- if (valenceDelta !== 0) {
106
- return valenceDelta;
107
- }
108
- const recencyDelta = recencyScore(b, now) - recencyScore(a, now);
109
- if (recencyDelta !== 0) {
110
- return recencyDelta;
111
- }
112
- return stableCompare(a, b);
113
- });
114
- }
115
- function sortedEntries(entries, query) {
116
- return rankEntries(entries, query);
117
- }
118
- /**
119
- * Extract the text of a message's content blocks without depending on the
120
- * dsh-llm ContentBlock type: string blocks pass through, object blocks
121
- * contribute their `text` field when present.
122
- */
123
- function extractBlockText(content) {
124
- if (typeof content === "string") {
125
- return content;
126
- }
127
- if (!Array.isArray(content)) {
128
- return "";
129
- }
130
- return content
131
- .map((block) => {
132
- if (typeof block === "string") {
133
- return block;
134
- }
135
- if (block !== null && typeof block === "object" && "text" in block && typeof block.text === "string") {
136
- return block.text;
137
- }
138
- return "";
139
- })
140
- .filter((text) => text.length > 0)
141
- .join(" ");
142
- }
143
- /**
144
- * Compose the relevance query from the assembling agent's most recent direct
145
- * user messages (event rows whose `type` is `user/message` and whose source
146
- * is a human `user`, so injected plugin context and tool results never leak
147
- * into the query). Returns "" when nothing qualifies — the ranking then
148
- * falls back to pure recency.
149
- */
150
- export function recentUserText(agent, opts) {
151
- const events = sessionEventsOf(agent);
152
- if (events.length === 0) {
153
- return "";
154
- }
155
- const maxMessages = opts?.maxMessages ?? MAX_QUERY_MESSAGES;
156
- const maxChars = opts?.maxChars ?? MAX_QUERY_CHARS;
157
- const parts = [];
158
- for (let i = events.length - 1; i >= 0 && parts.length < maxMessages; i -= 1) {
159
- const event = events[i];
160
- if (event?.type !== "user/message") {
161
- continue;
162
- }
163
- const source = event.data?.source;
164
- if (source && source.kind !== "user") {
165
- continue;
166
- }
167
- const text = extractBlockText(event.data?.content).trim();
168
- if (text.length > 0) {
169
- parts.unshift(text);
170
- }
171
- }
172
- return parts.join(" ").slice(0, maxChars);
173
- }
174
- /** True when the state carries at least one entry of any kind. */
175
- export function hasAnyEntries(state) {
176
- return Object.values(state.entries).some((byKind) => Object.keys(byKind).length > 0);
177
- }
178
- /** The additive prompt-notes block (empty when there are no visible prompt entries). */
179
- export function formatPromptEntriesSection(entries, query) {
180
- const visible = entries.filter((entry) => !isArchived(entry));
181
- if (visible.length === 0) {
182
- return "";
183
- }
184
- const lines = [
185
- "# Continual Harness — Prompt Notes",
186
- "Supplemental prompt notes (the base system prompt is immutable). Use evolve_list for the full text of any note.",
187
- ];
188
- for (const entry of sortedEntries(visible, query).slice(0, MAX_INJECTED_ENTRIES_PER_KIND)) {
189
- lines.push(entryLine(entry, MAX_INJECTED_CONTENT_LENGTH));
190
- }
191
- const overflow = visible.length - Math.min(visible.length, MAX_INJECTED_ENTRIES_PER_KIND);
192
- if (overflow > 0) {
193
- lines.push(`- +${overflow} more prompt notes (evolve_list)`);
194
- }
195
- return lines.join("\n");
196
- }
197
- /** The reusable delegation-specs block (empty when there are no visible subagent entries). */
198
- export function formatSubagentSpecsSection(entries, query) {
199
- const visible = entries.filter((entry) => !isArchived(entry));
200
- if (visible.length === 0) {
201
- return "";
202
- }
203
- const lines = [
204
- "# Continual Harness — Delegation Specs",
205
- "Reusable subagent specs: when you delegate work that matches a spec, assemble the child prompt from its content. Children inherit these specs through their parent chain.",
206
- ];
207
- for (const entry of sortedEntries(visible, query).slice(0, MAX_INJECTED_ENTRIES_PER_KIND)) {
208
- lines.push(entryLine(entry, MAX_INJECTED_CONTENT_LENGTH));
209
- }
210
- const overflow = visible.length - Math.min(visible.length, MAX_INJECTED_ENTRIES_PER_KIND);
211
- if (overflow > 0) {
212
- lines.push(`- +${overflow} more delegation specs (evolve_list)`);
213
- }
214
- return lines.join("\n");
215
- }
216
- /**
217
- * Gap B3: a lightweight directory of ALL non-archived entries across all
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.
224
- *
225
- * 2026-08-22 throttle: the directory is CAPPED at {@link DEFAULT_DIRECTORY_LINES}
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.
233
- */
234
- export const DEFAULT_DIRECTORY_LINES = 15;
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. */
248
- const CONTENT_SECTION_KINDS = 2;
249
- export function formatEntriesDirectory(...kindEntries) {
250
- return formatEntriesDirectoryCapped(DEFAULT_DIRECTORY_LINES, ...kindEntries);
251
- }
252
- /** {@link formatEntriesDirectory} with an explicit cap (configurable). */
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) {
290
- const allEntries = kindEntries.flat().filter((e) => !isArchived(e));
291
- if (allEntries.length === 0) {
292
- return [];
293
- }
294
- // Skip the directory only when EVERY entry is already content-visible.
295
- // Only the first two arrays (prompt, subagent) have curated sections —
296
- // memories and skills have NO content injection, so they are invisible
297
- // unless the directory lists them (pre-2026-08-22 the redundancy check
298
- // wrongly counted them as "already shown", hiding small stores entirely).
299
- const contentVisible = kindEntries
300
- .slice(0, CONTENT_SECTION_KINDS)
301
- .reduce((sum, entries) => sum + Math.min(entries.filter((e) => !isArchived(e)).length, MAX_INJECTED_ENTRIES_PER_KIND), 0);
302
- if (allEntries.length <= contentVisible) {
303
- return [];
304
- }
305
- return rankEntries(allEntries, query).slice(0, Math.max(maxLines, 1));
306
- }
307
- /**
308
- * Walk the parent-session chain from `agent` upward and return the nearest
309
- * session whose local store is non-empty, if any. Children inherit their
310
- * ancestor's prompt notes and delegation specs; the chain walk stops at the
311
- * first store that has entries (deep descendants do not re-inject ancestors
312
- * beyond the nearest carrying store).
313
- */
314
- export function nearestLocalStateWithEntries(engine, agent) {
315
- let cursor = agent;
316
- for (let depth = 0; cursor !== undefined && depth < MAX_PARENT_CHAIN_DEPTH; depth += 1) {
317
- const state = engine.load("local", cursor.id);
318
- if (hasAnyEntries(state)) {
319
- return state;
320
- }
321
- cursor = cursor.session?.header?.parentSession
322
- ? { id: cursor.session.header.parentSession }
323
- : undefined;
324
- }
325
- return undefined;
326
- }
327
- /**
328
- * The single three-store merge every injection path reads from: the global
329
- * store merged with this project's store (when the session cwd resolves one)
330
- * and the nearest carrying local store (precedence global < project < local —
331
- * see {@link mergeHarnessStates}). One home, so the memory section and the
332
- * entry directory can never disagree about what the model can see.
333
- */
334
- export function mergedInjectionState(engine, agent, opts) {
335
- const globalState = engine.load("global", undefined);
336
- const projectKey = opts?.projectKey ?? projectKeyOf(agent);
337
- const projectState = projectKey ? engine.load("project", projectKey) : undefined;
338
- const localState = nearestLocalStateWithEntries(engine, agent);
339
- return mergeHarnessStates(globalState, localState, projectState ? { projectState } : undefined);
340
- }
341
- /**
342
- * Compose the full injected block for one assembling agent: global entries
343
- * merged with this project's store (when the session cwd resolves one) and
344
- * the nearest carrying local store (precedence global < project < local).
345
- * The optional `query` — when absent, derived from the agent's most recent
346
- * direct user messages — ranks which entries fill the per-kind cap
347
- * (relevance first, then recency; see {@link rankEntries}). Returns "" when
348
- * nothing is injectable — the prompt renderer then drops the section, so an
349
- * empty store adds zero tokens to every assembly.
350
- *
351
- * `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle);
352
- * `opts.projectKey` pins the project layer explicitly (tests, tools) —
353
- * otherwise it is derived from the agent's session cwd, best-effort.
354
- * `opts.includeMemoryDirectory` (default true) drops memory lines from the
355
- * directory when the memory section already carries them (evolve v2) — one
356
- * fact, one home, so the same memories are never paid for twice.
357
- * Usage recording covers ALL kinds — memories and skills appear as directory
358
- * lines, prompts/subagents as content — and is deduped per session so the
359
- * counts read "how many sessions saw this", not "how many prompt builds".
360
- */
361
- export function entriesSectionText(engine, agent, query, opts) {
362
- if (!agent) {
363
- return "";
364
- }
365
- const merged = mergedInjectionState(engine, agent, opts);
366
- const promptEntries = Object.values(merged.entries.prompt);
367
- const subagentEntries = Object.values(merged.entries.subagent);
368
- const relevanceQuery = (query ?? recentUserText(agent)).trim();
369
- // Build injected text and collect which entries were included (gap B1).
370
- const promptText = formatPromptEntriesSection(promptEntries, relevanceQuery);
371
- const subagentText = formatSubagentSpecsSection(subagentEntries, relevanceQuery);
372
- const injectedKeys = new Set();
373
- // Collect keys from the visible (ranked, capped) entries that actually appear.
374
- const visiblePrompt = promptEntries.filter((e) => !isArchived(e));
375
- const visibleSubagent = subagentEntries.filter((e) => !isArchived(e));
376
- for (const entry of rankEntries(visiblePrompt, relevanceQuery).slice(0, MAX_INJECTED_ENTRIES_PER_KIND)) {
377
- injectedKeys.add(`prompt:${entry.id}`);
378
- }
379
- for (const entry of rankEntries(visibleSubagent, relevanceQuery).slice(0, MAX_INJECTED_ENTRIES_PER_KIND)) {
380
- injectedKeys.add(`subagent:${entry.id}`);
381
- }
382
- // Gap B3: lightweight directory of ALL entries (id+title, one line each).
383
- // Zero-cost index so the model knows what exists and can ask for full text.
384
- const directoryLines = opts?.directoryLines ?? DEFAULT_DIRECTORY_LINES;
385
- const promptKind = Object.values(merged.entries.prompt);
386
- const subagentKind = Object.values(merged.entries.subagent);
387
- // v2: memories live in their own content-carrying section, so the directory
388
- // stops listing them (passing [] keeps the positional contract intact).
389
- const memoryKind = (opts?.includeMemoryDirectory ?? true) ? Object.values(merged.entries.memory) : [];
390
- const skillKind = Object.values(merged.entries.skill);
391
- const directoryText = formatEntriesDirectoryRanked(directoryLines, relevanceQuery, promptKind, subagentKind, memoryKind, skillKind);
392
- // Directory-visible keys count too: a memory's injection IS its directory
393
- // line. Set semantics keep content-injected entries single-counted. Keys
394
- // come from the single display-ordered source (type hooks included), not
395
- // substring matching on the rendered text.
396
- for (const entry of rankedDirectoryEntries(directoryLines, relevanceQuery, promptKind, subagentKind, memoryKind, skillKind)) {
397
- injectedKeys.add(`${entry.kind}:${entry.id}`);
398
- }
399
- // Record usage durably (best-effort: failure never blocks injection).
400
- // Deduped per session — see recordInjection.
401
- if (injectedKeys.size > 0) {
402
- try {
403
- recordInjection(engine.baseDir, [...injectedKeys], agent.id);
404
- }
405
- catch {
406
- // Usage recording is diagnostic; never interrupt the injection path.
407
- }
408
- }
409
- const parts = [promptText, subagentText, directoryText].filter((part) => part.length > 0);
410
- return parts.join("\n\n");
411
- }
412
- //# sourceMappingURL=inject.js.map
package/lib/llm-text.d.ts DELETED
@@ -1,104 +0,0 @@
1
- /**
2
- * Unified LLM text call: a shared streaming-text helper used by the review
3
- * gate, planner, and wrap-up assessor. Eliminates ~120 lines of duplicated
4
- * BlockAssembler + finish-state-check + text-extraction boilerplate.
5
- *
6
- * Every caller needs the same sequence:
7
- * provider/model validation → stream → assemble → check finish → extract text
8
- * This module owns that sequence; callers keep only their prompt construction
9
- * and JSON parsing.
10
- */
11
- import type { Context } from "@deepseek-ai/cordis";
12
- import { type ContentBlock, type GenerateOptions, type LlmResolvedModelInfo, type Message, type ReasoningEffortId, type RequestMessage, type TokenUsage, type ToolSchema } 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
- /** Host-owned session identity type used by provider routing metadata. */
16
- export type LlmSessionId = NonNullable<GenerateOptions["sessionId"]>;
17
- /** Carry the host Agent id across the plugin's direct-call boundary. */
18
- export declare function asLlmSessionId(value: string): LlmSessionId;
19
- /** Provider usage and terminal result observed after the stream settles. */
20
- export interface StreamTextObservation {
21
- usage?: TokenUsage;
22
- outcome: StreamTextOutcome;
23
- /** Exact capability-derived effort used for this call; absent when no metadata was exposed. */
24
- reasoningEffort?: ReasoningEffortId;
25
- }
26
- /** Diagnostic observer invoked before streamText returns or throws. */
27
- export type StreamTextUsageObserver = (observation: StreamTextObservation) => void;
28
- export interface StreamTextOptions {
29
- provider: string;
30
- model: string;
31
- system: string;
32
- prompt: string;
33
- maxTokens?: number;
34
- signal?: AbortSignal | undefined;
35
- /** Host session identity forwarded to adapters for request routing. */
36
- sessionId?: LlmSessionId;
37
- /**
38
- * Route A session prefix: session-derived messages sent before the
39
- * trailing caller message so providers with prompt caching can serve
40
- * the shared context at cache-read price. Empty/omitted keeps the
41
- * legacy single-message request.
42
- */
43
- prefixMessages?: readonly Message[];
44
- /**
45
- * Observe the provider-reported usage and terminal outcome after the
46
- * stream settles. The callback runs before success/error return and is
47
- * diagnostic-only: throwing from it never changes the model call result.
48
- */
49
- onUsage?: StreamTextUsageObserver;
50
- }
51
- /** One provider request for a bounded internal agent/tool loop. */
52
- export interface StreamModelTurnOptions {
53
- provider: string;
54
- model: string;
55
- messages: readonly RequestMessage[];
56
- /** One-shot system slot. Omit when messages already carry the system prompt. */
57
- system?: string;
58
- /** Closed tool schemas for this request; omitted means no tools. */
59
- tools?: readonly ToolSchema[];
60
- maxTokens?: number;
61
- signal?: AbortSignal | undefined;
62
- /** Host session identity forwarded to adapters for request routing. */
63
- sessionId?: LlmSessionId;
64
- /** Require at least one text block; tool-only turns must leave this false. */
65
- requireText?: boolean;
66
- /** Require visible text or a tool call; reasoning-only agent turns are empty. */
67
- requireTextOrToolCall?: boolean;
68
- /** Same diagnostic observer contract as {@link StreamTextOptions.onUsage}. */
69
- onUsage?: StreamTextUsageObserver;
70
- }
71
- /**
72
- * Select the lowest reasoning effort an exact model advertises.
73
- *
74
- * An enabled effort wins over a declared closing effort. The adapter-preferred
75
- * first enabled effort is the lowest enabled level it exposes; a closing effort
76
- * is only a fallback when the model publishes no enabled level. No capability
77
- * list means no caller-supplied effort.
78
- *
79
- * @param modelInfo - exact resolved-model metadata, when the provider exposes it.
80
- * @returns The selected provider-owned effort id, or `undefined` when unknown.
81
- */
82
- export declare function selectLowestReasoningEffort(modelInfo: Pick<LlmResolvedModelInfo, "reasoning"> | undefined): ReasoningEffortId | undefined;
83
- /**
84
- * Stream a single-turn text completion through `ctx.llm`. The request uses the
85
- * exact provider/model's lowest advertised reasoning effort, omitting the field
86
- * when the model exposes no reasoning metadata.
87
- *
88
- * @returns The concatenated text blocks from the response.
89
- * @throws On provider error, abort, max-token truncation, or empty output.
90
- */
91
- /**
92
- * Stream one provider/model turn and return its assembled content blocks.
93
- * This is the shared primitive for text callers and bounded tool loops; finish
94
- * state, capability-derived effort, cancellation, and usage observation stay
95
- * inside this boundary.
96
- *
97
- * @param ctx - DSH context providing the LLM runtime.
98
- * @param opts - Exact route, messages, optional tools, and output requirements.
99
- * @returns The assembled content blocks; caller decides how to interpret them.
100
- * @throws On provider error, abort, max-token truncation, or capability lookup failure.
101
- */
102
- export declare function streamModelTurn(ctx: Context, opts: StreamModelTurnOptions): Promise<ContentBlock[]>;
103
- export declare function streamText(ctx: Context, opts: StreamTextOptions): Promise<string>;
104
- //# sourceMappingURL=llm-text.d.ts.map
package/lib/llm-text.js DELETED
@@ -1,140 +0,0 @@
1
- import { BlockAssembler, createUserMessage, } from "@deepseek-ai/dsh-llm";
2
- import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
3
- /** Carry the host Agent id across the plugin's direct-call boundary. */
4
- export function asLlmSessionId(value) {
5
- return value;
6
- }
7
- const CLOSED_REASONING_EFFORTS = new Set(["disabled", "off", "none"]);
8
- /**
9
- * Select the lowest reasoning effort an exact model advertises.
10
- *
11
- * An enabled effort wins over a declared closing effort. The adapter-preferred
12
- * first enabled effort is the lowest enabled level it exposes; a closing effort
13
- * is only a fallback when the model publishes no enabled level. No capability
14
- * list means no caller-supplied effort.
15
- *
16
- * @param modelInfo - exact resolved-model metadata, when the provider exposes it.
17
- * @returns The selected provider-owned effort id, or `undefined` when unknown.
18
- */
19
- export function selectLowestReasoningEffort(modelInfo) {
20
- const efforts = modelInfo?.reasoning?.efforts;
21
- if (efforts !== undefined && !Array.isArray(efforts)) {
22
- throw new Error("evolve: invalid model reasoning metadata");
23
- }
24
- if (!efforts || efforts.length === 0)
25
- return undefined;
26
- for (const effort of efforts) {
27
- if (typeof effort?.id !== "string") {
28
- throw new Error("evolve: invalid model reasoning metadata");
29
- }
30
- }
31
- return efforts.find((effort) => !CLOSED_REASONING_EFFORTS.has(effort.id))?.id ?? efforts[0]?.id;
32
- }
33
- async function resolveReasoningEffort(ctx, opts) {
34
- // The current DSH host exposes this method. Keep a capability-less test or
35
- // older host usable by treating the absent query as unknown metadata.
36
- if (typeof ctx.llm.resolveModelInfo !== "function")
37
- return undefined;
38
- const modelInfo = await ctx.llm.resolveModelInfo(opts.provider, opts.model);
39
- return selectLowestReasoningEffort(modelInfo);
40
- }
41
- /**
42
- * Stream a single-turn text completion through `ctx.llm`. The request uses the
43
- * exact provider/model's lowest advertised reasoning effort, omitting the field
44
- * when the model exposes no reasoning metadata.
45
- *
46
- * @returns The concatenated text blocks from the response.
47
- * @throws On provider error, abort, max-token truncation, or empty output.
48
- */
49
- /**
50
- * Stream one provider/model turn and return its assembled content blocks.
51
- * This is the shared primitive for text callers and bounded tool loops; finish
52
- * state, capability-derived effort, cancellation, and usage observation stay
53
- * inside this boundary.
54
- *
55
- * @param ctx - DSH context providing the LLM runtime.
56
- * @param opts - Exact route, messages, optional tools, and output requirements.
57
- * @returns The assembled content blocks; caller decides how to interpret them.
58
- * @throws On provider error, abort, max-token truncation, or capability lookup failure.
59
- */
60
- export async function streamModelTurn(ctx, opts) {
61
- const assembler = new BlockAssembler();
62
- let outcome = "error";
63
- let reasoningEffort;
64
- try {
65
- reasoningEffort = await resolveReasoningEffort(ctx, opts);
66
- for await (const chunk of ctx.llm.stream({
67
- provider: opts.provider,
68
- model: opts.model,
69
- messages: [...opts.messages],
70
- ...(opts.system === undefined ? {} : { system: opts.system }),
71
- ...(opts.tools === undefined ? {} : { tools: [...opts.tools] }),
72
- ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
73
- maxTokens: opts.maxTokens ?? 8000,
74
- ...(opts.signal ? { signal: opts.signal } : {}),
75
- ...(opts.sessionId ? { sessionId: opts.sessionId } : {}),
76
- })) {
77
- assembler.push(chunk);
78
- }
79
- const finish = assembler.finish;
80
- if (finish.kind === "error") {
81
- outcome = "error";
82
- throw new Error(`evolve: LLM call failed: ${finish.failure?.message ?? "unknown"}`);
83
- }
84
- if (finish.kind === "aborted") {
85
- outcome = "aborted";
86
- throw new Error("evolve: LLM call aborted");
87
- }
88
- if (finish.kind === "max-tokens") {
89
- outcome = "max-tokens";
90
- throw new Error("evolve: LLM output budget exhausted (max-tokens)");
91
- }
92
- const blocks = assembler.blocks();
93
- const hasText = blocks.some((block) => block.type === "text" && block.text.length > 0);
94
- const hasToolCall = blocks.some((block) => block.type === "tool-call");
95
- const missingRequiredOutput = (opts.requireText === true && !hasText)
96
- || (opts.requireTextOrToolCall === true && !hasText && !hasToolCall);
97
- outcome = blocks.length === 0 || missingRequiredOutput ? "empty" : "success";
98
- return blocks;
99
- }
100
- finally {
101
- try {
102
- opts.onUsage?.({
103
- ...(assembler.usage ? { usage: assembler.usage } : {}),
104
- outcome,
105
- ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
106
- });
107
- }
108
- catch {
109
- // Usage observation is diagnostic; its failure must not alter the model call.
110
- }
111
- }
112
- }
113
- export async function streamText(ctx, opts) {
114
- const blocks = await streamModelTurn(ctx, {
115
- provider: opts.provider,
116
- model: opts.model,
117
- system: opts.system,
118
- messages: [
119
- ...(opts.prefixMessages ?? []),
120
- createUserMessage({
121
- content: [{ type: "text", text: opts.prompt }],
122
- source: EVOLVE_MESSAGE_SOURCE,
123
- }),
124
- ],
125
- requireText: true,
126
- ...(opts.maxTokens === undefined ? {} : { maxTokens: opts.maxTokens }),
127
- ...(opts.signal ? { signal: opts.signal } : {}),
128
- ...(opts.sessionId ? { sessionId: opts.sessionId } : {}),
129
- ...(opts.onUsage ? { onUsage: opts.onUsage } : {}),
130
- });
131
- const text = blocks
132
- .filter((block) => block.type === "text")
133
- .map((block) => block.text)
134
- .join("\n");
135
- if (text.length === 0) {
136
- throw new Error("evolve: LLM produced no text output");
137
- }
138
- return text;
139
- }
140
- //# sourceMappingURL=llm-text.js.map