dsh-continual-evolve 0.7.5 → 0.8.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 +32 -21
- package/README.zh.md +16 -11
- package/lib/auto.d.ts +31 -2
- package/lib/auto.js +124 -6
- package/lib/command.d.ts +4 -3
- package/lib/command.js +100 -16
- package/lib/index.d.ts +12 -4
- package/lib/index.js +38 -40
- package/lib/memory-agent.d.ts +1 -1
- package/lib/memory-agent.js +1 -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 +53 -0
- 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/service.js +14 -0
- package/lib/tool.js +40 -0
- package/package.json +1 -1
package/lib/notify.js
CHANGED
|
@@ -40,4 +40,57 @@ export function notifyAutoReview(ctx, agent, result, turnsSinceLastReview) {
|
|
|
40
40
|
.warn(`auto-review notice failed for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Compose the user-visible memory receipt from applied refinement batches
|
|
45
|
+
* (never model text): every landed memory edit with its rollback command.
|
|
46
|
+
* No-op and declined-only outcomes render one quiet line each — a foreground
|
|
47
|
+
* notice for zero landed edits would be nagging, so callers only queue
|
|
48
|
+
* follow-ups for applied outcomes.
|
|
49
|
+
*/
|
|
50
|
+
export function buildMemoryReceipt(receipt) {
|
|
51
|
+
const seconds = (receipt.durationMs / 1000).toFixed(1);
|
|
52
|
+
const stats = `${receipt.turns} 轮推理 / ${receipt.searches} 次检索 / ${seconds}s`;
|
|
53
|
+
if (receipt.outcome === "noop") {
|
|
54
|
+
return `🧠 记忆提取:本轮无可沉淀的持久事实(no-op,${stats}),未写入任何条目。`;
|
|
55
|
+
}
|
|
56
|
+
if (receipt.outcome === "declined" || (receipt.results ?? []).length === 0) {
|
|
57
|
+
const scopes = (receipt.declinedScopes ?? []).join("、") || "持久化作用域";
|
|
58
|
+
return `🧠 记忆提取:${scopes}的写入未获批准,已跳过(${stats}),未写入任何条目。`;
|
|
59
|
+
}
|
|
60
|
+
const results = receipt.results ?? [];
|
|
61
|
+
const applied = results.flatMap((result) => result.appliedEdits.filter((edit) => edit.applied));
|
|
62
|
+
const lines = applied.slice(0, 12).map((edit) => `- 记忆「${edit.title ?? edit.id}」(${edit.id})`);
|
|
63
|
+
if (applied.length > 12)
|
|
64
|
+
lines.push(`- …另有 ${applied.length - 12} 条(见 /evolve history)`);
|
|
65
|
+
const declined = (receipt.declinedScopes ?? []).length > 0 ? `\n另有作用域${(receipt.declinedScopes ?? []).join("、")}未经批准(已跳过)。` : "";
|
|
66
|
+
const rollbacks = [...new Set(results.map((result) => result.id))].map((id) => `/evolve rollback ${id}`).join(";");
|
|
67
|
+
return [
|
|
68
|
+
`🧠 记忆提取:本轮沉淀 ${applied.length} 条记忆(${results.length} 个作用域批次,${stats}):`,
|
|
69
|
+
lines.length > 0 ? lines.join("\n") : "(无条目成功应用)",
|
|
70
|
+
declined,
|
|
71
|
+
`回看:/evolve recall <关键词>;回滚:${rollbacks}`,
|
|
72
|
+
"请用一句话简短确认即可,不要调用任何工具。",
|
|
73
|
+
]
|
|
74
|
+
.filter((part) => part !== "")
|
|
75
|
+
.join("\n");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Queue the memory receipt follow-up. Same containment as the gate notice:
|
|
79
|
+
* notification failure only logs. Callers gate on applied outcomes — no-op
|
|
80
|
+
* and declined-only runs stay audit-only (reviews.jsonl) and never wake
|
|
81
|
+
* the agent.
|
|
82
|
+
*/
|
|
83
|
+
export function notifyMemoryExtraction(ctx, agent, receipt) {
|
|
84
|
+
try {
|
|
85
|
+
agent.followup(createUserMessage({
|
|
86
|
+
content: [{ type: "text", text: buildMemoryReceipt(receipt) }],
|
|
87
|
+
source: EVOLVE_MESSAGE_SOURCE,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
catch (cause) {
|
|
91
|
+
ctx
|
|
92
|
+
.logger("continual-evolve")
|
|
93
|
+
.warn(`memory receipt notice failed for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
43
96
|
//# sourceMappingURL=notify.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HarnessState } from "./types.js";
|
|
2
|
+
/** Index file name at the store root. */
|
|
3
|
+
export declare const MEMORY_INDEX_FILE = "MEMORY.md";
|
|
4
|
+
/** Per-fact file directory inside the store. */
|
|
5
|
+
export declare const MEMORY_FACTS_DIR = "memory";
|
|
6
|
+
/** Map a memory id to a flat filename (`:` is legal on disk but hostile to tools). */
|
|
7
|
+
export declare function memoryFactFilename(id: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Rewrite the whole projection from the post-apply state: index, one file
|
|
10
|
+
* per memory entry (including archived ones, flagged), and deletion of
|
|
11
|
+
* files whose ids no longer exist. Deterministic — the same state always
|
|
12
|
+
* produces the same files.
|
|
13
|
+
*
|
|
14
|
+
* @param stateDir The store directory holding harness_state.json.
|
|
15
|
+
* @param state The freshly persisted state (post-apply, post-rollback).
|
|
16
|
+
*/
|
|
17
|
+
export declare function materializeMemoryProjection(stateDir: string, state: HarnessState): void;
|
|
18
|
+
/** Read back one projected fact file (round-trip helper for tests and tools). */
|
|
19
|
+
export declare function readMemoryFact(factsDir: string, filename: string): string;
|
|
20
|
+
/** True when the store directory carries a projection from a previous apply. */
|
|
21
|
+
export declare function hasMemoryProjection(stateDir: string): boolean;
|
|
22
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readable Markdown projection (P1): a ZCode-style `MEMORY.md` index plus
|
|
3
|
+
* one file per memory fact, materialized from the JSON store.
|
|
4
|
+
*
|
|
5
|
+
* The JSON harness state stays the single source of truth (versions,
|
|
6
|
+
* rollback, audit). The projection is a derived, human-trustable view:
|
|
7
|
+
* it is rewritten from the post-apply state inside the engine, so the
|
|
8
|
+
* model can never write it directly and rollback/archive stay in sync.
|
|
9
|
+
*
|
|
10
|
+
* Layout per store directory:
|
|
11
|
+
*
|
|
12
|
+
* <stateDir>/MEMORY.md one line per memory entry
|
|
13
|
+
* <stateDir>/memory/<file>.md one fact per file with frontmatter
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { isArchived, isMemoryType, MEMORY_TYPE_KEY } from "./types.js";
|
|
18
|
+
/** Index file name at the store root. */
|
|
19
|
+
export const MEMORY_INDEX_FILE = "MEMORY.md";
|
|
20
|
+
/** Per-fact file directory inside the store. */
|
|
21
|
+
export const MEMORY_FACTS_DIR = "memory";
|
|
22
|
+
/** Map a memory id to a flat filename (`:` is legal on disk but hostile to tools). */
|
|
23
|
+
export function memoryFactFilename(id) {
|
|
24
|
+
return `${id.replace(/:/g, "_")}.md`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Rewrite the whole projection from the post-apply state: index, one file
|
|
28
|
+
* per memory entry (including archived ones, flagged), and deletion of
|
|
29
|
+
* files whose ids no longer exist. Deterministic — the same state always
|
|
30
|
+
* produces the same files.
|
|
31
|
+
*
|
|
32
|
+
* @param stateDir The store directory holding harness_state.json.
|
|
33
|
+
* @param state The freshly persisted state (post-apply, post-rollback).
|
|
34
|
+
*/
|
|
35
|
+
export function materializeMemoryProjection(stateDir, state) {
|
|
36
|
+
const entries = Object.values(state.entries.memory).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
37
|
+
const factsDir = join(stateDir, MEMORY_FACTS_DIR);
|
|
38
|
+
mkdirSync(factsDir, { recursive: true });
|
|
39
|
+
const expected = new Map();
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
let filename = memoryFactFilename(entry.id);
|
|
42
|
+
let suffix = 1;
|
|
43
|
+
while (expected.has(filename)) {
|
|
44
|
+
suffix += 1;
|
|
45
|
+
filename = `${memoryFactFilename(entry.id).replace(/\.md$/, "")}~${suffix}.md`;
|
|
46
|
+
}
|
|
47
|
+
expected.set(filename, entry);
|
|
48
|
+
}
|
|
49
|
+
for (const [filename, entry] of expected) {
|
|
50
|
+
writeFileSync(join(factsDir, filename), renderMemoryFact(entry), { encoding: "utf8", mode: 0o600 });
|
|
51
|
+
}
|
|
52
|
+
// Sweep facts whose entries vanished (delete); archived entries keep
|
|
53
|
+
// their file with archived:true frontmatter.
|
|
54
|
+
let existing;
|
|
55
|
+
try {
|
|
56
|
+
existing = readdirSync(factsDir).filter((file) => file.endsWith(".md"));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
existing = [];
|
|
60
|
+
}
|
|
61
|
+
for (const file of existing) {
|
|
62
|
+
if (!expected.has(file)) {
|
|
63
|
+
try {
|
|
64
|
+
unlinkSync(join(factsDir, file));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// A stale fact file must not break the apply path; the next
|
|
68
|
+
// materialization retries the sweep.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
writeFileSync(join(stateDir, MEMORY_INDEX_FILE), renderMemoryIndex([...expected]), { encoding: "utf8", mode: 0o600 });
|
|
73
|
+
}
|
|
74
|
+
/** Read back one projected fact file (round-trip helper for tests and tools). */
|
|
75
|
+
export function readMemoryFact(factsDir, filename) {
|
|
76
|
+
return readFileSync(join(factsDir, filename), "utf8");
|
|
77
|
+
}
|
|
78
|
+
/** True when the store directory carries a projection from a previous apply. */
|
|
79
|
+
export function hasMemoryProjection(stateDir) {
|
|
80
|
+
return existsSync(join(stateDir, MEMORY_INDEX_FILE)) && existsSync(join(stateDir, MEMORY_FACTS_DIR));
|
|
81
|
+
}
|
|
82
|
+
function renderMemoryFact(entry) {
|
|
83
|
+
const type = isMemoryType(entry.metadata[MEMORY_TYPE_KEY]) ? String(entry.metadata[MEMORY_TYPE_KEY]) : "untyped";
|
|
84
|
+
const frontmatter = [
|
|
85
|
+
"---",
|
|
86
|
+
`id: ${entry.id}`,
|
|
87
|
+
`scope: ${entry.scope}`,
|
|
88
|
+
`kind: memory`,
|
|
89
|
+
`memoryType: ${type}`,
|
|
90
|
+
`version: ${entry.version}`,
|
|
91
|
+
`path: ${entry.path}`,
|
|
92
|
+
`archived: ${isArchived(entry)}`,
|
|
93
|
+
`created_at: ${entry.created_at}`,
|
|
94
|
+
`updated_at: ${entry.updated_at}`,
|
|
95
|
+
"---",
|
|
96
|
+
].join("\n");
|
|
97
|
+
return `${frontmatter}\n\n# ${entry.title}\n\n${entry.content}\n`;
|
|
98
|
+
}
|
|
99
|
+
function renderMemoryIndex(files) {
|
|
100
|
+
const lines = ["# Memory Index", "", "Derived from the JSON harness state — do not edit by hand; it is rewritten on every memory apply.", ""];
|
|
101
|
+
if (files.length === 0) {
|
|
102
|
+
lines.push("(no memory entries yet)");
|
|
103
|
+
return `${lines.join("\n")}\n`;
|
|
104
|
+
}
|
|
105
|
+
for (const [file, entry] of files) {
|
|
106
|
+
const type = isMemoryType(entry.metadata[MEMORY_TYPE_KEY]) ? String(entry.metadata[MEMORY_TYPE_KEY]) : "untyped";
|
|
107
|
+
const archived = isArchived(entry) ? ", archived" : "";
|
|
108
|
+
lines.push(`- [${entry.scope}:${entry.id}] ${entry.title} (type=${type}, v${entry.version}${archived}) → ${MEMORY_FACTS_DIR}/${file}`);
|
|
109
|
+
}
|
|
110
|
+
return `${lines.join("\n")}\n`;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=projection.js.map
|
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/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/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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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": {
|