dsh-continual-evolve 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -19
- package/README.zh.md +64 -21
- package/lib/apply.js +2 -0
- package/lib/approval.d.ts +19 -0
- package/lib/auto.d.ts +61 -1
- package/lib/auto.js +108 -2
- package/lib/benchmark.d.ts +14 -0
- package/lib/command.js +234 -5
- package/lib/evaluate.d.ts +36 -7
- package/lib/evaluate.js +157 -43
- package/lib/fate.d.ts +126 -0
- package/lib/fate.js +338 -0
- package/lib/index.d.ts +26 -0
- package/lib/index.js +18 -2
- package/lib/mount.js +5 -0
- package/lib/planner.d.ts +8 -1
- package/lib/planner.js +27 -0
- package/lib/render.js +2 -1
- package/lib/review.d.ts +1 -1
- package/lib/review.js +17 -0
- package/lib/score.d.ts +22 -4
- package/lib/score.js +48 -7
- package/lib/skill.d.ts +10 -2
- package/lib/skill.js +34 -2
- package/lib/skillquality.d.ts +81 -0
- package/lib/skillquality.js +311 -0
- package/lib/tool.js +6 -3
- package/lib/types.d.ts +31 -0
- package/lib/types.js +19 -0
- package/lib/validate.js +25 -1
- package/lib/wrapup.d.ts +210 -0
- package/lib/wrapup.js +439 -0
- package/package.json +24 -14
package/lib/types.js
CHANGED
|
@@ -24,6 +24,25 @@ export const SOURCE_SEQS_KEY = "sourceSeqs";
|
|
|
24
24
|
* the entry can be restored (unarchive) or rolled back like any other edit.
|
|
25
25
|
*/
|
|
26
26
|
export const ARCHIVED_AT_KEY = "archivedAt";
|
|
27
|
+
/**
|
|
28
|
+
* Metadata key stamped on a LOCAL entry that was promoted to the global
|
|
29
|
+
* store by a session wrap-up: the id of the global entry it became. Present
|
|
30
|
+
* means the entry's lifecycle is finished — it must not be offered for
|
|
31
|
+
* promotion again (the global copy is the live one, the local copy is a
|
|
32
|
+
* restorable trace).
|
|
33
|
+
*/
|
|
34
|
+
export const PROMOTED_TO_KEY = "promotedTo";
|
|
35
|
+
/**
|
|
36
|
+
* Metadata key recording when a local entry was promoted to the global
|
|
37
|
+
* store (companion of {@link PROMOTED_TO_KEY}).
|
|
38
|
+
*/
|
|
39
|
+
export const PROMOTED_AT_KEY = "promotedAt";
|
|
40
|
+
/**
|
|
41
|
+
* Metadata key stamped on a GLOBAL entry created by a session wrap-up
|
|
42
|
+
* promotion: `<sessionId>:<localEntryId>` — the反向 provenance link from the
|
|
43
|
+
* cross-session copy back to the session it was distilled from.
|
|
44
|
+
*/
|
|
45
|
+
export const SOURCED_FROM_KEY = "sourcedFromLocal";
|
|
27
46
|
/**
|
|
28
47
|
* True when the entry is archived (hidden from injection, restorable).
|
|
29
48
|
* Absent or empty archivedAt means the entry is active.
|
package/lib/validate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateSkillEntryContent } from "./skillquality.js";
|
|
1
2
|
const ACTIONS = new Set(["create", "update", "delete", "archive"]);
|
|
2
3
|
const KINDS = new Set(["prompt", "memory", "skill", "subagent"]);
|
|
3
4
|
export const BASE_SYSTEM_PROMPT_ID = "base_system_prompt";
|
|
@@ -24,7 +25,30 @@ export function validateEdit(edit, computedId) {
|
|
|
24
25
|
return `${edit.action} requires title and content`;
|
|
25
26
|
}
|
|
26
27
|
if (edit.action !== "delete" && edit.kind === "skill") {
|
|
27
|
-
|
|
28
|
+
// Guidance skills are SKILL.md documents: no python reference (a
|
|
29
|
+
// reference on a guidance skill would be an invented contract) and
|
|
30
|
+
// no arguments contract. Executable skills keep the full contract.
|
|
31
|
+
if (edit.skill_kind === "guidance") {
|
|
32
|
+
if (edit.reference !== undefined && Object.keys(edit.reference).length > 0) {
|
|
33
|
+
return "guidance skill must not carry a python reference (it is a SKILL.md document, not an executable)";
|
|
34
|
+
}
|
|
35
|
+
if (edit.arguments !== undefined && Object.keys(edit.arguments).length > 0) {
|
|
36
|
+
return "guidance skill must not carry an arguments contract (only executable skills declare inputs)";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const contractError = validateSkillContract(edit);
|
|
41
|
+
if (contractError)
|
|
42
|
+
return contractError;
|
|
43
|
+
}
|
|
44
|
+
// The entry body materializes as a SKILL.md under generated
|
|
45
|
+
// frontmatter; content-level mechanics (no shadowing `---`, no
|
|
46
|
+
// escaping resource refs) are code-enforced so a bad body never
|
|
47
|
+
// reaches the store (mirrors skill-creator's validate-frontmatter).
|
|
48
|
+
const contentProblems = validateSkillEntryContent(edit.content ?? "");
|
|
49
|
+
if (contentProblems.length > 0) {
|
|
50
|
+
return contentProblems.join("; ");
|
|
51
|
+
}
|
|
28
52
|
}
|
|
29
53
|
return undefined;
|
|
30
54
|
}
|
package/lib/wrapup.d.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session wrap-up: the lifecycle exit for a session's local harness entries.
|
|
3
|
+
*
|
|
4
|
+
* When a session ends, its local entries default to orphans: a later session
|
|
5
|
+
* (not on the parentSession chain) never sees them, and nothing promotes or
|
|
6
|
+
* archives them — the exploration results effectively "die" with the session.
|
|
7
|
+
* Wrap-up gives those entries a real exit:
|
|
8
|
+
*
|
|
9
|
+
* - cross-session-reusable content is classified `promote` and moved into the
|
|
10
|
+
* global store (through the human approval gate — global is a governed
|
|
11
|
+
* resource, exactly like skill proposals);
|
|
12
|
+
* - session-specific / superseded / already-covered content is classified
|
|
13
|
+
* `archive` (hidden from injection, data stays restorable, rollbackable);
|
|
14
|
+
* - everything else is kept.
|
|
15
|
+
*
|
|
16
|
+
* Division of labor is deliberate: the mechanical audit proposes, the LLM
|
|
17
|
+
* classifies, the user approves, the code applies deterministically. The
|
|
18
|
+
* apply-side guard (`filterPromotable`) re-checks global coverage at apply
|
|
19
|
+
* time so a stale classification can never write a duplicate global entry.
|
|
20
|
+
*/
|
|
21
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
22
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
23
|
+
import type { HarnessEntry, HarnessState, RefinementKind, RefinementProposal } from "./types.js";
|
|
24
|
+
/** What should happen to one local entry at session end. */
|
|
25
|
+
export type WrapupVerdict = "promote" | "archive" | "keep";
|
|
26
|
+
/** A classified local entry: `key` matches one audited candidate exactly. */
|
|
27
|
+
export interface WrapupItem {
|
|
28
|
+
/** `kind:id` of the candidate this verdict refers to. */
|
|
29
|
+
key: string;
|
|
30
|
+
verdict: WrapupVerdict;
|
|
31
|
+
reason: string;
|
|
32
|
+
/**
|
|
33
|
+
* Optional split-promotion payload (verdict "archive" only): the entry is
|
|
34
|
+
* archived as a whole, but a CLEANED cross-session-reusable part is
|
|
35
|
+
* offered for promotion — the durable fact distilled out of the mixed
|
|
36
|
+
* entry, with the ephemeral snapshot left behind in the archive.
|
|
37
|
+
*/
|
|
38
|
+
promote?: {
|
|
39
|
+
title: string;
|
|
40
|
+
content: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** A real global entry worth showing the assessor for the same topic. */
|
|
44
|
+
export interface GlobalHint {
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
}
|
|
48
|
+
/** The model's full classification of a session's local entries. */
|
|
49
|
+
export interface WrapupAssessment {
|
|
50
|
+
items: WrapupItem[];
|
|
51
|
+
rationale: string;
|
|
52
|
+
}
|
|
53
|
+
/** A local entry offered for assessment, plus its deterministic audit flags. */
|
|
54
|
+
export interface WrapupCandidate {
|
|
55
|
+
kind: RefinementKind;
|
|
56
|
+
id: string;
|
|
57
|
+
title: string;
|
|
58
|
+
content: string;
|
|
59
|
+
path: string;
|
|
60
|
+
version: number;
|
|
61
|
+
metadata: Record<string, unknown>;
|
|
62
|
+
/**
|
|
63
|
+
* True when the global store already covers this topic by a STRONG
|
|
64
|
+
* signal: a title that normalizes equal to, or (beyond a length floor)
|
|
65
|
+
* contains, the candidate's title. Collisions on id alone with a wildly
|
|
66
|
+
* different title are deliberately NOT coverage — see {@link globalHintsFor}.
|
|
67
|
+
*/
|
|
68
|
+
coveredGlobally: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Actual global entries that touch the same topic (same id, equal
|
|
71
|
+
* normalized title, or title overlap). Shown to the assessor so it judges
|
|
72
|
+
* against real titles instead of a bare boolean; a bare same-id collision
|
|
73
|
+
* shows up here precisely so the model can tell whether the global copy
|
|
74
|
+
* really covers the local content.
|
|
75
|
+
*/
|
|
76
|
+
globalHints: GlobalHint[];
|
|
77
|
+
}
|
|
78
|
+
export declare function candidateKey(kind: RefinementKind, id: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Deterministic global-coverage check (STRONG signal): the global store
|
|
81
|
+
* already covers a topic when it holds a title that normalizes equal to, or
|
|
82
|
+
* (beyond a length floor) contains, the candidate's normalized title.
|
|
83
|
+
* Archived global entries count too — the topic was already judged
|
|
84
|
+
* cross-session; a local duplicate would only re-sediment it.
|
|
85
|
+
*
|
|
86
|
+
* The bare same-id case is deliberately NOT coverage: ids are slugs derived
|
|
87
|
+
* from titles, so a real collision is usually caught by the title check
|
|
88
|
+
* below. A same-id entry with a wildly different title is a weak signal — the
|
|
89
|
+
* caller routes it through {@link globalHintsFor} for the assessor to judge
|
|
90
|
+
* against the actual global title (real case: local `memory` "用户产品愿景与
|
|
91
|
+
* 收入需求(本会话)" vs global `memory` "用户画像(持续更新)").
|
|
92
|
+
*/
|
|
93
|
+
export declare function globalCoverageDetected(globalState: HarnessState, kind: RefinementKind, entry: Pick<HarnessEntry, "id" | "title">): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* The actual global entries that touch the same topic as a local candidate:
|
|
96
|
+
* same id (regardless of title — the weak collision signal that is NOT
|
|
97
|
+
* coverage on its own), equal normalized title, or title overlap. The raw ids
|
|
98
|
+
* and titles let the assessor judge enrichment against real global content
|
|
99
|
+
* (does the global copy already hold what the local one adds?) rather than a
|
|
100
|
+
* bare boolean. Bounded: a handful of best matches, never the whole store.
|
|
101
|
+
*/
|
|
102
|
+
export declare function globalHintsFor(globalState: HarnessState, kind: RefinementKind, entry: Pick<HarnessEntry, "id" | "title">): GlobalHint[];
|
|
103
|
+
/**
|
|
104
|
+
* The auditable local candidates of a session: every non-archived local
|
|
105
|
+
* entry that has not already been promoted (a promoted entry's lifecycle is
|
|
106
|
+
* finished — the global copy is the live one). Each carries its
|
|
107
|
+
* `coveredGlobally` flag so the assessor never wastes a promote on a topic
|
|
108
|
+
* the global store already owns.
|
|
109
|
+
*/
|
|
110
|
+
export declare function listLocalCandidates(state: HarnessState, globalState: HarnessState): WrapupCandidate[];
|
|
111
|
+
/**
|
|
112
|
+
* Parse and validate the model's assessment JSON. Defense is mechanical:
|
|
113
|
+
* keys outside the candidate list are dropped, verdicts outside the enum
|
|
114
|
+
* collapse to "keep", and candidates the model omitted default to "keep" —
|
|
115
|
+
* a malformed reply can never change an entry's fate by itself.
|
|
116
|
+
*
|
|
117
|
+
* Split promotion (verdict "archive" with a `promote` sub-object): the
|
|
118
|
+
* sub-object is accepted ONLY on archive verdicts and ONLY when both cleaned
|
|
119
|
+
* title and content are non-empty strings — a dropped/malformed sub-object
|
|
120
|
+
* silently degrades to a plain archive (the entry is never half-promoted).
|
|
121
|
+
*/
|
|
122
|
+
export declare function parseWrapupAssessment(text: string, candidates: readonly WrapupCandidate[]): WrapupAssessment;
|
|
123
|
+
export interface PromotableSplit {
|
|
124
|
+
/** Items that may be promoted: classified promote AND not covered globally. */
|
|
125
|
+
promotable: WrapupItem[];
|
|
126
|
+
/** Items classified promote but blocked by the deterministic guard, with why. */
|
|
127
|
+
skipped: {
|
|
128
|
+
key: string;
|
|
129
|
+
reason: string;
|
|
130
|
+
}[];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Apply-time deterministic guard: re-check every promote verdict against the
|
|
134
|
+
* global store right before it lands. The LLM classification may be stale
|
|
135
|
+
* (a gate ran while assessing) or wrong; this ensures a promote never writes
|
|
136
|
+
* a duplicate global entry. Pure and unit-tested.
|
|
137
|
+
*/
|
|
138
|
+
export declare function filterPromotable(items: readonly WrapupItem[], globalState: HarnessState, candidates: readonly WrapupCandidate[]): PromotableSplit;
|
|
139
|
+
export interface ArchiveReviewSplit {
|
|
140
|
+
/** Archives that may proceed silently: topic already covered, no real
|
|
141
|
+
* distillation source, or the archive half of an already-approved split. */
|
|
142
|
+
silent: WrapupItem[];
|
|
143
|
+
/**
|
|
144
|
+
* Archives that would bury possibly-reusable content: not covered
|
|
145
|
+
* globally AND distilled from real user messages (sourceSeqs present).
|
|
146
|
+
* These MAY NOT archive silently — the command must get user
|
|
147
|
+
* confirmation first (the symmetric guard to filterPromotable: it stops
|
|
148
|
+
* over-archiving, not just over-writing).
|
|
149
|
+
*/
|
|
150
|
+
review: WrapupItem[];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The symmetric archive guard. `filterPromotable` is one-directional: it
|
|
154
|
+
* stops the model from WRITING duplicate global entries, but nothing stopped
|
|
155
|
+
* an unfounded ARCHIVE from hiding content that was actually only local.
|
|
156
|
+
* Guard criteria: an archive needs user confirmation when it is NOT covered
|
|
157
|
+
* globally AND the entry carries a real distillation source (sourceSeqs /
|
|
158
|
+
* sourceSession — i.e. it was distilled from actual user messages, so it
|
|
159
|
+
* may hold reusable value). Operational/empty entries archive silently as
|
|
160
|
+
* before. Split archives (archive + promote sub-object) skip this check:
|
|
161
|
+
* their promotion already crosses a human approval gate, so the archive is
|
|
162
|
+
* the completion of an approved action, not a silent burial.
|
|
163
|
+
*/
|
|
164
|
+
export declare function needsArchiveReview(item: WrapupItem, candidate: WrapupCandidate): boolean;
|
|
165
|
+
/** Partition archive items into silent vs review-required (see needsArchiveReview). */
|
|
166
|
+
export declare function splitArchiveGuards(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[]): ArchiveReviewSplit;
|
|
167
|
+
/**
|
|
168
|
+
* Apply-time guard for a split promotion (archive + promote sub-object):
|
|
169
|
+
* the cleaned title must not duplicate a topic already covered globally. A
|
|
170
|
+
* duplicate split is dropped (the entry still archives plain) rather than
|
|
171
|
+
* half-promoting a redundancy.
|
|
172
|
+
*/
|
|
173
|
+
export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind): string | undefined;
|
|
174
|
+
/**
|
|
175
|
+
* Shared proposal builders for a WHOLE promotion — used by both the
|
|
176
|
+
* `/evolve wrapup` command and the gate's local-fate dimension so the two
|
|
177
|
+
* paths apply IDENTICAL edits (global create + local retirement stamp).
|
|
178
|
+
*
|
|
179
|
+
* The local stamp is a factory: the `promotedTo` id is only known after the
|
|
180
|
+
* global create lands (validation may slugify the id), so the caller applies
|
|
181
|
+
* the global proposal first and stamps the local copy with the created id.
|
|
182
|
+
*/
|
|
183
|
+
export declare function wholePromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
|
|
184
|
+
global: RefinementProposal;
|
|
185
|
+
localStamp: (createdId: string) => RefinementProposal;
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Shared proposal builders for a SPLIT promotion (A-form): archive a mixed
|
|
189
|
+
* local entry but promote ONLY the cleaned durable part the model extracted.
|
|
190
|
+
* Same usage contract as {@link wholePromoteProposals}: apply the global
|
|
191
|
+
* create, then stamp the original local entry with the created id.
|
|
192
|
+
*/
|
|
193
|
+
export declare function splitPromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
|
|
194
|
+
global: RefinementProposal;
|
|
195
|
+
localStamp: (createdId: string) => RefinementProposal;
|
|
196
|
+
};
|
|
197
|
+
export declare const WRAPUP_ASSESS_SYSTEM_PROMPT = "You are the /evolve session wrap-up assessor.\n\nA session is ending and its local harness entries need a fate. Classify each\nlisted entry exactly once:\n\n- \"promote\" \u2014 the content is a stable, durable, CROSS-SESSION reusable lesson:\n a durable user preference, a project-level fact or convention, a reusable\n procedure or skill. Future sessions would benefit from seeing it.\n- \"archive\" \u2014 the content is session-specific task progress, one-off noise,\n superseded or obsolete, or already covered by the global store (note\n \"covered globally\" in the reason).\n- \"keep\" \u2014 still actively useful to this session, or genuinely uncertain.\n\nRules:\n- When an entry is marked \"covered globally\" in the listing, prefer \"archive\"\n or \"keep\" over \"promote\" \u2014 promoting a duplicate gains nothing.\n- Do not promote local task state, work-in-progress notes, or content tied to\n one session's ephemeral details.\n- Skills: only \"promote\" a skill entry that is a genuinely reusable procedure\n meeting the DSH skill quality standard; one-off workflows are \"archive\" or\n \"keep\".\n- SPLIT PROMOTION: when an entry mixes a stable, cross-session-reusable part\n WITH session-specific snapshot details, do NOT promote it whole. Instead\n give verdict \"archive\" WITH a \"promote\" sub-object holding a CLEANED\n version of only the durable part (a stable title + the persistent facts,\n stripped of dates/states/one-off figures). Ephemeral snapshot content stays\n out of the sub-object \u2014 it is left behind in the archive. A sub-object is\n only meaningful on \"archive\" verdicts.\n\nReturn JSON only:\n{\n \"rationale\": \"one or two sentences\",\n \"items\": [\n {\"key\": \"memory:foo\", \"verdict\": \"promote|archive|keep\", \"reason\": \"why\"},\n {\"key\": \"memory:bar\", \"verdict\": \"archive\", \"reason\": \"why\",\n \"promote\": {\"title\": \"cleaned stable title\", \"content\": \"cleaned durable part only\"}}\n ]\n}\nOnly keys from the provided list are allowed; any entry you omit defaults to \"keep\".";
|
|
198
|
+
export interface AssessOptions {
|
|
199
|
+
/** Output token budget for the assessment call. */
|
|
200
|
+
maxOutputTokens?: number;
|
|
201
|
+
/** Abort signal forwarded to the model call. */
|
|
202
|
+
signal?: AbortSignal;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Ask the model to classify the audited local candidates. Routes through the
|
|
206
|
+
* calling agent's own provider/model (same model the session runs on), with
|
|
207
|
+
* reasoning disabled so the output budget goes to the JSON verdicts.
|
|
208
|
+
*/
|
|
209
|
+
export declare function assessLocalEntries(ctx: Context, agent: Agent, candidates: readonly WrapupCandidate[], options?: AssessOptions): Promise<WrapupAssessment>;
|
|
210
|
+
//# sourceMappingURL=wrapup.d.ts.map
|