dsh-continual-evolve 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -371
- package/README.zh.md +84 -235
- package/lib/apply.js +8 -2
- package/lib/approval.d.ts +6 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +55 -4
- package/lib/auto.js +61 -5
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +333 -0
- package/lib/benchmark.d.ts +70 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.d.ts +3 -0
- package/lib/command.js +62 -441
- package/lib/evaluate.d.ts +7 -0
- package/lib/evaluate.js +22 -7
- package/lib/evolve-event.d.ts +38 -0
- package/lib/evolve-event.js +49 -0
- package/lib/failures.d.ts +39 -0
- package/lib/failures.js +170 -0
- package/lib/fate.d.ts +5 -2
- package/lib/fate.js +13 -8
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +51 -25
- package/lib/index.js +33 -1
- package/lib/inject.d.ts +24 -1
- package/lib/inject.js +93 -5
- package/lib/llm-text.d.ts +30 -0
- package/lib/llm-text.js +49 -0
- package/lib/mount-command.d.ts +10 -0
- package/lib/mount-command.js +48 -0
- package/lib/plan.js +5 -0
- package/lib/planner.d.ts +1 -1
- package/lib/planner.js +13 -39
- package/lib/promotion.d.ts +62 -0
- package/lib/promotion.js +102 -0
- package/lib/render.d.ts +1 -3
- package/lib/render.js +0 -4
- package/lib/review.d.ts +4 -1
- package/lib/review.js +10 -38
- package/lib/rollback.d.ts +1 -3
- package/lib/rollback.js +0 -8
- package/lib/score.d.ts +15 -0
- package/lib/score.js +74 -5
- package/lib/service.d.ts +2 -2
- package/lib/service.js +7 -3
- package/lib/skill-render.d.ts +23 -0
- package/lib/skill-render.js +68 -0
- package/lib/skill.d.ts +2 -5
- package/lib/skill.js +2 -29
- package/lib/skillquality.d.ts +1 -2
- package/lib/skillquality.js +2 -2
- package/lib/state.js +6 -1
- package/lib/store.d.ts +1 -3
- package/lib/store.js +0 -7
- package/lib/tool.js +22 -1
- package/lib/types.d.ts +8 -0
- package/lib/usage.d.ts +45 -0
- package/lib/usage.js +115 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +26 -1
- package/lib/wrapup-command.d.ts +9 -0
- package/lib/wrapup-command.js +212 -0
- package/lib/wrapup.d.ts +29 -15
- package/lib/wrapup.js +69 -42
- package/package.json +10 -8
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { RefinementResult } from "./types.js";
|
|
2
|
+
/** The structured event payload emitted after every successful refinement. */
|
|
3
|
+
export interface EvolveCompleteEvent {
|
|
4
|
+
/** Event discriminator for consumers. */
|
|
5
|
+
type: "evolve_complete";
|
|
6
|
+
/** The refinement id (same as the result). */
|
|
7
|
+
refinementId: string;
|
|
8
|
+
/** One-line summary of the refinement. */
|
|
9
|
+
summary: string;
|
|
10
|
+
/** Number of edits that were actually applied. */
|
|
11
|
+
appliedEdits: number;
|
|
12
|
+
/** Number of edits that failed to apply. */
|
|
13
|
+
failedEdits: number;
|
|
14
|
+
/** Scope of the refinement ("local" or "global"). */
|
|
15
|
+
scope: string;
|
|
16
|
+
/** What triggered this refinement (e.g. "auto_review", "manual_plan", "manual_tool"). */
|
|
17
|
+
trigger: string;
|
|
18
|
+
/** Session id that owns the refinement (auto or manual). */
|
|
19
|
+
sessionId: string;
|
|
20
|
+
/** ISO timestamp. */
|
|
21
|
+
timestamp: string;
|
|
22
|
+
/** Per-edit summaries (kind + id + action) for consumers that want detail. */
|
|
23
|
+
edits: {
|
|
24
|
+
action: string;
|
|
25
|
+
kind: string;
|
|
26
|
+
id: string;
|
|
27
|
+
applied: boolean;
|
|
28
|
+
}[];
|
|
29
|
+
}
|
|
30
|
+
/** Build a structured evolve-complete event from a refinement result. */
|
|
31
|
+
export declare function buildEvolveCompleteEvent(result: RefinementResult, trigger: string, sessionId: string): EvolveCompleteEvent;
|
|
32
|
+
/**
|
|
33
|
+
* Emit an evolve_complete event to the reviews.jsonl audit trail. The event
|
|
34
|
+
* is JSONL-formatted (one line) so consumers can tail and parse it. This is
|
|
35
|
+
* a best-effort write — failure never blocks the refinement path.
|
|
36
|
+
*/
|
|
37
|
+
export declare function emitEvolveComplete(baseDir: string, event: EvolveCompleteEvent): void;
|
|
38
|
+
//# sourceMappingURL=evolve-event.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured "evolve complete" events (gap C4): a durable, machine-readable
|
|
3
|
+
* record emitted after every successful refinement application — whether
|
|
4
|
+
* auto-gated or manual. Third-party consumers can observe these events via
|
|
5
|
+
* the plugin log (JSONL) and the reviews.jsonl audit trail.
|
|
6
|
+
*
|
|
7
|
+
* Design: prime-agent `/refine` emits `refine_complete{id,summary,
|
|
8
|
+
* appliedEdits,scope}` extension events. We follow the same pattern with
|
|
9
|
+
* added provenance (trigger, source) so consumers know WHY the refinement
|
|
10
|
+
* happened and WHO initiated it.
|
|
11
|
+
*/
|
|
12
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
/** Build a structured evolve-complete event from a refinement result. */
|
|
15
|
+
export function buildEvolveCompleteEvent(result, trigger, sessionId) {
|
|
16
|
+
return {
|
|
17
|
+
type: "evolve_complete",
|
|
18
|
+
refinementId: result.id,
|
|
19
|
+
summary: result.summary,
|
|
20
|
+
appliedEdits: result.appliedEdits.filter((e) => e.applied).length,
|
|
21
|
+
failedEdits: result.appliedEdits.filter((e) => !e.applied).length,
|
|
22
|
+
scope: result.scope ?? "local",
|
|
23
|
+
trigger,
|
|
24
|
+
sessionId,
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
edits: result.appliedEdits.map((e) => ({
|
|
27
|
+
action: e.action,
|
|
28
|
+
kind: e.kind,
|
|
29
|
+
id: e.id,
|
|
30
|
+
applied: e.applied,
|
|
31
|
+
})),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Emit an evolve_complete event to the reviews.jsonl audit trail. The event
|
|
36
|
+
* is JSONL-formatted (one line) so consumers can tail and parse it. This is
|
|
37
|
+
* a best-effort write — failure never blocks the refinement path.
|
|
38
|
+
*/
|
|
39
|
+
export function emitEvolveComplete(baseDir, event) {
|
|
40
|
+
try {
|
|
41
|
+
const dir = join(baseDir, "evolve");
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
appendFileSync(join(dir, "reviews.jsonl"), `${JSON.stringify(event)}\n`, "utf8");
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Event emission is diagnostic; never interrupt the refinement path.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=evolve-event.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface FailureRecord {
|
|
2
|
+
/** When the failure was recorded (ISO). Unknown for aggregated benchmark cells without timestamps. */
|
|
3
|
+
timestamp?: string;
|
|
4
|
+
/** Where the failure came from: "review-gate" | "benchmark:<bid>:<caseId>". */
|
|
5
|
+
source: string;
|
|
6
|
+
/** Failure class (see classifyFailure). */
|
|
7
|
+
kind: string;
|
|
8
|
+
/** The original failure text (notes or rationale). */
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
export interface FailureSummary {
|
|
12
|
+
total: number;
|
|
13
|
+
/** Count per failure class, sorted descending (most frequent first). */
|
|
14
|
+
byKind: Record<string, number>;
|
|
15
|
+
/** Count per source (gate vs benchmark:bid). */
|
|
16
|
+
bySource: Record<string, number>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Classify a failure message by prefix rules. Deterministic and additive:
|
|
20
|
+
* unknown text falls into "other" so the summary never drops a failure.
|
|
21
|
+
*/
|
|
22
|
+
export declare function classifyFailure(message: string): string;
|
|
23
|
+
/** Aggregate a record list into counts. Empty input yields an all-zero summary. */
|
|
24
|
+
export declare function summarizeFailures(records: readonly FailureRecord[]): FailureSummary;
|
|
25
|
+
/**
|
|
26
|
+
* Read failed review-gate records from `<baseDir>/evolve/reviews.jsonl`
|
|
27
|
+
* (outcome === "failed"). Tolerant of a missing/corrupt file.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readReviewFailures(baseDir: string): FailureRecord[];
|
|
30
|
+
/**
|
|
31
|
+
* Read failed cells from every benchmark scoreboard under
|
|
32
|
+
* `<baseDir>/evolve/benchmarks/<bid>/`. Tolerant of missing/corrupt data.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readBenchmarkFailures(baseDir: string): FailureRecord[];
|
|
35
|
+
/** Combine both sources into one summary. */
|
|
36
|
+
export declare function collectFailureSummary(baseDir: string): FailureSummary;
|
|
37
|
+
/** Human-readable report for the command line. */
|
|
38
|
+
export declare function formatFailureSummary(summary: FailureSummary): string;
|
|
39
|
+
//# sourceMappingURL=failures.d.ts.map
|
package/lib/failures.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Failure-signature aggregation (gap R/D1 — observation stage): turn the
|
|
3
|
+
* free-text failure records the system already produces (review-gate failures
|
|
4
|
+
* in reviews.jsonl, failed cells in benchmark scoreboards) into a structured
|
|
5
|
+
* count by failure CLASS. This is deliberately NOT the full failure-signature
|
|
6
|
+
* Refiner from gap D1 — no routing, no policy — it is the data layer that
|
|
7
|
+
* lets a later patch decide whether a given failure class recurs often enough
|
|
8
|
+
* to deserve one.
|
|
9
|
+
*
|
|
10
|
+
* Classes are extracted with pure prefix rules (see classifyFailure), so the
|
|
11
|
+
* aggregation is deterministic and unit-testable: the same failure text
|
|
12
|
+
* always lands in the same class.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
/**
|
|
17
|
+
* Classify a failure message by prefix rules. Deterministic and additive:
|
|
18
|
+
* unknown text falls into "other" so the summary never drops a failure.
|
|
19
|
+
*/
|
|
20
|
+
export function classifyFailure(message) {
|
|
21
|
+
const text = (message ?? "").trim();
|
|
22
|
+
const lower = text.toLowerCase();
|
|
23
|
+
if (lower.includes("rubric decrypt failed"))
|
|
24
|
+
return "rubric-decrypt";
|
|
25
|
+
if (lower.includes("materials changed"))
|
|
26
|
+
return "material-drift";
|
|
27
|
+
if (lower.includes("executor failed") || lower.includes("executor stopped"))
|
|
28
|
+
return "executor";
|
|
29
|
+
if (lower.includes("reviewer failed") || lower.includes("reviewer stopped"))
|
|
30
|
+
return "reviewer";
|
|
31
|
+
if (lower.includes("fate assessment error"))
|
|
32
|
+
return "fate-assessor";
|
|
33
|
+
if (lower.includes("trajectory unavailable"))
|
|
34
|
+
return "trajectory";
|
|
35
|
+
if (lower.includes("output budget exhausted") || lower.includes("max-tokens"))
|
|
36
|
+
return "max-tokens";
|
|
37
|
+
if (lower.includes("llm call aborted") || lower.includes("aborted"))
|
|
38
|
+
return "aborted";
|
|
39
|
+
if (lower.includes("llm call failed"))
|
|
40
|
+
return "llm";
|
|
41
|
+
if (lower.includes("gate error"))
|
|
42
|
+
return "gate";
|
|
43
|
+
if (lower.includes("casecheck") || lower.includes("case check"))
|
|
44
|
+
return "casecheck";
|
|
45
|
+
return "other";
|
|
46
|
+
}
|
|
47
|
+
/** Aggregate a record list into counts. Empty input yields an all-zero summary. */
|
|
48
|
+
export function summarizeFailures(records) {
|
|
49
|
+
const byKind = {};
|
|
50
|
+
const bySource = {};
|
|
51
|
+
for (const record of records) {
|
|
52
|
+
byKind[record.kind] = (byKind[record.kind] ?? 0) + 1;
|
|
53
|
+
bySource[record.source] = (bySource[record.source] ?? 0) + 1;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
total: records.length,
|
|
57
|
+
byKind: Object.fromEntries(Object.entries(byKind).sort((a, b) => b[1] - a[1])),
|
|
58
|
+
bySource: Object.fromEntries(Object.entries(bySource).sort((a, b) => b[1] - a[1])),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read failed review-gate records from `<baseDir>/evolve/reviews.jsonl`
|
|
63
|
+
* (outcome === "failed"). Tolerant of a missing/corrupt file.
|
|
64
|
+
*/
|
|
65
|
+
export function readReviewFailures(baseDir) {
|
|
66
|
+
const path = join(baseDir, "evolve", "reviews.jsonl");
|
|
67
|
+
if (!existsSync(path))
|
|
68
|
+
return [];
|
|
69
|
+
const records = [];
|
|
70
|
+
try {
|
|
71
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
72
|
+
if (line.trim().length === 0)
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
const raw = JSON.parse(line);
|
|
76
|
+
if (raw.outcome !== "failed" || !raw.rationale)
|
|
77
|
+
continue;
|
|
78
|
+
records.push({
|
|
79
|
+
...(raw.timestamp ? { timestamp: raw.timestamp } : {}),
|
|
80
|
+
source: `review-gate${raw.reason ? `:${raw.reason}` : ""}`,
|
|
81
|
+
kind: classifyFailure(raw.rationale),
|
|
82
|
+
message: raw.rationale,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// skip malformed lines — the audit file must never break reporting
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
return records;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Read failed cells from every benchmark scoreboard under
|
|
97
|
+
* `<baseDir>/evolve/benchmarks/<bid>/`. Tolerant of missing/corrupt data.
|
|
98
|
+
*/
|
|
99
|
+
export function readBenchmarkFailures(baseDir) {
|
|
100
|
+
const root = join(baseDir, "evolve", "benchmarks");
|
|
101
|
+
if (!existsSync(root))
|
|
102
|
+
return [];
|
|
103
|
+
const records = [];
|
|
104
|
+
let bids;
|
|
105
|
+
try {
|
|
106
|
+
bids = readdirSync(root);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
for (const bid of bids) {
|
|
112
|
+
const boardPath = join(root, bid, "scoreboard.json");
|
|
113
|
+
if (!existsSync(boardPath))
|
|
114
|
+
continue;
|
|
115
|
+
let board;
|
|
116
|
+
try {
|
|
117
|
+
board = JSON.parse(readFileSync(boardPath, "utf8"));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const cellLists = [];
|
|
123
|
+
if (board.reference?.cells)
|
|
124
|
+
cellLists.push({ label: "reference", cells: board.reference.cells });
|
|
125
|
+
for (const entry of board.candidates ?? []) {
|
|
126
|
+
if (entry.cells)
|
|
127
|
+
cellLists.push({ label: entry.label ?? "candidate", cells: entry.cells });
|
|
128
|
+
}
|
|
129
|
+
for (const list of cellLists) {
|
|
130
|
+
for (const cell of list.cells) {
|
|
131
|
+
const c = cell;
|
|
132
|
+
if (c.status !== "failed" || !c.notes)
|
|
133
|
+
continue;
|
|
134
|
+
records.push({
|
|
135
|
+
source: `benchmark:${bid}:${c.caseId ?? "?"}`,
|
|
136
|
+
kind: classifyFailure(c.notes),
|
|
137
|
+
message: c.notes,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return records;
|
|
143
|
+
}
|
|
144
|
+
/** Combine both sources into one summary. */
|
|
145
|
+
export function collectFailureSummary(baseDir) {
|
|
146
|
+
return summarizeFailures([...readReviewFailures(baseDir), ...readBenchmarkFailures(baseDir)]);
|
|
147
|
+
}
|
|
148
|
+
/** Human-readable report for the command line. */
|
|
149
|
+
export function formatFailureSummary(summary) {
|
|
150
|
+
const lines = [`failure summary: ${summary.total} total`];
|
|
151
|
+
const kinds = Object.entries(summary.byKind);
|
|
152
|
+
if (kinds.length > 0) {
|
|
153
|
+
lines.push("by class:");
|
|
154
|
+
for (const [kind, count] of kinds) {
|
|
155
|
+
lines.push(` ${kind}: ${count}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
lines.push("by class: (none)");
|
|
160
|
+
}
|
|
161
|
+
const sources = Object.entries(summary.bySource);
|
|
162
|
+
if (sources.length > 0) {
|
|
163
|
+
lines.push("by source:");
|
|
164
|
+
for (const [source, count] of sources) {
|
|
165
|
+
lines.push(` ${source}: ${count}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=failures.js.map
|
package/lib/fate.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { HarnessState, RefinementResult } from "./types.js";
|
|
|
34
34
|
import type { EvolutionEngine } from "./service.js";
|
|
35
35
|
import type { AutoRefineReason } from "./review.js";
|
|
36
36
|
import type { AutoReviewConfig, GateState, ReviewRecord } from "./auto.js";
|
|
37
|
+
import { type PromotionPolicy } from "./promotion.js";
|
|
37
38
|
import { type WrapupCandidate, type WrapupItem } from "./wrapup.js";
|
|
38
39
|
/** Turns a declined local-fate proposal stays silent before being offered again. */
|
|
39
40
|
export declare const FATE_CONSULT_COOLDOWN_TURNS = 10;
|
|
@@ -68,7 +69,7 @@ export interface FatePlan {
|
|
|
68
69
|
* may have changed while the LLM call was in flight). Pure and unit-tested;
|
|
69
70
|
* mirrors the partition step of the wrap-up command.
|
|
70
71
|
*/
|
|
71
|
-
export declare function planLocalFates(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[], globalState: HarnessState): FatePlan;
|
|
72
|
+
export declare function planLocalFates(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[], globalState: HarnessState, policy?: PromotionPolicy): FatePlan;
|
|
72
73
|
/**
|
|
73
74
|
* The cooldown key of a candidate set: the sorted `kind:id` list. The set is
|
|
74
75
|
* the unit of consultation — a declined proposal is not offered again within
|
|
@@ -81,7 +82,9 @@ export declare function fateSetKey(candidates: readonly WrapupCandidate[]): stri
|
|
|
81
82
|
* gates respect the fate cadence (an independent counter — goal-driven
|
|
82
83
|
* sessions run the review EVERY round, the fate assessment must not);
|
|
83
84
|
* compaction is unconditional: experiences about to be summarized away get
|
|
84
|
-
* their fate check regardless.
|
|
85
|
+
* their fate check regardless. Goal-blocked assessments are unconditional
|
|
86
|
+
* here too — the gate's own streak counter (auto.ts runGoalBlockedFate)
|
|
87
|
+
* already gates their frequency, so the cadence must not re-block them.
|
|
85
88
|
*/
|
|
86
89
|
export declare function fateCadenceDue(state: GateState, reason: AutoRefineReason, intervalTurns: number): boolean;
|
|
87
90
|
export interface FateConsultResult {
|
package/lib/fate.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
|
|
3
|
+
import { questionServiceOf } from "./approval.js";
|
|
2
4
|
import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals, } from "./wrapup.js";
|
|
3
5
|
/** Turns a declined local-fate proposal stays silent before being offered again. */
|
|
4
6
|
export const FATE_CONSULT_COOLDOWN_TURNS = 10;
|
|
@@ -8,9 +10,9 @@ export const FATE_CONSULT_COOLDOWN_TURNS = 10;
|
|
|
8
10
|
* may have changed while the LLM call was in flight). Pure and unit-tested;
|
|
9
11
|
* mirrors the partition step of the wrap-up command.
|
|
10
12
|
*/
|
|
11
|
-
export function planLocalFates(items, candidates, globalState) {
|
|
13
|
+
export function planLocalFates(items, candidates, globalState, policy = DEFAULT_PROMOTION_POLICY) {
|
|
12
14
|
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
13
|
-
const { promotable, skipped } = filterPromotable(items, globalState, candidates);
|
|
15
|
+
const { promotable, skipped } = filterPromotable(items, globalState, candidates, policy);
|
|
14
16
|
const promoteItems = promotable.filter((item) => item.verdict === "promote");
|
|
15
17
|
const archiveItems = items.filter((item) => item.verdict === "archive");
|
|
16
18
|
const splits = [];
|
|
@@ -23,7 +25,7 @@ export function planLocalFates(items, candidates, globalState) {
|
|
|
23
25
|
splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
|
|
24
26
|
continue;
|
|
25
27
|
}
|
|
26
|
-
const blocked = splitPromoteBlocked(item, globalState, candidate.kind);
|
|
28
|
+
const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy);
|
|
27
29
|
if (blocked) {
|
|
28
30
|
splitSkipped.push({ key: item.key, reason: blocked });
|
|
29
31
|
continue;
|
|
@@ -51,10 +53,13 @@ export function fateSetKey(candidates) {
|
|
|
51
53
|
* gates respect the fate cadence (an independent counter — goal-driven
|
|
52
54
|
* sessions run the review EVERY round, the fate assessment must not);
|
|
53
55
|
* compaction is unconditional: experiences about to be summarized away get
|
|
54
|
-
* their fate check regardless.
|
|
56
|
+
* their fate check regardless. Goal-blocked assessments are unconditional
|
|
57
|
+
* here too — the gate's own streak counter (auto.ts runGoalBlockedFate)
|
|
58
|
+
* already gates their frequency, so the cadence must not re-block them.
|
|
55
59
|
*/
|
|
56
60
|
export function fateCadenceDue(state, reason, intervalTurns) {
|
|
57
|
-
|
|
61
|
+
// Compact and goal-blocked assessments bypass the cadence (see header).
|
|
62
|
+
if (reason === "compact" || reason === "goal_blocked")
|
|
58
63
|
return true;
|
|
59
64
|
return state.turns - state.lastFateAt >= intervalTurns;
|
|
60
65
|
}
|
|
@@ -76,7 +81,7 @@ export async function consultLocalFates(ctx, agent, plan, gate) {
|
|
|
76
81
|
if (lastReject !== undefined && gate.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
|
|
77
82
|
return { approved: false, asked: false, reason: "cooldown" };
|
|
78
83
|
}
|
|
79
|
-
const userQuestions = ctx
|
|
84
|
+
const userQuestions = questionServiceOf(ctx);
|
|
80
85
|
if (!userQuestions) {
|
|
81
86
|
return { approved: false, asked: false, reason: "unavailable" };
|
|
82
87
|
}
|
|
@@ -197,7 +202,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
197
202
|
const sessionId = agent.id;
|
|
198
203
|
const localState = engine.load("local", sessionId);
|
|
199
204
|
const globalState = engine.load("global", undefined);
|
|
200
|
-
const candidates = listLocalCandidates(localState, globalState);
|
|
205
|
+
const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
|
|
201
206
|
if (candidates.length === 0)
|
|
202
207
|
return;
|
|
203
208
|
if (!fateCadenceDue(state, reason, config.fateIntervalTurns))
|
|
@@ -226,7 +231,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
226
231
|
});
|
|
227
232
|
return;
|
|
228
233
|
}
|
|
229
|
-
const plan = planLocalFates(assessment.items, candidates, globalState);
|
|
234
|
+
const plan = planLocalFates(assessment.items, candidates, globalState, config.promotionPolicy);
|
|
230
235
|
const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
|
|
231
236
|
let consent = { approved: false, asked: false, reason: "nothing-to-ask" };
|
|
232
237
|
if (reason !== "compact" && needsDialog) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/evolve goal` 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
|
+
export declare function executeGoalCommand(ctx: Context, invocation: CommandInvocation, rest: string[]): CommandResult;
|
|
7
|
+
//# sourceMappingURL=goal-command.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { blockEvolutionGoal, completeEvolutionGoal, goalServiceOf, goalStatusText, upsertEvolutionGoal } from "./goal.js";
|
|
2
|
+
function success(text) {
|
|
3
|
+
return { kind: "success", text };
|
|
4
|
+
}
|
|
5
|
+
function error(text) {
|
|
6
|
+
return { kind: "error", text };
|
|
7
|
+
}
|
|
8
|
+
export function executeGoalCommand(ctx, invocation, rest) {
|
|
9
|
+
const agent = invocation.agent;
|
|
10
|
+
const goals = goalServiceOf(ctx);
|
|
11
|
+
if (!goals) {
|
|
12
|
+
return error(`/evolve goal requires the goals service (load @deepseek-ai/dsh-goal)`);
|
|
13
|
+
}
|
|
14
|
+
const sub = rest[0] ?? "";
|
|
15
|
+
try {
|
|
16
|
+
if (sub === "done") {
|
|
17
|
+
const view = completeEvolutionGoal(ctx, agent);
|
|
18
|
+
return view ? success(`evolution goal completed: ${goalStatusText(view)}`) : success("(no goal to complete)");
|
|
19
|
+
}
|
|
20
|
+
if (sub === "block") {
|
|
21
|
+
const reason = rest.slice(1).join(" ") || "user requested block";
|
|
22
|
+
const view = blockEvolutionGoal(ctx, agent, reason);
|
|
23
|
+
return view ? success(`evolution goal blocked: ${goalStatusText(view)}`) : success("(no active goal to block)");
|
|
24
|
+
}
|
|
25
|
+
if (sub.length === 0) {
|
|
26
|
+
const current = goals.get(agent);
|
|
27
|
+
return current ? success(goalStatusText(current)) : success("(no evolution goal — /evolve goal <objective> to create one)");
|
|
28
|
+
}
|
|
29
|
+
const objective = rest.join(" ");
|
|
30
|
+
const view = upsertEvolutionGoal(ctx, agent, objective);
|
|
31
|
+
return success(`evolution goal ready: ${goalStatusText(view)}\n(active goal drives the review gate every round)`);
|
|
32
|
+
}
|
|
33
|
+
catch (cause) {
|
|
34
|
+
return error(cause instanceof Error ? cause.message : String(cause));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=goal-command.js.map
|
package/lib/index.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
34
34
|
logMaxBytes: z<number, number>;
|
|
35
35
|
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
36
36
|
autoRollbackOnReject: z<boolean, boolean>;
|
|
37
|
+
/**
|
|
38
|
+
* Gap C1: optional model override for the review gate (cheaper model).
|
|
39
|
+
* Format: "provider/model" or just "model" (same provider as the agent).
|
|
40
|
+
* When absent, the review gate uses the agent's own provider/model.
|
|
41
|
+
*/
|
|
42
|
+
reviewModel: z<string, string>;
|
|
37
43
|
/**
|
|
38
44
|
* Gate local-fate dimension (#11 P2): the gate audits the session's local
|
|
39
45
|
* entries on its own cadence and proposes promote/archive — consulted
|
|
@@ -45,6 +51,23 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
45
51
|
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
46
52
|
*/
|
|
47
53
|
fateIntervalTurns: z<number, number>;
|
|
54
|
+
/**
|
|
55
|
+
* Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
|
|
56
|
+
* observe the session goal in phase "blocked", run one local-fate
|
|
57
|
+
* assessment so the encounter is distilled. 0 disables.
|
|
58
|
+
*/
|
|
59
|
+
goalBlockedWrapupTurns: z<number, number>;
|
|
60
|
+
/**
|
|
61
|
+
* Promotion policy (2026-08-22): regex sources whose match in a
|
|
62
|
+
* candidate's title/content marks it project-scoped — such entries are
|
|
63
|
+
* never promoted to the cross-session global store. Replaces the built-in
|
|
64
|
+
* defaults when set.
|
|
65
|
+
*/
|
|
66
|
+
promotionBlockPatterns: z<string[], string[]>;
|
|
67
|
+
/** Whole promotions below this content length stay local (chars). */
|
|
68
|
+
promotionMinChars: z<number, number>;
|
|
69
|
+
/** Entry-directory lines injected per build before folding into a counter. */
|
|
70
|
+
injectionDirectoryLines: z<number, number>;
|
|
48
71
|
}>, Schemastery.ObjectT<{
|
|
49
72
|
/** Root for evolution stores; defaults to the resolved DSH home. */
|
|
50
73
|
baseDir: z<string, string>;
|
|
@@ -74,6 +97,12 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
74
97
|
logMaxBytes: z<number, number>;
|
|
75
98
|
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
76
99
|
autoRollbackOnReject: z<boolean, boolean>;
|
|
100
|
+
/**
|
|
101
|
+
* Gap C1: optional model override for the review gate (cheaper model).
|
|
102
|
+
* Format: "provider/model" or just "model" (same provider as the agent).
|
|
103
|
+
* When absent, the review gate uses the agent's own provider/model.
|
|
104
|
+
*/
|
|
105
|
+
reviewModel: z<string, string>;
|
|
77
106
|
/**
|
|
78
107
|
* Gate local-fate dimension (#11 P2): the gate audits the session's local
|
|
79
108
|
* entries on its own cadence and proposes promote/archive — consulted
|
|
@@ -85,32 +114,29 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
85
114
|
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
86
115
|
*/
|
|
87
116
|
fateIntervalTurns: z<number, number>;
|
|
117
|
+
/**
|
|
118
|
+
* Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
|
|
119
|
+
* observe the session goal in phase "blocked", run one local-fate
|
|
120
|
+
* assessment so the encounter is distilled. 0 disables.
|
|
121
|
+
*/
|
|
122
|
+
goalBlockedWrapupTurns: z<number, number>;
|
|
123
|
+
/**
|
|
124
|
+
* Promotion policy (2026-08-22): regex sources whose match in a
|
|
125
|
+
* candidate's title/content marks it project-scoped — such entries are
|
|
126
|
+
* never promoted to the cross-session global store. Replaces the built-in
|
|
127
|
+
* defaults when set.
|
|
128
|
+
*/
|
|
129
|
+
promotionBlockPatterns: z<string[], string[]>;
|
|
130
|
+
/** Whole promotions below this content length stay local (chars). */
|
|
131
|
+
promotionMinChars: z<number, number>;
|
|
132
|
+
/** Entry-directory lines injected per build before folding into a counter. */
|
|
133
|
+
injectionDirectoryLines: z<number, number>;
|
|
88
134
|
}>>;
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
reviewIntervalTurns?: number;
|
|
95
|
-
maxReviewInputChars?: number;
|
|
96
|
-
reviewBudgetTokens?: number;
|
|
97
|
-
notifyOnAutoReview?: boolean;
|
|
98
|
-
requireGlobalApproval?: boolean;
|
|
99
|
-
skillsDir?: string;
|
|
100
|
-
rubricKey?: string;
|
|
101
|
-
/** Write all cordis log messages to <baseDir>/evolve/plugin.log (JSONL). */
|
|
102
|
-
logToFile?: boolean;
|
|
103
|
-
/** File log level: 0=error, 1=info, 2=warn, 3=debug. */
|
|
104
|
-
logLevel?: number;
|
|
105
|
-
/** Rotate the file log when it exceeds this many bytes. */
|
|
106
|
-
logMaxBytes?: number;
|
|
107
|
-
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
108
|
-
autoRollbackOnReject?: boolean;
|
|
109
|
-
/** Gate local-fate dimension: propose promote/archive of local entries automatically. */
|
|
110
|
-
localFate?: boolean;
|
|
111
|
-
/** Minimum turns between local-fate assessments (compaction is unconditional). */
|
|
112
|
-
fateIntervalTurns?: number;
|
|
113
|
-
}
|
|
135
|
+
/**
|
|
136
|
+
* Structurally typed resolved config (loader passes the validated object).
|
|
137
|
+
* Derived from the schemastery schema — single source of truth, no manual sync.
|
|
138
|
+
*/
|
|
139
|
+
export type EvolveConfig = Partial<Schemastery.TypeT<typeof Config>>;
|
|
114
140
|
export interface EvolutionService {
|
|
115
141
|
readonly engine: EvolutionEngine;
|
|
116
142
|
readonly baseDir: string;
|
package/lib/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { entriesSectionText } from "./inject.js";
|
|
|
19
19
|
import { resolveRubricKey } from "./rubric.js";
|
|
20
20
|
import { restoreMounted } from "./mount.js";
|
|
21
21
|
import { registerFileLogger } from "./logfile.js";
|
|
22
|
+
import { resolvePromotionPolicy } from "./promotion.js";
|
|
22
23
|
export const name = "continual-evolve";
|
|
23
24
|
/** Service key under which the evolution engine is published. */
|
|
24
25
|
export const EVOLUTION_SERVICE = "evolution";
|
|
@@ -52,6 +53,12 @@ export const Config = z.object({
|
|
|
52
53
|
logMaxBytes: z.natural().default(5 * 1024 * 1024),
|
|
53
54
|
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
54
55
|
autoRollbackOnReject: z.boolean().default(true),
|
|
56
|
+
/**
|
|
57
|
+
* Gap C1: optional model override for the review gate (cheaper model).
|
|
58
|
+
* Format: "provider/model" or just "model" (same provider as the agent).
|
|
59
|
+
* When absent, the review gate uses the agent's own provider/model.
|
|
60
|
+
*/
|
|
61
|
+
reviewModel: z.string(),
|
|
55
62
|
/**
|
|
56
63
|
* Gate local-fate dimension (#11 P2): the gate audits the session's local
|
|
57
64
|
* entries on its own cadence and proposes promote/archive — consulted
|
|
@@ -63,6 +70,23 @@ export const Config = z.object({
|
|
|
63
70
|
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
64
71
|
*/
|
|
65
72
|
fateIntervalTurns: z.natural(),
|
|
73
|
+
/**
|
|
74
|
+
* Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
|
|
75
|
+
* observe the session goal in phase "blocked", run one local-fate
|
|
76
|
+
* assessment so the encounter is distilled. 0 disables.
|
|
77
|
+
*/
|
|
78
|
+
goalBlockedWrapupTurns: z.natural().min(0).default(3),
|
|
79
|
+
/**
|
|
80
|
+
* Promotion policy (2026-08-22): regex sources whose match in a
|
|
81
|
+
* candidate's title/content marks it project-scoped — such entries are
|
|
82
|
+
* never promoted to the cross-session global store. Replaces the built-in
|
|
83
|
+
* defaults when set.
|
|
84
|
+
*/
|
|
85
|
+
promotionBlockPatterns: z.array(z.string()),
|
|
86
|
+
/** Whole promotions below this content length stay local (chars). */
|
|
87
|
+
promotionMinChars: z.natural().default(100),
|
|
88
|
+
/** Entry-directory lines injected per build before folding into a counter. */
|
|
89
|
+
injectionDirectoryLines: z.natural().default(15),
|
|
66
90
|
});
|
|
67
91
|
export function apply(ctx, config) {
|
|
68
92
|
const baseDir = resolveDshHome(config.baseDir);
|
|
@@ -95,13 +119,18 @@ export function apply(ctx, config) {
|
|
|
95
119
|
ctx.systemPrompt.section({
|
|
96
120
|
name: "tool:continual-evolve:entries",
|
|
97
121
|
order: (config.sectionOrder ?? 118) + 1,
|
|
98
|
-
text: (context) => entriesSectionText(engine, context.agent),
|
|
122
|
+
text: (context) => entriesSectionText(engine, context.agent, undefined, { directoryLines: config.injectionDirectoryLines ?? 15 }),
|
|
99
123
|
});
|
|
100
124
|
const gate = { requireGlobalApproval: config.requireGlobalApproval ?? true };
|
|
125
|
+
const promotionPolicy = resolvePromotionPolicy({
|
|
126
|
+
blockPatterns: config.promotionBlockPatterns,
|
|
127
|
+
minPromoteChars: config.promotionMinChars,
|
|
128
|
+
});
|
|
101
129
|
registerEvolveTools(ctx, engine, gate);
|
|
102
130
|
registerEvolveCommand(ctx, engine, gate, {
|
|
103
131
|
rubricKey: resolveRubricKey(baseDir, config.rubricKey, process.env, (m) => ctx.logger("continual-evolve").warn(m)),
|
|
104
132
|
autoRollbackOnReject: config.autoRollbackOnReject ?? true,
|
|
133
|
+
promotionPolicy,
|
|
105
134
|
});
|
|
106
135
|
// Plugin-owned file logging: every cordis log message lands in
|
|
107
136
|
// <baseDir>/evolve/plugin.log regardless of how dsh web was launched —
|
|
@@ -124,6 +153,9 @@ export function apply(ctx, config) {
|
|
|
124
153
|
notifyOnAutoReview: config.notifyOnAutoReview ?? true,
|
|
125
154
|
localFate: config.localFate ?? true,
|
|
126
155
|
fateIntervalTurns: config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6,
|
|
156
|
+
goalBlockedWrapupTurns: config.goalBlockedWrapupTurns ?? 3,
|
|
157
|
+
promotionPolicy,
|
|
158
|
+
...(config.reviewModel ? { reviewModel: config.reviewModel } : {}),
|
|
127
159
|
});
|
|
128
160
|
ctx.logger("continual-evolve").info(`continual-evolve auto-review enabled (every ${config.reviewIntervalTurns ?? 6} turns; local-fate ${config.localFate ?? true ? "on" : "off"} every ${config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6} turns)`);
|
|
129
161
|
}
|
package/lib/inject.d.ts
CHANGED
|
@@ -103,6 +103,22 @@ export declare function hasAnyEntries(state: HarnessState): boolean;
|
|
|
103
103
|
export declare function formatPromptEntriesSection(entries: readonly HarnessEntry[], query?: string): string;
|
|
104
104
|
/** The reusable delegation-specs block (empty when there are no visible subagent entries). */
|
|
105
105
|
export declare function formatSubagentSpecsSection(entries: readonly HarnessEntry[], query?: string): string;
|
|
106
|
+
/**
|
|
107
|
+
* Gap B3: a lightweight directory of ALL non-archived entries across all
|
|
108
|
+
* kinds — one line per entry (`- [kind:id] title`), no content. This gives
|
|
109
|
+
* the model a zero-cost overview of what exists so it can ask for full text
|
|
110
|
+
* via `evolve_list` or `/evolve list`. The directory is appended after the
|
|
111
|
+
* curated top-N injection sections and adds minimal tokens.
|
|
112
|
+
*
|
|
113
|
+
* 2026-08-22 throttle: the directory is CAPPED at {@link DEFAULT_DIRECTORY_LINES}
|
|
114
|
+
* lines (oldest-sorted stable order) with the remainder folded into a single
|
|
115
|
+
* counter line — an uncapped directory across a polluted global store was
|
|
116
|
+
* measured at ~2K chars of every build in every project.
|
|
117
|
+
*/
|
|
118
|
+
export declare const DEFAULT_DIRECTORY_LINES = 15;
|
|
119
|
+
export declare function formatEntriesDirectory(...kindEntries: readonly HarnessEntry[][]): string;
|
|
120
|
+
/** {@link formatEntriesDirectory} with an explicit cap (configurable). */
|
|
121
|
+
export declare function formatEntriesDirectoryCapped(maxLines: number, ...kindEntries: readonly HarnessEntry[][]): string;
|
|
106
122
|
/**
|
|
107
123
|
* Walk the parent-session chain from `agent` upward and return the nearest
|
|
108
124
|
* session whose local store is non-empty, if any. Children inherit their
|
|
@@ -119,6 +135,13 @@ export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, ag
|
|
|
119
135
|
* (relevance first, then recency; see {@link rankEntries}). Returns "" when
|
|
120
136
|
* nothing is injectable — the prompt renderer then drops the section, so an
|
|
121
137
|
* empty store adds zero tokens to every assembly.
|
|
138
|
+
*
|
|
139
|
+
* `opts.directoryLines` caps the entry-directory index (2026-08-22 throttle).
|
|
140
|
+
* Usage recording covers ALL kinds — memories and skills appear as directory
|
|
141
|
+
* lines, prompts/subagents as content — and is deduped per session so the
|
|
142
|
+
* counts read "how many sessions saw this", not "how many prompt builds".
|
|
122
143
|
*/
|
|
123
|
-
export declare function entriesSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, query?: string
|
|
144
|
+
export declare function entriesSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, query?: string, opts?: {
|
|
145
|
+
directoryLines?: number;
|
|
146
|
+
}): string;
|
|
124
147
|
//# sourceMappingURL=inject.d.ts.map
|