dsh-continual-evolve 0.7.0 → 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/lib/tool.d.ts CHANGED
@@ -11,6 +11,15 @@ import type { EvolutionEngine } from "./service.js";
11
11
  * parameter (`global: true`) as fallback. `scope` wins when both are given.
12
12
  */
13
13
  export declare function scopeOf(value: unknown, fallback: HarnessScope): HarnessScope;
14
+ /**
15
+ * Collect the target ids for an evolve_delete call: legacy single `id`
16
+ * plus the batch `ids` array, de-duplicated in first-seen order. Throws
17
+ * loudly when neither carries an id — a silent no-op delete would look
18
+ * like success while deleting nothing.
19
+ *
20
+ * @throws when both `id` and `ids` are absent or empty.
21
+ */
22
+ export declare function collectDeleteIds(id: unknown, ids: unknown): string[];
14
23
  export interface ToolGateOptions {
15
24
  requireGlobalApproval: boolean;
16
25
  }
package/lib/tool.js CHANGED
@@ -7,6 +7,7 @@ import { CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry } from "./pr
7
7
  import { entrySourceOf } from "./source.js";
8
8
  import { getUsageCount, loadUsage } from "./usage.js";
9
9
  import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
10
+ import { DEFAULT_REVIEWS_RETAIN } from "./store.js";
10
11
  const SCOPES = ["local", "project", "global"];
11
12
  /**
12
13
  * Accept the string form (`scope: "project"`) with the legacy boolean tool
@@ -25,6 +26,30 @@ export function scopeOf(value, fallback) {
25
26
  function sessionIdOf(exec) {
26
27
  return exec.agent?.id;
27
28
  }
29
+ /**
30
+ * Collect the target ids for an evolve_delete call: legacy single `id`
31
+ * plus the batch `ids` array, de-duplicated in first-seen order. Throws
32
+ * loudly when neither carries an id — a silent no-op delete would look
33
+ * like success while deleting nothing.
34
+ *
35
+ * @throws when both `id` and `ids` are absent or empty.
36
+ */
37
+ export function collectDeleteIds(id, ids) {
38
+ const collected = [];
39
+ if (typeof id === "string" && id.length > 0)
40
+ collected.push(id);
41
+ if (Array.isArray(ids)) {
42
+ for (const item of ids) {
43
+ if (typeof item === "string" && item.length > 0 && !collected.includes(item)) {
44
+ collected.push(item);
45
+ }
46
+ }
47
+ }
48
+ if (collected.length === 0) {
49
+ throw new Error("evolve_delete requires id or a non-empty ids array");
50
+ }
51
+ return collected;
52
+ }
28
53
  /**
29
54
  * The store id a scope reads/writes: session id for local, derived project
30
55
  * key for project, undefined for global. Throws for project when the
@@ -177,10 +202,15 @@ export function registerEvolveTools(ctx, engine, opts) {
177
202
  }));
178
203
  ctx.tools.register(defineTool({
179
204
  name: "evolve_delete",
180
- description: "Delete one harness entry by id.",
205
+ description: "Delete harness entries by id. Pass `id` for one entry or `ids` for several — a batch lands as one refinement behind one approval. Literal ids win: entries whose stored id itself carries a scope-like prefix (e.g. a legacy `local:…` id) are addressed verbatim.",
181
206
  parameters: {
182
207
  kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
183
- id: { type: "string", required: true },
208
+ id: { type: "string", description: "Existing entry id (single delete; combine with ids or use ids for a batch)." },
209
+ ids: {
210
+ type: "array",
211
+ items: { type: "string" },
212
+ description: "Existing entry ids for one batch delete (one refinement, one approval).",
213
+ },
184
214
  scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
185
215
  global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
186
216
  },
@@ -191,11 +221,12 @@ export function registerEvolveTools(ctx, engine, opts) {
191
221
  execute: async (args, exec) => {
192
222
  const scope = scopeOf(args.scope ?? args.global, "local");
193
223
  const storeId = storeIdFor(scope, exec);
224
+ const ids = collectDeleteIds(args.id, args.ids);
194
225
  if (needsApproval(scope) && opts.requireGlobalApproval) {
195
- await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${args.id} → ${storeLabel(scope)}`);
226
+ await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${ids.length > 1 ? `${ids.length} entries (${ids.join(", ")})` : ids[0]} → ${storeLabel(scope)}`);
196
227
  }
197
- const edit = { action: "delete", kind: args.kind, id: args.id };
198
- return textResult(applyEditsText(engine, scope, storeId, [edit], exec.agent, sessionIdOf(exec)));
228
+ const edits = ids.map((id) => ({ action: "delete", kind: args.kind, id }));
229
+ return textResult(applyEditsText(engine, scope, storeId, edits, exec.agent, sessionIdOf(exec)));
199
230
  },
200
231
  }));
201
232
  ctx.tools.register(defineTool({
@@ -235,7 +266,7 @@ function applyEditsText(engine, scope, storeId, edits, agent, eventSessionId) {
235
266
  // The event keys on the live session (not the project store key).
236
267
  const eventSession = eventSessionId ?? storeId;
237
268
  if (applied.length > 0 && eventSession) {
238
- emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", eventSession));
269
+ emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", eventSession), engine.retention?.reviews ?? DEFAULT_REVIEWS_RETAIN);
239
270
  }
240
271
  const lines = [`refinement ${result.id}: ${applied.length} applied, ${failed.length} failed`];
241
272
  for (const e of applied) {
@@ -13,7 +13,16 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
13
13
  return success(`(nothing to wrap up: ${sessionId}'s local store has no active, un-promoted entries — use /evolve list to inspect it)`);
14
14
  }
15
15
  // 1. Classify: the model judges each audited candidate's fate.
16
- const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, { signal: invocation.signal });
16
+ const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, {
17
+ signal: invocation.signal,
18
+ tokenUsage: {
19
+ baseDir: engine.baseDir,
20
+ sessionId,
21
+ retain: engine.retention.tokenUsage,
22
+ onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
23
+ },
24
+ tokenUsagePhase: "wrapup",
25
+ });
17
26
  const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
18
27
  // 2. Partition by action. Deterministic guards re-check the LIVE global
19
28
  // store right before anything lands (state may have changed mid-call).
package/lib/wrapup.d.ts CHANGED
@@ -21,6 +21,7 @@
21
21
  import type { Context } from "@deepseek-ai/cordis";
22
22
  import type { Agent } from "@deepseek-ai/dsh-agent";
23
23
  import type { HarnessEntry, HarnessState, RefinementKind, RefinementProposal } from "./types.js";
24
+ import { type TokenUsageTarget } from "./token-usage.js";
24
25
  import { type PromotionPolicy } from "./promotion.js";
25
26
  /** What should happen to one local entry at session end. */
