dsh-continual-evolve 0.1.1 → 0.3.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 (64) hide show
  1. package/README.md +172 -17
  2. package/README.zh.md +87 -10
  3. package/lib/apply.js +3 -1
  4. package/lib/approval.d.ts +25 -0
  5. package/lib/approval.js +9 -1
  6. package/lib/auto.d.ts +99 -5
  7. package/lib/auto.js +165 -6
  8. package/lib/benchmark-command.d.ts +9 -0
  9. package/lib/benchmark-command.js +331 -0
  10. package/lib/benchmark.d.ts +84 -0
  11. package/lib/benchmark.js +107 -1
  12. package/lib/command.js +33 -221
  13. package/lib/evaluate.d.ts +43 -7
  14. package/lib/evaluate.js +172 -43
  15. package/lib/evolve-event.d.ts +38 -0
  16. package/lib/evolve-event.js +49 -0
  17. package/lib/failures.d.ts +39 -0
  18. package/lib/failures.js +170 -0
  19. package/lib/fate.d.ts +128 -0
  20. package/lib/fate.js +342 -0
  21. package/lib/goal-command.d.ts +7 -0
  22. package/lib/goal-command.js +37 -0
  23. package/lib/index.d.ts +51 -21
  24. package/lib/index.js +32 -2
  25. package/lib/inject.d.ts +8 -0
  26. package/lib/inject.js +51 -4
  27. package/lib/llm-text.d.ts +30 -0
  28. package/lib/llm-text.js +49 -0
  29. package/lib/mount-command.d.ts +10 -0
  30. package/lib/mount-command.js +48 -0
  31. package/lib/mount.js +5 -0
  32. package/lib/plan.js +5 -0
  33. package/lib/planner.d.ts +8 -1
  34. package/lib/planner.js +40 -39
  35. package/lib/render.d.ts +1 -3
  36. package/lib/render.js +2 -5
  37. package/lib/review.d.ts +5 -2
  38. package/lib/review.js +27 -38
  39. package/lib/rollback.d.ts +1 -3
  40. package/lib/rollback.js +0 -8
  41. package/lib/score.d.ts +37 -4
  42. package/lib/score.js +120 -10
  43. package/lib/service.d.ts +2 -2
  44. package/lib/service.js +5 -2
  45. package/lib/skill-render.d.ts +15 -0
  46. package/lib/skill-render.js +30 -0
  47. package/lib/skill.d.ts +12 -7
  48. package/lib/skill.js +36 -31
  49. package/lib/skillquality.d.ts +80 -0
  50. package/lib/skillquality.js +311 -0
  51. package/lib/store.d.ts +1 -3
  52. package/lib/store.js +0 -7
  53. package/lib/tool.js +28 -4
  54. package/lib/types.d.ts +39 -0
  55. package/lib/types.js +19 -0
  56. package/lib/usage.d.ts +32 -0
  57. package/lib/usage.js +84 -0
  58. package/lib/validate.d.ts +12 -2
  59. package/lib/validate.js +51 -2
  60. package/lib/wrapup-command.d.ts +8 -0
  61. package/lib/wrapup-command.js +211 -0
  62. package/lib/wrapup.d.ts +215 -0
  63. package/lib/wrapup.js +427 -0
  64. package/package.json +8 -8
package/lib/auto.d.ts CHANGED
@@ -1,23 +1,84 @@
1
1
  import type { Context } from "@deepseek-ai/cordis";
2
- import type { HarnessState } from "./types.js";
2
+ import type { Agent } from "@deepseek-ai/dsh-agent";
3
+ import type { HarnessState, RefinementEdit, RefinementProposal } from "./types.js";
3
4
  import type { EvolutionEngine } from "./service.js";
