@lmzhen/dsh-memory 0.44.0 → 0.46.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/lib/files/index.js +187 -0
- package/lib/tools/index.js +341 -0
- package/lib/types/files/index.d.ts +60 -0
- package/lib/types/files/index.js +261 -0
- package/lib/types/index.js +117 -0
- package/lib/types/tools/index.d.ts +42 -0
- package/lib/types/tools/index.js +438 -0
- package/locale/files/de.json +6 -0
- package/locale/files/en.json +6 -0
- package/locale/files/es.json +6 -0
- package/locale/files/fr.json +6 -0
- package/locale/files/ja.json +6 -0
- package/locale/files/pt.json +6 -0
- package/locale/files/ru.json +6 -0
- package/locale/files/zh.json +6 -0
- package/locale/tools/de.json +6 -0
- package/locale/tools/en.json +6 -0
- package/locale/tools/es.json +6 -0
- package/locale/tools/fr.json +6 -0
- package/locale/tools/ja.json +6 -0
- package/locale/tools/pt.json +6 -0
- package/locale/tools/ru.json +6 -0
- package/locale/tools/zh.json +6 -0
- package/package.json +19 -5
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { DEFAULT_CONSOLIDATION_FAILURES, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_USER_CHAR_LIMIT, MemoryStore, clampOnce, evolutionIoAdapter, liveBool, makeSerialQueue, paramRowId, payloadText, unwrapLive, userSetKeys } from "@lmzhen/dsh-evolution-core";
|
|
5
|
+
//#region lib/types/files/index.js
|
|
6
|
+
/**
|
|
7
|
+
* Local layered memory provider for `ctx.memory`.
|
|
8
|
+
* @module @lmzhen/dsh-memory-files
|
|
9
|
+
*/
|
|
10
|
+
const name = "memory-files";
|
|
11
|
+
const inject = ["memory", "evolutionIo"];
|
|
12
|
+
const Config = z.object({
|
|
13
|
+
providerName: z.string().default("files"),
|
|
14
|
+
memoryChars: z.number().min(1).volatile(),
|
|
15
|
+
userChars: z.number().min(1).volatile(),
|
|
16
|
+
memoryCharLimit: z.number().min(1).default(DEFAULT_MEMORY_CHAR_LIMIT),
|
|
17
|
+
userCharLimit: z.number().min(1).default(DEFAULT_USER_CHAR_LIMIT),
|
|
18
|
+
addDatePrefix: z.boolean().default(false).volatile(),
|
|
19
|
+
root: z.string().default(""),
|
|
20
|
+
maxConsolidationFailures: z.number().min(1).default(DEFAULT_CONSOLIDATION_FAILURES).volatile(),
|
|
21
|
+
threatExemptLabels: z.array(z.string()).default([])
|
|
22
|
+
});
|
|
23
|
+
function apply(ctx, rawConfig = {}) {
|
|
24
|
+
ctx.inject(["settings"], (injected) => {
|
|
25
|
+
const settings = injected.settings;
|
|
26
|
+
if (typeof settings?.configure !== "function") return;
|
|
27
|
+
const disposer = settings.configure({ auto: false }, ctx.fiber);
|
|
28
|
+
if (typeof disposer === "function") ctx.effect(() => disposer, "memory-files: settings presentation");
|
|
29
|
+
});
|
|
30
|
+
const clampedKeys = /* @__PURE__ */ new Set();
|
|
31
|
+
/**
|
|
32
|
+
* Clamp one supplied numeric value, warning once per key.
|
|
33
|
+
* @param name - the config key, for the warning.
|
|
34
|
+
* @param value - the value as supplied by the deployment or the user.
|
|
35
|
+
* @param fallback - the package default a corrected value falls back to.
|
|
36
|
+
* @returns the value the store may use.
|
|
37
|
+
*/
|
|
38
|
+
const field = (name, value, fallback) => clampOnce(clampedKeys, (message) => {
|
|
39
|
+
ctx.logger.warn(message);
|
|
40
|
+
}, `memory-files: ${name}`, value, fallback, 1);
|
|
41
|
+
const floorLimit = (value) => Math.floor(value);
|
|
42
|
+
const policyBudget = () => {
|
|
43
|
+
const snapshot = ctx.get("evolutionPolicy")?.get?.();
|
|
44
|
+
return {
|
|
45
|
+
memory: snapshot?.memoryChars,
|
|
46
|
+
user: snapshot?.userChars
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
const aliasDefaults = Config["~standard"].validate({}).value;
|
|
50
|
+
const ALIAS_OF = {
|
|
51
|
+
memoryChars: "memoryCharLimit",
|
|
52
|
+
userChars: "userCharLimit"
|
|
53
|
+
};
|
|
54
|
+
const DEFAULTS = {
|
|
55
|
+
memoryChars: DEFAULT_MEMORY_CHAR_LIMIT,
|
|
56
|
+
userChars: DEFAULT_USER_CHAR_LIMIT
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The live value of one canonical budget key (G1 §8.3).
|
|
60
|
+
* @param id - the canonical registry id.
|
|
61
|
+
* @returns the value the platform resolved (user layer included), or undefined.
|
|
62
|
+
*/
|
|
63
|
+
const canonicalBudget = (id) => unwrapLive(id === "memoryChars" ? rawConfig.memoryChars : rawConfig.userChars);
|
|
64
|
+
/**
|
|
65
|
+
* Whether the OPERATOR set this budget on THIS ROW (G0/S0.4 + G1).
|
|
66
|
+
*
|
|
67
|
+
* The canonical key answers through its live reference — a user-layer value
|
|
68
|
+
* arrives the same way, which is what keeps a user choice ahead of the policy —
|
|
69
|
+
* and the deprecated alias through the default comparison above. The pair is
|
|
70
|
+
* documented in the Config JSDoc and reported by /evolution doctor (budgetIssues).
|
|
71
|
+
* @param id - the canonical registry id.
|
|
72
|
+
* @returns whether the row or the user supplied a value.
|
|
73
|
+
*/
|
|
74
|
+
const explicitBudget = (id) => {
|
|
75
|
+
if (canonicalBudget(id) !== void 0) return true;
|
|
76
|
+
const alias = rawConfig[ALIAS_OF[id]];
|
|
77
|
+
return alias !== void 0 && alias !== aliasDefaults[ALIAS_OF[id]];
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The budget one key resolves to (G1 §8.4-A): the row's own value — canonical or
|
|
81
|
+
* alias — over the mounted policy, over the package default.
|
|
82
|
+
* @param id - the canonical registry id.
|
|
83
|
+
* @returns the resolved, unclamped budget.
|
|
84
|
+
*/
|
|
85
|
+
const budget = (id) => {
|
|
86
|
+
const explicit = canonicalBudget(id) ?? (explicitBudget(id) ? rawConfig[ALIAS_OF[id]] : void 0);
|
|
87
|
+
if (explicit !== void 0) return explicit;
|
|
88
|
+
const policy = policyBudget();
|
|
89
|
+
return (id === "memoryChars" ? policy.memory : policy.user) ?? DEFAULTS[id];
|
|
90
|
+
};
|
|
91
|
+
const memoryChars = () => floorLimit(field("memoryCharLimit", budget("memoryChars"), DEFAULT_MEMORY_CHAR_LIMIT));
|
|
92
|
+
const userChars = () => floorLimit(field("userCharLimit", budget("userChars"), DEFAULT_USER_CHAR_LIMIT));
|
|
93
|
+
const consolidationFailures = () => field("maxConsolidationFailures", rawConfig.maxConsolidationFailures, DEFAULT_CONSOLIDATION_FAILURES);
|
|
94
|
+
const addDatePrefix = () => liveBool(rawConfig.addDatePrefix);
|
|
95
|
+
const policyAtMount = policyBudget();
|
|
96
|
+
if (explicitBudget("memoryChars") && policyAtMount.memory !== void 0 && memoryChars() !== policyAtMount.memory) ctx.logger.warn(`memory-files: the row's memoryChars=${memoryChars()} contradicts evolution-policy memoryChars=${policyAtMount.memory} — the store enforces the row value while the review plans against the policy value; align them or leave the row unset`);
|
|
97
|
+
if (explicitBudget("userChars") && policyAtMount.user !== void 0 && userChars() !== policyAtMount.user) ctx.logger.warn(`memory-files: the row's userChars=${userChars()} contradicts evolution-policy userChars=${policyAtMount.user} — same divergence as memoryChars`);
|
|
98
|
+
const io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
|
|
99
|
+
const authority = ctx.get("evolutionMutationAuthority");
|
|
100
|
+
const serializedWrite = makeSerialQueue();
|
|
101
|
+
const trimmedRoot = (rawConfig.root || "").trim();
|
|
102
|
+
const resolvedRoot = trimmedRoot === "" ? "" : resolve(trimmedRoot);
|
|
103
|
+
let store;
|
|
104
|
+
let storeKey = "";
|
|
105
|
+
/**
|
|
106
|
+
* The store for the configuration as it stands right now.
|
|
107
|
+
* @returns the current store, rebuilt when a resolved limit changed.
|
|
108
|
+
*/
|
|
109
|
+
const currentStore = () => {
|
|
110
|
+
const options = {
|
|
111
|
+
memoryCharLimit: memoryChars(),
|
|
112
|
+
userCharLimit: userChars(),
|
|
113
|
+
addDatePrefix: addDatePrefix(),
|
|
114
|
+
maxConsolidationFailures: consolidationFailures()
|
|
115
|
+
};
|
|
116
|
+
const key = `${options.memoryCharLimit}|${options.userCharLimit}|${options.addDatePrefix}|${options.maxConsolidationFailures}`;
|
|
117
|
+
if (store !== void 0 && key === storeKey) return store;
|
|
118
|
+
store = new MemoryStore({
|
|
119
|
+
...options,
|
|
120
|
+
...rawConfig.threatExemptLabels !== void 0 ? { threatExemptLabels: rawConfig.threatExemptLabels } : {},
|
|
121
|
+
...resolvedRoot !== "" ? { root: resolvedRoot } : {},
|
|
122
|
+
io,
|
|
123
|
+
...authority?.homeFence === void 0 ? {} : { fence: authority.homeFence }
|
|
124
|
+
});
|
|
125
|
+
storeKey = key;
|
|
126
|
+
return store;
|
|
127
|
+
};
|
|
128
|
+
const provider = {
|
|
129
|
+
name: rawConfig.providerName ?? "files",
|
|
130
|
+
read: (target) => currentStore().read(target),
|
|
131
|
+
applyBatch: async (target, operations) => {
|
|
132
|
+
const normalized = operations.map((op) => ({
|
|
133
|
+
action: op.action,
|
|
134
|
+
facts: payloadText(op),
|
|
135
|
+
old_text: op.old_text
|
|
136
|
+
}));
|
|
137
|
+
return await serializedWrite(() => currentStore().applyBatch(target, normalized));
|
|
138
|
+
},
|
|
139
|
+
snapshot: async () => {
|
|
140
|
+
const [memory, user] = await serializedWrite(async () => {
|
|
141
|
+
const current = currentStore();
|
|
142
|
+
return [await current.read("memory"), await current.read("user")];
|
|
143
|
+
});
|
|
144
|
+
const text = JSON.stringify([memory, user]);
|
|
145
|
+
return {
|
|
146
|
+
version: 1,
|
|
147
|
+
sha256: createHash("sha256").update(text).digest("hex"),
|
|
148
|
+
memory,
|
|
149
|
+
user
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
renderContext: () => serializedWrite(() => currentStore().renderContext())
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Which surface supplied one budget.
|
|
156
|
+
* @param id - the canonical registry id.
|
|
157
|
+
* @returns 'user', 'config', 'policy' or 'default'.
|
|
158
|
+
*/
|
|
159
|
+
const limitSource = (id) => {
|
|
160
|
+
if (userSetKeys(ctx, paramRowId("memory-files")).has(id)) return "user";
|
|
161
|
+
if (explicitBudget(id)) return "config";
|
|
162
|
+
return (id === "memoryChars" ? policyBudget().memory : policyBudget().user) === void 0 ? "default" : "policy";
|
|
163
|
+
};
|
|
164
|
+
ctx.provide("evolutionMemoryBudget", {
|
|
165
|
+
get memoryCharLimit() {
|
|
166
|
+
return memoryChars();
|
|
167
|
+
},
|
|
168
|
+
get userCharLimit() {
|
|
169
|
+
return userChars();
|
|
170
|
+
},
|
|
171
|
+
get memorySource() {
|
|
172
|
+
return limitSource("memoryChars");
|
|
173
|
+
},
|
|
174
|
+
get userSource() {
|
|
175
|
+
return limitSource("userChars");
|
|
176
|
+
},
|
|
177
|
+
get policyMemoryChars() {
|
|
178
|
+
return policyBudget().memory;
|
|
179
|
+
},
|
|
180
|
+
get policyUserChars() {
|
|
181
|
+
return policyBudget().user;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
ctx.effect(() => ctx.memory.registerProvider(provider), "memory-files.provider");
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { Config, apply, inject, name };
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { MEMORY_GUIDANCE_SECTION_ORDER, clampedNumber, payloadText, unwrapLive } from "@lmzhen/dsh-evolution-core";
|
|
3
|
+
import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
|
|
4
|
+
import { defineTool, registerEvolutionTool } from "@lmzhen/dsh-evolution-core/platform";
|
|
5
|
+
import { resolveExecOrigins } from "@lmzhen/dsh-evolution-core/skills";
|
|
6
|
+
//#region lib/types/tools/index.js
|
|
7
|
+
/**
|
|
8
|
+
* Model-facing memory tool and runtime-context memory snapshot.
|
|
9
|
+
* @module @deepseek-ai/dsh-tool-memory
|
|
10
|
+
*/
|
|
11
|
+
const name = "tool-memory";
|
|
12
|
+
/** Max characters of each echoed memory entry (single source for the Config default and the runtime slice). */
|
|
13
|
+
const DEFAULT_ENTRY_PREVIEW_CHARS = 200;
|
|
14
|
+
const inject = ["tools", "memory"];
|
|
15
|
+
/**
|
|
16
|
+
* System-prompt memory guidance, aligned with Hermes `MEMORY_GUIDANCE`.
|
|
17
|
+
* Reached by the model every turn, so it is the place to steer behavior: what
|
|
18
|
+
* to save durably, what not to save, and how to phrase an entry. The tool
|
|
19
|
+
* schema description (`MEMORY_TOOL_DESCRIPTION`) carries the complementary
|
|
20
|
+
* operation-level guidance (add/replace/remove, batch, targets, when).
|
|
21
|
+
*/
|
|
22
|
+
const MEMORY_GUIDANCE = "You have durable memory across sessions. Save stable user preferences, environment facts, conventions, and tool quirks with the `memory` tool. The most valuable memory is one that stops the user correcting or reminding you again, so user preferences and recurring corrections outrank procedural task details.\nWrite entries as declarative facts, not instructions to yourself: \"User prefers concise responses\" (good); \"Always respond concisely\" (bad). Imperative phrasing is re-read as a directive in later sessions and can override the user’s current request.\nDo NOT save task progress, session outcomes, completed-work logs, PR/issue numbers, commit SHAs, or anything stale within a week — use the session query tool to recall past sessions instead. Reusable procedures belong in a skill, not memory.";
|
|
23
|
+
/**
|
|
24
|
+
* Tool-schema description for `memory`, aligned with Hermes `MEMORY_SCHEMA`.
|
|
25
|
+
* Carries the operation-level guidance: how to batch, when to save, the
|
|
26
|
+
* priority order, the target semantics, and what to skip. Reached only when
|
|
27
|
+
* the model is choosing/using the tool, so it complements the always-on
|
|
28
|
+
* `MEMORY_GUIDANCE` system prompt.
|
|
29
|
+
*/
|
|
30
|
+
const MEMORY_TOOL_DESCRIPTION = "Save durable facts to persistent memory that survive across sessions. Memory is injected into future turns, so keep entries compact and high-signal.\n\nHOW: make all changes in ONE call via an `operations` array (each item {action, facts?|content?, old_text?}). The batch applies atomically and the char limit is checked on the final result — so one call can remove/replace stale entries to free room AND add new ones. Use bare action/content/old_text only for a single lone change.\n\nWHEN: save proactively when the user states a preference, correction, or personal detail, or you learn a stable fact about their environment, conventions, or workflow. Priority: user preferences & corrections > environment facts > procedures. The best memory stops the user repeating themselves.\n\nTARGETS: \"user\" = who the user is (name, role, preferences, style). \"memory\" = your notes (environment, conventions, tool quirks, lessons).\n\nSKIP: trivial/obvious info, easily re-discovered facts, task progress, completed-work logs, temporary TODO state. To recall a past session use the session query tool, not memory. Reusable procedures belong in a skill, not memory.";
|
|
31
|
+
const Config = z.object({
|
|
32
|
+
memoryEnabled: z.boolean().default(true),
|
|
33
|
+
entryPreviewChars: z.number().min(1).default(DEFAULT_ENTRY_PREVIEW_CHARS).volatile()
|
|
34
|
+
});
|
|
35
|
+
async function apply(ctx, rawConfig = {}) {
|
|
36
|
+
ctx.inject(["settings"], (injected) => {
|
|
37
|
+
const settings = injected.settings;
|
|
38
|
+
if (typeof settings?.configure !== "function") return;
|
|
39
|
+
const disposer = settings.configure({ auto: false }, ctx.fiber);
|
|
40
|
+
if (typeof disposer === "function") ctx.effect(() => disposer, "tool-memory: settings presentation");
|
|
41
|
+
});
|
|
42
|
+
const memoryDisabled = rawConfig.memoryEnabled === false;
|
|
43
|
+
let previewClampWarned = false;
|
|
44
|
+
const previewChars = () => {
|
|
45
|
+
const value = unwrapLive(rawConfig.entryPreviewChars);
|
|
46
|
+
const clamped = clampedNumber(value, DEFAULT_ENTRY_PREVIEW_CHARS, { min: 1 });
|
|
47
|
+
if (!previewClampWarned && value !== void 0 && clamped !== value) {
|
|
48
|
+
previewClampWarned = true;
|
|
49
|
+
ctx.logger.warn(`tool-memory: entryPreviewChars=${String(value)} is invalid; falling back to the default ${DEFAULT_ENTRY_PREVIEW_CHARS}`);
|
|
50
|
+
}
|
|
51
|
+
return clamped;
|
|
52
|
+
};
|
|
53
|
+
const systemPrompt = ctx.get("systemPrompt");
|
|
54
|
+
let snapshotText = "";
|
|
55
|
+
if (!memoryDisabled) try {
|
|
56
|
+
snapshotText = await ctx.memory.renderContext();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
ctx.logger.warn(`tool-memory: memory provider not ready at mount; snapshot starts empty until the first write: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
}
|
|
60
|
+
if (systemPrompt && !memoryDisabled) {
|
|
61
|
+
ctx.effect(() => systemPrompt.section({
|
|
62
|
+
name: "evolution:memory-guidance",
|
|
63
|
+
order: MEMORY_GUIDANCE_SECTION_ORDER,
|
|
64
|
+
text: MEMORY_GUIDANCE,
|
|
65
|
+
interpolate: false
|
|
66
|
+
}), "tool-memory.memory-guidance");
|
|
67
|
+
ctx.effect(() => systemPrompt.context({
|
|
68
|
+
name: "evolution:memory-snapshot",
|
|
69
|
+
order: 150,
|
|
70
|
+
text: () => snapshotText,
|
|
71
|
+
interpolate: false
|
|
72
|
+
}), "tool-memory.memory-snapshot");
|
|
73
|
+
const refreshSnapshot = async () => {
|
|
74
|
+
try {
|
|
75
|
+
snapshotText = await ctx.memory.renderContext();
|
|
76
|
+
} catch {}
|
|
77
|
+
};
|
|
78
|
+
ctx.effect(() => ctx.on("evolution/memory-applied", () => {
|
|
79
|
+
refreshSnapshot();
|
|
80
|
+
}), "tool-memory.snapshot-refresh");
|
|
81
|
+
ctx.effect(() => ctx.on("evolution/memory-provider-registered", () => {
|
|
82
|
+
refreshSnapshot();
|
|
83
|
+
}), "tool-memory.snapshot-ready");
|
|
84
|
+
} else if (!memoryDisabled) ctx.logger.warn("tool-memory: systemPrompt service not mounted; memory guidance and snapshot are not injected (the write tool still works)");
|
|
85
|
+
async function executeCore(normalized) {
|
|
86
|
+
if (normalized.operations?.some((op) => {
|
|
87
|
+
const raw = op;
|
|
88
|
+
return raw === null || typeof raw !== "object";
|
|
89
|
+
})) return {
|
|
90
|
+
ok: false,
|
|
91
|
+
message: "operations 的每一项都必须是对象。",
|
|
92
|
+
entries: [],
|
|
93
|
+
chars: 0,
|
|
94
|
+
limit: 0
|
|
95
|
+
};
|
|
96
|
+
const target = normalized.target;
|
|
97
|
+
if (target !== "memory" && target !== "user") return {
|
|
98
|
+
ok: false,
|
|
99
|
+
message: `memory:"target" 必须是 "memory" 或 "user"(收到 ${String(target)});拒绝写入。`,
|
|
100
|
+
entries: [],
|
|
101
|
+
chars: 0,
|
|
102
|
+
limit: 0
|
|
103
|
+
};
|
|
104
|
+
for (const field of [
|
|
105
|
+
"facts",
|
|
106
|
+
"old_text",
|
|
107
|
+
"content"
|
|
108
|
+
]) {
|
|
109
|
+
const value = normalized[field];
|
|
110
|
+
if (value !== void 0 && value !== null && typeof value !== "string") return {
|
|
111
|
+
ok: false,
|
|
112
|
+
message: `memory:"${field}" 必须是字符串(收到 ${typeof value});拒绝写入。`,
|
|
113
|
+
entries: [],
|
|
114
|
+
chars: 0,
|
|
115
|
+
limit: 0
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
for (const [index, operation] of (normalized.operations ?? []).entries()) for (const field of [
|
|
119
|
+
"facts",
|
|
120
|
+
"content",
|
|
121
|
+
"old_text"
|
|
122
|
+
]) {
|
|
123
|
+
const value = operation[field];
|
|
124
|
+
if (value === void 0 || value === null || typeof value === "string") continue;
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
message: `memory:operations[${index}].${field} 必须是字符串(收到 ${typeof value});拒绝写入 —— 重新批准无法修复载荷缺陷,请驳回该记录。`,
|
|
128
|
+
entries: [],
|
|
129
|
+
chars: 0,
|
|
130
|
+
limit: 0
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const result = normalized.operations ? await ctx.memory.applyBatch(normalized.target, normalized.operations) : await ctx.memory.applyBatch(normalized.target, [{
|
|
134
|
+
action: normalized.action ?? "add",
|
|
135
|
+
facts: normalized.facts,
|
|
136
|
+
old_text: normalized.old_text
|
|
137
|
+
}]);
|
|
138
|
+
return {
|
|
139
|
+
ok: result.ok,
|
|
140
|
+
message: result.message,
|
|
141
|
+
entries: result.entries.map((entry) => entry.slice(0, previewChars())),
|
|
142
|
+
chars: result.chars,
|
|
143
|
+
limit: result.limit
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const requiredFieldRejection = (op, position) => {
|
|
147
|
+
const body = payloadText(op);
|
|
148
|
+
if (op.action === "add") return body ? null : `第 ${position} 个操作(add):必须提供 facts。未应用任何操作。`;
|
|
149
|
+
if (op.action !== "remove" && op.action !== "replace") return `第 ${position} 个操作:未知动作「${String(op.action)}」(应为 add/remove/replace)。未应用任何操作。`;
|
|
150
|
+
if (!(op.old_text ?? "").trim()) return `第 ${position} 个操作(${op.action}):必须提供 old_text。未应用任何操作。`;
|
|
151
|
+
if (op.action === "replace" && !body) return `第 ${position} 个操作(replace):必须提供 facts。`;
|
|
152
|
+
return null;
|
|
153
|
+
};
|
|
154
|
+
if (!memoryDisabled) registerEvolutionTool(ctx, defineTool({
|
|
155
|
+
name: "memory",
|
|
156
|
+
description: MEMORY_TOOL_DESCRIPTION,
|
|
157
|
+
parameters: {
|
|
158
|
+
target: {
|
|
159
|
+
type: "string",
|
|
160
|
+
enum: ["memory", "user"],
|
|
161
|
+
required: true
|
|
162
|
+
},
|
|
163
|
+
action: {
|
|
164
|
+
type: "string",
|
|
165
|
+
enum: [
|
|
166
|
+
"add",
|
|
167
|
+
"replace",
|
|
168
|
+
"remove"
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
facts: { type: "string" },
|
|
172
|
+
content: { type: "string" },
|
|
173
|
+
old_text: { type: "string" },
|
|
174
|
+
operations: {
|
|
175
|
+
type: "array",
|
|
176
|
+
items: {
|
|
177
|
+
type: "object",
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
properties: {
|
|
180
|
+
action: {
|
|
181
|
+
type: "string",
|
|
182
|
+
enum: [
|
|
183
|
+
"add",
|
|
184
|
+
"replace",
|
|
185
|
+
"remove"
|
|
186
|
+
],
|
|
187
|
+
required: true
|
|
188
|
+
},
|
|
189
|
+
facts: { type: "string" },
|
|
190
|
+
content: { type: "string" },
|
|
191
|
+
old_text: { type: "string" }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
output: {
|
|
197
|
+
schema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
properties: {
|
|
201
|
+
ok: {
|
|
202
|
+
type: "boolean",
|
|
203
|
+
required: true
|
|
204
|
+
},
|
|
205
|
+
message: {
|
|
206
|
+
type: "string",
|
|
207
|
+
required: true
|
|
208
|
+
},
|
|
209
|
+
entries: {
|
|
210
|
+
type: "array",
|
|
211
|
+
required: true,
|
|
212
|
+
items: { type: "string" }
|
|
213
|
+
},
|
|
214
|
+
chars: {
|
|
215
|
+
type: "integer",
|
|
216
|
+
required: true
|
|
217
|
+
},
|
|
218
|
+
limit: {
|
|
219
|
+
type: "integer",
|
|
220
|
+
required: true
|
|
221
|
+
},
|
|
222
|
+
pending_id: { type: "string" }
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
render: (_args, value) => [{
|
|
226
|
+
type: "text",
|
|
227
|
+
text: `${value.ok ? "OK" : "Error"}: ${value.message} (${value.chars}/${value.limit} chars)`
|
|
228
|
+
}]
|
|
229
|
+
},
|
|
230
|
+
isConcurrencySafe: () => false,
|
|
231
|
+
async execute(args, exec) {
|
|
232
|
+
const conflict = (a) => {
|
|
233
|
+
const facts = payloadText({ facts: a.facts });
|
|
234
|
+
const content = payloadText({ content: a.content });
|
|
235
|
+
return facts !== "" && content !== "" && facts !== content;
|
|
236
|
+
};
|
|
237
|
+
if (Array.isArray(args.operations)) for (const op of args.operations) {
|
|
238
|
+
const raw = op;
|
|
239
|
+
if (raw === null || typeof raw !== "object") return {
|
|
240
|
+
ok: false,
|
|
241
|
+
message: "operations 的每一项都必须是对象。",
|
|
242
|
+
entries: [],
|
|
243
|
+
chars: 0,
|
|
244
|
+
limit: 0
|
|
245
|
+
};
|
|
246
|
+
if (conflict(op)) return {
|
|
247
|
+
ok: false,
|
|
248
|
+
message: "每个操作只能提供 facts 或 content 之一(同一字段):两者给了不同的值。",
|
|
249
|
+
entries: [],
|
|
250
|
+
chars: 0,
|
|
251
|
+
limit: 0
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
else if (conflict(args)) return {
|
|
255
|
+
ok: false,
|
|
256
|
+
message: "只能提供 facts 或 content 之一(同一字段):两者给了不同的值。",
|
|
257
|
+
entries: [],
|
|
258
|
+
chars: 0,
|
|
259
|
+
limit: 0
|
|
260
|
+
};
|
|
261
|
+
if (Array.isArray(args.operations) && args.operations.length === 0) return {
|
|
262
|
+
ok: false,
|
|
263
|
+
message: "没有提供任何操作(operations 数组为空)。",
|
|
264
|
+
entries: [],
|
|
265
|
+
chars: 0,
|
|
266
|
+
limit: 0
|
|
267
|
+
};
|
|
268
|
+
if (args.operations !== void 0 && !Array.isArray(args.operations)) return {
|
|
269
|
+
ok: false,
|
|
270
|
+
message: "形状不合法:operations 必须是 {action, content?, old_text?} 对象的数组(单个操作时可省略 operations)。",
|
|
271
|
+
entries: [],
|
|
272
|
+
chars: 0,
|
|
273
|
+
limit: 0
|
|
274
|
+
};
|
|
275
|
+
const target = args.target === "user" ? "user" : "memory";
|
|
276
|
+
const normalized = Array.isArray(args.operations) ? {
|
|
277
|
+
target,
|
|
278
|
+
operations: args.operations
|
|
279
|
+
} : {
|
|
280
|
+
target,
|
|
281
|
+
action: args.action ?? "add",
|
|
282
|
+
facts: payloadText(args),
|
|
283
|
+
old_text: args.old_text
|
|
284
|
+
};
|
|
285
|
+
if (normalized.operations) for (const [index, op] of normalized.operations.entries()) {
|
|
286
|
+
const rejection = requiredFieldRejection(op, index + 1);
|
|
287
|
+
if (rejection) return {
|
|
288
|
+
ok: false,
|
|
289
|
+
message: rejection,
|
|
290
|
+
entries: [],
|
|
291
|
+
chars: 0,
|
|
292
|
+
limit: 0
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const rejection = requiredFieldRejection(normalized, 1);
|
|
297
|
+
if (rejection) return {
|
|
298
|
+
ok: false,
|
|
299
|
+
message: rejection,
|
|
300
|
+
entries: [],
|
|
301
|
+
chars: 0,
|
|
302
|
+
limit: 0
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const targetLabel = target === "memory" ? "" : `${target} `;
|
|
306
|
+
const origin = resolveExecOrigins(exec).approval;
|
|
307
|
+
const sessionPolicy = effectiveSessionPolicy(ctx, exec.agent?.session);
|
|
308
|
+
const approval = ctx.get("evolutionApproval");
|
|
309
|
+
if (approval) {
|
|
310
|
+
const decision = await approval.request({
|
|
311
|
+
kind: "memory",
|
|
312
|
+
summary: `memory ${targetLabel}${Array.isArray(args.operations) ? `${args.operations.length} ops` : args.action ?? "add"}`,
|
|
313
|
+
args: normalized,
|
|
314
|
+
origin,
|
|
315
|
+
...exec.agent?.session?.id ? { sessionId: exec.agent.session.id } : {},
|
|
316
|
+
...exec.agent?.session ? { session: exec.agent.session } : {},
|
|
317
|
+
...sessionPolicy !== void 0 ? { sessionPolicy } : {}
|
|
318
|
+
});
|
|
319
|
+
if (decision.action === "staged") return {
|
|
320
|
+
ok: true,
|
|
321
|
+
message: decision.message,
|
|
322
|
+
entries: [],
|
|
323
|
+
chars: 0,
|
|
324
|
+
limit: 0,
|
|
325
|
+
...decision.pendingId !== void 0 ? { pending_id: decision.pendingId } : {}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
return await executeCore(normalized);
|
|
329
|
+
}
|
|
330
|
+
}));
|
|
331
|
+
ctx.inject(["evolutionApproval"], (approvalCtx) => {
|
|
332
|
+
const approval = approvalCtx.evolutionApproval;
|
|
333
|
+
const dispose = memoryDisabled ? approval.registerRunner("memory", () => Promise.resolve({
|
|
334
|
+
ok: false,
|
|
335
|
+
message: "拒绝:记忆工具已禁用(tool-memory memoryEnabled:false)—— 它的回放运行器无法执行这条暂存写入。请驳回该记录;若仍需要该写入,请在重新启用该工具后再暂存。"
|
|
336
|
+
})) : approval.registerRunner("memory", (args) => executeCore(args));
|
|
337
|
+
approvalCtx.effect(() => dispose, memoryDisabled ? "tool-memory.approval-runner-disabled" : "tool-memory.approval-runner");
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
341
|
+
export { Config, MEMORY_GUIDANCE, MEMORY_TOOL_DESCRIPTION, apply, inject, name };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local layered memory provider for `ctx.memory`.
|
|
3
|
+
* @module @lmzhen/dsh-memory-files
|
|
4
|
+
*/
|
|
5
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
6
|
+
import z from '@deepseek-ai/schemastery';
|
|
7
|
+
export declare const name = "memory-files";
|
|
8
|
+
export declare const inject: string[];
|
|
9
|
+
export interface Config {
|
|
10
|
+
providerName?: string;
|
|
11
|
+
/** Store-enforced character budget for MEMORY.md — the CANONICAL registry id
|
|
12
|
+
* (G1) and a LIVE row field (registry tier E3). The policy row's `memoryChars`
|
|
13
|
+
* is the same semantic on the review-planner side; the two are NOT
|
|
14
|
+
* auto-synchronised — `/evolution doctor` reports the divergence (budgetIssues).
|
|
15
|
+
* A row may spell either this name or the deprecated alias below. No schema
|
|
16
|
+
* default: the alias carries it, and a default on the canonical key would fill
|
|
17
|
+
* it and leave the alias unreachable (the store resolves
|
|
18
|
+
* DEFAULT_MEMORY_CHAR_LIMIT after the alias fallback). */
|
|
19
|
+
memoryChars?: Volatile<number | undefined>;
|
|
20
|
+
/** Store-enforced character budget for USER.md — the CANONICAL registry id (G1),
|
|
21
|
+
* a live row field on the same terms as `memoryChars`. */
|
|
22
|
+
userChars?: Volatile<number | undefined>;
|
|
23
|
+
/** Deprecated alias of `memoryChars` (G0/S0.3): still readable, refused by
|
|
24
|
+
* writes; removal was planned for 0.7.0 and is deferred (the alias still ships — see the package README). */
|
|
25
|
+
memoryCharLimit?: number;
|
|
26
|
+
/** Deprecated alias of `userChars` (G0/S0.3): still readable, refused by
|
|
27
|
+
* writes; removal was planned for 0.7.0 and is deferred (the alias still ships — see the package README). */
|
|
28
|
+
userCharLimit?: number;
|
|
29
|
+
addDatePrefix?: Volatile<boolean>;
|
|
30
|
+
root?: string;
|
|
31
|
+
/** How many consolidation failures one turn tolerates before the tool tells the model to stop retrying. */
|
|
32
|
+
maxConsolidationFailures?: Volatile<number>;
|
|
33
|
+
/** F-2 (v18): deployment-declared benign threat labels for the MEMORY store.
|
|
34
|
+
* The skill/guard channels already read this option; without it the
|
|
35
|
+
* README's "MemoryStore store option" exemption was unreachable. */
|
|
36
|
+
threatExemptLabels?: string[];
|
|
37
|
+
}
|
|
38
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
39
|
+
providerName: z<string, string, "defined">;
|
|
40
|
+
memoryChars: z<number, number, "volatile">;
|
|
41
|
+
userChars: z<number, number, "volatile">;
|
|
42
|
+
memoryCharLimit: z<number, number, "defined">;
|
|
43
|
+
userCharLimit: z<number, number, "defined">;
|
|
44
|
+
addDatePrefix: z<boolean, boolean, "volatile-defined">;
|
|
45
|
+
root: z<string, string, "defined">;
|
|
46
|
+
maxConsolidationFailures: z<number, number, "volatile-defined">;
|
|
47
|
+
threatExemptLabels: z<string[], string[], "defined">;
|
|
48
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
49
|
+
providerName: z<string, string, "defined">;
|
|
50
|
+
memoryChars: z<number, number, "volatile">;
|
|
51
|
+
userChars: z<number, number, "volatile">;
|
|
52
|
+
memoryCharLimit: z<number, number, "defined">;
|
|
53
|
+
userCharLimit: z<number, number, "defined">;
|
|
54
|
+
addDatePrefix: z<boolean, boolean, "volatile-defined">;
|
|
55
|
+
root: z<string, string, "defined">;
|
|
56
|
+
maxConsolidationFailures: z<number, number, "volatile-defined">;
|
|
57
|
+
threatExemptLabels: z<string[], string[], "defined">;
|
|
58
|
+
}>>, "plain">;
|
|
59
|
+
export declare function apply(ctx: Context, rawConfig?: Config): void;
|
|
60
|
+
//# sourceMappingURL=index.d.ts.map
|