cozo-memory 1.2.6 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -36
- package/dist/benchmark.js +410 -132
- package/dist/db-service.test.js +313 -0
- package/dist/export-import-service.js +9 -5
- package/dist/index.js +825 -10
- package/dist/logger.test.js +75 -0
- package/dist/memory-service.test.js +222 -0
- package/dist/timestamp-utils.test.js +68 -0
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ const emotional_salience_1 = require("./emotional-salience");
|
|
|
24
24
|
const proactive_suggestions_1 = require("./proactive-suggestions");
|
|
25
25
|
const spreading_activation_1 = require("./spreading-activation");
|
|
26
26
|
const temporal_conflict_resolution_1 = require("./temporal-conflict-resolution");
|
|
27
|
+
const explainable_retrieval_1 = require("./explainable-retrieval");
|
|
27
28
|
exports.DB_PATH = path_1.default.resolve(__dirname, "..", "memory_db.cozo");
|
|
28
29
|
const DB_ENGINE = process.env.DB_ENGINE || "sqlite"; // "sqlite" or "rocksdb"
|
|
29
30
|
exports.USER_ENTITY_ID = "global_user_profile";
|
|
@@ -48,6 +49,7 @@ class MemoryServer {
|
|
|
48
49
|
logicalEdgesService = null;
|
|
49
50
|
hierarchicalMemoryService = null;
|
|
50
51
|
queryAwareTraversal = null;
|
|
52
|
+
explainableService = null;
|
|
51
53
|
// Metrics tracking
|
|
52
54
|
metrics = {
|
|
53
55
|
operations: {
|
|
@@ -245,6 +247,12 @@ class MemoryServer {
|
|
|
245
247
|
}
|
|
246
248
|
return this.queryAwareTraversal;
|
|
247
249
|
}
|
|
250
|
+
getExplainableService() {
|
|
251
|
+
if (!this.explainableService) {
|
|
252
|
+
this.explainableService = new explainable_retrieval_1.ExplainableRetrievalService(this.db, this.embeddingService);
|
|
253
|
+
}
|
|
254
|
+
return this.explainableService;
|
|
255
|
+
}
|
|
248
256
|
async janitorCleanup(args) {
|
|
249
257
|
await this.initPromise;
|
|
250
258
|
console.error(`[Janitor] Starting cleanup (auto-compressing sessions first)...`);
|
|
@@ -3158,13 +3166,13 @@ Format MUST start with "ExecutiveSummary: " followed by the consolidated content
|
|
|
3158
3166
|
async exportMemory(args) {
|
|
3159
3167
|
await this.initPromise;
|
|
3160
3168
|
const dbService = { run: (query, params) => this.db.run(query, params) };
|
|
3161
|
-
const exportService = new export_import_service_1.ExportImportService(dbService);
|
|
3169
|
+
const exportService = new export_import_service_1.ExportImportService(dbService, this.embeddingService.getDimensions());
|
|
3162
3170
|
return exportService.exportMemory(args);
|
|
3163
3171
|
}
|
|
3164
3172
|
async importMemory(args) {
|
|
3165
3173
|
await this.initPromise;
|
|
3166
3174
|
const dbService = { run: (query, params) => this.db.run(query, params) };
|
|
3167
|
-
const exportService = new export_import_service_1.ExportImportService(dbService);
|
|
3175
|
+
const exportService = new export_import_service_1.ExportImportService(dbService, this.embeddingService.getDimensions());
|
|
3168
3176
|
return exportService.importMemory(args.data, {
|
|
3169
3177
|
sourceFormat: args.sourceFormat,
|
|
3170
3178
|
mergeStrategy: args.mergeStrategy,
|
|
@@ -3229,6 +3237,609 @@ Format MUST start with "ExecutiveSummary: " followed by the consolidated content
|
|
|
3229
3237
|
};
|
|
3230
3238
|
}
|
|
3231
3239
|
}
|
|
3240
|
+
// ===========================================================================
|
|
3241
|
+
// Agent Features (query/mutation convenience actions)
|
|
3242
|
+
// ===========================================================================
|
|
3243
|
+
/** Feature 1: List entities with filtering, sorting and pagination. */
|
|
3244
|
+
async listEntities(args) {
|
|
3245
|
+
await this.initPromise;
|
|
3246
|
+
try {
|
|
3247
|
+
const limit = Math.min(Math.max(args.limit ?? 20, 1), 1000);
|
|
3248
|
+
const offset = Math.max(args.offset ?? 0, 0);
|
|
3249
|
+
const sortBy = args.sort_by ?? "created_at";
|
|
3250
|
+
const sortOrder = args.sort_order ?? "desc";
|
|
3251
|
+
const res = await this.db.run(`?[id, name, type, metadata, ts] := *entity{id, name, type, metadata, created_at, @ "NOW"}, ts = to_int(created_at)`);
|
|
3252
|
+
let rows = res.rows.map((r) => ({
|
|
3253
|
+
id: r[0],
|
|
3254
|
+
name: r[1],
|
|
3255
|
+
type: r[2],
|
|
3256
|
+
metadata: (r[3] || {}),
|
|
3257
|
+
created_at_us: r[4],
|
|
3258
|
+
}));
|
|
3259
|
+
// Filters (applied in JS for robustness across value types)
|
|
3260
|
+
if (args.type)
|
|
3261
|
+
rows = rows.filter((e) => e.type === args.type);
|
|
3262
|
+
if (args.types && args.types.length > 0) {
|
|
3263
|
+
const set = new Set(args.types);
|
|
3264
|
+
rows = rows.filter((e) => set.has(e.type));
|
|
3265
|
+
}
|
|
3266
|
+
if (args.name_contains) {
|
|
3267
|
+
const needle = args.name_contains.toLowerCase();
|
|
3268
|
+
rows = rows.filter((e) => String(e.name).toLowerCase().includes(needle));
|
|
3269
|
+
}
|
|
3270
|
+
if (args.tags && args.tags.length > 0) {
|
|
3271
|
+
const want = args.tags;
|
|
3272
|
+
rows = rows.filter((e) => {
|
|
3273
|
+
const t = Array.isArray(e.metadata?.tags) ? e.metadata.tags : [];
|
|
3274
|
+
return want.every((tag) => t.includes(tag));
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
const total = rows.length;
|
|
3278
|
+
rows.sort((a, b) => {
|
|
3279
|
+
let cmp = 0;
|
|
3280
|
+
if (sortBy === "name")
|
|
3281
|
+
cmp = String(a.name).localeCompare(String(b.name));
|
|
3282
|
+
else
|
|
3283
|
+
cmp = a.created_at_us - b.created_at_us; // created_at / updated_at both track the validity timestamp
|
|
3284
|
+
return sortOrder === "asc" ? cmp : -cmp;
|
|
3285
|
+
});
|
|
3286
|
+
const page = rows.slice(offset, offset + limit);
|
|
3287
|
+
// Count maps (one query each, regardless of page size)
|
|
3288
|
+
const obsCount = new Map();
|
|
3289
|
+
const obsRes = await this.db.run('?[eid, count(oid)] := *observation{id: oid, entity_id: eid, @ "NOW"}');
|
|
3290
|
+
obsRes.rows.forEach((r) => obsCount.set(r[0], Number(r[1])));
|
|
3291
|
+
const relCount = new Map();
|
|
3292
|
+
const relOutRes = await this.db.run('?[eid, count(t)] := *relationship{from_id: eid, to_id: t, @ "NOW"}');
|
|
3293
|
+
relOutRes.rows.forEach((r) => relCount.set(r[0], Number(r[1])));
|
|
3294
|
+
const relInRes = await this.db.run('?[eid, count(f)] := *relationship{from_id: f, to_id: eid, @ "NOW"}');
|
|
3295
|
+
relInRes.rows.forEach((r) => relCount.set(r[0], (relCount.get(r[0]) || 0) + Number(r[1])));
|
|
3296
|
+
const entities = page.map((e) => ({
|
|
3297
|
+
id: e.id,
|
|
3298
|
+
name: e.name,
|
|
3299
|
+
type: e.type,
|
|
3300
|
+
metadata: e.metadata,
|
|
3301
|
+
tags: Array.isArray(e.metadata?.tags) ? e.metadata.tags : [],
|
|
3302
|
+
created_at: new Date(Math.floor(e.created_at_us / 1000)).toISOString(),
|
|
3303
|
+
observation_count: obsCount.get(e.id) || 0,
|
|
3304
|
+
relation_count: relCount.get(e.id) || 0,
|
|
3305
|
+
}));
|
|
3306
|
+
return { entities, total, offset, limit };
|
|
3307
|
+
}
|
|
3308
|
+
catch (error) {
|
|
3309
|
+
return { error: "Failed to list entities", message: error.message };
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
/** Feature 2: Aggregate statistics overview. */
|
|
3313
|
+
async getStats() {
|
|
3314
|
+
await this.initPromise;
|
|
3315
|
+
try {
|
|
3316
|
+
const [eRes, oRes, rRes] = await Promise.all([
|
|
3317
|
+
this.db.run('?[id, type, ts] := *entity{id, type, created_at, @ "NOW"}, ts = to_int(created_at)'),
|
|
3318
|
+
this.db.run('?[count(id)] := *observation{id, @ "NOW"}'),
|
|
3319
|
+
this.db.run('?[count(f)] := *relationship{from_id: f, to_id, @ "NOW"}'),
|
|
3320
|
+
]);
|
|
3321
|
+
const entities = eRes.rows.map((r) => ({ type: r[1], ts: r[2] }));
|
|
3322
|
+
const total_entities = entities.length;
|
|
3323
|
+
const total_observations = Number(oRes.rows[0]?.[0] || 0);
|
|
3324
|
+
const total_relations = Number(rRes.rows[0]?.[0] || 0);
|
|
3325
|
+
const typeMap = new Map();
|
|
3326
|
+
entities.forEach((e) => typeMap.set(e.type, (typeMap.get(e.type) || 0) + 1));
|
|
3327
|
+
const by_type = [...typeMap.entries()]
|
|
3328
|
+
.map(([type, count]) => ({ type, count }))
|
|
3329
|
+
.sort((a, b) => b.count - a.count);
|
|
3330
|
+
const tss = entities.map((e) => e.ts).filter((n) => typeof n === "number");
|
|
3331
|
+
const nowUs = Date.now() * 1000;
|
|
3332
|
+
const dayUs = 24 * 3600 * 1000 * 1000;
|
|
3333
|
+
const oldest = tss.length ? Math.min(...tss) : null;
|
|
3334
|
+
const newest = tss.length ? Math.max(...tss) : null;
|
|
3335
|
+
return {
|
|
3336
|
+
overview: { total_entities, total_observations, total_relations },
|
|
3337
|
+
by_type,
|
|
3338
|
+
timeline: {
|
|
3339
|
+
oldest_entity: oldest !== null ? new Date(Math.floor(oldest / 1000)).toISOString() : null,
|
|
3340
|
+
newest_entity: newest !== null ? new Date(Math.floor(newest / 1000)).toISOString() : null,
|
|
3341
|
+
entities_last_24h: tss.filter((t) => t >= nowUs - dayUs).length,
|
|
3342
|
+
entities_last_7d: tss.filter((t) => t >= nowUs - 7 * dayUs).length,
|
|
3343
|
+
},
|
|
3344
|
+
activity: {
|
|
3345
|
+
total_operations: this.metrics.performance.total_operations,
|
|
3346
|
+
top_types: by_type.slice(0, 5),
|
|
3347
|
+
},
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
catch (error) {
|
|
3351
|
+
return { error: "Failed to compute stats", message: error.message };
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
/** Feature 6: Update an observation's text and/or metadata (validity-preserving). */
|
|
3355
|
+
async updateObservation(args) {
|
|
3356
|
+
await this.initPromise;
|
|
3357
|
+
const startTime = Date.now();
|
|
3358
|
+
try {
|
|
3359
|
+
const existing = await this.db.run(`?[oid, eid, sid, tid, txt, emb, meta] := *observation{id: oid, entity_id: eid, session_id: sid, task_id: tid, text: txt, embedding: emb, metadata: meta, @ "NOW"}, oid = $id`, { id: args.observation_id });
|
|
3360
|
+
if (existing.rows.length === 0)
|
|
3361
|
+
return { error: "Observation not found" };
|
|
3362
|
+
const row = existing.rows[0];
|
|
3363
|
+
const prevText = row[4];
|
|
3364
|
+
const newText = args.text !== undefined && args.text !== null ? args.text : prevText;
|
|
3365
|
+
let newMeta;
|
|
3366
|
+
if (args.metadata !== undefined) {
|
|
3367
|
+
newMeta = args.merge_metadata ? { ...(row[6] || {}), ...args.metadata } : args.metadata;
|
|
3368
|
+
}
|
|
3369
|
+
else {
|
|
3370
|
+
newMeta = row[6] || {};
|
|
3371
|
+
}
|
|
3372
|
+
let embedding = row[5];
|
|
3373
|
+
if (args.text !== undefined && args.text !== prevText) {
|
|
3374
|
+
embedding = await this.embeddingService.embed(newText);
|
|
3375
|
+
}
|
|
3376
|
+
const now = Date.now() * 1000;
|
|
3377
|
+
await this.db.run(`
|
|
3378
|
+
?[id, created_at, entity_id, session_id, task_id, text, embedding, metadata] <- [[$id, $v, $entity_id, $session_id, $task_id, $text, $embedding, $metadata]]
|
|
3379
|
+
:put observation {id, created_at => entity_id, session_id, task_id, text, embedding, metadata}
|
|
3380
|
+
`, {
|
|
3381
|
+
id: row[0],
|
|
3382
|
+
v: [now, true],
|
|
3383
|
+
entity_id: row[1],
|
|
3384
|
+
session_id: row[2],
|
|
3385
|
+
task_id: row[3],
|
|
3386
|
+
text: newText,
|
|
3387
|
+
embedding,
|
|
3388
|
+
metadata: newMeta,
|
|
3389
|
+
});
|
|
3390
|
+
this.trackOperation('update_observation', startTime);
|
|
3391
|
+
return {
|
|
3392
|
+
status: "updated",
|
|
3393
|
+
observation: {
|
|
3394
|
+
id: row[0],
|
|
3395
|
+
entity_id: row[1],
|
|
3396
|
+
text: newText,
|
|
3397
|
+
metadata: newMeta,
|
|
3398
|
+
updated_at: new Date(Math.floor(now / 1000)).toISOString(),
|
|
3399
|
+
previous_text: prevText,
|
|
3400
|
+
},
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
catch (error) {
|
|
3404
|
+
this.trackError('update_observation');
|
|
3405
|
+
return { error: "Update failed", message: error.message };
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
/** Feature 4: Aggregate full entity detail (observations, relations, optional community/timeline). */
|
|
3409
|
+
async getEntityDetail(args) {
|
|
3410
|
+
await this.initPromise;
|
|
3411
|
+
try {
|
|
3412
|
+
const incObs = args.include_observations !== false;
|
|
3413
|
+
const incRel = args.include_relations !== false;
|
|
3414
|
+
const incCom = args.include_community === true;
|
|
3415
|
+
const incTl = args.include_timeline === true;
|
|
3416
|
+
const entRes = await this.db.run(`?[name, type, metadata, ts] := *entity{id: $id, name, type, metadata, created_at, @ "NOW"}, ts = to_int(created_at)`, { id: args.entity_id });
|
|
3417
|
+
if (entRes.rows.length === 0)
|
|
3418
|
+
return { error: "Entity not found" };
|
|
3419
|
+
const e = entRes.rows[0];
|
|
3420
|
+
const metadata = (e[2] || {});
|
|
3421
|
+
const created_at_us = e[3];
|
|
3422
|
+
const result = {
|
|
3423
|
+
entity: {
|
|
3424
|
+
id: args.entity_id,
|
|
3425
|
+
name: e[0],
|
|
3426
|
+
type: e[1],
|
|
3427
|
+
metadata,
|
|
3428
|
+
tags: Array.isArray(metadata.tags) ? metadata.tags : [],
|
|
3429
|
+
created_at: new Date(Math.floor(created_at_us / 1000)).toISOString(),
|
|
3430
|
+
},
|
|
3431
|
+
};
|
|
3432
|
+
if (incObs) {
|
|
3433
|
+
const obsRes = await this.db.run(`?[oid, text, metadata, ts] := *observation{id: oid, entity_id: $id, text, metadata, created_at, @ "NOW"}, ts = to_int(created_at) :order ts`, { id: args.entity_id });
|
|
3434
|
+
result.observations = obsRes.rows.map((r) => ({
|
|
3435
|
+
id: r[0],
|
|
3436
|
+
text: r[1],
|
|
3437
|
+
metadata: r[2],
|
|
3438
|
+
created_at: new Date(Math.floor(r[3] / 1000)).toISOString(),
|
|
3439
|
+
}));
|
|
3440
|
+
}
|
|
3441
|
+
if (incRel) {
|
|
3442
|
+
const outRes = await this.db.run(`?[tid, tname, ttype, rtype, strength, metadata] := *relationship{from_id: $id, to_id: tid, relation_type: rtype, strength, metadata, @ "NOW"}, *entity{id: tid, name: tname, type: ttype, @ "NOW"}`, { id: args.entity_id });
|
|
3443
|
+
const inRes = await this.db.run(`?[sid, sname, stype, rtype, strength, metadata] := *relationship{from_id: sid, to_id: $id, relation_type: rtype, strength, metadata, @ "NOW"}, *entity{id: sid, name: sname, type: stype, @ "NOW"}`, { id: args.entity_id });
|
|
3444
|
+
result.relations = {
|
|
3445
|
+
outgoing: outRes.rows.map((r) => ({
|
|
3446
|
+
target_id: r[0],
|
|
3447
|
+
target_name: r[1],
|
|
3448
|
+
target_type: r[2],
|
|
3449
|
+
relation_type: r[3],
|
|
3450
|
+
strength: r[4],
|
|
3451
|
+
metadata: r[5],
|
|
3452
|
+
})),
|
|
3453
|
+
incoming: inRes.rows.map((r) => ({
|
|
3454
|
+
source_id: r[0],
|
|
3455
|
+
source_name: r[1],
|
|
3456
|
+
source_type: r[2],
|
|
3457
|
+
relation_type: r[3],
|
|
3458
|
+
strength: r[4],
|
|
3459
|
+
metadata: r[5],
|
|
3460
|
+
})),
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
if (incCom) {
|
|
3464
|
+
try {
|
|
3465
|
+
const comRes = await this.db.run(`?[cid] := *entity_community{entity_id: $id, community_id: cid}`, { id: args.entity_id });
|
|
3466
|
+
if (comRes.rows.length > 0) {
|
|
3467
|
+
const cid = comRes.rows[0][0];
|
|
3468
|
+
const memRes = await this.db.run(`?[mid, name, type] := *entity_community{entity_id: mid, community_id: $cid}, *entity{id: mid, name, type, @ "NOW"}`, { cid });
|
|
3469
|
+
result.community = {
|
|
3470
|
+
id: cid,
|
|
3471
|
+
members: memRes.rows.map((r) => ({ id: r[0], name: r[1], type: r[2] })),
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
else {
|
|
3475
|
+
result.community = null;
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
catch {
|
|
3479
|
+
result.community = null;
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
if (incTl) {
|
|
3483
|
+
const timeline = [
|
|
3484
|
+
{
|
|
3485
|
+
timestamp: new Date(Math.floor(created_at_us / 1000)).toISOString(),
|
|
3486
|
+
action: "created",
|
|
3487
|
+
detail: `Entity '${e[0]}' created`,
|
|
3488
|
+
},
|
|
3489
|
+
];
|
|
3490
|
+
const obsT = await this.db.run(`?[text, ts, asserted] := *observation{entity_id: $id, text, created_at}, ts = to_int(created_at), asserted = to_bool(created_at)`, { id: args.entity_id });
|
|
3491
|
+
obsT.rows.forEach((r) => {
|
|
3492
|
+
if (r[2]) {
|
|
3493
|
+
timeline.push({
|
|
3494
|
+
timestamp: new Date(Math.floor(r[1] / 1000)).toISOString(),
|
|
3495
|
+
action: "observation_added",
|
|
3496
|
+
detail: String(r[0]).slice(0, 80),
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
});
|
|
3500
|
+
const relT = await this.db.run(`?[rtype, tid, ts, asserted] := *relationship{from_id: $id, to_id: tid, relation_type: rtype, created_at}, ts = to_int(created_at), asserted = to_bool(created_at)`, { id: args.entity_id });
|
|
3501
|
+
relT.rows.forEach((r) => {
|
|
3502
|
+
if (r[3]) {
|
|
3503
|
+
timeline.push({
|
|
3504
|
+
timestamp: new Date(Math.floor(r[2] / 1000)).toISOString(),
|
|
3505
|
+
action: "relation_added",
|
|
3506
|
+
detail: `${r[0]} -> ${r[1]}`,
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
});
|
|
3510
|
+
timeline.sort((a, b) => (a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0));
|
|
3511
|
+
result.timeline = timeline;
|
|
3512
|
+
}
|
|
3513
|
+
return result;
|
|
3514
|
+
}
|
|
3515
|
+
catch (error) {
|
|
3516
|
+
return { error: "Failed to get entity detail", message: error.message };
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
/** Feature 3: Delete multiple entities by explicit IDs or by filter, with dry-run support. */
|
|
3520
|
+
async batchDeleteEntities(args) {
|
|
3521
|
+
await this.initPromise;
|
|
3522
|
+
try {
|
|
3523
|
+
const dryRun = args.dry_run === true;
|
|
3524
|
+
let targetIds = [];
|
|
3525
|
+
if (args.entity_ids && args.entity_ids.length > 0) {
|
|
3526
|
+
targetIds = [...args.entity_ids];
|
|
3527
|
+
}
|
|
3528
|
+
else if (args.filter) {
|
|
3529
|
+
const f = args.filter;
|
|
3530
|
+
const all = await this.db.run(`?[id, name, type, metadata, ts] := *entity{id, name, type, metadata, created_at, @ "NOW"}, ts = to_int(created_at)`);
|
|
3531
|
+
const beforeUs = f.created_before ? Date.parse(f.created_before) * 1000 : null;
|
|
3532
|
+
const afterUs = f.created_after ? Date.parse(f.created_after) * 1000 : null;
|
|
3533
|
+
targetIds = all.rows
|
|
3534
|
+
.filter((r) => {
|
|
3535
|
+
const [, name, type, meta, ts] = r;
|
|
3536
|
+
if (f.type && type !== f.type)
|
|
3537
|
+
return false;
|
|
3538
|
+
if (f.name_contains && !String(name).toLowerCase().includes(f.name_contains.toLowerCase()))
|
|
3539
|
+
return false;
|
|
3540
|
+
if (beforeUs !== null && !(ts < beforeUs))
|
|
3541
|
+
return false;
|
|
3542
|
+
if (afterUs !== null && !(ts > afterUs))
|
|
3543
|
+
return false;
|
|
3544
|
+
if (f.metadata) {
|
|
3545
|
+
for (const [k, v] of Object.entries(f.metadata)) {
|
|
3546
|
+
if (JSON.stringify((meta || {})[k]) !== JSON.stringify(v))
|
|
3547
|
+
return false;
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
if (f.tags && f.tags.length > 0) {
|
|
3551
|
+
const t = Array.isArray(meta?.tags) ? meta.tags : [];
|
|
3552
|
+
if (!f.tags.every((tag) => t.includes(tag)))
|
|
3553
|
+
return false;
|
|
3554
|
+
}
|
|
3555
|
+
return true;
|
|
3556
|
+
})
|
|
3557
|
+
.map((r) => r[0]);
|
|
3558
|
+
}
|
|
3559
|
+
else {
|
|
3560
|
+
return { error: "Either entity_ids or filter is required" };
|
|
3561
|
+
}
|
|
3562
|
+
targetIds = [...new Set(targetIds)];
|
|
3563
|
+
let deleted_observations = 0;
|
|
3564
|
+
let deleted_relations = 0;
|
|
3565
|
+
const deleted_entities = [];
|
|
3566
|
+
const errors = [];
|
|
3567
|
+
for (const id of targetIds) {
|
|
3568
|
+
const exists = await this.db.run('?[name] := *entity{id: $id, name, @ "NOW"}', { id });
|
|
3569
|
+
if (exists.rows.length === 0) {
|
|
3570
|
+
errors.push({ id, error: "Entity not found" });
|
|
3571
|
+
continue;
|
|
3572
|
+
}
|
|
3573
|
+
if (dryRun) {
|
|
3574
|
+
const obsC = await this.db.run('?[count(oid)] := *observation{id: oid, entity_id: $id, @ "NOW"}', { id });
|
|
3575
|
+
const relOutC = await this.db.run('?[count(t)] := *relationship{from_id: $id, to_id: t, @ "NOW"}', { id });
|
|
3576
|
+
const relInC = await this.db.run('?[count(f)] := *relationship{from_id: f, to_id: $id, @ "NOW"}', { id });
|
|
3577
|
+
deleted_observations += Number(obsC.rows[0]?.[0] || 0);
|
|
3578
|
+
deleted_relations += Number(relOutC.rows[0]?.[0] || 0) + Number(relInC.rows[0]?.[0] || 0);
|
|
3579
|
+
deleted_entities.push(id);
|
|
3580
|
+
}
|
|
3581
|
+
else {
|
|
3582
|
+
const res = await this.deleteEntity({ entity_id: id });
|
|
3583
|
+
if (res.error) {
|
|
3584
|
+
errors.push({ id, error: res.message || res.error });
|
|
3585
|
+
continue;
|
|
3586
|
+
}
|
|
3587
|
+
deleted_observations += Number(res.deleted?.observations || 0);
|
|
3588
|
+
deleted_relations += Number(res.deleted?.outgoing_relations || 0) + Number(res.deleted?.incoming_relations || 0);
|
|
3589
|
+
deleted_entities.push(id);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
return {
|
|
3593
|
+
status: dryRun ? "dry_run" : "deleted",
|
|
3594
|
+
deleted_count: deleted_entities.length,
|
|
3595
|
+
deleted_entities,
|
|
3596
|
+
deleted_observations,
|
|
3597
|
+
deleted_relations,
|
|
3598
|
+
...(errors.length ? { errors } : {}),
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3601
|
+
catch (error) {
|
|
3602
|
+
return { error: "Batch delete failed", message: error.message };
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
/** Feature 5: Lightweight tag management stored in metadata.tags. */
|
|
3606
|
+
async manageTags(args) {
|
|
3607
|
+
await this.initPromise;
|
|
3608
|
+
try {
|
|
3609
|
+
const op = args.operation;
|
|
3610
|
+
if (op === "search") {
|
|
3611
|
+
if (!args.search_tag)
|
|
3612
|
+
return { error: "search_tag is required for search" };
|
|
3613
|
+
const all = await this.db.run(`?[id, name, type, metadata] := *entity{id, name, type, metadata, @ "NOW"}`);
|
|
3614
|
+
const entities = all.rows
|
|
3615
|
+
.filter((r) => Array.isArray(r[3]?.tags) && r[3].tags.includes(args.search_tag))
|
|
3616
|
+
.map((r) => ({ id: r[0], name: r[1], type: r[2] }));
|
|
3617
|
+
return { tag: args.search_tag, entities, count: entities.length };
|
|
3618
|
+
}
|
|
3619
|
+
if (op === "list") {
|
|
3620
|
+
if (args.entity_id) {
|
|
3621
|
+
const res = await this.db.run(`?[metadata] := *entity{id: $id, metadata, @ "NOW"}`, { id: args.entity_id });
|
|
3622
|
+
if (res.rows.length === 0)
|
|
3623
|
+
return { error: "Entity not found" };
|
|
3624
|
+
const t = Array.isArray(res.rows[0][0]?.tags) ? res.rows[0][0].tags : [];
|
|
3625
|
+
return { entity_id: args.entity_id, tags: t };
|
|
3626
|
+
}
|
|
3627
|
+
const all = await this.db.run(`?[metadata] := *entity{metadata, @ "NOW"}`);
|
|
3628
|
+
const counts = new Map();
|
|
3629
|
+
all.rows.forEach((r) => {
|
|
3630
|
+
const t = Array.isArray(r[0]?.tags) ? r[0].tags : [];
|
|
3631
|
+
t.forEach((tag) => counts.set(tag, (counts.get(tag) || 0) + 1));
|
|
3632
|
+
});
|
|
3633
|
+
return {
|
|
3634
|
+
tags: [...counts.entries()].map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count),
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
// add / remove / set
|
|
3638
|
+
if (!args.entity_id)
|
|
3639
|
+
return { error: "entity_id is required" };
|
|
3640
|
+
const tags = args.tags || [];
|
|
3641
|
+
const res = await this.db.run(`?[metadata] := *entity{id: $id, metadata, @ "NOW"}`, { id: args.entity_id });
|
|
3642
|
+
if (res.rows.length === 0)
|
|
3643
|
+
return { error: "Entity not found" };
|
|
3644
|
+
const meta = res.rows[0][0] || {};
|
|
3645
|
+
const current = Array.isArray(meta.tags) ? meta.tags : [];
|
|
3646
|
+
let next;
|
|
3647
|
+
if (op === "add")
|
|
3648
|
+
next = [...new Set([...current, ...tags])];
|
|
3649
|
+
else if (op === "remove")
|
|
3650
|
+
next = current.filter((t) => !tags.includes(t));
|
|
3651
|
+
else if (op === "set")
|
|
3652
|
+
next = [...new Set(tags)];
|
|
3653
|
+
else
|
|
3654
|
+
return { error: `Invalid operation: ${op}` };
|
|
3655
|
+
// Replace metadata.tags fully. updateEntity uses `++` which concatenates
|
|
3656
|
+
// arrays (producing duplicates), so write metadata directly instead.
|
|
3657
|
+
const newMeta = { ...meta, tags: next };
|
|
3658
|
+
await this.db.run(`?[id, created_at, metadata] := *entity{id, created_at, @ "NOW"}, id = $id, metadata = $meta :update entity {id, created_at, metadata}`, { id: args.entity_id, meta: newMeta });
|
|
3659
|
+
return { status: "tags_updated", entity_id: args.entity_id, operation: op, tags: next };
|
|
3660
|
+
}
|
|
3661
|
+
catch (error) {
|
|
3662
|
+
return { error: "Tag operation failed", message: error.message };
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
/** Feature 7: Execute a sequence of mutation operations (transactional or best-effort). */
|
|
3666
|
+
async executeBatch(args) {
|
|
3667
|
+
await this.initPromise;
|
|
3668
|
+
try {
|
|
3669
|
+
const ops = args.operations || [];
|
|
3670
|
+
if (ops.length === 0)
|
|
3671
|
+
return { error: "No operations provided" };
|
|
3672
|
+
const transactional = args.transactional !== false; // default: true (all-or-nothing)
|
|
3673
|
+
const continueOnError = args.continue_on_error === true;
|
|
3674
|
+
const opParams = (op) => {
|
|
3675
|
+
if (op.params !== undefined)
|
|
3676
|
+
return op.params;
|
|
3677
|
+
const { action, ...rest } = op;
|
|
3678
|
+
return rest;
|
|
3679
|
+
};
|
|
3680
|
+
if (transactional) {
|
|
3681
|
+
const mapped = ops.map((op) => ({ action: op.action, params: opParams(op) }));
|
|
3682
|
+
const txn = await this.runTransaction({ operations: mapped });
|
|
3683
|
+
if (txn.error) {
|
|
3684
|
+
return {
|
|
3685
|
+
status: "failed",
|
|
3686
|
+
results: ops.map((op, i) => ({ index: i, action: op.action, status: "error", error: txn.error })),
|
|
3687
|
+
summary: { total: ops.length, succeeded: 0, failed: ops.length },
|
|
3688
|
+
error: txn.error,
|
|
3689
|
+
message: txn.message,
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
return {
|
|
3693
|
+
status: "completed",
|
|
3694
|
+
results: ops.map((op, i) => ({ index: i, action: op.action, status: "success" })),
|
|
3695
|
+
summary: { total: ops.length, succeeded: ops.length, failed: 0 },
|
|
3696
|
+
transaction: txn,
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
const results = [];
|
|
3700
|
+
let succeeded = 0;
|
|
3701
|
+
let failed = 0;
|
|
3702
|
+
for (let i = 0; i < ops.length; i++) {
|
|
3703
|
+
const op = ops[i];
|
|
3704
|
+
const params = opParams(op);
|
|
3705
|
+
try {
|
|
3706
|
+
let r;
|
|
3707
|
+
if (op.action === "create_entity")
|
|
3708
|
+
r = await this.createEntity(params);
|
|
3709
|
+
else if (op.action === "add_observation")
|
|
3710
|
+
r = await this.addObservation(params);
|
|
3711
|
+
else if (op.action === "create_relation")
|
|
3712
|
+
r = await this.createRelation(params);
|
|
3713
|
+
else if (op.action === "delete_entity")
|
|
3714
|
+
r = await this.deleteEntity({ entity_id: params.entity_id });
|
|
3715
|
+
else if (op.action === "update_observation")
|
|
3716
|
+
r = await this.updateObservation(params);
|
|
3717
|
+
else
|
|
3718
|
+
r = { error: `Unsupported batch action: ${op.action}` };
|
|
3719
|
+
if (r && r.error) {
|
|
3720
|
+
failed++;
|
|
3721
|
+
results.push({ index: i, action: op.action, status: "error", error: r.message || r.error });
|
|
3722
|
+
if (!continueOnError)
|
|
3723
|
+
break;
|
|
3724
|
+
}
|
|
3725
|
+
else {
|
|
3726
|
+
succeeded++;
|
|
3727
|
+
results.push({ index: i, action: op.action, status: "success", result: r });
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
catch (e) {
|
|
3731
|
+
failed++;
|
|
3732
|
+
results.push({ index: i, action: op.action, status: "error", error: e.message || String(e) });
|
|
3733
|
+
if (!continueOnError)
|
|
3734
|
+
break;
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
const status = failed === 0 ? "completed" : succeeded === 0 ? "failed" : "partial";
|
|
3738
|
+
return { status, results, summary: { total: ops.length, succeeded, failed } };
|
|
3739
|
+
}
|
|
3740
|
+
catch (error) {
|
|
3741
|
+
return { error: "Batch execution failed", message: error.message };
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
/** Feature 8: Retrieve the context (observations/entities/timeline) of a session. */
|
|
3745
|
+
async getSessionContext(args) {
|
|
3746
|
+
await this.initPromise;
|
|
3747
|
+
try {
|
|
3748
|
+
const incObs = args.include_observations !== false;
|
|
3749
|
+
const incEnt = args.include_entities === true;
|
|
3750
|
+
const incTl = args.include_timeline === true;
|
|
3751
|
+
const limit = Math.min(Math.max(args.limit ?? 50, 1), 1000);
|
|
3752
|
+
let sessionId = args.session_id;
|
|
3753
|
+
if (!sessionId) {
|
|
3754
|
+
const latest = await this.db.run(`?[sid, la] := *session_state{session_id: sid, last_active: la} :order -la :limit 1`);
|
|
3755
|
+
if (latest.rows.length === 0)
|
|
3756
|
+
return { error: "No sessions found" };
|
|
3757
|
+
sessionId = latest.rows[0][0];
|
|
3758
|
+
}
|
|
3759
|
+
const ss = await this.db.run(`?[la, status, meta] := *session_state{session_id: $sid, last_active: la, status, metadata: meta}`, { sid: sessionId });
|
|
3760
|
+
const obsRes = await this.db.run(`?[oid, eid, text, metadata, ts] := *observation{id: oid, entity_id: eid, session_id: $sid, text, metadata, created_at, @ "NOW"}, ts = to_int(created_at) :order -ts`, { sid: sessionId });
|
|
3761
|
+
const obsRows = obsRes.rows;
|
|
3762
|
+
const entityIds = [...new Set(obsRows.map((r) => r[1]))];
|
|
3763
|
+
const nameMap = new Map();
|
|
3764
|
+
if (entityIds.length) {
|
|
3765
|
+
const entRes = await this.db.run(`?[id, name, type, ts] := *entity{id, name, type, created_at, @ "NOW"}, ts = to_int(created_at)`);
|
|
3766
|
+
entRes.rows.forEach((r) => nameMap.set(r[0], { name: r[1], type: r[2], ts: r[3] }));
|
|
3767
|
+
}
|
|
3768
|
+
const tss = obsRows.map((r) => r[4]);
|
|
3769
|
+
const created_at = tss.length ? new Date(Math.floor(Math.min(...tss) / 1000)).toISOString() : null;
|
|
3770
|
+
const session_metadata = {
|
|
3771
|
+
observation_count: obsRows.length,
|
|
3772
|
+
entity_count: entityIds.length,
|
|
3773
|
+
created_at,
|
|
3774
|
+
};
|
|
3775
|
+
if (ss.rows.length > 0) {
|
|
3776
|
+
session_metadata.last_active = ss.rows[0][0];
|
|
3777
|
+
session_metadata.status = ss.rows[0][1];
|
|
3778
|
+
session_metadata.metadata = ss.rows[0][2];
|
|
3779
|
+
}
|
|
3780
|
+
const result = { session_id: sessionId, session_metadata };
|
|
3781
|
+
if (incObs) {
|
|
3782
|
+
result.observations = obsRows.slice(0, limit).map((r) => ({
|
|
3783
|
+
id: r[0],
|
|
3784
|
+
text: r[2],
|
|
3785
|
+
entity_id: r[1],
|
|
3786
|
+
entity_name: nameMap.get(r[1])?.name ?? null,
|
|
3787
|
+
created_at: new Date(Math.floor(r[4] / 1000)).toISOString(),
|
|
3788
|
+
metadata: r[3],
|
|
3789
|
+
}));
|
|
3790
|
+
}
|
|
3791
|
+
if (incEnt) {
|
|
3792
|
+
result.entities = entityIds.map((id) => {
|
|
3793
|
+
const info = nameMap.get(id);
|
|
3794
|
+
return {
|
|
3795
|
+
id,
|
|
3796
|
+
name: info?.name ?? null,
|
|
3797
|
+
type: info?.type ?? null,
|
|
3798
|
+
created_at: info?.ts ? new Date(Math.floor(info.ts / 1000)).toISOString() : null,
|
|
3799
|
+
};
|
|
3800
|
+
});
|
|
3801
|
+
}
|
|
3802
|
+
if (incTl) {
|
|
3803
|
+
result.timeline = obsRows
|
|
3804
|
+
.slice()
|
|
3805
|
+
.sort((a, b) => a[4] - b[4])
|
|
3806
|
+
.map((r) => ({
|
|
3807
|
+
timestamp: new Date(Math.floor(r[4] / 1000)).toISOString(),
|
|
3808
|
+
action: "observation_added",
|
|
3809
|
+
detail: String(r[2]).slice(0, 80),
|
|
3810
|
+
}));
|
|
3811
|
+
}
|
|
3812
|
+
return result;
|
|
3813
|
+
}
|
|
3814
|
+
catch (error) {
|
|
3815
|
+
return { error: "Failed to get session context", message: error.message };
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
/** Feature 8: List known sessions with activity metadata. */
|
|
3819
|
+
async listSessions(args) {
|
|
3820
|
+
await this.initPromise;
|
|
3821
|
+
try {
|
|
3822
|
+
const limit = Math.min(Math.max(args.limit ?? 50, 1), 1000);
|
|
3823
|
+
const res = await this.db.run(`?[sid, la, status, meta] := *session_state{session_id: sid, last_active: la, status, metadata: meta} :order -la`);
|
|
3824
|
+
let rows = res.rows;
|
|
3825
|
+
if (args.active_only)
|
|
3826
|
+
rows = rows.filter((r) => r[2] === "active");
|
|
3827
|
+
const obsMap = new Map();
|
|
3828
|
+
const obsRes = await this.db.run(`?[sid, count(oid)] := *observation{id: oid, session_id: sid, @ "NOW"}, sid != ""`);
|
|
3829
|
+
obsRes.rows.forEach((r) => obsMap.set(r[0], Number(r[1])));
|
|
3830
|
+
const sessions = rows.slice(0, limit).map((r) => ({
|
|
3831
|
+
session_id: r[0],
|
|
3832
|
+
last_active: r[1],
|
|
3833
|
+
status: r[2],
|
|
3834
|
+
metadata: r[3],
|
|
3835
|
+
observation_count: obsMap.get(r[0]) || 0,
|
|
3836
|
+
}));
|
|
3837
|
+
return { sessions, total: rows.length };
|
|
3838
|
+
}
|
|
3839
|
+
catch (error) {
|
|
3840
|
+
return { error: "Failed to list sessions", message: error.message };
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3232
3843
|
registerTools() {
|
|
3233
3844
|
const MetadataSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.any());
|
|
3234
3845
|
const MutateMemorySchema = zod_1.z.discriminatedUnion("action", [
|
|
@@ -3401,10 +4012,49 @@ Format MUST start with "ExecutiveSummary: " followed by the consolidated content
|
|
|
3401
4012
|
entity_id: zod_1.z.string().describe("Entity ID to resolve conflicts for"),
|
|
3402
4013
|
auto_resolve: zod_1.z.boolean().optional().default(false).describe("Automatically resolve all conflicts"),
|
|
3403
4014
|
}),
|
|
4015
|
+
zod_1.z.object({
|
|
4016
|
+
action: zod_1.z.literal("update_observation"),
|
|
4017
|
+
observation_id: zod_1.z.string().describe("ID of the observation to update"),
|
|
4018
|
+
text: zod_1.z.string().optional().describe("New observation text (re-embedded if changed)"),
|
|
4019
|
+
metadata: MetadataSchema.optional().describe("New metadata"),
|
|
4020
|
+
merge_metadata: zod_1.z.boolean().optional().default(false).describe("Merge with existing metadata instead of replacing"),
|
|
4021
|
+
}).passthrough(),
|
|
4022
|
+
zod_1.z.object({
|
|
4023
|
+
action: zod_1.z.literal("batch_delete"),
|
|
4024
|
+
entity_ids: zod_1.z.array(zod_1.z.string()).optional().describe("Explicit list of entity IDs to delete"),
|
|
4025
|
+
filter: zod_1.z.object({
|
|
4026
|
+
type: zod_1.z.string().optional().describe("Delete all entities of this type"),
|
|
4027
|
+
name_contains: zod_1.z.string().optional().describe("Name contains (case-insensitive)"),
|
|
4028
|
+
created_before: zod_1.z.string().optional().describe("ISO date, only entities created before"),
|
|
4029
|
+
created_after: zod_1.z.string().optional().describe("ISO date, only entities created after"),
|
|
4030
|
+
metadata: MetadataSchema.optional().describe("Metadata exact-match filter"),
|
|
4031
|
+
tags: zod_1.z.array(zod_1.z.string()).optional().describe("Only entities having all of these tags (metadata.tags)"),
|
|
4032
|
+
}).optional().describe("Filter criteria (alternative to entity_ids)"),
|
|
4033
|
+
dry_run: zod_1.z.boolean().optional().default(false).describe("If true, only count without deleting"),
|
|
4034
|
+
}).passthrough().refine((v) => Boolean(v.entity_ids) || Boolean(v.filter), {
|
|
4035
|
+
message: "entity_ids or filter is required",
|
|
4036
|
+
path: ["entity_ids"],
|
|
4037
|
+
}),
|
|
4038
|
+
zod_1.z.object({
|
|
4039
|
+
action: zod_1.z.literal("manage_tags"),
|
|
4040
|
+
operation: zod_1.z.enum(["add", "remove", "set", "list", "search"]).describe("Tag operation"),
|
|
4041
|
+
entity_id: zod_1.z.string().optional().describe("Required for add/remove/set; optional for list"),
|
|
4042
|
+
tags: zod_1.z.array(zod_1.z.string()).optional().describe("Tag list for add/remove/set"),
|
|
4043
|
+
search_tag: zod_1.z.string().optional().describe("Tag to search for (operation=search)"),
|
|
4044
|
+
}).passthrough(),
|
|
4045
|
+
zod_1.z.object({
|
|
4046
|
+
action: zod_1.z.literal("batch"),
|
|
4047
|
+
operations: zod_1.z.array(zod_1.z.object({
|
|
4048
|
+
action: zod_1.z.enum(["create_entity", "add_observation", "create_relation", "delete_entity", "update_observation"]),
|
|
4049
|
+
params: zod_1.z.any().optional().describe("Parameters for the operation (object); flat fields also accepted"),
|
|
4050
|
+
}).passthrough()).describe("List of operations to execute"),
|
|
4051
|
+
continue_on_error: zod_1.z.boolean().optional().default(false).describe("Keep going after a failed operation (non-transactional)"),
|
|
4052
|
+
transactional: zod_1.z.boolean().optional().default(true).describe("All-or-nothing execution (default true)"),
|
|
4053
|
+
}).passthrough(),
|
|
3404
4054
|
]);
|
|
3405
4055
|
const MutateMemoryParameters = zod_1.z.object({
|
|
3406
4056
|
action: zod_1.z
|
|
3407
|
-
.enum(["create_entity", "update_entity", "delete_entity", "add_observation", "create_relation", "run_transaction", "add_inference_rule", "ingest_file", "start_session", "stop_session", "start_task", "stop_task", "invalidate_observation", "invalidate_relation", "enrich_observation", "record_memory_access", "prune_weak_memories", "detect_conflicts", "resolve_conflicts"])
|
|
4057
|
+
.enum(["create_entity", "update_entity", "delete_entity", "add_observation", "create_relation", "run_transaction", "add_inference_rule", "ingest_file", "start_session", "stop_session", "start_task", "stop_task", "invalidate_observation", "invalidate_relation", "enrich_observation", "record_memory_access", "prune_weak_memories", "detect_conflicts", "resolve_conflicts", "update_observation", "batch_delete", "manage_tags", "batch"])
|
|
3408
4058
|
.describe("Action (determines which fields are required)"),
|
|
3409
4059
|
name: zod_1.z.string().optional().describe("For create_entity (required) or add_inference_rule (required)"),
|
|
3410
4060
|
type: zod_1.z.string().optional().describe("For create_entity (required)"),
|
|
@@ -3426,12 +4076,22 @@ Format MUST start with "ExecutiveSummary: " followed by the consolidated content
|
|
|
3426
4076
|
relation_type: zod_1.z.string().optional().describe("For create_relation (required)"),
|
|
3427
4077
|
strength: zod_1.z.number().min(0).max(1).optional().describe("Optional for create_relation"),
|
|
3428
4078
|
metadata: MetadataSchema.optional().describe("Optional for create_entity/update_entity/add_observation/create_relation/ingest_file"),
|
|
3429
|
-
observation_id: zod_1.z.string().optional().describe("For invalidate_observation (required) or enrich_observation (required) or record_memory_access (required)"),
|
|
3430
|
-
|
|
4079
|
+
observation_id: zod_1.z.string().optional().describe("For invalidate_observation (required) or enrich_observation (required) or record_memory_access (required) or update_observation (required)"),
|
|
4080
|
+
session_id: zod_1.z.string().optional().describe("For add_observation/start_task: associate the observation/task with a session"),
|
|
4081
|
+
task_id: zod_1.z.string().optional().describe("For add_observation: associate the observation with a task"),
|
|
4082
|
+
dry_run: zod_1.z.boolean().optional().describe("For prune_weak_memories/batch_delete: if true, only shows candidates"),
|
|
3431
4083
|
operations: zod_1.z.array(zod_1.z.object({
|
|
3432
|
-
action: zod_1.z.enum(["create_entity", "add_observation", "create_relation", "delete_entity"]),
|
|
4084
|
+
action: zod_1.z.enum(["create_entity", "add_observation", "create_relation", "delete_entity", "update_observation"]),
|
|
3433
4085
|
params: zod_1.z.any().describe("Parameters for the operation as an object")
|
|
3434
|
-
})).optional().describe("For run_transaction: List of operations to be executed
|
|
4086
|
+
})).optional().describe("For run_transaction/batch: List of operations to be executed"),
|
|
4087
|
+
merge_metadata: zod_1.z.boolean().optional().describe("For update_observation: merge instead of replace metadata"),
|
|
4088
|
+
entity_ids: zod_1.z.array(zod_1.z.string()).optional().describe("For batch_delete: explicit entity IDs"),
|
|
4089
|
+
filter: zod_1.z.any().optional().describe("For batch_delete: filter criteria (type, name_contains, created_before/after, metadata, tags)"),
|
|
4090
|
+
operation: zod_1.z.enum(["add", "remove", "set", "list", "search"]).optional().describe("For manage_tags"),
|
|
4091
|
+
tags: zod_1.z.array(zod_1.z.string()).optional().describe("For manage_tags (add/remove/set)"),
|
|
4092
|
+
search_tag: zod_1.z.string().optional().describe("For manage_tags (operation=search)"),
|
|
4093
|
+
continue_on_error: zod_1.z.boolean().optional().describe("For batch: continue after failures (non-transactional)"),
|
|
4094
|
+
transactional: zod_1.z.boolean().optional().describe("For batch: all-or-nothing (default true)"),
|
|
3435
4095
|
});
|
|
3436
4096
|
this.mcp.addTool({
|
|
3437
4097
|
name: "mutate_memory",
|
|
@@ -3599,6 +4259,14 @@ Note: Inference rules must return exactly 5 columns: [from_id, to_id, relation_t
|
|
|
3599
4259
|
});
|
|
3600
4260
|
}
|
|
3601
4261
|
}
|
|
4262
|
+
if (action === "update_observation")
|
|
4263
|
+
return JSON.stringify(await this.updateObservation(rest));
|
|
4264
|
+
if (action === "batch_delete")
|
|
4265
|
+
return JSON.stringify(await this.batchDeleteEntities(rest));
|
|
4266
|
+
if (action === "manage_tags")
|
|
4267
|
+
return JSON.stringify(await this.manageTags(rest));
|
|
4268
|
+
if (action === "batch")
|
|
4269
|
+
return JSON.stringify(await this.executeBatch(rest));
|
|
3602
4270
|
return JSON.stringify({ error: "Unknown action" });
|
|
3603
4271
|
},
|
|
3604
4272
|
});
|
|
@@ -3762,10 +4430,42 @@ Note: Inference rules must return exactly 5 columns: [from_id, to_id, relation_t
|
|
|
3762
4430
|
levels: zod_1.z.array(zod_1.z.number().min(0).max(3)).optional().describe("Memory levels to query (0-3)"),
|
|
3763
4431
|
limit: zod_1.z.number().optional().default(10).describe("Maximum number of results"),
|
|
3764
4432
|
}),
|
|
4433
|
+
zod_1.z.object({
|
|
4434
|
+
action: zod_1.z.literal("list_entities"),
|
|
4435
|
+
type: zod_1.z.string().optional().describe("Filter by a single entity type"),
|
|
4436
|
+
types: zod_1.z.array(zod_1.z.string()).optional().describe("Filter by multiple entity types"),
|
|
4437
|
+
limit: zod_1.z.number().min(1).max(1000).optional().default(20).describe("Page size (default 20, max 1000)"),
|
|
4438
|
+
offset: zod_1.z.number().min(0).optional().default(0).describe("Pagination offset"),
|
|
4439
|
+
sort_by: zod_1.z.enum(["name", "created_at", "updated_at"]).optional().default("created_at").describe("Sort field"),
|
|
4440
|
+
sort_order: zod_1.z.enum(["asc", "desc"]).optional().default("desc").describe("Sort order"),
|
|
4441
|
+
name_contains: zod_1.z.string().optional().describe("Case-insensitive substring filter on name"),
|
|
4442
|
+
tags: zod_1.z.array(zod_1.z.string()).optional().describe("Only entities having all of these tags (metadata.tags)"),
|
|
4443
|
+
}),
|
|
4444
|
+
zod_1.z.object({
|
|
4445
|
+
action: zod_1.z.literal("get_entity_detail"),
|
|
4446
|
+
entity_id: zod_1.z.string().describe("ID of the entity"),
|
|
4447
|
+
include_observations: zod_1.z.boolean().optional().default(true).describe("Include observations"),
|
|
4448
|
+
include_relations: zod_1.z.boolean().optional().default(true).describe("Include outgoing/incoming relations"),
|
|
4449
|
+
include_community: zod_1.z.boolean().optional().default(false).describe("Include community membership"),
|
|
4450
|
+
include_timeline: zod_1.z.boolean().optional().default(false).describe("Include a chronological timeline"),
|
|
4451
|
+
}),
|
|
4452
|
+
zod_1.z.object({
|
|
4453
|
+
action: zod_1.z.literal("get_session_context"),
|
|
4454
|
+
session_id: zod_1.z.string().optional().describe("Session ID (defaults to the most recently active session)"),
|
|
4455
|
+
include_observations: zod_1.z.boolean().optional().default(true).describe("Include observations"),
|
|
4456
|
+
include_entities: zod_1.z.boolean().optional().default(false).describe("Include referenced entities"),
|
|
4457
|
+
include_timeline: zod_1.z.boolean().optional().default(false).describe("Include a chronological timeline"),
|
|
4458
|
+
limit: zod_1.z.number().min(1).max(1000).optional().default(50).describe("Max observations"),
|
|
4459
|
+
}),
|
|
4460
|
+
zod_1.z.object({
|
|
4461
|
+
action: zod_1.z.literal("list_sessions"),
|
|
4462
|
+
active_only: zod_1.z.boolean().optional().default(false).describe("Only sessions with status 'active'"),
|
|
4463
|
+
limit: zod_1.z.number().min(1).max(1000).optional().default(50).describe("Max sessions"),
|
|
4464
|
+
}),
|
|
3765
4465
|
]);
|
|
3766
4466
|
const QueryMemoryParameters = zod_1.z.object({
|
|
3767
4467
|
action: zod_1.z
|
|
3768
|
-
.enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking", "agentic_search", "dynamic_fusion", "adaptive_retrieval", "get_zettelkasten_stats", "get_activation_stats", "get_salience_stats", "suggest_connections", "spreading_activation", "qafd_search", "hierarchical_memory_query"])
|
|
4468
|
+
.enum(["search", "advancedSearch", "context", "entity_details", "history", "graph_rag", "graph_walking", "agentic_search", "dynamic_fusion", "adaptive_retrieval", "get_zettelkasten_stats", "get_activation_stats", "get_salience_stats", "suggest_connections", "spreading_activation", "qafd_search", "hierarchical_memory_query", "explain_results", "list_entities", "get_entity_detail", "get_session_context", "list_sessions"])
|
|
3769
4469
|
.describe("Retrieval strategy - use 'search' for simple queries, 'adaptive_retrieval' for auto-optimization, 'context' for exploration"),
|
|
3770
4470
|
query: zod_1.z.string().optional().describe("Search query text (required for most actions)"),
|
|
3771
4471
|
limit: zod_1.z.number().optional().describe("Maximum number of results to return (default: 10)"),
|
|
@@ -3785,6 +4485,17 @@ Note: Inference rules must return exactly 5 columns: [from_id, to_id, relation_t
|
|
|
3785
4485
|
start_entity_id: zod_1.z.string().optional().describe("For graph_walking: Starting entity for traversal"),
|
|
3786
4486
|
rerank: zod_1.z.boolean().optional().describe("For search/advancedSearch: Enable Cross-Encoder reranking for higher precision"),
|
|
3787
4487
|
config: zod_1.z.any().optional().describe("For dynamic_fusion: Fine-tune vector/sparse/FTS/graph weights and fusion strategy"),
|
|
4488
|
+
type: zod_1.z.string().optional().describe("For list_entities: filter by a single entity type"),
|
|
4489
|
+
types: zod_1.z.array(zod_1.z.string()).optional().describe("For list_entities: filter by multiple entity types"),
|
|
4490
|
+
offset: zod_1.z.number().optional().describe("For list_entities: pagination offset"),
|
|
4491
|
+
sort_by: zod_1.z.enum(["name", "created_at", "updated_at"]).optional().describe("For list_entities: sort field"),
|
|
4492
|
+
sort_order: zod_1.z.enum(["asc", "desc"]).optional().describe("For list_entities: sort order"),
|
|
4493
|
+
name_contains: zod_1.z.string().optional().describe("For list_entities: case-insensitive substring filter on name"),
|
|
4494
|
+
tags: zod_1.z.array(zod_1.z.string()).optional().describe("For list_entities: only entities having all of these tags"),
|
|
4495
|
+
include_relations: zod_1.z.boolean().optional().describe("For get_entity_detail: include relations (default true)"),
|
|
4496
|
+
include_community: zod_1.z.boolean().optional().describe("For get_entity_detail: include community (default false)"),
|
|
4497
|
+
include_timeline: zod_1.z.boolean().optional().describe("For get_entity_detail/get_session_context: include timeline"),
|
|
4498
|
+
active_only: zod_1.z.boolean().optional().describe("For list_sessions: only active sessions"),
|
|
3788
4499
|
});
|
|
3789
4500
|
this.mcp.addTool({
|
|
3790
4501
|
name: "query_memory",
|
|
@@ -4370,6 +5081,62 @@ Note: User profile observations (entity_id='global_user_profile') are automatica
|
|
|
4370
5081
|
},
|
|
4371
5082
|
});
|
|
4372
5083
|
}
|
|
5084
|
+
if (input.action === "explain_results") {
|
|
5085
|
+
try {
|
|
5086
|
+
console.log('[query_memory] Explaining', input.results.length, 'results for query:', input.query);
|
|
5087
|
+
const explained = await this.getExplainableService().explainResults(input.results, input.query, input.search_type || 'hybrid', {
|
|
5088
|
+
includePathVisualization: input.include_path_viz,
|
|
5089
|
+
includeReasoningSteps: input.include_reasoning,
|
|
5090
|
+
includeScoreBreakdown: input.include_score_breakdown
|
|
5091
|
+
});
|
|
5092
|
+
console.log('[query_memory] Explanation completed for', explained.length, 'results');
|
|
5093
|
+
return JSON.stringify({
|
|
5094
|
+
query: input.query,
|
|
5095
|
+
search_type: input.search_type,
|
|
5096
|
+
explained_results: explained
|
|
5097
|
+
});
|
|
5098
|
+
}
|
|
5099
|
+
catch (error) {
|
|
5100
|
+
console.error('[query_memory] Error explaining results:', error);
|
|
5101
|
+
return JSON.stringify({ error: "Failed to explain results", details: error.message });
|
|
5102
|
+
}
|
|
5103
|
+
}
|
|
5104
|
+
if (input.action === "list_entities") {
|
|
5105
|
+
return JSON.stringify(await this.listEntities({
|
|
5106
|
+
type: input.type,
|
|
5107
|
+
types: input.types,
|
|
5108
|
+
limit: input.limit,
|
|
5109
|
+
offset: input.offset,
|
|
5110
|
+
sort_by: input.sort_by,
|
|
5111
|
+
sort_order: input.sort_order,
|
|
5112
|
+
name_contains: input.name_contains,
|
|
5113
|
+
tags: input.tags,
|
|
5114
|
+
}));
|
|
5115
|
+
}
|
|
5116
|
+
if (input.action === "get_entity_detail") {
|
|
5117
|
+
return JSON.stringify(await this.getEntityDetail({
|
|
5118
|
+
entity_id: input.entity_id,
|
|
5119
|
+
include_observations: input.include_observations,
|
|
5120
|
+
include_relations: input.include_relations,
|
|
5121
|
+
include_community: input.include_community,
|
|
5122
|
+
include_timeline: input.include_timeline,
|
|
5123
|
+
}));
|
|
5124
|
+
}
|
|
5125
|
+
if (input.action === "get_session_context") {
|
|
5126
|
+
return JSON.stringify(await this.getSessionContext({
|
|
5127
|
+
session_id: input.session_id,
|
|
5128
|
+
include_observations: input.include_observations,
|
|
5129
|
+
include_entities: input.include_entities,
|
|
5130
|
+
include_timeline: input.include_timeline,
|
|
5131
|
+
limit: input.limit,
|
|
5132
|
+
}));
|
|
5133
|
+
}
|
|
5134
|
+
if (input.action === "list_sessions") {
|
|
5135
|
+
return JSON.stringify(await this.listSessions({
|
|
5136
|
+
active_only: input.active_only,
|
|
5137
|
+
limit: input.limit,
|
|
5138
|
+
}));
|
|
5139
|
+
}
|
|
4373
5140
|
return JSON.stringify({ error: "Unknown action" });
|
|
4374
5141
|
},
|
|
4375
5142
|
});
|
|
@@ -4811,6 +5578,7 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
|
|
|
4811
5578
|
const ManageSystemSchema = zod_1.z.discriminatedUnion("action", [
|
|
4812
5579
|
zod_1.z.object({ action: zod_1.z.literal("health") }),
|
|
4813
5580
|
zod_1.z.object({ action: zod_1.z.literal("metrics") }),
|
|
5581
|
+
zod_1.z.object({ action: zod_1.z.literal("stats") }),
|
|
4814
5582
|
zod_1.z.object({
|
|
4815
5583
|
action: zod_1.z.literal("export_memory"),
|
|
4816
5584
|
format: zod_1.z.enum(["json", "markdown", "obsidian"]).describe("Export format"),
|
|
@@ -4881,10 +5649,15 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
|
|
|
4881
5649
|
action: zod_1.z.literal("analyze_memory_distribution"),
|
|
4882
5650
|
entity_id: zod_1.z.string().describe("Entity ID to analyze memory distribution for"),
|
|
4883
5651
|
}),
|
|
5652
|
+
zod_1.z.object({ action: zod_1.z.literal("list_inference_rules") }),
|
|
5653
|
+
zod_1.z.object({
|
|
5654
|
+
action: zod_1.z.literal("delete_inference_rule"),
|
|
5655
|
+
rule_id: zod_1.z.string().describe("ID of the inference rule to delete"),
|
|
5656
|
+
}),
|
|
4884
5657
|
]);
|
|
4885
5658
|
const ManageSystemParameters = zod_1.z.object({
|
|
4886
5659
|
action: zod_1.z
|
|
4887
|
-
.enum(["health", "metrics", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "defrag", "reflect", "clear_memory", "summarize_communities", "compact", "compress_memory_levels", "analyze_memory_distribution"])
|
|
5660
|
+
.enum(["health", "metrics", "stats", "export_memory", "import_memory", "snapshot_create", "snapshot_list", "snapshot_diff", "cleanup", "defrag", "reflect", "clear_memory", "summarize_communities", "compact", "compress_memory_levels", "analyze_memory_distribution", "list_inference_rules", "delete_inference_rule"])
|
|
4888
5661
|
.describe("Action (determines which fields are required)"),
|
|
4889
5662
|
format: zod_1.z.enum(["json", "markdown", "obsidian"]).optional().describe("Export format (for export_memory)"),
|
|
4890
5663
|
includeMetadata: zod_1.z.boolean().optional().describe("Include metadata (for export_memory)"),
|
|
@@ -4910,6 +5683,7 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
|
|
|
4910
5683
|
min_community_size: zod_1.z.number().optional().describe("Optional for summarize_communities"),
|
|
4911
5684
|
mode: zod_1.z.enum(["summary", "discovery"]).optional().describe("Optional for reflect"),
|
|
4912
5685
|
level: zod_1.z.number().optional().describe("Required for compress_memory_levels (0-3: L0_RAW, L1_SESSION, L2_WEEKLY, L3_MONTHLY)"),
|
|
5686
|
+
rule_id: zod_1.z.string().optional().describe("Required for delete_inference_rule"),
|
|
4913
5687
|
});
|
|
4914
5688
|
this.mcp.addTool({
|
|
4915
5689
|
name: "manage_system",
|
|
@@ -4919,7 +5693,7 @@ Monitoring: health (status check), metrics (detailed stats)
|
|
|
4919
5693
|
Data portability: export_memory (JSON/Markdown/Obsidian), import_memory (Mem0/MemGPT/Cozo)
|
|
4920
5694
|
Backups: snapshot_create, snapshot_list, snapshot_diff
|
|
4921
5695
|
Optimization: cleanup (LLM consolidation), defrag (merge duplicates), reflect (find contradictions)
|
|
4922
|
-
Advanced: summarize_communities, compress_memory_levels, analyze_memory_distribution, compact
|
|
5696
|
+
Advanced: summarize_communities, compress_memory_levels, analyze_memory_distribution, compact, list_inference_rules, delete_inference_rule
|
|
4923
5697
|
|
|
4924
5698
|
Important: Use confirm=false for dry-run before cleanup/defrag. clear_memory requires confirm=true.
|
|
4925
5699
|
|
|
@@ -4978,6 +5752,9 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
|
|
|
4978
5752
|
return JSON.stringify({ error: "Failed to retrieve metrics", message: error.message });
|
|
4979
5753
|
}
|
|
4980
5754
|
}
|
|
5755
|
+
if (input.action === "stats") {
|
|
5756
|
+
return JSON.stringify(await this.getStats());
|
|
5757
|
+
}
|
|
4981
5758
|
if (input.action === "export_memory") {
|
|
4982
5759
|
try {
|
|
4983
5760
|
const result = await this.exportMemory({
|
|
@@ -5251,6 +6028,44 @@ For detailed action descriptions and parameters, see docs/USAGE-GUIDE.md.`,
|
|
|
5251
6028
|
return JSON.stringify({ error: error.message || "Error analyzing memory distribution" });
|
|
5252
6029
|
}
|
|
5253
6030
|
}
|
|
6031
|
+
if (input.action === "list_inference_rules") {
|
|
6032
|
+
try {
|
|
6033
|
+
const res = await this.db.run('?[id, name, datalog, created_at] := *inference_rule{id, name, datalog, created_at}');
|
|
6034
|
+
const rules = res.rows.map((r) => ({
|
|
6035
|
+
id: r[0],
|
|
6036
|
+
name: r[1],
|
|
6037
|
+
datalog: r[2],
|
|
6038
|
+
created_at: r[3] ? new Date(Number(r[3])).toISOString() : null,
|
|
6039
|
+
}));
|
|
6040
|
+
return JSON.stringify({ count: rules.length, rules });
|
|
6041
|
+
}
|
|
6042
|
+
catch (error) {
|
|
6043
|
+
return JSON.stringify({ error: error.message || "Error listing inference rules" });
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
if (input.action === "delete_inference_rule") {
|
|
6047
|
+
try {
|
|
6048
|
+
if (!input.rule_id) {
|
|
6049
|
+
return JSON.stringify({ error: "rule_id is required for delete_inference_rule" });
|
|
6050
|
+
}
|
|
6051
|
+
// Check if rule exists
|
|
6052
|
+
const existing = await this.db.run('?[id, name] := *inference_rule{id, name}, id = $id', { id: input.rule_id });
|
|
6053
|
+
if (existing.rows.length === 0) {
|
|
6054
|
+
return JSON.stringify({ error: `Inference rule with ID '${input.rule_id}' not found` });
|
|
6055
|
+
}
|
|
6056
|
+
const ruleName = existing.rows[0][1];
|
|
6057
|
+
// Delete the rule
|
|
6058
|
+
await this.db.run('{ ?[id, name, datalog, created_at] := *inference_rule{id, name, datalog, created_at}, id = $id :rm inference_rule {id, name, datalog, created_at} }', { id: input.rule_id });
|
|
6059
|
+
return JSON.stringify({
|
|
6060
|
+
status: "deleted",
|
|
6061
|
+
rule_id: input.rule_id,
|
|
6062
|
+
name: ruleName,
|
|
6063
|
+
});
|
|
6064
|
+
}
|
|
6065
|
+
catch (error) {
|
|
6066
|
+
return JSON.stringify({ error: error.message || "Error deleting inference rule" });
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
5254
6069
|
return JSON.stringify({ error: "Unknown action" });
|
|
5255
6070
|
},
|
|
5256
6071
|
});
|