5
+ import { type AutoRefineReason } from "./review.js";
4
6
  export interface AutoReviewConfig {
5
7
  intervalTurns: number;
6
8
  maxInputChars: number;
7
9
  budgetTokens: number;
8
10
  /** Queue a visible follow-up notice after an approved, applied gate run. */
9
11
  notifyOnAutoReview: boolean;
12
+ /**
13
+ * Local-fate dimension (#11 P2): the gate audits the session's local
14
+ * entries on its own cadence and proposes promote/archive (consulted
15
+ * first — never written silently). Off disables the whole dimension.
16
+ */
17
+ localFate: boolean;
18
+ /**
19
+ * Minimum turns between local-fate assessments on the turn-interval path
20
+ * (compaction is unconditional). Independent of the review cadence so
21
+ * goal-driven sessions (gate every round) do not pay an assessment per
22
+ * round.
23
+ */
24
+ fateIntervalTurns: number;
25
+ /**
26
+ * Gap C1: optional model override for the review gate (cheaper model).
27
+ * Format: "provider/model" or just "model" (same provider as the agent).
28
+ * When absent, the review gate uses the agent's own provider/model.
29
+ */
30
+ reviewModel?: string;
31
+ /**
32
+ * Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
33
+ * observe the session goal in phase "blocked", run one local-fate
34
+ * assessment (the same audit → classify → consult → apply pipeline as the
35
+ * gate's normal fate dimension) so the blocked encounter is distilled
36
+ * before the session moves on. 0 disables. The streak resets on any
37
+ * non-blocked run and after each triggered assessment; a declined
38
+ * proposal then follows the normal fate cooldown.
39
+ */
40
+ goalBlockedWrapupTurns: number;
10
41
  }
11
42
  export interface GateState {
12
43
  turns: number;
13
44
  lastReviewAt: number;
14
45
  running: boolean;
46
+ /**
47
+ * Per-candidate turn at which the user last rejected a skill proposal;
48
+ * consulted skill proposals are not offered again within the cooldown
49
+ * window (skills are governed resources — no nagging).
50
+ */
51
+ skillRejects: Map<string, number>;
52
+ /** Turn at which the local-fate dimension last assessed (cadence). */
53
+ lastFateAt: number;
54
+ /**
55
+ * Per-candidate-set turn at which the user last declined a local-fate
56
+ * proposal; declined sets are not offered again within the cooldown
57
+ * window (the consultSkillEdits pattern — no nagging).
58
+ */
59
+ fateRejects: Map<string, number>;
60
+ /**
61
+ * Consecutive gate runs that observed the goal phase "blocked" (D3).
62
+ * Reset to 0 by any non-blocked run and after a triggered assessment —
63
+ * see runGoalBlockedFate.
64
+ */
65
+ goalBlockStreak: number;
66
+ }
67
+ /** Turns a rejected skill candidate stays silent before being offered again. */
68
+ export declare const SKILL_CONSULT_COOLDOWN_TURNS = 10;
69
+ export interface ReviewRecord {
70
+ timestamp: string;
71
+ sessionId: string;
72
+ reason: AutoRefineReason;
73
+ turnsSinceLastReview: number;
74
+ outcome: "approved" | "declined" | "failed" | "assessed" | "deferred";
75
+ rationale?: string;
76
+ refinementId?: string;
15
77
  }
16
78
  /**
17
- * Count completed turns from agent/status transitions alone. The runtime
18
- * emits `agent/status` with a `{status}` payload and (per host consumers like
19
- * dsh-host-apiproxy) an injected `agent` subject; `agent/turn-stopping` does
20
- * not reliably carry the agent, so it is NOT used for counting.
79
+ * Count completed turns from agent/status transitions (running idle).
80
+ * Exported for unit testing; production counting uses agent/turn-stopping
81
+ * (see registerAutoReview) which empirically carries the agent subject.
21
82
  */
22
83
  export declare function advanceGateState(state: GateState, status: string): boolean;
23
84
  export declare function registerAutoReview(ctx: Context, engine: EvolutionEngine, config: AutoReviewConfig): void;
