dsh-cortex-memory 0.1.0 → 0.2.0-rc.11
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/LICENSE +3 -3
- package/README.md +85 -146
- package/bench/ab-engine.mjs +60 -0
- package/bench/ann-bench.mjs +78 -0
- package/bench/hnsw-bench.mjs +79 -0
- package/bench/metrics.js +8 -0
- package/bench/migration-bench.mjs +69 -0
- package/bench/run.js +5 -2
- package/bench/synthetic.js +51 -7
- package/bench/vector-ab.mjs +76 -0
- package/bin/cortex.mjs +2 -3
- package/client.js +269 -4
- package/cordis.patch.yml +1 -1
- package/docs/install-windows.md +115 -27
- package/docs/models.md +165 -30
- package/docs/multimodal.md +112 -0
- package/docs/scopes.md +142 -0
- package/examples/README.md +72 -0
- package/examples/setup-cortex-bge.ps1 +66 -0
- package/examples/uninstall-cortex.ps1 +91 -0
- package/index.js +15 -1
- package/package.json +6 -13
- package/src/adapters/markdown-import.js +20 -1
- package/src/adapters/markdown.js +12 -2
- package/src/adapters/shiori.js +1 -1
- package/src/api/routes.js +144 -4
- package/src/api/validate.js +26 -0
- package/src/core/attachment-reader.js +31 -0
- package/src/core/attachments.js +39 -0
- package/src/core/config.js +36 -1
- package/src/core/model-config.js +3 -1
- package/src/core/profile.js +32 -0
- package/src/engines/develop.js +1 -1
- package/src/engines/encode.js +68 -30
- package/src/engines/extract.js +13 -8
- package/src/engines/growth.js +1 -1
- package/src/engines/reflect.js +1 -1
- package/src/engines/retrieve.js +44 -13
- package/src/host/runtime.js +1 -1
- package/src/memory/query.js +1 -1
- package/src/memory/tokenizer.js +19 -4
- package/src/security/scope-grants.js +73 -0
- package/src/self/appraisal.js +3 -3
- package/src/self/emotion.js +1 -1
- package/src/self/identity.js +1 -1
- package/src/self/relationship.js +2 -2
- package/src/service/cortex.js +191 -3
- package/src/store/database.js +33 -1
- package/src/store/repositories/edges.js +48 -4
- package/src/store/repositories/traces.js +23 -12
- package/src/store/schema.sql +1 -0
- package/src/tools/definitions.js +62 -5
- package/src/vector/external-ann.js +37 -0
- package/src/vector/hnsw.js +246 -0
- package/src/vector/index.js +85 -2
- package/docs/compatibility.md +0 -39
package/src/store/database.js
CHANGED
|
@@ -5,10 +5,19 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
5
5
|
import { nowIso } from '../core/time.js';
|
|
6
6
|
import { normalizeConfig, validateConfig } from '../core/config.js';
|
|
7
7
|
import { CortexError, ERROR_CODES } from '../core/errors.js';
|
|
8
|
+
import { ftsText } from '../memory/tokenizer.js';
|
|
8
9
|
|
|
9
10
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const SCHEMA_FILE = path.join(HERE, 'schema.sql');
|
|
11
12
|
export const SCHEMA_VERSION = 6;
|
|
13
|
+
export const FTS_INDEX_VERSION = 2;
|
|
14
|
+
|
|
15
|
+
function ensureTraceColumns(db) {
|
|
16
|
+
const columns = db.prepare('PRAGMA table_info(traces)').all();
|
|
17
|
+
if (!columns.some((column) => column.name === 'metadata_json')) {
|
|
18
|
+
db.exec("ALTER TABLE traces ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}'");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
12
21
|
|
|
13
22
|
function ensureFactsColumns(db) {
|
|
14
23
|
const columns = db.prepare('PRAGMA table_info(facts)').all();
|
|
@@ -25,6 +34,27 @@ function ensureParent(file) {
|
|
|
25
34
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
26
35
|
}
|
|
27
36
|
|
|
37
|
+
function rebuildFtsIndex(db) {
|
|
38
|
+
db.exec('DELETE FROM traces_fts_index');
|
|
39
|
+
const insert = db.prepare('INSERT INTO traces_fts_index(trace_id, text) VALUES (?, ?)');
|
|
40
|
+
const rows = db.prepare('SELECT id, title, content, summary FROM traces').all();
|
|
41
|
+
for (const row of rows) {
|
|
42
|
+
const text = ftsText(`${row.title ?? ''} ${row.content ?? ''} ${row.summary ?? ''}`, { soft: true });
|
|
43
|
+
if (text) insert.run(row.id, text);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ensureFtsIndex(db) {
|
|
48
|
+
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get('fts_index_version');
|
|
49
|
+
if (Number(row?.value ?? 0) === FTS_INDEX_VERSION) return false;
|
|
50
|
+
rebuildFtsIndex(db);
|
|
51
|
+
db.prepare(`
|
|
52
|
+
INSERT INTO meta(key, value, updated_at) VALUES (?, ?, ?)
|
|
53
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
54
|
+
`).run('fts_index_version', String(FTS_INDEX_VERSION), nowIso());
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
28
58
|
function readSchema() {
|
|
29
59
|
return fs.readFileSync(SCHEMA_FILE, 'utf8');
|
|
30
60
|
}
|
|
@@ -44,6 +74,7 @@ export function migrate(db, { version = SCHEMA_VERSION } = {}) {
|
|
|
44
74
|
db.exec('BEGIN IMMEDIATE');
|
|
45
75
|
try {
|
|
46
76
|
db.exec(readSchema());
|
|
77
|
+
ensureTraceColumns(db);
|
|
47
78
|
ensureFactsColumns(db);
|
|
48
79
|
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get('schema_version');
|
|
49
80
|
if (!row) {
|
|
@@ -53,8 +84,9 @@ export function migrate(db, { version = SCHEMA_VERSION } = {}) {
|
|
|
53
84
|
} else if (Number(row.value) < version) {
|
|
54
85
|
db.prepare('UPDATE meta SET value = ?, updated_at = ? WHERE key = ?').run(String(version), nowIso(), 'schema_version');
|
|
55
86
|
}
|
|
87
|
+
const ftsRebuilt = ensureFtsIndex(db);
|
|
56
88
|
db.exec('COMMIT');
|
|
57
|
-
return { version, applied: true };
|
|
89
|
+
return { version, applied: true, ftsRebuilt };
|
|
58
90
|
} catch (error) {
|
|
59
91
|
try { db.exec('ROLLBACK'); } catch { /* ignore rollback failure */ }
|
|
60
92
|
throw error;
|
|
@@ -31,6 +31,9 @@ export function createEdgeRepository(db) {
|
|
|
31
31
|
ORDER BY valid_from DESC LIMIT 1
|
|
32
32
|
`).get(scopeId, fromId, toId, rel, validFrom);
|
|
33
33
|
if (existing) {
|
|
34
|
+
const current = rowToEdge(existing);
|
|
35
|
+
// A candidate must never downgrade an already-active edge.
|
|
36
|
+
if (metadata?.status === 'candidate' && current.metadata?.status !== 'candidate') return current;
|
|
34
37
|
db.prepare('UPDATE edges SET weight = ?, metadata_json = ?, updated_at = ? WHERE id = ?')
|
|
35
38
|
.run(Number(weight), stableStringify(metadata), nowIso(), existing.id);
|
|
36
39
|
return get(existing.id);
|
|
@@ -68,13 +71,54 @@ export function createEdgeRepository(db) {
|
|
|
68
71
|
return rows.map(rowToEdge).filter((edge) => {
|
|
69
72
|
if (seen.has(edge.id)) return false;
|
|
70
73
|
seen.add(edge.id);
|
|
71
|
-
return
|
|
74
|
+
return edge.metadata?.status !== 'candidate' && edge.metadata?.status !== 'rejected';
|
|
72
75
|
});
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
function list({ scopeId = 'global', limit = 200 } = {}) {
|
|
78
|
+
function list({ scopeId = 'global', limit = 200, includeCandidates = false } = {}) {
|
|
76
79
|
const count = Math.max(1, Math.min(10000, Number(limit) || 200));
|
|
77
|
-
return db.prepare('SELECT * FROM edges WHERE scope_id = ? ORDER BY updated_at DESC LIMIT ?').all(scopeId, count)
|
|
80
|
+
return db.prepare('SELECT * FROM edges WHERE scope_id = ? ORDER BY updated_at DESC LIMIT ?').all(scopeId, count)
|
|
81
|
+
.map(rowToEdge)
|
|
82
|
+
.filter((edge) => includeCandidates || (edge.metadata?.status !== 'candidate' && edge.metadata?.status !== 'rejected'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function addCandidate({ scopeId = 'global', fromId, toId, rel = 'related', weight = 0.5, confidence = 0.5, sourceEventId = null, sourceTraceId = null, evidence = null, metadata = {} } = {}) {
|
|
86
|
+
return upsert({
|
|
87
|
+
scopeId,
|
|
88
|
+
fromId,
|
|
89
|
+
toId,
|
|
90
|
+
rel,
|
|
91
|
+
weight,
|
|
92
|
+
metadata: {
|
|
93
|
+
...metadata,
|
|
94
|
+
status: 'candidate',
|
|
95
|
+
confidence,
|
|
96
|
+
sourceEventId,
|
|
97
|
+
sourceTraceId,
|
|
98
|
+
evidence,
|
|
99
|
+
proposedAt: nowIso(),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function listCandidates({ scopeId = 'global', limit = 200 } = {}) {
|
|
105
|
+
return list({ scopeId, limit, includeCandidates: true }).filter((edge) => edge.metadata?.status === 'candidate');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function approveCandidate(id, { at = nowIso() } = {}) {
|
|
109
|
+
const edge = get(id);
|
|
110
|
+
if (!edge || edge.metadata?.status !== 'candidate') return null;
|
|
111
|
+
db.prepare('UPDATE edges SET metadata_json = ?, updated_at = ? WHERE id = ?')
|
|
112
|
+
.run(stableStringify({ ...edge.metadata, status: 'active', approvedAt: at }), at, id);
|
|
113
|
+
return get(id);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function rejectCandidate(id, { at = nowIso() } = {}) {
|
|
117
|
+
const edge = get(id);
|
|
118
|
+
if (!edge || edge.metadata?.status !== 'candidate') return null;
|
|
119
|
+
db.prepare('UPDATE edges SET valid_to = ?, metadata_json = ?, updated_at = ? WHERE id = ?')
|
|
120
|
+
.run(at, stableStringify({ ...edge.metadata, status: 'rejected', rejectedAt: at }), at, id);
|
|
121
|
+
return get(id);
|
|
78
122
|
}
|
|
79
123
|
|
|
80
124
|
function invalidate(id, { at = nowIso() } = {}) {
|
|
@@ -82,5 +126,5 @@ export function createEdgeRepository(db) {
|
|
|
82
126
|
return get(id);
|
|
83
127
|
}
|
|
84
128
|
|
|
85
|
-
return { get, upsert, neighbors, list, invalidate };
|
|
129
|
+
return { get, upsert, neighbors, list, addCandidate, listCandidates, approveCandidate, rejectCandidate, invalidate };
|
|
86
130
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { hashText, traceId } from '../../core/ids.js';
|
|
2
|
-
import { asJsonArray, stableStringify } from '../../core/json.js';
|
|
2
|
+
import { asJsonArray, asJsonObject, stableStringify } from '../../core/json.js';
|
|
3
3
|
import { nowIso } from '../../core/time.js';
|
|
4
4
|
import { ftsText, buildMatchQuery } from '../../memory/tokenizer.js';
|
|
5
5
|
import { CortexError, ERROR_CODES } from '../../core/errors.js';
|
|
6
|
+
import { FTS_INDEX_VERSION } from '../database.js';
|
|
6
7
|
|
|
7
8
|
const COLUMNS = [
|
|
8
|
-
'title', 'content', 'summary', 'keywords_json', 'tags_json', 'event_id',
|
|
9
|
+
'title', 'content', 'summary', 'keywords_json', 'tags_json', 'metadata_json', 'event_id',
|
|
9
10
|
'session_id', 'turn_id', 'actor', 'source', 'valid_at', 'recorded_at',
|
|
10
11
|
'last_access_at', 'access_count', 'importance', 'confidence', 'salience',
|
|
11
12
|
'valence', 'arousal', 'sensitivity', 'state', 'compression', 'stability',
|
|
@@ -20,6 +21,7 @@ const FIELD_MAP = Object.freeze({
|
|
|
20
21
|
kind: 'kind',
|
|
21
22
|
keywords: 'keywords_json',
|
|
22
23
|
tags: 'tags_json',
|
|
24
|
+
metadata: 'metadata_json',
|
|
23
25
|
eventId: 'event_id',
|
|
24
26
|
sessionId: 'session_id',
|
|
25
27
|
turnId: 'turn_id',
|
|
@@ -57,6 +59,7 @@ function rowToTrace(row) {
|
|
|
57
59
|
summary: row.summary,
|
|
58
60
|
keywords: asJsonArray(row.keywords_json),
|
|
59
61
|
tags: asJsonArray(row.tags_json),
|
|
62
|
+
metadata: asJsonObject(row.metadata_json),
|
|
60
63
|
eventId: row.event_id,
|
|
61
64
|
sessionId: row.session_id,
|
|
62
65
|
turnId: row.turn_id,
|
|
@@ -87,9 +90,11 @@ function rowToTrace(row) {
|
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
export function createTraceRepository(db) {
|
|
90
|
-
function syncFts(trace) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
function syncFts(trace, { replace = true } = {}) {
|
|
94
|
+
// trace_id is UNINDEXED in FTS5. Deleting by it on a brand-new insert scans
|
|
95
|
+
// the whole index and makes bulk writes quadratic; only updates need delete.
|
|
96
|
+
if (replace) db.prepare('DELETE FROM traces_fts_index WHERE trace_id = ?').run(trace.id);
|
|
97
|
+
const text = ftsText(`${trace.title ?? ''} ${trace.content ?? ''} ${trace.summary ?? ''}`, { soft: true });
|
|
93
98
|
if (text) {
|
|
94
99
|
db.prepare('INSERT INTO traces_fts_index(trace_id, text) VALUES (?, ?)').run(trace.id, text);
|
|
95
100
|
}
|
|
@@ -110,6 +115,7 @@ export function createTraceRepository(db) {
|
|
|
110
115
|
summary: input.summary ?? null,
|
|
111
116
|
keywords: asJsonArray(input.keywords),
|
|
112
117
|
tags: asJsonArray(input.tags),
|
|
118
|
+
metadata: asJsonObject(input.metadata),
|
|
113
119
|
eventId: input.eventId,
|
|
114
120
|
sessionId: input.sessionId ?? null,
|
|
115
121
|
turnId: input.turnId ?? null,
|
|
@@ -140,15 +146,15 @@ export function createTraceRepository(db) {
|
|
|
140
146
|
db.prepare(`
|
|
141
147
|
INSERT INTO traces(
|
|
142
148
|
id, scope_id, kind, title, content, summary, keywords_json, tags_json,
|
|
143
|
-
event_id, session_id, turn_id, actor, source, valid_at, recorded_at,
|
|
149
|
+
metadata_json, event_id, session_id, turn_id, actor, source, valid_at, recorded_at,
|
|
144
150
|
last_access_at, access_count, importance, confidence, salience, valence,
|
|
145
151
|
arousal, sensitivity, state, compression, stability, difficulty,
|
|
146
152
|
retrievability, review_count, lapse_count, last_review_at, hash,
|
|
147
153
|
created_at, updated_at
|
|
148
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
154
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
149
155
|
`).run(
|
|
150
156
|
trace.id, trace.scopeId, trace.kind, trace.title, trace.content, trace.summary,
|
|
151
|
-
stableStringify(trace.keywords), stableStringify(trace.tags), trace.eventId,
|
|
157
|
+
stableStringify(trace.keywords), stableStringify(trace.tags), stableStringify(trace.metadata), trace.eventId,
|
|
152
158
|
trace.sessionId, trace.turnId, trace.actor, trace.source, trace.validAt,
|
|
153
159
|
trace.recordedAt, trace.lastAccessAt, trace.accessCount, trace.importance,
|
|
154
160
|
trace.confidence, trace.salience, trace.valence, trace.arousal,
|
|
@@ -156,7 +162,7 @@ export function createTraceRepository(db) {
|
|
|
156
162
|
trace.difficulty, trace.retrievability, trace.reviewCount, trace.lapseCount,
|
|
157
163
|
trace.lastReviewAt, trace.hash, trace.createdAt, trace.updatedAt,
|
|
158
164
|
);
|
|
159
|
-
syncFts(trace);
|
|
165
|
+
syncFts(trace, { replace: false });
|
|
160
166
|
return trace;
|
|
161
167
|
}
|
|
162
168
|
|
|
@@ -198,6 +204,7 @@ export function createTraceRepository(db) {
|
|
|
198
204
|
if (!(key in patch)) continue;
|
|
199
205
|
let value = patch[key];
|
|
200
206
|
if (key === 'keywords' || key === 'tags') value = stableStringify(asJsonArray(value));
|
|
207
|
+
if (key === 'metadata') value = stableStringify(asJsonObject(value));
|
|
201
208
|
sets.push(`${column} = ?`);
|
|
202
209
|
values.push(value);
|
|
203
210
|
}
|
|
@@ -222,8 +229,8 @@ export function createTraceRepository(db) {
|
|
|
222
229
|
return result.changes > 0;
|
|
223
230
|
}
|
|
224
231
|
|
|
225
|
-
function searchFts(query, { scopeId = 'global', limit = 30, includeArchived = false } = {}) {
|
|
226
|
-
const match = buildMatchQuery(query);
|
|
232
|
+
function searchFts(query, { scopeId = 'global', limit = 30, includeArchived = false, soft = false } = {}) {
|
|
233
|
+
const match = buildMatchQuery(query, { soft });
|
|
227
234
|
if (!match) return [];
|
|
228
235
|
const count = Math.max(1, Math.min(200, Number(limit) || 30));
|
|
229
236
|
const stateClause = includeArchived ? "t.state IN ('active','archived')" : "t.state = 'active'";
|
|
@@ -253,7 +260,11 @@ export function createTraceRepository(db) {
|
|
|
253
260
|
function reindexFts() {
|
|
254
261
|
db.exec('DELETE FROM traces_fts_index');
|
|
255
262
|
const rows = db.prepare('SELECT id, title, content, summary FROM traces').all();
|
|
256
|
-
for (const row of rows) syncFts({ id: row.id, title: row.title, content: row.content, summary: row.summary });
|
|
263
|
+
for (const row of rows) syncFts({ id: row.id, title: row.title, content: row.content, summary: row.summary }, { replace: false });
|
|
264
|
+
db.prepare(`
|
|
265
|
+
INSERT INTO meta(key, value, updated_at) VALUES (?, ?, ?)
|
|
266
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
|
|
267
|
+
`).run('fts_index_version', String(FTS_INDEX_VERSION), nowIso());
|
|
257
268
|
return rows.length;
|
|
258
269
|
}
|
|
259
270
|
|
package/src/store/schema.sql
CHANGED
|
@@ -40,6 +40,7 @@ CREATE TABLE IF NOT EXISTS traces (
|
|
|
40
40
|
summary TEXT,
|
|
41
41
|
keywords_json TEXT NOT NULL DEFAULT '[]',
|
|
42
42
|
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
43
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
43
44
|
event_id TEXT NOT NULL REFERENCES events(id),
|
|
44
45
|
session_id TEXT,
|
|
45
46
|
turn_id TEXT,
|
package/src/tools/definitions.js
CHANGED
|
@@ -118,15 +118,23 @@ export function createToolDefinitions(service) {
|
|
|
118
118
|
isConcurrencySafe: () => false,
|
|
119
119
|
async execute(args = {}) {
|
|
120
120
|
if (args.action === 'add') {
|
|
121
|
+
if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
|
|
121
122
|
const item = service.working.add({ text: args.text, kind: args.kind, priority: args.priority, pinned: args.pinned === true, ttlMs: args.ttlMs });
|
|
122
123
|
return { text: `已加入工作记忆 ${item.id}:${item.text}` };
|
|
123
124
|
}
|
|
124
125
|
if (args.action === 'pin') {
|
|
126
|
+
if (args.id && !service.scopeWritableById('working', args.id)) return { text: '未执行:该工作项属于只读共享 scope。' };
|
|
125
127
|
const item = service.working.update(args.id, { pinned: args.pinned !== false });
|
|
126
128
|
return { text: item ? `已${item.pinned ? '置顶' : '取消置顶'} ${item.id}` : `没有 ${args.id}` };
|
|
127
129
|
}
|
|
128
|
-
if (args.action === 'remove')
|
|
129
|
-
|
|
130
|
+
if (args.action === 'remove') {
|
|
131
|
+
if (args.id && !service.scopeWritableById('working', args.id)) return { text: '未执行:该工作项属于只读共享 scope。' };
|
|
132
|
+
return { text: service.working.remove(args.id) ? `已删除 ${args.id}` : `没有 ${args.id}` };
|
|
133
|
+
}
|
|
134
|
+
if (args.action === 'purge') {
|
|
135
|
+
if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
|
|
136
|
+
return { text: `已清理 ${service.working.purgeExpired()} 条过期工作记忆` };
|
|
137
|
+
}
|
|
130
138
|
const items = service.working.list();
|
|
131
139
|
return { text: items.length ? items.map((item) => `- ${item.pinned ? '[pin] ' : ''}${item.id} (${item.priority}) ${item.text}`).join('\n') : '(工作记忆为空)' };
|
|
132
140
|
},
|
|
@@ -153,14 +161,17 @@ export function createToolDefinitions(service) {
|
|
|
153
161
|
isConcurrencySafe: () => false,
|
|
154
162
|
async execute(args = {}) {
|
|
155
163
|
if (args.action === 'add') {
|
|
164
|
+
if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
|
|
156
165
|
const goal = service.goals.add({ title: args.title, description: args.description, priority: args.priority });
|
|
157
166
|
return { text: `目标已建 ${goal.id}:${goal.title}` };
|
|
158
167
|
}
|
|
159
168
|
if (args.action === 'complete') {
|
|
169
|
+
if (args.id && !service.scopeWritableById('goal', args.id)) return { text: '未执行:该目标属于只读共享 scope。' };
|
|
160
170
|
const done = service.goals.complete(args.id);
|
|
161
171
|
return { text: done ? `目标已完成:${done.title}` : `没有这个目标:${args.id}` };
|
|
162
172
|
}
|
|
163
173
|
if (args.action === 'update') {
|
|
174
|
+
if (args.id && !service.scopeWritableById('goal', args.id)) return { text: '未执行:该目标属于只读共享 scope。' };
|
|
164
175
|
const goal = service.goals.update(args.id, { title: args.title, description: args.description, priority: args.priority, progress: args.progress, status: args.status });
|
|
165
176
|
return { text: goal ? `已更新 ${goal.id}:${goal.status} ${Math.round(goal.progress * 100)}%` : `没有 ${args.id}` };
|
|
166
177
|
}
|
|
@@ -190,6 +201,7 @@ export function createToolDefinitions(service) {
|
|
|
190
201
|
return { text: result.revived ? `已复活 ${args.id}(下一次检索可命中)` : `没有这条记忆:${args.id}` };
|
|
191
202
|
}
|
|
192
203
|
if (args.action === 'compress') {
|
|
204
|
+
if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
|
|
193
205
|
const result = service.compression.compress({ limit: args.limit });
|
|
194
206
|
return { text: `压缩归档 ${result.archived} 条。${result.items.map((item) => `\n- ${item.id} coverage ${item.coverage.toFixed(2)}`).join('')}` };
|
|
195
207
|
}
|
|
@@ -219,6 +231,7 @@ export function createToolDefinitions(service) {
|
|
|
219
231
|
isConcurrencySafe: () => false,
|
|
220
232
|
async execute(args = {}) {
|
|
221
233
|
if (args.action === 'diary-write') {
|
|
234
|
+
if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
|
|
222
235
|
const item = service.life.diaryWrite({ text: args.text, mood: args.mood });
|
|
223
236
|
return { text: `日记已写:${item.id}\n${item.text}` };
|
|
224
237
|
}
|
|
@@ -230,6 +243,7 @@ export function createToolDefinitions(service) {
|
|
|
230
243
|
return { text: service.life.diaryForget(args.id) ? `已删日记 ${args.id}` : `没有这条日记:${args.id}` };
|
|
231
244
|
}
|
|
232
245
|
if (args.action === 'dream') {
|
|
246
|
+
if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
|
|
233
247
|
const dream = await service.life.dreamGenerate();
|
|
234
248
|
return { text: `梦 ${dream.id}(来源 ${dream.sourceCount} 条,${dream.model}):\n${dream.text}` };
|
|
235
249
|
}
|
|
@@ -237,6 +251,7 @@ export function createToolDefinitions(service) {
|
|
|
237
251
|
const list = service.life.sleepRuns({ limit: args.limit });
|
|
238
252
|
return { text: list.length ? list.map((item) => `- ${item.id} · ${item.at}:${JSON.stringify(item.report)}`).join('\n') : '(还没有睡眠记录)' };
|
|
239
253
|
}
|
|
254
|
+
if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
|
|
240
255
|
const report = await service.life.sleep({ windowDays: args.windowDays ?? 1 });
|
|
241
256
|
return { text: `睡眠整理完成:清理工作记忆 ${report.purgedWorking} 条 · 反思 ${report.reflected} · 阶段 ${report.stage ?? '—'} · 情感对象 ${report.affectTargets}` };
|
|
242
257
|
},
|
|
@@ -265,6 +280,7 @@ export function createToolDefinitions(service) {
|
|
|
265
280
|
return { text: result.emotions.length ? result.emotions.map((item) => `${item.name}:${item.score}`).join(' / ') : '没有命中情绪词。' };
|
|
266
281
|
}
|
|
267
282
|
if (args.action === 'record') {
|
|
283
|
+
if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
|
|
268
284
|
const result = service.emotion.record({ text: args.text, target: args.target, traceId: args.traceId });
|
|
269
285
|
return { text: result.event ? `已记情绪事件 ${result.event.id}:${result.emotions.map((item) => item.name).join('、')} → ${result.event.target}(${result.target.attitude})` : '没有命中情绪词。' };
|
|
270
286
|
}
|
|
@@ -352,6 +368,7 @@ export function createToolDefinitions(service) {
|
|
|
352
368
|
if (args.action === 'sync') {
|
|
353
369
|
const dir = args.dir;
|
|
354
370
|
if (!dir) return { text: '同步 Markdown 需要 dir。' };
|
|
371
|
+
if (args.dryRun !== true && !service.scopeWritable(args.scope ?? service.settings().scope)) return { text: '未执行:该 scope 只读共享。' };
|
|
355
372
|
const result = service.syncMarkdown(dir, { dryRun: args.dryRun === true, scopeId: args.scope });
|
|
356
373
|
return { text: `Markdown 同步${result.dryRun ? '(dry-run)' : ''}:更新 ${result.updated},新建 ${result.created},文件 ${result.files}` };
|
|
357
374
|
}
|
|
@@ -497,6 +514,7 @@ export function createToolDefinitions(service) {
|
|
|
497
514
|
if (args.action === 'add') {
|
|
498
515
|
if (!args.title) return { text: '准则需要 title。' };
|
|
499
516
|
if (!args.body) return { text: '准则需要 body。' };
|
|
517
|
+
if (!service.scopeWritable(args.scope ?? service.settings().scope)) return { text: '未执行:该 scope 只读共享。' };
|
|
500
518
|
const rule = service.playbook.add({ title: args.title, body: args.body, tags: args.tags, validTo: args.validTo ?? null, scopeId: args.scope });
|
|
501
519
|
return { text: `已加准则 ${rule.id}:${rule.title}${rule.validTo ? `(到期 ${rule.validTo})` : ''}` };
|
|
502
520
|
}
|
|
@@ -506,20 +524,24 @@ export function createToolDefinitions(service) {
|
|
|
506
524
|
}
|
|
507
525
|
if (args.action === 'vote') {
|
|
508
526
|
if (!args.id) return { text: '投票需要 id。' };
|
|
527
|
+
if (!service.scopeWritableById('playbook', args.id)) return { text: '未执行:该准则属于只读共享 scope。' };
|
|
509
528
|
const rule = service.playbook.vote({ id: args.id, signal: args.signal ?? 'helpful' });
|
|
510
529
|
return { text: rule ? `已记 ${rule.id}:helpful=${rule.helpful} harmful=${rule.harmful}` : `没有准则 ${args.id}` };
|
|
511
530
|
}
|
|
512
531
|
if (args.action === 'retire') {
|
|
513
532
|
if (!args.id) return { text: '停用需要 id。' };
|
|
533
|
+
if (!service.scopeWritableById('playbook', args.id)) return { text: '未执行:该准则属于只读共享 scope。' };
|
|
514
534
|
const rule = service.playbook.retire(args.id);
|
|
515
535
|
return { text: rule ? `已停用 ${rule.id}` : `没有准则 ${args.id}` };
|
|
516
536
|
}
|
|
517
537
|
if (args.action === 'suggest') {
|
|
538
|
+
if (!service.scopeWritable(args.scope ?? service.settings().scope)) return { text: '未执行:该 scope 只读共享。' };
|
|
518
539
|
const result = service.playbook.suggest({ scopeId: args.scope, limit: args.limit ?? 5 });
|
|
519
540
|
return { text: result.count ? result.candidates.map((rule) => `- [candidate] ${rule.id} · ${rule.title}:${rule.body}`).join('\n') : '(没有新的准则候选)' };
|
|
520
541
|
}
|
|
521
542
|
if (args.action === 'approve') {
|
|
522
543
|
if (!args.id) return { text: '确认需要 id。' };
|
|
544
|
+
if (!service.scopeWritableById('playbook', args.id)) return { text: '未执行:该准则属于只读共享 scope。' };
|
|
523
545
|
const rule = service.playbook.approve(args.id);
|
|
524
546
|
return { text: rule ? `已确认 ${rule.id},下一轮生效` : `没有准则 ${args.id}` };
|
|
525
547
|
}
|
|
@@ -614,6 +636,23 @@ export function createToolDefinitions(service) {
|
|
|
614
636
|
required: ['subject', 'predicate'],
|
|
615
637
|
},
|
|
616
638
|
},
|
|
639
|
+
attachments: {
|
|
640
|
+
type: 'array',
|
|
641
|
+
description: 'Reference-only attachment metadata; no bytes are fetched or stored.',
|
|
642
|
+
items: {
|
|
643
|
+
type: 'object',
|
|
644
|
+
additionalProperties: false,
|
|
645
|
+
properties: {
|
|
646
|
+
kind: { type: 'string', enum: ['image', 'audio', 'video', 'file'] },
|
|
647
|
+
uri: { type: 'string' },
|
|
648
|
+
name: { type: 'string' },
|
|
649
|
+
mime: { type: 'string' },
|
|
650
|
+
bytes: { type: 'number' },
|
|
651
|
+
sha256: { type: 'string' },
|
|
652
|
+
},
|
|
653
|
+
required: ['kind', 'uri'],
|
|
654
|
+
},
|
|
655
|
+
},
|
|
617
656
|
},
|
|
618
657
|
},
|
|
619
658
|
output: TEXT_OUTPUT,
|
|
@@ -629,10 +668,25 @@ export function createToolDefinitions(service) {
|
|
|
629
668
|
operationId: args.operationId,
|
|
630
669
|
scopeId: args.scope,
|
|
631
670
|
facts: args.facts,
|
|
671
|
+
attachments: args.attachments,
|
|
632
672
|
});
|
|
633
|
-
if (result.skipped)
|
|
673
|
+
if (result.skipped) {
|
|
674
|
+
const hint = {
|
|
675
|
+
'low-compression': '内容信息密度太低。若确实是用户要求记住的,请传 explicit: true,或提高 importance 后再试。',
|
|
676
|
+
'too-short': '内容太短。请写成一条完整、可检索的记忆。',
|
|
677
|
+
'stop-phrase': '这是停用短语。若确实要记,请换一条有具体内容的表达。',
|
|
678
|
+
duplicate: '已有相同内容。可以继续使用已有 trace,不必重复写。',
|
|
679
|
+
idempotent: '同一个 operationId 已处理过。可以继续使用已有 trace。',
|
|
680
|
+
'scope-read-only': '这个 scope 是只读共享的;请换到可写 scope,或让宿主解除只读。',
|
|
681
|
+
}[result.reason];
|
|
682
|
+
const factsNote = result.facts?.length ? `\n已应用 ${result.facts.length} 条事实。` : '';
|
|
683
|
+
const edgeNote = result.edgeCandidates?.length ? `\n候选边:${result.edgeCandidates.length} 条(待审核)。` : '';
|
|
684
|
+
return {
|
|
685
|
+
text: `未写入:${result.reason}${result.trace ? `(已有 ${result.trace.id})` : ''}${factsNote}${edgeNote}${hint ? `\n提示:${hint}` : ''}`,
|
|
686
|
+
};
|
|
687
|
+
}
|
|
634
688
|
return {
|
|
635
|
-
text: `已写入 ${result.trace.id}(${result.trace.kind})\n事件:${result.event.id}\n压缩值:${result.compression.toFixed(3)}${result.facts?.length ? `\n事实:${result.facts.length} 条` : ''}`,
|
|
689
|
+
text: `已写入 ${result.trace.id}(${result.trace.kind})\n事件:${result.event.id}\n压缩值:${result.compression.toFixed(3)}${result.facts?.length ? `\n事实:${result.facts.length} 条` : ''}${result.edgeCandidates?.length ? `\n候选边:${result.edgeCandidates.length} 条(待审核)` : ''}${result.trace.metadata?.attachments?.length ? `\n附件:${result.trace.metadata.attachments.length} 个(仅元数据)` : ''}`,
|
|
636
690
|
};
|
|
637
691
|
},
|
|
638
692
|
presentCall: () => ({ card: 'generic', title: '写入记忆', kind: 'other' }),
|
|
@@ -676,8 +730,11 @@ export function createToolDefinitions(service) {
|
|
|
676
730
|
output: TEXT_OUTPUT,
|
|
677
731
|
isConcurrencySafe: () => false,
|
|
678
732
|
async execute(args = {}) {
|
|
733
|
+
if (!service.scopeWritableById('trace', args.id)) return { text: '未执行:该记忆属于只读共享 scope。' };
|
|
679
734
|
const result = service.forget({ traceId: args.id, mode: args.mode });
|
|
680
|
-
if (!result.removed)
|
|
735
|
+
if (!result.removed) {
|
|
736
|
+
return { text: result.reason === 'scope-read-only' ? '未执行:该记忆属于只读共享 scope。' : `没有这条记忆:${args.id}` };
|
|
737
|
+
}
|
|
681
738
|
return { text: args.mode === 'delete' ? `已删除 ${args.id},并级联清理派生数据。` : `已归档 ${args.id},可用关键词唤醒。` };
|
|
682
739
|
},
|
|
683
740
|
presentCall: () => ({ card: 'generic', title: '遗忘/删除记忆', kind: 'other' }),
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Future external ANN client. It is intentionally not wired into the search
|
|
3
|
+
* path yet: a local index service with its own lifecycle must exist first.
|
|
4
|
+
*/
|
|
5
|
+
export function createExternalAnnClient({ endpoint, timeoutMs = 800, headers = {}, fetchImpl = fetch } = {}) {
|
|
6
|
+
const url = String(endpoint ?? '').trim();
|
|
7
|
+
if (!/^https?:\/\//i.test(url)) throw new Error('external ANN endpoint must be http(s)');
|
|
8
|
+
const timeout = Math.max(100, Math.min(10000, Number(timeoutMs) || 800));
|
|
9
|
+
|
|
10
|
+
async function search(vector, k = 10, { signal } = {}) {
|
|
11
|
+
if (!Array.isArray(vector) || !vector.length) throw new Error('external ANN query vector is required');
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
14
|
+
const onAbort = () => controller.abort();
|
|
15
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
16
|
+
try {
|
|
17
|
+
const response = await fetchImpl(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
20
|
+
body: JSON.stringify({ vector, k: Math.max(1, Math.min(200, Number(k) || 10)) }),
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) throw new Error(`external ANN returned ${response.status}`);
|
|
24
|
+
const body = await response.json();
|
|
25
|
+
if (!Array.isArray(body?.items)) throw new Error('external ANN response needs items[]');
|
|
26
|
+
return body.items
|
|
27
|
+
.filter((item) => typeof item?.id === 'string' && Number.isFinite(item?.score))
|
|
28
|
+
.map((item) => ({ id: item.id, score: item.score }))
|
|
29
|
+
.slice(0, Math.max(1, Math.min(200, Number(k) || 10)));
|
|
30
|
+
} finally {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { endpoint: url, search };
|
|
37
|
+
}
|