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/skill.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HarnessEntry, RefinementResult } from "./types.js";
|
|
2
|
+
/** Convert a harness entry id (underscore slug) to a kebab-case skill name. */
|
|
3
|
+
export declare function skillNameOf(id: string): string;
|
|
4
|
+
/** Resolve and defend the skill directory for an entry id. */
|
|
5
|
+
export declare function skillDir(skillsRoot: string, id: string): string;
|
|
6
|
+
/** Render a harness skill entry as a discoverable SKILL.md document. */
|
|
7
|
+
export declare function renderSkillMarkdown(entry: HarnessEntry): string;
|
|
8
|
+
/** Apply the skill-kind edits of an applied refinement to the skills root. */
|
|
9
|
+
export declare function syncSkillsFromResult(skillsRoot: string, result: RefinementResult): void;
|
|
10
|
+
//# sourceMappingURL=skill.d.ts.map
|
package/lib/skill.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill materializer: syncs skill-kind harness entries to the DSH skills
|
|
3
|
+
* filesystem (`$DSH_HOME/skills/<kebab-name>/SKILL.md`) so the `skill` tool
|
|
4
|
+
* and catalog can discover and load them. Writes are atomic (tmp + rename)
|
|
5
|
+
* so the filesystem watcher never sees a partial file.
|
|
6
|
+
*
|
|
7
|
+
* Skill names must be kebab-case (the harness store ids are underscore slugs;
|
|
8
|
+
* the materialized name converts `_` → `-`).
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join, resolve, sep } from "node:path";
|
|
12
|
+
/** Convert a harness entry id (underscore slug) to a kebab-case skill name. */
|
|
13
|
+
export function skillNameOf(id) {
|
|
14
|
+
return id.toLowerCase().replace(/_/g, "-");
|
|
15
|
+
}
|
|
16
|
+
/** Resolve and defend the skill directory for an entry id. */
|
|
17
|
+
export function skillDir(skillsRoot, id) {
|
|
18
|
+
const root = resolve(skillsRoot);
|
|
19
|
+
const dir = resolve(join(root, skillNameOf(id)));
|
|
20
|
+
if (dir !== root && !dir.startsWith(`${root}/`) && !dir.startsWith(`${root}${sep}`)) {
|
|
21
|
+
throw new Error(`skill path escapes skills root: ${dir}`);
|
|
22
|
+
}
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
/** Render a harness skill entry as a discoverable SKILL.md document. */
|
|
26
|
+
export function renderSkillMarkdown(entry) {
|
|
27
|
+
const lines = [
|
|
28
|
+
"---",
|
|
29
|
+
`name: ${skillNameOf(entry.id)}`,
|
|
30
|
+
`description: ${oneLine(entry.title)}`,
|
|
31
|
+
"---",
|
|
32
|
+
"",
|
|
33
|
+
entry.content.trim(),
|
|
34
|
+
];
|
|
35
|
+
const reference = entry.reference;
|
|
36
|
+
if (reference && typeof reference === "object" && Object.keys(reference).length > 0) {
|
|
37
|
+
lines.push("", "## Invocation");
|
|
38
|
+
for (const [key, value] of Object.entries(reference)) {
|
|
39
|
+
lines.push(`- ${key}: ${JSON.stringify(value)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (Object.keys(entry.arguments).length > 0) {
|
|
43
|
+
lines.push("", "## Arguments", "```json", JSON.stringify(entry.arguments, null, 2), "```");
|
|
44
|
+
}
|
|
45
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
46
|
+
}
|
|
47
|
+
/** Apply the skill-kind edits of an applied refinement to the skills root. */
|
|
48
|
+
export function syncSkillsFromResult(skillsRoot, result) {
|
|
49
|
+
for (const edit of result.appliedEdits) {
|
|
50
|
+
if (edit.kind !== "skill" || !edit.applied)
|
|
51
|
+
continue;
|
|
52
|
+
if (edit.action === "delete" || !edit.after) {
|
|
53
|
+
removeSkill(skillsRoot, edit.id);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
writeSkill(skillsRoot, edit.after);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function writeSkill(skillsRoot, entry) {
|
|
60
|
+
const dir = skillDir(skillsRoot, entry.id);
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
const temp = join(dir, `SKILL.md.${process.pid}.tmp`);
|
|
63
|
+
writeFileSync(temp, renderSkillMarkdown(entry), "utf8");
|
|
64
|
+
renameSync(temp, join(dir, "SKILL.md"));
|
|
65
|
+
}
|
|
66
|
+
function removeSkill(skillsRoot, id) {
|
|
67
|
+
const dir = skillDir(skillsRoot, id);
|
|
68
|
+
if (existsSync(dir)) {
|
|
69
|
+
rmSync(dir, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function oneLine(text) {
|
|
73
|
+
return text.replace(/\s+/g, " ").trim();
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=skill.js.map
|
package/lib/source.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trajectory citations: where distilled entries came from in the session
|
|
3
|
+
* log. DSH sessions are event-sourced with contiguous seq numbers, so a
|
|
4
|
+
* citation of (sessionId, seqs) expands back to the exact original
|
|
5
|
+
* conversation rows (the durable log under `<dshHome>/sessions/...`).
|
|
6
|
+
*
|
|
7
|
+
* The extraction reads the live `agent.session.events` log (duck-typed, no
|
|
8
|
+
* dsh-session dependency) and only ever selects direct human messages —
|
|
9
|
+
* injected plugin context and tool results never become citations.
|
|
10
|
+
*/
|
|
11
|
+
import type { EntrySource } from "./types.js";
|
|
12
|
+
import type { AgentLike } from "./inject.js";
|
|
13
|
+
/** At most this many source user messages are cited per entry. */
|
|
14
|
+
export declare const MAX_SOURCE_MESSAGES = 3;
|
|
15
|
+
/**
|
|
16
|
+
* The seqs of the agent's most recent direct user messages, in log order.
|
|
17
|
+
* Returns [] when the agent has no readable log or no qualifying messages —
|
|
18
|
+
* callers then simply omit the citation.
|
|
19
|
+
*/
|
|
20
|
+
export declare function recentUserSeqs(agent: AgentLike | undefined, opts?: {
|
|
21
|
+
maxMessages?: number;
|
|
22
|
+
}): number[];
|
|
23
|
+
/**
|
|
24
|
+
* Build the citation for an apply call: the session id plus the seqs of the
|
|
25
|
+
* most recent direct user messages. Returns undefined when neither can be
|
|
26
|
+
* determined, so callers can pass it straight through without special cases.
|
|
27
|
+
*/
|
|
28
|
+
export declare function entrySourceOf(agent: AgentLike | undefined, sessionId: string | undefined): EntrySource | undefined;
|
|
29
|
+
//# sourceMappingURL=source.d.ts.map
|
package/lib/source.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** At most this many source user messages are cited per entry. */
|
|
2
|
+
export const MAX_SOURCE_MESSAGES = 3;
|
|
3
|
+
/**
|
|
4
|
+
* The seqs of the agent's most recent direct user messages, in log order.
|
|
5
|
+
* Returns [] when the agent has no readable log or no qualifying messages —
|
|
6
|
+
* callers then simply omit the citation.
|
|
7
|
+
*/
|
|
8
|
+
export function recentUserSeqs(agent, opts) {
|
|
9
|
+
const events = agent?.session?.events;
|
|
10
|
+
if (!events || events.length === 0) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const maxMessages = opts?.maxMessages ?? MAX_SOURCE_MESSAGES;
|
|
14
|
+
const seqs = [];
|
|
15
|
+
for (let i = events.length - 1; i >= 0 && seqs.length < maxMessages; i -= 1) {
|
|
16
|
+
const row = events[i];
|
|
17
|
+
if (row?.type !== "user/message") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const source = row.data?.source;
|
|
21
|
+
if (source && source.kind !== "user") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (typeof row.seq === "number" && Number.isInteger(row.seq)) {
|
|
25
|
+
seqs.unshift(row.seq);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return seqs;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Build the citation for an apply call: the session id plus the seqs of the
|
|
32
|
+
* most recent direct user messages. Returns undefined when neither can be
|
|
33
|
+
* determined, so callers can pass it straight through without special cases.
|
|
34
|
+
*/
|
|
35
|
+
export function entrySourceOf(agent, sessionId) {
|
|
36
|
+
if (!sessionId) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const seqs = recentUserSeqs(agent);
|
|
40
|
+
return seqs.length > 0 ? { sessionId, seqs } : { sessionId };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=source.js.map
|
package/lib/state.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HarnessScope, HarnessState } from "./types.js";
|
|
2
|
+
/** Directory holding the cross-session (global) store. */
|
|
3
|
+
export declare function globalStateDir(baseDir: string): string;
|
|
4
|
+
/** Directory holding a session-scoped (local) store, if the session has one. */
|
|
5
|
+
export declare function localStateDir(sessionDir: string | undefined): string | undefined;
|
|
6
|
+
export declare function stateFilePath(stateDir: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Load state from disk, degrading to empty on any unreadable or malformed
|
|
9
|
+
* content. Called on every system-prompt build and before every apply, so it
|
|
10
|
+
* must never throw for a bad file.
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadHarnessState(stateDir: string, scope?: HarnessScope): HarnessState;
|
|
13
|
+
/**
|
|
14
|
+
* Merge global and local state into the view the model sees. Local entries
|
|
15
|
+
* win over same-id global entries; a colliding local id is prefixed
|
|
16
|
+
* `local:` so both remain addressable.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mergeHarnessStates(globalState: HarnessState, localState: HarnessState | undefined): HarnessState;
|
|
19
|
+
/**
|
|
20
|
+
* Persist state atomically: write to a temp file, fsync-free but rename-based,
|
|
21
|
+
* preserving the mode of an existing file (defaults to 0o600 for new files).
|
|
22
|
+
*/
|
|
23
|
+
export declare function saveHarnessState(stateDir: string, state: HarnessState): string;
|
|
24
|
+
/**
|
|
25
|
+
* Capture the state snapshot a plan was based on. At apply time the caller
|
|
26
|
+
* re-reads the file and compares; an entry that changed since planning is
|
|
27
|
+
* rejected per-edit, never silently overwritten.
|
|
28
|
+
*/
|
|
29
|
+
export declare function baselineOf(state: HarnessState): HarnessState;
|
|
30
|
+
/** True when an entry in `current` differs from the same entry in `baseline`. */
|
|
31
|
+
export declare function entryChangedSince(baseline: HarnessState, current: HarnessState, kind: keyof HarnessState["entries"], id: string): boolean;
|
|
32
|
+
/** The set of keys an entry must not expose beyond the persisted shape. */
|
|
33
|
+
export declare const ENTRY_KEYS: readonly ["id", "kind", "title", "content", "path", "scope", "reference", "arguments", "metadata", "source", "created_at", "updated_at", "version"];
|
|
34
|
+
//# sourceMappingURL=state.d.ts.map
|
package/lib/state.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer for harness state: atomic writes, corrupt-file degrade,
|
|
3
|
+
* scope merge, and optimistic-concurrency primitives.
|
|
4
|
+
*
|
|
5
|
+
* Safety properties (code-enforced, not prompt-enforced):
|
|
6
|
+
* - writes are atomic (temp file + rename) and preserve the file mode;
|
|
7
|
+
* - an unreadable or non-object state file degrades to empty state so a
|
|
8
|
+
* broken file can never take a session down; the next save rewrites it
|
|
9
|
+
* cleanly;
|
|
10
|
+
* - every entry read from disk is shape-normalized (scope, reference,
|
|
11
|
+
* arguments, metadata) so a hand-edited file cannot smuggle garbage into
|
|
12
|
+
* the system prompt renderer.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, mkdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { emptyHarnessState } from "./types.js";
|
|
19
|
+
/** Directory holding the cross-session (global) store. */
|
|
20
|
+
export function globalStateDir(baseDir) {
|
|
21
|
+
return join(baseDir, "evolve");
|
|
22
|
+
}
|
|
23
|
+
/** Directory holding a session-scoped (local) store, if the session has one. */
|
|
24
|
+
export function localStateDir(sessionDir) {
|
|
25
|
+
return sessionDir ? join(sessionDir, "evolve") : undefined;
|
|
26
|
+
}
|
|
27
|
+
export function stateFilePath(stateDir) {
|
|
28
|
+
return join(stateDir, "harness_state.json");
|
|
29
|
+
}
|
|
30
|
+
function normalizeScope(value, fallback) {
|
|
31
|
+
return value === "global" || value === "local" ? value : fallback;
|
|
32
|
+
}
|
|
33
|
+
function objectRecord(value) {
|
|
34
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Load state from disk, degrading to empty on any unreadable or malformed
|
|
41
|
+
* content. Called on every system-prompt build and before every apply, so it
|
|
42
|
+
* must never throw for a bad file.
|
|
43
|
+
*/
|
|
44
|
+
export function loadHarnessState(stateDir, scope = "global") {
|
|
45
|
+
const path = stateFilePath(stateDir);
|
|
46
|
+
if (!existsSync(path)) {
|
|
47
|
+
return emptyHarnessState();
|
|
48
|
+
}
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return emptyHarnessState();
|
|
55
|
+
}
|
|
56
|
+
const root = objectRecord(parsed);
|
|
57
|
+
if (!root) {
|
|
58
|
+
return emptyHarnessState();
|
|
59
|
+
}
|
|
60
|
+
const state = emptyHarnessState();
|
|
61
|
+
if (typeof root["schema"] === "number") {
|
|
62
|
+
state.schema = root["schema"];
|
|
63
|
+
}
|
|
64
|
+
const entries = objectRecord(root["entries"]);
|
|
65
|
+
if (entries) {
|
|
66
|
+
for (const kind of Object.keys(state.entries)) {
|
|
67
|
+
const records = objectRecord(entries[kind]);
|
|
68
|
+
if (!records)
|
|
69
|
+
continue;
|
|
70
|
+
for (const [id, raw] of Object.entries(records)) {
|
|
71
|
+
const entry = objectRecord(raw);
|
|
72
|
+
if (!entry)
|
|
73
|
+
continue;
|
|
74
|
+
state.entries[kind][id] = {
|
|
75
|
+
id: typeof entry["id"] === "string" ? entry["id"] : id,
|
|
76
|
+
kind,
|
|
77
|
+
title: typeof entry["title"] === "string" ? entry["title"] : id,
|
|
78
|
+
content: typeof entry["content"] === "string" ? entry["content"] : "",
|
|
79
|
+
path: typeof entry["path"] === "string" ? entry["path"] : "general",
|
|
80
|
+
scope: normalizeScope(entry["scope"], scope),
|
|
81
|
+
reference: objectRecord(entry["reference"]) ?? {},
|
|
82
|
+
arguments: objectRecord(entry["arguments"]) ?? {},
|
|
83
|
+
metadata: objectRecord(entry["metadata"]) ?? {},
|
|
84
|
+
source: entry["source"] === "evolve" ? "evolve" : "evolve",
|
|
85
|
+
created_at: typeof entry["created_at"] === "string" ? entry["created_at"] : new Date(0).toISOString(),
|
|
86
|
+
updated_at: typeof entry["updated_at"] === "string" ? entry["updated_at"] : new Date(0).toISOString(),
|
|
87
|
+
version: typeof entry["version"] === "number" ? entry["version"] : 1,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(root["refinements"])) {
|
|
93
|
+
state.refinements = root["refinements"];
|
|
94
|
+
}
|
|
95
|
+
return state;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Merge global and local state into the view the model sees. Local entries
|
|
99
|
+
* win over same-id global entries; a colliding local id is prefixed
|
|
100
|
+
* `local:` so both remain addressable.
|
|
101
|
+
*/
|
|
102
|
+
export function mergeHarnessStates(globalState, localState) {
|
|
103
|
+
const merged = emptyHarnessState();
|
|
104
|
+
merged.schema = Math.max(globalState.schema, localState?.schema ?? 1);
|
|
105
|
+
for (const kind of Object.keys(merged.entries)) {
|
|
106
|
+
for (const [id, entry] of Object.entries(globalState.entries[kind])) {
|
|
107
|
+
merged.entries[kind][id] = { ...entry, scope: "global" };
|
|
108
|
+
}
|
|
109
|
+
for (const [id, entry] of Object.entries(localState?.entries[kind] ?? {})) {
|
|
110
|
+
const scoped = { ...entry, scope: "local" };
|
|
111
|
+
const mergedId = merged.entries[kind][id] ? `local:${id}` : id;
|
|
112
|
+
merged.entries[kind][mergedId] = scoped;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
merged.refinements = [...globalState.refinements, ...(localState?.refinements ?? [])];
|
|
116
|
+
return merged;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Persist state atomically: write to a temp file, fsync-free but rename-based,
|
|
120
|
+
* preserving the mode of an existing file (defaults to 0o600 for new files).
|
|
121
|
+
*/
|
|
122
|
+
export function saveHarnessState(stateDir, state) {
|
|
123
|
+
const path = stateFilePath(stateDir);
|
|
124
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
125
|
+
mkdirSync(stateDir, { recursive: true });
|
|
126
|
+
try {
|
|
127
|
+
const mode = existsSync(path) ? statSync(path).mode & 0o777 : 0o600;
|
|
128
|
+
writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8", mode });
|
|
129
|
+
renameSync(tempPath, path);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
if (existsSync(tempPath)) {
|
|
133
|
+
unlinkSync(tempPath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return path;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Capture the state snapshot a plan was based on. At apply time the caller
|
|
140
|
+
* re-reads the file and compares; an entry that changed since planning is
|
|
141
|
+
* rejected per-edit, never silently overwritten.
|
|
142
|
+
*/
|
|
143
|
+
export function baselineOf(state) {
|
|
144
|
+
return JSON.parse(JSON.stringify(state));
|
|
145
|
+
}
|
|
146
|
+
/** True when an entry in `current` differs from the same entry in `baseline`. */
|
|
147
|
+
export function entryChangedSince(baseline, current, kind, id) {
|
|
148
|
+
const before = baseline.entries[kind][id];
|
|
149
|
+
const after = current.entries[kind][id];
|
|
150
|
+
return JSON.stringify(before ?? null) !== JSON.stringify(after ?? null);
|
|
151
|
+
}
|
|
152
|
+
/** The set of keys an entry must not expose beyond the persisted shape. */
|
|
153
|
+
export const ENTRY_KEYS = ["id", "kind", "title", "content", "path", "scope", "reference", "arguments", "metadata", "source", "created_at", "updated_at", "version"];
|
|
154
|
+
//# sourceMappingURL=state.js.map
|
package/lib/store.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HarnessScope, HarnessState, RefinementResult } from "./types.js";
|
|
2
|
+
export declare const EVOLVE_DIR = "evolve";
|
|
3
|
+
export interface StorePaths {
|
|
4
|
+
/** Directory holding harness_state.json. */
|
|
5
|
+
stateDir: string;
|
|
6
|
+
/** Directory holding snapshots for this store. */
|
|
7
|
+
snapshotsDir: string;
|
|
8
|
+
/** JSONL path for applied refinement results. */
|
|
9
|
+
resultsPath: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function storePaths(baseDir: string, scope: HarnessScope, sessionId?: string): StorePaths;
|
|
12
|
+
/** Snapshot the current state file before a mutation, if one exists. */
|
|
13
|
+
export declare function snapshotBefore(paths: StorePaths, refinementId: string): void;
|
|
14
|
+
/** Append an applied result to the store's JSONL history. */
|
|
15
|
+
export declare function appendResult(paths: StorePaths, result: RefinementResult): void;
|
|
16
|
+
/** Read the applied results history; malformed lines are skipped, never fatal. */
|
|
17
|
+
export declare function loadResults(paths: StorePaths): RefinementResult[];
|
|
18
|
+
/** Load a state file into memory, returning empty state when absent. */
|
|
19
|
+
export declare function loadStateFile(paths: StorePaths): HarnessState;
|
|
20
|
+
//# sourceMappingURL=store.d.ts.map
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store layout and snapshot discipline for the evolution plugin.
|
|
3
|
+
*
|
|
4
|
+
* Layout (self-contained under the DSH home; no dependency on session
|
|
5
|
+
* persistence internals):
|
|
6
|
+
*
|
|
7
|
+
* <dshHome>/evolve/global/harness_state.json cross-session store
|
|
8
|
+
* <dshHome>/evolve/global/refinements.jsonl applied results (rollback source)
|
|
9
|
+
* <dshHome>/evolve/local/<sessionId>/... per-session store
|
|
10
|
+
*
|
|
11
|
+
* Snapshot discipline is code-enforced: before any mutating apply, the
|
|
12
|
+
* pre-apply state is copied to `snapshots/<refinementId>.json`. The model has
|
|
13
|
+
* no way to skip it — it runs inside the service, not in a prompt.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { emptyHarnessState } from "./types.js";
|
|
18
|
+
import { stateFilePath } from "./state.js";
|
|
19
|
+
export const EVOLVE_DIR = "evolve";
|
|
20
|
+
export function storePaths(baseDir, scope, sessionId) {
|
|
21
|
+
const scopeDir = scope === "global" ? "global" : join("local", sessionId ?? "anonymous");
|
|
22
|
+
const stateDir = join(baseDir, EVOLVE_DIR, scopeDir);
|
|
23
|
+
return {
|
|
24
|
+
stateDir,
|
|
25
|
+
snapshotsDir: join(stateDir, "snapshots"),
|
|
26
|
+
resultsPath: join(stateDir, "refinements.jsonl"),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Snapshot the current state file before a mutation, if one exists. */
|
|
30
|
+
export function snapshotBefore(paths, refinementId) {
|
|
31
|
+
const statePath = stateFilePath(paths.stateDir);
|
|
32
|
+
if (!existsSync(statePath)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
mkdirSync(paths.snapshotsDir, { recursive: true });
|
|
36
|
+
writeFileSync(join(paths.snapshotsDir, `${refinementId}.json`), readFileSync(statePath, "utf8"), "utf8");
|
|
37
|
+
}
|
|
38
|
+
/** Append an applied result to the store's JSONL history. */
|
|
39
|
+
export function appendResult(paths, result) {
|
|
40
|
+
mkdirSync(paths.stateDir, { recursive: true });
|
|
41
|
+
writeFileSync(paths.resultsPath, `${JSON.stringify(result)}\n`, { encoding: "utf8", flag: "a" });
|
|
42
|
+
}
|
|
43
|
+
/** Read the applied results history; malformed lines are skipped, never fatal. */
|
|
44
|
+
export function loadResults(paths) {
|
|
45
|
+
if (!existsSync(paths.resultsPath)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const results = [];
|
|
49
|
+
for (const line of readFileSync(paths.resultsPath, "utf8").split("\n")) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed)
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
const parsed = JSON.parse(trimmed);
|
|
55
|
+
if (isResult(parsed)) {
|
|
56
|
+
results.push(parsed);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// skip malformed line
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
function isResult(data) {
|
|
66
|
+
return typeof data === "object" && data !== null && "id" in data && "appliedEdits" in data;
|
|
67
|
+
}
|
|
68
|
+
/** Load a state file into memory, returning empty state when absent. */
|
|
69
|
+
export function loadStateFile(paths) {
|
|
70
|
+
return existsSync(stateFilePath(paths.stateDir))
|
|
71
|
+
? JSON.parse(readFileSync(stateFilePath(paths.stateDir), "utf8"))
|
|
72
|
+
: emptyHarnessState();
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=store.js.map
|
package/lib/tool.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing evolve_* tools. The model supplies content; every guarantee
|
|
3
|
+
* (validation, snapshot, versioning, history, rollback) is code-enforced in
|
|
4
|
+
* the engine. `global: true` is required explicitly for cross-session edits.
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
7
|
+
import type { HarnessScope } from "./types.js";
|
|
8
|
+
import type { EvolutionEngine } from "./service.js";
|
|
9
|
+
/** Accept both the boolean tool parameter (`global: true`) and the string form. */
|
|
10
|
+
export declare function scopeOf(value: unknown, fallback: HarnessScope): HarnessScope;
|
|
11
|
+
export interface ToolGateOptions {
|
|
12
|
+
requireGlobalApproval: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function registerEvolveTools(ctx: Context, engine: EvolutionEngine, opts: ToolGateOptions): void;
|
|
15
|
+
//# sourceMappingURL=tool.d.ts.map
|
package/lib/tool.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { formatHarnessStateForPrompt } from "./render.js";
|
|
3
|
+
import { requireGlobalApproval } from "./approval.js";
|
|
4
|
+
import { entrySourceOf } from "./source.js";
|
|
5
|
+
const SCOPES = ["local", "global"];
|
|
6
|
+
/** Accept both the boolean tool parameter (`global: true`) and the string form. */
|
|
7
|
+
export function scopeOf(value, fallback) {
|
|
8
|
+
return value === "global" || value === true ? "global" : fallback;
|
|
9
|
+
}
|
|
10
|
+
/** The calling agent's session id; tools always run inside an agent scope. */
|
|
11
|
+
function sessionIdOf(exec) {
|
|
12
|
+
return exec.agent?.id;
|
|
13
|
+
}
|
|
14
|
+
function textResult(text) {
|
|
15
|
+
return { text };
|
|
16
|
+
}
|
|
17
|
+
export function registerEvolveTools(ctx, engine, opts) {
|
|
18
|
+
ctx.tools.register(defineTool({
|
|
19
|
+
name: "evolve_list",
|
|
20
|
+
description: "List the continual harness state (prompt notes, memories, skills, subagent specs) for the current session (local) or across sessions (global).",
|
|
21
|
+
parameters: {
|
|
22
|
+
scope: {
|
|
23
|
+
type: "string",
|
|
24
|
+
enum: SCOPES,
|
|
25
|
+
description: "Which store to list: 'local' (default) or 'global'.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
output: {
|
|
29
|
+
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
|
30
|
+
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
31
|
+
},
|
|
32
|
+
execute: async (args, exec) => {
|
|
33
|
+
const scope = scopeOf(args.scope, "local");
|
|
34
|
+
const state = engine.load(scope, sessionIdOf(exec));
|
|
35
|
+
return textResult(formatHarnessStateForPrompt(state));
|
|
36
|
+
},
|
|
37
|
+
}));
|
|
38
|
+
ctx.tools.register(defineTool({
|
|
39
|
+
name: "evolve_add",
|
|
40
|
+
description: "Create one harness entry (prompt/memory/skill/subagent). Skills require reference {type:python, import, callable} and an arguments contract. Snapshot, version, and history are handled automatically.",
|
|
41
|
+
parameters: {
|
|
42
|
+
kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true, description: "Entry kind." },
|
|
43
|
+
title: { type: "string", required: true, description: "Stable title." },
|
|
44
|
+
content: { type: "string", required: true, description: "Entry body." },
|
|
45
|
+
path: { type: "string", description: "Optional grouping path." },
|
|
46
|
+
reference: { type: "object", additionalProperties: true, description: "For skills: {type:'python', import, callable}." },
|
|
47
|
+
arguments: { type: "object", additionalProperties: true, description: "For skills: accepted input contract." },
|
|
48
|
+
global: { type: "boolean", description: "Set true to write the cross-session store (requires human approval; only for durable, reusable lessons)." },
|
|
49
|
+
},
|
|
50
|
+
output: {
|
|
51
|
+
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
|
52
|
+
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
53
|
+
},
|
|
54
|
+
execute: async (args, exec) => {
|
|
55
|
+
const scope = scopeOf(args.global, "local");
|
|
56
|
+
if (scope === "global" && opts.requireGlobalApproval) {
|
|
57
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → 跨会话全局 store`);
|
|
58
|
+
}
|
|
59
|
+
const edit = {
|
|
60
|
+
action: "create",
|
|
61
|
+
kind: args.kind,
|
|
62
|
+
title: args.title,
|
|
63
|
+
content: args.content,
|
|
64
|
+
};
|
|
65
|
+
if (args.path !== undefined)
|
|
66
|
+
edit.path = args.path;
|
|
67
|
+
if (args.reference !== undefined)
|
|
68
|
+
edit.reference = args.reference;
|
|
69
|
+
if (args.arguments !== undefined)
|
|
70
|
+
edit.arguments = args.arguments;
|
|
71
|
+
return textResult(applyEditsText(engine, scope, sessionIdOf(exec), [edit], exec.agent));
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
ctx.tools.register(defineTool({
|
|
75
|
+
name: "evolve_update",
|
|
76
|
+
description: "Update one harness entry by id. Pass only the fields that change.",
|
|
77
|
+
parameters: {
|
|
78
|
+
kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
|
|
79
|
+
id: { type: "string", required: true, description: "Existing entry id." },
|
|
80
|
+
title: { type: "string" },
|
|
81
|
+
content: { type: "string" },
|
|
82
|
+
global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
|
|
83
|
+
},
|
|
84
|
+
output: {
|
|
85
|
+
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
|
86
|
+
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
87
|
+
},
|
|
88
|
+
execute: async (args, exec) => {
|
|
89
|
+
const scope = scopeOf(args.global, "local");
|
|
90
|
+
if (scope === "global" && opts.requireGlobalApproval) {
|
|
91
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_update ${args.kind}:${args.id} → 跨会话全局 store`);
|
|
92
|
+
}
|
|
93
|
+
const edit = { action: "update", kind: args.kind, id: args.id };
|
|
94
|
+
if (args.title !== undefined)
|
|
95
|
+
edit.title = args.title;
|
|
96
|
+
if (args.content !== undefined)
|
|
97
|
+
edit.content = args.content;
|
|
98
|
+
return textResult(applyEditsText(engine, scope, sessionIdOf(exec), [edit], exec.agent));
|
|
99
|
+
},
|
|
100
|
+
}));
|
|
101
|
+
ctx.tools.register(defineTool({
|
|
102
|
+
name: "evolve_delete",
|
|
103
|
+
description: "Delete one harness entry by id.",
|
|
104
|
+
parameters: {
|
|
105
|
+
kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
|
|
106
|
+
id: { type: "string", required: true },
|
|
107
|
+
global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
|
|
108
|
+
},
|
|
109
|
+
output: {
|
|
110
|
+
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
|
111
|
+
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
112
|
+
},
|
|
113
|
+
execute: async (args, exec) => {
|
|
114
|
+
const scope = scopeOf(args.global, "local");
|
|
115
|
+
if (scope === "global" && opts.requireGlobalApproval) {
|
|
116
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${args.id} → 跨会话全局 store`);
|
|
117
|
+
}
|
|
118
|
+
const edit = { action: "delete", kind: args.kind, id: args.id };
|
|
119
|
+
return textResult(applyEditsText(engine, scope, sessionIdOf(exec), [edit], exec.agent));
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
ctx.tools.register(defineTool({
|
|
123
|
+
name: "evolve_rollback",
|
|
124
|
+
description: "Deterministically revert a previous refinement by its id (from evolve_list history or the /evolve command).",
|
|
125
|
+
parameters: {
|
|
126
|
+
refinementId: { type: "string", required: true, description: "The refinement id to roll back." },
|
|
127
|
+
global: { type: "boolean", description: "Set true to roll back a cross-session refinement." },
|
|
128
|
+
},
|
|
129
|
+
output: {
|
|
130
|
+
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
|
131
|
+
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
132
|
+
},
|
|
133
|
+
execute: async (args, exec) => {
|
|
134
|
+
const scope = scopeOf(args.global, "local");
|
|
135
|
+
const result = engine.rollback(scope, sessionIdOf(exec), args.refinementId);
|
|
136
|
+
return textResult(`Rolled back ${result.rollbackOf ?? result.id}: ${result.appliedEdits.filter((e) => e.applied).length} edit(s) reverted.`);
|
|
137
|
+
},
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
function applyEditsText(engine, scope, sessionId, edits, agent) {
|
|
141
|
+
const result = engine.apply(scope, sessionId, {
|
|
142
|
+
summary: "Direct tool edit",
|
|
143
|
+
rationale: "Model-invoked single edit via evolve_* tool.",
|
|
144
|
+
expectedOutcome: "Entry is created, updated, or deleted as requested.",
|
|
145
|
+
edits,
|
|
146
|
+
}, agent
|
|
147
|
+
? {
|
|
148
|
+
scope,
|
|
149
|
+
...(entrySourceOf(agent, sessionId) ? { source: entrySourceOf(agent, sessionId) } : {}),
|
|
150
|
+
}
|
|
151
|
+
: { scope });
|
|
152
|
+
const applied = result.appliedEdits.filter((e) => e.applied);
|
|
153
|
+
const failed = result.appliedEdits.filter((e) => !e.applied);
|
|
154
|
+
const lines = [`refinement ${result.id}: ${applied.length} applied, ${failed.length} failed`];
|
|
155
|
+
for (const e of applied) {
|
|
156
|
+
lines.push(`- ${e.action} ${e.kind}:${e.id} (v${(e.after?.version ?? e.before?.version) ?? "?"})`);
|
|
157
|
+
}
|
|
158
|
+
for (const e of failed) {
|
|
159
|
+
lines.push(`- failed ${e.action} ${e.kind}:${e.id ?? "(computed)"} — ${e.error ?? "unknown error"}`);
|
|
160
|
+
}
|
|
161
|
+
return lines.join("\n");
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=tool.js.map
|