@@ -31,4 +92,37 @@ export declare function registerAutoReview(ctx: Context, engine: EvolutionEngine
31
92
  * local state (baseline checks compare local entries only).
32
93
  */
33
94
  export declare function loadGateHarnessView(engine: EvolutionEngine, sessionId: string): HarnessState;
95
+ /**
96
+ * D3 (goal blocked → wrap-up coupling, reverse direction): count consecutive
97
+ * gate runs whose goal is in phase "blocked"; when the streak reaches
98
+ * `goalBlockedWrapupTurns`, run ONE local-fate assessment (same pipeline as
99
+ * the normal fate dimension — audit, classify, consult, apply deterministically).
100
+ * The streak resets on any non-blocked run and after a triggered assessment;
101
+ * a declined proposal is then protected by the normal fate cooldown, so a
102
+ * blocked session can never be nagged into another dialog.
103
+ *
104
+ * Exported for unit testing (the advanceGateState precedent); production runs
105
+ * it from runGate.
106
+ */
107
+ export declare function runGoalBlockedFate(ctx: Context, engine: EvolutionEngine, agent: Agent, config: AutoReviewConfig, state: GateState, _reason: AutoRefineReason, record: (entry: Omit<ReviewRecord, "timestamp">) => void): Promise<void>;
108
+ /**
109
+ * Split a proposal into skill edits and everything else. Skill edits are the
110
+ * governed part: they need explicit user consent before the gate applies
111
+ * them, while the remaining edits flow through the normal auto path.
112
+ */
113
+ export declare function splitSkillEdits(proposal: RefinementProposal): {
114
+ skillEdits: RefinementEdit[];
115
+ otherEdits: RefinementEdit[];
116
+ };
117
+ /**
118
+ * Ask the user whether to solidify proposed skill edits (guidance or
119
+ * executable) into the harness. Returns true when every skill edit is
120
+ * consented. Never writes a skill silently:
121
+ * - no question service available → false (conservative);
122
+ * - the same candidate was rejected within the cooldown window → false
123
+ * without asking again (no nagging);
124
+ * - the user declines → false and the rejection is recorded for cooldown;
125
+ * - the question call fails/aborts → false (conservative).
126
+ */
127
+ export declare function consultSkillEdits(ctx: Context, agent: Agent, skillEdits: RefinementEdit[], gate: GateState): Promise<boolean>;
34
128
  //# sourceMappingURL=auto.d.ts.map
package/lib/auto.js CHANGED
@@ -17,17 +17,22 @@
17
17
  */
18
18
  import { appendFileSync, mkdirSync } from "node:fs";
19
19
  import { join } from "node:path";
20
+ import { slug } from "./types.js";
20
21
  import { planWithLlm } from "./planner.js";
21
22
  import { reviewAutoRefine, serializeSurface } from "./review.js";
22
23
  import { goalServiceOf } from "./goal.js";
23
24
  import { notifyAutoReview } from "./notify.js";
25
+ import { runLocalFatePhase } from "./fate.js";
24
26
  import { entrySourceOf } from "./source.js";
25
27
  import { mergeHarnessStates } from "./state.js";
28
+ import { questionServiceOf } from "./approval.js";
29
+ import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
30
+ /** Turns a rejected skill candidate stays silent before being offered again. */
31
+ export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
26
32
  /**
27
- * Count completed turns from agent/status transitions alone. The runtime
28
- * emits `agent/status` with a `{status}` payload and (per host consumers like
29
- * dsh-host-apiproxy) an injected `agent` subject; `agent/turn-stopping` does
30
- * not reliably carry the agent, so it is NOT used for counting.
33
+ * Count completed turns from agent/status transitions (running idle).
34
+ * Exported for unit testing; production counting uses agent/turn-stopping
35
+ * (see registerAutoReview) which empirically carries the agent subject.
31
36
  */
32
37
  export function advanceGateState(state, status) {
33
38
  if (status === "running") {
@@ -130,10 +135,31 @@ export function registerAutoReview(ctx, engine, config) {
130
135
  });
131
136
  });
