dsh-continual-evolve 0.7.6 → 0.9.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 +20 -7
- package/README.zh.md +16 -7
- package/lib/auto.d.ts +31 -2
- package/lib/auto.js +124 -6
- package/lib/benchmark-command.js +1 -1
- package/lib/command.js +93 -1
- package/lib/fate.js +10 -10
- package/lib/index.d.ts +10 -0
- package/lib/index.js +6 -0
- package/lib/memory-agent.d.ts +8 -1
- package/lib/memory-agent.js +13 -0
- package/lib/memory-benchmark.d.ts +76 -0
- package/lib/memory-benchmark.js +170 -0
- package/lib/notify.d.ts +27 -0
- package/lib/notify.js +54 -0
- package/lib/planner.d.ts +1 -1
- package/lib/planner.js +7 -1
- package/lib/projection.d.ts +22 -0
- package/lib/projection.js +112 -0
- package/lib/recall.d.ts +85 -0
- package/lib/recall.js +142 -0
- package/lib/review-scheduler.js +3 -4
- package/lib/review.d.ts +1 -1
- package/lib/review.js +8 -3
- package/lib/service.js +14 -0
- package/lib/skill-render.js +2 -1
- package/lib/skillquality.d.ts +7 -2
- package/lib/skillquality.js +12 -3
- package/lib/tool.js +40 -0
- package/lib/types.d.ts +9 -11
- package/lib/types.js +14 -2
- package/package.json +7 -3
package/lib/recall.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Targeted memory recall (P1): the precise-reading counterpart of the cheap
|
|
3
|
+
* directory index.
|
|
4
|
+
*
|
|
5
|
+
* The injected directory view (`inject.ts`) shows one line per entry and the
|
|
6
|
+
* full `evolve_list` dumps whole stores; neither answers "give me everything
|
|
7
|
+
* about X". Recall fills that gap: filter by query/kind/scope/memory-type,
|
|
8
|
+
* rank with the same CJK BM25 machinery as injection, and return full
|
|
9
|
+
* content plus source, version, and staleness signals. Read-only — it never
|
|
10
|
+
* mutates a store.
|
|
11
|
+
*/
|
|
12
|
+
import type { EvolutionEngine } from "./service.js";
|
|
13
|
+
import type { HarnessScope, RefinementKind } from "./types.js";
|
|
14
|
+
/** Memory recall types (the `memoryType` taxonomy, not entry kinds). */
|
|
15
|
+
export declare const RECALL_MEMORY_TYPES: readonly ["user", "feedback", "project", "reference"];
|
|
16
|
+
export type RecallMemoryType = (typeof RECALL_MEMORY_TYPES)[number];
|
|
17
|
+
/** Default and maximum hits returned per recall call. */
|
|
18
|
+
export declare const DEFAULT_RECALL_LIMIT = 10;
|
|
19
|
+
export declare const MAX_RECALL_LIMIT = 50;
|
|
20
|
+
export interface RecallFilters {
|
|
21
|
+
/** Free-text relevance query; empty means "most recently updated first". */
|
|
22
|
+
query?: string;
|
|
23
|
+
/** Entry kinds to search; default is memory only. */
|
|
24
|
+
kinds?: RefinementKind[];
|
|
25
|
+
/** Stores to search; default is local + project + global. */
|
|
26
|
+
scopes?: HarnessScope[];
|
|
27
|
+
/** Memory-type filter (applies to memory entries only). */
|
|
28
|
+
memoryTypes?: RecallMemoryType[];
|
|
29
|
+
/** Maximum hits; clamped to 1..50, defaults to 10. */
|
|
30
|
+
limit?: number;
|
|
31
|
+
/** Include archived entries; default false. */
|
|
32
|
+
includeArchived?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** One recalled entry with full content and provenance. */
|
|
35
|
+
export interface RecallHit {
|
|
36
|
+
scope: HarnessScope;
|
|
37
|
+
kind: RefinementKind;
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
content: string;
|
|
41
|
+
path: string;
|
|
42
|
+
version: number;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
archived: boolean;
|
|
46
|
+
/** Memory-type hook for memory entries; undefined for other kinds. */
|
|
47
|
+
memoryType?: string;
|
|
48
|
+
/** Conflict-hint stamp left by the promotion guard, when present. */
|
|
49
|
+
staleHint?: string;
|
|
50
|
+
/** Session the entry was distilled from, when recorded. */
|
|
51
|
+
sourceSession?: string;
|
|
52
|
+
/** Source user-message seqs, when recorded. */
|
|
53
|
+
sourceSeqs?: number[];
|
|
54
|
+
}
|
|
55
|
+
export interface RecallResult {
|
|
56
|
+
hits: RecallHit[];
|
|
57
|
+
/** Skipped scopes with reasons (read-path honesty: never silently drop a store). */
|
|
58
|
+
notes: string[];
|
|
59
|
+
/** Candidates seen before ranking/truncation. */
|
|
60
|
+
totalCandidates: number;
|
|
61
|
+
}
|
|
62
|
+
export interface RecallContext {
|
|
63
|
+
/** Live session id for the local store; absent → local is skipped with a note. */
|
|
64
|
+
sessionId?: string | undefined;
|
|
65
|
+
/** Resolved project key; absent → project is skipped with a note. */
|
|
66
|
+
projectKey?: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Search the harness stores with mechanical filters and BM25 ranking.
|
|
70
|
+
*
|
|
71
|
+
* @param engine The evolution engine (read-only use).
|
|
72
|
+
* @param context Store identities for the session-scoped stores.
|
|
73
|
+
* @param filters Query/kind/scope/type/limit/archive filters.
|
|
74
|
+
* @returns Ranked hits plus a note for every scope that could not be read.
|
|
75
|
+
* @throws On an unknown kind, scope, or memory type — fail loud instead of
|
|
76
|
+
* silently narrowing the search.
|
|
77
|
+
*/
|
|
78
|
+
export declare function recallMemories(engine: EvolutionEngine, context: RecallContext, filters?: RecallFilters): RecallResult;
|
|
79
|
+
/**
|
|
80
|
+
* Render recall hits for a model or human reader: full content, version,
|
|
81
|
+
* source, and staleness on every hit; skipped scopes are always shown so
|
|
82
|
+
* "no results" can never hide an unread store.
|
|
83
|
+
*/
|
|
84
|
+
export declare function formatRecallResult(result: RecallResult, query?: string): string;
|
|
85
|
+
//# sourceMappingURL=recall.d.ts.map
|
package/lib/recall.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { buildRelevanceIndex, relevanceScore } from "./search.js";
|
|
2
|
+
import { CONFLICT_HINT_KEY } from "./types.js";
|
|
3
|
+
import { isArchived, isMemoryType, ARCHIVED_AT_KEY, MEMORY_TYPE_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY } from "./types.js";
|
|
4
|
+
/** Memory recall types (the `memoryType` taxonomy, not entry kinds). */
|
|
5
|
+
export const RECALL_MEMORY_TYPES = ["user", "feedback", "project", "reference"];
|
|
6
|
+
/** Entry kinds recall can filter on; memory is the default. */
|
|
7
|
+
const RECALL_KINDS = ["prompt", "memory", "skill", "subagent"];
|
|
8
|
+
/** Default and maximum hits returned per recall call. */
|
|
9
|
+
export const DEFAULT_RECALL_LIMIT = 10;
|
|
10
|
+
export const MAX_RECALL_LIMIT = 50;
|
|
11
|
+
/**
|
|
12
|
+
* Search the harness stores with mechanical filters and BM25 ranking.
|
|
13
|
+
*
|
|
14
|
+
* @param engine The evolution engine (read-only use).
|
|
15
|
+
* @param context Store identities for the session-scoped stores.
|
|
16
|
+
* @param filters Query/kind/scope/type/limit/archive filters.
|
|
17
|
+
* @returns Ranked hits plus a note for every scope that could not be read.
|
|
18
|
+
* @throws On an unknown kind, scope, or memory type — fail loud instead of
|
|
19
|
+
* silently narrowing the search.
|
|
20
|
+
*/
|
|
21
|
+
export function recallMemories(engine, context, filters = {}) {
|
|
22
|
+
const kinds = filters.kinds ?? ["memory"];
|
|
23
|
+
for (const kind of kinds) {
|
|
24
|
+
if (!RECALL_KINDS.includes(kind)) {
|
|
25
|
+
throw new Error(`evolve_recall: unknown kind "${kind}" (expected one of ${RECALL_KINDS.join(", ")})`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const scopes = filters.scopes ?? ["local", "project", "global"];
|
|
29
|
+
for (const scope of scopes) {
|
|
30
|
+
if (scope !== "local" && scope !== "project" && scope !== "global") {
|
|
31
|
+
throw new Error(`evolve_recall: unknown scope "${scope}" (expected local, project, or global)`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const memoryTypes = filters.memoryTypes ?? [];
|
|
35
|
+
for (const memoryType of memoryTypes) {
|
|
36
|
+
if (!RECALL_MEMORY_TYPES.includes(memoryType)) {
|
|
37
|
+
throw new Error(`evolve_recall: unknown memoryType "${memoryType}" (expected one of ${RECALL_MEMORY_TYPES.join(", ")})`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const limit = typeof filters.limit === "number" && Number.isFinite(filters.limit) && filters.limit > 0
|
|
41
|
+
? Math.min(Math.floor(filters.limit), MAX_RECALL_LIMIT)
|
|
42
|
+
: DEFAULT_RECALL_LIMIT;
|
|
43
|
+
const query = filters.query?.trim() ?? "";
|
|
44
|
+
const candidates = [];
|
|
45
|
+
const notes = [];
|
|
46
|
+
for (const scope of scopes) {
|
|
47
|
+
const storeId = scope === "local" ? context.sessionId : scope === "project" ? context.projectKey : undefined;
|
|
48
|
+
if (scope !== "global" && storeId === undefined) {
|
|
49
|
+
notes.push(`${scope} store skipped: ${scope === "local" ? "no live session id" : "no project key for this session"}`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let state;
|
|
53
|
+
try {
|
|
54
|
+
state = engine.load(scope, storeId);
|
|
55
|
+
}
|
|
56
|
+
catch (cause) {
|
|
57
|
+
notes.push(`${scope} store unreadable: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const kind of kinds) {
|
|
61
|
+
for (const entry of Object.values(state.entries[kind])) {
|
|
62
|
+
if (!filters.includeArchived && isArchived(entry))
|
|
63
|
+
continue;
|
|
64
|
+
if (kind === "memory" && memoryTypes.length > 0) {
|
|
65
|
+
const type = entry.metadata[MEMORY_TYPE_KEY];
|
|
66
|
+
if (typeof type !== "string" || !memoryTypes.includes(type))
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
candidates.push(entry);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const ranked = query.length > 0 ? rankByRelevance(candidates, query) : [...candidates].sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0));
|
|
74
|
+
return { hits: ranked.slice(0, limit).map(toRecallHit), notes, totalCandidates: candidates.length };
|
|
75
|
+
}
|
|
76
|
+
function rankByRelevance(candidates, query) {
|
|
77
|
+
const index = buildRelevanceIndex(candidates);
|
|
78
|
+
const normalized = query.trim().toLowerCase();
|
|
79
|
+
return candidates
|
|
80
|
+
.map((entry) => {
|
|
81
|
+
let score = relevanceScore(index, entry, query);
|
|
82
|
+
// Exact scope:id addressing wins over fuzzy relevance (the
|
|
83
|
+
// memory-agent manifest search precedent).
|
|
84
|
+
if (`${entry.scope}:${entry.id}`.toLowerCase() === normalized)
|
|
85
|
+
score += 1000;
|
|
86
|
+
return { entry, score };
|
|
87
|
+
})
|
|
88
|
+
.filter((hit) => hit.score > 0)
|
|
89
|
+
.sort((a, b) => b.score - a.score || (a.entry.updated_at < b.entry.updated_at ? 1 : -1))
|
|
90
|
+
.map((hit) => hit.entry);
|
|
91
|
+
}
|
|
92
|
+
function toRecallHit(entry) {
|
|
93
|
+
const memoryType = entry.kind === "memory" && isMemoryType(entry.metadata[MEMORY_TYPE_KEY])
|
|
94
|
+
? String(entry.metadata[MEMORY_TYPE_KEY])
|
|
95
|
+
: undefined;
|
|
96
|
+
const staleHint = typeof entry.metadata[CONFLICT_HINT_KEY] === "string" && entry.metadata[CONFLICT_HINT_KEY].length > 0
|
|
97
|
+
? String(entry.metadata[CONFLICT_HINT_KEY])
|
|
98
|
+
: undefined;
|
|
99
|
+
const sourceSession = typeof entry.metadata[SOURCE_SESSION_KEY] === "string" ? String(entry.metadata[SOURCE_SESSION_KEY]) : undefined;
|
|
100
|
+
const rawSeqs = entry.metadata[SOURCE_SEQS_KEY];
|
|
101
|
+
const sourceSeqs = Array.isArray(rawSeqs) && rawSeqs.every((seq) => typeof seq === "number") ? rawSeqs : undefined;
|
|
102
|
+
return {
|
|
103
|
+
scope: entry.scope,
|
|
104
|
+
kind: entry.kind,
|
|
105
|
+
id: entry.id,
|
|
106
|
+
title: entry.title,
|
|
107
|
+
content: entry.content,
|
|
108
|
+
path: entry.path,
|
|
109
|
+
version: entry.version,
|
|
110
|
+
createdAt: entry.created_at,
|
|
111
|
+
updatedAt: entry.updated_at,
|
|
112
|
+
archived: isArchived(entry),
|
|
113
|
+
...(memoryType !== undefined ? { memoryType } : {}),
|
|
114
|
+
...(staleHint !== undefined ? { staleHint } : {}),
|
|
115
|
+
...(sourceSession !== undefined ? { sourceSession } : {}),
|
|
116
|
+
...(sourceSeqs !== undefined ? { sourceSeqs } : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Render recall hits for a model or human reader: full content, version,
|
|
121
|
+
* source, and staleness on every hit; skipped scopes are always shown so
|
|
122
|
+
* "no results" can never hide an unread store.
|
|
123
|
+
*/
|
|
124
|
+
export function formatRecallResult(result, query) {
|
|
125
|
+
const lines = [`recall: ${result.hits.length} hit(s)${query?.trim() ? ` for "${query.trim()}"` : ""} (${result.totalCandidates} candidate(s))`];
|
|
126
|
+
for (const hit of result.hits) {
|
|
127
|
+
const type = hit.memoryType !== undefined ? ` · type=${hit.memoryType}` : "";
|
|
128
|
+
const archived = hit.archived ? " · archived" : "";
|
|
129
|
+
const stale = hit.staleHint !== undefined ? ` · conflict-hint=${hit.staleHint}` : "";
|
|
130
|
+
const source = hit.sourceSession !== undefined ? ` · src=${hit.sourceSession}${hit.sourceSeqs ? `:${hit.sourceSeqs.join(",")}` : ""}` : "";
|
|
131
|
+
lines.push(`- [${hit.scope}:${hit.kind}:${hit.id}] ${hit.title} (v${hit.version}${type}${archived}${stale}${source})`);
|
|
132
|
+
lines.push(` ${hit.content}`);
|
|
133
|
+
}
|
|
134
|
+
for (const note of result.notes) {
|
|
135
|
+
lines.push(`note: ${note}`);
|
|
136
|
+
}
|
|
137
|
+
if (result.hits.length === 0) {
|
|
138
|
+
lines.push(`(${ARCHIVED_AT_KEY} entries are hidden unless includeArchived is set)`);
|
|
139
|
+
}
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=recall.js.map
|
package/lib/review-scheduler.js
CHANGED
|
@@ -68,10 +68,9 @@ export function createReviewScheduler(execute, cursorOf) {
|
|
|
68
68
|
bestCursor = candidate;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
71
|
+
// bestIndex is always set here: the error purge above leaves only
|
|
72
|
+
// acquired snapshots in `settled` (SnapshotAcquisition has exactly
|
|
73
|
+
// two variants), so the first settled entry takes the slot.
|
|
75
74
|
const chosen = pending[bestIndex];
|
|
76
75
|
pending.splice(bestIndex, 1);
|
|
77
76
|
for (let index = pending.length - 1; index >= 0; index -= 1) {
|
package/lib/review.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface ReviewOptions {
|
|
|
44
44
|
/** Direct-call token ledger destination; the review phase is fixed by this module. */
|
|
45
45
|
tokenUsage?: TokenUsageTarget;
|
|
46
46
|
}
|
|
47
|
-
export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should continue the non-memory /evolve path.\nA dedicated memory extraction agent has already processed the same snapshot\nwith a frozen memory manifest, so do NOT approve solely to create, update,\narchive, or delete a memory entry. Decline with a rationale that the memory\nphase owns that evidence. For the remaining kinds, approve repeated
|
|
47
|
+
export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should continue the non-memory /evolve path.\nA dedicated memory extraction agent has already processed the same snapshot\nwith a frozen memory manifest, so do NOT approve solely to create, update,\narchive, or delete a memory entry. Decline with a rationale that the memory\nphase owns that evidence. For the remaining kinds, approve ONLY when the\ntrajectory shows repeated evidence: the same failure, tactic, delegation\nrole, or behavioral policy recurs at least twice across turns. A single\none-off interaction defaults to decline no matter how useful it looks for\nfuture turns \u2014 one data point is never enough to generalize. When approving,\ncite the repeated spans in the instructions so the planner can ground its\nedits.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nSkill-related trajectories (the evidence concerns creating or improving a\nskill entry) are judged against the DSH skill quality standard (skill-audit\ndimensions: frontmatter routing, the 7 structural features, paragraph\nskeleton, no duplication of the official 11 skills or covered skills).\nApprove only when the trajectory shows a REAL trigger scenario and the\nresulting skill would meet the standard; otherwise decline and say in the\nrationale what must improve \u2014 drafting follows skill-creator, and the\nplanner receives the standard as its <skill_quality_standard> block.\n\nRepeated multi-step workflows (session start/end routines, recurring\nwrap-up or handoff procedures) are a valid refine target: approve with\ninstructions telling the planner to propose a guidance skill (kind=skill,\nskill_kind=guidance \u2014 a SKILL.md document, no python reference). Only\npropose when the same workflow recurs in the trajectory \u2014 never for\none-off flows. Auto-created skills are always offered to the user for a\ndecision before they land; the gate never writes a skill silently.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
|
|
48
48
|
/** Parse the gate's JSON reply. */
|
|
49
49
|
export declare function parseAutoRefineReview(text: string): AutoRefineReview;
|
|
50
50
|
/** Serialize surface events to bounded role-prefixed text. */
|
package/lib/review.js
CHANGED
|
@@ -10,8 +10,13 @@ Decide whether this checkpoint should continue the non-memory /evolve path.
|
|
|
10
10
|
A dedicated memory extraction agent has already processed the same snapshot
|
|
11
11
|
with a frozen memory manifest, so do NOT approve solely to create, update,
|
|
12
12
|
archive, or delete a memory entry. Decline with a rationale that the memory
|
|
13
|
-
phase owns that evidence. For the remaining kinds, approve
|
|
14
|
-
|
|
13
|
+
phase owns that evidence. For the remaining kinds, approve ONLY when the
|
|
14
|
+
trajectory shows repeated evidence: the same failure, tactic, delegation
|
|
15
|
+
role, or behavioral policy recurs at least twice across turns. A single
|
|
16
|
+
one-off interaction defaults to decline no matter how useful it looks for
|
|
17
|
+
future turns — one data point is never enough to generalize. When approving,
|
|
18
|
+
cite the repeated spans in the instructions so the planner can ground its
|
|
19
|
+
edits.
|
|
15
20
|
|
|
16
21
|
The current harness state below includes GLOBAL entries (scope=global) plus
|
|
17
22
|
this session's local entries (scope=local). When a topic is already covered
|
|
@@ -119,7 +124,7 @@ export async function reviewAutoRefine(ctx, options) {
|
|
|
119
124
|
`<current_harness_state>\n${formatHarnessStateForPrompt(state)}\n</current_harness_state>`,
|
|
120
125
|
`<refinement_history>\n${historyForPrompt(history)}\n</refinement_history>`,
|
|
121
126
|
conversationBlock,
|
|
122
|
-
"Return shouldRefine=true when the trajectory
|
|
127
|
+
"Return shouldRefine=true only when the trajectory shows repeated evidence (>=2 occurrences) useful to this session's future turns. A single one-off interaction defaults to shouldRefine=false. Prefer local edits; do not ask for global refinement here.",
|
|
123
128
|
]
|
|
124
129
|
.filter(Boolean)
|
|
125
130
|
.join("\n\n");
|
package/lib/service.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { rollbackProposal } from "./rollback.js";
|
|
5
5
|
import { loadHarnessState, saveHarnessState } from "./state.js";
|
|
6
6
|
import { appendResult, loadResults, pruneJsonlFile, pruneSnapshots, resolveHistoryRetention, snapshotBefore, storePaths } from "./store.js";
|
|
7
|
+
import { materializeMemoryProjection } from "./projection.js";
|
|
7
8
|
import { CONFLICT_BLOCK_SCORE, CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry, secretLeakReason } from "./promotion.js";
|
|
8
9
|
/** A post-commit apply failure that still carries the durable applied result. */
|
|
9
10
|
export class EvolutionApplyPostCommitError extends Error {
|
|
@@ -137,6 +138,19 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
|
|
|
137
138
|
catch {
|
|
138
139
|
// ignored — the next apply retries
|
|
139
140
|
}
|
|
141
|
+
// Readable Markdown projection (P1): rewritten from the post-apply
|
|
142
|
+
// state whenever a memory edit landed, so rollback and archive stay
|
|
143
|
+
// in sync through this same apply path. The model has no writer for
|
|
144
|
+
// these files — only this engine entry point materializes them.
|
|
145
|
+
if (result.appliedEdits.some((edit) => edit.applied && edit.kind === "memory")) {
|
|
146
|
+
try {
|
|
147
|
+
materializeMemoryProjection(paths.stateDir, state);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// ignored — the projection is derived (JSON stays the source
|
|
151
|
+
// of truth) and the next memory apply retries
|
|
152
|
+
}
|
|
153
|
+
}
|
|
140
154
|
try {
|
|
141
155
|
hooks.onApplied?.(result);
|
|
142
156
|
}
|
package/lib/skill-render.js
CHANGED
|
@@ -57,7 +57,8 @@ export function renderSkillMarkdown(entry) {
|
|
|
57
57
|
lines.push(`- ${key}: ${JSON.stringify(value)}`);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
const args = entry.arguments;
|
|
61
|
+
if (args && typeof args === "object" && Object.keys(args).length > 0) {
|
|
61
62
|
lines.push("", "## Arguments", "```json", JSON.stringify(entry.arguments, null, 2), "```");
|
|
62
63
|
}
|
|
63
64
|
return `${lines.join("\n").trimEnd()}\n`;
|
package/lib/skillquality.d.ts
CHANGED
|
@@ -54,8 +54,11 @@ export declare function validateRenderedSkillMarkdown(markdown: string): string[
|
|
|
54
54
|
* parsed instead of the generated one (the platform reads the FIRST
|
|
55
55
|
* closing `---`), so the file could be ignored or routed wrongly;
|
|
56
56
|
* - resource references (`references/…`, `scripts/…`) must be skill-local
|
|
57
|
-
* relative paths —
|
|
58
|
-
*
|
|
57
|
+
* relative paths — an embedded `/../` after the category escapes the skill
|
|
58
|
+
* directory and is rejected. A leading `../` instead addresses a sibling
|
|
59
|
+
* skill (cross-skill interlink, e.g. `../skill-creator/…`) and is allowed:
|
|
60
|
+
* it is not dangling-checked since cross-skill targets live outside this
|
|
61
|
+
* entry's shipped files (see skillResourceRefs).
|
|
59
62
|
* Returns human-readable problems; an empty array means the content is
|
|
60
63
|
* mechanically acceptable.
|
|
61
64
|
*/
|
|
@@ -73,6 +76,8 @@ export declare function validateRenderedSkill(entry: HarnessEntry): string[];
|
|
|
73
76
|
* targets starting with the category, plus backticked/prose paths carrying
|
|
74
77
|
* a filename extension. Used after materialization to warn about dangling
|
|
75
78
|
* references (a body referencing a resource the entry never ships).
|
|
79
|
+
* Leading-`../` cross-skill interlinks are skipped, not warned: they resolve
|
|
80
|
+
* against sibling skill directories, never against shipped files.
|
|
76
81
|
*/
|
|
77
82
|
export declare function skillResourceRefs(content: string): string[];
|
|
78
83
|
/** Kebab-case name under which the entry materializes (exported for diagnostics). */
|
package/lib/skillquality.js
CHANGED
|
@@ -244,8 +244,11 @@ export function validateRenderedSkillMarkdown(markdown) {
|
|
|
244
244
|
* parsed instead of the generated one (the platform reads the FIRST
|
|
245
245
|
* closing `---`), so the file could be ignored or routed wrongly;
|
|
246
246
|
* - resource references (`references/…`, `scripts/…`) must be skill-local
|
|
247
|
-
* relative paths —
|
|
248
|
-
*
|
|
247
|
+
* relative paths — an embedded `/../` after the category escapes the skill
|
|
248
|
+
* directory and is rejected. A leading `../` instead addresses a sibling
|
|
249
|
+
* skill (cross-skill interlink, e.g. `../skill-creator/…`) and is allowed:
|
|
250
|
+
* it is not dangling-checked since cross-skill targets live outside this
|
|
251
|
+
* entry's shipped files (see skillResourceRefs).
|
|
249
252
|
* Returns human-readable problems; an empty array means the content is
|
|
250
253
|
* mechanically acceptable.
|
|
251
254
|
*/
|
|
@@ -260,8 +263,12 @@ export function validateSkillEntryContent(content) {
|
|
|
260
263
|
problems.push("skill content must not start with a `---` frontmatter block (the materializer generates frontmatter from id/title; a body-level `---` would shadow it and the platform could IGNORE the file)");
|
|
261
264
|
}
|
|
262
265
|
for (const match of trimmed.matchAll(/(?<![\w])(references|scripts)\/[^\s)]+/g)) {
|
|
266
|
+
// match[0] always starts with `references/` or `scripts/` (the regex
|
|
267
|
+
// anchors it there), so only an embedded `/../` can escape the skill
|
|
268
|
+
// directory — the ../-prefix, absolute-path, and drive-letter
|
|
269
|
+
// spellings can never match and are intentionally not checked.
|
|
263
270
|
const ref = match[0] ?? "";
|
|
264
|
-
if (ref.
|
|
271
|
+
if (ref.includes("/../")) {
|
|
265
272
|
problems.push(`skill content resource reference escapes the skill directory: ${ref}`);
|
|
266
273
|
}
|
|
267
274
|
}
|
|
@@ -282,6 +289,8 @@ export function validateRenderedSkill(entry) {
|
|
|
282
289
|
* targets starting with the category, plus backticked/prose paths carrying
|
|
283
290
|
* a filename extension. Used after materialization to warn about dangling
|
|
284
291
|
* references (a body referencing a resource the entry never ships).
|
|
292
|
+
* Leading-`../` cross-skill interlinks are skipped, not warned: they resolve
|
|
293
|
+
* against sibling skill directories, never against shipped files.
|
|
285
294
|
*/
|
|
286
295
|
export function skillResourceRefs(content) {
|
|
287
296
|
const refs = new Set();
|
package/lib/tool.js
CHANGED
|
@@ -5,6 +5,7 @@ import { formatHarnessStateForPrompt } from "./render.js";
|
|
|
5
5
|
import { requireGlobalApproval } from "./approval.js";
|
|
6
6
|
import { CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry } from "./promotion.js";
|
|
7
7
|
import { entrySourceOf } from "./source.js";
|
|
8
|
+
import { formatRecallResult, recallMemories } from "./recall.js";
|
|
8
9
|
import { getUsageCount, loadUsage } from "./usage.js";
|
|
9
10
|
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
10
11
|
import { DEFAULT_REVIEWS_RETAIN } from "./store.js";
|
|
@@ -115,6 +116,45 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
115
116
|
return textResult(text);
|
|
116
117
|
},
|
|
117
118
|
}));
|
|
119
|
+
ctx.tools.register(defineTool({
|
|
120
|
+
name: "evolve_recall",
|
|
121
|
+
description: "Targeted recall over the harness stores: filter memories (or other kinds) by query, kind, scope, and memory type, and read back full content with version, source, and staleness signals. Read-only — it never mutates a store. Prefer this over evolve_list when looking for something specific.",
|
|
122
|
+
parameters: {
|
|
123
|
+
query: { type: "string", description: "Free-text relevance query. Empty means most-recently-updated first." },
|
|
124
|
+
kinds: {
|
|
125
|
+
type: "array",
|
|
126
|
+
items: { type: "string" },
|
|
127
|
+
description: "Entry kinds to search (default memory only).",
|
|
128
|
+
},
|
|
129
|
+
scopes: {
|
|
130
|
+
type: "array",
|
|
131
|
+
items: { type: "string" },
|
|
132
|
+
description: "Stores to search (default local, project, and global).",
|
|
133
|
+
},
|
|
134
|
+
memoryTypes: {
|
|
135
|
+
type: "array",
|
|
136
|
+
items: { type: "string" },
|
|
137
|
+
description: "Memory-type filter: user, feedback, project, reference (memory entries only).",
|
|
138
|
+
},
|
|
139
|
+
limit: { type: "number", description: "Maximum hits (default 10, at most 50)." },
|
|
140
|
+
includeArchived: { type: "boolean", description: "Include archived entries (default false)." },
|
|
141
|
+
},
|
|
142
|
+
output: {
|
|
143
|
+
schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
|
|
144
|
+
render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
|
|
145
|
+
},
|
|
146
|
+
execute: async (args, exec) => {
|
|
147
|
+
const result = recallMemories(engine, { sessionId: sessionIdOf(exec), projectKey: projectKeyOf(exec.agent) ?? undefined }, {
|
|
148
|
+
...(typeof args.query === "string" ? { query: args.query } : {}),
|
|
149
|
+
...(Array.isArray(args.kinds) ? { kinds: args.kinds } : {}),
|
|
150
|
+
...(Array.isArray(args.scopes) ? { scopes: args.scopes } : {}),
|
|
151
|
+
...(Array.isArray(args.memoryTypes) ? { memoryTypes: args.memoryTypes } : {}),
|
|
152
|
+
...(typeof args.limit === "number" ? { limit: args.limit } : {}),
|
|
153
|
+
...(typeof args.includeArchived === "boolean" ? { includeArchived: args.includeArchived } : {}),
|
|
154
|
+
});
|
|
155
|
+
return textResult(formatRecallResult(result, typeof args.query === "string" ? args.query : undefined));
|
|
156
|
+
},
|
|
157
|
+
}));
|
|
118
158
|
ctx.tools.register(defineTool({
|
|
119
159
|
name: "evolve_add",
|
|
120
160
|
description: "Create one harness entry (prompt/memory/skill/subagent). Executable skills require reference {type:python, import, callable} and an arguments contract; guidance skills (skill_kind=guidance) are SKILL.md documents — recurring multi-step workflows — and must NOT carry a reference. Snapshot, version, and history are handled automatically.",
|
package/lib/types.d.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type model for the continual-evolution harness state.
|
|
3
|
-
*
|
|
4
|
-
* Design provenance: the state model follows the continual-harness design
|
|
5
|
-
* validated in prime-agent's `/refine` subsystem (MIT): versioned entries
|
|
6
|
-
* keyed by kind, an append-only refinement history carrying an evidence
|
|
7
|
-
* trail, atomic persistence, and deterministic inverse-op rollback. The
|
|
8
|
-
* code here is an original implementation, written for the DeepSeek Harness
|
|
9
|
-
* plugin surface.
|
|
10
|
-
*/
|
|
11
1
|
/** What a harness entry can be. */
|
|
12
2
|
export type RefinementKind = "prompt" | "memory" | "skill" | "subagent";
|
|
13
3
|
/**
|
|
@@ -201,7 +191,15 @@ export interface PythonReference {
|
|
|
201
191
|
callable?: string;
|
|
202
192
|
call_pattern?: string;
|
|
203
193
|
}
|
|
204
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* A stable id derived from a title: ASCII runs become snake_case (unchanged
|
|
196
|
+
* legacy behavior). Titles with no ASCII content at all (e.g. pure Chinese)
|
|
197
|
+
* used to collapse onto the bare fallback, so distinct memories shared one
|
|
198
|
+
* id and every later create failed as "already exists". They now take
|
|
199
|
+
* `<fallback>_<sha256-8>` instead: distinct titles get distinct ids while
|
|
200
|
+
* identical titles still dedupe onto the same id. The fallback itself is
|
|
201
|
+
* assumed id-safe (callers pass entry kinds).
|
|
202
|
+
*/
|
|
205
203
|
export declare function slug(raw: string, fallback: string): string;
|
|
206
204
|
/** Fresh empty state with the current schema version. */
|
|
207
205
|
export declare function emptyHarnessState(): HarnessState;
|
package/lib/types.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* code here is an original implementation, written for the DeepSeek Harness
|
|
9
9
|
* plugin surface.
|
|
10
10
|
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
11
12
|
/**
|
|
12
13
|
* Memory-entry shape discipline (one fact per entry, typed like ZCode's
|
|
13
14
|
* memory frontmatter): every `memory` create must stamp one of these in
|
|
@@ -85,7 +86,15 @@ export function isArchived(entry) {
|
|
|
85
86
|
const value = entry.metadata[ARCHIVED_AT_KEY];
|
|
86
87
|
return typeof value === "string" && value.length > 0;
|
|
87
88
|
}
|
|
88
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* A stable id derived from a title: ASCII runs become snake_case (unchanged
|
|
91
|
+
* legacy behavior). Titles with no ASCII content at all (e.g. pure Chinese)
|
|
92
|
+
* used to collapse onto the bare fallback, so distinct memories shared one
|
|
93
|
+
* id and every later create failed as "already exists". They now take
|
|
94
|
+
* `<fallback>_<sha256-8>` instead: distinct titles get distinct ids while
|
|
95
|
+
* identical titles still dedupe onto the same id. The fallback itself is
|
|
96
|
+
* assumed id-safe (callers pass entry kinds).
|
|
97
|
+
*/
|
|
89
98
|
export function slug(raw, fallback) {
|
|
90
99
|
const normalized = raw
|
|
91
100
|
.trim()
|
|
@@ -93,7 +102,10 @@ export function slug(raw, fallback) {
|
|
|
93
102
|
.replace(/[^a-z0-9]+/g, "_")
|
|
94
103
|
.replace(/^_+|_+$/g, "")
|
|
95
104
|
.slice(0, 80);
|
|
96
|
-
|
|
105
|
+
if (normalized)
|
|
106
|
+
return normalized;
|
|
107
|
+
const digest = createHash("sha256").update(raw, "utf8").digest("hex").slice(0, 8);
|
|
108
|
+
return `${fallback}_${digest}`;
|
|
97
109
|
}
|
|
98
110
|
/** Fresh empty state with the current schema version. */
|
|
99
111
|
export function emptyHarnessState() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,12 +48,15 @@
|
|
|
48
48
|
"build": "tsc -p tsconfig.json",
|
|
49
49
|
"prepare": "tsc -p tsconfig.json",
|
|
50
50
|
"dev": "tsc -p tsconfig.json --watch",
|
|
51
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.scripts.json --noEmit",
|
|
52
52
|
"test": "vitest run",
|
|
53
53
|
"test:coverage": "vitest run --coverage",
|
|
54
|
+
"coverage:gaps": "tsx scripts/coverage-gaps.ts",
|
|
54
55
|
"test:watch": "vitest",
|
|
55
56
|
"lint": "oxlint src test",
|
|
56
|
-
"clean": "rm -rf lib"
|
|
57
|
+
"clean": "rm -rf lib",
|
|
58
|
+
"check:docs": "tsx scripts/run-gates.ts docs",
|
|
59
|
+
"check:governance": "tsx scripts/verify-governance.ts"
|
|
57
60
|
},
|
|
58
61
|
"peerDependencies": {
|
|
59
62
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -73,6 +76,7 @@
|
|
|
73
76
|
"@types/node": "^22.10.0",
|
|
74
77
|
"@vitest/coverage-v8": "^3.2.0",
|
|
75
78
|
"oxlint": "^0.16.0",
|
|
79
|
+
"tsx": "^4.23.15",
|
|
76
80
|
"typescript": "^5.9.0",
|
|
77
81
|
"vitest": "^3.2.0"
|
|
78
82
|
}
|