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/mount.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hot mounting: materialize a skill-kind harness entry as a real cordis
|
|
3
|
+
* plugin (v2 optional item, design.md §6) and load it into the live loader
|
|
4
|
+
* tree — no process restart. The generated plugin registers a model-facing
|
|
5
|
+
* tool that carries the skill's description, argument contract, and python
|
|
6
|
+
* reference; the `skill` tool remains the way to actually execute it.
|
|
7
|
+
*
|
|
8
|
+
* Layout under `<baseDir>/evolve/mounted/`:
|
|
9
|
+
* index.json mount ledger (id -> path, restored at boot)
|
|
10
|
+
* <skillName>/package.json plugin manifest (ESM, main index.js)
|
|
11
|
+
* <skillName>/index.js generated plugin: registers one tool
|
|
12
|
+
*
|
|
13
|
+
* The generated plugin imports nothing outside node builtins, so the mount
|
|
14
|
+
* directory needs no node_modules of its own.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { skillNameOf } from "./skill.js";
|
|
19
|
+
export function mountedDir(baseDir) {
|
|
20
|
+
return join(baseDir, "evolve", "mounted");
|
|
21
|
+
}
|
|
22
|
+
export function ledgerPath(baseDir) {
|
|
23
|
+
return join(mountedDir(baseDir), "index.json");
|
|
24
|
+
}
|
|
25
|
+
export function loadLedger(baseDir) {
|
|
26
|
+
const path = ledgerPath(baseDir);
|
|
27
|
+
if (!existsSync(path)) {
|
|
28
|
+
return { mounted: [] };
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
32
|
+
return { mounted: Array.isArray(raw.mounted) ? raw.mounted : [] };
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { mounted: [] };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function saveLedger(baseDir, ledger) {
|
|
39
|
+
mkdirSync(mountedDir(baseDir), { recursive: true });
|
|
40
|
+
writeFileSync(ledgerPath(baseDir), `${JSON.stringify(ledger, null, 2)}\n`, "utf8");
|
|
41
|
+
}
|
|
42
|
+
/** Generate the plugin package files for one skill entry; returns the package dir. */
|
|
43
|
+
export function renderMountPackage(baseDir, entry) {
|
|
44
|
+
const dir = join(mountedDir(baseDir), skillNameOf(entry.id));
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
const toolName = `skill_${skillNameOf(entry.id)}`;
|
|
47
|
+
writeFileSync(join(dir, "package.json"), `{\n "name": "evolve-skill-${skillNameOf(entry.id)}",\n "version": "1.0.0",\n "main": "index.js",\n "type": "module"\n}\n`, "utf8");
|
|
48
|
+
writeFileSync(join(dir, "index.js"), renderPluginSource(toolName, entry), "utf8");
|
|
49
|
+
return dir;
|
|
50
|
+
}
|
|
51
|
+
/** The generated plugin source: one tool registration, no external imports. */
|
|
52
|
+
export function renderPluginSource(toolName, entry) {
|
|
53
|
+
const description = `${oneLine(entry.title)}. ${oneLine(entry.content)}`.slice(0, 400);
|
|
54
|
+
const parameters = renderParameters(entry);
|
|
55
|
+
const reference = JSON.stringify(entry.reference ?? {}, null, 2);
|
|
56
|
+
const entryId = JSON.stringify(entry.id);
|
|
57
|
+
const entryVersion = JSON.stringify(entry.version);
|
|
58
|
+
const entryTitle = JSON.stringify(oneLine(entry.title));
|
|
59
|
+
const skillName = JSON.stringify(skillNameOf(entry.id));
|
|
60
|
+
return `/**
|
|
61
|
+
* Generated by dsh-continual-evolve mount (${entry.id} v${entry.version}).
|
|
62
|
+
* Hot-mounted into the live loader tree; edit the harness entry and remount
|
|
63
|
+
* to regenerate. Execute the skill through the \`skill\` tool — this plugin
|
|
64
|
+
* exposes its contract to the model.
|
|
65
|
+
*/
|
|
66
|
+
export const name = "evolve-skill-${skillNameOf(entry.id)}";
|
|
67
|
+
|
|
68
|
+
// Cordis requires declared service access; without this, \`ctx.tools\` throws
|
|
69
|
+
// "cannot get property \\"tools\\" without inject".
|
|
70
|
+
export const inject = ["tools"];
|
|
71
|
+
|
|
72
|
+
export function apply(ctx) {
|
|
73
|
+
ctx.tools.register({
|
|
74
|
+
name: ${JSON.stringify(toolName)},
|
|
75
|
+
description: ${JSON.stringify(description)},
|
|
76
|
+
parameters: ${JSON.stringify(parameters, null, 2)},
|
|
77
|
+
output: {
|
|
78
|
+
// No \`required\` on the string property: the direct-register value-schema
|
|
79
|
+
// path rejects it ("required is not supported on type string").
|
|
80
|
+
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string" } } },
|
|
81
|
+
render: (_args, value) => [{ type: "text", text: value?.text ?? "" }],
|
|
82
|
+
},
|
|
83
|
+
execute: async (args) => ({
|
|
84
|
+
text: "Mounted skill " + ${entryId} + " (v" + ${entryVersion} + ").\\n" +
|
|
85
|
+
"Description: " + ${entryTitle} + "\\n" +
|
|
86
|
+
"Argument contract: " + JSON.stringify(args ?? {}) + "\\n" +
|
|
87
|
+
"Python reference: " + ${JSON.stringify(reference)} + "\\n" +
|
|
88
|
+
"Execute via the \`skill\` tool (skill name " + ${skillName} + ").",
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Map a skill entry's arguments contract to the tool-parameter schema.
|
|
96
|
+
* Requiredness goes in a root-level `required` array (valid JSON Schema):
|
|
97
|
+
* the mounted plugin registers via raw `ctx.tools.register`, and dsh-llm
|
|
98
|
+
* sends `parameters` verbatim to the API, which rejects per-property
|
|
99
|
+
* `required: true` ("true is not of type array").
|
|
100
|
+
*/
|
|
101
|
+
export function renderParameters(entry) {
|
|
102
|
+
const contract = entry.arguments ?? {};
|
|
103
|
+
const properties = {};
|
|
104
|
+
const required = [];
|
|
105
|
+
for (const [key, spec] of Object.entries(contract)) {
|
|
106
|
+
const record = typeof spec === "object" && spec !== null && !Array.isArray(spec) ? spec : {};
|
|
107
|
+
const type = typeof record["type"] === "string" ? record["type"] : "string";
|
|
108
|
+
const description = typeof record["description"] === "string" ? record["description"] : key;
|
|
109
|
+
properties[key] = { type, description };
|
|
110
|
+
if (record["required"] === true) {
|
|
111
|
+
required.push(key);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
type: "object",
|
|
116
|
+
additionalProperties: false,
|
|
117
|
+
properties,
|
|
118
|
+
...(required.length > 0 ? { required } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Mount one skill entry into the live loader. Requires the `loader` service
|
|
123
|
+
* (resolved lazily, see FAQ #1); without it the package is still written and
|
|
124
|
+
* the ledger records the entry for the next boot.
|
|
125
|
+
*/
|
|
126
|
+
export async function mountSkill(ctx, baseDir, entry) {
|
|
127
|
+
const dir = renderMountPackage(baseDir, entry);
|
|
128
|
+
const entryId = `evolve-mount-${skillNameOf(entry.id)}`;
|
|
129
|
+
const loader = ctx.get("loader");
|
|
130
|
+
if (loader) {
|
|
131
|
+
try {
|
|
132
|
+
// EntryOptions: {id, name (module specifier), config, group, disabled, inject} —
|
|
133
|
+
// `name` must resolve as an ES module: Node ESM does not support
|
|
134
|
+
// directory imports, so point at the generated index.js explicitly.
|
|
135
|
+
await loader.create({ id: entryId, name: join(dir, "index.js") });
|
|
136
|
+
}
|
|
137
|
+
catch (cause) {
|
|
138
|
+
throw new Error(`hot mount failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const record = {
|
|
142
|
+
id: entry.id,
|
|
143
|
+
entryId,
|
|
144
|
+
path: dir,
|
|
145
|
+
version: entry.version,
|
|
146
|
+
mountedAt: new Date().toISOString(),
|
|
147
|
+
};
|
|
148
|
+
const ledger = loadLedger(baseDir);
|
|
149
|
+
ledger.mounted = [...ledger.mounted.filter((m) => m.id !== entry.id), record];
|
|
150
|
+
saveLedger(baseDir, ledger);
|
|
151
|
+
return record;
|
|
152
|
+
}
|
|
153
|
+
/** Unmount a skill entry: remove the loader entry and the generated package. */
|
|
154
|
+
export async function unmountSkill(ctx, baseDir, id) {
|
|
155
|
+
const ledger = loadLedger(baseDir);
|
|
156
|
+
const record = ledger.mounted.find((m) => m.id === id);
|
|
157
|
+
if (!record) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
const loader = ctx.get("loader");
|
|
161
|
+
if (loader) {
|
|
162
|
+
try {
|
|
163
|
+
await loader.remove(record.entryId);
|
|
164
|
+
}
|
|
165
|
+
catch (cause) {
|
|
166
|
+
throw new Error(`hot unmount failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (existsSync(record.path)) {
|
|
170
|
+
rmSync(record.path, { recursive: true, force: true });
|
|
171
|
+
}
|
|
172
|
+
ledger.mounted = ledger.mounted.filter((m) => m.id !== id);
|
|
173
|
+
saveLedger(baseDir, ledger);
|
|
174
|
+
return record;
|
|
175
|
+
}
|
|
176
|
+
/** Re-mount every ledger entry at plugin boot (restart persistence). */
|
|
177
|
+
export async function restoreMounted(ctx, baseDir) {
|
|
178
|
+
const ledger = loadLedger(baseDir);
|
|
179
|
+
const loader = ctx.get("loader");
|
|
180
|
+
for (const record of ledger.mounted) {
|
|
181
|
+
if (!existsSync(join(record.path, "index.js"))) {
|
|
182
|
+
continue; // package was removed; ledger prunes on next unmount
|
|
183
|
+
}
|
|
184
|
+
if (!loader) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
await loader.create({ id: record.entryId, name: join(record.path, "index.js") });
|
|
189
|
+
}
|
|
190
|
+
catch (cause) {
|
|
191
|
+
ctx.logger("continual-evolve").warn(`mount restore failed for ${record.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function oneLine(text) {
|
|
196
|
+
return text.replace(/\s+/g, " ").trim();
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=mount.js.map
|
package/lib/notify.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-review visibility: the gate used to work fully in the background —
|
|
3
|
+
* the user never saw a decision, a persisted entry, or the token spend. This
|
|
4
|
+
* module queues a short follow-up turn after an approved gate run so the
|
|
5
|
+
* user SEES what was persisted, how to inspect it, and how to roll it back.
|
|
6
|
+
*
|
|
7
|
+
* The notice is a plugin-sourced user message (`agent.followup`), so it is
|
|
8
|
+
* rendered in the session transcript like any other input and the agent
|
|
9
|
+
* answers with a one-line confirmation. It never fakes tool or assistant
|
|
10
|
+
* events, so session replay, the ordered surface, and derived history stay
|
|
11
|
+
* untouched: the notice is a plain `user/message` with a plugin source.
|
|
12
|
+
*
|
|
13
|
+
* Every mechanical property stays in code: the notice text is built from the
|
|
14
|
+
* applied refinement result, never from model text.
|
|
15
|
+
*/
|
|
16
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
17
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
18
|
+
import type { RefinementResult } from "./types.js";
|
|
19
|
+
/**
|
|
20
|
+
* Compose the user-visible gate notice from an applied refinement result.
|
|
21
|
+
* Lists every successfully applied edit (kind + title + id) and the rollback
|
|
22
|
+
* command; failed edits are summarized in one line so nothing is hidden.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildGateNotice(result: RefinementResult, turnsSinceLastReview: number): string;
|
|
25
|
+
/**
|
|
26
|
+
* Queue the follow-up notice turn for the agent. Failure is contained: a
|
|
27
|
+
* broken notification must never break the gate path that already recorded
|
|
28
|
+
* the decision in reviews.jsonl.
|
|
29
|
+
*/
|
|
30
|
+
export declare function notifyAutoReview(ctx: Context, agent: Agent, result: RefinementResult, turnsSinceLastReview: number): void;
|
|
31
|
+
//# sourceMappingURL=notify.d.ts.map
|
package/lib/notify.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
/**
|
|
3
|
+
* Compose the user-visible gate notice from an applied refinement result.
|
|
4
|
+
* Lists every successfully applied edit (kind + title + id) and the rollback
|
|
5
|
+
* command; failed edits are summarized in one line so nothing is hidden.
|
|
6
|
+
*/
|
|
7
|
+
export function buildGateNotice(result, turnsSinceLastReview) {
|
|
8
|
+
const applied = result.appliedEdits.filter((edit) => edit.applied);
|
|
9
|
+
const failed = result.appliedEdits.filter((edit) => !edit.applied);
|
|
10
|
+
const kindLabel = { prompt: "提示词", memory: "记忆", skill: "技能", subagent: "子代理" };
|
|
11
|
+
const lines = applied.map((edit) => `- ${kindLabel[edit.kind] ?? edit.kind}「${edit.title ?? edit.id}」(${edit.id})`);
|
|
12
|
+
const linesText = lines.length > 0 ? lines.join("\n") : "(无条目成功应用)";
|
|
13
|
+
const failedText = failed.length > 0 ? `\n另有 ${failed.length} 条编辑未应用。` : "";
|
|
14
|
+
return [
|
|
15
|
+
`🔎 自动进化门禁:会话第 ${turnsSinceLastReview} 回合检查完成,本次沉淀 ${applied.length} 条条目:`,
|
|
16
|
+
linesText,
|
|
17
|
+
failedText,
|
|
18
|
+
`查看全部条目:/evolve list;回滚本次沉淀:/evolve rollback ${result.id}`,
|
|
19
|
+
"请用一句话简短确认即可,不要调用任何工具。",
|
|
20
|
+
]
|
|
21
|
+
.filter((part) => part !== "")
|
|
22
|
+
.join("\n");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Queue the follow-up notice turn for the agent. Failure is contained: a
|
|
26
|
+
* broken notification must never break the gate path that already recorded
|
|
27
|
+
* the decision in reviews.jsonl.
|
|
28
|
+
*/
|
|
29
|
+
export function notifyAutoReview(ctx, agent, result, turnsSinceLastReview) {
|
|
30
|
+
try {
|
|
31
|
+
agent.followup(createUserMessage({
|
|
32
|
+
content: [{ type: "text", text: buildGateNotice(result, turnsSinceLastReview) }],
|
|
33
|
+
source: { kind: "plugin", plugin: "dsh-continual-evolve" },
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
catch (cause) {
|
|
37
|
+
ctx
|
|
38
|
+
.logger("continual-evolve")
|
|
39
|
+
.warn(`auto-review notice failed for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=notify.js.map
|
package/lib/plan.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing helpers for the model-produced proposal JSON. A planner reply is
|
|
3
|
+
* untrusted text: it may carry prose, a fenced block, or be truncated by an
|
|
4
|
+
* exhausted output budget. These helpers recover the JSON object when
|
|
5
|
+
* possible and name the cause (truncation vs malformed) when not.
|
|
6
|
+
*/
|
|
7
|
+
import type { RefinementProposal } from "./types.js";
|
|
8
|
+
/** True when the text ends mid-string or with unclosed brackets. */
|
|
9
|
+
export declare function isIncompleteJson(candidate: string): boolean;
|
|
10
|
+
/** Parse a JSON candidate, distinguishing truncation from malformation. */
|
|
11
|
+
export declare function parseJsonCandidate(candidate: string): unknown;
|
|
12
|
+
/** Recover a JSON object from raw model text. */
|
|
13
|
+
export declare function extractJsonObject(text: string): unknown;
|
|
14
|
+
/** Parse and shape a proposal, dropping non-object edits. */
|
|
15
|
+
export declare function parseProposal(text: string): RefinementProposal;
|
|
16
|
+
//# sourceMappingURL=plan.d.ts.map
|
package/lib/plan.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** True when the text ends mid-string or with unclosed brackets. */
|
|
2
|
+
export function isIncompleteJson(candidate) {
|
|
3
|
+
let depth = 0;
|
|
4
|
+
let inString = false;
|
|
5
|
+
let escaped = false;
|
|
6
|
+
for (const char of candidate) {
|
|
7
|
+
if (escaped) {
|
|
8
|
+
escaped = false;
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (inString) {
|
|
12
|
+
if (char === "\\")
|
|
13
|
+
escaped = true;
|
|
14
|
+
else if (char === '"')
|
|
15
|
+
inString = false;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (char === '"')
|
|
19
|
+
inString = true;
|
|
20
|
+
else if (char === "{" || char === "[")
|
|
21
|
+
depth++;
|
|
22
|
+
else if (char === "}" || char === "]")
|
|
23
|
+
depth--;
|
|
24
|
+
}
|
|
25
|
+
return inString || depth > 0;
|
|
26
|
+
}
|
|
27
|
+
/** Parse a JSON candidate, distinguishing truncation from malformation. */
|
|
28
|
+
export function parseJsonCandidate(candidate) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(candidate);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (isIncompleteJson(candidate)) {
|
|
34
|
+
throw new Error("the model stopped before completing its JSON object (output budget exhausted?)");
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`the model did not return valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Recover a JSON object from raw model text. */
|
|
40
|
+
export function extractJsonObject(text) {
|
|
41
|
+
const trimmed = text.trim();
|
|
42
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
43
|
+
return parseJsonCandidate(trimmed);
|
|
44
|
+
}
|
|
45
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
46
|
+
if (fenced) {
|
|
47
|
+
return parseJsonCandidate(fenced[1]?.trim() ?? "");
|
|
48
|
+
}
|
|
49
|
+
const start = trimmed.indexOf("{");
|
|
50
|
+
const end = trimmed.lastIndexOf("}");
|
|
51
|
+
if (start !== -1 && end > start) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(trimmed.slice(start, end + 1));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return parseJsonCandidate(trimmed.slice(start));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (isIncompleteJson(trimmed)) {
|
|
60
|
+
throw new Error("the model stopped before completing its JSON object (output budget exhausted?)");
|
|
61
|
+
}
|
|
62
|
+
throw new Error("the planner did not return a JSON object");
|
|
63
|
+
}
|
|
64
|
+
function asString(value) {
|
|
65
|
+
return typeof value === "string" ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
function asRecord(value) {
|
|
68
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
/** Assign a string field only when it is present, honoring exactOptionalPropertyTypes. */
|
|
74
|
+
function assignIfString(target, key, value) {
|
|
75
|
+
const str = asString(value);
|
|
76
|
+
if (str !== undefined) {
|
|
77
|
+
target[key] = str;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Parse and shape a proposal, dropping non-object edits. */
|
|
81
|
+
export function parseProposal(text) {
|
|
82
|
+
const value = extractJsonObject(text);
|
|
83
|
+
const record = asRecord(value);
|
|
84
|
+
if (!record) {
|
|
85
|
+
throw new Error("the planner JSON must be an object");
|
|
86
|
+
}
|
|
87
|
+
const edits = [];
|
|
88
|
+
if (Array.isArray(record["edits"])) {
|
|
89
|
+
for (const raw of record["edits"]) {
|
|
90
|
+
const edit = asRecord(raw);
|
|
91
|
+
if (!edit)
|
|
92
|
+
continue;
|
|
93
|
+
const built = {
|
|
94
|
+
action: asString(edit["action"]),
|
|
95
|
+
kind: asString(edit["kind"]),
|
|
96
|
+
};
|
|
97
|
+
assignIfString(built, "id", edit["id"]);
|
|
98
|
+
assignIfString(built, "title", edit["title"]);
|
|
99
|
+
assignIfString(built, "content", edit["content"]);
|
|
100
|
+
assignIfString(built, "path", edit["path"]);
|
|
101
|
+
assignIfString(built, "reason", edit["reason"]);
|
|
102
|
+
const reference = asRecord(edit["reference"]);
|
|
103
|
+
if (reference)
|
|
104
|
+
built.reference = reference;
|
|
105
|
+
const argumentsRecord = asRecord(edit["arguments"]);
|
|
106
|
+
if (argumentsRecord)
|
|
107
|
+
built.arguments = argumentsRecord;
|
|
108
|
+
const metadata = asRecord(edit["metadata"]);
|
|
109
|
+
if (metadata)
|
|
110
|
+
built.metadata = metadata;
|
|
111
|
+
edits.push(built);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
summary: asString(record["summary"]) ?? "Refined harness state",
|
|
116
|
+
rationale: asString(record["rationale"]) ?? "",
|
|
117
|
+
expectedOutcome: asString(record["expectedOutcome"]) ?? "",
|
|
118
|
+
edits,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=plan.js.map
|
package/lib/planner.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The LLM planning pass. Given the current harness state, refinement history,
|
|
3
|
+
* and optional instructions, a direct model call produces a JSON proposal
|
|
4
|
+
* which is parsed (truncation-aware) and validated by the pure core.
|
|
5
|
+
*
|
|
6
|
+
* The call routes through `ctx.llm` with the calling agent's own
|
|
7
|
+
* provider/model so the plan uses the same model the session runs on.
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
10
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
11
|
+
import type { HarnessState, RefinementProposal, RefinementResult } from "./types.js";
|
|
12
|
+
export declare const PLANNER_SYSTEM_PROMPT = "You are the /evolve continual harness subsystem.\n\nYour job is to improve the editable continual harness state. Instead of\nsummarizing the conversation you emit precise Create, Update, or Delete edits\nto reusable state: prompt notes, memories, skills, and subagent specs.\n\nRules:\n- The base system prompt is immutable and MUST NOT be rewritten (never edit id \"base_system_prompt\").\n- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.\n- prompt = narrow behavioral policy addendums; memory = durable facts/preferences/failures;\n skill = repeatable procedures (must carry a python reference {type:\"python\", import, callable}\n and an arguments object); subagent = reusable delegation roles.\n- Local edits are session-scoped; global edits persist across sessions.\n- Ground every edit in evidence: the session trajectory (recent direct user\n messages) is provided when available; prefer edits backed by it over\n speculation, and never invent preferences the user did not express.\n- Stale entries (superseded by newer ones, never referenced in recent\n trajectories, obsolete facts): propose action \"archive\" instead of\n \"delete\" \u2014 archive hides the entry from injection while keeping its data\n restorable; it requires only kind + id.\n- Output JSON only, exactly this shape:\n{\n \"summary\": \"one sentence\",\n \"rationale\": \"why these edits are justified by the evidence\",\n \"expectedOutcome\": \"what should improve and how to validate it\",\n \"edits\": [\n {\n \"action\": \"create|update|delete\",\n \"kind\": \"prompt|memory|skill|subagent\",\n \"id\": \"stable id for update/delete, optional for create\",\n \"title\": \"required for create/update except delete\",\n \"content\": \"required for create/update except delete\",\n \"path\": \"optional grouping path\",\n \"reference\": {\"type\":\"python\",\"import\":\"pkg.mod\",\"callable\":\"fn\"} ,\n \"arguments\": {\"name\": {\"type\":\"string\",\"required\":true,\"description\":\"...\"}},\n \"metadata\": {},\n \"reason\": \"why this edit is useful\"\n }\n ]\n}";
|
|
13
|
+
export interface PlanOptions {
|
|
14
|
+
agent: Agent;
|
|
15
|
+
state: HarnessState;
|
|
16
|
+
history: readonly RefinementResult[];
|
|
17
|
+
instructions?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Explicit session-trajectory text (recent direct user messages). When
|
|
20
|
+
* omitted, it is extracted from the agent's own session log via
|
|
21
|
+
* `recentUserText` — the same extraction the injection ranking uses — so
|
|
22
|
+
* every planning call is grounded in what the user actually said.
|
|
23
|
+
*/
|
|
24
|
+
trajectory?: string;
|
|
25
|
+
global?: boolean;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
maxOutputTokens?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function planWithLlm(ctx: Context, options: PlanOptions): Promise<RefinementProposal>;
|
|
30
|
+
//# sourceMappingURL=planner.d.ts.map
|
package/lib/planner.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { parseProposal } from "./plan.js";
|
|
3
|
+
import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
|
|
4
|
+
import { recentUserText } from "./inject.js";
|
|
5
|
+
export const PLANNER_SYSTEM_PROMPT = `You are the /evolve continual harness subsystem.
|
|
6
|
+
|
|
7
|
+
Your job is to improve the editable continual harness state. Instead of
|
|
8
|
+
summarizing the conversation you emit precise Create, Update, or Delete edits
|
|
9
|
+
to reusable state: prompt notes, memories, skills, and subagent specs.
|
|
10
|
+
|
|
11
|
+
Rules:
|
|
12
|
+
- The base system prompt is immutable and MUST NOT be rewritten (never edit id "base_system_prompt").
|
|
13
|
+
- Prefer small evidence-backed edits. If no useful edit is justified, return an empty edits array.
|
|
14
|
+
- prompt = narrow behavioral policy addendums; memory = durable facts/preferences/failures;
|
|
15
|
+
skill = repeatable procedures (must carry a python reference {type:"python", import, callable}
|
|
16
|
+
and an arguments object); subagent = reusable delegation roles.
|
|
17
|
+
- Local edits are session-scoped; global edits persist across sessions.
|
|
18
|
+
- Ground every edit in evidence: the session trajectory (recent direct user
|
|
19
|
+
messages) is provided when available; prefer edits backed by it over
|
|
20
|
+
speculation, and never invent preferences the user did not express.
|
|
21
|
+
- Stale entries (superseded by newer ones, never referenced in recent
|
|
22
|
+
trajectories, obsolete facts): propose action "archive" instead of
|
|
23
|
+
"delete" — archive hides the entry from injection while keeping its data
|
|
24
|
+
restorable; it requires only kind + id.
|
|
25
|
+
- Output JSON only, exactly this shape:
|
|
26
|
+
{
|
|
27
|
+
"summary": "one sentence",
|
|
28
|
+
"rationale": "why these edits are justified by the evidence",
|
|
29
|
+
"expectedOutcome": "what should improve and how to validate it",
|
|
30
|
+
"edits": [
|
|
31
|
+
{
|
|
32
|
+
"action": "create|update|delete",
|
|
33
|
+
"kind": "prompt|memory|skill|subagent",
|
|
34
|
+
"id": "stable id for update/delete, optional for create",
|
|
35
|
+
"title": "required for create/update except delete",
|
|
36
|
+
"content": "required for create/update except delete",
|
|
37
|
+
"path": "optional grouping path",
|
|
38
|
+
"reference": {"type":"python","import":"pkg.mod","callable":"fn"} ,
|
|
39
|
+
"arguments": {"name": {"type":"string","required":true,"description":"..."}},
|
|
40
|
+
"metadata": {},
|
|
41
|
+
"reason": "why this edit is useful"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}`;
|
|
45
|
+
export async function planWithLlm(ctx, options) {
|
|
46
|
+
const { agent, state, history } = options;
|
|
47
|
+
if (!agent.options.provider || !agent.options.model) {
|
|
48
|
+
throw new Error("evolve: the calling agent has no provider/model route to plan with");
|
|
49
|
+
}
|
|
50
|
+
const scopeInstruction = options.global
|
|
51
|
+
? "Requested scope: global. Only propose stable cross-session lessons, durable preferences, reusable skills/subagents, or explicitly project-qualified facts."
|
|
52
|
+
: "Requested scope: local. Prefer session-scoped edits for current task progress; global entries are read-only context — do not propose update/delete for them.";
|
|
53
|
+
// Ground the plan in the caller's session: the trajectory block is the
|
|
54
|
+
// most recent direct user messages ("" when none qualify — the block is
|
|
55
|
+
// then omitted entirely, keeping an empty trajectory zero-cost).
|
|
56
|
+
const trajectory = options.trajectory ?? recentUserText(agent);
|
|
57
|
+
const userPrompt = [
|
|
58
|
+
`<current_harness_state>\n${formatHarnessStateForPrompt(state)}\n</current_harness_state>`,
|
|
59
|
+
`<refinement_history>\n${historyForPrompt(history)}\n</refinement_history>`,
|
|
60
|
+
`<scope_policy>\n${scopeInstruction}\n</scope_policy>`,
|
|
61
|
+
trajectory ? `<session_trajectory>\n${trajectory}\n</session_trajectory>` : "",
|
|
62
|
+
options.instructions ? `<user_instructions>\n${options.instructions}\n</user_instructions>` : "",
|
|
63
|
+
"Return only JSON edits. If no useful edit is justified, return an empty edits array with a rationale.",
|
|
64
|
+
]
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join("\n\n");
|
|
67
|
+
const assembler = new BlockAssembler();
|
|
68
|
+
for await (const chunk of ctx.llm.stream({
|
|
69
|
+
provider: agent.options.provider,
|
|
70
|
+
model: agent.options.model,
|
|
71
|
+
system: PLANNER_SYSTEM_PROMPT,
|
|
72
|
+
messages: [
|
|
73
|
+
createUserMessage({
|
|
74
|
+
content: [{ type: "text", text: userPrompt }],
|
|
75
|
+
source: { kind: "plugin", plugin: "dsh-continual-evolve" },
|
|
76
|
+
}),
|
|
77
|
+
],
|
|
78
|
+
// Force non-reasoning output: the proposal must be pure JSON text.
|
|
79
|
+
reasoningEffort: ReasoningEffortId("off"),
|
|
80
|
+
maxTokens: options.maxOutputTokens ?? 8000,
|
|
81
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
82
|
+
})) {
|
|
83
|
+
assembler.push(chunk);
|
|
84
|
+
}
|
|
85
|
+
throwOnFinishError(assembler.finish);
|
|
86
|
+
const blocks = assembler.blocks();
|
|
87
|
+
const text = blocks
|
|
88
|
+
.filter((block) => block.type === "text")
|
|
89
|
+
.map((block) => block.text)
|
|
90
|
+
.join("\n");
|
|
91
|
+
if (text.length === 0) {
|
|
92
|
+
throw new Error("evolve: planner produced no text output");
|
|
93
|
+
}
|
|
94
|
+
return parseProposal(text);
|
|
95
|
+
}
|
|
96
|
+
/** Surface terminal stream states as errors so the caller never sees a silent partial plan. */
|
|
97
|
+
function throwOnFinishError(finish) {
|
|
98
|
+
switch (finish.kind) {
|
|
99
|
+
case "stop":
|
|
100
|
+
case "tool-calls":
|
|
101
|
+
return;
|
|
102
|
+
case "max-tokens":
|
|
103
|
+
throw new Error("evolve: planner output budget exhausted (max-tokens)");
|
|
104
|
+
case "aborted":
|
|
105
|
+
throw new Error("evolve: planner call aborted");
|
|
106
|
+
case "error":
|
|
107
|
+
throw new Error(`evolve: planner call failed: ${finish.failure?.message ?? "unknown error"}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=planner.js.map
|
package/lib/pool.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded-concurrency map: run async workers over items with at most
|
|
3
|
+
* `concurrency` in flight. Used by the evaluation matrix so case × run units
|
|
4
|
+
* execute in parallel without unbounded subagent fan-out.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mapPool<T, R>(items: readonly T[], concurrency: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
7
|
+
//# sourceMappingURL=pool.d.ts.map
|
package/lib/pool.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded-concurrency map: run async workers over items with at most
|
|
3
|
+
* `concurrency` in flight. Used by the evaluation matrix so case × run units
|
|
4
|
+
* execute in parallel without unbounded subagent fan-out.
|
|
5
|
+
*/
|
|
6
|
+
export async function mapPool(items, concurrency, worker) {
|
|
7
|
+
if (items.length === 0) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
const results = new Array(items.length);
|
|
11
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
12
|
+
let next = 0;
|
|
13
|
+
async function runner() {
|
|
14
|
+
while (true) {
|
|
15
|
+
const index = next;
|
|
16
|
+
next += 1;
|
|
17
|
+
if (index >= items.length)
|
|
18
|
+
return;
|
|
19
|
+
results[index] = await worker(items[index], index);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
await Promise.all(Array.from({ length: limit }, () => runner()));
|
|
23
|
+
return results;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=pool.js.map
|
package/lib/render.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt rendering: bounded, model-visible views of harness state and
|
|
3
|
+
* refinement history. Every cap exists to keep token cost predictable no
|
|
4
|
+
* matter how large the store grows.
|
|
5
|
+
*/
|
|
6
|
+
import type { HarnessEntry, HarnessRefinementEvent, HarnessState, RefinementResult } from "./types.js";
|
|
7
|
+
export declare function compactText(text: string, maxLength: number): string;
|
|
8
|
+
export declare function entryLine(entry: HarnessEntry, maxContentLength: number): string;
|
|
9
|
+
/** Render the full merged state as a bounded overview for the system prompt. */
|
|
10
|
+
export declare function formatHarnessStateForPrompt(state: HarnessState): string;
|
|
11
|
+
/** Render recent refinement results for the planner. */
|
|
12
|
+
export declare function historyForPrompt(history: readonly RefinementResult[]): string;
|
|
13
|
+
/** Serialize a refinement event for persistence (lightweight). */
|
|
14
|
+
export declare function eventToLine(event: HarnessRefinementEvent): string;
|
|
15
|
+
//# sourceMappingURL=render.d.ts.map
|