dsh-continual-evolve 0.10.6 → 0.11.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 +24 -31
- package/README.zh.md +17 -21
- package/lib/approval.d.ts +2 -1
- package/lib/audit-log.d.ts +19 -0
- package/lib/audit-log.js +52 -0
- package/lib/benchmark-command.d.ts +1 -1
- package/lib/benchmark-command.js +1 -1
- package/lib/command-util.d.ts +70 -0
- package/lib/command-util.js +123 -0
- package/lib/command.d.ts +7 -43
- package/lib/command.js +24 -458
- package/lib/copy.d.ts +4 -41
- package/lib/copy.js +0 -102
- package/lib/declines.d.ts +2 -0
- package/lib/declines.js +4 -0
- package/lib/evolve-event.js +2 -1
- package/lib/extraction-phase.d.ts +132 -0
- package/lib/extraction-phase.js +236 -0
- package/lib/index.d.ts +26 -42
- package/lib/index.js +19 -33
- package/lib/inject.d.ts +13 -119
- package/lib/inject.js +12 -260
- package/lib/injection-caps.d.ts +4 -0
- package/lib/injection-caps.js +4 -0
- package/lib/injection-directory.d.ts +29 -0
- package/lib/injection-directory.js +78 -0
- package/lib/injection-rank.d.ts +85 -0
- package/lib/injection-rank.js +161 -0
- package/lib/listener.d.ts +45 -0
- package/lib/listener.js +199 -0
- package/lib/maintenance-commands.d.ts +23 -0
- package/lib/maintenance-commands.js +136 -0
- package/lib/memory-agent.d.ts +8 -44
- package/lib/memory-agent.js +8 -276
- package/lib/memory-apply.d.ts +44 -0
- package/lib/memory-apply.js +202 -0
- package/lib/memory-manifest.d.ts +16 -0
- package/lib/memory-manifest.js +70 -0
- package/lib/mount-command.js +1 -1
- package/lib/notify.d.ts +9 -20
- package/lib/notify.js +0 -40
- package/lib/plan-command.d.ts +12 -0
- package/lib/plan-command.js +38 -0
- package/lib/prefix-cache.d.ts +2 -1
- package/lib/review-scheduler.d.ts +1 -1
- package/lib/review-scheduler.js +2 -10
- package/lib/runtime-commands.d.ts +14 -0
- package/lib/runtime-commands.js +115 -0
- package/lib/service.d.ts +16 -2
- package/lib/service.js +74 -1
- package/lib/store.d.ts +1 -2
- package/lib/store.js +2 -2
- package/lib/tool.js +1 -1
- package/lib/turn-snapshot.d.ts +7 -1
- package/lib/turn-snapshot.js +14 -0
- package/lib/types.d.ts +10 -0
- package/lib/types.js +6 -0
- package/lib/wrapup-candidates.d.ts +127 -0
- package/lib/wrapup-candidates.js +161 -0
- package/lib/wrapup-proposals.d.ts +101 -0
- package/lib/wrapup-proposals.js +281 -0
- package/lib/wrapup.d.ts +8 -212
- package/lib/wrapup.js +4 -438
- package/package.json +1 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { MEMORY_TYPE_KEY } from "./types.js";
|
|
2
|
+
import { requestScopeApproval } from "./approval.js";
|
|
3
|
+
import { fingerprintMemoryBatch, isDeclinedRepeat, loadDeclinedMemory, recordDeclinedMemory, } from "./declines.js";
|
|
4
|
+
import { compactText } from "./render.js";
|
|
5
|
+
import { buildConflictNotice, CONFLICT_WARN_SCORE, mostSimilarEntry } from "./promotion.js";
|
|
6
|
+
import { EvolutionApplyPostCommitError } from "./service.js";
|
|
7
|
+
/**
|
|
8
|
+
* Render the authoritative persistent-scope diff shown to the human. Model
|
|
9
|
+
* prose is retained only as an explicitly untrusted summary; actions, targets,
|
|
10
|
+
* content previews, memory types, and conflict warnings come from code.
|
|
11
|
+
*/
|
|
12
|
+
export function renderMemoryApprovalDetails(proposal, scope, state, maxChars = 6000) {
|
|
13
|
+
const edits = proposal.edits.filter((candidate) => candidate.targetScope === scope);
|
|
14
|
+
const details = edits.map((edit) => {
|
|
15
|
+
const before = edit.id ? state.entries.memory[edit.id] : undefined;
|
|
16
|
+
const title = edit.title ?? before?.title ?? "(existing title unchanged)";
|
|
17
|
+
const path = edit.path ?? before?.path ?? "(path unchanged)";
|
|
18
|
+
const content = edit.content ?? (edit.action === "update" ? "(content unchanged)" : "(no content field)");
|
|
19
|
+
const type = edit.metadata?.[MEMORY_TYPE_KEY] ?? before?.metadata[MEMORY_TYPE_KEY] ?? "untyped";
|
|
20
|
+
const candidates = Object.values(state.entries.memory).filter((entry) => entry.id !== edit.id);
|
|
21
|
+
const hit = edit.action === "create" || edit.action === "update"
|
|
22
|
+
? mostSimilarEntry(candidates, edit.title ?? before?.title ?? "", edit.content ?? before?.content ?? "", CONFLICT_WARN_SCORE)
|
|
23
|
+
: undefined;
|
|
24
|
+
const conflict = hit ? `; conflict: ${compactText(buildConflictNotice(hit), 120)}` : "";
|
|
25
|
+
const targetId = edit.id === undefined ? "(new)" : JSON.stringify(edit.id);
|
|
26
|
+
return {
|
|
27
|
+
target: `- ${edit.action} ${scope}:${targetId} — ${compactText(title, 80)}`,
|
|
28
|
+
detail: ` path=${compactText(path, 60)}; memoryType=${String(type)}; content=${compactText(content, 100)}${conflict}`,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
let titleLimit = 80;
|
|
32
|
+
let targets = details.map((detail) => detail.target.replace(/ — .*$/, ` — ${compactText(detail.target.split(" — ")[1] ?? "", titleLimit)}`));
|
|
33
|
+
let base = ["edits to approve:", ...targets].join("\n");
|
|
34
|
+
while (base.length > maxChars && titleLimit > 20) {
|
|
35
|
+
titleLimit = Math.floor(titleLimit / 2);
|
|
36
|
+
targets = details.map((detail) => `- ${detail.target.split(" — ")[0]} — ${compactText(detail.target.split(" — ")[1] ?? "", titleLimit)}`);
|
|
37
|
+
base = ["edits to approve:", ...targets].join("\n");
|
|
38
|
+
}
|
|
39
|
+
if (base.length > maxChars)
|
|
40
|
+
throw new Error(`memory approval diff exceeds ${maxChars} characters`);
|
|
41
|
+
const output = [base];
|
|
42
|
+
const summary = `model summary (untrusted): ${compactText(proposal.summary, 180)}`;
|
|
43
|
+
if ([...output, summary].join("\n").length <= maxChars)
|
|
44
|
+
output.push(summary);
|
|
45
|
+
for (const detail of details) {
|
|
46
|
+
const line = detail.detail;
|
|
47
|
+
if ([...output, line].join("\n").length <= maxChars)
|
|
48
|
+
output.push(line);
|
|
49
|
+
}
|
|
50
|
+
if ([...output, " (additional previews omitted; every target is listed above)"].join("\n").length <= maxChars) {
|
|
51
|
+
output.push(" (additional previews omitted; every target is listed above)");
|
|
52
|
+
}
|
|
53
|
+
return output.join("\n");
|
|
54
|
+
}
|
|
55
|
+
function memoryScopeDecisionKey(cursor, scope, edits) {
|
|
56
|
+
return `${cursor}:${scope}:${JSON.stringify(edits.map(stripTargetScope))}`;
|
|
57
|
+
}
|
|
58
|
+
class MemoryApplyBatchError extends Error {
|
|
59
|
+
result;
|
|
60
|
+
scope;
|
|
61
|
+
storeId;
|
|
62
|
+
constructor(message, result, scope, storeId) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.result = result;
|
|
65
|
+
this.scope = scope;
|
|
66
|
+
this.storeId = storeId;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Apply one accepted proposal through the only mutation engine. All persistent
|
|
71
|
+
* approvals are resolved before the first write, so an unavailable approval
|
|
72
|
+
* boundary cannot leave a partially applied batch. Explicit rejection is a
|
|
73
|
+
* no-op for that scope; service/question failures remain retryable errors.
|
|
74
|
+
*/
|
|
75
|
+
export async function applyMemoryExtractionProposal(ctx, engine, proposal, options) {
|
|
76
|
+
const scopeOrder = ["local", "project", "global"];
|
|
77
|
+
const editsByScope = new Map();
|
|
78
|
+
for (const scope of scopeOrder) {
|
|
79
|
+
const edits = proposal.edits.filter((edit) => edit.targetScope === scope);
|
|
80
|
+
if (edits.length > 0)
|
|
81
|
+
editsByScope.set(scope, edits);
|
|
82
|
+
}
|
|
83
|
+
if (editsByScope.has("project") && !options.projectKey) {
|
|
84
|
+
throw new Error("memory agent cannot target project scope without a resolved project key");
|
|
85
|
+
}
|
|
86
|
+
const approvedScopes = new Set();
|
|
87
|
+
const declinedScopes = [];
|
|
88
|
+
for (const scope of ["project", "global"]) {
|
|
89
|
+
if (!editsByScope.has(scope))
|
|
90
|
+
continue;
|
|
91
|
+
if (!options.requireApproval) {
|
|
92
|
+
approvedScopes.add(scope);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const baseline = options.baselines[scope];
|
|
96
|
+
if (!baseline)
|
|
97
|
+
throw new Error(`memory agent has no ${scope} baseline`);
|
|
98
|
+
const scopeEdits = proposal.edits.filter((edit) => edit.targetScope === scope);
|
|
99
|
+
const fingerprint = fingerprintMemoryBatch(scope, scopeEdits);
|
|
100
|
+
if (isDeclinedRepeat(loadDeclinedMemory(engine.baseDir), scope, fingerprint)) {
|
|
101
|
+
ctx.logger("continual-evolve").info(`memory ${scope} proposal repeat-suppressed (declined before, no popup)`);
|
|
102
|
+
declinedScopes.push(scope);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const decisionKey = options.decisionCursor === undefined
|
|
106
|
+
? undefined
|
|
107
|
+
: memoryScopeDecisionKey(options.decisionCursor, scope, proposal.edits.filter((edit) => edit.targetScope === scope));
|
|
108
|
+
const previous = decisionKey === undefined ? undefined : options.scopeDecisions?.[decisionKey];
|
|
109
|
+
if (previous === "approved") {
|
|
110
|
+
approvedScopes.add(scope);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (previous === "declined") {
|
|
114
|
+
declinedScopes.push(scope);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const approved = await requestScopeApproval(ctx, options.agent, options.signal, scope, renderMemoryApprovalDetails(proposal, scope, baseline), options.recordLanguage);
|
|
118
|
+
if (decisionKey !== undefined)
|
|
119
|
+
options.onScopeDecision?.(decisionKey, approved);
|
|
120
|
+
if (approved === "approved")
|
|
121
|
+
approvedScopes.add(scope);
|
|
122
|
+
else {
|
|
123
|
+
declinedScopes.push(scope);
|
|
124
|
+
try {
|
|
125
|
+
recordDeclinedMemory(engine.baseDir, scope, scopeEdits, compactText(scopeEdits[0]?.title ?? proposal.summary, 120));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
ctx.logger("continual-evolve").warn(`declined-memory ledger write failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const results = [];
|
|
133
|
+
const applied = [];
|
|
134
|
+
try {
|
|
135
|
+
for (const scope of scopeOrder) {
|
|
136
|
+
const edits = editsByScope.get(scope);
|
|
137
|
+
if (!edits)
|
|
138
|
+
continue;
|
|
139
|
+
if (scope !== "local" && !approvedScopes.has(scope))
|
|
140
|
+
continue;
|
|
141
|
+
options.signal?.throwIfAborted();
|
|
142
|
+
const storeId = scope === "local" ? options.agent.id : scope === "project" ? options.projectKey : undefined;
|
|
143
|
+
const baseline = options.baselines[scope];
|
|
144
|
+
if (!baseline)
|
|
145
|
+
throw new Error(`memory agent has no ${scope} baseline`);
|
|
146
|
+
const result = engine.apply(scope, storeId, {
|
|
147
|
+
summary: proposal.summary,
|
|
148
|
+
rationale: proposal.rationale,
|
|
149
|
+
expectedOutcome: proposal.expectedOutcome,
|
|
150
|
+
edits: prepareMemoryEdits(edits, baseline),
|
|
151
|
+
}, {
|
|
152
|
+
scope,
|
|
153
|
+
baselineState: baseline,
|
|
154
|
+
...(options.source ? { source: options.source } : {}),
|
|
155
|
+
});
|
|
156
|
+
const failed = result.appliedEdits.filter((edit) => !edit.applied);
|
|
157
|
+
if (failed.length > 0) {
|
|
158
|
+
throw new MemoryApplyBatchError(`${scope} batch ${result.id} left ${failed.length} edit(s) unapplied: ${failed.map((edit) => edit.error ?? "unknown").join("; ")}`, result, scope, storeId);
|
|
159
|
+
}
|
|
160
|
+
results.push(result);
|
|
161
|
+
applied.push({ scope, storeId, result });
|
|
162
|
+
}
|
|
163
|
+
return { results, declinedScopes };
|
|
164
|
+
}
|
|
165
|
+
catch (cause) {
|
|
166
|
+
const attempted = [...applied];
|
|
167
|
+
if (cause instanceof MemoryApplyBatchError && cause.result && cause.scope) {
|
|
168
|
+
attempted.push({ scope: cause.scope, storeId: cause.storeId, result: cause.result });
|
|
169
|
+
}
|
|
170
|
+
else if (cause instanceof EvolutionApplyPostCommitError) {
|
|
171
|
+
attempted.push({ scope: cause.scope, storeId: cause.storeId, result: cause.result });
|
|
172
|
+
}
|
|
173
|
+
const rollbackErrors = [];
|
|
174
|
+
for (const item of attempted.reverse()) {
|
|
175
|
+
try {
|
|
176
|
+
const rollback = engine.rollbackResult(item.scope, item.storeId, item.result);
|
|
177
|
+
const failed = rollback.appliedEdits.filter((edit) => !edit.applied);
|
|
178
|
+
if (failed.length > 0)
|
|
179
|
+
rollbackErrors.push(`${item.scope}:${item.result.id} left ${failed.length} rollback edit(s) unapplied`);
|
|
180
|
+
}
|
|
181
|
+
catch (rollbackCause) {
|
|
182
|
+
rollbackErrors.push(`${item.scope}:${item.result.id} rollback failed: ${rollbackCause instanceof Error ? rollbackCause.message : String(rollbackCause)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const suffix = rollbackErrors.length > 0 ? `; compensation errors: ${rollbackErrors.join("; ")}` : "; prior memory writes were rolled back";
|
|
186
|
+
throw new Error(`memory agent apply failed: ${cause instanceof Error ? cause.message : String(cause)}${suffix}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function prepareMemoryEdits(edits, baseline) {
|
|
190
|
+
return edits.map((edit) => {
|
|
191
|
+
const plain = stripTargetScope(edit);
|
|
192
|
+
if (plain.action !== "update" || plain.metadata === undefined || plain.id === undefined)
|
|
193
|
+
return plain;
|
|
194
|
+
const before = baseline.entries.memory[plain.id];
|
|
195
|
+
return before ? { ...plain, metadata: { ...before.metadata, ...plain.metadata } } : plain;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function stripTargetScope(edit) {
|
|
199
|
+
const { targetScope: _targetScope, ...plain } = edit;
|
|
200
|
+
return plain;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=memory-apply.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The frozen memory manifest: a deterministic snapshot view of every live
|
|
3
|
+
* memory entry the extractor may see, plus its bounded serialization and
|
|
4
|
+
* manifest-scoped search. Pure functions over harness state.
|
|
5
|
+
*/
|
|
6
|
+
import type { HarnessEntry, HarnessState } from "./types.js";
|
|
7
|
+
/** One manifest row is exactly one live memory entry. */
|
|
8
|
+
export type MemoryManifestEntry = HarnessEntry;
|
|
9
|
+
export declare function buildMemoryManifest(state: HarnessState): MemoryManifestEntry[];
|
|
10
|
+
/** Render a bounded manifest index; full bodies remain available through memory_search. */
|
|
11
|
+
export declare function formatMemoryManifest(entries: readonly MemoryManifestEntry[], maxChars?: number): string;
|
|
12
|
+
/** Rank frozen manifest entries with CJK-aware token overlap and stable tie-breaking. */
|
|
13
|
+
export declare function searchMemoryManifest(entries: readonly MemoryManifestEntry[], query: string, maxResults?: number): MemoryManifestEntry[];
|
|
14
|
+
/** Inputs for one bounded background memory extraction loop. */
|
|
15
|
+
export declare function manifestResult(entry: MemoryManifestEntry): Record<string, unknown>;
|
|
16
|
+
//# sourceMappingURL=memory-manifest.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isArchived, isMemoryType, MEMORY_TYPE_KEY } from "./types.js";
|
|
2
|
+
import { tokenize } from "./search.js";
|
|
3
|
+
import { compactText } from "./render.js";
|
|
4
|
+
export function buildMemoryManifest(state) {
|
|
5
|
+
return Object.values(state.entries.memory)
|
|
6
|
+
.map((entry) => ({ ...entry, metadata: { ...entry.metadata }, reference: {}, arguments: {} }))
|
|
7
|
+
.sort((a, b) => manifestOrder(a).localeCompare(manifestOrder(b), "en"));
|
|
8
|
+
}
|
|
9
|
+
/** Render a bounded manifest index; full bodies remain available through memory_search. */
|
|
10
|
+
export function formatMemoryManifest(entries, maxChars = 8000) {
|
|
11
|
+
if (entries.length === 0)
|
|
12
|
+
return "No saved memory entries yet.";
|
|
13
|
+
const lines = [];
|
|
14
|
+
let used = 0;
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
const type = isMemoryType(entry.metadata[MEMORY_TYPE_KEY]) ? entry.metadata[MEMORY_TYPE_KEY] : "untyped";
|
|
17
|
+
const archived = isArchived(entry) ? ", archived" : "";
|
|
18
|
+
const line = `- [${entry.scope}:${entry.id}] ${entry.title} (memoryType=${type}, path=${entry.path}${archived}): ${compactText(entry.content, 240)}`;
|
|
19
|
+
if (used + line.length + 1 > maxChars)
|
|
20
|
+
break;
|
|
21
|
+
lines.push(line);
|
|
22
|
+
used += line.length + 1;
|
|
23
|
+
}
|
|
24
|
+
const omitted = entries.length - lines.length;
|
|
25
|
+
if (omitted > 0)
|
|
26
|
+
lines.push(`- +${omitted} more entries; use memory_search with a narrower query.`);
|
|
27
|
+
return lines.join("\n");
|
|
28
|
+
}
|
|
29
|
+
/** Rank frozen manifest entries with CJK-aware token overlap and stable tie-breaking. */
|
|
30
|
+
export function searchMemoryManifest(entries, query, maxResults = 8) {
|
|
31
|
+
const queryTokens = new Set(tokenize(query));
|
|
32
|
+
if (queryTokens.size === 0)
|
|
33
|
+
return [];
|
|
34
|
+
return entries
|
|
35
|
+
.map((entry) => {
|
|
36
|
+
const title = tokenize(entry.title);
|
|
37
|
+
const body = tokenize(`${entry.content} ${entry.path} ${String(entry.metadata[MEMORY_TYPE_KEY] ?? "")}`);
|
|
38
|
+
let score = 0;
|
|
39
|
+
for (const token of queryTokens) {
|
|
40
|
+
if (title.includes(token))
|
|
41
|
+
score += 2;
|
|
42
|
+
if (body.includes(token))
|
|
43
|
+
score += 1;
|
|
44
|
+
}
|
|
45
|
+
if (`${entry.scope}:${entry.id}`.toLowerCase() === query.trim().toLowerCase())
|
|
46
|
+
score += 100;
|
|
47
|
+
return { entry, score };
|
|
48
|
+
})
|
|
49
|
+
.filter((hit) => hit.score > 0)
|
|
50
|
+
.sort((a, b) => b.score - a.score || manifestOrder(a.entry).localeCompare(manifestOrder(b.entry), "en"))
|
|
51
|
+
.slice(0, maxResults)
|
|
52
|
+
.map((hit) => hit.entry);
|
|
53
|
+
}
|
|
54
|
+
/** Inputs for one bounded background memory extraction loop. */
|
|
55
|
+
export function manifestResult(entry) {
|
|
56
|
+
return {
|
|
57
|
+
scope: entry.scope,
|
|
58
|
+
id: entry.id,
|
|
59
|
+
title: entry.title,
|
|
60
|
+
content: entry.content,
|
|
61
|
+
path: entry.path,
|
|
62
|
+
memoryType: entry.metadata[MEMORY_TYPE_KEY],
|
|
63
|
+
archived: isArchived(entry),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function manifestOrder(entry) {
|
|
67
|
+
const scope = entry.scope === "global" ? "0" : entry.scope === "project" ? "1" : "2";
|
|
68
|
+
return `${scope}:${entry.id}:${entry.title}`;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=memory-manifest.js.map
|
package/lib/mount-command.js
CHANGED
package/lib/notify.d.ts
CHANGED
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* the user never
|
|
4
|
-
* module queues a short follow-up turn after an
|
|
5
|
-
* user SEES what was persisted, how to inspect it, and how to roll
|
|
2
|
+
* Memory-extraction visibility: the extractor works in the background —
|
|
3
|
+
* the user would never see a persisted entry or the token spend. This
|
|
4
|
+
* module queues a short follow-up turn after an applied extraction moment
|
|
5
|
+
* so the user SEES what was persisted, how to inspect it, and how to roll
|
|
6
|
+
* it back.
|
|
6
7
|
*
|
|
7
|
-
* The
|
|
8
|
+
* The receipt is a `continual-evolve`-sourced user message (`agent.followup`),
|
|
8
9
|
* so it is rendered in the session transcript like any other input and the
|
|
9
10
|
* agent answers with a one-line confirmation. It never fakes tool or assistant
|
|
10
11
|
* events, so session replay, the ordered surface, and derived history stay
|
|
11
|
-
* untouched: the
|
|
12
|
+
* untouched: the receipt is a plain `user/message` carrying this plugin's own
|
|
12
13
|
* source kind.
|
|
13
14
|
*
|
|
14
|
-
* Every mechanical property stays in code: the
|
|
15
|
-
* applied refinement
|
|
15
|
+
* Every mechanical property stays in code: the receipt text is built from the
|
|
16
|
+
* applied refinement batches, never from model text.
|
|
16
17
|
*/
|
|
17
18
|
import type { Context } from "@deepseek-ai/cordis";
|
|
18
19
|
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
19
20
|
import type { RefinementResult } from "./types.js";
|
|
20
|
-
/**
|
|
21
|
-
* Compose the user-visible gate notice from an applied refinement result.
|
|
22
|
-
* Lists every successfully applied edit (kind + title + id) and the rollback
|
|
23
|
-
* command; failed edits are summarized in one line so nothing is hidden.
|
|
24
|
-
*/
|
|
25
|
-
export declare function buildGateNotice(result: RefinementResult, turnsSinceLastReview: number): string;
|
|
26
|
-
/**
|
|
27
|
-
* Queue the follow-up notice turn for the agent. Failure is contained: a
|
|
28
|
-
* broken notification must never break the gate path that already recorded
|
|
29
|
-
* the decision in reviews.jsonl.
|
|
30
|
-
*/
|
|
31
|
-
export declare function notifyAutoReview(ctx: Context, agent: Agent, result: RefinementResult, turnsSinceLastReview: number): void;
|
|
32
21
|
/** Unified extraction receipt: one shape for applied / declined / no-op. */
|
|
33
22
|
export interface MemoryReceipt {
|
|
34
23
|
outcome: "applied" | "declined" | "noop";
|
package/lib/notify.js
CHANGED
|
@@ -1,45 +1,5 @@
|
|
|
1
1
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
2
|
import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
|
|
3
|
-
/**
|
|
4
|
-
* Compose the user-visible gate notice from an applied refinement result.
|
|
5
|
-
* Lists every successfully applied edit (kind + title + id) and the rollback
|
|
6
|
-
* command; failed edits are summarized in one line so nothing is hidden.
|
|
7
|
-
*/
|
|
8
|
-
export function buildGateNotice(result, turnsSinceLastReview) {
|
|
9
|
-
const applied = result.appliedEdits.filter((edit) => edit.applied);
|
|
10
|
-
const failed = result.appliedEdits.filter((edit) => !edit.applied);
|
|
11
|
-
const kindLabel = { prompt: "提示词", memory: "记忆", skill: "技能", subagent: "子代理" };
|
|
12
|
-
const lines = applied.map((edit) => `- ${kindLabel[edit.kind] ?? edit.kind}「${edit.title ?? edit.id}」(${edit.id})`);
|
|
13
|
-
const linesText = lines.length > 0 ? lines.join("\n") : "(无条目成功应用)";
|
|
14
|
-
const failedText = failed.length > 0 ? `\n另有 ${failed.length} 条编辑未应用。` : "";
|
|
15
|
-
return [
|
|
16
|
-
`🔎 自动进化门禁:会话第 ${turnsSinceLastReview} 回合检查完成,本次沉淀 ${applied.length} 条条目:`,
|
|
17
|
-
linesText,
|
|
18
|
-
failedText,
|
|
19
|
-
`查看全部条目:/evolve list;回滚本次沉淀:/evolve rollback ${result.id}`,
|
|
20
|
-
"请用一句话简短确认即可,不要调用任何工具。",
|
|
21
|
-
]
|
|
22
|
-
.filter((part) => part !== "")
|
|
23
|
-
.join("\n");
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Queue the follow-up notice turn for the agent. Failure is contained: a
|
|
27
|
-
* broken notification must never break the gate path that already recorded
|
|
28
|
-
* the decision in reviews.jsonl.
|
|
29
|
-
*/
|
|
30
|
-
export function notifyAutoReview(ctx, agent, result, turnsSinceLastReview) {
|
|
31
|
-
try {
|
|
32
|
-
agent.followup(createUserMessage({
|
|
33
|
-
content: [{ type: "text", text: buildGateNotice(result, turnsSinceLastReview) }],
|
|
34
|
-
source: EVOLVE_MESSAGE_SOURCE,
|
|
35
|
-
}));
|
|
36
|
-
}
|
|
37
|
-
catch (cause) {
|
|
38
|
-
ctx
|
|
39
|
-
.logger("continual-evolve")
|
|
40
|
-
.warn(`auto-review notice failed for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
3
|
/**
|
|
44
4
|
* Compose the user-visible memory receipt from applied refinement batches
|
|
45
5
|
* (never model text): every landed memory edit with its rollback command.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/evolve plan` — the manual LLM planner path: propose edits against one
|
|
3
|
+
* store with the main-session model, ask for approval on persistent scopes,
|
|
4
|
+
* and apply with the normal baseline checks.
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
7
|
+
import type { CommandInvocation } from "@deepseek-ai/dsh-commands";
|
|
8
|
+
import type { HarnessScope } from "./types.js";
|
|
9
|
+
import type { EvolutionEngine } from "./service.js";
|
|
10
|
+
import { type CommandGateOptions, type CommandRuntimeOptions, type CommandTextResult } from "./command-util.js";
|
|
11
|
+
export declare function executePlanCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation, scope: HarnessScope, after: string[], opts: CommandGateOptions, runtime: CommandRuntimeOptions): Promise<CommandTextResult>;
|
|
12
|
+
//# sourceMappingURL=plan-command.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { planWithLlm } from "./planner.js";
|
|
3
|
+
import { requireGlobalApproval } from "./approval.js";
|
|
4
|
+
import { entrySourceOf } from "./source.js";
|
|
5
|
+
import { renderResult, storeIdForCommand, success } from "./command-util.js";
|
|
6
|
+
export async function executePlanCommand(ctx, engine, invocation, scope, after, opts, runtime) {
|
|
7
|
+
const instructions = after.length > 0 ? after.join(" ") : undefined;
|
|
8
|
+
const sessionId = invocation.agent.id;
|
|
9
|
+
const state = engine.load(scope, storeIdForCommand(scope, invocation));
|
|
10
|
+
const history = engine.history(scope, storeIdForCommand(scope, invocation));
|
|
11
|
+
const proposal = await planWithLlm(ctx, {
|
|
12
|
+
agent: invocation.agent,
|
|
13
|
+
state,
|
|
14
|
+
history,
|
|
15
|
+
...(instructions ? { instructions } : {}),
|
|
16
|
+
global: scope === "global",
|
|
17
|
+
signal: invocation.signal,
|
|
18
|
+
tokenUsage: {
|
|
19
|
+
baseDir: engine.baseDir,
|
|
20
|
+
sessionId,
|
|
21
|
+
retain: engine.retention.tokenUsage,
|
|
22
|
+
onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
23
|
+
},
|
|
24
|
+
// skill-creator template facts (fallback: builtin guide).
|
|
25
|
+
skillsRoot: join(engine.baseDir, "skills"),
|
|
26
|
+
});
|
|
27
|
+
if ((scope === "global" || scope === "project") && opts.requireGlobalApproval && proposal.edits.length > 0) {
|
|
28
|
+
await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve plan ${scope} 将应用 ${proposal.edits.length} 条编辑到${scope === "project" ? "本项目" : "跨会话"} store:${proposal.summary}`, runtime.recordLanguage);
|
|
29
|
+
}
|
|
30
|
+
const source = entrySourceOf(invocation.agent, sessionId);
|
|
31
|
+
const result = engine.apply(scope, storeIdForCommand(scope, invocation), proposal, {
|
|
32
|
+
scope,
|
|
33
|
+
baselineState: state,
|
|
34
|
+
...(source ? { source } : {}),
|
|
35
|
+
});
|
|
36
|
+
return success(renderResult(result));
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=plan-command.js.map
|
package/lib/prefix-cache.d.ts
CHANGED
|
@@ -18,7 +18,8 @@ import { type AssistantMessage, type UserMessage } from "@deepseek-ai/dsh-llm";
|
|
|
18
18
|
/** Planning input shape: A = session-derived message prefix, B = flat-text trajectory. */
|
|
19
19
|
export type PlannerRoute = "A" | "B";
|
|
20
20
|
/** Routing mode: auto-detect from cache evidence, always prefix, or never. */
|
|
21
|
-
export type PlannerPrefixCacheMode
|
|
21
|
+
export { type PlannerPrefixCacheMode } from "./types.js";
|
|
22
|
+
import type { PlannerPrefixCacheMode } from "./types.js";
|
|
22
23
|
/** Per-call routing overrides; absent fields fall back to the resolved defaults. */
|
|
23
24
|
export interface PrefixCacheOptions {
|
|
24
25
|
mode?: PlannerPrefixCacheMode;
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* errors, aborts, and stale boundaries leave it unchanged.
|
|
8
8
|
*/
|
|
9
9
|
/** Compare two scheduler cursors; undefined means the cursor families differ. */
|
|
10
|
-
export declare function compareReviewCursors(left: string, right: string): number | undefined;
|
|
11
10
|
/** Terminal state of one scheduled snapshot. */
|
|
12
11
|
export type ReviewSchedulerStatus = "success" | "no-op" | "error" | "aborted";
|
|
13
12
|
/** A scheduler execution receives the exact snapshot and its owner signal. */
|
|
@@ -35,4 +34,5 @@ export interface ReviewScheduler<TSnapshot> {
|
|
|
35
34
|
* @param cursorOf - extracts the durable boundary from a snapshot
|
|
36
35
|
*/
|
|
37
36
|
export declare function createReviewScheduler<TSnapshot>(execute: (input: ReviewSchedulerExecution<TSnapshot>) => Promise<ReviewSchedulerStatus>, cursorOf: (snapshot: TSnapshot) => string | undefined): ReviewScheduler<TSnapshot>;
|
|
37
|
+
export { compareReviewCursors } from "./turn-snapshot.js";
|
|
38
38
|
//# sourceMappingURL=review-scheduler.d.ts.map
|
package/lib/review-scheduler.js
CHANGED
|
@@ -6,16 +6,6 @@
|
|
|
6
6
|
* comparable boundary runs first. A cursor advances only after success/no-op;
|
|
7
7
|
* errors, aborts, and stale boundaries leave it unchanged.
|
|
8
8
|
*/
|
|
9
|
-
/** Compare two scheduler cursors; undefined means the cursor families differ. */
|
|
10
|
-
export function compareReviewCursors(left, right) {
|
|
11
|
-
if (left === right)
|
|
12
|
-
return 0;
|
|
13
|
-
const leftMatch = /^(seq|index):(\d+)$/.exec(left);
|
|
14
|
-
const rightMatch = /^(seq|index):(\d+)$/.exec(right);
|
|
15
|
-
if (!leftMatch || !rightMatch || leftMatch[1] !== rightMatch[1])
|
|
16
|
-
return undefined;
|
|
17
|
-
return Number(leftMatch[2]) - Number(rightMatch[2]);
|
|
18
|
-
}
|
|
19
9
|
/** Bound unresolved acquisitions; actual auto capture is serialized per session. */
|
|
20
10
|
const MAX_PENDING_ACQUISITIONS = 16;
|
|
21
11
|
/**
|
|
@@ -188,4 +178,6 @@ function waitForSnapshotAcquisitionOrShutdown(acquisition, signal) {
|
|
|
188
178
|
});
|
|
189
179
|
});
|
|
190
180
|
}
|
|
181
|
+
export { compareReviewCursors } from "./turn-snapshot.js";
|
|
182
|
+
import { compareReviewCursors } from "./turn-snapshot.js";
|
|
191
183
|
//# sourceMappingURL=review-scheduler.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EvolutionEngine } from "./service.js";
|
|
2
|
+
import { type CommandRuntimeOptions, type CommandTextResult } from "./command-util.js";
|
|
3
|
+
/** `/evolve pause|resume` — the runtime switch is independent of registration. */
|
|
4
|
+
export declare function executePauseResumeCommand(engine: EvolutionEngine, sub: "pause" | "resume", runtime: CommandRuntimeOptions): CommandTextResult;
|
|
5
|
+
/** `/evolve status` — listener wiring plus per-store entry counts and retention. */
|
|
6
|
+
export declare function executeStatusCommand(engine: EvolutionEngine, sessionId: string, projectKey: string | undefined, runtime: CommandRuntimeOptions): CommandTextResult;
|
|
7
|
+
/**
|
|
8
|
+
* `/evolve usage` — injection counts per entry across the stores the human
|
|
9
|
+
* can see (global + this session + this project), plus the exact direct-call
|
|
10
|
+
* token report. Counts are per-session since the v2 usage shape — "in how
|
|
11
|
+
* many sessions did this entry surface".
|
|
12
|
+
*/
|
|
13
|
+
export declare function executeUsageCommand(engine: EvolutionEngine, sessionId: string, projectKey: string | undefined): CommandTextResult;
|
|
14
|
+
//# sourceMappingURL=runtime-commands.d.ts.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { loadUsage, getUsageCount } from "./usage.js";
|
|
2
|
+
import { loadGateRuntime, saveGateRuntime } from "./runtime.js";
|
|
3
|
+
import { loadTokenUsage, renderTokenUsageReport } from "./token-usage.js";
|
|
4
|
+
import { success } from "./command-util.js";
|
|
5
|
+
/** `/evolve pause|resume` — the runtime switch is independent of registration. */
|
|
6
|
+
export function executePauseResumeCommand(engine, sub, runtime) {
|
|
7
|
+
const pausing = sub === "pause";
|
|
8
|
+
const defaultEnabled = runtime.autoReview ?? false;
|
|
9
|
+
const current = loadGateRuntime(engine.baseDir, defaultEnabled);
|
|
10
|
+
if (pausing && current.paused) {
|
|
11
|
+
return success("automatic Memory Agent is already paused (no change).");
|
|
12
|
+
}
|
|
13
|
+
if (!pausing && current.enabled && !current.paused) {
|
|
14
|
+
return success("automatic Memory Agent is already running (no change).");
|
|
15
|
+
}
|
|
16
|
+
saveGateRuntime(engine.baseDir, pausing, pausing ? current.enabled : true);
|
|
17
|
+
return success(pausing
|
|
18
|
+
? "automatic Memory Agent paused: no automatic memory extraction calls until /evolve resume. Manual evolve_* tools and /evolve commands keep working."
|
|
19
|
+
: "automatic Memory Agent resumed: extraction moments (compaction / goal-blocked / session-close drain) are live again.");
|
|
20
|
+
}
|
|
21
|
+
/** `/evolve status` — listener wiring plus per-store entry counts and retention. */
|
|
22
|
+
export function executeStatusCommand(engine, sessionId, projectKey, runtime) {
|
|
23
|
+
const configuredDefault = runtime.autoReview === undefined ? "unknown" : runtime.autoReview ? "on" : "off";
|
|
24
|
+
const current = loadGateRuntime(engine.baseDir, runtime.autoReview ?? false);
|
|
25
|
+
const effective = current.paused
|
|
26
|
+
? "PAUSED (resume with /evolve resume)"
|
|
27
|
+
: current.enabled
|
|
28
|
+
? "Memory Agent running"
|
|
29
|
+
: "Memory Agent off (enable with /evolve resume)";
|
|
30
|
+
const gateLine = `gate: moment-driven Memory Agent listener registered · config default ${configuredDefault} · runtime ${effective}`;
|
|
31
|
+
const countEntries = (state) => Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
|
|
32
|
+
const lines = [gateLine];
|
|
33
|
+
lines.push(`stores: global ${countEntries(engine.load("global", undefined))} entries · local(${sessionId}) ${countEntries(engine.load("local", sessionId))} entries`);
|
|
34
|
+
if (projectKey) {
|
|
35
|
+
try {
|
|
36
|
+
lines[lines.length - 1] += ` · project ${countEntries(engine.load("project", projectKey))} entries`;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
lines[lines.length - 1] += " · project (unavailable)";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const retention = engine.retention;
|
|
43
|
+
if (retention) {
|
|
44
|
+
lines.push(`retention: snapshots ${retention.snapshots} · refinements ${retention.refinements} · reviews ${retention.reviews} · token usage ${retention.tokenUsage} (historyRetain)`);
|
|
45
|
+
}
|
|
46
|
+
return success(lines.join("\n"));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* `/evolve usage` — injection counts per entry across the stores the human
|
|
50
|
+
* can see (global + this session + this project), plus the exact direct-call
|
|
51
|
+
* token report. Counts are per-session since the v2 usage shape — "in how
|
|
52
|
+
* many sessions did this entry surface".
|
|
53
|
+
*/
|
|
54
|
+
export function executeUsageCommand(engine, sessionId, projectKey) {
|
|
55
|
+
const store = loadUsage(engine.baseDir);
|
|
56
|
+
const rows = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const collect = (state) => {
|
|
59
|
+
for (const kind of Object.keys(state.entries)) {
|
|
60
|
+
for (const entry of Object.values(state.entries[kind])) {
|
|
61
|
+
const key = `${kind}:${entry.id}`;
|
|
62
|
+
if (seen.has(key))
|
|
63
|
+
continue;
|
|
64
|
+
seen.add(key);
|
|
65
|
+
const count = getUsageCount(store, kind, entry.id);
|
|
66
|
+
rows.push({
|
|
67
|
+
key,
|
|
68
|
+
title: entry.title,
|
|
69
|
+
count,
|
|
70
|
+
...(store.lastSession?.[`${kind}:${entry.id}`] ? { lastSession: store.lastSession[`${kind}:${entry.id}`] } : {}),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
collect(engine.load("global", undefined));
|
|
76
|
+
collect(engine.load("local", sessionId));
|
|
77
|
+
if (projectKey) {
|
|
78
|
+
try {
|
|
79
|
+
collect(engine.load("project", projectKey));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// project store unavailable here — global+local still report
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const liveKeys = new Set(rows.map((r) => r.key));
|
|
86
|
+
const orphaned = Object.keys(store.counts).filter((k) => !liveKeys.has(k)).length;
|
|
87
|
+
const total = rows.reduce((n, r) => n + r.count, 0);
|
|
88
|
+
const injected = rows.filter((r) => r.count > 0).sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));
|
|
89
|
+
const stale = rows.filter((r) => r.count === 0).sort((a, b) => (a.key < b.key ? -1 : 1));
|
|
90
|
+
const lines = [`injected entries: ${total} injections across ${injected.length} of ${rows.length} stored entries${orphaned > 0 ? ` (+${orphaned} historical key(s) for deleted entries)` : ""}`];
|
|
91
|
+
if (injected.length > 0) {
|
|
92
|
+
lines.push("injected (top 15):");
|
|
93
|
+
for (const row of injected.slice(0, 15)) {
|
|
94
|
+
lines.push(` ${row.key} — ${row.count}× · ${row.title}${row.lastSession ? ` (last in ${row.lastSession})` : ""}`);
|
|
95
|
+
}
|
|
96
|
+
if (injected.length > 15) {
|
|
97
|
+
lines.push(` … and ${injected.length - 15} more`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
lines.push("injected: (none yet — nothing has surfaced into a session prompt)");
|
|
102
|
+
}
|
|
103
|
+
if (stale.length > 0) {
|
|
104
|
+
lines.push(`never injected (${stale.length}):`);
|
|
105
|
+
for (const row of stale.slice(0, 20)) {
|
|
106
|
+
lines.push(` ${row.key} · ${row.title}`);
|
|
107
|
+
}
|
|
108
|
+
if (stale.length > 20) {
|
|
109
|
+
lines.push(` … and ${stale.length - 20} more`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
lines.push("", ...renderTokenUsageReport(loadTokenUsage(engine.baseDir), engine.retention.tokenUsage));
|
|
113
|
+
return success(lines.join("\n"));
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=runtime-commands.js.map
|