dsh-continual-evolve 0.1.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/LICENSE +21 -0
- package/README.md +290 -0
- package/README.zh.md +240 -0
- package/cordis.patch.yml +9 -0
- package/lib/apply.d.ts +24 -0
- package/lib/apply.js +131 -0
- package/lib/approval.d.ts +14 -0
- package/lib/approval.js +27 -0
- package/lib/auto.d.ts +34 -0
- package/lib/auto.js +217 -0
- package/lib/benchmark.d.ts +72 -0
- package/lib/benchmark.js +167 -0
- package/lib/command.d.ts +36 -0
- package/lib/command.js +549 -0
- package/lib/evaluate.d.ts +38 -0
- package/lib/evaluate.js +142 -0
- package/lib/goal.d.ts +72 -0
- package/lib/goal.js +72 -0
- package/lib/index.d.ts +93 -0
- package/lib/index.js +116 -0
- package/lib/inject.d.ts +124 -0
- package/lib/inject.js +231 -0
- package/lib/logfile.d.ts +71 -0
- package/lib/logfile.js +159 -0
- package/lib/mount.d.ts +42 -0
- package/lib/mount.js +198 -0
- package/lib/notify.d.ts +31 -0
- package/lib/notify.js +42 -0
- package/lib/plan.d.ts +16 -0
- package/lib/plan.js +121 -0
- package/lib/planner.d.ts +30 -0
- package/lib/planner.js +110 -0
- package/lib/pool.d.ts +7 -0
- package/lib/pool.js +25 -0
- package/lib/render.d.ts +15 -0
- package/lib/render.js +83 -0
- package/lib/review.d.ts +37 -0
- package/lib/review.js +127 -0
- package/lib/rollback.d.ts +11 -0
- package/lib/rollback.js +69 -0
- package/lib/rubric.d.ts +29 -0
- package/lib/rubric.js +119 -0
- package/lib/score.d.ts +31 -0
- package/lib/score.js +81 -0
- package/lib/service.d.ts +30 -0
- package/lib/service.js +42 -0
- package/lib/skill.d.ts +10 -0
- package/lib/skill.js +75 -0
- package/lib/source.d.ts +29 -0
- package/lib/source.js +42 -0
- package/lib/state.d.ts +34 -0
- package/lib/state.js +154 -0
- package/lib/store.d.ts +20 -0
- package/lib/store.js +74 -0
- package/lib/tool.d.ts +15 -0
- package/lib/tool.js +163 -0
- package/lib/types.d.ts +137 -0
- package/lib/types.js +62 -0
- package/lib/validate.d.ts +11 -0
- package/lib/validate.js +55 -0
- package/package.json +67 -0
package/lib/evaluate.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { mapPool } from "./pool.js";
|
|
2
|
+
import { decryptRubric, deriveKey, DEV_RUBRIC_KEY } from "./rubric.js";
|
|
3
|
+
/** Key used when the caller did not resolve one: mirrors resolveRubricKey's last-resort dev fallback. */
|
|
4
|
+
function devRubricKey() {
|
|
5
|
+
return deriveKey(DEV_RUBRIC_KEY);
|
|
6
|
+
}
|
|
7
|
+
const EVAL_SYSTEM_PROMPT = `You are one evaluation unit in a benchmark matrix.
|
|
8
|
+
|
|
9
|
+
You are the agent under evaluation. Perform the case task using your tools,
|
|
10
|
+
then score your own execution strictly against the rubric. The harness
|
|
11
|
+
guidance attached to the state under test is included in the task.
|
|
12
|
+
|
|
13
|
+
Your reply is structured (see the requested output schema): caseId, run,
|
|
14
|
+
score (0-100), passed (true iff score >= the stated threshold), and notes
|
|
15
|
+
(concrete evidence for the score).`;
|
|
16
|
+
const CELL_SCHEMA = {
|
|
17
|
+
type: "object",
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
properties: {
|
|
20
|
+
caseId: { type: "string" },
|
|
21
|
+
run: { type: "number" },
|
|
22
|
+
score: { type: "number" },
|
|
23
|
+
passed: { type: "boolean" },
|
|
24
|
+
notes: { type: "string" },
|
|
25
|
+
},
|
|
26
|
+
required: ["caseId", "run", "score", "passed", "notes"],
|
|
27
|
+
};
|
|
28
|
+
/** How many evaluation units may run concurrently (bounded subagent fan-out). */
|
|
29
|
+
export const DEFAULT_EVALUATION_CONCURRENCY = 4;
|
|
30
|
+
export async function evaluateState(ctx, agent, options) {
|
|
31
|
+
if (!agent.options.provider || !agent.options.model) {
|
|
32
|
+
throw new Error("evolve: benchmark evaluation requires a provider/model route");
|
|
33
|
+
}
|
|
34
|
+
const subagents = ctx.subagents;
|
|
35
|
+
if (!subagents) {
|
|
36
|
+
throw new Error("evolve: benchmark evaluation requires the subagents service");
|
|
37
|
+
}
|
|
38
|
+
const units = [];
|
|
39
|
+
for (const c of options.cases) {
|
|
40
|
+
for (let run = 1; run <= options.runs; run += 1) {
|
|
41
|
+
units.push({ case: c, run });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const cells = await mapPool(units, DEFAULT_EVALUATION_CONCURRENCY, (unit) => runUnit(subagents, agent, options, unit.case, unit.run));
|
|
45
|
+
return { label: options.label, cells, stopReason: "completed" };
|
|
46
|
+
}
|
|
47
|
+
async function runUnit(subagents, agent, options, c, run) {
|
|
48
|
+
// The ONLY rubric decryption point: the envelope is opened here, in the
|
|
49
|
+
// host, and the plaintext goes straight into the child prompt. The
|
|
50
|
+
// optimizer never reaches this path.
|
|
51
|
+
let rubric;
|
|
52
|
+
try {
|
|
53
|
+
rubric = decryptRubric(c.rubric, options.rubricKey ?? devRubricKey());
|
|
54
|
+
}
|
|
55
|
+
catch (cause) {
|
|
56
|
+
return {
|
|
57
|
+
caseId: c.id,
|
|
58
|
+
run,
|
|
59
|
+
score: 0,
|
|
60
|
+
passed: false,
|
|
61
|
+
notes: `rubric decrypt failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const prompt = [
|
|
65
|
+
EVAL_SYSTEM_PROMPT,
|
|
66
|
+
"---",
|
|
67
|
+
"Your harness guidance (state under test):",
|
|
68
|
+
`<harness_overview>\n${options.harnessOverview}\n</harness_overview>`,
|
|
69
|
+
`Case ${c.id} — task (statement):\n${c.statement}`,
|
|
70
|
+
`Rubric — score yourself strictly against these criteria:\n${rubric}`,
|
|
71
|
+
`Run ${run} of ${options.runs}. passThreshold = ${options.passThreshold}.`,
|
|
72
|
+
"Execute the task with your tools, then produce the structured evaluation.",
|
|
73
|
+
].join("\n\n");
|
|
74
|
+
try {
|
|
75
|
+
const runObj = await subagents.start("spawn", {
|
|
76
|
+
label: `${c.id} r${run}`,
|
|
77
|
+
prompt: [{ type: "text", text: prompt }],
|
|
78
|
+
parent: agent,
|
|
79
|
+
signal: options.signal ?? new AbortController().signal,
|
|
80
|
+
outputSchema: CELL_SCHEMA,
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
const settled = await runObj.result;
|
|
84
|
+
if (settled.stopReason !== "completed") {
|
|
85
|
+
throw new Error(`child stopped: ${settled.stopReason ?? "unknown"}`);
|
|
86
|
+
}
|
|
87
|
+
const parsed = normalizeCell(settled.structured, c.id, run, options.passThreshold) ??
|
|
88
|
+
fromOutputText(settled.output, c.id, run, options.passThreshold);
|
|
89
|
+
if (!parsed) {
|
|
90
|
+
throw new Error("child returned neither a structured value nor usable text");
|
|
91
|
+
}
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
runObj.dispose();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (cause) {
|
|
99
|
+
return {
|
|
100
|
+
caseId: c.id,
|
|
101
|
+
run,
|
|
102
|
+
score: 0,
|
|
103
|
+
passed: false,
|
|
104
|
+
notes: `unit failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Validate a provider-validated structured cell; returns undefined when malformed. */
|
|
109
|
+
export function normalizeCell(value, caseId, run, passThreshold) {
|
|
110
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
111
|
+
return undefined;
|
|
112
|
+
const record = value;
|
|
113
|
+
const score = Number(record["score"]);
|
|
114
|
+
if (!Number.isFinite(score))
|
|
115
|
+
return undefined;
|
|
116
|
+
return {
|
|
117
|
+
caseId: typeof record["caseId"] === "string" && record["caseId"].length > 0 ? record["caseId"] : caseId,
|
|
118
|
+
run: typeof record["run"] === "number" && Number.isFinite(record["run"]) ? Math.trunc(record["run"]) : run,
|
|
119
|
+
score: Math.min(100, Math.max(0, score)),
|
|
120
|
+
passed: record["passed"] === true || score >= passThreshold,
|
|
121
|
+
notes: typeof record["notes"] === "string" ? record["notes"] : "",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Fallback: recover a cell from the child's text blocks when no structured value arrived. */
|
|
125
|
+
function fromOutputText(blocks, caseId, run, passThreshold) {
|
|
126
|
+
if (!Array.isArray(blocks))
|
|
127
|
+
return undefined;
|
|
128
|
+
const text = blocks
|
|
129
|
+
.filter((block) => block.type === "text")
|
|
130
|
+
.map((block) => block.text ?? "")
|
|
131
|
+
.join("\n");
|
|
132
|
+
if (text.length === 0)
|
|
133
|
+
return undefined;
|
|
134
|
+
try {
|
|
135
|
+
const value = JSON.parse(text);
|
|
136
|
+
return normalizeCell(value, caseId, run, passThreshold);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=evaluate.js.map
|
package/lib/goal.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal-driven evolution rounds (v3 optional item, design.md §6): a
|
|
3
|
+
* same-session goal turns the auto-review into a per-round driver — while a
|
|
4
|
+
* goal is active, the review gate runs every round instead of every
|
|
5
|
+
* `reviewIntervalTurns`, so the "continual evolution loop" is driven by the
|
|
6
|
+
* goal's round machine (goal-round-driver keeps the session continuing) and
|
|
7
|
+
* stops when the goal is completed or blocked.
|
|
8
|
+
*
|
|
9
|
+
* The goal service (`ctx.goals`) is resolved lazily and never required
|
|
10
|
+
* (FAQ #1 discipline): without it, `/evolve goal` reports the feature
|
|
11
|
+
* unavailable and auto-review keeps its plain interval.
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
14
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
15
|
+
/** The durable goal phases we drive on. */
|
|
16
|
+
export type GoalPhase = "active" | "paused" | "blocked" | "complete";
|
|
17
|
+
/** Minimal structural view of a goal view (duck-typed against dsh-goal). */
|
|
18
|
+
export interface GoalViewLike {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly revision: number;
|
|
21
|
+
readonly objective: string;
|
|
22
|
+
readonly phase: GoalPhase;
|
|
23
|
+
readonly maxGoalRounds: number;
|
|
24
|
+
}
|
|
25
|
+
export interface GoalRefLike {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly revision: number;
|
|
28
|
+
}
|
|
29
|
+
export interface GoalServiceLike {
|
|
30
|
+
get(agent: Agent): GoalViewLike | undefined;
|
|
31
|
+
create(agent: Agent, request: {
|
|
32
|
+
objective: string;
|
|
33
|
+
maxGoalRounds?: number;
|
|
34
|
+
}): GoalViewLike;
|
|
35
|
+
edit(agent: Agent, ref: GoalRefLike, request: {
|
|
36
|
+
objective?: string;
|
|
37
|
+
maxGoalRounds?: number;
|
|
38
|
+
}): GoalViewLike;
|
|
39
|
+
complete(agent: Agent, ref: GoalRefLike): GoalViewLike;
|
|
40
|
+
block?(agent: Agent, ref: GoalRefLike, reason: {
|
|
41
|
+
code: string;
|
|
42
|
+
reason: string;
|
|
43
|
+
}): GoalViewLike;
|
|
44
|
+
}
|
|
45
|
+
/** The default objective used by `/evolve goal` without an explicit one. */
|
|
46
|
+
export declare const DEFAULT_EVOLVE_GOAL_OBJECTIVE = "\u6301\u7EED\u8FDB\u5316\u672C\u4F1A\u8BDD harness \u72B6\u6001\uFF1A\u6BCF\u8F6E\u6C89\u6DC0\u53EF\u590D\u7528\u7ECF\u9A8C\uFF08\u5931\u8D25/\u6218\u672F/\u4E8B\u5B9E/\u59D4\u6D3E\u89C4\u683C\uFF09\uFF0C\u4FDD\u6301\u6761\u76EE\u5C0F\u800C\u5E26\u8BC1\u636E";
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the goal service lazily; undefined when the profile lacks it.
|
|
49
|
+
* Uses `ctx.get("goals")` (the global service registry) — a direct property
|
|
50
|
+
* access like `ctx.goals` walks only the caller's fiber ancestor chain and
|
|
51
|
+
* throws "cannot get property \"goals\" without inject" for services
|
|
52
|
+
* provided by sibling plugin entries (the goal plugin is a sibling of this
|
|
53
|
+
* one in the profile tree).
|
|
54
|
+
*/
|
|
55
|
+
export declare function goalServiceOf(ctx: Context): GoalServiceLike | undefined;
|
|
56
|
+
/** True when a goal view exists and is in a round-driving phase (active). */
|
|
57
|
+
export declare function goalDrivesRounds(view: GoalViewLike | undefined): boolean;
|
|
58
|
+
/** True when a goal exists at all (any phase) — used to gate create vs edit. */
|
|
59
|
+
export declare function goalExists(view: GoalViewLike | undefined): view is GoalViewLike;
|
|
60
|
+
/** Human-readable one-line status for a goal view. */
|
|
61
|
+
export declare function goalStatusText(view: GoalViewLike): string;
|
|
62
|
+
/**
|
|
63
|
+
* Create or edit the session's evolution goal. With no current goal: create.
|
|
64
|
+
* With a current goal: edit its objective (create-on-first-use semantics are
|
|
65
|
+
* enforced by the goal service itself; completed goals may be replaced).
|
|
66
|
+
*/
|
|
67
|
+
export declare function upsertEvolutionGoal(ctx: Context, agent: Agent, objective?: string): GoalViewLike;
|
|
68
|
+
/** Complete the current goal, returning the completed view or undefined. */
|
|
69
|
+
export declare function completeEvolutionGoal(ctx: Context, agent: Agent): GoalViewLike | undefined;
|
|
70
|
+
/** Block the current goal with a reason (when the service supports it). */
|
|
71
|
+
export declare function blockEvolutionGoal(ctx: Context, agent: Agent, reason: string): GoalViewLike | undefined;
|
|
72
|
+
//# sourceMappingURL=goal.d.ts.map
|
package/lib/goal.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** The default objective used by `/evolve goal` without an explicit one. */
|
|
2
|
+
export const DEFAULT_EVOLVE_GOAL_OBJECTIVE = "持续进化本会话 harness 状态:每轮沉淀可复用经验(失败/战术/事实/委派规格),保持条目小而带证据";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the goal service lazily; undefined when the profile lacks it.
|
|
5
|
+
* Uses `ctx.get("goals")` (the global service registry) — a direct property
|
|
6
|
+
* access like `ctx.goals` walks only the caller's fiber ancestor chain and
|
|
7
|
+
* throws "cannot get property \"goals\" without inject" for services
|
|
8
|
+
* provided by sibling plugin entries (the goal plugin is a sibling of this
|
|
9
|
+
* one in the profile tree).
|
|
10
|
+
*/
|
|
11
|
+
export function goalServiceOf(ctx) {
|
|
12
|
+
return ctx.get("goals");
|
|
13
|
+
}
|
|
14
|
+
/** True when a goal view exists and is in a round-driving phase (active). */
|
|
15
|
+
export function goalDrivesRounds(view) {
|
|
16
|
+
return view?.phase === "active";
|
|
17
|
+
}
|
|
18
|
+
/** True when a goal exists at all (any phase) — used to gate create vs edit. */
|
|
19
|
+
export function goalExists(view) {
|
|
20
|
+
return view !== undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Human-readable one-line status for a goal view. */
|
|
23
|
+
export function goalStatusText(view) {
|
|
24
|
+
const rounds = typeof view.roundsStarted === "number"
|
|
25
|
+
? String(view.roundsStarted)
|
|
26
|
+
: "?";
|
|
27
|
+
return `[${view.phase}] ${view.objective} (rounds=${rounds}/${view.maxGoalRounds}, revision=${view.revision})`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create or edit the session's evolution goal. With no current goal: create.
|
|
31
|
+
* With a current goal: edit its objective (create-on-first-use semantics are
|
|
32
|
+
* enforced by the goal service itself; completed goals may be replaced).
|
|
33
|
+
*/
|
|
34
|
+
export function upsertEvolutionGoal(ctx, agent, objective) {
|
|
35
|
+
const goals = goalServiceOf(ctx);
|
|
36
|
+
if (!goals) {
|
|
37
|
+
throw new Error("evolve: /evolve goal requires the goals service (load @deepseek-ai/dsh-goal)");
|
|
38
|
+
}
|
|
39
|
+
const current = goals.get(agent);
|
|
40
|
+
const nextObjective = objective && objective.length > 0 ? objective : DEFAULT_EVOLVE_GOAL_OBJECTIVE;
|
|
41
|
+
if (goalExists(current)) {
|
|
42
|
+
if (current.phase === "active" || current.phase === "paused") {
|
|
43
|
+
return goals.edit(agent, { id: current.id, revision: current.revision }, { objective: nextObjective });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return goals.create(agent, { objective: nextObjective });
|
|
47
|
+
}
|
|
48
|
+
/** Complete the current goal, returning the completed view or undefined. */
|
|
49
|
+
export function completeEvolutionGoal(ctx, agent) {
|
|
50
|
+
const goals = goalServiceOf(ctx);
|
|
51
|
+
if (!goals) {
|
|
52
|
+
throw new Error("evolve: /evolve goal requires the goals service (load @deepseek-ai/dsh-goal)");
|
|
53
|
+
}
|
|
54
|
+
const current = goals.get(agent);
|
|
55
|
+
if (!goalExists(current) || current.phase === "complete") {
|
|
56
|
+
return current;
|
|
57
|
+
}
|
|
58
|
+
return goals.complete(agent, { id: current.id, revision: current.revision });
|
|
59
|
+
}
|
|
60
|
+
/** Block the current goal with a reason (when the service supports it). */
|
|
61
|
+
export function blockEvolutionGoal(ctx, agent, reason) {
|
|
62
|
+
const goals = goalServiceOf(ctx);
|
|
63
|
+
if (!goals || typeof goals.block !== "function") {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const current = goals.get(agent);
|
|
67
|
+
if (!goalExists(current) || current.phase !== "active") {
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
return goals.block(agent, { id: current.id, revision: current.revision }, { code: "evolve-blocked", reason });
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=goal.js.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
3
|
+
import { type EvolutionEngine } from "./service.js";
|
|
4
|
+
export declare const name = "continual-evolve";
|
|
5
|
+
/** Service key under which the evolution engine is published. */
|
|
6
|
+
export declare const EVOLUTION_SERVICE = "evolution";
|
|
7
|
+
export declare const inject: string[];
|
|
8
|
+
export declare const Config: z<Schemastery.ObjectS<{
|
|
9
|
+
/** Root for evolution stores; defaults to the resolved DSH home. */
|
|
10
|
+
baseDir: z<string, string>;
|
|
11
|
+
/** System-prompt section order for the evolution guidance. */
|
|
12
|
+
sectionOrder: z<number, number>;
|
|
13
|
+
/** Enable the automatic review gate (off by default: it costs model calls). */
|
|
14
|
+
autoReview: z<boolean, boolean>;
|
|
15
|
+
/** Gate runs when this many turns have passed since the last review. */
|
|
16
|
+
reviewIntervalTurns: z<number, number>;
|
|
17
|
+
/** Trajectory slice handed to the gate, in characters. */
|
|
18
|
+
maxReviewInputChars: z<number, number>;
|
|
19
|
+
/** Output budget for the cheap gate call. */
|
|
20
|
+
reviewBudgetTokens: z<number, number>;
|
|
21
|
+
/** After an approved gate run with applied edits, queue a visible follow-up notice. */
|
|
22
|
+
notifyOnAutoReview: z<boolean, boolean>;
|
|
23
|
+
/** Cross-session (global) edits require an explicit human approval. */
|
|
24
|
+
requireGlobalApproval: z<boolean, boolean>;
|
|
25
|
+
/** Skills root for materialized skill entries; defaults to <dshHome>/skills. */
|
|
26
|
+
skillsDir: z<string, string>;
|
|
27
|
+
/** Passphrase for rubric encryption; falls back to DSH_EVOLVE_RUBRIC_KEY, then a local key file. */
|
|
28
|
+
rubricKey: z<string, string>;
|
|
29
|
+
/** Write all cordis log messages to <baseDir>/evolve/plugin.log (JSONL). */
|
|
30
|
+
logToFile: z<boolean, boolean>;
|
|
31
|
+
/** File log level: 0=error, 1=info, 2=warn, 3=debug. */
|
|
32
|
+
logLevel: z<number, number>;
|
|
33
|
+
/** Rotate the file log when it exceeds this many bytes. */
|
|
34
|
+
logMaxBytes: z<number, number>;
|
|
35
|
+
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
36
|
+
autoRollbackOnReject: z<boolean, boolean>;
|
|
37
|
+
}>, Schemastery.ObjectT<{
|
|
38
|
+
/** Root for evolution stores; defaults to the resolved DSH home. */
|
|
39
|
+
baseDir: z<string, string>;
|
|
40
|
+
/** System-prompt section order for the evolution guidance. */
|
|
41
|
+
sectionOrder: z<number, number>;
|
|
42
|
+
/** Enable the automatic review gate (off by default: it costs model calls). */
|
|
43
|
+
autoReview: z<boolean, boolean>;
|
|
44
|
+
/** Gate runs when this many turns have passed since the last review. */
|
|
45
|
+
reviewIntervalTurns: z<number, number>;
|
|
46
|
+
/** Trajectory slice handed to the gate, in characters. */
|
|
47
|
+
maxReviewInputChars: z<number, number>;
|
|
48
|
+
/** Output budget for the cheap gate call. */
|
|
49
|
+
reviewBudgetTokens: z<number, number>;
|
|
50
|
+
/** After an approved gate run with applied edits, queue a visible follow-up notice. */
|
|
51
|
+
notifyOnAutoReview: z<boolean, boolean>;
|
|
52
|
+
/** Cross-session (global) edits require an explicit human approval. */
|
|
53
|
+
requireGlobalApproval: z<boolean, boolean>;
|
|
54
|
+
/** Skills root for materialized skill entries; defaults to <dshHome>/skills. */
|
|
55
|
+
skillsDir: z<string, string>;
|
|
56
|
+
/** Passphrase for rubric encryption; falls back to DSH_EVOLVE_RUBRIC_KEY, then a local key file. */
|
|
57
|
+
rubricKey: z<string, string>;
|
|
58
|
+
/** Write all cordis log messages to <baseDir>/evolve/plugin.log (JSONL). */
|
|
59
|
+
logToFile: z<boolean, boolean>;
|
|
60
|
+
/** File log level: 0=error, 1=info, 2=warn, 3=debug. */
|
|
61
|
+
logLevel: z<number, number>;
|
|
62
|
+
/** Rotate the file log when it exceeds this many bytes. */
|
|
63
|
+
logMaxBytes: z<number, number>;
|
|
64
|
+
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
65
|
+
autoRollbackOnReject: z<boolean, boolean>;
|
|
66
|
+
}>>;
|
|
67
|
+
/** Structurally typed resolved config (loader passes the validated object). */
|
|
68
|
+
export interface EvolveConfig {
|
|
69
|
+
baseDir?: string;
|
|
70
|
+
sectionOrder?: number;
|
|
71
|
+
autoReview?: boolean;
|
|
72
|
+
reviewIntervalTurns?: number;
|
|
73
|
+
maxReviewInputChars?: number;
|
|
74
|
+
reviewBudgetTokens?: number;
|
|
75
|
+
notifyOnAutoReview?: boolean;
|
|
76
|
+
requireGlobalApproval?: boolean;
|
|
77
|
+
skillsDir?: string;
|
|
78
|
+
rubricKey?: string;
|
|
79
|
+
/** Write all cordis log messages to <baseDir>/evolve/plugin.log (JSONL). */
|
|
80
|
+
logToFile?: boolean;
|
|
81
|
+
/** File log level: 0=error, 1=info, 2=warn, 3=debug. */
|
|
82
|
+
logLevel?: number;
|
|
83
|
+
/** Rotate the file log when it exceeds this many bytes. */
|
|
84
|
+
logMaxBytes?: number;
|
|
85
|
+
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
86
|
+
autoRollbackOnReject?: boolean;
|
|
87
|
+
}
|
|
88
|
+
export interface EvolutionService {
|
|
89
|
+
readonly engine: EvolutionEngine;
|
|
90
|
+
readonly baseDir: string;
|
|
91
|
+
}
|
|
92
|
+
export declare function apply(ctx: Context, config: EvolveConfig): void;
|
|
93
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-continual-evolve — plugin entry (Phase 2: auto review gate).
|
|
3
|
+
*
|
|
4
|
+
* Mounts the evolution engine, registers the model-facing evolve_* tools,
|
|
5
|
+
* the human-facing /evolve command, the system-prompt guidance section, and
|
|
6
|
+
* (opt-in) the automatic review gate that runs the planner on a turn
|
|
7
|
+
* interval. Store roots default under the resolved DSH home; a deployment
|
|
8
|
+
* may override `baseDir` in the plugin config.
|
|
9
|
+
*/
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import z from "@deepseek-ai/schemastery";
|
|
12
|
+
import { expandHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
13
|
+
import { createEvolutionEngine } from "./service.js";
|
|
14
|
+
import { registerEvolveTools } from "./tool.js";
|
|
15
|
+
import { registerEvolveCommand } from "./command.js";
|
|
16
|
+
import { registerAutoReview } from "./auto.js";
|
|
17
|
+
import { syncSkillsFromResult } from "./skill.js";
|
|
18
|
+
import { entriesSectionText } from "./inject.js";
|
|
19
|
+
import { resolveRubricKey } from "./rubric.js";
|
|
20
|
+
import { restoreMounted } from "./mount.js";
|
|
21
|
+
import { registerFileLogger } from "./logfile.js";
|
|
22
|
+
export const name = "continual-evolve";
|
|
23
|
+
/** Service key under which the evolution engine is published. */
|
|
24
|
+
export const EVOLUTION_SERVICE = "evolution";
|
|
25
|
+
export const inject = ["tools", "commands", "systemPrompt", "llm", "sessionQuery", "agents", "userQuestions", "subagents"];
|
|
26
|
+
export const Config = z.object({
|
|
27
|
+
/** Root for evolution stores; defaults to the resolved DSH home. */
|
|
28
|
+
baseDir: z.string(),
|
|
29
|
+
/** System-prompt section order for the evolution guidance. */
|
|
30
|
+
sectionOrder: z.natural().default(118),
|
|
31
|
+
/** Enable the automatic review gate (off by default: it costs model calls). */
|
|
32
|
+
autoReview: z.boolean().default(false),
|
|
33
|
+
/** Gate runs when this many turns have passed since the last review. */
|
|
34
|
+
reviewIntervalTurns: z.natural().default(6),
|
|
35
|
+
/** Trajectory slice handed to the gate, in characters. */
|
|
36
|
+
maxReviewInputChars: z.natural().default(40000),
|
|
37
|
+
/** Output budget for the cheap gate call. */
|
|
38
|
+
reviewBudgetTokens: z.natural().default(4096),
|
|
39
|
+
/** After an approved gate run with applied edits, queue a visible follow-up notice. */
|
|
40
|
+
notifyOnAutoReview: z.boolean().default(true),
|
|
41
|
+
/** Cross-session (global) edits require an explicit human approval. */
|
|
42
|
+
requireGlobalApproval: z.boolean().default(true),
|
|
43
|
+
/** Skills root for materialized skill entries; defaults to <dshHome>/skills. */
|
|
44
|
+
skillsDir: z.string(),
|
|
45
|
+
/** Passphrase for rubric encryption; falls back to DSH_EVOLVE_RUBRIC_KEY, then a local key file. */
|
|
46
|
+
rubricKey: z.string(),
|
|
47
|
+
/** Write all cordis log messages to <baseDir>/evolve/plugin.log (JSONL). */
|
|
48
|
+
logToFile: z.boolean().default(true),
|
|
49
|
+
/** File log level: 0=error, 1=info, 2=warn, 3=debug. */
|
|
50
|
+
logLevel: z.natural().default(1),
|
|
51
|
+
/** Rotate the file log when it exceeds this many bytes. */
|
|
52
|
+
logMaxBytes: z.natural().default(5 * 1024 * 1024),
|
|
53
|
+
/** After a benchmark decision rejects a candidate, roll the refinement back automatically. */
|
|
54
|
+
autoRollbackOnReject: z.boolean().default(true),
|
|
55
|
+
});
|
|
56
|
+
export function apply(ctx, config) {
|
|
57
|
+
const baseDir = resolveDshHome(config.baseDir);
|
|
58
|
+
const skillsRoot = config.skillsDir ? expandHomePath(config.skillsDir) : join(baseDir, "skills");
|
|
59
|
+
const engine = createEvolutionEngine(baseDir, {
|
|
60
|
+
onApplied: (result) => {
|
|
61
|
+
try {
|
|
62
|
+
syncSkillsFromResult(skillsRoot, result);
|
|
63
|
+
}
|
|
64
|
+
catch (cause) {
|
|
65
|
+
ctx
|
|
66
|
+
.logger("continual-evolve")
|
|
67
|
+
.warn(`skill materialization failed for ${result.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
ctx.provide(EVOLUTION_SERVICE, { engine, baseDir });
|
|
72
|
+
ctx.systemPrompt.section({
|
|
73
|
+
name: "tool:continual-evolve",
|
|
74
|
+
order: config.sectionOrder ?? 118,
|
|
75
|
+
text: "You have a continual harness: versioned, persistent prompt notes, memories, skills, and subagent specs. Prompt notes and delegation specs are injected below; use evolve_list for the full state. Create an entry (evolve_add) after a repeated failure, a reusable tactic, a durable fact or preference, a repeated procedure, or a repeated delegation role. Keep edits small and evidence-backed; prefer local scope, use global: true only for stable cross-session lessons. Update or delete (evolve_update / evolve_delete) when an entry is wrong or obsolete; roll back faulty refinements with evolve_rollback. Every edit is snapshotted, versioned, and recorded — no edit can be silently lost.",
|
|
76
|
+
});
|
|
77
|
+
// Phase 2: make prompt entries real system-prompt content and subagent
|
|
78
|
+
// entries real delegation specs. The text is a provider evaluated at every
|
|
79
|
+
// assembly with the assembling agent; a store without prompt/subagent
|
|
80
|
+
// entries renders to "" and the prompt renderer drops the section.
|
|
81
|
+
ctx.systemPrompt.section({
|
|
82
|
+
name: "tool:continual-evolve:entries",
|
|
83
|
+
order: (config.sectionOrder ?? 118) + 1,
|
|
84
|
+
text: (context) => entriesSectionText(engine, context.agent),
|
|
85
|
+
});
|
|
86
|
+
const gate = { requireGlobalApproval: config.requireGlobalApproval ?? true };
|
|
87
|
+
registerEvolveTools(ctx, engine, gate);
|
|
88
|
+
registerEvolveCommand(ctx, engine, gate, {
|
|
89
|
+
rubricKey: resolveRubricKey(baseDir, config.rubricKey, process.env, (m) => ctx.logger("continual-evolve").warn(m)),
|
|
90
|
+
autoRollbackOnReject: config.autoRollbackOnReject ?? true,
|
|
91
|
+
});
|
|
92
|
+
// Plugin-owned file logging: every cordis log message lands in
|
|
93
|
+
// <baseDir>/evolve/plugin.log regardless of how dsh web was launched —
|
|
94
|
+
// no extra component to install, no startup-script dependency.
|
|
95
|
+
if (config.logToFile !== false) {
|
|
96
|
+
registerFileLogger(ctx, baseDir, {
|
|
97
|
+
logLevel: config.logLevel ?? 1,
|
|
98
|
+
...(config.logMaxBytes !== undefined ? { logMaxBytes: config.logMaxBytes } : {}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// v2 optional: restore hot-mounted skill plugins after a restart.
|
|
102
|
+
void restoreMounted(ctx, baseDir).catch((cause) => {
|
|
103
|
+
ctx.logger("continual-evolve").warn(`mount restore failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
104
|
+
});
|
|
105
|
+
if (config.autoReview) {
|
|
106
|
+
registerAutoReview(ctx, engine, {
|
|
107
|
+
intervalTurns: config.reviewIntervalTurns ?? 6,
|
|
108
|
+
maxInputChars: config.maxReviewInputChars ?? 40000,
|
|
109
|
+
budgetTokens: config.reviewBudgetTokens ?? 4096,
|
|
110
|
+
notifyOnAutoReview: config.notifyOnAutoReview ?? true,
|
|
111
|
+
});
|
|
112
|
+
ctx.logger("continual-evolve").info(`continual-evolve auto-review enabled (every ${config.reviewIntervalTurns ?? 6} turns)`);
|
|
113
|
+
}
|
|
114
|
+
ctx.logger("continual-evolve").info(`continual-evolve mounted (baseDir=${baseDir})`);
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
package/lib/inject.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real prompt/spec injection: the dynamic system-prompt section that makes
|
|
3
|
+
* `prompt` entries visible to the model without a tool call, and `subagent`
|
|
4
|
+
* entries available as reusable delegation specs at the delegation seam.
|
|
5
|
+
*
|
|
6
|
+
* Design (design.md §7 Phase 2):
|
|
7
|
+
* - the section text is a provider evaluated at every assembly with the
|
|
8
|
+
* assembling agent; a section that renders to "" is dropped by the prompt
|
|
9
|
+
* renderer, so an empty store costs zero tokens;
|
|
10
|
+
* - prompt entries render as an additive section (the base system prompt is
|
|
11
|
+
* never touched); subagent entries render as delegation specs the parent
|
|
12
|
+
* follows when delegating, and are inherited by child agents through the
|
|
13
|
+
* `SessionHeader.parentSession` chain so a freshly spawned subagent carries
|
|
14
|
+
* its parent's specs without any provider wrapping;
|
|
15
|
+
* - every cap mirrors render.ts (6 entries/kind, 180 chars/entry, stable
|
|
16
|
+
* sort), keeping the injected cost bounded no matter how the store grows;
|
|
17
|
+
* - full text stays one `evolve_list` call away: the injected block is a
|
|
18
|
+
* summary index, not a duplicate of the store.
|
|
19
|
+
*/
|
|
20
|
+
import type { HarnessEntry, HarnessState } from "./types.js";
|
|
21
|
+
import type { EvolutionEngine } from "./service.js";
|
|
22
|
+
/** Prompt sections render at most this many entries per kind. */
|
|
23
|
+
export declare const MAX_INJECTED_ENTRIES_PER_KIND = 6;
|
|
24
|
+
/** Per-entry content budget inside the injected block (matches render.ts). */
|
|
25
|
+
export declare const MAX_INJECTED_CONTENT_LENGTH = 180;
|
|
26
|
+
/** How many `parentSession` hops a child walks to inherit entries. */
|
|
27
|
+
export declare const MAX_PARENT_CHAIN_DEPTH = 8;
|
|
28
|
+
/** Recency half-life for the injection ranking: an entry this old scores 0. */
|
|
29
|
+
export declare const RECENCY_HALF_LIFE_MS: number;
|
|
30
|
+
/** At most this many recent user messages feed the relevance query. */
|
|
31
|
+
export declare const MAX_QUERY_MESSAGES = 3;
|
|
32
|
+
/** Query text handed to the relevance scorer is capped at this many chars. */
|
|
33
|
+
export declare const MAX_QUERY_CHARS = 400;
|
|
34
|
+
/** A user-message event's durable shape, loosened for duck typing. */
|
|
35
|
+
export interface UserMessageEventLike {
|
|
36
|
+
type?: string;
|
|
37
|
+
data?: {
|
|
38
|
+
content?: unknown;
|
|
39
|
+
source?: {
|
|
40
|
+
kind?: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** The minimal agent shape the section provider needs (duck-typed). */
|
|
45
|
+
export interface AgentLike {
|
|
46
|
+
id: string;
|
|
47
|
+
session?: {
|
|
48
|
+
header?: {
|
|
49
|
+
parentSession?: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Live append-only session log. Typed loosely (`unknown[]`) so the real
|
|
53
|
+
* `SessionEvent[]` union from dsh-session is assignable; rows are
|
|
54
|
+
* narrowed to {@link UserMessageEventLike} at read time.
|
|
55
|
+
*/
|
|
56
|
+
events?: readonly unknown[];
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** The section-provider context shape we consume (subset of AssembleContext). */
|
|
60
|
+
export interface InjectContext {
|
|
61
|
+
agent?: AgentLike;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Lowercase tokenization for the keyword relevance scorer: runs of ASCII
|
|
65
|
+
* alphanumerics and CJK characters become tokens (CJK is not split so whole
|
|
66
|
+
* Chinese words/characters stay comparable), everything else is a separator.
|
|
67
|
+
*/
|
|
68
|
+
export declare function tokenize(text: string): string[];
|
|
69
|
+
/**
|
|
70
|
+
* Keyword hit count of `query` tokens inside an entry: title hits weigh 2×,
|
|
71
|
+
* content/path hits 1×. BM25-level relevance without any external service.
|
|
72
|
+
*/
|
|
73
|
+
export declare function relevanceHits(entry: HarnessEntry, query: string): number;
|
|
74
|
+
/**
|
|
75
|
+
* Normalized recency in [0, 1]: 1 when the entry was just updated, decaying
|
|
76
|
+
* linearly to 0 after {@link RECENCY_HALF_LIFE_MS}. Unparseable timestamps
|
|
77
|
+
* score 0 (never preferred over a timestamped entry).
|
|
78
|
+
*/
|
|
79
|
+
export declare function recencyScore(entry: HarnessEntry, now: number): number;
|
|
80
|
+
/**
|
|
81
|
+
* Rank entries for injection, best first. With no query the ranking is pure
|
|
82
|
+
* recency (newest first). With a query, any entry with at least one keyword
|
|
83
|
+
* hit outranks every hit-less entry (`hits * 2 + recency <= 1` for the
|
|
84
|
+
* latter), and hits decide the order among relevant entries; recency then
|
|
85
|
+
* breaks remaining ties, and the stable dictionary order is the final
|
|
86
|
+
* tiebreak, so the result is deterministic.
|
|
87
|
+
*/
|
|
88
|
+
export declare function rankEntries(entries: readonly HarnessEntry[], query?: string, now?: number): HarnessEntry[];
|
|
89
|
+
/**
|
|
90
|
+
* Compose the relevance query from the assembling agent's most recent direct
|
|
91
|
+
* user messages (event rows whose `type` is `user/message` and whose source
|
|
92
|
+
* is a human `user`, so injected plugin context and tool results never leak
|
|
93
|
+
* into the query). Returns "" when nothing qualifies — the ranking then
|
|
94
|
+
* falls back to pure recency.
|
|
95
|
+
*/
|
|
96
|
+
export declare function recentUserText(agent: AgentLike | undefined, opts?: {
|
|
97
|
+
maxMessages?: number;
|
|
98
|
+
maxChars?: number;
|
|
99
|
+
}): string;
|
|
100
|
+
/** True when the state carries at least one entry of any kind. */
|
|
101
|
+
export declare function hasAnyEntries(state: HarnessState): boolean;
|
|
102
|
+
/** The additive prompt-notes block (empty when there are no visible prompt entries). */
|
|
103
|
+
export declare function formatPromptEntriesSection(entries: readonly HarnessEntry[], query?: string): string;
|
|
104
|
+
/** The reusable delegation-specs block (empty when there are no visible subagent entries). */
|
|
105
|
+
export declare function formatSubagentSpecsSection(entries: readonly HarnessEntry[], query?: string): string;
|
|
106
|
+
/**
|
|
107
|
+
* Walk the parent-session chain from `agent` upward and return the nearest
|
|
108
|
+
* session whose local store is non-empty, if any. Children inherit their
|
|
109
|
+
* ancestor's prompt notes and delegation specs; the chain walk stops at the
|
|
110
|
+
* first store that has entries (deep descendants do not re-inject ancestors
|
|
111
|
+
* beyond the nearest carrying store).
|
|
112
|
+
*/
|
|
113
|
+
export declare function nearestLocalStateWithEntries(engine: EvolutionEngine, agent: AgentLike): HarnessState | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* Compose the full injected block for one assembling agent: global entries
|
|
116
|
+
* merged with the nearest carrying local store (local wins on id collision).
|
|
117
|
+
* The optional `query` — when absent, derived from the agent's most recent
|
|
118
|
+
* direct user messages — ranks which entries fill the per-kind cap
|
|
119
|
+
* (relevance first, then recency; see {@link rankEntries}). Returns "" when
|
|
120
|
+
* nothing is injectable — the prompt renderer then drops the section, so an
|
|
121
|
+
* empty store adds zero tokens to every assembly.
|
|
122
|
+
*/
|
|
123
|
+
export declare function entriesSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, query?: string): string;
|
|
124
|
+
//# sourceMappingURL=inject.d.ts.map
|