memtap 2.1.1 → 3.1.1
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/index.ts +784 -37
- package/openclaw.plugin.json +22 -2
- package/package.json +1 -1
- package/LICENSE +0 -21
package/index.ts
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MemTap — Graph-based Long-Term Memory Plugin for OpenClaw
|
|
3
|
-
*
|
|
3
|
+
* v3.1.0 "The Oracle" — Proactive Surfacing, Learning Loop & Inference Engine
|
|
4
4
|
*
|
|
5
5
|
* Tools:
|
|
6
|
-
* - memtap_recall
|
|
7
|
-
* - memtap_remember
|
|
8
|
-
* - memtap_memory
|
|
9
|
-
* - memtap_bulletin
|
|
10
|
-
* - memtap_graph
|
|
11
|
-
* - memtap_decide
|
|
12
|
-
* - memtap_graphrag
|
|
13
|
-
* - memtap_maintenance
|
|
14
|
-
* - memtap_entities
|
|
15
|
-
* - memtap_edges
|
|
16
|
-
* -
|
|
6
|
+
* - memtap_recall — semantic graph recall
|
|
7
|
+
* - memtap_remember — store a memory in the graph (supports immutable flag)
|
|
8
|
+
* - memtap_memory — get, update, delete individual memories
|
|
9
|
+
* - memtap_bulletin — context bulletin with graph expansion
|
|
10
|
+
* - memtap_graph — graph analysis (overview, gaps, clusters, connections, traverse)
|
|
11
|
+
* - memtap_decide — decision tracking (list, create, resolve, defer)
|
|
12
|
+
* - memtap_graphrag — vector/BM25 + graph traversal search
|
|
13
|
+
* - memtap_maintenance — memory maintenance (decay-report, contradictions, dedup-scan, resolve-contradictions, run-all)
|
|
14
|
+
* - memtap_entities — entity management (list, memories, merge)
|
|
15
|
+
* - memtap_edges — create edges between memories
|
|
16
|
+
* - memtap_consolidate — consolidate related memories into synthesized summaries
|
|
17
|
+
* - memtap_profile — view/refresh agent memory profile
|
|
18
|
+
* - memtap_export — export memory graph (json, graphml, markdown)
|
|
19
|
+
* - memtap_health — server health check and statistics
|
|
20
|
+
* - memtap_outcome — record decision outcomes for learning loop
|
|
21
|
+
* - memtap_infer — inference engine: discover implicit knowledge via graph traversal + LLM reasoning
|
|
17
22
|
*
|
|
18
23
|
* Hooks:
|
|
19
|
-
* - preMessage — neuromimetic tiered recall with working memory simulation
|
|
20
|
-
*
|
|
24
|
+
* - preMessage — neuromimetic tiered recall with working memory simulation + adaptive decay reinforcement
|
|
25
|
+
* + proactive surfacing of thematically related memories
|
|
26
|
+
* + decision outcome context injection (learning loop)
|
|
27
|
+
* - message_completed — attention-gated encoding with emotional weighting + auto-category assignment
|
|
28
|
+
* + proactive memory usage tracking with importance reinforcement
|
|
21
29
|
* - agent:bootstrap — inject memory bulletin at session start
|
|
22
30
|
* - periodic — dream-mode consolidation and neural maintenance
|
|
23
31
|
* - session_end — performance monitoring and neural analytics
|
|
@@ -38,6 +46,10 @@ interface MemTapConfig {
|
|
|
38
46
|
embeddingModel?: string;
|
|
39
47
|
embeddingApiKey?: string;
|
|
40
48
|
decayRate?: number;
|
|
49
|
+
instructions?: {
|
|
50
|
+
include?: string[];
|
|
51
|
+
exclude?: string[];
|
|
52
|
+
};
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
function getConfig(api: any): MemTapConfig {
|
|
@@ -46,7 +58,9 @@ function getConfig(api: any): MemTapConfig {
|
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
function baseUrl(cfg: MemTapConfig): string {
|
|
49
|
-
|
|
61
|
+
const url = (cfg.serverUrl || 'https://api.memtap.ai').replace(/\/$/, '');
|
|
62
|
+
// Ensure /v1 prefix for production API
|
|
63
|
+
return url.endsWith('/v1') ? url : `${url}/v1`;
|
|
50
64
|
}
|
|
51
65
|
|
|
52
66
|
function agentId(cfg: MemTapConfig, api: any): string {
|
|
@@ -88,7 +102,9 @@ function storeImportance(userValue: number): number {
|
|
|
88
102
|
|
|
89
103
|
// ── Memory types ─────────────────────────────────────────────────────────────
|
|
90
104
|
|
|
91
|
-
const MEMORY_TYPES = ['fact', 'preference', 'decision', 'identity', 'event', 'observation', 'goal', 'task'] as const;
|
|
105
|
+
const MEMORY_TYPES = ['fact', 'preference', 'decision', 'identity', 'event', 'observation', 'goal', 'task', 'consolidated', 'outcome', 'inferred'] as const;
|
|
106
|
+
|
|
107
|
+
const MEMORY_CATEGORIES = ['personal', 'professional', 'technical', 'project', 'health', 'preferences'] as const;
|
|
92
108
|
|
|
93
109
|
// ── Neuromimetic Memory System (v2.1 "The Neuron") ──────────────────────────
|
|
94
110
|
|
|
@@ -178,6 +194,9 @@ const memoryCache = new Map<string, { data: any[]; timestamp: number; query: str
|
|
|
178
194
|
// Attention tracking for encoding decisions
|
|
179
195
|
let attentionHistory: Array<{ timestamp: number; agent: string; level: string; trigger: string }> = [];
|
|
180
196
|
|
|
197
|
+
// Proactive surfacing tracking (which proactive memories were injected per agent)
|
|
198
|
+
const proactiveSurfacedMemories = new Map<string, { memoryIds: string[]; timestamp: number }>();
|
|
199
|
+
|
|
181
200
|
// ── Neuromimetic Functions ──────────────────────────────────────────────────
|
|
182
201
|
|
|
183
202
|
function updateWorkingMemory(agentId: string, topics: string[], memories: any[] = []): WorkingMemory {
|
|
@@ -571,7 +590,8 @@ Antwort als JSON-Array (NUR das Array, kein Markdown):
|
|
|
571
590
|
"content": "Kurze, prägnante Beschreibung des Fakts",
|
|
572
591
|
"type": "fact|preference|decision|identity|event|observation|goal|task",
|
|
573
592
|
"importance": 1-10,
|
|
574
|
-
"tags": ["tag1", "tag2"]
|
|
593
|
+
"tags": ["tag1", "tag2"],
|
|
594
|
+
"category": "personal|professional|technical|project|health|preferences"
|
|
575
595
|
}
|
|
576
596
|
]
|
|
577
597
|
|
|
@@ -680,19 +700,21 @@ export default function register(api: any) {
|
|
|
680
700
|
type: { type: 'string', enum: MEMORY_TYPES, description: 'Memory type (default: fact)' },
|
|
681
701
|
importance: { type: 'number', description: 'Importance 1-10 (default: 5)' },
|
|
682
702
|
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags for categorization' },
|
|
703
|
+
immutable: { type: 'boolean', description: 'Mark memory as immutable (cannot be auto-decayed or auto-archived)' },
|
|
683
704
|
},
|
|
684
705
|
required: ['content'],
|
|
685
706
|
},
|
|
686
|
-
async execute(_id: string, params: { content: string; type?: string; importance?: number; tags?: string[] }) {
|
|
707
|
+
async execute(_id: string, params: { content: string; type?: string; importance?: number; tags?: string[]; immutable?: boolean }) {
|
|
687
708
|
const cfg = getConfig(api);
|
|
688
709
|
const importance = params.importance ?? 5;
|
|
689
|
-
const body = {
|
|
710
|
+
const body: Record<string, any> = {
|
|
690
711
|
content: params.content,
|
|
691
712
|
type: params.type || 'fact',
|
|
692
713
|
agent: agentId(cfg, api),
|
|
693
714
|
importance: storeImportance(importance),
|
|
694
715
|
tags: params.tags || [],
|
|
695
716
|
};
|
|
717
|
+
if (params.immutable) body.immutable = true;
|
|
696
718
|
|
|
697
719
|
try {
|
|
698
720
|
const data = await bbFetch(cfg, `${baseUrl(cfg)}/memories`, {
|
|
@@ -857,13 +879,14 @@ export default function register(api: any) {
|
|
|
857
879
|
'- decay-report: find memories that have decayed below importance threshold\n' +
|
|
858
880
|
'- contradictions: find contradicting memory pairs\n' +
|
|
859
881
|
'- dedup-scan: find potential duplicate memories\n' +
|
|
882
|
+
'- resolve-contradictions: use LLM to resolve contradicting memories\n' +
|
|
860
883
|
'- run-all: combined report of all checks',
|
|
861
884
|
parameters: {
|
|
862
885
|
type: 'object',
|
|
863
886
|
properties: {
|
|
864
887
|
action: {
|
|
865
888
|
type: 'string',
|
|
866
|
-
enum: ['decay-report', 'contradictions', 'dedup-scan', 'run-all'],
|
|
889
|
+
enum: ['decay-report', 'contradictions', 'dedup-scan', 'resolve-contradictions', 'run-all'],
|
|
867
890
|
description: 'Maintenance action to run',
|
|
868
891
|
},
|
|
869
892
|
},
|
|
@@ -925,6 +948,63 @@ export default function register(api: any) {
|
|
|
925
948
|
return { content: [{ type: 'text', text: output }] };
|
|
926
949
|
}
|
|
927
950
|
|
|
951
|
+
case 'resolve-contradictions': {
|
|
952
|
+
// Fetch contradictions
|
|
953
|
+
data = await bbFetch(cfg, `${base}/maintenance/contradictions`);
|
|
954
|
+
const contradictions = data.results || data.contradictions || [];
|
|
955
|
+
if (contradictions.length === 0) {
|
|
956
|
+
return { content: [{ type: 'text', text: 'No contradictions found to resolve.' }] };
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
let resolved = 0;
|
|
960
|
+
const resolutions: string[] = [];
|
|
961
|
+
|
|
962
|
+
for (const c of contradictions.slice(0, 10)) {
|
|
963
|
+
const m1 = c.memory1 || c;
|
|
964
|
+
const m2 = c.memory2 || c;
|
|
965
|
+
const m1Summary = m1.summary || m1.content || m1.from || '';
|
|
966
|
+
const m2Summary = m2.summary || m2.content || m2.to || '';
|
|
967
|
+
|
|
968
|
+
try {
|
|
969
|
+
const llmUrl = cfg.llmUrl || 'http://127.0.0.1:18789/v1/chat/completions';
|
|
970
|
+
const model = cfg.llmModel || 'anthropic/claude-sonnet-4-20250514';
|
|
971
|
+
const llmRes = await fetch(llmUrl, {
|
|
972
|
+
method: 'POST',
|
|
973
|
+
headers: { 'Content-Type': 'application/json' },
|
|
974
|
+
body: JSON.stringify({
|
|
975
|
+
model,
|
|
976
|
+
max_tokens: 500,
|
|
977
|
+
messages: [
|
|
978
|
+
{ role: 'system', content: 'You resolve contradictions between two memories. Decide which is more current/accurate. Respond with JSON: {"keep": 1 or 2, "reason": "short reason"}' },
|
|
979
|
+
{ role: 'user', content: `Memory 1 (created: ${m1.created || '?'}): ${m1Summary}\nMemory 2 (created: ${m2.created || '?'}): ${m2Summary}` },
|
|
980
|
+
],
|
|
981
|
+
}),
|
|
982
|
+
});
|
|
983
|
+
if (!llmRes.ok) continue;
|
|
984
|
+
const llmData = await llmRes.json();
|
|
985
|
+
const text = llmData.choices?.[0]?.message?.content?.trim() || '';
|
|
986
|
+
const cleaned = text.replace(/^```json?\n?/m, '').replace(/\n?```$/m, '').trim();
|
|
987
|
+
const verdict = JSON.parse(cleaned);
|
|
988
|
+
|
|
989
|
+
const keepId = verdict.keep === 1 ? (m1.id || m1._key) : (m2.id || m2._key);
|
|
990
|
+
const archiveId = verdict.keep === 1 ? (m2.id || m2._key) : (m1.id || m1._key);
|
|
991
|
+
|
|
992
|
+
if (keepId && archiveId) {
|
|
993
|
+
await bbFetch(cfg, `${base}/maintenance/resolve-contradiction`, {
|
|
994
|
+
method: 'POST',
|
|
995
|
+
body: JSON.stringify({ keep: keepId, archive: archiveId, reason: verdict.reason }),
|
|
996
|
+
});
|
|
997
|
+
resolved++;
|
|
998
|
+
resolutions.push(`Kept [${keepId}], archived [${archiveId}]: ${verdict.reason}`);
|
|
999
|
+
}
|
|
1000
|
+
} catch { /* skip individual resolution failures */ }
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
let output = `Contradiction Resolution: ${resolved}/${contradictions.length} resolved\n\n`;
|
|
1004
|
+
output += resolutions.map((r, i) => ` ${i + 1}. ${r}`).join('\n');
|
|
1005
|
+
return { content: [{ type: 'text', text: output }] };
|
|
1006
|
+
}
|
|
1007
|
+
|
|
928
1008
|
case 'run-all': {
|
|
929
1009
|
data = await bbFetch(cfg, `${base}/maintenance/run-all`, { method: 'POST', body: '{}' });
|
|
930
1010
|
const decay = data.decay || data.decayReport || {};
|
|
@@ -1342,6 +1422,201 @@ export default function register(api: any) {
|
|
|
1342
1422
|
},
|
|
1343
1423
|
});
|
|
1344
1424
|
|
|
1425
|
+
// ── Tool: memtap_consolidate ──────────────────────────────────────────────
|
|
1426
|
+
|
|
1427
|
+
api.registerTool({
|
|
1428
|
+
name: 'memtap_consolidate',
|
|
1429
|
+
description:
|
|
1430
|
+
'Consolidate related memories into a synthesized summary. ' +
|
|
1431
|
+
'Searches memories by entity or topic, uses LLM to create a synthesis, ' +
|
|
1432
|
+
'and stores the result as a consolidated memory linked to the originals.',
|
|
1433
|
+
parameters: {
|
|
1434
|
+
type: 'object',
|
|
1435
|
+
properties: {
|
|
1436
|
+
entity: { type: 'string', description: 'Entity name to consolidate memories around' },
|
|
1437
|
+
topic: { type: 'string', description: 'Topic to consolidate memories about' },
|
|
1438
|
+
},
|
|
1439
|
+
},
|
|
1440
|
+
async execute(_id: string, params: { entity?: string; topic?: string }) {
|
|
1441
|
+
if (!params.entity && !params.topic) {
|
|
1442
|
+
return { content: [{ type: 'text', text: 'Provide at least "entity" or "topic" to consolidate.' }], isError: true };
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const cfg = getConfig(api);
|
|
1446
|
+
const base = baseUrl(cfg);
|
|
1447
|
+
const agent = agentId(cfg, api);
|
|
1448
|
+
const query = params.entity || params.topic || '';
|
|
1449
|
+
|
|
1450
|
+
try {
|
|
1451
|
+
// 1. Recall related memories
|
|
1452
|
+
const recallUrl = new URL('/recall', base);
|
|
1453
|
+
recallUrl.searchParams.set('q', query);
|
|
1454
|
+
recallUrl.searchParams.set('agent', agent);
|
|
1455
|
+
recallUrl.searchParams.set('limit', '20');
|
|
1456
|
+
const recallData = await bbFetch(cfg, recallUrl.toString());
|
|
1457
|
+
const memories = recallData.results || recallData.memories || [];
|
|
1458
|
+
|
|
1459
|
+
if (memories.length < 2) {
|
|
1460
|
+
return { content: [{ type: 'text', text: `Only ${memories.length} memory found for "${query}" — need at least 2 to consolidate.` }] };
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// 2. LLM synthesis
|
|
1464
|
+
const memoryTexts = memories.map((m: any, i: number) =>
|
|
1465
|
+
`${i + 1}. [${m.type}] ${m.content} (importance: ${displayImportance(m.importance)}/10, created: ${m.created || '?'})`
|
|
1466
|
+
).join('\n');
|
|
1467
|
+
|
|
1468
|
+
const llmUrl = cfg.llmUrl || 'http://127.0.0.1:18789/v1/chat/completions';
|
|
1469
|
+
const model = cfg.llmModel || 'anthropic/claude-sonnet-4-20250514';
|
|
1470
|
+
|
|
1471
|
+
const synthRes = await fetch(llmUrl, {
|
|
1472
|
+
method: 'POST',
|
|
1473
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1474
|
+
body: JSON.stringify({
|
|
1475
|
+
model,
|
|
1476
|
+
max_tokens: 1500,
|
|
1477
|
+
messages: [
|
|
1478
|
+
{ role: 'system', content: 'Du erstellst eine prägnante Synthese aus mehreren Erinnerungen. Fasse Kernaussagen zusammen, entferne Redundanzen, hebe Widersprüche hervor. Antwort als JSON: {"synthesis": "...", "keyInsights": ["..."], "importance": 1-10}' },
|
|
1479
|
+
{ role: 'user', content: `Konsolidiere diese ${memories.length} Erinnerungen zum Thema "${query}":\n\n${memoryTexts}` },
|
|
1480
|
+
],
|
|
1481
|
+
}),
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
if (!synthRes.ok) throw new Error(`LLM synthesis failed: ${synthRes.status}`);
|
|
1485
|
+
const synthData = await synthRes.json();
|
|
1486
|
+
const synthText = synthData.choices?.[0]?.message?.content?.trim() || '';
|
|
1487
|
+
const cleanedSynth = synthText.replace(/^```json?\n?/m, '').replace(/\n?```$/m, '').trim();
|
|
1488
|
+
const synthesis = JSON.parse(cleanedSynth);
|
|
1489
|
+
|
|
1490
|
+
// 3. Store consolidated memory
|
|
1491
|
+
const storeRes = await bbFetch(cfg, `${base}/memories`, {
|
|
1492
|
+
method: 'POST',
|
|
1493
|
+
body: JSON.stringify({
|
|
1494
|
+
content: synthesis.synthesis,
|
|
1495
|
+
type: 'consolidated',
|
|
1496
|
+
agent,
|
|
1497
|
+
importance: storeImportance(synthesis.importance || 7),
|
|
1498
|
+
tags: ['consolidated', `topic:${query}`, ...(synthesis.keyInsights || []).slice(0, 3)],
|
|
1499
|
+
source: 'plugin:consolidation',
|
|
1500
|
+
}),
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
const consolidatedId = storeRes.id || storeRes._key;
|
|
1504
|
+
|
|
1505
|
+
// 4. Create PART_OF edges from originals to consolidated
|
|
1506
|
+
let edgesCreated = 0;
|
|
1507
|
+
for (const mem of memories) {
|
|
1508
|
+
const memId = mem.id || mem._key;
|
|
1509
|
+
if (memId && consolidatedId) {
|
|
1510
|
+
try {
|
|
1511
|
+
await bbFetch(cfg, `${base}/relate`, {
|
|
1512
|
+
method: 'POST',
|
|
1513
|
+
body: JSON.stringify({ from: memId, to: consolidatedId, type: 'PART_OF', weight: 0.8 }),
|
|
1514
|
+
});
|
|
1515
|
+
edgesCreated++;
|
|
1516
|
+
} catch { /* skip individual edge failures */ }
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
let response = `Consolidated ${memories.length} memories into [${consolidatedId}]\n`;
|
|
1521
|
+
response += `Synthesis: ${synthesis.synthesis.substring(0, 200)}...\n`;
|
|
1522
|
+
if (synthesis.keyInsights?.length) {
|
|
1523
|
+
response += `Key insights: ${synthesis.keyInsights.join(', ')}\n`;
|
|
1524
|
+
}
|
|
1525
|
+
response += `Edges created: ${edgesCreated} PART_OF links`;
|
|
1526
|
+
|
|
1527
|
+
return { content: [{ type: 'text', text: response }] };
|
|
1528
|
+
} catch (err: any) {
|
|
1529
|
+
return { content: [{ type: 'text', text: `MemTap consolidation error: ${err.message}` }], isError: true };
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
// ── Tool: memtap_profile ──────────────────────────────────────────────────
|
|
1535
|
+
|
|
1536
|
+
api.registerTool({
|
|
1537
|
+
name: 'memtap_profile',
|
|
1538
|
+
description:
|
|
1539
|
+
'View or refresh the agent memory profile. ' +
|
|
1540
|
+
'Shows a summary of stored knowledge, topic distribution, and memory health.',
|
|
1541
|
+
parameters: {
|
|
1542
|
+
type: 'object',
|
|
1543
|
+
properties: {
|
|
1544
|
+
action: {
|
|
1545
|
+
type: 'string',
|
|
1546
|
+
enum: ['view', 'refresh'],
|
|
1547
|
+
description: 'View the current profile or refresh it from the knowledge graph',
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
required: ['action'],
|
|
1551
|
+
},
|
|
1552
|
+
async execute(_id: string, params: { action: 'view' | 'refresh' }) {
|
|
1553
|
+
const cfg = getConfig(api);
|
|
1554
|
+
const base = baseUrl(cfg);
|
|
1555
|
+
const agent = agentId(cfg, api);
|
|
1556
|
+
|
|
1557
|
+
try {
|
|
1558
|
+
if (params.action === 'view') {
|
|
1559
|
+
const data = await bbFetch(cfg, `${base}/profiles?agent=${encodeURIComponent(agent)}`);
|
|
1560
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1561
|
+
} else {
|
|
1562
|
+
const data = await bbFetch(cfg, `${base}/profiles`, {
|
|
1563
|
+
method: 'POST',
|
|
1564
|
+
body: JSON.stringify({ agent }),
|
|
1565
|
+
});
|
|
1566
|
+
return { content: [{ type: 'text', text: `Profile refreshed.\n${JSON.stringify(data, null, 2)}` }] };
|
|
1567
|
+
}
|
|
1568
|
+
} catch (err: any) {
|
|
1569
|
+
return { content: [{ type: 'text', text: `MemTap profile error: ${err.message}` }], isError: true };
|
|
1570
|
+
}
|
|
1571
|
+
},
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
// ── Tool: memtap_export ───────────────────────────────────────────────────
|
|
1575
|
+
|
|
1576
|
+
api.registerTool({
|
|
1577
|
+
name: 'memtap_export',
|
|
1578
|
+
description:
|
|
1579
|
+
'Export the memory graph in various formats. ' +
|
|
1580
|
+
'Supports JSON (full data), GraphML (for graph visualization tools), and Markdown (human-readable).',
|
|
1581
|
+
parameters: {
|
|
1582
|
+
type: 'object',
|
|
1583
|
+
properties: {
|
|
1584
|
+
format: {
|
|
1585
|
+
type: 'string',
|
|
1586
|
+
enum: ['json', 'graphml', 'markdown'],
|
|
1587
|
+
description: 'Export format',
|
|
1588
|
+
},
|
|
1589
|
+
},
|
|
1590
|
+
required: ['format'],
|
|
1591
|
+
},
|
|
1592
|
+
async execute(_id: string, params: { format: 'json' | 'graphml' | 'markdown' }) {
|
|
1593
|
+
const cfg = getConfig(api);
|
|
1594
|
+
const base = baseUrl(cfg);
|
|
1595
|
+
const agent = agentId(cfg, api);
|
|
1596
|
+
|
|
1597
|
+
try {
|
|
1598
|
+
const url = new URL('/export', base);
|
|
1599
|
+
url.searchParams.set('format', params.format);
|
|
1600
|
+
url.searchParams.set('agent', agent);
|
|
1601
|
+
|
|
1602
|
+
const data = await bbFetch(cfg, url.toString());
|
|
1603
|
+
|
|
1604
|
+
if (params.format === 'json') {
|
|
1605
|
+
const summary = data.memories?.length ?? data.count ?? '?';
|
|
1606
|
+
return { content: [{ type: 'text', text: `Exported ${summary} memories as JSON.\n\n${JSON.stringify(data, null, 2).substring(0, 5000)}` }] };
|
|
1607
|
+
} else if (params.format === 'graphml') {
|
|
1608
|
+
const graphml = typeof data === 'string' ? data : data.graphml || JSON.stringify(data);
|
|
1609
|
+
return { content: [{ type: 'text', text: `GraphML export:\n\n${String(graphml).substring(0, 5000)}` }] };
|
|
1610
|
+
} else {
|
|
1611
|
+
const md = typeof data === 'string' ? data : data.markdown || JSON.stringify(data, null, 2);
|
|
1612
|
+
return { content: [{ type: 'text', text: String(md).substring(0, 5000) }] };
|
|
1613
|
+
}
|
|
1614
|
+
} catch (err: any) {
|
|
1615
|
+
return { content: [{ type: 'text', text: `MemTap export error: ${err.message}` }], isError: true };
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1345
1620
|
// ── Tool: memtap_health (Enhanced Neural Monitoring) ────────────────────────
|
|
1346
1621
|
|
|
1347
1622
|
api.registerTool({
|
|
@@ -1558,26 +1833,54 @@ export default function register(api: any) {
|
|
|
1558
1833
|
}
|
|
1559
1834
|
}
|
|
1560
1835
|
|
|
1836
|
+
// Apply instructions filtering (include/exclude patterns)
|
|
1837
|
+
if (cfg.instructions) {
|
|
1838
|
+
if (cfg.instructions.exclude?.length) {
|
|
1839
|
+
memories = memories.filter((m: any) => {
|
|
1840
|
+
const text = (m.content || '').toLowerCase();
|
|
1841
|
+
return !cfg.instructions!.exclude!.some(pat => text.includes(pat.toLowerCase()));
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
if (cfg.instructions.include?.length) {
|
|
1845
|
+
// Boost memories matching include patterns
|
|
1846
|
+
memories.forEach((m: any) => {
|
|
1847
|
+
const text = (m.content || '').toLowerCase();
|
|
1848
|
+
if (cfg.instructions!.include!.some(pat => text.includes(pat.toLowerCase()))) {
|
|
1849
|
+
m.importance = Math.min(1, (m.importance || 0.5) + 0.1);
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1561
1855
|
if (memories.length > 0) {
|
|
1562
|
-
//
|
|
1856
|
+
// Adaptive Decay: Track access_count and send reinforcement signal
|
|
1563
1857
|
memories.forEach(async (m: any) => {
|
|
1564
1858
|
try {
|
|
1565
1859
|
if (!useCache && m.id) {
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1860
|
+
const accessCount = (m.accessCount || m.retrievalCount || 0) + 1;
|
|
1861
|
+
|
|
1862
|
+
// Adaptive reinforcement: frequently accessed memories decay slower
|
|
1863
|
+
const accessBonus = Math.min(0.3, accessCount * 0.02);
|
|
1864
|
+
const attentionMultiplier = conversationCtx.attentionLevel === 'flow' ? 1.5 :
|
|
1865
|
+
conversationCtx.attentionLevel === 'focused' ? 1.2 : 1.0;
|
|
1866
|
+
const strengthBoost = (FORGETTING_CURVE.retrievalStrengthening + accessBonus) * attentionMultiplier;
|
|
1867
|
+
|
|
1868
|
+
// Send reinforcement signal to API (fire and forget)
|
|
1571
1869
|
bbFetch(cfg, `${baseUrl(cfg)}/memories/${encodeURIComponent(m.id)}/access`, {
|
|
1572
1870
|
method: 'POST',
|
|
1573
|
-
body: JSON.stringify({
|
|
1871
|
+
body: JSON.stringify({
|
|
1574
1872
|
boost: strengthBoost,
|
|
1575
|
-
retrievalCount:
|
|
1873
|
+
retrievalCount: accessCount,
|
|
1576
1874
|
lastAccess: Date.now(),
|
|
1577
|
-
attentionLevel:
|
|
1875
|
+
attentionLevel: conversationCtx.attentionLevel || 'unknown',
|
|
1876
|
+
reinforcement: {
|
|
1877
|
+
accessCount,
|
|
1878
|
+
adaptiveDecayRate: Math.max(0.001, (cfg.decayRate || FORGETTING_CURVE.baseDecayRate) - accessBonus),
|
|
1879
|
+
emotionalProtection: FORGETTING_CURVE.emotionalProtection * attentionMultiplier
|
|
1880
|
+
}
|
|
1578
1881
|
})
|
|
1579
1882
|
}).catch(() => {}); // Silent fail
|
|
1580
|
-
|
|
1883
|
+
|
|
1581
1884
|
// Update local cache retrieval count
|
|
1582
1885
|
if (memoryCache.has(cacheKey)) {
|
|
1583
1886
|
const cached = memoryCache.get(cacheKey)!;
|
|
@@ -1632,6 +1935,67 @@ ${memoryContext}
|
|
|
1632
1935
|
|
|
1633
1936
|
injection += instructions;
|
|
1634
1937
|
|
|
1938
|
+
// ── Proactive Surfacing ──────────────────────────────────────
|
|
1939
|
+
// Search for thematically related memories NOT already in the recall set
|
|
1940
|
+
try {
|
|
1941
|
+
const existingIds = new Set(memories.map((m: any) => m.id || m._key).filter(Boolean));
|
|
1942
|
+
const proactiveTopics = predictiveTopicBoost(conversationCtx, recallLevel.topics)
|
|
1943
|
+
.filter(t => !recallLevel.topics.includes(t));
|
|
1944
|
+
|
|
1945
|
+
if (proactiveTopics.length > 0) {
|
|
1946
|
+
const proactiveQuery = proactiveTopics.join(' ');
|
|
1947
|
+
const proactiveData = await bbFetch(cfg, `${baseUrl(cfg)}/recall?q=${encodeURIComponent(proactiveQuery)}&limit=5&agent=${currentAgent}`)
|
|
1948
|
+
.catch(() => ({ results: [] }));
|
|
1949
|
+
const proactiveResults = (proactiveData.results || proactiveData.memories || [])
|
|
1950
|
+
.filter((m: any) => !existingIds.has(m.id || m._key))
|
|
1951
|
+
.slice(0, 3);
|
|
1952
|
+
|
|
1953
|
+
if (proactiveResults.length > 0) {
|
|
1954
|
+
const proactiveContext = proactiveResults.map((m: any, i: number) =>
|
|
1955
|
+
`${i + 1}. [${m.type}] ${m.content}\n └─ Importance: ${displayImportance(m.importance)}/10`
|
|
1956
|
+
).join('\n\n');
|
|
1957
|
+
|
|
1958
|
+
injection += `\n\n## 💡 You might also want to know:\n${proactiveContext}\n`;
|
|
1959
|
+
|
|
1960
|
+
// Track which memories were proactively surfaced for feedback loop
|
|
1961
|
+
const surfacedIds = proactiveResults.map((m: any) => m.id || m._key).filter(Boolean);
|
|
1962
|
+
proactiveSurfacedMemories.set(currentAgent, {
|
|
1963
|
+
memoryIds: surfacedIds,
|
|
1964
|
+
timestamp: Date.now()
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
} catch { /* proactive surfacing failed, not critical */ }
|
|
1969
|
+
|
|
1970
|
+
// ── Decision Outcome Context (Learning Loop) ─────────────────
|
|
1971
|
+
// If the message is decision-relevant, load past decision outcomes
|
|
1972
|
+
try {
|
|
1973
|
+
if (/\b(entscheidung|decision|decide|option|alternative|choose|wahl)\b/i.test(message)) {
|
|
1974
|
+
const outcomeData = await bbFetch(cfg, `${baseUrl(cfg)}/recall?q=${encodeURIComponent('decision outcome result')}&limit=10&agent=${currentAgent}&types=outcome,decision`)
|
|
1975
|
+
.catch(() => ({ results: [] }));
|
|
1976
|
+
const outcomes = (outcomeData.results || outcomeData.memories || [])
|
|
1977
|
+
.filter((m: any) => m.type === 'outcome' || (m.type === 'decision' && m.tags?.includes('has-outcome')));
|
|
1978
|
+
|
|
1979
|
+
if (outcomes.length > 0) {
|
|
1980
|
+
// Summarize past decision outcomes
|
|
1981
|
+
const successCount = outcomes.filter((m: any) => m.tags?.includes('outcome:success')).length;
|
|
1982
|
+
const failCount = outcomes.filter((m: any) => m.tags?.includes('outcome:failure')).length;
|
|
1983
|
+
|
|
1984
|
+
let outcomeContext = '\n\n## 📊 Based on past decisions:\n';
|
|
1985
|
+
if (successCount + failCount > 0) {
|
|
1986
|
+
outcomeContext += `Track record: ${successCount} successful, ${failCount} unsuccessful outcomes recorded.\n\n`;
|
|
1987
|
+
}
|
|
1988
|
+
outcomeContext += outcomes.slice(0, 4).map((m: any, i: number) => {
|
|
1989
|
+
const success = m.tags?.includes('outcome:success') ? '✅' : m.tags?.includes('outcome:failure') ? '❌' : '📝';
|
|
1990
|
+
return `${i + 1}. ${success} ${m.content}\n └─ Importance: ${displayImportance(m.importance)}/10`;
|
|
1991
|
+
}).join('\n\n');
|
|
1992
|
+
|
|
1993
|
+
outcomeContext += '\n\n*Consider these past outcomes when advising on the current decision.*\n';
|
|
1994
|
+
injection += outcomeContext;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
} catch { /* decision outcome loading failed, not critical */ }
|
|
1998
|
+
|
|
1635
1999
|
// Append to existing system prompt
|
|
1636
2000
|
event.context.systemPrompt = (event.context.systemPrompt || '') + injection;
|
|
1637
2001
|
|
|
@@ -1706,22 +2070,31 @@ ${memoryContext}
|
|
|
1706
2070
|
|
|
1707
2071
|
const finalImportance = (mem.importance ?? 5) * emotionalWeight * contextualWeight;
|
|
1708
2072
|
|
|
2073
|
+
// Determine category (from LLM extraction or fallback)
|
|
2074
|
+
const memCategory = MEMORY_CATEGORIES.includes(mem.category as any)
|
|
2075
|
+
? mem.category
|
|
2076
|
+
: (memoryType === 'preference' ? 'preferences' :
|
|
2077
|
+
memoryType === 'decision' || memoryType === 'goal' || memoryType === 'task' ? 'project' :
|
|
2078
|
+
'technical');
|
|
2079
|
+
|
|
1709
2080
|
// Enhanced memory with neuromimetic features
|
|
1710
|
-
const enhancedMem = {
|
|
2081
|
+
const enhancedMem: Record<string, any> = {
|
|
1711
2082
|
content: mem.content,
|
|
1712
2083
|
type: memoryType,
|
|
1713
2084
|
agent: currentAgent,
|
|
1714
2085
|
importance: storeImportance(Math.min(10, finalImportance)),
|
|
2086
|
+
category: memCategory,
|
|
1715
2087
|
tags: [
|
|
1716
|
-
...(mem.tags || []),
|
|
2088
|
+
...(mem.tags || []),
|
|
1717
2089
|
'auto-captured',
|
|
1718
2090
|
'neuromimetic',
|
|
2091
|
+
`category:${memCategory}`,
|
|
1719
2092
|
...(context?.dominantTopic ? [`topic:${context.dominantTopic}`] : []),
|
|
1720
2093
|
`engagement:${context?.userEngagement || 'unknown'}`,
|
|
1721
2094
|
`attention:${context?.attentionLevel || 'unknown'}`,
|
|
1722
2095
|
`emotion:${context?.emotionalContext || 'neutral'}`
|
|
1723
2096
|
],
|
|
1724
|
-
source: 'plugin:neuromimetic-capture-
|
|
2097
|
+
source: 'plugin:neuromimetic-capture-v3.0',
|
|
1725
2098
|
conversationContext: {
|
|
1726
2099
|
dominantTopic: context?.dominantTopic,
|
|
1727
2100
|
engagement: context?.userEngagement,
|
|
@@ -1778,11 +2151,70 @@ ${memoryContext}
|
|
|
1778
2151
|
const profile = getUserProfile(currentAgent);
|
|
1779
2152
|
profile.successfulRecalls += stored; // Treat captures as successes
|
|
1780
2153
|
userProfiles.set(currentAgent, profile);
|
|
1781
|
-
|
|
2154
|
+
|
|
1782
2155
|
logger.info?.(`[memtap] Auto-captured ${stored} enhanced memories with context`) ??
|
|
1783
2156
|
console.log(`[memtap] Auto-captured ${stored} enhanced memories with context`);
|
|
1784
2157
|
}
|
|
1785
|
-
|
|
2158
|
+
|
|
2159
|
+
// ── Proactive Surfacing Feedback Loop ──────────────────────
|
|
2160
|
+
// Check if the agent's response referenced any proactively surfaced memories
|
|
2161
|
+
try {
|
|
2162
|
+
const surfaced = proactiveSurfacedMemories.get(currentAgent);
|
|
2163
|
+
if (surfaced && (Date.now() - surfaced.timestamp) < 600000) { // within 10min
|
|
2164
|
+
const contentLower = content.toLowerCase();
|
|
2165
|
+
const usedIds: string[] = [];
|
|
2166
|
+
|
|
2167
|
+
for (const memId of surfaced.memoryIds) {
|
|
2168
|
+
// Check if the memory ID or content from proactive surfacing appears in the response
|
|
2169
|
+
if (contentLower.includes(memId.toLowerCase())) {
|
|
2170
|
+
usedIds.push(memId);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// Also do a fuzzy check: recall proactive memories and see if their content appears
|
|
2175
|
+
if (usedIds.length === 0 && surfaced.memoryIds.length > 0) {
|
|
2176
|
+
for (const memId of surfaced.memoryIds) {
|
|
2177
|
+
try {
|
|
2178
|
+
const memData = await bbFetch(cfg, `${baseUrl(cfg)}/memories/${encodeURIComponent(memId)}`);
|
|
2179
|
+
const memContent = (memData.content || '').toLowerCase();
|
|
2180
|
+
// Check if key phrases from the memory appear in the response
|
|
2181
|
+
const keyPhrases = memContent.split(/\s+/).filter((w: string) => w.length > 5).slice(0, 5);
|
|
2182
|
+
const matchCount = keyPhrases.filter((phrase: string) => contentLower.includes(phrase)).length;
|
|
2183
|
+
if (matchCount >= 2) {
|
|
2184
|
+
usedIds.push(memId);
|
|
2185
|
+
}
|
|
2186
|
+
} catch { /* skip */ }
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// Importance reinforcement for used proactive memories
|
|
2191
|
+
if (usedIds.length > 0) {
|
|
2192
|
+
for (const memId of usedIds) {
|
|
2193
|
+
try {
|
|
2194
|
+
await bbFetch(cfg, `${baseUrl(cfg)}/memories/${encodeURIComponent(memId)}/access`, {
|
|
2195
|
+
method: 'POST',
|
|
2196
|
+
body: JSON.stringify({
|
|
2197
|
+
boost: FORGETTING_CURVE.retrievalStrengthening * 1.5, // Extra boost for proactive hit
|
|
2198
|
+
lastAccess: Date.now(),
|
|
2199
|
+
attentionLevel: 'proactive-hit',
|
|
2200
|
+
reinforcement: {
|
|
2201
|
+
proactiveHit: true,
|
|
2202
|
+
adaptiveDecayRate: Math.max(0.001, (cfg.decayRate || FORGETTING_CURVE.baseDecayRate) * 0.5)
|
|
2203
|
+
}
|
|
2204
|
+
})
|
|
2205
|
+
});
|
|
2206
|
+
} catch { /* reinforcement failed, not critical */ }
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
logger.info?.(`[memtap] Proactive surfacing feedback: ${usedIds.length}/${surfaced.memoryIds.length} memories used by agent`) ??
|
|
2210
|
+
console.log(`[memtap] Proactive surfacing feedback: ${usedIds.length}/${surfaced.memoryIds.length} memories used by agent`);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// Clear after processing
|
|
2214
|
+
proactiveSurfacedMemories.delete(currentAgent);
|
|
2215
|
+
}
|
|
2216
|
+
} catch { /* proactive feedback loop failed, not critical */ }
|
|
2217
|
+
|
|
1786
2218
|
} catch (err: any) {
|
|
1787
2219
|
logger.warn?.(`[memtap] Auto-capture failed: ${err.message}`) ??
|
|
1788
2220
|
console.warn(`[memtap] Auto-capture failed: ${err.message}`);
|
|
@@ -2811,6 +3243,321 @@ async function neuralMaintenance() {
|
|
|
2811
3243
|
},
|
|
2812
3244
|
});
|
|
2813
3245
|
|
|
2814
|
-
|
|
2815
|
-
|
|
3246
|
+
// ── Tool: memtap_outcome (Agent Learning Loop) ──────────────────────────────
|
|
3247
|
+
|
|
3248
|
+
api.registerTool({
|
|
3249
|
+
name: 'memtap_outcome',
|
|
3250
|
+
description:
|
|
3251
|
+
'Record the outcome of a past decision for the learning loop. ' +
|
|
3252
|
+
'Links the outcome to the original decision via a CAUSED_BY edge. ' +
|
|
3253
|
+
'Future decision contexts will automatically surface past outcomes to improve advice quality.',
|
|
3254
|
+
parameters: {
|
|
3255
|
+
type: 'object',
|
|
3256
|
+
properties: {
|
|
3257
|
+
decisionId: { type: 'string', description: 'The ID of the decision this outcome relates to' },
|
|
3258
|
+
outcome: { type: 'string', description: 'Description of what happened as a result of the decision' },
|
|
3259
|
+
success: { type: 'boolean', description: 'Whether the decision led to a successful outcome' },
|
|
3260
|
+
learnings: { type: 'string', description: 'Optional lessons learned from this outcome' },
|
|
3261
|
+
},
|
|
3262
|
+
required: ['decisionId', 'outcome', 'success'],
|
|
3263
|
+
},
|
|
3264
|
+
async execute(_id: string, params: { decisionId: string; outcome: string; success: boolean; learnings?: string }) {
|
|
3265
|
+
const cfg = getConfig(api);
|
|
3266
|
+
const base = baseUrl(cfg);
|
|
3267
|
+
const agent = agentId(cfg, api);
|
|
3268
|
+
|
|
3269
|
+
try {
|
|
3270
|
+
// 1. Verify the decision exists
|
|
3271
|
+
let decisionContent = '';
|
|
3272
|
+
try {
|
|
3273
|
+
const decisionData = await bbFetch(cfg, `${base}/memories/${encodeURIComponent(params.decisionId)}`);
|
|
3274
|
+
decisionContent = decisionData.content || '';
|
|
3275
|
+
} catch {
|
|
3276
|
+
return { content: [{ type: 'text', text: `Decision [${params.decisionId}] not found.` }], isError: true };
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
// 2. Build outcome memory content
|
|
3280
|
+
const outcomeContent = params.learnings
|
|
3281
|
+
? `Outcome: ${params.outcome} | Learnings: ${params.learnings}`
|
|
3282
|
+
: `Outcome: ${params.outcome}`;
|
|
3283
|
+
|
|
3284
|
+
// 3. Store outcome memory
|
|
3285
|
+
const outcomeImportance = params.success ? 0.6 : 0.8; // Failures are more important to remember
|
|
3286
|
+
const outcomeMem = await bbFetch(cfg, `${base}/memories`, {
|
|
3287
|
+
method: 'POST',
|
|
3288
|
+
body: JSON.stringify({
|
|
3289
|
+
content: outcomeContent,
|
|
3290
|
+
type: 'outcome',
|
|
3291
|
+
agent,
|
|
3292
|
+
importance: outcomeImportance,
|
|
3293
|
+
tags: [
|
|
3294
|
+
'outcome',
|
|
3295
|
+
`outcome:${params.success ? 'success' : 'failure'}`,
|
|
3296
|
+
`decision:${params.decisionId}`,
|
|
3297
|
+
...(params.learnings ? ['has-learnings'] : []),
|
|
3298
|
+
],
|
|
3299
|
+
source: 'plugin:learning-loop',
|
|
3300
|
+
}),
|
|
3301
|
+
});
|
|
3302
|
+
|
|
3303
|
+
const outcomeId = outcomeMem.id || outcomeMem._key;
|
|
3304
|
+
|
|
3305
|
+
// 4. Create CAUSED_BY edge from outcome to decision
|
|
3306
|
+
await bbFetch(cfg, `${base}/relate`, {
|
|
3307
|
+
method: 'POST',
|
|
3308
|
+
body: JSON.stringify({
|
|
3309
|
+
from: outcomeId,
|
|
3310
|
+
to: params.decisionId,
|
|
3311
|
+
type: 'CAUSED_BY',
|
|
3312
|
+
weight: 0.9,
|
|
3313
|
+
}),
|
|
3314
|
+
});
|
|
3315
|
+
|
|
3316
|
+
// 5. Tag the original decision as having an outcome
|
|
3317
|
+
try {
|
|
3318
|
+
await bbFetch(cfg, `${base}/memories/${encodeURIComponent(params.decisionId)}/update`, {
|
|
3319
|
+
method: 'POST',
|
|
3320
|
+
body: JSON.stringify({
|
|
3321
|
+
tags: ['has-outcome', `outcome:${params.success ? 'success' : 'failure'}`],
|
|
3322
|
+
}),
|
|
3323
|
+
});
|
|
3324
|
+
} catch { /* tagging failed, not critical */ }
|
|
3325
|
+
|
|
3326
|
+
const icon = params.success ? '✅' : '❌';
|
|
3327
|
+
let response = `${icon} Outcome recorded [${outcomeId}] for decision [${params.decisionId}]\n`;
|
|
3328
|
+
response += ` Result: ${params.success ? 'Success' : 'Failure'}\n`;
|
|
3329
|
+
response += ` Edge: [${outcomeId}] -[CAUSED_BY]-> [${params.decisionId}]`;
|
|
3330
|
+
if (params.learnings) response += `\n Learnings: ${params.learnings}`;
|
|
3331
|
+
|
|
3332
|
+
return { content: [{ type: 'text', text: response }] };
|
|
3333
|
+
} catch (err: any) {
|
|
3334
|
+
return { content: [{ type: 'text', text: `MemTap outcome error: ${err.message}` }], isError: true };
|
|
3335
|
+
}
|
|
3336
|
+
},
|
|
3337
|
+
});
|
|
3338
|
+
|
|
3339
|
+
// ── Tool: memtap_infer (Inference Engine) ─────────────────────────────────
|
|
3340
|
+
|
|
3341
|
+
api.registerTool({
|
|
3342
|
+
name: 'memtap_infer',
|
|
3343
|
+
description:
|
|
3344
|
+
'Inference engine: traverses the knowledge graph (2-3 hops) to discover implicit relationships ' +
|
|
3345
|
+
'and hidden knowledge not explicitly stored. Uses LLM reasoning to draw conclusions from connected facts. ' +
|
|
3346
|
+
'Stores inferences as new memories with confidence decay (inferred memories decay 2x faster).',
|
|
3347
|
+
parameters: {
|
|
3348
|
+
type: 'object',
|
|
3349
|
+
properties: {
|
|
3350
|
+
depth: { type: 'number', description: 'Graph traversal depth in hops (default 2, max 3)' },
|
|
3351
|
+
limit: { type: 'number', description: 'Max number of inferences to generate (default 3)' },
|
|
3352
|
+
},
|
|
3353
|
+
},
|
|
3354
|
+
async execute(_id: string, params: { depth?: number; limit?: number }) {
|
|
3355
|
+
const cfg = getConfig(api);
|
|
3356
|
+
const base = baseUrl(cfg);
|
|
3357
|
+
const agent = agentId(cfg, api);
|
|
3358
|
+
const depth = Math.min(3, params.depth ?? 2);
|
|
3359
|
+
const limit = Math.min(5, params.limit ?? 3);
|
|
3360
|
+
|
|
3361
|
+
try {
|
|
3362
|
+
// 1. Get graph overview to find highly connected nodes as starting points
|
|
3363
|
+
const overviewData = await bbFetch(cfg, `${base}/graph/overview`).catch(() => ({}));
|
|
3364
|
+
const topNodes = (overviewData.topConnected || overviewData.hubs || []).slice(0, 5);
|
|
3365
|
+
|
|
3366
|
+
// 2. If no top nodes from overview, use recent important memories as seeds
|
|
3367
|
+
let seedIds: string[] = topNodes.map((n: any) => n.id || n._key).filter(Boolean);
|
|
3368
|
+
if (seedIds.length === 0) {
|
|
3369
|
+
const recallData = await bbFetch(cfg, `${base}/recall?q=important&limit=5&agent=${agent}`);
|
|
3370
|
+
seedIds = (recallData.results || recallData.memories || [])
|
|
3371
|
+
.map((m: any) => m.id || m._key).filter(Boolean);
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
if (seedIds.length === 0) {
|
|
3375
|
+
return { content: [{ type: 'text', text: 'Not enough memories in the graph to run inference. Store more memories first.' }] };
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
// 3. Traverse graph from each seed to collect connected memories
|
|
3379
|
+
const allTraversals: any[] = [];
|
|
3380
|
+
const visitedMemories = new Map<string, any>();
|
|
3381
|
+
|
|
3382
|
+
for (const seedId of seedIds.slice(0, 3)) {
|
|
3383
|
+
try {
|
|
3384
|
+
const traverseUrl = new URL('/graph/traverse', base);
|
|
3385
|
+
traverseUrl.searchParams.set('start', seedId);
|
|
3386
|
+
traverseUrl.searchParams.set('depth', String(depth));
|
|
3387
|
+
const traverseData = await bbFetch(cfg, traverseUrl.toString());
|
|
3388
|
+
|
|
3389
|
+
const nodes = traverseData.nodes || traverseData.results || [];
|
|
3390
|
+
for (const node of nodes) {
|
|
3391
|
+
const nodeId = node.id || node._key;
|
|
3392
|
+
if (nodeId && !visitedMemories.has(nodeId)) {
|
|
3393
|
+
visitedMemories.set(nodeId, node);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
allTraversals.push({
|
|
3398
|
+
seed: seedId,
|
|
3399
|
+
nodes: nodes.length,
|
|
3400
|
+
edges: (traverseData.edges || []).length,
|
|
3401
|
+
});
|
|
3402
|
+
} catch { /* skip individual traversal failures */ }
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
if (visitedMemories.size < 3) {
|
|
3406
|
+
return { content: [{ type: 'text', text: `Only ${visitedMemories.size} connected memories found. Need at least 3 for meaningful inference.` }] };
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// 4. Build context for LLM inference
|
|
3410
|
+
const memoryTexts = Array.from(visitedMemories.values())
|
|
3411
|
+
.slice(0, 20)
|
|
3412
|
+
.map((m: any, i: number) => {
|
|
3413
|
+
const mType = m.type || 'unknown';
|
|
3414
|
+
const mContent = m.content || m.summary || '';
|
|
3415
|
+
return `${i + 1}. [${mType}] ${mContent}`;
|
|
3416
|
+
}).join('\n');
|
|
3417
|
+
|
|
3418
|
+
// 5. Use LLM to find implicit connections and draw inferences
|
|
3419
|
+
const llmUrl = cfg.llmUrl || 'http://127.0.0.1:18789/v1/chat/completions';
|
|
3420
|
+
const model = cfg.llmModel || 'anthropic/claude-sonnet-4-20250514';
|
|
3421
|
+
|
|
3422
|
+
const inferRes = await fetch(llmUrl, {
|
|
3423
|
+
method: 'POST',
|
|
3424
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3425
|
+
body: JSON.stringify({
|
|
3426
|
+
model,
|
|
3427
|
+
max_tokens: 2000,
|
|
3428
|
+
messages: [
|
|
3429
|
+
{
|
|
3430
|
+
role: 'system',
|
|
3431
|
+
content: `Du bist ein Inference-Engine für einen Wissensgraphen. Analysiere die folgenden verbundenen Fakten und finde IMPLIZITE Schlussfolgerungen die nicht direkt gespeichert sind.
|
|
3432
|
+
|
|
3433
|
+
Regeln:
|
|
3434
|
+
- Nur nicht-triviale, wertvolle Schlüsse
|
|
3435
|
+
- Jeder Schluss muss auf mindestens 2 konkreten Fakten basieren
|
|
3436
|
+
- Gib die IDs der Quell-Memories an (Nummern aus der Liste)
|
|
3437
|
+
- Confidence 0-1: wie sicher ist der Schluss?
|
|
3438
|
+
|
|
3439
|
+
Antwort als JSON-Array (NUR das Array):
|
|
3440
|
+
[
|
|
3441
|
+
{
|
|
3442
|
+
"inference": "Der Schluss in einem Satz",
|
|
3443
|
+
"basedOn": [1, 3, 7],
|
|
3444
|
+
"confidence": 0.7,
|
|
3445
|
+
"category": "pattern|prediction|connection|risk"
|
|
3446
|
+
}
|
|
3447
|
+
]
|
|
3448
|
+
|
|
3449
|
+
Wenn keine sinnvollen Schlüsse möglich: []`
|
|
3450
|
+
},
|
|
3451
|
+
{
|
|
3452
|
+
role: 'user',
|
|
3453
|
+
content: `Welche impliziten Schlüsse kann man aus diesen ${visitedMemories.size} verbundenen Fakten ziehen?\n\n${memoryTexts}`
|
|
3454
|
+
},
|
|
3455
|
+
],
|
|
3456
|
+
}),
|
|
3457
|
+
});
|
|
3458
|
+
|
|
3459
|
+
if (!inferRes.ok) throw new Error(`LLM inference failed: ${inferRes.status}`);
|
|
3460
|
+
const inferData = await inferRes.json();
|
|
3461
|
+
const inferText = inferData.choices?.[0]?.message?.content?.trim() || '[]';
|
|
3462
|
+
const cleanedInfer = inferText.replace(/^```json?\n?/m, '').replace(/\n?```$/m, '').trim();
|
|
3463
|
+
|
|
3464
|
+
let inferences: any[];
|
|
3465
|
+
try {
|
|
3466
|
+
inferences = JSON.parse(cleanedInfer);
|
|
3467
|
+
if (!Array.isArray(inferences)) inferences = [];
|
|
3468
|
+
} catch {
|
|
3469
|
+
inferences = [];
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
if (inferences.length === 0) {
|
|
3473
|
+
return { content: [{ type: 'text', text: `Traversed ${visitedMemories.size} memories across ${allTraversals.length} paths but no meaningful inferences could be drawn.` }] };
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// 6. Store inferences as memories with lower importance and 2x decay
|
|
3477
|
+
const storedInferences: string[] = [];
|
|
3478
|
+
const memoryArray = Array.from(visitedMemories.values());
|
|
3479
|
+
|
|
3480
|
+
for (const inf of inferences.slice(0, limit)) {
|
|
3481
|
+
try {
|
|
3482
|
+
// Calculate importance based on confidence (lower than explicit memories)
|
|
3483
|
+
const inferImportance = Math.max(0.2, Math.min(0.6, (inf.confidence || 0.5) * 0.7));
|
|
3484
|
+
|
|
3485
|
+
const inferMem = await bbFetch(cfg, `${base}/memories`, {
|
|
3486
|
+
method: 'POST',
|
|
3487
|
+
body: JSON.stringify({
|
|
3488
|
+
content: inf.inference,
|
|
3489
|
+
type: 'inferred',
|
|
3490
|
+
agent,
|
|
3491
|
+
importance: inferImportance,
|
|
3492
|
+
tags: [
|
|
3493
|
+
'inferred',
|
|
3494
|
+
`inference:${inf.category || 'connection'}`,
|
|
3495
|
+
`confidence:${Math.round((inf.confidence || 0.5) * 100)}`,
|
|
3496
|
+
'fast-decay', // Signal to server that this decays 2x faster
|
|
3497
|
+
],
|
|
3498
|
+
source: 'plugin:inference-engine',
|
|
3499
|
+
metadata: {
|
|
3500
|
+
confidence: inf.confidence || 0.5,
|
|
3501
|
+
decayMultiplier: 2.0, // Inferred memories decay 2x faster
|
|
3502
|
+
basedOnCount: (inf.basedOn || []).length,
|
|
3503
|
+
inferenceCategory: inf.category || 'connection',
|
|
3504
|
+
},
|
|
3505
|
+
}),
|
|
3506
|
+
});
|
|
3507
|
+
|
|
3508
|
+
const inferenceId = inferMem.id || inferMem._key;
|
|
3509
|
+
|
|
3510
|
+
// 7. Create PART_OF edges (inference chain) from source memories to inference
|
|
3511
|
+
const basedOnIndices = (inf.basedOn || []).map((n: number) => n - 1);
|
|
3512
|
+
let edgesCreated = 0;
|
|
3513
|
+
|
|
3514
|
+
for (const idx of basedOnIndices) {
|
|
3515
|
+
if (idx >= 0 && idx < memoryArray.length) {
|
|
3516
|
+
const sourceId = memoryArray[idx].id || memoryArray[idx]._key;
|
|
3517
|
+
if (sourceId && inferenceId) {
|
|
3518
|
+
try {
|
|
3519
|
+
await bbFetch(cfg, `${base}/relate`, {
|
|
3520
|
+
method: 'POST',
|
|
3521
|
+
body: JSON.stringify({
|
|
3522
|
+
from: sourceId,
|
|
3523
|
+
to: inferenceId,
|
|
3524
|
+
type: 'PART_OF',
|
|
3525
|
+
weight: inf.confidence || 0.5,
|
|
3526
|
+
}),
|
|
3527
|
+
});
|
|
3528
|
+
edgesCreated++;
|
|
3529
|
+
} catch { /* skip edge failures */ }
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
storedInferences.push(
|
|
3535
|
+
`[${inferenceId}] ${inf.inference}\n ` +
|
|
3536
|
+
`Confidence: ${Math.round((inf.confidence || 0.5) * 100)}% | ` +
|
|
3537
|
+
`Category: ${inf.category || 'connection'} | ` +
|
|
3538
|
+
`Based on: ${(inf.basedOn || []).length} memories | ` +
|
|
3539
|
+
`Edges: ${edgesCreated} PART_OF links`
|
|
3540
|
+
);
|
|
3541
|
+
} catch { /* skip individual inference storage failures */ }
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
let output = `Inference Engine Results\n`;
|
|
3545
|
+
output += `Traversed: ${visitedMemories.size} memories, ${allTraversals.length} paths, depth ${depth}\n\n`;
|
|
3546
|
+
|
|
3547
|
+
if (storedInferences.length > 0) {
|
|
3548
|
+
output += `Inferences discovered and stored (decay: 2x faster):\n\n`;
|
|
3549
|
+
output += storedInferences.map((s, i) => `${i + 1}. ${s}`).join('\n\n');
|
|
3550
|
+
} else {
|
|
3551
|
+
output += 'Inferences were generated but could not be stored.';
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
return { content: [{ type: 'text', text: output }] };
|
|
3555
|
+
} catch (err: any) {
|
|
3556
|
+
return { content: [{ type: 'text', text: `MemTap inference error: ${err.message}` }], isError: true };
|
|
3557
|
+
}
|
|
3558
|
+
},
|
|
3559
|
+
});
|
|
3560
|
+
|
|
3561
|
+
logger.info?.('[memtap] Plugin v3.1.0 "The Oracle" registered: 18 tools + 5 neuromimetic hooks') ??
|
|
3562
|
+
console.log('[memtap] Plugin v3.1.0 "The Oracle" registered: 18 tools + 5 neuromimetic hooks');
|
|
2816
3563
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"id": "memtap",
|
|
3
3
|
"name": "MemTap",
|
|
4
4
|
"kind": "memory",
|
|
5
|
-
"version": "
|
|
6
|
-
"description": "Graph-based long-term memory for OpenClaw agents — semantic recall, GraphRAG, entity management, decision tracking, neural auto-capture, and
|
|
5
|
+
"version": "3.1.0",
|
|
6
|
+
"description": "Graph-based long-term memory for OpenClaw agents — semantic recall, GraphRAG, entity management, decision tracking, neural auto-capture, anomaly detection, consolidation, profiles, and adaptive decay.",
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
|
@@ -60,6 +60,22 @@
|
|
|
60
60
|
"decayRate": {
|
|
61
61
|
"type": "number",
|
|
62
62
|
"description": "Memory importance decay rate per day (default: 0.005)"
|
|
63
|
+
},
|
|
64
|
+
"instructions": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"description": "Content filtering instructions for memory recall and capture",
|
|
67
|
+
"properties": {
|
|
68
|
+
"include": {
|
|
69
|
+
"type": "array",
|
|
70
|
+
"items": { "type": "string" },
|
|
71
|
+
"description": "Patterns to prioritize in recall (boost matching memories)"
|
|
72
|
+
},
|
|
73
|
+
"exclude": {
|
|
74
|
+
"type": "array",
|
|
75
|
+
"items": { "type": "string" },
|
|
76
|
+
"description": "Patterns to exclude from recall results"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
},
|
|
@@ -110,6 +126,10 @@
|
|
|
110
126
|
"decayRate": {
|
|
111
127
|
"label": "Memory Decay Rate",
|
|
112
128
|
"placeholder": "0.005"
|
|
129
|
+
},
|
|
130
|
+
"instructions": {
|
|
131
|
+
"label": "Content Filter Instructions",
|
|
132
|
+
"helpText": "Include/exclude patterns for memory recall filtering"
|
|
113
133
|
}
|
|
114
134
|
}
|
|
115
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memtap",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "MemTap — Graph-based long-term memory plugin for OpenClaw agents. Knowledge graph with semantic recall, GraphRAG, entity management, decision tracking, and auto-capture.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 psifactory LLC
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|