dsh-prime-memory 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.
Files changed (151) hide show
  1. package/CHANGELOG.en.md +28 -0
  2. package/CHANGELOG.ja.md +30 -0
  3. package/CHANGELOG.ko.md +30 -0
  4. package/CHANGELOG.md +1220 -0
  5. package/ENGINEERING-NOTES.md +452 -0
  6. package/INSTALL.en.md +92 -0
  7. package/INSTALL.ja.md +92 -0
  8. package/INSTALL.ko.md +92 -0
  9. package/INSTALL.md +92 -0
  10. package/LICENSE +21 -0
  11. package/README.en.md +458 -0
  12. package/README.ja.md +306 -0
  13. package/README.ko.md +306 -0
  14. package/README.md +425 -0
  15. package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
  16. package/assets/changelog/0.8.9/01-panel.png +0 -0
  17. package/assets/changelog/0.8.9/02-halo.png +0 -0
  18. package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
  19. package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
  20. package/assets/img/EmbeddingSource.png +0 -0
  21. package/assets/img/Hero.png +0 -0
  22. package/assets/img/Layers.png +0 -0
  23. package/assets/img/MemoryTools.png +0 -0
  24. package/assets/img/Modes.png +0 -0
  25. package/assets/img/ToolTrajectory.png +0 -0
  26. package/assets/img/ui-dark.jpg +0 -0
  27. package/assets/img/ui-light.jpg +0 -0
  28. package/assets/readme/bench-dialog.svg +70 -0
  29. package/assets/readme/bench-workflow.svg +79 -0
  30. package/assets/readme/flow.svg +189 -0
  31. package/assets/readme/storage.svg +115 -0
  32. package/cordis.patch.yml +16 -0
  33. package/dist/bench-control.d.ts +34 -0
  34. package/dist/bench-control.js +16 -0
  35. package/dist/client.js +4293 -0
  36. package/dist/config.d.ts +683 -0
  37. package/dist/config.js +129 -0
  38. package/dist/contract.d.ts +820 -0
  39. package/dist/contract.js +1 -0
  40. package/dist/embedding-worker.cjs +176 -0
  41. package/dist/graph/apply.d.ts +37 -0
  42. package/dist/graph/apply.js +270 -0
  43. package/dist/graph/constraints.d.ts +47 -0
  44. package/dist/graph/constraints.js +38 -0
  45. package/dist/graph/search.d.ts +16 -0
  46. package/dist/graph/search.js +115 -0
  47. package/dist/graph/types.d.ts +142 -0
  48. package/dist/graph/types.js +14 -0
  49. package/dist/hooks/capture.d.ts +32 -0
  50. package/dist/hooks/capture.js +194 -0
  51. package/dist/hooks/recall.d.ts +63 -0
  52. package/dist/hooks/recall.js +429 -0
  53. package/dist/index.d.ts +534 -0
  54. package/dist/index.js +344 -0
  55. package/dist/llm-usage.d.ts +26 -0
  56. package/dist/llm-usage.js +37 -0
  57. package/dist/llm.d.ts +153 -0
  58. package/dist/llm.js +530 -0
  59. package/dist/pipeline/graph.d.ts +35 -0
  60. package/dist/pipeline/graph.js +104 -0
  61. package/dist/pipeline/l1.d.ts +19 -0
  62. package/dist/pipeline/l1.js +271 -0
  63. package/dist/pipeline/l2.d.ts +13 -0
  64. package/dist/pipeline/l2.js +83 -0
  65. package/dist/pipeline/l3.d.ts +15 -0
  66. package/dist/pipeline/l3.js +78 -0
  67. package/dist/pipeline/rebuild.d.ts +61 -0
  68. package/dist/pipeline/rebuild.js +307 -0
  69. package/dist/pipeline/ruminate.d.ts +89 -0
  70. package/dist/pipeline/ruminate.js +298 -0
  71. package/dist/pipeline/runner.d.ts +167 -0
  72. package/dist/pipeline/runner.js +638 -0
  73. package/dist/pipeline/trigger.d.ts +40 -0
  74. package/dist/pipeline/trigger.js +75 -0
  75. package/dist/prompts/graph-projection.d.ts +70 -0
  76. package/dist/prompts/graph-projection.js +167 -0
  77. package/dist/prompts/l1-dedup.d.ts +22 -0
  78. package/dist/prompts/l1-dedup.js +251 -0
  79. package/dist/prompts/l1-extraction.d.ts +22 -0
  80. package/dist/prompts/l1-extraction.js +457 -0
  81. package/dist/prompts/persona.d.ts +23 -0
  82. package/dist/prompts/persona.js +240 -0
  83. package/dist/prompts/scene.d.ts +32 -0
  84. package/dist/prompts/scene.js +414 -0
  85. package/dist/runtime-package-lock.json +982 -0
  86. package/dist/settings.d.ts +50 -0
  87. package/dist/settings.js +355 -0
  88. package/dist/stats.d.ts +109 -0
  89. package/dist/stats.js +929 -0
  90. package/dist/store/bm25.d.ts +19 -0
  91. package/dist/store/bm25.js +63 -0
  92. package/dist/store/cost-ledger.d.ts +75 -0
  93. package/dist/store/cost-ledger.js +171 -0
  94. package/dist/store/download-queue.d.ts +79 -0
  95. package/dist/store/download-queue.js +424 -0
  96. package/dist/store/embedding-source.d.ts +118 -0
  97. package/dist/store/embedding-source.js +443 -0
  98. package/dist/store/embedding.d.ts +90 -0
  99. package/dist/store/embedding.js +206 -0
  100. package/dist/store/graph-store.d.ts +94 -0
  101. package/dist/store/graph-store.js +641 -0
  102. package/dist/store/l0.d.ts +40 -0
  103. package/dist/store/l0.js +197 -0
  104. package/dist/store/l1.d.ts +93 -0
  105. package/dist/store/l1.js +297 -0
  106. package/dist/store/local-embedding.d.ts +89 -0
  107. package/dist/store/local-embedding.js +227 -0
  108. package/dist/store/model-catalog.d.ts +48 -0
  109. package/dist/store/model-catalog.js +81 -0
  110. package/dist/store/occupancy.d.ts +30 -0
  111. package/dist/store/occupancy.js +134 -0
  112. package/dist/store/pending.d.ts +36 -0
  113. package/dist/store/pending.js +103 -0
  114. package/dist/store/persona.d.ts +15 -0
  115. package/dist/store/persona.js +60 -0
  116. package/dist/store/recall-dedupe.d.ts +26 -0
  117. package/dist/store/recall-dedupe.js +138 -0
  118. package/dist/store/runtime-installer.d.ts +59 -0
  119. package/dist/store/runtime-installer.js +243 -0
  120. package/dist/store/scenes.d.ts +24 -0
  121. package/dist/store/scenes.js +160 -0
  122. package/dist/store/search-utils.d.ts +38 -0
  123. package/dist/store/search-utils.js +100 -0
  124. package/dist/store/session-modes.d.ts +35 -0
  125. package/dist/store/session-modes.js +144 -0
  126. package/dist/store/sqlite.d.ts +246 -0
  127. package/dist/store/sqlite.js +1491 -0
  128. package/dist/store/state.d.ts +41 -0
  129. package/dist/store/state.js +72 -0
  130. package/dist/token-cost.d.ts +23 -0
  131. package/dist/token-cost.js +185 -0
  132. package/dist/tools/index.d.ts +34 -0
  133. package/dist/tools/index.js +758 -0
  134. package/dist/types.d.ts +139 -0
  135. package/dist/types.js +38 -0
  136. package/dist/util/context-occupancy.d.ts +68 -0
  137. package/dist/util/context-occupancy.js +92 -0
  138. package/dist/util/filelog.d.ts +6 -0
  139. package/dist/util/filelog.js +108 -0
  140. package/dist/util/io.d.ts +18 -0
  141. package/dist/util/io.js +97 -0
  142. package/dist/util/recall-budget.d.ts +31 -0
  143. package/dist/util/recall-budget.js +84 -0
  144. package/dist/util/sanitize.d.ts +11 -0
  145. package/dist/util/sanitize.js +67 -0
  146. package/dist/util/text.d.ts +16 -0
  147. package/dist/util/text.js +61 -0
  148. package/dist/util/tokenizer.d.ts +9 -0
  149. package/dist/util/tokenizer.js +50 -0
  150. package/dsh.plugin.json +22 -0
  151. package/package.json +118 -0
