dsh-continual-evolve 0.2.0 → 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.
- package/README.md +45 -7
- package/README.zh.md +44 -7
- package/lib/apply.js +1 -1
- package/lib/approval.d.ts +6 -0
- package/lib/approval.js +9 -1
- package/lib/auto.d.ts +38 -4
- package/lib/auto.js +58 -5
- package/lib/benchmark-command.d.ts +9 -0
- package/lib/benchmark-command.js +331 -0
- package/lib/benchmark.d.ts +70 -0
- package/lib/benchmark.js +107 -1
- package/lib/command.js +25 -442
- 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 +3 -1
- package/lib/fate.js +8 -4
- package/lib/goal-command.d.ts +7 -0
- package/lib/goal-command.js +37 -0
- package/lib/index.d.ts +29 -25
- package/lib/index.js +14 -0
- package/lib/inject.d.ts +8 -0
- package/lib/inject.js +51 -4
- 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/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 +5 -2
- package/lib/skill-render.d.ts +15 -0
- package/lib/skill-render.js +30 -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/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 +32 -0
- package/lib/usage.js +84 -0
- package/lib/validate.d.ts +12 -2
- package/lib/validate.js +26 -1
- package/lib/wrapup-command.d.ts +8 -0
- package/lib/wrapup-command.js +211 -0
- package/lib/wrapup.d.ts +14 -9
- package/lib/wrapup.js +24 -36
- package/package.json +8 -8
|
@@ -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
|
@@ -81,7 +81,9 @@ export declare function fateSetKey(candidates: readonly WrapupCandidate[]): stri
|
|
|
81
81
|
* gates respect the fate cadence (an independent counter — goal-driven
|
|
82
82
|
* sessions run the review EVERY round, the fate assessment must not);
|
|
83
83
|
* compaction is unconditional: experiences about to be summarized away get
|
|
84
|
-
* their fate check regardless.
|
|
84
|
+
* their fate check regardless. Goal-blocked assessments are unconditional
|
|
85
|
+
* here too — the gate's own streak counter (auto.ts runGoalBlockedFate)
|
|
86
|
+
* already gates their frequency, so the cadence must not re-block them.
|
|
85
87
|
*/
|
|
86
88
|
export declare function fateCadenceDue(state: GateState, reason: AutoRefineReason, intervalTurns: number): boolean;
|
|
87
89
|
export interface FateConsultResult {
|
package/lib/fate.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { questionServiceOf } from "./approval.js";
|
|
2
3
|
import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals, } from "./wrapup.js";
|
|
3
4
|
/** Turns a declined local-fate proposal stays silent before being offered again. */
|
|
4
5
|
export const FATE_CONSULT_COOLDOWN_TURNS = 10;
|
|
@@ -51,10 +52,13 @@ export function fateSetKey(candidates) {
|
|
|
51
52
|
* gates respect the fate cadence (an independent counter — goal-driven
|
|
52
53
|
* sessions run the review EVERY round, the fate assessment must not);
|
|
53
54
|
* compaction is unconditional: experiences about to be summarized away get
|
|
54
|
-
* their fate check regardless.
|
|
55
|
+
* their fate check regardless. Goal-blocked assessments are unconditional
|
|
56
|
+
* here too — the gate's own streak counter (auto.ts runGoalBlockedFate)
|
|
57
|
+
* already gates their frequency, so the cadence must not re-block them.
|
|
55
58
|
*/
|
|
56
59
|
export function fateCadenceDue(state, reason, intervalTurns) {
|
|
57
|
-
|
|
60
|
+
// Compact and goal-blocked assessments bypass the cadence (see header).
|
|
61
|
+
if (reason === "compact" || reason === "goal_blocked")
|
|
58
62
|
return true;
|
|
59
63
|
return state.turns - state.lastFateAt >= intervalTurns;
|
|
60
64
|
}
|
|
@@ -76,7 +80,7 @@ export async function consultLocalFates(ctx, agent, plan, gate) {
|
|
|
76
80
|
if (lastReject !== undefined && gate.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
|
|
77
81
|
return { approved: false, asked: false, reason: "cooldown" };
|
|
78
82
|
}
|
|
79
|
-
const userQuestions = ctx
|
|
83
|
+
const userQuestions = questionServiceOf(ctx);
|
|
80
84
|
if (!userQuestions) {
|
|
81
85
|
return { approved: false, asked: false, reason: "unavailable" };
|
|
82
86
|
}
|
|
@@ -197,7 +201,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
197
201
|
const sessionId = agent.id;
|
|
198
202
|
const localState = engine.load("local", sessionId);
|
|
199
203
|
const globalState = engine.load("global", undefined);
|
|
200
|
-
const candidates = listLocalCandidates(localState, globalState);
|
|
204
|
+
const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
|
|
201
205
|
if (candidates.length === 0)
|
|
202
206
|
return;
|
|
203
207
|
if (!fateCadenceDue(state, reason, config.fateIntervalTurns))
|
|
@@ -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,12 @@ 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>;
|
|
48
60
|
}>, Schemastery.ObjectT<{
|
|
49
61
|
/** Root for evolution stores; defaults to the resolved DSH home. */
|
|
50
62
|
baseDir: z<string, string>;
|
|
@@ -74,6 +86,12 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
74
86
|
logMaxBytes: z<number, number>;
|
|
75
87
|
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
76
88
|
autoRollbackOnReject: z<boolean, boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* Gap C1: optional model override for the review gate (cheaper model).
|
|
91
|
+
* Format: "provider/model" or just "model" (same provider as the agent).
|
|
92
|
+
* When absent, the review gate uses the agent's own provider/model.
|
|
93
|
+
*/
|
|
94
|
+
reviewModel: z<string, string>;
|
|
77
95
|
/**
|
|
78
96
|
* Gate local-fate dimension (#11 P2): the gate audits the session's local
|
|
79
97
|
* entries on its own cadence and proposes promote/archive — consulted
|
|
@@ -85,32 +103,18 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
85
103
|
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
86
104
|
*/
|
|
87
105
|
fateIntervalTurns: z<number, number>;
|
|
106
|
+
/**
|
|
107
|
+
* Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
|
|
108
|
+
* observe the session goal in phase "blocked", run one local-fate
|
|
109
|
+
* assessment so the encounter is distilled. 0 disables.
|
|
110
|
+
*/
|
|
111
|
+
goalBlockedWrapupTurns: z<number, number>;
|
|
88
112
|
}>>;
|
|
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
|
-
}
|
|
113
|
+
/**
|
|
114
|
+
* Structurally typed resolved config (loader passes the validated object).
|
|
115
|
+
* Derived from the schemastery schema — single source of truth, no manual sync.
|
|
116
|
+
*/
|
|
117
|
+
export type EvolveConfig = Partial<Schemastery.TypeT<typeof Config>>;
|
|
114
118
|
export interface EvolutionService {
|
|
115
119
|
readonly engine: EvolutionEngine;
|
|
116
120
|
readonly baseDir: string;
|
package/lib/index.js
CHANGED
|
@@ -52,6 +52,12 @@ export const Config = z.object({
|
|
|
52
52
|
logMaxBytes: z.natural().default(5 * 1024 * 1024),
|
|
53
53
|
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
54
54
|
autoRollbackOnReject: z.boolean().default(true),
|
|
55
|
+
/**
|
|
56
|
+
* Gap C1: optional model override for the review gate (cheaper model).
|
|
57
|
+
* Format: "provider/model" or just "model" (same provider as the agent).
|
|
58
|
+
* When absent, the review gate uses the agent's own provider/model.
|
|
59
|
+
*/
|
|
60
|
+
reviewModel: z.string(),
|
|
55
61
|
/**
|
|
56
62
|
* Gate local-fate dimension (#11 P2): the gate audits the session's local
|
|
57
63
|
* entries on its own cadence and proposes promote/archive — consulted
|
|
@@ -63,6 +69,12 @@ export const Config = z.object({
|
|
|
63
69
|
* (compaction is unconditional). Absent → follows reviewIntervalTurns.
|
|
64
70
|
*/
|
|
65
71
|
fateIntervalTurns: z.natural(),
|
|
72
|
+
/**
|
|
73
|
+
* Goal-blocked trigger (D3): after this many CONSECUTIVE gate runs that
|
|
74
|
+
* observe the session goal in phase "blocked", run one local-fate
|
|
75
|
+
* assessment so the encounter is distilled. 0 disables.
|
|
76
|
+
*/
|
|
77
|
+
goalBlockedWrapupTurns: z.natural().min(0).default(3),
|
|
66
78
|
});
|
|
67
79
|
export function apply(ctx, config) {
|
|
68
80
|
const baseDir = resolveDshHome(config.baseDir);
|
|
@@ -124,6 +136,8 @@ export function apply(ctx, config) {
|
|
|
124
136
|
notifyOnAutoReview: config.notifyOnAutoReview ?? true,
|
|
125
137
|
localFate: config.localFate ?? true,
|
|
126
138
|
fateIntervalTurns: config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6,
|
|
139
|
+
goalBlockedWrapupTurns: config.goalBlockedWrapupTurns ?? 3,
|
|
140
|
+
...(config.reviewModel ? { reviewModel: config.reviewModel } : {}),
|
|
127
141
|
});
|
|
128
142
|
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
143
|
}
|
package/lib/inject.d.ts
CHANGED
|
@@ -103,6 +103,14 @@ 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
|
+
export declare function formatEntriesDirectory(...kindEntries: readonly HarnessEntry[][]): string;
|
|
106
114
|
/**
|
|
107
115
|
* Walk the parent-session chain from `agent` upward and return the nearest
|
|
108
116
|
* session whose local store is non-empty, if any. Children inherit their
|
package/lib/inject.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isArchived } from "./types.js";
|
|
2
2
|
import { mergeHarnessStates } from "./state.js";
|
|
3
3
|
import { entryLine } from "./render.js";
|
|
4
|
+
import { recordInjection } from "./usage.js";
|
|
4
5
|
/** Prompt sections render at most this many entries per kind. */
|
|
5
6
|
export const MAX_INJECTED_ENTRIES_PER_KIND = 6;
|
|
6
7
|
/** Per-entry content budget inside the injected block (matches render.ts). */
|
|
@@ -183,6 +184,30 @@ export function formatSubagentSpecsSection(entries, query) {
|
|
|
183
184
|
}
|
|
184
185
|
return lines.join("\n");
|
|
185
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Gap B3: a lightweight directory of ALL non-archived entries across all
|
|
189
|
+
* kinds — one line per entry (`- [kind:id] title`), no content. This gives
|
|
190
|
+
* the model a zero-cost overview of what exists so it can ask for full text
|
|
191
|
+
* via `evolve_list` or `/evolve list`. The directory is appended after the
|
|
192
|
+
* curated top-N injection sections and adds minimal tokens.
|
|
193
|
+
*/
|
|
194
|
+
export function formatEntriesDirectory(...kindEntries) {
|
|
195
|
+
const allEntries = kindEntries.flat().filter((e) => !isArchived(e));
|
|
196
|
+
if (allEntries.length === 0) {
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
199
|
+
// Skip the directory when it would be redundant (all entries already shown
|
|
200
|
+
// in the curated sections above — 6/kind cap means ≤6 entries total).
|
|
201
|
+
const totalCapped = kindEntries.reduce((sum, entries) => sum + Math.min(entries.filter((e) => !isArchived(e)).length, MAX_INJECTED_ENTRIES_PER_KIND), 0);
|
|
202
|
+
if (allEntries.length <= totalCapped) {
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
const lines = ["# Continual Harness — Entry Directory", "All entries (use evolve_list for full text of any entry):"];
|
|
206
|
+
for (const entry of allEntries.sort((a, b) => `${a.kind}:${a.id}`.localeCompare(`${b.kind}:${b.id}`))) {
|
|
207
|
+
lines.push(`- [${entry.kind}:${entry.id}] ${entry.title}`);
|
|
208
|
+
}
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
}
|
|
186
211
|
/**
|
|
187
212
|
* Walk the parent-session chain from `agent` upward and return the nearest
|
|
188
213
|
* session whose local store is non-empty, if any. Children inherit their
|
|
@@ -222,10 +247,32 @@ export function entriesSectionText(engine, agent, query) {
|
|
|
222
247
|
const promptEntries = Object.values(merged.entries.prompt);
|
|
223
248
|
const subagentEntries = Object.values(merged.entries.subagent);
|
|
224
249
|
const relevanceQuery = (query ?? recentUserText(agent)).trim();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
250
|
+
// Build injected text and collect which entries were included (gap B1).
|
|
251
|
+
const promptText = formatPromptEntriesSection(promptEntries, relevanceQuery);
|
|
252
|
+
const subagentText = formatSubagentSpecsSection(subagentEntries, relevanceQuery);
|
|
253
|
+
const injectedKeys = [];
|
|
254
|
+
// Collect keys from the visible (ranked, capped) entries that actually appear.
|
|
255
|
+
const visiblePrompt = promptEntries.filter((e) => !isArchived(e));
|
|
256
|
+
const visibleSubagent = subagentEntries.filter((e) => !isArchived(e));
|
|
257
|
+
for (const entry of rankEntries(visiblePrompt, relevanceQuery).slice(0, MAX_INJECTED_ENTRIES_PER_KIND)) {
|
|
258
|
+
injectedKeys.push(`prompt:${entry.id}`);
|
|
259
|
+
}
|
|
260
|
+
for (const entry of rankEntries(visibleSubagent, relevanceQuery).slice(0, MAX_INJECTED_ENTRIES_PER_KIND)) {
|
|
261
|
+
injectedKeys.push(`subagent:${entry.id}`);
|
|
262
|
+
}
|
|
263
|
+
// Record usage durably (best-effort: failure never blocks injection).
|
|
264
|
+
if (injectedKeys.length > 0) {
|
|
265
|
+
try {
|
|
266
|
+
recordInjection(engine.baseDir, injectedKeys);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Usage recording is diagnostic; never interrupt the injection path.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Gap B3: lightweight directory of ALL entries (id+title, one line each).
|
|
273
|
+
// Zero-cost index so the model knows what exists and can ask for full text.
|
|
274
|
+
const directoryText = formatEntriesDirectory(Object.values(merged.entries.prompt), Object.values(merged.entries.memory), Object.values(merged.entries.skill), Object.values(merged.entries.subagent));
|
|
275
|
+
const parts = [promptText, subagentText, directoryText].filter((part) => part.length > 0);
|
|
229
276
|
return parts.join("\n\n");
|
|
230
277
|
}
|
|
231
278
|
//# sourceMappingURL=inject.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
export interface StreamTextOptions {
|
|
13
|
+
provider: string;
|
|
14
|
+
model: string;
|
|
15
|
+
system: string;
|
|
16
|
+
prompt: string;
|
|
17
|
+
maxTokens?: number;
|
|
18
|
+
signal?: AbortSignal | undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Stream a single-turn text completion through `ctx.llm`. Forces
|
|
22
|
+
* `reasoningEffort: off` so the model spends its budget on the answer,
|
|
23
|
+
* not visible thinking (reasoning models otherwise produce zero text
|
|
24
|
+
* blocks — the exact failure recorded in FAQ #7).
|
|
25
|
+
*
|
|
26
|
+
* @returns The concatenated text blocks from the response.
|
|
27
|
+
* @throws On provider error, abort, max-token truncation, or empty output.
|
|
28
|
+
*/
|
|
29
|
+
export declare function streamText(ctx: Context, opts: StreamTextOptions): Promise<string>;
|
|
30
|
+
//# sourceMappingURL=llm-text.d.ts.map
|
package/lib/llm-text.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
/**
|
|
3
|
+
* Stream a single-turn text completion through `ctx.llm`. Forces
|
|
4
|
+
* `reasoningEffort: off` so the model spends its budget on the answer,
|
|
5
|
+
* not visible thinking (reasoning models otherwise produce zero text
|
|
6
|
+
* blocks — the exact failure recorded in FAQ #7).
|
|
7
|
+
*
|
|
8
|
+
* @returns The concatenated text blocks from the response.
|
|
9
|
+
* @throws On provider error, abort, max-token truncation, or empty output.
|
|
10
|
+
*/
|
|
11
|
+
export async function streamText(ctx, opts) {
|
|
12
|
+
const assembler = new BlockAssembler();
|
|
13
|
+
for await (const chunk of ctx.llm.stream({
|
|
14
|
+
provider: opts.provider,
|
|
15
|
+
model: opts.model,
|
|
16
|
+
system: opts.system,
|
|
17
|
+
messages: [
|
|
18
|
+
createUserMessage({
|
|
19
|
+
content: [{ type: "text", text: opts.prompt }],
|
|
20
|
+
source: { kind: "plugin", plugin: "dsh-continual-evolve" },
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
reasoningEffort: ReasoningEffortId("off"),
|
|
24
|
+
maxTokens: opts.maxTokens ?? 8000,
|
|
25
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
26
|
+
})) {
|
|
27
|
+
assembler.push(chunk);
|
|
28
|
+
}
|
|
29
|
+
const finish = assembler.finish;
|
|
30
|
+
if (finish.kind === "error") {
|
|
31
|
+
throw new Error(`evolve: LLM call failed: ${finish.failure?.message ?? "unknown"}`);
|
|
32
|
+
}
|
|
33
|
+
if (finish.kind === "aborted") {
|
|
34
|
+
throw new Error("evolve: LLM call aborted");
|
|
35
|
+
}
|
|
36
|
+
if (finish.kind === "max-tokens") {
|
|
37
|
+
throw new Error("evolve: LLM output budget exhausted (max-tokens)");
|
|
38
|
+
}
|
|
39
|
+
const text = assembler
|
|
40
|
+
.blocks()
|
|
41
|
+
.filter((block) => block.type === "text")
|
|
42
|
+
.map((block) => block.text)
|
|
43
|
+
.join("\n");
|
|
44
|
+
if (text.length === 0) {
|
|
45
|
+
throw new Error("evolve: LLM produced no text output");
|
|
46
|
+
}
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=llm-text.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/evolve mount` and `/evolve unmount` subcommand handlers.
|
|
3
|
+
* Extracted from command.ts (P2-2).
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
6
|
+
import type { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
|
|
7
|
+
import type { EvolutionEngine } from "./service.js";
|
|
8
|
+
export declare function executeMountCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation, rest: string[]): Promise<CommandResult>;
|
|
9
|
+
export declare function executeUnmountCommand(ctx: Context, engine: EvolutionEngine, rest: string[]): Promise<CommandResult>;
|
|
10
|
+
//# sourceMappingURL=mount-command.d.ts.map
|