26
27
  export type WrapupVerdict = "promote" | "archive" | "keep";
@@ -238,6 +239,10 @@ export interface AssessOptions {
238
239
  maxOutputTokens?: number;
239
240
  /** Abort signal forwarded to the model call. */
240
241
  signal?: AbortSignal;
242
+ /** Direct-call token ledger destination; phase is selected by the caller. */
243
+ tokenUsage?: TokenUsageTarget;
244
+ /** Ledger phase for this classifier call (manual wrapup or automatic fate). */
245
+ tokenUsagePhase?: "wrapup" | "fate";
241
246
  }
242
247
  /**
243
248
  * Ask the model to classify the audited local candidates. Routes through the
package/lib/wrapup.js CHANGED
@@ -4,6 +4,7 @@ import { extractJsonObject } from "./plan.js";
4
4
  import { compactText } from "./render.js";
5
5
  import { streamText } from "./llm-text.js";
6
6
  import { getUsageCount, loadUsage } from "./usage.js";
7
+ import { createTokenUsageObserver } from "./token-usage.js";
7
8
  import { recencyScore } from "./inject.js";
8
9
  import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason, secretLeakReason } from "./promotion.js";
9
10
  export function candidateKey(kind, id) {
@@ -525,6 +526,11 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
525
526
  prompt: userPrompt,
526
527
  maxTokens: options.maxOutputTokens ?? 4096,
527
528
  signal: options.signal,
529
+ ...(options.tokenUsage
530
+ ? {
531
+ onUsage: createTokenUsageObserver(options.tokenUsage, options.tokenUsagePhase ?? "wrapup", agent.options.provider, agent.options.model),
532
+ }
533
+ : {}),
528
534
  });
529
535
  return parseWrapupAssessment(text, candidates);
530
536
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
5
5
  "license": "MIT",
6
6
  "repository": {