mocode-ai 1.2.4 → 1.2.5
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/dist/config/index.js +3 -2
- package/dist/context/classifier.js +1 -0
- package/dist/memory/graph.js +409 -0
- package/dist/memory/index.js +2 -1
- package/dist/memory/reflect.js +25 -6
- package/dist/repl/index.js +2 -2
- package/dist/sandbox/policy.js +1 -1
- package/dist/tools/builtins/index.js +5 -1
- package/dist/tools/builtins/memory-graph.js +118 -0
- package/dist/tools/builtins/memory-save.js +42 -5
- package/dist/tools/builtins/memory-search.js +26 -5
- package/dist/tools/constants.js +6 -0
- package/dist/ui/layout.js +2 -2
- package/dist/ui/theme.js +11 -11
- package/package.json +1 -1
package/dist/config/index.js
CHANGED
|
@@ -216,8 +216,9 @@ export function reinjectActivePlanIntoSystem(history) {
|
|
|
216
216
|
}
|
|
217
217
|
const SYSTEM_PROMPT_MEMORY_SECTION = `
|
|
218
218
|
## Memory (cross-session facts)
|
|
219
|
-
- The prompt may contain a title/summary index; retrieve details with memory_search or inspect all with memory_list.
|
|
220
|
-
- Save only stable, non-obvious cross-session facts. Search before saving; update an existing entry instead of duplicating it, and archive stale entries
|
|
219
|
+
- The prompt may contain a title/summary index; retrieve details with memory_search or inspect all with memory_list. memory_search also surfaces knowledge-graph facts (relations between entities) alongside entry bodies.
|
|
220
|
+
- Save only stable, non-obvious cross-session facts. Search before saving; update an existing entry instead of duplicating it, and archive stale entries.
|
|
221
|
+
- A knowledge-graph layer links entities across memories: explore relations/neighbors with memory_graph (neighbors/add/stats), and attach meaningful links via the links parameter of memory_save when saving.`;
|
|
221
222
|
/** Inject only retrieval guidance; MOCODE.md contents stay outside the prompt until read on demand. */
|
|
222
223
|
function buildMemoryPromptSection() {
|
|
223
224
|
if (!isMemoryEnabled())
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// memory 知识图谱层(Tier-2):Graphiti 式时序三元组,纯 JSON 文件存储。
|
|
2
|
+
// 叶子模块:仅依赖 node 标准库 + tools/constants(常量叶子)+ store.ts 的类型,
|
|
3
|
+
// 与 store.ts 同风格:同步读写、整文件 tmp+rename 原子落盘、静默容错。
|
|
4
|
+
//
|
|
5
|
+
// 两文件(镜像 store.ts 的双 scope):
|
|
6
|
+
// 全局 ~/.mocode/memory-graph.json
|
|
7
|
+
// 项目 <cwd>/.mocode/memory-graph.json
|
|
8
|
+
// 文件形如 {"entities":[...],"edges":[...]}。scope 以所在文件为准(loadAllGraph 归一化)。
|
|
9
|
+
//
|
|
10
|
+
// 时序语义(抄 Graphiti/Zep 的核心思想,文件实现):边带 validAt/invalidAt。
|
|
11
|
+
// 新三元组与既有 active 边同(src,dst,relation)且 fact 不同 → 旧边 invalidAt=now(不删,可追溯);
|
|
12
|
+
// fact 相同 → 幂等跳过。查询默认只看 active(invalidAt==null)。
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { MAX_GRAPH_EDGES, MAX_GRAPH_ENTITIES } from '../tools/constants.js';
|
|
17
|
+
// ── 路径 / 原子写(同 store.ts 风格)──────────────────────────────────────
|
|
18
|
+
function globalGraphPath() {
|
|
19
|
+
return path.join(os.homedir(), '.mocode', 'memory-graph.json');
|
|
20
|
+
}
|
|
21
|
+
function projectGraphPath() {
|
|
22
|
+
return path.join(process.cwd(), '.mocode', 'memory-graph.json');
|
|
23
|
+
}
|
|
24
|
+
function graphPathForScope(scope) {
|
|
25
|
+
return scope === 'global' ? globalGraphPath() : projectGraphPath();
|
|
26
|
+
}
|
|
27
|
+
function ensureDir(p) {
|
|
28
|
+
const dir = path.dirname(p);
|
|
29
|
+
if (!existsSync(dir))
|
|
30
|
+
mkdirSync(dir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
function writeGraphAtomic(p, g) {
|
|
33
|
+
const tmp = p + '.tmp';
|
|
34
|
+
writeFileSync(tmp, JSON.stringify(g, null, 2), 'utf8');
|
|
35
|
+
renameSync(tmp, p);
|
|
36
|
+
}
|
|
37
|
+
function readGraphFile(p) {
|
|
38
|
+
if (!existsSync(p))
|
|
39
|
+
return { entities: [], edges: [] };
|
|
40
|
+
try {
|
|
41
|
+
const raw = readFileSync(p, 'utf8');
|
|
42
|
+
if (!raw.trim())
|
|
43
|
+
return { entities: [], edges: [] };
|
|
44
|
+
const obj = JSON.parse(raw);
|
|
45
|
+
return {
|
|
46
|
+
entities: Array.isArray(obj.entities) ? obj.entities : [],
|
|
47
|
+
edges: Array.isArray(obj.edges) ? obj.edges : [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { entities: [], edges: [] }; // 损坏文件不连累全局
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function nowIso() {
|
|
55
|
+
return new Date().toISOString();
|
|
56
|
+
}
|
|
57
|
+
let entCounter = 0;
|
|
58
|
+
/** 实体名 → ASCII slug;纯 CJK 等空结果用 ent 前缀兜底(同 store.ts slugify 思路)。 */
|
|
59
|
+
function entitySlug(name) {
|
|
60
|
+
const s = name
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
63
|
+
.replace(/^-+|-+$/g, '');
|
|
64
|
+
if (s)
|
|
65
|
+
return s;
|
|
66
|
+
entCounter++;
|
|
67
|
+
return 'ent-' + Date.now().toString(36) + entCounter.toString(36);
|
|
68
|
+
}
|
|
69
|
+
let edgeCounter = 0;
|
|
70
|
+
function nextEdgeId() {
|
|
71
|
+
edgeCounter++;
|
|
72
|
+
return 'edge-' + Date.now().toString(36) + edgeCounter.toString(36);
|
|
73
|
+
}
|
|
74
|
+
/** 读两文件并按文件归一化 scope。 */
|
|
75
|
+
export function loadAllGraph() {
|
|
76
|
+
const out = { entities: [], edges: [] };
|
|
77
|
+
for (const scope of ['global', 'project']) {
|
|
78
|
+
const g = readGraphFile(graphPathForScope(scope));
|
|
79
|
+
for (const e of g.entities) {
|
|
80
|
+
e.scope = scope;
|
|
81
|
+
out.entities.push(e);
|
|
82
|
+
}
|
|
83
|
+
for (const e of g.edges) {
|
|
84
|
+
e.scope = scope;
|
|
85
|
+
out.edges.push(e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function writeGraphForScope(scope, g) {
|
|
91
|
+
const p = graphPathForScope(scope);
|
|
92
|
+
ensureDir(p);
|
|
93
|
+
writeGraphAtomic(p, { entities: g.entities, edges: g.edges });
|
|
94
|
+
}
|
|
95
|
+
/** 单 scope 的完整图(读文件,scope 归一化)。 */
|
|
96
|
+
function loadScopeGraph(scope) {
|
|
97
|
+
const g = readGraphFile(graphPathForScope(scope));
|
|
98
|
+
for (const e of g.entities)
|
|
99
|
+
e.scope = scope;
|
|
100
|
+
for (const e of g.edges)
|
|
101
|
+
e.scope = scope;
|
|
102
|
+
return g;
|
|
103
|
+
}
|
|
104
|
+
// ── 实体 ─────────────────────────────────────────────────────────────────
|
|
105
|
+
const norm = (s) => s.trim().toLowerCase();
|
|
106
|
+
function findEntityIn(g, name) {
|
|
107
|
+
const q = norm(name);
|
|
108
|
+
if (!q)
|
|
109
|
+
return undefined;
|
|
110
|
+
return g.entities.find((e) => e.id === q || norm(e.name) === q || e.aliases.some((a) => norm(a) === q));
|
|
111
|
+
}
|
|
112
|
+
/** 全局找实体(两 scope,project 优先——项目事实比全局更具体)。 */
|
|
113
|
+
export function findEntity(name) {
|
|
114
|
+
const proj = findEntityIn(loadScopeGraph('project'), name);
|
|
115
|
+
if (proj)
|
|
116
|
+
return proj;
|
|
117
|
+
return findEntityIn(loadScopeGraph('global'), name);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* upsert 实体:按 name/alias/id 匹配,命中则合并 alias/summary;未命中建新。
|
|
121
|
+
* scope 容量保护:超限先清孤儿实体(无任何 active 边相连);仍超 → 拒绝新实体。
|
|
122
|
+
*/
|
|
123
|
+
export function upsertEntity(name, opts = {}) {
|
|
124
|
+
const scope = opts.scope === 'global' ? 'global' : 'project';
|
|
125
|
+
const trimmed = name.trim();
|
|
126
|
+
if (!trimmed)
|
|
127
|
+
return { id: '', created: false, rejected: 'empty-name' };
|
|
128
|
+
const g = loadScopeGraph(scope);
|
|
129
|
+
const hit = findEntityIn(g, trimmed);
|
|
130
|
+
if (hit) {
|
|
131
|
+
let dirty = false;
|
|
132
|
+
if (opts.alias && !hit.aliases.some((a) => norm(a) === norm(opts.alias))) {
|
|
133
|
+
hit.aliases.push(opts.alias.trim());
|
|
134
|
+
dirty = true;
|
|
135
|
+
}
|
|
136
|
+
if (opts.summary && opts.summary.trim() && opts.summary.trim() !== hit.summary) {
|
|
137
|
+
hit.summary = opts.summary.trim();
|
|
138
|
+
dirty = true;
|
|
139
|
+
}
|
|
140
|
+
if (dirty) {
|
|
141
|
+
hit.updatedAt = nowIso();
|
|
142
|
+
writeGraphForScope(scope, g);
|
|
143
|
+
}
|
|
144
|
+
return { id: hit.id, created: false };
|
|
145
|
+
}
|
|
146
|
+
// 新建:容量保护
|
|
147
|
+
if (g.entities.length >= MAX_GRAPH_ENTITIES) {
|
|
148
|
+
const linked = new Set();
|
|
149
|
+
for (const e of g.edges) {
|
|
150
|
+
if (!e.invalidAt) {
|
|
151
|
+
linked.add(e.src);
|
|
152
|
+
linked.add(e.dst);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
g.entities = g.entities.filter((e) => linked.has(e.id));
|
|
156
|
+
if (g.entities.length >= MAX_GRAPH_ENTITIES) {
|
|
157
|
+
return { id: '', created: false, rejected: 'entity-cap' };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const now = nowIso();
|
|
161
|
+
const ent = {
|
|
162
|
+
id: entitySlug(trimmed),
|
|
163
|
+
name: trimmed,
|
|
164
|
+
aliases: opts.alias && norm(opts.alias) !== norm(trimmed) ? [opts.alias.trim()] : [],
|
|
165
|
+
summary: opts.summary?.trim() ?? '',
|
|
166
|
+
createdAt: now,
|
|
167
|
+
updatedAt: now,
|
|
168
|
+
scope,
|
|
169
|
+
};
|
|
170
|
+
// slug 碰撞兜底(两个不同名字 slug 相同)
|
|
171
|
+
while (g.entities.some((e) => e.id === ent.id))
|
|
172
|
+
ent.id += 'x';
|
|
173
|
+
g.entities.push(ent);
|
|
174
|
+
writeGraphForScope(scope, g);
|
|
175
|
+
return { id: ent.id, created: true };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 写一条三元组。src/dst 自动 upsert 为实体。
|
|
179
|
+
* 冲突策略:同 scope 内已有 active 边同(src,dst,relation):
|
|
180
|
+
* - fact 相同 → 幂等跳过(duplicate);
|
|
181
|
+
* - fact 不同 → 旧边 invalidAt=now(时序失效),新边入库。
|
|
182
|
+
* 容量保护:边超限先清已失效边;仍超 → 拒绝。
|
|
183
|
+
*/
|
|
184
|
+
export function addTriple(input) {
|
|
185
|
+
const src = input.src?.trim();
|
|
186
|
+
const dst = input.dst?.trim();
|
|
187
|
+
const relation = input.relation?.trim().toLowerCase().replace(/\s+/g, '_');
|
|
188
|
+
if (!src || !dst || !relation)
|
|
189
|
+
return { ok: false, reason: 'missing src/relation/dst' };
|
|
190
|
+
if (src.length > 80 || dst.length > 80 || relation.length > 60) {
|
|
191
|
+
return { ok: false, reason: 'src/dst/relation too long' };
|
|
192
|
+
}
|
|
193
|
+
const scope = input.scope === 'global' ? 'global' : 'project';
|
|
194
|
+
const s = upsertEntity(src, { scope });
|
|
195
|
+
if (s.rejected)
|
|
196
|
+
return { ok: false, reason: `src entity: ${s.rejected}` };
|
|
197
|
+
const d = upsertEntity(dst, { scope });
|
|
198
|
+
if (d.rejected)
|
|
199
|
+
return { ok: false, reason: `dst entity: ${d.rejected}` };
|
|
200
|
+
const g = loadScopeGraph(scope);
|
|
201
|
+
const fact = (input.fact ?? '').trim();
|
|
202
|
+
const clash = g.edges.find((e) => !e.invalidAt && e.src === s.id && e.dst === d.id && e.relation === relation);
|
|
203
|
+
if (clash) {
|
|
204
|
+
if (!fact || clash.fact === fact) {
|
|
205
|
+
return { ok: true, edgeId: clash.id, superseded: 0, duplicate: true };
|
|
206
|
+
}
|
|
207
|
+
clash.invalidAt = nowIso(); // 时序失效,保留可追溯
|
|
208
|
+
}
|
|
209
|
+
// 容量保护:先清失效边
|
|
210
|
+
if (g.edges.length >= MAX_GRAPH_EDGES) {
|
|
211
|
+
g.edges = g.edges.filter((e) => !e.invalidAt);
|
|
212
|
+
if (g.edges.length >= MAX_GRAPH_EDGES) {
|
|
213
|
+
return { ok: false, reason: 'edge-cap' };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const edge = {
|
|
217
|
+
id: nextEdgeId(),
|
|
218
|
+
src: s.id,
|
|
219
|
+
dst: d.id,
|
|
220
|
+
relation,
|
|
221
|
+
fact,
|
|
222
|
+
validAt: nowIso(),
|
|
223
|
+
invalidAt: null,
|
|
224
|
+
sourceEntry: input.sourceEntry ?? null,
|
|
225
|
+
scope,
|
|
226
|
+
};
|
|
227
|
+
g.edges.push(edge);
|
|
228
|
+
writeGraphForScope(scope, g);
|
|
229
|
+
return { ok: true, edgeId: edge.id, superseded: clash ? 1 : 0 };
|
|
230
|
+
}
|
|
231
|
+
/** 实体关键词搜索(多词子串,name/alias/id 加权),返回命中实体 + 相连 active 边。 */
|
|
232
|
+
export function searchGraph(query, limit = 8) {
|
|
233
|
+
const g = loadAllGraph();
|
|
234
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
235
|
+
const scored = g.entities
|
|
236
|
+
.map((e) => {
|
|
237
|
+
let sc = 0;
|
|
238
|
+
const name = e.name.toLowerCase();
|
|
239
|
+
const id = e.id.toLowerCase();
|
|
240
|
+
const aliases = e.aliases.map((a) => a.toLowerCase());
|
|
241
|
+
for (const t of terms) {
|
|
242
|
+
if (id.includes(t))
|
|
243
|
+
sc += 8;
|
|
244
|
+
if (name.includes(t))
|
|
245
|
+
sc += 10;
|
|
246
|
+
if (aliases.some((a) => a.includes(t)))
|
|
247
|
+
sc += 6;
|
|
248
|
+
if (e.summary.toLowerCase().includes(t))
|
|
249
|
+
sc += 2;
|
|
250
|
+
}
|
|
251
|
+
return { e, sc };
|
|
252
|
+
})
|
|
253
|
+
.filter((x) => (terms.length === 0 ? true : x.sc > 0))
|
|
254
|
+
.sort((a, b) => b.sc - a.sc)
|
|
255
|
+
.slice(0, Math.max(1, Math.min(limit, 20)));
|
|
256
|
+
const hitIds = new Set(scored.map((x) => x.e.id));
|
|
257
|
+
const edges = g.edges.filter((e) => !e.invalidAt && (hitIds.has(e.src) || hitIds.has(e.dst)));
|
|
258
|
+
return { entities: scored.map((x) => x.e), edges };
|
|
259
|
+
}
|
|
260
|
+
function buildAdjacency(g, relation) {
|
|
261
|
+
const adj = new Map();
|
|
262
|
+
for (const e of g.edges) {
|
|
263
|
+
if (e.invalidAt)
|
|
264
|
+
continue;
|
|
265
|
+
if (relation && e.relation !== relation)
|
|
266
|
+
continue;
|
|
267
|
+
const a = adj.get(e.src);
|
|
268
|
+
if (a)
|
|
269
|
+
a.push(e);
|
|
270
|
+
else
|
|
271
|
+
adj.set(e.src, [e]);
|
|
272
|
+
const b = adj.get(e.dst);
|
|
273
|
+
if (b)
|
|
274
|
+
b.push(e);
|
|
275
|
+
else
|
|
276
|
+
adj.set(e.dst, [e]);
|
|
277
|
+
}
|
|
278
|
+
return adj;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* 邻居遍历:depth 1-3(BFS + 邻接表 O(V+E)),只看 active 边,边数封顶 40 防图爆炸。
|
|
282
|
+
* relation 可选:只沿该类型的边走(深跳聚焦用,如沿 depends_on 链追踪)。
|
|
283
|
+
*/
|
|
284
|
+
export function neighborsOf(nameOrId, depth = 1, relation) {
|
|
285
|
+
const g = loadAllGraph();
|
|
286
|
+
const center = findEntityIn(g, nameOrId) ?? g.entities.find((e) => e.id === nameOrId);
|
|
287
|
+
if (!center)
|
|
288
|
+
return { center: null, entities: [], edges: [], truncated: false };
|
|
289
|
+
const rel = relation?.trim().toLowerCase().replace(/\s+/g, '_') || undefined;
|
|
290
|
+
const maxDepth = Math.max(1, Math.min(Math.floor(depth) || 1, 3));
|
|
291
|
+
const MAX_EDGES_OUT = 40;
|
|
292
|
+
const adj = buildAdjacency(g, rel);
|
|
293
|
+
const visited = new Set([center.id]);
|
|
294
|
+
const seenEdges = new Set();
|
|
295
|
+
const outEdges = [];
|
|
296
|
+
let frontier = [center.id];
|
|
297
|
+
let truncated = false;
|
|
298
|
+
outer: for (let d = 0; d < maxDepth; d++) {
|
|
299
|
+
const next = [];
|
|
300
|
+
for (const id of frontier) {
|
|
301
|
+
for (const e of adj.get(id) ?? []) {
|
|
302
|
+
if (seenEdges.has(e.id))
|
|
303
|
+
continue;
|
|
304
|
+
seenEdges.add(e.id);
|
|
305
|
+
if (outEdges.length >= MAX_EDGES_OUT) {
|
|
306
|
+
truncated = true;
|
|
307
|
+
break outer; // 截断立即退出,不再空扫剩余边
|
|
308
|
+
}
|
|
309
|
+
outEdges.push(e);
|
|
310
|
+
const other = e.src === id ? e.dst : e.src;
|
|
311
|
+
if (!visited.has(other)) {
|
|
312
|
+
visited.add(other);
|
|
313
|
+
next.push(other);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
frontier = next;
|
|
318
|
+
}
|
|
319
|
+
const entities = g.entities.filter((e) => visited.has(e.id) && e.id !== center.id);
|
|
320
|
+
return { center, entities, edges: outEdges, truncated };
|
|
321
|
+
}
|
|
322
|
+
const MAX_PATH_DEPTH = 6;
|
|
323
|
+
/**
|
|
324
|
+
* 两实体间最短路径:active 边视为无向,双向 BFS(每次展开较小前沿),
|
|
325
|
+
* 总跳数封顶 MAX_PATH_DEPTH 防图爆炸。不连通 / 未知端 → null。
|
|
326
|
+
*/
|
|
327
|
+
export function pathBetween(aNameOrId, bNameOrId) {
|
|
328
|
+
const g = loadAllGraph();
|
|
329
|
+
const from = findEntityIn(g, aNameOrId) ?? g.entities.find((e) => e.id === aNameOrId);
|
|
330
|
+
const to = findEntityIn(g, bNameOrId) ?? g.entities.find((e) => e.id === bNameOrId);
|
|
331
|
+
if (!from || !to)
|
|
332
|
+
return null;
|
|
333
|
+
if (from.id === to.id)
|
|
334
|
+
return { from, to, path: [from], edges: [] };
|
|
335
|
+
const fwd = new Map([[from.id, { prev: null, edge: null }]]);
|
|
336
|
+
const bwd = new Map([[to.id, { prev: null, edge: null }]]);
|
|
337
|
+
let fFront = [from.id];
|
|
338
|
+
let bFront = [to.id];
|
|
339
|
+
const adj = buildAdjacency(g);
|
|
340
|
+
let meet = null;
|
|
341
|
+
for (let d = 0; d < MAX_PATH_DEPTH && !meet; d++) {
|
|
342
|
+
const expandFwd = fFront.length <= bFront.length;
|
|
343
|
+
const mine = expandFwd ? fwd : bwd;
|
|
344
|
+
const theirs = expandFwd ? bwd : fwd;
|
|
345
|
+
const frontier = expandFwd ? fFront : bFront;
|
|
346
|
+
const next = [];
|
|
347
|
+
for (const id of frontier) {
|
|
348
|
+
for (const e of adj.get(id) ?? []) {
|
|
349
|
+
const other = e.src === id ? e.dst : e.src;
|
|
350
|
+
if (mine.has(other))
|
|
351
|
+
continue;
|
|
352
|
+
mine.set(other, { prev: id, edge: e });
|
|
353
|
+
next.push(other);
|
|
354
|
+
if (theirs.has(other)) {
|
|
355
|
+
meet = other;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (meet)
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
if (expandFwd)
|
|
363
|
+
fFront = next;
|
|
364
|
+
else
|
|
365
|
+
bFront = next;
|
|
366
|
+
}
|
|
367
|
+
if (!meet)
|
|
368
|
+
return null;
|
|
369
|
+
// 重组:fwd 链 from→…→meet,再接 bwd 链 meet→…→to
|
|
370
|
+
const rev = [];
|
|
371
|
+
for (let cur = meet; cur; cur = fwd.get(cur)?.prev ?? null)
|
|
372
|
+
rev.push(cur);
|
|
373
|
+
rev.reverse(); // [from, ..., meet]
|
|
374
|
+
const pathIds = [...rev];
|
|
375
|
+
const pathEdges = [];
|
|
376
|
+
for (const id of rev.slice(1)) {
|
|
377
|
+
const e = fwd.get(id)?.edge;
|
|
378
|
+
if (e)
|
|
379
|
+
pathEdges.push(e);
|
|
380
|
+
}
|
|
381
|
+
for (let cur = meet;;) {
|
|
382
|
+
const t = bwd.get(cur);
|
|
383
|
+
if (!t?.prev)
|
|
384
|
+
break;
|
|
385
|
+
if (t.edge)
|
|
386
|
+
pathEdges.push(t.edge);
|
|
387
|
+
pathIds.push(t.prev);
|
|
388
|
+
cur = t.prev;
|
|
389
|
+
}
|
|
390
|
+
const byId = new Map();
|
|
391
|
+
for (const e of g.entities)
|
|
392
|
+
if (!byId.has(e.id))
|
|
393
|
+
byId.set(e.id, e);
|
|
394
|
+
const path = pathIds.map((id) => byId.get(id)).filter((e) => !!e);
|
|
395
|
+
return { from, to, path, edges: pathEdges };
|
|
396
|
+
}
|
|
397
|
+
export function graphStats() {
|
|
398
|
+
const g = loadAllGraph();
|
|
399
|
+
const count = (scope) => ({
|
|
400
|
+
entities: g.entities.filter((e) => e.scope === scope).length,
|
|
401
|
+
edges: g.edges.filter((e) => e.scope === scope && !e.invalidAt).length,
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
entities: g.entities.length,
|
|
405
|
+
edgesActive: g.edges.filter((e) => !e.invalidAt).length,
|
|
406
|
+
edgesInvalid: g.edges.filter((e) => !!e.invalidAt).length,
|
|
407
|
+
byScope: { project: count('project'), global: count('global') },
|
|
408
|
+
};
|
|
409
|
+
}
|
package/dist/memory/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Memory barrel: Tier-2 JSONL store + background reflection.
|
|
1
|
+
// Memory barrel: Tier-2 JSONL store + knowledge-graph layer + background reflection.
|
|
2
2
|
// MOCODE.md is intentionally not loaded here: the system prompt only tells the agent
|
|
3
3
|
// to read the workspace file on demand, keeping its full body out of every request.
|
|
4
4
|
export { buildMemoryIndexSection, loadAll, gcMemories, } from './store.js';
|
|
5
|
+
export { addTriple, upsertEntity, findEntity, searchGraph, neighborsOf, pathBetween, graphStats, } from './graph.js';
|
|
5
6
|
export { kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, runReflection, } from './reflect.js';
|
package/dist/memory/reflect.js
CHANGED
|
@@ -13,6 +13,7 @@ import path from 'node:path';
|
|
|
13
13
|
import { chat } from '../llm/index.js';
|
|
14
14
|
import { config, isMemoryEnabled } from '../config/index.js';
|
|
15
15
|
import { saveEntry, updateEntry, forgetEntry, loadAll, gcMemories, } from './store.js';
|
|
16
|
+
import { addTriple } from './graph.js';
|
|
16
17
|
// ── 日志(静默容错,裁尾保最近)─────────────────────────────────────────────
|
|
17
18
|
function logPath() {
|
|
18
19
|
return path.join(process.cwd(), '.mocode', 'memory.log');
|
|
@@ -93,15 +94,16 @@ function buildMemorySample() {
|
|
|
93
94
|
}
|
|
94
95
|
const TYPES = 'decision | fact | pitfall | reference | feedback';
|
|
95
96
|
const REFLECT_SYS = `You are mocode's memory reflector. Review the recent session and existing memories, producing **only** updates worth remembering long-term.
|
|
96
|
-
Output strictly JSON (no markdown code blocks, no explanatory text): {"saves":[{"type":"...","name":"...","summary":"...","body":"..."}],"updates":[{"id":"...","reason":"...","summary":"...","body":"..."}],"forgets":[{"id":"...","reason":"..."}]}
|
|
97
|
-
Empty arrays are valid (if nothing is worth saving, all
|
|
97
|
+
Output strictly JSON (no markdown code blocks, no explanatory text): {"saves":[{"type":"...","name":"...","summary":"...","body":"..."}],"updates":[{"id":"...","reason":"...","summary":"...","body":"..."}],"forgets":[{"id":"...","reason":"..."}],"triples":[{"src":"...","relation":"...","dst":"...","fact":"..."}]}
|
|
98
|
+
Empty arrays are valid (if nothing is worth saving, all arrays are empty).
|
|
98
99
|
Rules:
|
|
99
100
|
① Only store non-obvious, cross-session-useful facts/decisions/pitfalls; do not store current bugs, temp files, undecided TODOs, or volatile items;
|
|
100
101
|
② Better to store less than to store trivially correct info (e.g. "keep it concise");
|
|
101
102
|
③ ids in updates/forgets must come from the "existing memories" list below; do not fabricate ids not listed there;
|
|
102
103
|
④ names in saves must be concise and not collide with existing ones; type ∈ {${TYPES}};
|
|
103
104
|
⑤ If an existing memory contradicts new facts or is outdated, update the old entry (modify summary/body) rather than creating a duplicate;
|
|
104
|
-
⑥ forgets are for memories clearly stale / superseded by a new entry (archive, not hard-delete)
|
|
105
|
+
⑥ forgets are for memories clearly stale / superseded by a new entry (archive, not hard-delete);
|
|
106
|
+
⑦ triples are knowledge-graph facts distilled from this session: concise entity names (lowercase snake_case or proper nouns), relation in snake_case (e.g. depends_on, decided_by, implemented_in, conflicts_with), plus a one-line fact. Only emit triples that are stable, non-obvious and cross-session-useful (2-6 at most); they may reference entities from saves/updates or existing memories.`;
|
|
105
107
|
const REFLECT_USER = (transcript, sample) => `## Recent session\n${transcript}\n\n## Existing memories\n${sample}\n\nProduce JSON:`;
|
|
106
108
|
function parsePlan(content) {
|
|
107
109
|
if (!content)
|
|
@@ -141,6 +143,7 @@ export async function runReflection(transcript, signal) {
|
|
|
141
143
|
saves: 0,
|
|
142
144
|
updates: 0,
|
|
143
145
|
forgets: 0,
|
|
146
|
+
triples: 0,
|
|
144
147
|
gcDecayed: 0,
|
|
145
148
|
gcCapped: 0,
|
|
146
149
|
gcGced: 0,
|
|
@@ -200,8 +203,24 @@ export async function runReflection(transcript, signal) {
|
|
|
200
203
|
forgets++;
|
|
201
204
|
}
|
|
202
205
|
}
|
|
206
|
+
// 知识图谱三元组:容错——单项失败跳过,不影响 saves/updates/forgets 已落地的结果。
|
|
207
|
+
let triples = 0;
|
|
208
|
+
if (Array.isArray(plan.triples)) {
|
|
209
|
+
for (const t of plan.triples.slice(0, 10)) {
|
|
210
|
+
if (!t?.src || !t?.relation || !t?.dst)
|
|
211
|
+
continue;
|
|
212
|
+
const r = addTriple({
|
|
213
|
+
src: String(t.src),
|
|
214
|
+
relation: String(t.relation),
|
|
215
|
+
dst: String(t.dst),
|
|
216
|
+
fact: t.fact ? String(t.fact) : undefined,
|
|
217
|
+
});
|
|
218
|
+
if (r.ok && !r.duplicate)
|
|
219
|
+
triples++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
203
222
|
const gc = gcMemories();
|
|
204
|
-
result = { ...result, saves, updates, forgets, gcDecayed: gc.decayed, gcCapped: gc.capped, gcGced: gc.gced };
|
|
223
|
+
result = { ...result, saves, updates, forgets, triples, gcDecayed: gc.decayed, gcCapped: gc.capped, gcGced: gc.gced };
|
|
205
224
|
return result;
|
|
206
225
|
}
|
|
207
226
|
function normalizeType(t) {
|
|
@@ -215,9 +234,9 @@ function normalizeType(t) {
|
|
|
215
234
|
// ── 后台编排:kickoff / drain / 缓存 ─────────────────────────────────────────
|
|
216
235
|
let inflight = null;
|
|
217
236
|
let lastReflectResult = null;
|
|
218
|
-
/** 摘要串(供 repl flush):存N 改N 忘N;有错误附上。 */
|
|
237
|
+
/** 摘要串(供 repl flush):存N 改N 忘N 图N;有错误附上。 */
|
|
219
238
|
export function formatReflectResult(r) {
|
|
220
|
-
const parts = [`存${r.saves}`, `改${r.updates}`, `忘${r.forgets}`];
|
|
239
|
+
const parts = [`存${r.saves}`, `改${r.updates}`, `忘${r.forgets}`, `图${r.triples}`];
|
|
221
240
|
if (r.gcDecayed || r.gcCapped || r.gcGced) {
|
|
222
241
|
parts.push(`遗忘(衰减${r.gcDecayed}/封顶${r.gcCapped}/清除${r.gcGced})`);
|
|
223
242
|
}
|
package/dist/repl/index.js
CHANGED
|
@@ -2186,9 +2186,9 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
|
|
|
2186
2186
|
// 写盘:mode 文件 values,/~/.mocode/config;writeConfigKeys 不会动其它键(主题 / 模型等)
|
|
2187
2187
|
updateConfigKey('MEMORY_ENABLED', nextEnabled ? 'true' : 'false');
|
|
2188
2188
|
const note = nextEnabled
|
|
2189
|
-
? `${ui.green}已开启记忆子系统${ui.reset} — memory_save/search/list/update/forget 进入工具表;` +
|
|
2189
|
+
? `${ui.green}已开启记忆子系统${ui.reset} — memory_save/search/list/update/forget/graph 进入工具表;` +
|
|
2190
2190
|
`Memory Index 段会在下次拼 system message 时注入。工具表本身的快照需要重启 REPL 才完整刷新。`
|
|
2191
|
-
: `${ui.yellow}已关闭记忆子系统${ui.reset} —
|
|
2191
|
+
: `${ui.yellow}已关闭记忆子系统${ui.reset} — 六个 memory_* 工具将在下次拼 system message 时从工具表过滤;` +
|
|
2192
2192
|
`Memory Index 段不再出现;plan-mode 提示词里的 memory_* 字样消失。重启 REPL 后工具表完全不出现。`;
|
|
2193
2193
|
layout.contentWrite(`${note}\n`);
|
|
2194
2194
|
layout.contentWrite(`${ui.dim}(写入 ${CONFIG_PATH}:MEMORY_ENABLED=${nextEnabled ? 'true' : 'false'};${ui.reset}` +
|
package/dist/sandbox/policy.js
CHANGED
|
@@ -14,7 +14,7 @@ import { jailResolve, jailGlobPattern } from './jail.js';
|
|
|
14
14
|
* Shift+Tab 等用户面触发,不经工具路径。
|
|
15
15
|
*/
|
|
16
16
|
export const SANDBOX_EXEMPT_TOOLS = new Set([
|
|
17
|
-
'memory_save', 'memory_update', 'memory_forget', 'memory_search', 'memory_list',
|
|
17
|
+
'memory_save', 'memory_update', 'memory_forget', 'memory_search', 'memory_list', 'memory_graph',
|
|
18
18
|
'use_skill',
|
|
19
19
|
'web_search', 'web_fetch',
|
|
20
20
|
'ask_human',
|
|
@@ -19,12 +19,13 @@ import { memorySearchTool } from './memory-search.js';
|
|
|
19
19
|
import { memoryListTool } from './memory-list.js';
|
|
20
20
|
import { memoryUpdateTool } from './memory-update.js';
|
|
21
21
|
import { memoryForgetTool } from './memory-forget.js';
|
|
22
|
+
import { memoryGraphTool } from './memory-graph.js';
|
|
22
23
|
import { subAgentTool } from './task.js';
|
|
23
24
|
/**
|
|
24
25
|
* 所有内置工具,按注册顺序排列。
|
|
25
26
|
* 加新工具:在本目录新建 `xxx.ts` 导出一个 Tool,再在下面数组里加一行。无需改 agent / llm。
|
|
26
27
|
*
|
|
27
|
-
* 记忆子系统总开关(MEMORY_ENABLED !== 'true'):
|
|
28
|
+
* 记忆子系统总开关(MEMORY_ENABLED !== 'true'):6 个 memory_* 工具整体不进 builtinTools,
|
|
28
29
|
* 进而不进 LLM 的工具表(模型根本看不到、也不会想着去调)。运行时通过 /memory_switch 切;
|
|
29
30
|
* 切换对当前会话的 tool list 不重算(取的是模块初始化时的快照),所以需要重启 REPL 才生效
|
|
30
31
|
* —— 这是有意为之,避免切开关瞬间把已发出请求的工具列表打乱。
|
|
@@ -41,6 +42,7 @@ const _memoryTools = _memoryEnabledAtBoot
|
|
|
41
42
|
memoryListTool,
|
|
42
43
|
memoryUpdateTool,
|
|
43
44
|
memoryForgetTool,
|
|
45
|
+
memoryGraphTool,
|
|
44
46
|
]
|
|
45
47
|
: [];
|
|
46
48
|
const pathResource = (args) => typeof args.path === 'string' && args.path ? [`file:${args.path}`] : ['workspace'];
|
|
@@ -71,6 +73,8 @@ const CAPABILITIES = {
|
|
|
71
73
|
memory_list: { effect: 'read', concurrency: 'serial', resources: memoryResource },
|
|
72
74
|
memory_update: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
73
75
|
memory_forget: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
76
|
+
// memory_graph:search/neighbors/stats 只读、add 写,统一按写处理走串行(调用不频繁,简化)。
|
|
77
|
+
memory_graph: { effect: 'write', concurrency: 'serial', resources: memoryResource },
|
|
74
78
|
// sub-agent 动态协调:只读任务无锁并行;写任务在 overlay 中执行,merge 时由 ChangeSet 持 canonical lock。
|
|
75
79
|
'sub-agent': {
|
|
76
80
|
effect: 'write',
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { addTriple, graphStats, neighborsOf, pathBetween, } from '../../memory/graph.js';
|
|
2
|
+
// ---------- memory_graph ----------
|
|
3
|
+
// 知识图谱维护工具:邻居遍历(1-3 跳,可 relation 过滤)、两点最短路径、手工加三元组、看图统计。
|
|
4
|
+
// 关键词搜索已并入 memory_search(条目 + 图谱事实一次返回),本工具不再提供 search。
|
|
5
|
+
// 底层 memory-graph.json(Graphiti 式时序边:新事实取代旧边时旧边置 invalidAt,不删)。
|
|
6
|
+
// neighbors/path/stats 只读;add 写。
|
|
7
|
+
function fmtEdges(edges) {
|
|
8
|
+
if (edges.length === 0)
|
|
9
|
+
return '(无边)';
|
|
10
|
+
return edges
|
|
11
|
+
.map((e) => `${e.src} --[${e.relation}]--> ${e.dst}${e.fact ? ` (${e.fact})` : ''}`)
|
|
12
|
+
.join('\n');
|
|
13
|
+
}
|
|
14
|
+
function fmtEntities(entities) {
|
|
15
|
+
if (entities.length === 0)
|
|
16
|
+
return '(无实体)';
|
|
17
|
+
return entities
|
|
18
|
+
.map((e) => `- ${e.id}: ${e.name}${e.summary ? ` — ${e.summary}` : ''} [${e.scope}]`)
|
|
19
|
+
.join('\n');
|
|
20
|
+
}
|
|
21
|
+
export const memoryGraphTool = {
|
|
22
|
+
name: 'memory_graph',
|
|
23
|
+
description: 'Maintain/explore the knowledge-graph memory layer (entities + temporal triples). Keyword search lives in memory_search. ' +
|
|
24
|
+
'action=neighbors: BFS 1-3 hops around an entity (optional relation filter to follow one edge type); ' +
|
|
25
|
+
'action=path: shortest path between two entities (bidirectional BFS, max 6 hops); ' +
|
|
26
|
+
'action=add: add a triple (src --relation--> dst); existing edges with same src/relation/dst are temporally invalidated, not deleted; ' +
|
|
27
|
+
'action=stats: graph size overview.',
|
|
28
|
+
parameters: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
action: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
enum: ['neighbors', 'path', 'add', 'stats'],
|
|
34
|
+
description: 'What to do',
|
|
35
|
+
},
|
|
36
|
+
query: { type: 'string', description: 'neighbors: entity name or id; path: start entity' },
|
|
37
|
+
depth: { type: 'integer', description: 'neighbors: hops, 1-3, default 1' },
|
|
38
|
+
src: { type: 'string', description: 'add: source entity name' },
|
|
39
|
+
relation: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'add: relation in snake_case, e.g. depends_on / decided_by. neighbors: optional edge-type filter to follow only that relation',
|
|
42
|
+
},
|
|
43
|
+
dst: { type: 'string', description: 'add: target entity name; path: end entity' },
|
|
44
|
+
fact: { type: 'string', description: 'add: optional one-line statement for the edge' },
|
|
45
|
+
scope: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
enum: ['project', 'global'],
|
|
48
|
+
description: 'add: which graph file to write, default project',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
required: ['action'],
|
|
52
|
+
},
|
|
53
|
+
async execute(args) {
|
|
54
|
+
const action = String(args.action ?? '').trim();
|
|
55
|
+
if (action === 'stats') {
|
|
56
|
+
const s = graphStats();
|
|
57
|
+
return [
|
|
58
|
+
`实体 ${s.entities} 条边(active ${s.edgesActive} / 已失效 ${s.edgesInvalid})`,
|
|
59
|
+
`project: ${s.byScope.project.entities} 实体, ${s.byScope.project.edges} 边`,
|
|
60
|
+
`global: ${s.byScope.global.entities} 实体, ${s.byScope.global.edges} 边`,
|
|
61
|
+
].join('\n');
|
|
62
|
+
}
|
|
63
|
+
if (action === 'neighbors') {
|
|
64
|
+
const query = String(args.query ?? '').trim();
|
|
65
|
+
if (!query)
|
|
66
|
+
return '错误:neighbors 需要 query(实体名或 id)。';
|
|
67
|
+
const depth = typeof args.depth === 'number' ? args.depth : 1;
|
|
68
|
+
const rel = typeof args.relation === 'string' ? args.relation.trim() : '';
|
|
69
|
+
const r = neighborsOf(query, depth, rel || undefined);
|
|
70
|
+
if (!r.center)
|
|
71
|
+
return `(图中没有实体 "${query}")`;
|
|
72
|
+
const relNote = rel ? `,仅 ${rel} 边` : '';
|
|
73
|
+
const lines = [
|
|
74
|
+
`## ${r.center.name} (${r.center.id})${r.center.summary ? ` — ${r.center.summary}` : ''}`,
|
|
75
|
+
r.entities.length > 0 ? `\n## 邻居实体\n${fmtEntities(r.entities)}` : '',
|
|
76
|
+
`\n## 边(${r.edges.length}${relNote}${r.truncated ? ',已截断' : ''})\n${fmtEdges(r.edges)}`,
|
|
77
|
+
].filter(Boolean);
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
if (action === 'path') {
|
|
81
|
+
const from = String(args.query ?? '').trim();
|
|
82
|
+
const to = String(args.dst ?? '').trim();
|
|
83
|
+
if (!from || !to)
|
|
84
|
+
return '错误:path 需要 query(起点实体)和 dst(终点实体)。';
|
|
85
|
+
const r = pathBetween(from, to);
|
|
86
|
+
if (!r)
|
|
87
|
+
return `(无 active 路径:${from} ⇸ ${to},或端点实体不存在)`;
|
|
88
|
+
const chain = r.path.map((e) => e.name).join(' → ');
|
|
89
|
+
const lines = [
|
|
90
|
+
`## ${r.from.name} ⇢ ${r.to.name}(${r.edges.length} 跳)`,
|
|
91
|
+
`路径:${chain}`,
|
|
92
|
+
`\n## 边\n${fmtEdges(r.edges)}`,
|
|
93
|
+
];
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
if (action === 'add') {
|
|
97
|
+
const src = String(args.src ?? '').trim();
|
|
98
|
+
const relation = String(args.relation ?? '').trim();
|
|
99
|
+
const dst = String(args.dst ?? '').trim();
|
|
100
|
+
if (!src || !relation || !dst)
|
|
101
|
+
return '错误:add 需要 src、relation、dst。';
|
|
102
|
+
const r = addTriple({
|
|
103
|
+
src,
|
|
104
|
+
relation,
|
|
105
|
+
dst,
|
|
106
|
+
fact: typeof args.fact === 'string' ? args.fact : undefined,
|
|
107
|
+
scope: args.scope === 'global' ? 'global' : 'project',
|
|
108
|
+
});
|
|
109
|
+
if (!r.ok)
|
|
110
|
+
return `错误:三元组未写入 (${r.reason})。`;
|
|
111
|
+
if (r.duplicate)
|
|
112
|
+
return `已存在相同三元组 (${r.edgeId}),幂等跳过。`;
|
|
113
|
+
const sup = r.superseded > 0 ? `;旧边已时序失效(${r.superseded} 条)` : '';
|
|
114
|
+
return `已写入三元组 ${src} --[${relation.toLowerCase().replace(/\s+/g, '_')}]--> ${dst} [${r.edgeId}]${sup}。`;
|
|
115
|
+
}
|
|
116
|
+
return `错误:未知 action "${action}",可用 neighbors/path/add/stats(关键词搜索请用 memory_search)。`;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { saveEntry } from '../../memory/store.js';
|
|
2
|
+
import { addTriple } from '../../memory/graph.js';
|
|
2
3
|
// ---------- memory_save ----------
|
|
3
4
|
// 存一条长期记忆(跨会话)。启动只把标题/摘要注入索引(几百 token);详情按需 memory_search 取。
|
|
4
5
|
// 撞库(name→id 已存在)拒绝,引导用 memory_update。
|
|
6
|
+
// 可选 links:把本条记忆挂进知识图谱(memory-graph.json)——src 省略时默认以记忆 name 为主实体。
|
|
5
7
|
export const memorySaveTool = {
|
|
6
8
|
name: 'memory_save',
|
|
7
|
-
description: 'Save a cross-session long-term memory entry. Store only non-obvious, useful facts/decisions/pitfalls. Title enters the startup index; retrieve body via memory_search.',
|
|
9
|
+
description: 'Save a cross-session long-term memory entry. Store only non-obvious, useful facts/decisions/pitfalls. Title enters the startup index; retrieve body via memory_search. Optionally attach knowledge-graph links (triples) to relate this memory to entities.',
|
|
8
10
|
risk: 'confirm',
|
|
9
11
|
parameters: {
|
|
10
12
|
type: 'object',
|
|
@@ -23,6 +25,20 @@ export const memorySaveTool = {
|
|
|
23
25
|
enum: ['project', 'global'],
|
|
24
26
|
description: 'Store at project level (<cwd>/.mocode/) or global (~/.mocode/), default project',
|
|
25
27
|
},
|
|
28
|
+
links: {
|
|
29
|
+
type: 'array',
|
|
30
|
+
description: 'Optional knowledge-graph triples relating this memory to entities, e.g. [{"src":"mocode","relation":"depends_on","dst":"JSONL store"}]. src defaults to the memory name when omitted.',
|
|
31
|
+
items: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
src: { type: 'string', description: 'Source entity name (defaults to the memory name)' },
|
|
35
|
+
relation: { type: 'string', description: 'Relation, snake_case, e.g. depends_on / decided_by / conflicts_with' },
|
|
36
|
+
dst: { type: 'string', description: 'Target entity name' },
|
|
37
|
+
fact: { type: 'string', description: 'Optional one-line statement for the edge' },
|
|
38
|
+
},
|
|
39
|
+
required: ['relation', 'dst'],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
26
42
|
},
|
|
27
43
|
required: ['name', 'summary', 'body'],
|
|
28
44
|
},
|
|
@@ -37,16 +53,37 @@ export const memorySaveTool = {
|
|
|
37
53
|
if (!body)
|
|
38
54
|
return '错误:缺少 body。';
|
|
39
55
|
const type = typeof args.type === 'string' ? args.type : undefined;
|
|
56
|
+
const scope = args.scope === 'global' ? 'global' : 'project';
|
|
40
57
|
const r = saveEntry({
|
|
41
58
|
name,
|
|
42
59
|
summary,
|
|
43
60
|
body,
|
|
44
61
|
type,
|
|
45
62
|
pinned: args.pinned === true,
|
|
46
|
-
scope
|
|
63
|
+
scope,
|
|
47
64
|
});
|
|
48
|
-
if (r.ok)
|
|
49
|
-
return
|
|
50
|
-
|
|
65
|
+
if (!r.ok) {
|
|
66
|
+
return `已存在同名记忆 [${r.exists}]。改用 memory_update(id="${r.exists}", …) 更新,或换一个 name。`;
|
|
67
|
+
}
|
|
68
|
+
// 知识图谱挂边:容错——图失败不影响记忆保存结果
|
|
69
|
+
const links = Array.isArray(args.links) ? args.links : [];
|
|
70
|
+
let linked = 0;
|
|
71
|
+
for (const l of links) {
|
|
72
|
+
if (!l || typeof l !== 'object')
|
|
73
|
+
continue;
|
|
74
|
+
const link = l;
|
|
75
|
+
const tr = addTriple({
|
|
76
|
+
src: typeof link.src === 'string' && link.src.trim() ? link.src : name,
|
|
77
|
+
relation: typeof link.relation === 'string' ? link.relation : '',
|
|
78
|
+
dst: typeof link.dst === 'string' ? link.dst : '',
|
|
79
|
+
fact: typeof link.fact === 'string' ? link.fact : undefined,
|
|
80
|
+
sourceEntry: r.id,
|
|
81
|
+
scope,
|
|
82
|
+
});
|
|
83
|
+
if (tr.ok)
|
|
84
|
+
linked++;
|
|
85
|
+
}
|
|
86
|
+
const linkNote = links.length > 0 ? `;知识图谱挂边 ${linked}/${links.length}` : '';
|
|
87
|
+
return `已保存记忆 [${r.id}] "${name}"(下次启动进索引)${linkNote}。`;
|
|
51
88
|
},
|
|
52
89
|
};
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { searchEntries } from '../../memory/store.js';
|
|
2
|
+
import { searchGraph } from '../../memory/graph.js';
|
|
2
3
|
// ---------- memory_search ----------
|
|
3
|
-
//
|
|
4
|
+
// 唯一记忆搜索入口:关键词搜记忆正文(多词子串匹配,name 权重最高)+ 知识图谱事实段
|
|
5
|
+
// (命中实体的 active 边)。命中条目即 bump recallCount(遗忘衰减依据)。
|
|
4
6
|
// 结果走 capToolResultForHistory 的放宽上限(同 use_skill,保正文完整)。
|
|
7
|
+
const GRAPH_FACTS_LIMIT = 10;
|
|
5
8
|
export const memorySearchTool = {
|
|
6
9
|
name: 'memory_search',
|
|
7
|
-
description: 'Search memory entries by keyword (substring match), returning full body.',
|
|
10
|
+
description: 'Search memory entries by keyword (substring match), returning full body. Also surfaces knowledge-graph facts (active edges) for entities matching the query.',
|
|
8
11
|
parameters: {
|
|
9
12
|
type: 'object',
|
|
10
13
|
properties: {
|
|
@@ -33,10 +36,28 @@ export const memorySearchTool = {
|
|
|
33
36
|
: undefined,
|
|
34
37
|
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
35
38
|
});
|
|
36
|
-
|
|
37
|
-
return `(无匹配记忆:query="${query}")`;
|
|
38
|
-
return r
|
|
39
|
+
const entryText = r
|
|
39
40
|
.map((e) => `# [${e.id}] ${e.name} (${e.type}, recalled ${e.recallCount})\nsummary: ${e.summary}\n\n${e.body}`)
|
|
40
41
|
.join('\n\n---\n\n');
|
|
42
|
+
// 知识图谱事实段:命中实体的 active 边(容错:图坏了不连累条目搜索)。
|
|
43
|
+
let graphText = '';
|
|
44
|
+
try {
|
|
45
|
+
const g = searchGraph(query, 8);
|
|
46
|
+
if (g.edges.length > 0) {
|
|
47
|
+
const lines = g.edges
|
|
48
|
+
.slice(0, GRAPH_FACTS_LIMIT)
|
|
49
|
+
.map((e) => `${e.src} --[${e.relation}]--> ${e.dst}${e.fact ? ` (${e.fact})` : ''}`);
|
|
50
|
+
const more = g.edges.length > GRAPH_FACTS_LIMIT ? `\n…(共 ${g.edges.length} 条,其余用 memory_graph action=neighbors 展开)` : '';
|
|
51
|
+
graphText = `\n\n## 知识图谱事实\n${lines.join('\n')}${more}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// 静默:图谱段是增强,失败只降级为纯条目结果
|
|
56
|
+
}
|
|
57
|
+
if (!entryText && !graphText)
|
|
58
|
+
return `(无匹配记忆:query="${query}")`;
|
|
59
|
+
if (!entryText)
|
|
60
|
+
return `(无匹配记忆条目,但图谱有命中)\n${graphText.trimStart()}`;
|
|
61
|
+
return entryText + graphText;
|
|
41
62
|
},
|
|
42
63
|
};
|
package/dist/tools/constants.js
CHANGED
|
@@ -23,6 +23,11 @@ export const DECAY_DAYS = 30;
|
|
|
23
23
|
export const GC_DAYS = 90;
|
|
24
24
|
/** memory_search 结果(召回的记忆正文)的放宽上限:指令性内容,中截破坏语义,对齐 use_skill。 */
|
|
25
25
|
export const MAX_MEMORY_RESULT = 64000;
|
|
26
|
+
// ── 知识图谱层(memory-graph.json,单 scope 容量)──────────────────────────
|
|
27
|
+
/** 单 scope 实体封顶:超限先清孤儿实体(无 active 边相连),仍超则拒绝新建。 */
|
|
28
|
+
export const MAX_GRAPH_ENTITIES = 500;
|
|
29
|
+
/** 单 scope 边封顶:超限先清已失效边,仍超则拒绝新边。 */
|
|
30
|
+
export const MAX_GRAPH_EDGES = 2000;
|
|
26
31
|
// .codegraph:codegraph 索引目录(codegraph.db 是 SQLite 二进制 + daemon.log),
|
|
27
32
|
// grep/glob 扫它无意义且会产出数 KB 的超长「行」,污染 TUI 展开渲染。
|
|
28
33
|
export const IGNORE = ['**/node_modules/**', '**/.git/**', '**/.codegraph/**'];
|
|
@@ -56,6 +61,7 @@ export const PLAN_DISABLED_TOOLS = new Set([
|
|
|
56
61
|
'memory_save',
|
|
57
62
|
'memory_update',
|
|
58
63
|
'memory_forget',
|
|
64
|
+
'memory_graph', // 混合工具(add 写图),plan 只读模式整体屏蔽
|
|
59
65
|
'sub-agent',
|
|
60
66
|
'run_skill', // fork 子 agent 执行面;plan 模式不应派生子工作流
|
|
61
67
|
]);
|
package/dist/ui/layout.js
CHANGED
|
@@ -1361,8 +1361,8 @@ function composeSpinnerLine(status, cols) {
|
|
|
1361
1361
|
leadW = 1 + 1 + displayWidth(status.status) + (elapsed ? 1 + displayWidth(elapsed) : 0);
|
|
1362
1362
|
}
|
|
1363
1363
|
else if (spinning) {
|
|
1364
|
-
// 运行态心跳帧(流式输出中):帧 +
|
|
1365
|
-
const label = '生成中';
|
|
1364
|
+
// 运行态心跳帧(流式输出中 / 命令态如 /rollback /compact /resume):帧 + 状态文字(优先)或生成中(兜底) + 走时
|
|
1365
|
+
const label = status.status || '生成中';
|
|
1366
1366
|
const ePart = elapsed ? ` ${ui.dim}${elapsed}${ui.reset}` : '';
|
|
1367
1367
|
lead = `${ui.bold}${ui.accent}${RUNNING_FRAMES[runningFrame]}${ui.reset} ${ui.dim}${label}${ui.reset}${ePart}`;
|
|
1368
1368
|
leadW = 1 + 1 + displayWidth(label) + (elapsed ? 1 + displayWidth(elapsed) : 0);
|
package/dist/ui/theme.js
CHANGED
|
@@ -29,7 +29,7 @@ const DEFAULT = {
|
|
|
29
29
|
brightCyan: '\x1B[38;2;86;182;194m',
|
|
30
30
|
brightMagenta: '\x1B[38;2;198;120;221m',
|
|
31
31
|
accent: '\x1B[38;2;86;182;194m', // 与 cyan 同源(One Dark):logo/标题/输入框顶线/选中项统一承载
|
|
32
|
-
userBg: '\x1B[48;2;
|
|
32
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
33
33
|
// diff 行底色:One Dark bg #282c34 上加 ~14% 亮度的对应色,够辨识但不刺眼
|
|
34
34
|
addBg: '\x1B[48;2;44;62;42m', // 偏暗绿(One Dark green(152,195,121)暗化)
|
|
35
35
|
delBg: '\x1B[48;2;62;38;42m', // 偏暗红(One Dark red(224,108,117)暗化)
|
|
@@ -54,7 +54,7 @@ const THEMES = {
|
|
|
54
54
|
brightCyan: '\x1B[38;2;42;161;152m',
|
|
55
55
|
brightMagenta: '\x1B[38;2;108;113;196m',
|
|
56
56
|
accent: '\x1B[38;2;38;139;210m', // Solarized blue:浅底下更醒目的强调
|
|
57
|
-
userBg: '\x1B[48;2;
|
|
57
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
58
58
|
// diff 行底色:Solarized Light base2(238,232,213)上贴同色族浅 tint,
|
|
59
59
|
// 比直接用 base1 更柔,跟深 fg(red/green)对比充足
|
|
60
60
|
addBg: '\x1B[48;2;220;235;205m',
|
|
@@ -72,7 +72,7 @@ const THEMES = {
|
|
|
72
72
|
brightCyan: '\x1B[38;2;147;161;161m',
|
|
73
73
|
brightMagenta: '\x1B[38;2;108;113;196m',
|
|
74
74
|
accent: '\x1B[38;2;42;161;152m', // Solarized cyan(深底版):暗底上跳出
|
|
75
|
-
userBg: '\x1B[48;2;
|
|
75
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
76
76
|
// diff 行底色:Solarized Dark base03(0,43,54)上加对应色族暗 tint
|
|
77
77
|
addBg: '\x1B[48;2;20;50;38m',
|
|
78
78
|
delBg: '\x1B[48;2;55;30;30m',
|
|
@@ -89,7 +89,7 @@ const THEMES = {
|
|
|
89
89
|
brightCyan: '\x1B[38;2;142;192;124m',
|
|
90
90
|
brightMagenta: '\x1B[38;2;211;134;155m',
|
|
91
91
|
accent: '\x1B[38;2;250;189;47m', // Gruvbox yellow(主题色)
|
|
92
|
-
userBg: '\x1B[48;2;
|
|
92
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
93
93
|
// diff 行底色:Gruvbox dark bg(40,40,40)上贴暗 bg0_a 风格
|
|
94
94
|
addBg: '\x1B[48;2;40;55;30m',
|
|
95
95
|
delBg: '\x1B[48;2;70;35;30m',
|
|
@@ -106,7 +106,7 @@ const THEMES = {
|
|
|
106
106
|
brightCyan: '\x1B[38;2;136;192;208m',
|
|
107
107
|
brightMagenta: '\x1B[38;2;180;142;173m',
|
|
108
108
|
accent: '\x1B[38;2;136;192;208m', // Nord 浅冰蓝(主题色)
|
|
109
|
-
userBg: '\x1B[48;2;
|
|
109
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
110
110
|
// diff 行底色:Nord polar night(46,52,64)上贴对应色族暗 tint
|
|
111
111
|
addBg: '\x1B[48;2;46;66;52m',
|
|
112
112
|
delBg: '\x1B[48;2;72;46;52m',
|
|
@@ -126,7 +126,7 @@ const THEMES = {
|
|
|
126
126
|
brightCyan: '\x1B[38;2;160;220;220m',
|
|
127
127
|
brightMagenta: '\x1B[38;2;245;165;215m',
|
|
128
128
|
accent: '\x1B[38;2;255;170;60m', // 南瓜橙(主题主色:logo/标题/输入框顶线/选中项)
|
|
129
|
-
userBg: '\x1B[48;2;
|
|
129
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
130
130
|
// diff 行底色:暖深棕底上贴对应色族暗 tint,跟 fg 配对柔和可辨
|
|
131
131
|
addBg: '\x1B[48;2;55;70;35m',
|
|
132
132
|
delBg: '\x1B[48;2;78;42;32m',
|
|
@@ -145,7 +145,7 @@ const THEMES = {
|
|
|
145
145
|
brightCyan: '\x1B[38;2;170;220;220m',
|
|
146
146
|
brightMagenta: '\x1B[38;2;250;160;200m',
|
|
147
147
|
accent: '\x1B[38;2;230;90;150m', // 玫粉(主题主色)
|
|
148
|
-
userBg: '\x1B[48;2;
|
|
148
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
149
149
|
// diff 行底色:深紫底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏紫红
|
|
150
150
|
addBg: '\x1B[48;2;50;60;42m',
|
|
151
151
|
delBg: '\x1B[48;2;75;40;52m',
|
|
@@ -164,7 +164,7 @@ const THEMES = {
|
|
|
164
164
|
brightCyan: '\x1B[38;2;140;230;210m',
|
|
165
165
|
brightMagenta: '\x1B[38;2;210;170;230m',
|
|
166
166
|
accent: '\x1B[38;2;80;210;140m', // 翡翠绿(主题主色)
|
|
167
|
-
userBg: '\x1B[48;2;
|
|
167
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
168
168
|
// diff 行底色:深绿底贴对应色族暗 tint,绿暗化偏深绿、红暗化偏暗红
|
|
169
169
|
addBg: '\x1B[48;2;30;55;40m',
|
|
170
170
|
delBg: '\x1B[48;2;60;40;40m',
|
|
@@ -183,7 +183,7 @@ const THEMES = {
|
|
|
183
183
|
brightCyan: '\x1B[38;2;170;210;200m',
|
|
184
184
|
brightMagenta: '\x1B[38;2;240;180;210m',
|
|
185
185
|
accent: '\x1B[38;2;255;200;80m', // 琥珀金黄(主题主色)
|
|
186
|
-
userBg: '\x1B[48;2;
|
|
186
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
187
187
|
// diff 行底色:深棕底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏暗棕红
|
|
188
188
|
addBg: '\x1B[48;2;50;55;25m',
|
|
189
189
|
delBg: '\x1B[48;2;70;40;28m',
|
|
@@ -202,7 +202,7 @@ const THEMES = {
|
|
|
202
202
|
brightCyan: '\x1B[38;2;180;230;230m',
|
|
203
203
|
brightMagenta: '\x1B[38;2;210;180;250m',
|
|
204
204
|
accent: '\x1B[38;2;180;150;230m', // 薰衣草紫(主题主色)
|
|
205
|
-
userBg: '\x1B[48;2;
|
|
205
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
206
206
|
// diff 行底色:深紫底贴对应色族暗 tint,绿暗化偏冷绿、红暗化偏冷紫红
|
|
207
207
|
addBg: '\x1B[48;2;38;46;42m',
|
|
208
208
|
delBg: '\x1B[48;2;60;40;55m',
|
|
@@ -221,7 +221,7 @@ const THEMES = {
|
|
|
221
221
|
brightCyan: '\x1B[38;2;170;225;215m',
|
|
222
222
|
brightMagenta: '\x1B[38;2;250;170;210m',
|
|
223
223
|
accent: '\x1B[38;2;255;120;100m', // 珊瑚红(主题主色)
|
|
224
|
-
userBg: '\x1B[48;2;
|
|
224
|
+
userBg: '\x1B[48;2;72;78;90m', // 统一灰白用户消息底(黑底终端可见)
|
|
225
225
|
// diff 行底色:深棕红底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏暗棕红
|
|
226
226
|
addBg: '\x1B[48;2;50;55;30m',
|
|
227
227
|
delBg: '\x1B[48;2;75;32;30m',
|