132
137
  }
138
+ /**
139
+ * Gap C1: parse a "provider/model" or "model" string into its components.
140
+ * Returns undefined when the input is empty (no override).
141
+ */
142
+ function parseReviewModel(reviewModel, fallbackProvider) {
143
+ if (!reviewModel || reviewModel.trim().length === 0)
144
+ return undefined;
145
+ const slash = reviewModel.indexOf("/");
146
+ if (slash > 0) {
147
+ return { provider: reviewModel.slice(0, slash), model: reviewModel.slice(slash + 1) };
148
+ }
149
+ return { provider: fallbackProvider ?? "deepseek", model: reviewModel };
150
+ }
133
151
  function stateFor(map, sessionId) {
134
152
  let state = map.get(sessionId);
135
153
  if (!state) {
136
- state = { turns: 0, lastReviewAt: 0, running: false };
154
+ state = {
155
+ turns: 0,
156
+ lastReviewAt: 0,
157
+ running: false,
158
+ skillRejects: new Map(),
159
+ lastFateAt: 0,
160
+ fateRejects: new Map(),
161
+ goalBlockStreak: 0,
162
+ };
137
163
  map.set(sessionId, state);
138
164
  }
139
165
  return state;
@@ -150,7 +176,52 @@ function stateFor(map, sessionId) {
150
176
  export function loadGateHarnessView(engine, sessionId) {
151
177
  return mergeHarnessStates(engine.load("global", undefined), engine.load("local", sessionId));
152
178
  }
179
+ /**
180
+ * One gate run = review phase + local-fate phase (#11 P2). The review phase
181
+ * judges and applies local refinements; the local-fate phase then gives the
182
+ * session's existing local entries a running exit (promote/archive proposals,
183
+ * consulted before they land). Running fate AFTER the review keeps the
184
+ * review's baseline fresh — fate re-loads the store and never races the
185
+ * review's optimistic-concurrency checks.
186
+ */
153
187
  async function runGate(ctx, engine, agent, config, state, reason, record) {
188
+ await runReviewPhase(ctx, engine, agent, config, state, reason, record);
189
+ // D3: a goal stuck in "blocked" for consecutive gate runs gets one
190
+ // local-fate assessment (the pipeline below), so whatever led the goal
191
+ // astray is distilled before the session moves on.
192
+ await runGoalBlockedFate(ctx, engine, agent, config, state, reason, record);
193
+ await runLocalFatePhase(ctx, engine, agent, config, state, reason, record);
194
+ }
195
+ /**
196
+ * D3 (goal blocked → wrap-up coupling, reverse direction): count consecutive
197
+ * gate runs whose goal is in phase "blocked"; when the streak reaches
198
+ * `goalBlockedWrapupTurns`, run ONE local-fate assessment (same pipeline as
199
+ * the normal fate dimension — audit, classify, consult, apply deterministically).
200
+ * The streak resets on any non-blocked run and after a triggered assessment;
201
+ * a declined proposal is then protected by the normal fate cooldown, so a
202
+ * blocked session can never be nagged into another dialog.
203
+ *
204
+ * Exported for unit testing (the advanceGateState precedent); production runs
205
+ * it from runGate.
206
+ */
207
+ export async function runGoalBlockedFate(ctx, engine, agent, config, state, _reason, record) {
208
+ if (config.goalBlockedWrapupTurns <= 0)
209
+ return;
210
+ const goal = goalServiceOf(ctx)?.get(agent);
211
+ if (goal?.phase !== "blocked") {
212
+ state.goalBlockStreak = 0;
213
+ return;
214
+ }
215
+ state.goalBlockStreak += 1;
216
+ if (state.goalBlockStreak < config.goalBlockedWrapupTurns) {
217
+ return;
218
+ }
219
+ state.goalBlockStreak = 0; // one assessment per streak; declines follow the fate cooldown
220
+ const logger = ctx.logger("continual-evolve");
221
+ logger.info(`auto-review goal-blocked trigger [${agent.id}]: ${config.goalBlockedWrapupTurns} consecutive blocked gate runs → local-fate assessment`);
222
+ await runLocalFatePhase(ctx, engine, agent, config, state, "goal_blocked", record);
223
+ }
224
+ async function runReviewPhase(ctx, engine, agent, config, state, reason, record) {
154
225
  const sessionId = agent.id;
155
226
  const turnsSinceLastReview = state.turns - state.lastReviewAt;
156
227
  const logger = ctx.logger("continual-evolve");
@@ -169,6 +240,8 @@ async function runGate(ctx, engine, agent, config, state, reason, record) {
169
240
  const localState = engine.load("local", sessionId);
170
241
  const harnessState = loadGateHarnessView(engine, sessionId);
171
242
  const history = engine.history("local", sessionId);
243
+ // Gap C1: resolve optional review model override.
244
+ const reviewRoute = parseReviewModel(config.reviewModel, agent.options.provider);
172
245
  const review = await reviewAutoRefine(ctx, {
173
246
  agent,
174
247
  state: harnessState,
@@ -176,6 +249,7 @@ async function runGate(ctx, engine, agent, config, state, reason, record) {
176
249
  trajectory,
177
250
  context: { reason, turnsSinceLastReview },
178
251
  budgetTokens: config.budgetTokens,
252
+ ...(reviewRoute ? { overrideProvider: reviewRoute.provider, overrideModel: reviewRoute.model } : {}),
179
253
  });
180
254
  state.lastReviewAt = state.turns;
181
255
  if (!review.shouldRefine) {
@@ -189,15 +263,39 @@ async function runGate(ctx, engine, agent, config, state, reason, record) {
189
263
  history,
190
264
  ...(review.instructions ? { instructions: review.instructions } : {}),
191
265
  global: false,
266
+ // Read the skill-creator template facts (fallback: builtin distilled
267
+ // guide) so skill proposals follow the standard.
268
+ skillsRoot: join(engine.baseDir, "skills"),
192
269
  });
270
+ // Skills are governed resources: an auto-created skill is OFFERED to the
271
+ // user for a decision (固化/不固化) before it lands — the gate never
272
+ // writes a skill silently. Without consent the skill edits are withheld
273
+ // and the rest of the proposal proceeds as usual.
274
+ const { skillEdits, otherEdits } = splitSkillEdits(proposal);
275
+ const skillConsented = await consultSkillEdits(ctx, agent, skillEdits, state);
276
+ const finalProposal = skillConsented
277
+ ? proposal
278
+ : {
279
+ ...proposal,
280
+ edits: otherEdits,
281
+ summary: skillEdits.length > 0 ? `${proposal.summary} (skill edits withheld — pending user decision)` : proposal.summary,
282
+ };
283
+ if (finalProposal.edits.length === 0) {
284
+ const withheld = skillEdits.length > 0 ? " (skill proposal withheld — user not consulted or declined)" : "";
285
+ logger.info(`auto-review declined (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns: no consented edits${withheld} — ${review.rationale}`);
286
+ record({ sessionId, reason, turnsSinceLastReview, outcome: "declined", rationale: `${review.rationale}${withheld}` });
287
+ return;
288
+ }
193
289
  const source = entrySourceOf(agent, sessionId);
194
- const result = engine.apply("local", sessionId, proposal, {
290
+ const result = engine.apply("local", sessionId, finalProposal, {
195
291
  scope: "local",
196
292
  baselineState: localState,
197
293
  ...(source ? { source } : {}),
198
294
  });
199
295
  logger.info(`auto-review approved (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns; auto-refine ${result.id}: ${result.appliedEdits.filter((e) => e.applied).length} applied, ${result.appliedEdits.filter((e) => !e.applied).length} failed — ${review.rationale}`);
200
296
  record({ sessionId, reason, turnsSinceLastReview, outcome: "approved", rationale: review.rationale, refinementId: result.id });
297
+ // Gap C4: emit structured evolve_complete event for third-party consumers.
298
+ emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId));
201
299
  // Visibility: tell the user what the gate just persisted. Only the
202
300
  // turn-interval path notifies — a compaction-triggered gate must not wake
203
301
  // the agent mid-compaction — and only when something was actually applied
@@ -214,4 +312,65 @@ async function readTrajectory(ctx, agent, maxChars) {
214
312
  const snapshot = await sessionQuery.readSurface(agent.id);
215
313
  return serializeSurface(snapshot.events, maxChars);
216
314
  }
315
+ /**
316
+ * Split a proposal into skill edits and everything else. Skill edits are the
317
+ * governed part: they need explicit user consent before the gate applies
318
+ * them, while the remaining edits flow through the normal auto path.
319
+ */
320
+ export function splitSkillEdits(proposal) {
321
+ return {
322
+ skillEdits: proposal.edits.filter((edit) => edit.kind === "skill"),
323
+ otherEdits: proposal.edits.filter((edit) => edit.kind !== "skill"),
324
+ };
325
+ }
326
+ /**
327
+ * Ask the user whether to solidify proposed skill edits (guidance or
328
+ * executable) into the harness. Returns true when every skill edit is
329
+ * consented. Never writes a skill silently:
330
+ * - no question service available → false (conservative);
331
+ * - the same candidate was rejected within the cooldown window → false
332
+ * without asking again (no nagging);
333
+ * - the user declines → false and the rejection is recorded for cooldown;
334
+ * - the question call fails/aborts → false (conservative).
335
+ */
336
+ export async function consultSkillEdits(ctx, agent, skillEdits, gate) {
337
+ if (skillEdits.length === 0)
338
+ return true;
339
+ const key = skillEdits.map((edit) => edit.id ?? slug(edit.title ?? edit.kind, edit.kind)).join("|");
340
+ const lastReject = gate.skillRejects.get(key);
341
+ if (lastReject !== undefined && gate.turns - lastReject < SKILL_CONSULT_COOLDOWN_TURNS) {
342
+ return false;
343
+ }
344
+ const userQuestions = questionServiceOf(ctx);
345
+ if (!userQuestions) {
346
+ return false;
347
+ }
348
+ const description = skillEdits
349
+ .map((edit) => {
350
+ const form = edit.skill_kind === "guidance" ? "guidance 技能(SKILL.md 文档)" : "可执行技能";
351
+ return `- ${edit.action}「${edit.title ?? edit.id}」(${form})`;
352
+ })
353
+ .join("\n");
354
+ try {
355
+ const answer = await userQuestions.ask({
356
+ questions: [
357
+ {
358
+ id: "evolve-skill-consult",
359
+ question: `自进化检测到反复出现的流程/技能候选,建议沉淀:\n\n${description}\n\n是否固化?`,
360
+ options: [{ label: "固化" }, { label: "不固化" }],
361
+ },
362
+ ],
363
+ agent,
364
+ });
365
+ const item = answer.answers?.find((entry) => entry.id === "evolve-skill-consult");
366
+ const consented = item?.selected?.includes("固化") ?? false;
367
+ if (!consented) {
368
+ gate.skillRejects.set(key, gate.turns);
369
+ }
370
+ return consented;
371
+ }
372
+ catch {
373
+ return false;
374
+ }
375
+ }
217
376
  //# sourceMappingURL=auto.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The `/evolve benchmark` subcommand handler. Extracted from command.ts (P2-2).
3
+ */
4
+ import type { Context } from "@deepseek-ai/cordis";
5
+ import type { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
6
+ import type { EvolutionEngine } from "./service.js";
7
+ import type { CommandRuntimeOptions } from "./command.js";
8
+ export declare function executeBenchmarkCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation, rest: string[], runtime: CommandRuntimeOptions): Promise<CommandResult>;
9
+ //# sourceMappingURL=benchmark-command.d.ts.map