@lmzhen/dsh-evolution-curator 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @deepseek-ai/dsh-evolution-curator
2
+
3
+ Deterministic skill lifecycle and recovery
4
+
5
+ ## Model Experience
6
+
7
+ ### Indirect model surface
8
+
9
+ #### What the model sees
10
+
11
+ `@deepseek-ai/dsh-evolution-curator` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
12
+
13
+ #### Token effect
14
+
15
+ Zero direct token effect from this package; consumers add any model-visible tokens.
16
+
17
+ #### KV Cache effect
18
+
19
+ Independent of request-prefix construction. This package does not alter the assembled prompt or tool list.
20
+
21
+ ## Known Limitations and Deferred Work
22
+
23
+
24
+ - - LLM nomination pass is advisory and disabled by default; deterministic lifecycle remains authoritative.
package/lib/index.js ADDED
@@ -0,0 +1,252 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import { randomUUID } from "node:crypto";
3
+ import { join } from "node:path";
4
+ import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
+ import z from "@deepseek-ai/schemastery";
6
+ import { CURATOR_PROMPT, SkillLibrary, buildCuratorRunReport, computeLifecycleTransitions, evolutionHome, evolutionIoAdapter, loadUsage, saveUsage } from "@lmzhen/dsh-evolution-core";
7
+ //#region lib/types/index.js
8
+ /**
9
+ * Deterministic skill lifecycle curator with interval gate and archive.
10
+ * @module @lmzhen/dsh-evolution-curator
11
+ */
12
+ var EvolutionCurator = class extends Service {
13
+ static inject = ["evolutionIo"];
14
+ static Config = z.object({
15
+ enabled: z.boolean().default(true),
16
+ intervalHours: z.number().default(168),
17
+ staleAfterDays: z.number().default(30),
18
+ archiveAfterDays: z.number().default(90),
19
+ llmReview: z.boolean().default(false),
20
+ curatorProvider: z.string().default("deepseek-official"),
21
+ qualityWarnStaleAfterDays: z.number().default(7),
22
+ minIdleHours: z.number().default(0),
23
+ excludeSkillNames: z.array(z.string()).default([]),
24
+ manageUnmanaged: z.boolean().default(false),
25
+ curatorReviewMaxTokens: z.number().default(2048)
26
+ });
27
+ skills;
28
+ io;
29
+ enabled;
30
+ intervalHours;
31
+ staleAfterDays;
32
+ archiveAfterDays;
33
+ llmReview;
34
+ curatorProvider;
35
+ qualityWarnStaleAfterDays;
36
+ minIdleHours;
37
+ excludeSkillNames;
38
+ manageUnmanaged;
39
+ curatorReviewMaxTokens;
40
+ lastRun = 0;
41
+ timer;
42
+ constructor(ctx, config = {}) {
43
+ super(ctx, "evolutionCurator");
44
+ this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
45
+ this.skills = new SkillLibrary(void 0, this.io);
46
+ this.enabled = config.enabled ?? true;
47
+ this.intervalHours = config.intervalHours ?? 168;
48
+ this.staleAfterDays = config.staleAfterDays ?? 30;
49
+ this.archiveAfterDays = config.archiveAfterDays ?? 90;
50
+ this.llmReview = config.llmReview ?? false;
51
+ this.curatorProvider = config.curatorProvider ?? "deepseek-official";
52
+ this.qualityWarnStaleAfterDays = config.qualityWarnStaleAfterDays ?? 7;
53
+ this.minIdleHours = config.minIdleHours ?? 0;
54
+ this.excludeSkillNames = new Set(config.excludeSkillNames ?? []);
55
+ this.manageUnmanaged = config.manageUnmanaged ?? false;
56
+ this.curatorReviewMaxTokens = config.curatorReviewMaxTokens ?? 2048;
57
+ this.lastRun = Date.now();
58
+ this.ctx.effect(() => {
59
+ return () => {
60
+ this.stop();
61
+ };
62
+ }, "evolution-curator.stop");
63
+ }
64
+ lifecycle() {
65
+ const snapshot = this.ctx.get("evolutionPolicy")?.get();
66
+ return {
67
+ intervalHours: snapshot?.curatorIntervalHours ?? this.intervalHours,
68
+ staleAfterDays: snapshot?.staleAfterDays ?? this.staleAfterDays,
69
+ archiveAfterDays: snapshot?.archiveAfterDays ?? this.archiveAfterDays
70
+ };
71
+ }
72
+ start() {
73
+ if (!this.enabled || this.timer) return;
74
+ this.timer = setInterval(() => {
75
+ if (Date.now() - this.lastRun >= this.lifecycle().intervalHours * 36e5) this.run();
76
+ }, 3600 * 1e3);
77
+ this.timer.unref();
78
+ }
79
+ stop() {
80
+ if (this.timer) clearInterval(this.timer);
81
+ this.timer = void 0;
82
+ }
83
+ /**
84
+ * Optional Hermes-curator LLM pass. The model may only NOMINATE pruning
85
+ * candidates; archive/restore remains a control-plane operation and every
86
+ * nominated name is still checked against lifecycle thresholds and
87
+ * protected markers before any file move.
88
+ */
89
+ async recommend(candidates) {
90
+ if (candidates.length === 0) return [];
91
+ const llm = this.ctx.get("llm");
92
+ if (!llm) return [];
93
+ const model = this.ctx.get("evolutionPolicy")?.get().curatorModel ?? "deepseek-v4-pro";
94
+ const prompt = [
95
+ CURATOR_PROMPT,
96
+ "",
97
+ "Stale candidates observed by the deterministic lifecycle scanner:",
98
+ ...candidates.map((name) => `- ${name}`),
99
+ "",
100
+ "Return a YAML summary with a prunings list. Nominate only candidates whose archival is clearly safe."
101
+ ].join("\n");
102
+ try {
103
+ const assembler = new BlockAssembler();
104
+ for await (const chunk of llm.stream({
105
+ provider: this.curatorProvider,
106
+ model,
107
+ messages: [createUserMessage({
108
+ content: [{
109
+ type: "text",
110
+ text: prompt
111
+ }],
112
+ source: {
113
+ kind: "plugin",
114
+ plugin: "dsh-evolution-curator",
115
+ form: "notice",
116
+ summary: "curator review"
117
+ }
118
+ })],
119
+ maxTokens: this.curatorReviewMaxTokens,
120
+ purpose: "evolution-curator"
121
+ })) assembler.push(chunk);
122
+ const text = assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n");
123
+ const names = /* @__PURE__ */ new Set();
124
+ const section = text.slice(text.indexOf("prunings:"));
125
+ for (const [, name] of section.matchAll(/^\s*-\s*name:\s*([a-z0-9][a-z0-9-]*)\s*$/gm)) if (name) names.add(name);
126
+ return [...names].filter((name) => candidates.includes(name));
127
+ } catch {
128
+ return [];
129
+ }
130
+ }
131
+ skippedReport(runId, startedAt) {
132
+ return buildCuratorRunReport({
133
+ runId,
134
+ startedAt,
135
+ finishedAt: startedAt,
136
+ staleCandidates: [],
137
+ llmNominations: [],
138
+ archiveCandidates: [],
139
+ archived: [],
140
+ failed: []
141
+ });
142
+ }
143
+ async run() {
144
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
145
+ const runId = randomUUID();
146
+ const stateService = this.ctx.get("evolutionState");
147
+ const lifecycle = this.lifecycle();
148
+ const persisted = await stateService?.loadCuratorState();
149
+ if (persisted && Date.now() - persisted.lastRunAt < lifecycle.intervalHours * 36e5) return {
150
+ stale: [],
151
+ archived: [],
152
+ errors: [],
153
+ report: this.skippedReport(runId, startedAt),
154
+ skipped: "interval"
155
+ };
156
+ if (this.minIdleHours > 0 && this.recentSessionActive()) return {
157
+ stale: [],
158
+ archived: [],
159
+ errors: [],
160
+ report: this.skippedReport(runId, startedAt),
161
+ skipped: "active-session"
162
+ };
163
+ const root = this.skills.root;
164
+ const snapshotPath = await this.skills.snapshotAll("pre-curator-run");
165
+ const usage = await loadUsage(root, this.io);
166
+ const result = computeLifecycleTransitions(usage, {
167
+ staleAfterDays: lifecycle.staleAfterDays,
168
+ archiveAfterDays: lifecycle.archiveAfterDays,
169
+ qualityWarnStaleAfterDays: this.qualityWarnStaleAfterDays,
170
+ excludeSkillNames: this.excludeSkillNames,
171
+ manageUnmanaged: this.manageUnmanaged
172
+ });
173
+ const errors = [];
174
+ const archivedSkills = [];
175
+ const llmNominations = this.llmReview ? await this.recommend(result.markStale) : [];
176
+ const archiveCandidates = [...new Set([...result.archive, ...llmNominations])];
177
+ for (const name of archiveCandidates) {
178
+ const archived = await this.skills.archive(name, "Lifecycle: reached archive threshold");
179
+ if (!archived.ok) {
180
+ const record = usage.get(name);
181
+ if (record) record.state = "active";
182
+ errors.push(`${name}: ${archived.message}`);
183
+ } else archivedSkills.push({
184
+ name,
185
+ path: archived.path ?? "",
186
+ reason: "Lifecycle: reached archive threshold"
187
+ });
188
+ }
189
+ await saveUsage(root, usage, this.io);
190
+ this.lastRun = Date.now();
191
+ const report = buildCuratorRunReport({
192
+ runId,
193
+ startedAt,
194
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
195
+ staleCandidates: result.markStale,
196
+ llmNominations,
197
+ archiveCandidates,
198
+ archived: archivedSkills,
199
+ failed: archiveCandidates.filter((name) => errors.some((error) => error.startsWith(`${name}:`))).map((name) => {
200
+ return {
201
+ name,
202
+ reason: errors.find((item) => item.startsWith(`${name}:`))?.slice(name.length + 2) ?? "unknown"
203
+ };
204
+ }),
205
+ snapshotPath
206
+ });
207
+ const reportsRoot = join(evolutionHome(), "reports");
208
+ try {
209
+ await this.io.writeText(join(reportsRoot, `curator-${runId}.json`), JSON.stringify(report, null, 2));
210
+ } catch (error) {
211
+ this.ctx.logger.warn(`evolution-curator: failed to persist report ${runId}`);
212
+ this.ctx.logger.warn(error);
213
+ }
214
+ await stateService?.saveCuratorState({
215
+ lastRunAt: this.lastRun,
216
+ runCount: (persisted?.runCount ?? 0) + 1,
217
+ lastSummary: `stale:${result.markStale.length} archived:${archivedSkills.length}`,
218
+ paused: false
219
+ });
220
+ return {
221
+ stale: result.markStale,
222
+ archived: archivedSkills.map((item) => item.name),
223
+ errors,
224
+ report
225
+ };
226
+ }
227
+ recentSessionActive() {
228
+ const agents = this.ctx.get("agents");
229
+ if (!agents) return false;
230
+ let latest = 0;
231
+ for (const agent of agents.list()) {
232
+ const events = agent.session.events;
233
+ const last = events.length === 0 ? 0 : events[events.length - 1]?.time ?? 0;
234
+ latest = Math.max(latest, last);
235
+ }
236
+ return latest > 0 && Date.now() - latest < this.minIdleHours * 36e5;
237
+ }
238
+ async latestReport() {
239
+ const reportsRoot = join(evolutionHome(), "reports");
240
+ const latest = (await this.io.list(reportsRoot)).filter((name) => name.startsWith("curator-") && name.endsWith(".json")).sort().reverse()[0];
241
+ if (!latest) return null;
242
+ const raw = await this.io.readText(join(reportsRoot, latest));
243
+ if (raw === null) return null;
244
+ try {
245
+ return JSON.parse(raw);
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ };
251
+ //#endregion
252
+ export { EvolutionCurator, EvolutionCurator as default };
@@ -0,0 +1,8 @@
1
+ //#region lib/types/invariant.js
2
+ const PACKAGE_NAME = "@deepseek-ai/dsh-evolution-curator";
3
+ const name = "evolution-curator-invariant";
4
+ const inject = ["invariants"];
5
+ const install = () => {};
6
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
7
+ //#endregion
8
+ export { apply, inject, name };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Deterministic skill lifecycle curator with interval gate and archive.
3
+ * @module @deepseek-ai/dsh-evolution-curator
4
+ */
5
+ import { Context, Service } from '@deepseek-ai/cordis';
6
+ import type Schema from '@deepseek-ai/schemastery';
7
+ import { SkillLibrary } from '@deepseek-ai/dsh-evolution-core';
8
+ import { type CuratorRunReport } from '@deepseek-ai/dsh-evolution-core';
9
+ declare module '@deepseek-ai/cordis' {
10
+ interface Context {
11
+ evolutionCurator: EvolutionCurator;
12
+ }
13
+ }
14
+ export interface Config {
15
+ enabled?: boolean;
16
+ intervalHours?: number;
17
+ staleAfterDays?: number;
18
+ archiveAfterDays?: number;
19
+ /** Spend one LLM review pass on stale candidates before the deterministic archive step. */
20
+ llmReview?: boolean;
21
+ curatorProvider?: string;
22
+ /** Quality-warned skills may turn stale after this many idle days. */
23
+ qualityWarnStaleAfterDays?: number;
24
+ /** Skip automatic runs while any session was active within this many hours (0 disables). */
25
+ minIdleHours?: number;
26
+ /** Skill names excluded from the automated lifecycle. */
27
+ excludeSkillNames?: string[];
28
+ /** Include usage records whose created_by is not 'agent' in lifecycle decisions. */
29
+ manageUnmanaged?: boolean;
30
+ /** Max tokens for the optional LLM nomination pass. */
31
+ curatorReviewMaxTokens?: number;
32
+ }
33
+ export declare class EvolutionCurator extends Service {
34
+ static inject: string[];
35
+ static Config: Schema<Config>;
36
+ readonly skills: SkillLibrary;
37
+ private readonly io;
38
+ private readonly enabled;
39
+ private readonly intervalHours;
40
+ private readonly staleAfterDays;
41
+ private readonly archiveAfterDays;
42
+ private readonly llmReview;
43
+ private readonly curatorProvider;
44
+ private readonly qualityWarnStaleAfterDays;
45
+ private readonly minIdleHours;
46
+ private readonly excludeSkillNames;
47
+ private readonly manageUnmanaged;
48
+ private readonly curatorReviewMaxTokens;
49
+ private lastRun;
50
+ private timer;
51
+ constructor(ctx: Context, config?: Config);
52
+ private lifecycle;
53
+ start(): void;
54
+ stop(): void;
55
+ /**
56
+ * Optional Hermes-curator LLM pass. The model may only NOMINATE pruning
57
+ * candidates; archive/restore remains a control-plane operation and every
58
+ * nominated name is still checked against lifecycle thresholds and
59
+ * protected markers before any file move.
60
+ */
61
+ recommend(candidates: string[]): Promise<string[]>;
62
+ private skippedReport;
63
+ run(): Promise<{
64
+ stale: string[];
65
+ archived: string[];
66
+ errors: string[];
67
+ report: CuratorRunReport;
68
+ skipped?: string;
69
+ }>;
70
+ private recentSessionActive;
71
+ latestReport(): Promise<CuratorRunReport | null>;
72
+ }
73
+ export default EvolutionCurator;
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "evolution-curator-invariant";
3
+ export declare const inject: string[];
4
+ export declare const apply: (ctx: Context) => Promise<() => void>;
5
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@lmzhen/dsh-evolution-curator",
3
+ "description": "Deterministic skill lifecycle and recovery (community build)",
4
+ "version": "0.1.0-rc.1",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/lmzhen/dsh-evolution.git",
11
+ "directory": "packages/dsh-evolution-curator"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "lib/invariant.js",
30
+ "lib/types/**/*.d.ts",
31
+ "lib/types/invariant.d.ts"
32
+ ],
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@deepseek-ai/schemastery": "^3.18.1",
36
+ "@lmzhen/dsh-evolution-core": "^0.1.0-rc.1"
37
+ },
38
+ "peerDependencies": {
39
+ "@deepseek-ai/cordis": "^4.0.1",
40
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
41
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
42
+ "@lmzhen/dsh-evolution-io": "^0.1.0-rc.1",
43
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.1"
44
+ },
45
+ "devDependencies": {
46
+ "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
47
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
48
+ "@lmzhen/dsh-evolution-core": "^0.1.0-rc.1",
49
+ "@lmzhen/dsh-evolution-io": "^0.1.0-rc.1",
50
+ "@lmzhen/dsh-evolution-state": "^0.1.0-rc.1"
51
+ }
52
+ }