@@ -0,0 +1,758 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools';
2
+ import { normPersistence } from '../types.js';
3
+ import { GRAPH_STATUS_LABELS } from '../prompts/graph-projection.js';
4
+ const OFF_NOTICE = '本会话的记忆档位为"关闭":该会话对记忆系统完全隐身,不读取也不写入记忆。';
5
+ const WRITE_ONLY_NOTICE = '本会话为只写模式:记忆照常沉淀,但不读取。';
6
+ const GLOBAL_OFF_NOTICE = '记忆注入已全局停用:本会话不读取记忆(沉淀照常)。';
7
+ export function registerMemoryTools(ctx, cfg, stores, logger, modes, live,
8
+ /** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
9
+ ruminate) {
10
+ if (!cfg.tools)
11
+ return;
12
+ /**
13
+ * 调用会话的检索族(auto → undefined 不过滤;off/只写 → null 表示整体禁用)。
14
+ * fail-open:exec.agent 缺失(宿主调用路径未带 agent 标识)按全族检索放行——
15
+ * 档位隔离依赖宿主正确传递 exec.agent.id,缺失只告警一次不拒绝工具调用。
16
+ */
17
+ let warnedNoAgent = false;
18
+ const familyOfCaller = (agentId) => {
19
+ if (agentId === undefined) {
20
+ if (!warnedNoAgent) {
21
+ warnedNoAgent = true;
22
+ logger.warn('[memory] 工具调用缺少 agent 标识(exec.agent 未传递),档位过滤退化为全族检索');
23
+ }
24
+ return undefined;
25
+ }
26
+ const mode = modes.get(agentId);
27
+ if (mode === 'off')
28
+ return null;
29
+ // 只写会话拒读:与注入同属读维度,不拒则"不注入"从工具路径漏风
30
+ if (!modes.resolvedRecall(agentId, live.get().recall))
31
+ return null;
32
+ return mode === 'auto' ? undefined : mode;
33
+ };
34
+ /** 拒读时的归因文案(familyOfCaller 判 null 后重查内存 Map,成本可忽略):
35
+ * off 完全隐身 / 会话只写覆盖 / 全局召回关——三种停用各说各话,不谎报只写。 */
36
+ const blockNoticeOf = (agentId) => {
37
+ if (agentId !== undefined) {
38
+ if (modes.get(agentId) === 'off')
39
+ return OFF_NOTICE;
40
+ if (modes.getRecall(agentId) === false)
41
+ return WRITE_ONLY_NOTICE;
42
+ if (!modes.resolvedRecall(agentId, live.get().recall))
43
+ return GLOBAL_OFF_NOTICE;
44
+ }
45
+ return OFF_NOTICE;
46
+ };
47
+ // ── memory_search: L1 结构化记忆 ──
48
+ ctx.tools.register(defineTool({
49
+ name: 'memory_search',
50
+ description: '搜索结构化记忆(L1 原子记忆)。返回与查询相关的记忆片段:用户偏好、历史事件、项目事实、任务、规则、工作方法等。',
51
+ parameters: {
52
+ query: { type: 'string', required: true, description: '搜索查询文本(自然语言)' },
53
+ limit: { type: 'number', description: '最大返回条数(默认 5)' },
54
+ type: { type: 'string', description: '按记忆类型过滤(如 persona/episodic/instruction/work_fact/work_task/work_method/work_artifact)' },
55
+ },
56
+ output: {
57
+ schema: {
58
+ type: 'object',
59
+ properties: {
60
+ items: {
61
+ type: 'array',
62
+ items: {
63
+ type: 'object',
64
+ properties: {
65
+ content: { type: 'string' },
66
+ type: { type: 'string' },
67
+ scene_name: { type: 'string' },
68
+ score: { type: 'number' },
69
+ },
70
+ additionalProperties: false,
71
+ },
72
+ },
73
+ notice: { type: 'string', description: '非搜索结果的状态提示(如本会话记忆已关闭)' },
74
+ },
75
+ additionalProperties: false,
76
+ },
77
+ render: (_args, value) => [
78
+ { type: 'text', text: value.notice ?? renderMemoryItems(value.items ?? []) },
79
+ ],
80
+ },
81
+ execute: async (args, exec) => {
82
+ const family = familyOfCaller(exec.agent?.id);
83
+ if (family === null)
84
+ return { items: [], notice: blockNoticeOf(exec.agent?.id) };
85
+ const limit = Math.min(Math.max(args.limit ?? 5, 1), 20);
86
+ const hits = await stores.l1.search(args.query, limit, { type: args.type || undefined, family: family ?? undefined });
87
+ return {
88
+ items: hits.map((h) => ({
89
+ content: h.content,
90
+ type: h.type,
91
+ scene_name: h.scene_name,
92
+ score: Math.round(h.score * 100) / 100,
93
+ })),
94
+ };
95
+ },
96
+ }));
97
+ // ── conversation_search: L0 原始对话 ──
98
+ ctx.tools.register(defineTool({
99
+ name: 'conversation_search',
100
+ description: '搜索原始对话历史(L0)。返回带时间戳的原始消息,适用于查找具体消息原文、时间线、上下文细节。',
101
+ parameters: {
102
+ query: { type: 'string', required: true, description: '搜索查询文本' },
103
+ limit: { type: 'number', description: '最大返回条数(默认 5)' },
104
+ },
105
+ output: {
106
+ schema: {
107
+ type: 'object',
108
+ properties: {
109
+ items: {
110
+ type: 'array',
111
+ items: {
112
+ type: 'object',
113
+ properties: {
114
+ session_id: { type: 'string' },
115
+ role: { type: 'string' },
116
+ content: { type: 'string' },
117
+ timestamp: { type: 'number' },
118
+ },
119
+ additionalProperties: false,
120
+ },
121
+ },
122
+ notice: { type: 'string', description: '非搜索结果的状态提示(如本会话记忆已关闭)' },
123
+ },
124
+ additionalProperties: false,
125
+ },
126
+ render: (_args, value) => [
127
+ { type: 'text', text: value.notice ?? renderConversationItems(value.items ?? []) },
128
+ ],
129
+ },
130
+ execute: async (args, exec) => {
131
+ if (familyOfCaller(exec.agent?.id) === null)
132
+ return { items: [], notice: blockNoticeOf(exec.agent?.id) };
133
+ const limit = Math.min(Math.max(args.limit ?? 5, 1), 20);
134
+ const records = await stores.l0.search(args.query, limit);
135
+ return {
136
+ items: records.map((r) => ({
137
+ session_id: r.sessionId,
138
+ role: r.role,
139
+ content: r.content,
140
+ timestamp: r.timestamp,
141
+ })),
142
+ };
143
+ },
144
+ }));
145
+ // ── memory_read_scene: 读取 L2 场景块 / L3 画像 ──
146
+ ctx.tools.register(defineTool({
147
+ name: 'memory_read_scene',
148
+ description: '读取记忆文件详情:L2 场景块(场景目录下的 .md 文件)或 L3 画像(persona-chat.md / persona-work.md)。返回文件完整内容。',
149
+ parameters: {
150
+ path: { type: 'string', required: true, description: '场景文件名,或 persona-chat.md / persona-work.md' },
151
+ },
152
+ output: {
153
+ schema: {
154
+ type: 'object',
155
+ properties: {
156
+ content: { type: 'string', description: '文件内容(不存在则为空字符串)' },
157
+ },
158
+ additionalProperties: false,
159
+ },
160
+ render: (_args, value) => [
161
+ { type: 'text', text: value.content ? `\`\`\`markdown\n${value.content}\n\`\`\`` : '(文件不存在或为空)' },
162
+ ],
163
+ },
164
+ execute: async (args, exec) => {
165
+ if (familyOfCaller(exec.agent?.id) === null)
166
+ return { content: blockNoticeOf(exec.agent?.id) };
167
+ const p = args.path.trim();
168
+ let content;
169
+ if (p === 'persona.md' || p === 'persona-chat.md' || p === 'persona' || p === 'persona-chat') {
170
+ content = await stores.persona.chat.read();
171
+ }
172
+ else if (p === 'persona-work.md' || p === 'persona-work') {
173
+ content = await stores.persona.work.read();
174
+ }
175
+ else {
176
+ // 场景文件在两族目录里按名查找(先本族后另一族)
177
+ const primary = familyOfCaller(exec.agent?.id) ?? 'chat';
178
+ const other = primary === 'chat' ? 'work' : 'chat';
179
+ content =
180
+ (await stores.scenes[primary].read(p)) ?? (await stores.scenes[other].read(p));
181
+ }
182
+ return { content: content ?? '' };
183
+ },
184
+ }));
185
+ // ── 写删工具(高权限门控):memory_add / memory_delete ──
186
+ const MUTATE_OFF_NOTICE = '记忆写删未开放:请在记忆库面板开启「高权限模式」后,模型才能写入/删除记忆。';
187
+ const ADD_TYPES = ['persona', 'episodic', 'instruction', 'work_fact', 'work_task', 'work_method', 'work_artifact'];
188
+ const newMemId = () => 'mem-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
189
+ // ── 工具刷写的公共装配(时间轴 + 溯源 + 冲突标记) ──
190
+ //
191
+ // 时间被拆成三条**互不替代**的轴:created_at/updated_at(进库/变更时刻)、
192
+ // valid_from/valid_to(事实在真实世界成立的时间区间)、persistence(是否会随时间失效)。
193
+ // 把事件发生时间填进 created_at 是最常见的误用——那会让时效衰减与"目前是否成立"
194
+ // 同时失准,所以两者在入参层面就分开。
195
+ const MAX_IMPORT = 200;
196
+ const DEFAULT_IMPORT_SCENE = '外部导入';
197
+ /** ISO/epoch → epoch ms;非法或非正一律 undefined(不猜测时间)。 */
198
+ const parseTime = (raw) => {
199
+ if (typeof raw !== 'string' && typeof raw !== 'number')
200
+ return undefined;
201
+ const t = typeof raw === 'number' ? raw : Date.parse(raw);
202
+ return Number.isFinite(t) && t > 0 ? t : undefined;
203
+ };
204
+ const toIsoOrNull = (ms) => ms === undefined ? null : new Date(ms).toISOString();
205
+ /**
206
+ * 入参 → L1 记录。
207
+ *
208
+ * 时间轴同时写顶层字段(时间增强列)与 metadata(列迁移前的兼容层,
209
+ * 也是面板与图谱时间锚的读取点);`cf`/`rw` 落在 metadata.conflict/rewritten。
210
+ */
211
+ function buildRecord(item, sceneName, now) {
212
+ const content = String(item.content ?? '').trim();
213
+ const type = ADD_TYPES.includes(String(item.type ?? '')) ? String(item.type) : 'episodic';
214
+ const family = type.startsWith('work') ? 'work' : 'chat';
215
+ const validFrom = parseTime(item.valid_from);
216
+ const validTo = parseTime(item.valid_to);
217
+ const persistence = normPersistence(item.persistence);
218
+ const createdAt = parseTime(item.created_at) ?? now;
219
+ const updatedAt = parseTime(item.updated_at) ?? createdAt;
220
+ const hall = typeof item.hall === 'string' && item.hall.trim() ? item.hall.trim().slice(0, 40) : undefined;
221
+ const origin = typeof item.origin === 'string' && item.origin.trim() ? item.origin.trim().slice(0, 200) : undefined;
222
+ const metadata = {
223
+ temporal: { st: persistence ?? '?', vf: toIsoOrNull(validFrom), vt: toIsoOrNull(validTo) },
224
+ };
225
+ if (hall)
226
+ metadata.hall = hall;
227
+ if (origin)
228
+ metadata.origin = origin;
229
+ if (validFrom !== undefined)
230
+ metadata.activity_start_time = toIsoOrNull(validFrom);
231
+ if (validTo !== undefined)
232
+ metadata.activity_end_time = toIsoOrNull(validTo);
233
+ if (item.conflict === true)
234
+ metadata.conflict = true;
235
+ if (item.rewritten === true)
236
+ metadata.rewritten = true;
237
+ const priority = Number(item.priority);
238
+ return {
239
+ id: newMemId(),
240
+ content,
241
+ type,
242
+ priority: Number.isFinite(priority) && priority >= 0 ? Math.min(priority, 100) : 80,
243
+ scene_name: sceneName,
244
+ timestamps: Array.from(new Set([validFrom ?? createdAt, createdAt, updatedAt])).sort((a, b) => a - b),
245
+ createdAt,
246
+ updatedAt,
247
+ version: 0,
248
+ metadata,
249
+ family,
250
+ ...(validFrom !== undefined ? { validFrom } : {}),
251
+ ...(validTo !== undefined ? { validTo } : {}),
252
+ ...(persistence !== undefined ? { persistence } : {}),
253
+ };
254
+ }
255
+ /** 时间轴/溯源字段的可选入参(两个写工具共用同一形状)。 */
256
+ const writeFields = {
257
+ type: {
258
+ type: 'string',
259
+ description: '记忆类型(persona/episodic/instruction/work_fact/work_task/work_method/work_artifact;缺省 episodic)',
260
+ },
261
+ hall: { type: 'string', description: '可选的粗分类 Hall(work/relationships/general/finance/journey)' },
262
+ persistence: {
263
+ type: 'string',
264
+ description: '持续性:t 无时间性(规则/偏好/恒真事实)、o 仍在持续、s 已结束的区间、p 时点事件;缺省=未判定',
265
+ },
266
+ valid_from: { type: 'string', description: '有效期起(ISO 8601):该事实在真实世界开始成立的时间' },
267
+ valid_to: { type: 'string', description: '有效期止(ISO 8601):留空表示尚未结束或无时间性' },
268
+ created_at: { type: 'string', description: '记录时间(ISO 8601):缺省为当前时刻' },
269
+ updated_at: { type: 'string', description: '变更时间(ISO 8601):缺省等于记录时间' },
270
+ origin: { type: 'string', description: '溯源:这条记忆来自哪里(文件路径/工具名/会话)' },
271
+ conflict: { type: 'boolean', description: '是否与库内既有记忆存在未裁决的冲突' },
272
+ rewritten: { type: 'boolean', description: '是否已被后续条目改写/取代' },
273
+ };
274
+ // memory_add:显式"记得X"直接落库一条 L1 记忆(绕过抽取管线,需高权限)。
275
+ ctx.tools.register(defineTool({
276
+ name: 'memory_add',
277
+ description: '直接写入一条结构化记忆(L1)。仅当用户显式要求"记住/记下 X"时用;需高权限模式开启。内容须是待记忆的事实/偏好/任务/规则,不应包含对话过程。',
278
+ parameters: {
279
+ content: { type: 'string', required: true, description: '要记忆的完整内容(一句话事实,语义完整)' },
280
+ ...writeFields,
281
+ scene: {
282
+ type: 'string',
283
+ description: '归属场景名(缺省 __manual__;导入外部记忆建议填"外部导入/<来源>")',
284
+ },
285
+ priority: { type: 'number', description: '优先级 0-100(缺省 80)' },
286
+ },
287
+ output: {
288
+ schema: {
289
+ type: 'object',
290
+ properties: {
291
+ id: { type: 'string' },
292
+ notice: { type: 'string' },
293
+ },
294
+ additionalProperties: false,
295
+ },
296
+ render: (_args, value) => [{ type: 'text', text: value.notice ?? ('已记录记忆 ' + (value.id ?? '')) }],
297
+ },
298
+ execute: async (args) => {
299
+ if (!live.get().memoryMutate)
300
+ return { notice: MUTATE_OFF_NOTICE };
301
+ const content = String(args.content ?? '').trim();
302
+ if (!content)
303
+ return { notice: 'content 为空,未写入' };
304
+ const scene = typeof args.scene === 'string' && args.scene.trim() ? args.scene.trim().slice(0, 120) : '__manual__';
305
+ const record = buildRecord(args, scene, Date.now());
306
+ await stores.l1.appendNew([record]);
307
+ logger.info(`[memory] 高权限写入记忆(${record.type}${record.metadata?.hall ? '/' + String(record.metadata.hall) : ''},时间轴 ${record.persistence ?? '?'}):${record.content.slice(0, 120)}`);
308
+ return { id: record.id };
309
+ },
310
+ }));
311
+ // memory_import:批量写入(工具刷写通道——外部记忆包导入/迁移,免逐条调用)。
312
+ ctx.tools.register(defineTool({
313
+ name: 'memory_import',
314
+ description: '批量写入多条记忆(L1)。用于把已整理好的外部记忆(其他 AI 工具导出的记忆包)一次性导入;需高权限模式开启。去重与冲突判定由调用方先行完成(见 memport Skill):本工具只做结构校验与批内重复拦截,不替调用方裁决语义冲突。单次上限 200 条。',
315
+ parameters: {
316
+ records: {
317
+ type: 'array',
318
+ required: true,
319
+ description: '待写入的记录数组(content 必填,其余字段语义同 memory_add)',
320
+ items: {
321
+ type: 'object',
322
+ additionalProperties: false,
323
+ properties: {
324
+ content: {
325
+ type: 'string',
326
+ required: true,
327
+ description: '要记忆的完整内容(一句话事实,语义完整)',
328
+ },
329
+ ...writeFields,
330
+ priority: { type: 'number', description: '优先级 0-100(缺省 80)' },
331
+ },
332
+ },
333
+ },
334
+ scene: {
335
+ type: 'string',
336
+ description: `归属场景名(缺省 ${DEFAULT_IMPORT_SCENE};建议用"${DEFAULT_IMPORT_SCENE}/<来源>")`,
337
+ },
338
+ },
339
+ output: {
340
+ schema: {
341
+ type: 'object',
342
+ properties: {
343
+ written: { type: 'number' },
344
+ ids: { type: 'array', items: { type: 'string' } },
345
+ skipped: {
346
+ type: 'array',
347
+ items: {
348
+ type: 'object',
349
+ properties: {
350
+ index: { type: 'number' },
351
+ reason: { type: 'string' },
352
+ },
353
+ additionalProperties: false,
354
+ },
355
+ },
356
+ notice: { type: 'string' },
357
+ },
358
+ additionalProperties: false,
359
+ },
360
+ render: (_args, value) => [
361
+ { type: 'text', text: value.notice ?? `已导入 ${value.written ?? 0} 条记忆` },
362
+ ],
363
+ },
364
+ execute: async (args) => {
365
+ if (!live.get().memoryMutate) {
366
+ return { written: 0, ids: [], skipped: [], notice: MUTATE_OFF_NOTICE };
367
+ }
368
+ const raw = Array.isArray(args.records) ? args.records : [];
369
+ if (raw.length === 0) {
370
+ return { written: 0, ids: [], skipped: [], notice: 'records 为空,未写入' };
371
+ }
372
+ if (raw.length > MAX_IMPORT) {
373
+ return {
374
+ written: 0,
375
+ ids: [],
376
+ skipped: [],
377
+ notice: `单次上限 ${MAX_IMPORT} 条(收到 ${raw.length} 条),请拆批导入`,
378
+ };
379
+ }
380
+ const scene = typeof args.scene === 'string' && args.scene.trim()
381
+ ? args.scene.trim().slice(0, 120)
382
+ : DEFAULT_IMPORT_SCENE;
383
+ const now = Date.now();
384
+ const seen = new Set();
385
+ const records = [];
386
+ const skipped = [];
387
+ raw.forEach((item, index) => {
388
+ const content = String(item?.content ?? '').trim();
389
+ if (!content) {
390
+ skipped.push({ index, reason: 'content 为空' });
391
+ return;
392
+ }
393
+ const key = content.replace(/\s+/g, ' ').toLowerCase();
394
+ if (seen.has(key)) {
395
+ skipped.push({ index, reason: '与本批前面的记录内容重复' });
396
+ return;
397
+ }
398
+ seen.add(key);
399
+ records.push(buildRecord(item, scene, now));
400
+ });
401
+ if (records.length > 0)
402
+ await stores.l1.appendNew(records);
403
+ logger.info(`[memory] 批量导入 ${records.length} 条(跳过 ${skipped.length} 条,场景 ${scene})`);
404
+ return { written: records.length, ids: records.map((r) => r.id), skipped };
405
+ },
406
+ }));
407
+ // memory_delete:显式"忘了 X"——按语义检索命中后删除(高权限门控)。
408
+ ctx.tools.register(defineTool({
409
+ name: 'memory_delete',
410
+ description: '删除与查询相关的记忆(L1)。仅当用户显式要求"忘记/删除某条记忆"时用;需高权限模式开启。按语义检索命中后删除(最多若干条),无法精确匹配时返回 zero。',
411
+ parameters: {
412
+ query: { type: 'string', required: true, description: '要删除的记忆描述(自然语言,匹配最贴近的现存记忆)' },
413
+ limit: { type: 'number', description: '最多删除条数(默认 3,上限 10)' },
414
+ },
415
+ output: {
416
+ schema: {
417
+ type: 'object',
418
+ properties: {
419
+ deleted: { type: 'number' },
420
+ ids: { type: 'array', items: { type: 'string' } },
421
+ notice: { type: 'string' },
422
+ },
423
+ additionalProperties: false,
424
+ },
425
+ render: (_args, value) => [
426
+ { type: 'text', text: value.notice ?? `已删除 ${value.deleted ?? 0} 条记忆` },
427
+ ],
428
+ },
429
+ execute: async (args, exec) => {
430
+ if (!live.get().memoryMutate)
431
+ return { deleted: 0, ids: [], notice: MUTATE_OFF_NOTICE };
432
+ const query = String(args.query ?? '').trim();
433
+ if (!query)
434
+ return { deleted: 0, ids: [], notice: 'query 为空,未删除' };
435
+ const family = familyOfCaller(exec.agent?.id);
436
+ const limit = Math.min(Math.max(args.limit ?? 3, 1), 10);
437
+ const hits = await stores.l1.search(query, limit, { family: family && family !== null ? family : undefined });
438
+ const ids = hits.map((h) => h.id);
439
+ if (ids.length === 0)
440
+ return { deleted: 0, ids: [], notice: '未找到匹配的记忆,未删除' };
441
+ await stores.l1.deleteBatch(ids);
442
+ logger.info(`[memory] 高权限删除记忆 ${ids.length} 条(${ids.join(',')})`);
443
+ return { deleted: ids.length, ids };
444
+ },
445
+ }));
446
+ // ── 图谱工具(读;受与 memory_search 同款的档位/注入拒读门 + 族过滤) ──
447
+ const GRAPH_OFF_NOTICE = '图谱功能未启用:部署配置 graph.enabled 未开启,当前没有可用的知识图谱。';
448
+ // memory_search_graph: 图谱节点检索(紧凑节点卡)
449
+ ctx.tools.register(defineTool({
450
+ name: 'memory_search_graph',
451
+ description: '搜索知识图谱(实体节点:人物/项目/组织/工具/地点)。返回实体的当前状态摘要与匹配说明,适用于查"某人/某项目现在什么状态"这类问题;需要完整属性与关系时再用 memory_expand_graph_node 展开。',
452
+ parameters: {
453
+ query: { type: 'string', required: true, description: '搜索查询文本(实体名、别名、标签或状态关键词)' },
454
+ limit: { type: 'number', description: '最大返回条数(默认 8,上限 20)' },
455
+ },
456
+ output: {
457
+ schema: {
458
+ type: 'object',
459
+ properties: {
460
+ items: {
461
+ type: 'array',
462
+ items: {
463
+ type: 'object',
464
+ properties: {
465
+ id: { type: 'string' },
466
+ name: { type: 'string' },
467
+ type: { type: 'string' },
468
+ status: { type: 'string' },
469
+ current_state: { type: 'string' },
470
+ score: { type: 'number' },
471
+ match_reason: { type: 'string' },
472
+ },
473
+ additionalProperties: false,
474
+ },
475
+ },
476
+ notice: { type: 'string' },
477
+ },
478
+ additionalProperties: false,
479
+ },
480
+ render: (_args, value) => [{ type: 'text', text: value.notice ?? renderGraphCards(value.items ?? []) }],
481
+ },
482
+ execute: async (args, exec) => {
483
+ const family = familyOfCaller(exec.agent?.id);
484
+ if (family === null)
485
+ return { items: [], notice: blockNoticeOf(exec.agent?.id) };
486
+ const graph = stores.graph;
487
+ if (!graph)
488
+ return { items: [], notice: GRAPH_OFF_NOTICE };
489
+ const query = String(args.query ?? '').trim();
490
+ if (!query)
491
+ return { items: [], notice: 'query 为空' };
492
+ const limit = Math.min(Math.max(args.limit ?? 8, 1), 20);
493
+ // 族过滤:纯档会话只见本族衍生节点(auto/fail-open 不过滤)
494
+ const hits = graph.searchNodes(query, limit, family ? [family] : undefined);
495
+ return {
496
+ items: hits.map((h) => ({
497
+ id: h.node.id,
498
+ name: h.node.name,
499
+ type: h.node.type,
500
+ status: h.node.status,
501
+ current_state: h.node.currentState,
502
+ score: Math.round(h.score * 100) / 100,
503
+ match_reason: h.matchReason,
504
+ })),
505
+ };
506
+ },
507
+ }));
508
+ // memory_expand_graph_node: 展开单个节点(facts 全量含历史 + 关系边)
509
+ ctx.tools.register(defineTool({
510
+ name: 'memory_expand_graph_node',
511
+ description: '展开一个图谱节点的完整详情:全部属性(facts,含已被更新的历史值与生效区间)、关联关系边与来源记忆 id。先用 memory_search_graph 拿到节点 id。',
512
+ parameters: {
513
+ id: { type: 'string', required: true, description: '节点 id(memory_search_graph 返回的 id)' },
514
+ },
515
+ output: {
516
+ schema: {
517
+ type: 'object',
518
+ properties: {
519
+ node: { type: 'string', description: '节点详情文本(不存在为空串)' },
520
+ notice: { type: 'string' },
521
+ },
522
+ additionalProperties: false,
523
+ },
524
+ render: (_args, value) => [{ type: 'text', text: value.notice ?? (value.node || '(节点不存在)') }],
525
+ },
526
+ execute: async (args, exec) => {
527
+ const family = familyOfCaller(exec.agent?.id);
528
+ if (family === null)
529
+ return { notice: blockNoticeOf(exec.agent?.id) };
530
+ const graph = stores.graph;
531
+ if (!graph)
532
+ return { notice: GRAPH_OFF_NOTICE };
533
+ const id = String(args.id ?? '').trim();
534
+ if (!id || id.length > 200)
535
+ return { notice: 'id 缺失或过长' };
536
+ const node = graph.getNode(id);
537
+ // 悬挂 id 与跨族节点一律"不解析":纯档会话探测不到他族节点的存在
538
+ if (!node || (family && !node.families.includes(family)))
539
+ return { notice: '(节点不存在)' };
540
+ const lines = [
541
+ `[${node.name}](类型 ${node.type},${GRAPH_STATUS_LABELS[node.status]})`,
542
+ ...(node.aliases.length > 0 ? [`别名: ${node.aliases.join('、')}`] : []),
543
+ ...(node.tags?.length ? [`标签: ${node.tags.join('、')}`] : []),
544
+ ...(node.currentState ? [`当前状态:\n${node.currentState}`] : []),
545
+ '',
546
+ '属性(含历史):',
547
+ ];
548
+ for (const f of node.facts) {
549
+ const value = Array.isArray(f.value) ? f.value.join('、') : f.value;
550
+ const span = [f.validFrom ? `自 ${f.validFrom}` : '', f.validTo ? `至 ${f.validTo}` : ''].filter(Boolean).join(' ');
551
+ lines.push(`- ${f.key}: ${value}(${GRAPH_STATUS_LABELS[f.status]}${span ? `,${span}` : ''})`);
552
+ }
553
+ const edges = graph.edgesOf(id);
554
+ if (edges.length > 0) {
555
+ lines.push('', '关系:');
556
+ for (const e of edges) {
557
+ const other = e.fromNodeId === id ? e.toNodeId : e.fromNodeId;
558
+ const arrow = e.fromNodeId === id ? '→' : '←';
559
+ lines.push(`- ${arrow} ${other}(${e.relation},${GRAPH_STATUS_LABELS[e.status]})`);
560
+ }
561
+ }
562
+ lines.push('', `来源记忆: ${node.sourceRecordIds.join('、') || '(无)'}`);
563
+ return { node: lines.join('\n') };
564
+ },
565
+ }));
566
+ // ── 反刍工具(按需触发 L1→L2→L3 消化,受蒸馏开关门控) ──
567
+ const RUMINATE_OFF_NOTICE = '反刍功能未开放:请在记忆库面板开启「蒸馏」开关后使用。';
568
+ const RUMINATE_UNAVAIL_NOTICE = '反刍未初始化:存储处于降级态,无法反刍。';
569
+ const RUMINATE_RUNNING_NOTICE = '反刍已在进行中,请稍后再试。';
570
+ ctx.tools.register(defineTool({
571
+ name: 'memory_ruminate',
572
+ description: '触发记忆反刍:把未蒸馏的对话缓冲冲刷出来,跑一轮 L1 抽取 → L2 场景整合 → L3 画像更新。与重建不同,不清库不改 L0,仅消化攒而未蒸馏的切片;无缓冲时做轻量 L2/L3 刷新。',
573
+ parameters: {},
574
+ output: {
575
+ schema: {
576
+ type: 'object',
577
+ properties: {
578
+ running: { type: 'boolean' },
579
+ phase: { type: 'string' },
580
+ done: { type: 'number' },
581
+ total: { type: 'number' },
582
+ recordsBuilt: { type: 'number' },
583
+ startedAt: { type: 'string' },
584
+ notice: { type: 'string' },
585
+ },
586
+ additionalProperties: false,
587
+ },
588
+ render: (_args, value) => [
589
+ { type: 'text', text: value.notice ?? renderRuminateStatus(value) },
590
+ ],
591
+ },
592
+ execute: async () => {
593
+ if (!ruminate)
594
+ return { notice: RUMINATE_UNAVAIL_NOTICE, running: false, phase: 'idle', done: 0, total: 0, recordsBuilt: 0, startedAt: undefined };
595
+ const s = live.get();
596
+ if (!s.enabled || !s.distill)
597
+ return { notice: RUMINATE_OFF_NOTICE, running: false, phase: 'idle', done: 0, total: 0, recordsBuilt: 0, startedAt: undefined };
598
+ try {
599
+ const result = await ruminate.start();
600
+ return {
601
+ running: result.running,
602
+ phase: result.phase,
603
+ done: result.done,
604
+ total: result.total,
605
+ recordsBuilt: result.recordsBuilt,
606
+ startedAt: result.startedAt ? new Date(result.startedAt).toISOString() : undefined,
607
+ };
608
+ }
609
+ catch (err) {
610
+ const msg = err instanceof Error ? err.message : String(err);
611
+ if (msg.includes('已在进行中'))
612
+ return { notice: RUMINATE_RUNNING_NOTICE, running: true, phase: err.phase ?? 'distilling', done: 0, total: 0, recordsBuilt: 0, startedAt: undefined };
613
+ return { notice: `反刍启动失败: ${msg}`, running: false, phase: 'failed', done: 0, total: 0, recordsBuilt: 0, startedAt: undefined };
614
+ }
615
+ },
616
+ }));
617
+ // ── 取消反刍(按需取消正在进行的反刍) ──
618
+ ctx.tools.register(defineTool({
619
+ name: 'memory_ruminate_cancel',
620
+ description: '取消正在进行的记忆反刍。已蒸馏部分保留,pending 切片中未被消费的部分维持原状。',
621
+ parameters: {},
622
+ output: {
623
+ schema: {
624
+ type: 'object',
625
+ properties: {
626
+ notice: { type: 'string' },
627
+ phase: { type: 'string' },
628
+ running: { type: 'boolean' },
629
+ },
630
+ additionalProperties: false,
631
+ },
632
+ render: (_args, value) => [{ type: 'text', text: value.notice ?? `反刍已取消(当前阶段:${value.phase})` }],
633
+ },
634
+ execute: async () => {
635
+ if (!ruminate)
636
+ return { notice: RUMINATE_UNAVAIL_NOTICE, phase: 'idle', running: false };
637
+ try {
638
+ const result = ruminate.requestCancel();
639
+ return {
640
+ notice: result.phase === 'cancelled' ? '反刍已取消' : '取消请求已发送',
641
+ phase: result.phase,
642
+ running: result.running,
643
+ };
644
+ }
645
+ catch (err) {
646
+ const msg = err instanceof Error ? err.message : String(err);
647
+ return { notice: msg, phase: 'idle', running: false };
648
+ }
649
+ },
650
+ }));
651
+ // ── 反刍状态查询(查看当前反刍进度) ──
652
+ ctx.tools.register(defineTool({
653
+ name: 'memory_ruminate_status',
654
+ description: '查询当前记忆反刍的状态与进度(是否运行中、当前阶段、已完成/总会话数等)。\n\n注意:在 DSH Web GUI 的对话中无法直接调用此工具,因为模型可调用工具列表不包含 ruminate_status。如需查询进度,请在记忆库面板的蒸馏面板查看。',
655
+ parameters: {},
656
+ output: {
657
+ schema: {
658
+ type: 'object',
659
+ properties: {
660
+ running: { type: 'boolean' },
661
+ phase: { type: 'string' },
662
+ done: { type: 'number' },
663
+ total: { type: 'number' },
664
+ recordsBuilt: { type: 'number' },
665
+ cancelRequested: { type: 'boolean' },
666
+ startedAt: { type: 'string' },
667
+ finishedAt: { type: 'string' },
668
+ error: { type: 'string' },
669
+ notice: { type: 'string' },
670
+ },
671
+ additionalProperties: false,
672
+ },
673
+ render: (_args, value) => [
674
+ { type: 'text', text: value.notice ?? renderRuminateStatus(value) },
675
+ ],
676
+ },
677
+ execute: async () => {
678
+ if (!ruminate)
679
+ return { notice: RUMINATE_UNAVAIL_NOTICE, running: false, phase: 'idle', done: 0, total: 0, recordsBuilt: 0, cancelRequested: false, startedAt: undefined, finishedAt: undefined, error: undefined };
680
+ const result = ruminate.getStatus();
681
+ return {
682
+ running: result.running,
683
+ phase: result.phase,
684
+ done: result.done,
685
+ total: result.total,
686
+ recordsBuilt: result.recordsBuilt,
687
+ cancelRequested: result.cancelRequested,
688
+ startedAt: result.startedAt ? new Date(result.startedAt).toISOString() : undefined,
689
+ finishedAt: result.finishedAt ? new Date(result.finishedAt).toISOString() : undefined,
690
+ error: result.error ?? undefined,
691
+ };
692
+ },
693
+ }));
694
+ logger.info('[memory] 工具已注册: memory_search / conversation_search / memory_read_scene / memory_search_graph / memory_expand_graph_node,及高权限 memory_add/memory_import/memory_delete / memory_ruminate / memory_ruminate_cancel / memory_ruminate_status');
695
+ }
696
+ function renderGraphCards(items) {
697
+ if (!items || items.length === 0)
698
+ return '(图谱中没有找到相关实体)';
699
+ return items
700
+ .map((it, i) => {
701
+ const state = it.current_state ? `\n 状态: ${it.current_state.replaceAll('\n', ' / ')}` : '';
702
+ return `${i + 1}. ${it.name ?? ''}(类型 ${it.type ?? ''},id=${it.id ?? ''})${state}\n 匹配: ${it.match_reason ?? ''}`;
703
+ })
704
+ .join('\n');
705
+ }
706
+ function renderMemoryItems(items) {
707
+ if (!items || items.length === 0)
708
+ return '(没有找到相关记忆)';
709
+ return items
710
+ .map((it, i) => `${i + 1}. [${it.type ?? ''}]${it.scene_name ? ` (${it.scene_name})` : ''} ${it.content ?? ''}`)
711
+ .join('\n');
712
+ }
713
+ function renderConversationItems(items) {
714
+ if (!items || items.length === 0)
715
+ return '(没有找到相关对话)';
716
+ return items
717
+ .map((it, i) => {
718
+ const time = it.timestamp ? new Date(it.timestamp).toISOString() : '';
719
+ return `${i + 1}. [${it.role ?? ''}]${time ? ` ${time}` : ''} (session=${it.session_id ?? ''})\n${it.content ?? ''}`;
720
+ })
721
+ .join('\n\n');
722
+ }
723
+ function renderRuminateStatus(v) {
724
+ const running = v.running ?? false;
725
+ const status = running ? `🔄 反刍运行中` : '✅ 反刍已完成';
726
+ const phaseMap = {
727
+ idle: '空闲',
728
+ distilling: 'L1 蒸馏中',
729
+ consolidating: 'L2 场景整合中',
730
+ updating: 'L3 画像更新中',
731
+ done: '完成',
732
+ cancelled: '已取消',
733
+ failed: '失败',
734
+ };
735
+ const phase = phaseMap[v.phase ?? 'idle'] ?? v.phase ?? 'idle';
736
+ const total = v.total ?? 0;
737
+ const done = v.done ?? 0;
738
+ const parts = [`${status} [${phase}]`];
739
+ if (running || total > 0) {
740
+ parts.push(`进度: ${done}/${total} 会话`);
741
+ }
742
+ if ((v.recordsBuilt ?? 0) > 0) {
743
+ parts.push(`产出: ${v.recordsBuilt} 条记忆`);
744
+ }
745
+ if (v.startedAt) {
746
+ parts.push(`开始: ${v.startedAt}`);
747
+ }
748
+ if (v.finishedAt) {
749
+ parts.push(`结束: ${v.finishedAt}`);
750
+ }
751
+ if (v.error) {
752
+ parts.push(`错误: ${v.error}`);
753
+ }
754
+ if (v.cancelRequested) {
755
+ parts.push('⚠️ 已请求取消');
756
+ }
757
+ return parts.join(' | ');
758
+ }