vesper-memory 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -3
- package/LICENSE +59 -0
- package/README.md +420 -23
- package/config/migrations/001_add_skill_summary.sql +75 -0
- package/config/migrations/001_rollback_skill_summary.sql +60 -0
- package/config/sqlite-schema.sql +68 -4
- package/dist/benchmark/types.d.ts +4 -4
- package/dist/cli.js +0 -0
- package/dist/consolidation/pipeline.d.ts +6 -1
- package/dist/consolidation/pipeline.d.ts.map +1 -1
- package/dist/consolidation/pipeline.js +53 -35
- package/dist/consolidation/pipeline.js.map +1 -1
- package/dist/memory-layers/semantic-memory.d.ts +9 -19
- package/dist/memory-layers/semantic-memory.d.ts.map +1 -1
- package/dist/memory-layers/semantic-memory.js +31 -54
- package/dist/memory-layers/semantic-memory.js.map +1 -1
- package/dist/memory-layers/skill-lazy-loading.d.ts +160 -0
- package/dist/memory-layers/skill-lazy-loading.d.ts.map +1 -0
- package/dist/memory-layers/skill-lazy-loading.js +247 -0
- package/dist/memory-layers/skill-lazy-loading.js.map +1 -0
- package/dist/memory-layers/skill-library.d.ts +133 -4
- package/dist/memory-layers/skill-library.d.ts.map +1 -1
- package/dist/memory-layers/skill-library.js +427 -8
- package/dist/memory-layers/skill-library.js.map +1 -1
- package/dist/memory-layers/working-memory.d.ts +35 -14
- package/dist/memory-layers/working-memory.d.ts.map +1 -1
- package/dist/memory-layers/working-memory.js +114 -36
- package/dist/memory-layers/working-memory.js.map +1 -1
- package/dist/retrieval/hybrid-search.d.ts +2 -2
- package/dist/retrieval/hybrid-search.d.ts.map +1 -1
- package/dist/retrieval/hybrid-search.js +4 -3
- package/dist/retrieval/hybrid-search.js.map +1 -1
- package/dist/router/smart-router.d.ts +25 -0
- package/dist/router/smart-router.d.ts.map +1 -1
- package/dist/router/smart-router.js +90 -10
- package/dist/router/smart-router.js.map +1 -1
- package/dist/scheduler/consolidation-scheduler.d.ts +129 -0
- package/dist/scheduler/consolidation-scheduler.d.ts.map +1 -0
- package/dist/scheduler/consolidation-scheduler.js +179 -0
- package/dist/scheduler/consolidation-scheduler.js.map +1 -0
- package/dist/scheduler/index.d.ts +7 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +7 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server-wrapper.js +0 -0
- package/dist/server.js +879 -28
- package/dist/server.js.map +1 -1
- package/dist/synthesis/conflict-detector.d.ts +4 -6
- package/dist/synthesis/conflict-detector.d.ts.map +1 -1
- package/dist/synthesis/conflict-detector.js +19 -21
- package/dist/synthesis/conflict-detector.js.map +1 -1
- package/dist/utils/validation.d.ts +123 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +146 -7
- package/dist/utils/validation.js.map +1 -1
- package/docker-compose.yml +6 -6
- package/embedding-service/README.md +8 -0
- package/package.json +4 -2
- package/scripts/FRESH_INSTALL.md +1 -1
- package/scripts/ensure-infrastructure.sh +86 -26
- package/scripts/health-check.sh +2 -1
- package/scripts/stop-infrastructure.sh +23 -0
- package/scripts/test-mcp-server.sh +2 -1
package/dist/server.js
CHANGED
|
@@ -16,11 +16,14 @@ import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode, } f
|
|
|
16
16
|
import Redis from "ioredis";
|
|
17
17
|
import { createRequire } from "module";
|
|
18
18
|
import { randomUUID } from "crypto";
|
|
19
|
-
import { validateInput, StoreMemoryInputSchema, RetrieveMemoryInputSchema, ListRecentInputSchema, GetStatsInputSchema, } from "./utils/validation.js";
|
|
19
|
+
import { validateInput, StoreMemoryInputSchema, RetrieveMemoryInputSchema, ListRecentInputSchema, GetStatsInputSchema, RecordSkillOutcomeInputSchema, LoadSkillInputSchema, ShareContextInputSchema, StoreDecisionInputSchema, NamespaceStatsInputSchema, } from "./utils/validation.js";
|
|
20
20
|
import { getSqlitePath, ensureDirectories } from "./utils/paths.js";
|
|
21
21
|
import { createEmbeddingClient } from "./embeddings/client.js";
|
|
22
22
|
import { HybridSearchEngine } from "./retrieval/hybrid-search.js";
|
|
23
23
|
import { WorkingMemoryLayer } from "./memory-layers/working-memory.js";
|
|
24
|
+
import { SemanticMemoryLayer } from "./memory-layers/semantic-memory.js";
|
|
25
|
+
import { SkillLibrary } from "./memory-layers/skill-library.js";
|
|
26
|
+
import { ConsolidationPipeline } from "./consolidation/pipeline.js";
|
|
24
27
|
import * as SmartRouter from "./router/smart-router.js";
|
|
25
28
|
// Import better-sqlite3 constructor using createRequire
|
|
26
29
|
const require = createRequire(import.meta.url);
|
|
@@ -34,6 +37,10 @@ let vesperEnabled = true;
|
|
|
34
37
|
/**
|
|
35
38
|
* Tool definitions for the MCP server
|
|
36
39
|
*/
|
|
40
|
+
const NAMESPACE_PROP = {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Namespace for multi-agent isolation (default: 'default')",
|
|
43
|
+
};
|
|
37
44
|
const TOOLS = [
|
|
38
45
|
{
|
|
39
46
|
name: "store_memory",
|
|
@@ -47,7 +54,7 @@ const TOOLS = [
|
|
|
47
54
|
},
|
|
48
55
|
memory_type: {
|
|
49
56
|
type: "string",
|
|
50
|
-
enum: ["episodic", "semantic", "procedural"],
|
|
57
|
+
enum: ["episodic", "semantic", "procedural", "decision"],
|
|
51
58
|
description: "Type of memory being stored",
|
|
52
59
|
},
|
|
53
60
|
metadata: {
|
|
@@ -55,6 +62,10 @@ const TOOLS = [
|
|
|
55
62
|
description: "Additional metadata (timestamp, source, tags, etc.)",
|
|
56
63
|
additionalProperties: true,
|
|
57
64
|
},
|
|
65
|
+
namespace: NAMESPACE_PROP,
|
|
66
|
+
agent_id: { type: "string", description: "ID of the agent storing this memory" },
|
|
67
|
+
agent_role: { type: "string", description: "Role of the agent (e.g., 'orchestrator', 'code-reviewer')" },
|
|
68
|
+
task_id: { type: "string", description: "Task ID this memory is associated with" },
|
|
58
69
|
},
|
|
59
70
|
required: ["content", "memory_type"],
|
|
60
71
|
},
|
|
@@ -72,7 +83,7 @@ const TOOLS = [
|
|
|
72
83
|
memory_types: {
|
|
73
84
|
type: "array",
|
|
74
85
|
items: { type: "string" },
|
|
75
|
-
description: "Filter by memory types (episodic, semantic, procedural). Omit for all types.",
|
|
86
|
+
description: "Filter by memory types (episodic, semantic, procedural, decision). Omit for all types.",
|
|
76
87
|
},
|
|
77
88
|
max_results: {
|
|
78
89
|
type: "number",
|
|
@@ -83,6 +94,10 @@ const TOOLS = [
|
|
|
83
94
|
enum: ["fast_path", "semantic", "full_text", "graph"],
|
|
84
95
|
description: "Retrieval strategy to use (default: auto-select)",
|
|
85
96
|
},
|
|
97
|
+
namespace: NAMESPACE_PROP,
|
|
98
|
+
agent_id: { type: "string", description: "Filter by agent ID" },
|
|
99
|
+
task_id: { type: "string", description: "Filter by task ID" },
|
|
100
|
+
exclude_agent: { type: "string", description: "Exclude memories from this agent" },
|
|
86
101
|
},
|
|
87
102
|
required: ["query"],
|
|
88
103
|
},
|
|
@@ -101,6 +116,7 @@ const TOOLS = [
|
|
|
101
116
|
type: "string",
|
|
102
117
|
description: "Optional filter by memory type",
|
|
103
118
|
},
|
|
119
|
+
namespace: NAMESPACE_PROP,
|
|
104
120
|
},
|
|
105
121
|
},
|
|
106
122
|
},
|
|
@@ -114,6 +130,7 @@ const TOOLS = [
|
|
|
114
130
|
type: "boolean",
|
|
115
131
|
description: "Include detailed per-layer statistics (default: false)",
|
|
116
132
|
},
|
|
133
|
+
namespace: NAMESPACE_PROP,
|
|
117
134
|
},
|
|
118
135
|
},
|
|
119
136
|
},
|
|
@@ -141,6 +158,98 @@ const TOOLS = [
|
|
|
141
158
|
properties: {},
|
|
142
159
|
},
|
|
143
160
|
},
|
|
161
|
+
{
|
|
162
|
+
name: "record_skill_outcome",
|
|
163
|
+
description: "Record success or failure feedback for a skill execution. Used to improve skill ranking over time.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
skill_id: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "The ID of the skill to record feedback for",
|
|
170
|
+
},
|
|
171
|
+
outcome: {
|
|
172
|
+
type: "string",
|
|
173
|
+
enum: ["success", "failure"],
|
|
174
|
+
description: "Whether the skill execution was successful or not",
|
|
175
|
+
},
|
|
176
|
+
satisfaction: {
|
|
177
|
+
type: "number",
|
|
178
|
+
description: "User satisfaction score (0-1). Required for success outcome.",
|
|
179
|
+
},
|
|
180
|
+
namespace: NAMESPACE_PROP,
|
|
181
|
+
},
|
|
182
|
+
required: ["skill_id", "outcome"],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "load_skill",
|
|
187
|
+
description: "Load full skill implementation on-demand. Use when a skill summary needs complete details for execution. Loaded skills are cached for the session.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
skill_id: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "The ID of the skill to load",
|
|
194
|
+
},
|
|
195
|
+
namespace: NAMESPACE_PROP,
|
|
196
|
+
},
|
|
197
|
+
required: ["skill_id"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "share_context",
|
|
202
|
+
description: "Share context (memories, skills, entities) from one namespace to another. Used for agent handoffs in multi-agent workflows.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
source_namespace: { type: "string", description: "Namespace to share from" },
|
|
207
|
+
target_namespace: { type: "string", description: "Namespace to share to" },
|
|
208
|
+
task_id: { type: "string", description: "Filter source memories by task ID" },
|
|
209
|
+
query: { type: "string", description: "Semantic query to select relevant memories" },
|
|
210
|
+
max_items: { type: "number", description: "Max items to share (default: 10)" },
|
|
211
|
+
include_skills: { type: "boolean", description: "Include matching skills (default: false)" },
|
|
212
|
+
include_entities: { type: "boolean", description: "Include related entities (default: true)" },
|
|
213
|
+
},
|
|
214
|
+
required: ["source_namespace", "target_namespace"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "store_decision",
|
|
219
|
+
description: "Store an architectural or design decision with reduced decay and automatic conflict detection against existing decisions.",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
content: { type: "string", description: "The decision content" },
|
|
224
|
+
namespace: NAMESPACE_PROP,
|
|
225
|
+
agent_id: { type: "string", description: "ID of the agent making this decision" },
|
|
226
|
+
agent_role: { type: "string", description: "Role of the agent" },
|
|
227
|
+
task_id: { type: "string", description: "Related task ID" },
|
|
228
|
+
supersedes: { type: "string", description: "ID of a previous decision this replaces" },
|
|
229
|
+
metadata: { type: "object", description: "Additional metadata", additionalProperties: true },
|
|
230
|
+
},
|
|
231
|
+
required: ["content"],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "list_namespaces",
|
|
236
|
+
description: "List all namespaces with memory counts. Useful for discovering active agent namespaces.",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "namespace_stats",
|
|
244
|
+
description: "Get detailed statistics for a specific namespace including memory counts, entity counts, skill counts, and active agents.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: {
|
|
248
|
+
namespace: { type: "string", description: "The namespace to get stats for" },
|
|
249
|
+
},
|
|
250
|
+
required: ["namespace"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
144
253
|
];
|
|
145
254
|
/**
|
|
146
255
|
* Initialize connections to Redis, Qdrant, and SQLite
|
|
@@ -244,10 +353,71 @@ async function initializeConnections() {
|
|
|
244
353
|
console.error("\x1b[32m💡 Solution:\x1b[0m Start Qdrant with: docker-compose up -d qdrant");
|
|
245
354
|
connections.hybridSearch = undefined;
|
|
246
355
|
}
|
|
247
|
-
// Initialize
|
|
356
|
+
// Initialize Semantic Memory Layer (requires SQLite)
|
|
357
|
+
if (connections.sqlite) {
|
|
358
|
+
try {
|
|
359
|
+
// Initialize semantic memory schema
|
|
360
|
+
initializeSemanticMemorySchema();
|
|
361
|
+
connections.semanticMemory = new SemanticMemoryLayer(connections.sqlite);
|
|
362
|
+
console.error("[INFO] ✅ Semantic memory layer initialized");
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
console.error("[WARN] Semantic memory initialization failed:", err instanceof Error ? err.message : String(err));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Initialize Skill Library (requires SQLite)
|
|
369
|
+
if (connections.sqlite) {
|
|
370
|
+
try {
|
|
371
|
+
// Initialize skill library schema
|
|
372
|
+
initializeSkillLibrarySchema();
|
|
373
|
+
connections.skillLibrary = new SkillLibrary(connections.sqlite);
|
|
374
|
+
console.error("[INFO] ✅ Skill library initialized");
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
console.error("[WARN] Skill library initialization failed:", err instanceof Error ? err.message : String(err));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Initialize Consolidation Pipeline (requires all memory layers)
|
|
381
|
+
if (connections.workingMemory && connections.semanticMemory && connections.skillLibrary && connections.sqlite) {
|
|
382
|
+
try {
|
|
383
|
+
connections.consolidationPipeline = new ConsolidationPipeline(connections.workingMemory, connections.semanticMemory, connections.skillLibrary, connections.sqlite);
|
|
384
|
+
console.error("[INFO] ✅ Consolidation pipeline initialized");
|
|
385
|
+
// Run startup consolidation asynchronously (non-blocking)
|
|
386
|
+
// This runs every time the MCP server starts (when Claude Code restarts)
|
|
387
|
+
setImmediate(async () => {
|
|
388
|
+
try {
|
|
389
|
+
console.error("[INFO] 🔄 Running startup consolidation...");
|
|
390
|
+
const stats = await connections.consolidationPipeline.consolidate();
|
|
391
|
+
console.error(`[INFO] ✅ Startup consolidation complete: ${stats.memoriesProcessed} memories, ` +
|
|
392
|
+
`${stats.entitiesExtracted} entities, ${stats.conflictsDetected} conflicts, ` +
|
|
393
|
+
`${stats.skillsExtracted} skills in ${stats.duration}ms`);
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
console.error("[WARN] Startup consolidation failed:", err instanceof Error ? err.message : String(err));
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
console.error("[WARN] Consolidation pipeline initialization failed:", err instanceof Error ? err.message : String(err));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
console.error("[WARN] Consolidation pipeline not initialized (missing dependencies)");
|
|
406
|
+
}
|
|
407
|
+
// Initialize Smart Router with working memory and skill library
|
|
248
408
|
SmartRouter.init({
|
|
249
409
|
workingMemory: connections.workingMemory,
|
|
250
410
|
});
|
|
411
|
+
// Initialize skill handler in router
|
|
412
|
+
if (connections.skillLibrary) {
|
|
413
|
+
SmartRouter.initSkillHandler(connections.skillLibrary);
|
|
414
|
+
console.error("[INFO] ✅ Smart router skill handler initialized");
|
|
415
|
+
}
|
|
416
|
+
// Initialize preference handler in router
|
|
417
|
+
if (connections.semanticMemory) {
|
|
418
|
+
SmartRouter.initPreferenceHandler(connections.semanticMemory);
|
|
419
|
+
console.error("[INFO] ✅ Smart router preference handler initialized");
|
|
420
|
+
}
|
|
251
421
|
console.error("[INFO] ✅ Smart router initialized");
|
|
252
422
|
// Print service status summary
|
|
253
423
|
console.error("\n" + "=".repeat(60));
|
|
@@ -299,6 +469,280 @@ function initializeSqliteSchema() {
|
|
|
299
469
|
updated_at INTEGER
|
|
300
470
|
);
|
|
301
471
|
`);
|
|
472
|
+
// v0.5.0 namespace migration for memories table
|
|
473
|
+
const memoryNamespaceCols = [
|
|
474
|
+
"ALTER TABLE memories ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
475
|
+
"ALTER TABLE memories ADD COLUMN agent_id TEXT",
|
|
476
|
+
"ALTER TABLE memories ADD COLUMN agent_role TEXT",
|
|
477
|
+
"ALTER TABLE memories ADD COLUMN task_id TEXT",
|
|
478
|
+
];
|
|
479
|
+
for (const sql of memoryNamespaceCols) {
|
|
480
|
+
try {
|
|
481
|
+
connections.sqlite.exec(sql);
|
|
482
|
+
}
|
|
483
|
+
catch (_) { /* column already exists */ }
|
|
484
|
+
}
|
|
485
|
+
connections.sqlite.exec(`
|
|
486
|
+
CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories(namespace);
|
|
487
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(namespace, agent_id);
|
|
488
|
+
`);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Initialize SQLite schema for semantic memory (HippoRAG knowledge graph)
|
|
492
|
+
*/
|
|
493
|
+
function initializeSemanticMemorySchema() {
|
|
494
|
+
if (!connections.sqlite) {
|
|
495
|
+
throw new Error("SQLite not initialized");
|
|
496
|
+
}
|
|
497
|
+
connections.sqlite.exec(`
|
|
498
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
499
|
+
id TEXT PRIMARY KEY,
|
|
500
|
+
name TEXT NOT NULL,
|
|
501
|
+
type TEXT NOT NULL,
|
|
502
|
+
description TEXT,
|
|
503
|
+
confidence REAL DEFAULT 1.0,
|
|
504
|
+
created_at TEXT NOT NULL,
|
|
505
|
+
last_accessed TEXT NOT NULL,
|
|
506
|
+
access_count INTEGER DEFAULT 1
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
510
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
511
|
+
|
|
512
|
+
CREATE TABLE IF NOT EXISTS relationships (
|
|
513
|
+
id TEXT PRIMARY KEY,
|
|
514
|
+
source_id TEXT NOT NULL,
|
|
515
|
+
target_id TEXT NOT NULL,
|
|
516
|
+
relation_type TEXT NOT NULL,
|
|
517
|
+
strength REAL DEFAULT 0.8,
|
|
518
|
+
evidence TEXT,
|
|
519
|
+
created_at TEXT NOT NULL,
|
|
520
|
+
last_reinforced TEXT NOT NULL
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_source ON relationships(source_id);
|
|
524
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_target ON relationships(target_id);
|
|
525
|
+
|
|
526
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
527
|
+
id TEXT PRIMARY KEY,
|
|
528
|
+
entity_id TEXT NOT NULL,
|
|
529
|
+
property TEXT NOT NULL,
|
|
530
|
+
value TEXT NOT NULL,
|
|
531
|
+
confidence REAL DEFAULT 1.0,
|
|
532
|
+
valid_from TEXT,
|
|
533
|
+
valid_until TEXT,
|
|
534
|
+
source_conversation TEXT
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity_id);
|
|
538
|
+
|
|
539
|
+
CREATE TABLE IF NOT EXISTS conflicts (
|
|
540
|
+
id TEXT PRIMARY KEY,
|
|
541
|
+
fact_id_1 TEXT,
|
|
542
|
+
fact_id_2 TEXT,
|
|
543
|
+
conflict_type TEXT,
|
|
544
|
+
description TEXT,
|
|
545
|
+
severity TEXT,
|
|
546
|
+
resolution_status TEXT
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
CREATE TABLE IF NOT EXISTS backup_metadata (
|
|
550
|
+
id TEXT PRIMARY KEY,
|
|
551
|
+
backup_type TEXT NOT NULL,
|
|
552
|
+
backup_path TEXT NOT NULL,
|
|
553
|
+
created_at TEXT NOT NULL,
|
|
554
|
+
expires_at TEXT NOT NULL
|
|
555
|
+
);
|
|
556
|
+
`);
|
|
557
|
+
// v0.5.0 namespace migration for semantic memory tables
|
|
558
|
+
const semanticNamespaceMigrations = [
|
|
559
|
+
"ALTER TABLE entities ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
560
|
+
"ALTER TABLE relationships ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
561
|
+
"ALTER TABLE facts ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
562
|
+
"ALTER TABLE conflicts ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
563
|
+
];
|
|
564
|
+
for (const sql of semanticNamespaceMigrations) {
|
|
565
|
+
try {
|
|
566
|
+
connections.sqlite.exec(sql);
|
|
567
|
+
}
|
|
568
|
+
catch (_) { /* column already exists */ }
|
|
569
|
+
}
|
|
570
|
+
connections.sqlite.exec(`
|
|
571
|
+
CREATE INDEX IF NOT EXISTS idx_entities_namespace ON entities(namespace);
|
|
572
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_namespace ON relationships(namespace);
|
|
573
|
+
CREATE INDEX IF NOT EXISTS idx_facts_namespace ON facts(namespace);
|
|
574
|
+
CREATE INDEX IF NOT EXISTS idx_conflicts_namespace ON conflicts(namespace);
|
|
575
|
+
`);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Initialize SQLite schema for skill library (procedural memory)
|
|
579
|
+
*/
|
|
580
|
+
function initializeSkillLibrarySchema() {
|
|
581
|
+
if (!connections.sqlite) {
|
|
582
|
+
throw new Error("SQLite not initialized");
|
|
583
|
+
}
|
|
584
|
+
connections.sqlite.exec(`
|
|
585
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
586
|
+
id TEXT PRIMARY KEY,
|
|
587
|
+
name TEXT NOT NULL,
|
|
588
|
+
description TEXT NOT NULL,
|
|
589
|
+
category TEXT NOT NULL,
|
|
590
|
+
triggers TEXT NOT NULL,
|
|
591
|
+
success_count INTEGER DEFAULT 0,
|
|
592
|
+
failure_count INTEGER DEFAULT 0,
|
|
593
|
+
avg_user_satisfaction REAL DEFAULT 0.5
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
CREATE INDEX IF NOT EXISTS idx_skills_category ON skills(category);
|
|
597
|
+
`);
|
|
598
|
+
// v0.5.0 namespace migration for skills table
|
|
599
|
+
try {
|
|
600
|
+
connections.sqlite.exec("ALTER TABLE skills ADD COLUMN namespace TEXT DEFAULT 'default'");
|
|
601
|
+
}
|
|
602
|
+
catch (_) { /* column already exists */ }
|
|
603
|
+
connections.sqlite.exec(`CREATE INDEX IF NOT EXISTS idx_skills_namespace ON skills(namespace);`);
|
|
604
|
+
// Apply lazy loading migration (idempotent - safe to run multiple times)
|
|
605
|
+
try {
|
|
606
|
+
connections.sqlite.exec(`
|
|
607
|
+
-- Add lazy loading columns if they don't exist
|
|
608
|
+
-- SQLite 3.35.0+ supports IF NOT EXISTS for ALTER TABLE ADD COLUMN
|
|
609
|
+
|
|
610
|
+
-- Add summary column (lightweight description for context injection)
|
|
611
|
+
ALTER TABLE skills ADD COLUMN summary TEXT;
|
|
612
|
+
`);
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
// Column already exists, continue
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
connections.sqlite.exec(`
|
|
619
|
+
ALTER TABLE skills ADD COLUMN is_archived INTEGER DEFAULT 0;
|
|
620
|
+
`);
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
// Column already exists
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
connections.sqlite.exec(`
|
|
627
|
+
ALTER TABLE skills ADD COLUMN last_used TEXT;
|
|
628
|
+
`);
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
// Column already exists
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
connections.sqlite.exec(`
|
|
635
|
+
ALTER TABLE skills ADD COLUMN created_at TEXT DEFAULT CURRENT_TIMESTAMP;
|
|
636
|
+
`);
|
|
637
|
+
}
|
|
638
|
+
catch (err) {
|
|
639
|
+
// Column already exists
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
connections.sqlite.exec(`
|
|
643
|
+
ALTER TABLE skills ADD COLUMN last_modified TEXT DEFAULT CURRENT_TIMESTAMP;
|
|
644
|
+
`);
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
// Column already exists
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
connections.sqlite.exec(`
|
|
651
|
+
ALTER TABLE skills ADD COLUMN version INTEGER DEFAULT 1;
|
|
652
|
+
`);
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
// Column already exists
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
connections.sqlite.exec(`
|
|
659
|
+
ALTER TABLE skills ADD COLUMN code TEXT;
|
|
660
|
+
`);
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
// Column already exists
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
connections.sqlite.exec(`
|
|
667
|
+
ALTER TABLE skills ADD COLUMN code_type TEXT DEFAULT 'reference';
|
|
668
|
+
`);
|
|
669
|
+
}
|
|
670
|
+
catch (err) {
|
|
671
|
+
// Column already exists
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
connections.sqlite.exec(`
|
|
675
|
+
ALTER TABLE skills ADD COLUMN prerequisites TEXT;
|
|
676
|
+
`);
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
// Column already exists
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
connections.sqlite.exec(`
|
|
683
|
+
ALTER TABLE skills ADD COLUMN uses_skills TEXT;
|
|
684
|
+
`);
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
// Column already exists
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
connections.sqlite.exec(`
|
|
691
|
+
ALTER TABLE skills ADD COLUMN used_by_skills TEXT;
|
|
692
|
+
`);
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
// Column already exists
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
connections.sqlite.exec(`
|
|
699
|
+
ALTER TABLE skills ADD COLUMN created_from TEXT;
|
|
700
|
+
`);
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
// Column already exists
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
connections.sqlite.exec(`
|
|
707
|
+
ALTER TABLE skills ADD COLUMN notes TEXT;
|
|
708
|
+
`);
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
// Column already exists
|
|
712
|
+
}
|
|
713
|
+
// Create indexes for lazy loading (idempotent)
|
|
714
|
+
connections.sqlite.exec(`
|
|
715
|
+
CREATE INDEX IF NOT EXISTS idx_skills_lazy_loading
|
|
716
|
+
ON skills(is_archived, avg_user_satisfaction DESC, success_count DESC);
|
|
717
|
+
|
|
718
|
+
CREATE INDEX IF NOT EXISTS idx_skills_last_used
|
|
719
|
+
ON skills(last_used DESC);
|
|
720
|
+
|
|
721
|
+
CREATE INDEX IF NOT EXISTS idx_skills_category_quality
|
|
722
|
+
ON skills(category, avg_user_satisfaction DESC, success_count DESC);
|
|
723
|
+
`);
|
|
724
|
+
// Update existing skills with defaults
|
|
725
|
+
connections.sqlite.exec(`
|
|
726
|
+
UPDATE skills
|
|
727
|
+
SET summary = CASE
|
|
728
|
+
WHEN summary IS NULL AND length(description) <= 100 THEN description
|
|
729
|
+
WHEN summary IS NULL THEN substr(description, 1, 97) || '...'
|
|
730
|
+
ELSE summary
|
|
731
|
+
END
|
|
732
|
+
WHERE summary IS NULL OR summary = '';
|
|
733
|
+
|
|
734
|
+
UPDATE skills
|
|
735
|
+
SET created_at = CURRENT_TIMESTAMP
|
|
736
|
+
WHERE created_at IS NULL;
|
|
737
|
+
|
|
738
|
+
UPDATE skills
|
|
739
|
+
SET last_modified = CURRENT_TIMESTAMP
|
|
740
|
+
WHERE last_modified IS NULL;
|
|
741
|
+
|
|
742
|
+
UPDATE skills
|
|
743
|
+
SET is_archived = 0
|
|
744
|
+
WHERE is_archived IS NULL;
|
|
745
|
+
`);
|
|
302
746
|
}
|
|
303
747
|
/**
|
|
304
748
|
* Handle store_memory tool call
|
|
@@ -310,6 +754,7 @@ async function handleStoreMemory(input) {
|
|
|
310
754
|
try {
|
|
311
755
|
// Validate input
|
|
312
756
|
const validatedInput = validateInput(StoreMemoryInputSchema, input);
|
|
757
|
+
const namespace = validatedInput.namespace || 'default';
|
|
313
758
|
// Use pure UUID for Qdrant compatibility
|
|
314
759
|
const id = randomUUID();
|
|
315
760
|
const now = Date.now();
|
|
@@ -324,19 +769,22 @@ async function handleStoreMemory(input) {
|
|
|
324
769
|
console.error("[WARN] Failed to generate embedding (continuing without):", err instanceof Error ? err.message : String(err));
|
|
325
770
|
}
|
|
326
771
|
}
|
|
327
|
-
// Store in SQLite
|
|
772
|
+
// Store in SQLite with namespace and agent attribution
|
|
328
773
|
const stmt = connections.sqlite.prepare(`
|
|
329
|
-
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata)
|
|
330
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
774
|
+
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace, agent_id, agent_role, task_id)
|
|
775
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
331
776
|
`);
|
|
332
|
-
stmt.run(id, validatedInput.content, validatedInput.memory_type, now, now, JSON.stringify(validatedInput.metadata || {}));
|
|
333
|
-
// Store embedding in Qdrant if available
|
|
777
|
+
stmt.run(id, validatedInput.content, validatedInput.memory_type, now, now, JSON.stringify(validatedInput.metadata || {}), namespace, validatedInput.agent_id || null, validatedInput.agent_role || null, validatedInput.task_id || null);
|
|
778
|
+
// Store embedding in Qdrant if available (include namespace in payload)
|
|
334
779
|
if (embedding && connections.hybridSearch) {
|
|
335
780
|
try {
|
|
336
781
|
await connections.hybridSearch.upsertMemory(id, embedding, {
|
|
337
782
|
content: validatedInput.content,
|
|
338
783
|
memory_type: validatedInput.memory_type,
|
|
339
784
|
created_at: now,
|
|
785
|
+
namespace,
|
|
786
|
+
agent_id: validatedInput.agent_id,
|
|
787
|
+
task_id: validatedInput.task_id,
|
|
340
788
|
...validatedInput.metadata,
|
|
341
789
|
});
|
|
342
790
|
console.error(`[INFO] Stored embedding in Qdrant for ${id}`);
|
|
@@ -346,10 +794,11 @@ async function handleStoreMemory(input) {
|
|
|
346
794
|
throw new McpError(ErrorCode.InternalError, `Qdrant storage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
347
795
|
}
|
|
348
796
|
}
|
|
349
|
-
console.error(`[INFO] Memory stored: ${id} (type: ${validatedInput.memory_type}, length: ${validatedInput.content.length}, embedding: ${embedding ? "yes" : "no"})`);
|
|
797
|
+
console.error(`[INFO] Memory stored: ${id} (type: ${validatedInput.memory_type}, ns: ${namespace}, length: ${validatedInput.content.length}, embedding: ${embedding ? "yes" : "no"})`);
|
|
350
798
|
return {
|
|
351
799
|
success: true,
|
|
352
800
|
memory_id: id,
|
|
801
|
+
namespace,
|
|
353
802
|
timestamp: now,
|
|
354
803
|
message: `Memory stored successfully (${validatedInput.memory_type})`,
|
|
355
804
|
has_embedding: !!embedding,
|
|
@@ -369,18 +818,36 @@ async function handleRetrieveMemory(input) {
|
|
|
369
818
|
try {
|
|
370
819
|
// Validate input
|
|
371
820
|
const validatedInput = validateInput(RetrieveMemoryInputSchema, input);
|
|
821
|
+
const namespace = validatedInput.namespace || 'default';
|
|
372
822
|
const maxResults = validatedInput.max_results || 5;
|
|
823
|
+
// Build Qdrant filter for namespace
|
|
824
|
+
const qdrantFilter = {
|
|
825
|
+
must: [
|
|
826
|
+
{ key: "namespace", match: { value: namespace } },
|
|
827
|
+
...(validatedInput.agent_id ? [{ key: "agent_id", match: { value: validatedInput.agent_id } }] : []),
|
|
828
|
+
...(validatedInput.task_id ? [{ key: "task_id", match: { value: validatedInput.task_id } }] : []),
|
|
829
|
+
],
|
|
830
|
+
...(validatedInput.exclude_agent ? {
|
|
831
|
+
must_not: [{ key: "agent_id", match: { value: validatedInput.exclude_agent } }]
|
|
832
|
+
} : {}),
|
|
833
|
+
};
|
|
373
834
|
// Try semantic search if embedding service is available
|
|
374
835
|
if (connections.embeddingClient && connections.hybridSearch) {
|
|
375
836
|
try {
|
|
376
837
|
// Generate query embedding
|
|
377
838
|
const queryEmbedding = await connections.embeddingClient.embed(validatedInput.query);
|
|
378
|
-
// Perform hybrid search
|
|
379
|
-
|
|
380
|
-
|
|
839
|
+
// Perform hybrid search with namespace filter
|
|
840
|
+
let searchResults = await connections.hybridSearch.hybridSearch(queryEmbedding, maxResults, qdrantFilter);
|
|
841
|
+
// Fallback: if namespace is "default" and filtered search returns empty,
|
|
842
|
+
// retry without filter (backward compat for pre-namespace vectors)
|
|
843
|
+
if (searchResults.length === 0 && namespace === 'default') {
|
|
844
|
+
searchResults = await connections.hybridSearch.hybridSearch(queryEmbedding, maxResults);
|
|
845
|
+
}
|
|
846
|
+
console.error(`[INFO] Semantic search: query="${validatedInput.query}", ns=${namespace}, results=${searchResults.length}`);
|
|
381
847
|
return {
|
|
382
848
|
success: true,
|
|
383
849
|
query: validatedInput.query,
|
|
850
|
+
namespace,
|
|
384
851
|
routing_strategy: "semantic",
|
|
385
852
|
results: searchResults.map((r) => ({
|
|
386
853
|
id: r.id,
|
|
@@ -398,29 +865,45 @@ async function handleRetrieveMemory(input) {
|
|
|
398
865
|
console.error("[WARN] Semantic search failed, falling back to text search:", err instanceof Error ? err.message : String(err));
|
|
399
866
|
}
|
|
400
867
|
}
|
|
401
|
-
// Fallback: SQL-based text search
|
|
402
|
-
let query = "SELECT id, content, memory_type, created_at, metadata FROM memories";
|
|
403
|
-
const params = [];
|
|
868
|
+
// Fallback: SQL-based text search with namespace filter
|
|
869
|
+
let query = "SELECT id, content, memory_type, created_at, metadata, agent_id, task_id FROM memories WHERE namespace = ?";
|
|
870
|
+
const params = [namespace];
|
|
404
871
|
// Add type filter if specified
|
|
405
872
|
if (validatedInput.memory_types && validatedInput.memory_types.length > 0) {
|
|
406
873
|
const placeholders = validatedInput.memory_types.map(() => "?").join(",");
|
|
407
|
-
query += `
|
|
874
|
+
query += ` AND memory_type IN (${placeholders})`;
|
|
408
875
|
params.push(...validatedInput.memory_types);
|
|
409
876
|
}
|
|
877
|
+
// Add agent/task filters
|
|
878
|
+
if (validatedInput.agent_id) {
|
|
879
|
+
query += " AND agent_id = ?";
|
|
880
|
+
params.push(validatedInput.agent_id);
|
|
881
|
+
}
|
|
882
|
+
if (validatedInput.task_id) {
|
|
883
|
+
query += " AND task_id = ?";
|
|
884
|
+
params.push(validatedInput.task_id);
|
|
885
|
+
}
|
|
886
|
+
if (validatedInput.exclude_agent) {
|
|
887
|
+
query += " AND (agent_id IS NULL OR agent_id != ?)";
|
|
888
|
+
params.push(validatedInput.exclude_agent);
|
|
889
|
+
}
|
|
410
890
|
query += " ORDER BY created_at DESC LIMIT ?";
|
|
411
891
|
params.push(maxResults);
|
|
412
892
|
const stmt = connections.sqlite.prepare(query);
|
|
413
893
|
const results = stmt.all(...params);
|
|
414
|
-
console.error(`[INFO] Text search (fallback): query="${validatedInput.query}", results=${results.length}`);
|
|
894
|
+
console.error(`[INFO] Text search (fallback): query="${validatedInput.query}", ns=${namespace}, results=${results.length}`);
|
|
415
895
|
return {
|
|
416
896
|
success: true,
|
|
417
897
|
query: validatedInput.query,
|
|
898
|
+
namespace,
|
|
418
899
|
routing_strategy: "text_fallback",
|
|
419
900
|
results: results.map((r) => ({
|
|
420
901
|
id: r.id,
|
|
421
902
|
content: r.content,
|
|
422
903
|
memory_type: r.memory_type,
|
|
423
904
|
created_at: r.created_at,
|
|
905
|
+
agent_id: r.agent_id,
|
|
906
|
+
task_id: r.task_id,
|
|
424
907
|
metadata: JSON.parse(r.metadata || "{}"),
|
|
425
908
|
})),
|
|
426
909
|
count: results.length,
|
|
@@ -440,20 +923,22 @@ async function handleListRecent(input) {
|
|
|
440
923
|
try {
|
|
441
924
|
// Validate input
|
|
442
925
|
const validatedInput = validateInput(ListRecentInputSchema, input);
|
|
926
|
+
const namespace = validatedInput.namespace || 'default';
|
|
443
927
|
const limit = validatedInput.limit || 5;
|
|
444
|
-
let query = "SELECT id, content, memory_type, created_at FROM memories";
|
|
445
|
-
const params = [];
|
|
928
|
+
let query = "SELECT id, content, memory_type, created_at, agent_id, task_id FROM memories WHERE namespace = ?";
|
|
929
|
+
const params = [namespace];
|
|
446
930
|
if (validatedInput.memory_type) {
|
|
447
|
-
query += "
|
|
931
|
+
query += " AND memory_type = ?";
|
|
448
932
|
params.push(validatedInput.memory_type);
|
|
449
933
|
}
|
|
450
934
|
query += " ORDER BY created_at DESC LIMIT ?";
|
|
451
935
|
params.push(limit);
|
|
452
936
|
const stmt = connections.sqlite.prepare(query);
|
|
453
937
|
const results = stmt.all(...params);
|
|
454
|
-
console.error(`[INFO] Listed ${results.length} recent memories (limit=${limit}, type=${validatedInput.memory_type || "all"})`);
|
|
938
|
+
console.error(`[INFO] Listed ${results.length} recent memories (ns=${namespace}, limit=${limit}, type=${validatedInput.memory_type || "all"})`);
|
|
455
939
|
return {
|
|
456
940
|
success: true,
|
|
941
|
+
namespace,
|
|
457
942
|
limit,
|
|
458
943
|
memory_type_filter: validatedInput.memory_type || null,
|
|
459
944
|
entries: results.map((r) => ({
|
|
@@ -461,6 +946,8 @@ async function handleListRecent(input) {
|
|
|
461
946
|
content: r.content,
|
|
462
947
|
memory_type: r.memory_type,
|
|
463
948
|
created_at: r.created_at,
|
|
949
|
+
agent_id: r.agent_id,
|
|
950
|
+
task_id: r.task_id,
|
|
464
951
|
})),
|
|
465
952
|
count: results.length,
|
|
466
953
|
};
|
|
@@ -479,15 +966,17 @@ async function handleGetStats(input) {
|
|
|
479
966
|
try {
|
|
480
967
|
// Validate input
|
|
481
968
|
const validatedInput = validateInput(GetStatsInputSchema, input);
|
|
482
|
-
const
|
|
483
|
-
const
|
|
969
|
+
const namespace = validatedInput.namespace || 'default';
|
|
970
|
+
const countStmt = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories WHERE namespace = ?");
|
|
971
|
+
const countResult = countStmt.get(namespace);
|
|
484
972
|
const totalMemories = countResult.count;
|
|
485
973
|
const typeStmt = connections.sqlite.prepare(`
|
|
486
|
-
SELECT memory_type, COUNT(*) as count FROM memories GROUP BY memory_type
|
|
974
|
+
SELECT memory_type, COUNT(*) as count FROM memories WHERE namespace = ? GROUP BY memory_type
|
|
487
975
|
`);
|
|
488
|
-
const typeResults = typeStmt.all();
|
|
976
|
+
const typeResults = typeStmt.all(namespace);
|
|
489
977
|
const stats = {
|
|
490
978
|
success: true,
|
|
979
|
+
namespace,
|
|
491
980
|
timestamp: Date.now(),
|
|
492
981
|
total_memories: totalMemories,
|
|
493
982
|
redis_connected: connections.redis ? true : false,
|
|
@@ -556,6 +1045,342 @@ async function handleVesperStatus() {
|
|
|
556
1045
|
: "Vesper is in pass-through mode (memory operations are skipped)",
|
|
557
1046
|
};
|
|
558
1047
|
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Handle record_skill_outcome tool call
|
|
1050
|
+
*
|
|
1051
|
+
* Records success or failure feedback for a skill execution.
|
|
1052
|
+
* Used to improve skill ranking over time.
|
|
1053
|
+
*/
|
|
1054
|
+
async function handleRecordSkillOutcome(input) {
|
|
1055
|
+
if (!connections.skillLibrary) {
|
|
1056
|
+
throw new McpError(ErrorCode.InternalError, "Skill library not available");
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
// Validate input
|
|
1060
|
+
const validatedInput = validateInput(RecordSkillOutcomeInputSchema, input);
|
|
1061
|
+
if (validatedInput.outcome === "success") {
|
|
1062
|
+
if (validatedInput.satisfaction === undefined) {
|
|
1063
|
+
throw new McpError(ErrorCode.InvalidParams, "satisfaction is required for success outcome");
|
|
1064
|
+
}
|
|
1065
|
+
connections.skillLibrary.recordSuccess(validatedInput.skill_id, validatedInput.satisfaction);
|
|
1066
|
+
console.error(`[INFO] Recorded skill success: ${validatedInput.skill_id} (satisfaction: ${validatedInput.satisfaction})`);
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
connections.skillLibrary.recordFailure(validatedInput.skill_id);
|
|
1070
|
+
console.error(`[INFO] Recorded skill failure: ${validatedInput.skill_id}`);
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
success: true,
|
|
1074
|
+
skill_id: validatedInput.skill_id,
|
|
1075
|
+
outcome: validatedInput.outcome,
|
|
1076
|
+
satisfaction: validatedInput.satisfaction,
|
|
1077
|
+
message: `Skill ${validatedInput.outcome} recorded for ${validatedInput.skill_id}`,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
throw new McpError(ErrorCode.InternalError, `Failed to record skill outcome: ${err instanceof Error ? err.message : String(err)}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Handle load_skill tool call
|
|
1086
|
+
*
|
|
1087
|
+
* Loads full skill implementation on-demand when a skill is invoked.
|
|
1088
|
+
* This is part of the lazy loading system to reduce token usage by 80-90%.
|
|
1089
|
+
*
|
|
1090
|
+
* Performance:
|
|
1091
|
+
* - First load: ~20ms (database query)
|
|
1092
|
+
* - Cached load: ~5ms (working memory cache hit)
|
|
1093
|
+
*
|
|
1094
|
+
* Caching:
|
|
1095
|
+
* - Loaded skills are cached in Redis for 1 hour (3600s)
|
|
1096
|
+
* - Cache automatically expires or can be invalidated
|
|
1097
|
+
* - Reduces database load and improves response time
|
|
1098
|
+
*
|
|
1099
|
+
* @param input - LoadSkillInput with skill_id
|
|
1100
|
+
* @returns Full skill implementation with all details
|
|
1101
|
+
*/
|
|
1102
|
+
async function handleLoadSkill(input) {
|
|
1103
|
+
if (!connections.skillLibrary) {
|
|
1104
|
+
throw new McpError(ErrorCode.InternalError, "Skill library not available");
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
// Validate input
|
|
1108
|
+
const validatedInput = validateInput(LoadSkillInputSchema, input);
|
|
1109
|
+
const startTime = Date.now();
|
|
1110
|
+
let fromCache = false;
|
|
1111
|
+
// Step 1: Check cache first (if working memory available)
|
|
1112
|
+
let fullSkill = null;
|
|
1113
|
+
if (connections.workingMemory) {
|
|
1114
|
+
try {
|
|
1115
|
+
const cached = await connections.workingMemory.getCachedSkill(validatedInput.skill_id);
|
|
1116
|
+
if (cached) {
|
|
1117
|
+
fullSkill = cached.skill;
|
|
1118
|
+
fromCache = true;
|
|
1119
|
+
console.error(`[INFO] Skill cache hit: ${fullSkill.name} (${validatedInput.skill_id}) ` +
|
|
1120
|
+
`access count: ${cached.access_count}`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
catch (err) {
|
|
1124
|
+
console.warn(`[WARN] Skill cache lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
// Step 2: Load from database if not cached
|
|
1128
|
+
if (!fullSkill) {
|
|
1129
|
+
fullSkill = connections.skillLibrary.loadFull(validatedInput.skill_id);
|
|
1130
|
+
if (!fullSkill) {
|
|
1131
|
+
throw new McpError(ErrorCode.InvalidParams, `Skill not found: ${validatedInput.skill_id}`);
|
|
1132
|
+
}
|
|
1133
|
+
// Cache the loaded skill (if working memory available)
|
|
1134
|
+
if (connections.workingMemory) {
|
|
1135
|
+
try {
|
|
1136
|
+
await connections.workingMemory.cacheSkill(fullSkill, 3600); // 1 hour TTL
|
|
1137
|
+
}
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
console.warn(`[WARN] Failed to cache skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
const loadTime = Date.now() - startTime;
|
|
1144
|
+
console.error(`[INFO] Loaded full skill: ${fullSkill.name} (${validatedInput.skill_id}) ` +
|
|
1145
|
+
`in ${loadTime}ms (${fromCache ? 'cache' : 'database'})`);
|
|
1146
|
+
return {
|
|
1147
|
+
success: true,
|
|
1148
|
+
skill: {
|
|
1149
|
+
id: fullSkill.id,
|
|
1150
|
+
name: fullSkill.name,
|
|
1151
|
+
summary: fullSkill.summary,
|
|
1152
|
+
description: fullSkill.description,
|
|
1153
|
+
category: fullSkill.category,
|
|
1154
|
+
triggers: fullSkill.triggers,
|
|
1155
|
+
code: fullSkill.code,
|
|
1156
|
+
code_type: fullSkill.code_type,
|
|
1157
|
+
prerequisites: fullSkill.prerequisites,
|
|
1158
|
+
quality_score: fullSkill.quality_score,
|
|
1159
|
+
success_count: fullSkill.success_count,
|
|
1160
|
+
failure_count: fullSkill.failure_count,
|
|
1161
|
+
avg_user_satisfaction: fullSkill.avg_user_satisfaction,
|
|
1162
|
+
uses_skills: fullSkill.uses_skills,
|
|
1163
|
+
used_by_skills: fullSkill.used_by_skills,
|
|
1164
|
+
created_from: fullSkill.created_from,
|
|
1165
|
+
created_at: fullSkill.created_at.toISOString(),
|
|
1166
|
+
last_modified: fullSkill.last_modified.toISOString(),
|
|
1167
|
+
last_used: fullSkill.last_used?.toISOString(),
|
|
1168
|
+
version: fullSkill.version,
|
|
1169
|
+
notes: fullSkill.notes,
|
|
1170
|
+
},
|
|
1171
|
+
load_time_ms: loadTime,
|
|
1172
|
+
from_cache: fromCache,
|
|
1173
|
+
message: `Loaded full skill: ${fullSkill.name}${fromCache ? ' (cached)' : ''}`,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
catch (err) {
|
|
1177
|
+
if (err instanceof McpError) {
|
|
1178
|
+
throw err;
|
|
1179
|
+
}
|
|
1180
|
+
throw new McpError(ErrorCode.InternalError, `Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Handle share_context tool call
|
|
1185
|
+
*
|
|
1186
|
+
* Shares memories from one namespace to another for agent handoffs.
|
|
1187
|
+
*/
|
|
1188
|
+
async function handleShareContext(input) {
|
|
1189
|
+
if (!connections.sqlite) {
|
|
1190
|
+
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
const validatedInput = validateInput(ShareContextInputSchema, input);
|
|
1194
|
+
const { source_namespace, target_namespace } = validatedInput;
|
|
1195
|
+
const maxItems = validatedInput.max_items || 10;
|
|
1196
|
+
const now = Date.now();
|
|
1197
|
+
const sharedItems = [];
|
|
1198
|
+
// Query memories from source namespace
|
|
1199
|
+
let memQuery = "SELECT id, content, memory_type, created_at, metadata, agent_id, task_id FROM memories WHERE namespace = ?";
|
|
1200
|
+
const memParams = [source_namespace];
|
|
1201
|
+
if (validatedInput.task_id) {
|
|
1202
|
+
memQuery += " AND task_id = ?";
|
|
1203
|
+
memParams.push(validatedInput.task_id);
|
|
1204
|
+
}
|
|
1205
|
+
memQuery += " ORDER BY created_at DESC LIMIT ?";
|
|
1206
|
+
memParams.push(maxItems);
|
|
1207
|
+
const memories = connections.sqlite.prepare(memQuery).all(...memParams);
|
|
1208
|
+
// Copy memories to target namespace
|
|
1209
|
+
for (const mem of memories) {
|
|
1210
|
+
const newId = randomUUID();
|
|
1211
|
+
connections.sqlite.prepare(`
|
|
1212
|
+
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace, agent_id, agent_role, task_id)
|
|
1213
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1214
|
+
`).run(newId, mem.content, mem.memory_type, mem.created_at, now, mem.metadata, target_namespace, mem.agent_id, null, mem.task_id);
|
|
1215
|
+
sharedItems.push({ type: 'memory', id: newId, original_id: mem.id });
|
|
1216
|
+
}
|
|
1217
|
+
// Include entities if requested
|
|
1218
|
+
let entityCount = 0;
|
|
1219
|
+
if (validatedInput.include_entities) {
|
|
1220
|
+
const entities = connections.sqlite.prepare("SELECT * FROM entities WHERE namespace = ? LIMIT ?").all(source_namespace, maxItems);
|
|
1221
|
+
for (const entity of entities) {
|
|
1222
|
+
if (connections.semanticMemory) {
|
|
1223
|
+
connections.semanticMemory.upsertEntity({ name: entity.name, type: entity.type, description: entity.description, confidence: entity.confidence }, target_namespace);
|
|
1224
|
+
entityCount++;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
// Store handoff event in target namespace
|
|
1229
|
+
const handoffId = randomUUID();
|
|
1230
|
+
connections.sqlite.prepare(`
|
|
1231
|
+
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace)
|
|
1232
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1233
|
+
`).run(handoffId, `Context handoff from namespace '${source_namespace}': ${memories.length} memories, ${entityCount} entities shared`, 'episodic', now, now, JSON.stringify({ handoff: true, source_namespace, items_shared: sharedItems.length, entities_shared: entityCount }), target_namespace);
|
|
1234
|
+
console.error(`[INFO] Shared ${sharedItems.length} memories + ${entityCount} entities from ${source_namespace} to ${target_namespace}`);
|
|
1235
|
+
return {
|
|
1236
|
+
success: true,
|
|
1237
|
+
source_namespace,
|
|
1238
|
+
target_namespace,
|
|
1239
|
+
memories_shared: sharedItems.length,
|
|
1240
|
+
entities_shared: entityCount,
|
|
1241
|
+
handoff_id: handoffId,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
catch (err) {
|
|
1245
|
+
throw new McpError(ErrorCode.InternalError, `Failed to share context: ${err instanceof Error ? err.message : String(err)}`);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Handle store_decision tool call
|
|
1250
|
+
*
|
|
1251
|
+
* Stores a decision with reduced decay and conflict detection.
|
|
1252
|
+
*/
|
|
1253
|
+
async function handleStoreDecision(input) {
|
|
1254
|
+
if (!connections.sqlite) {
|
|
1255
|
+
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
const validatedInput = validateInput(StoreDecisionInputSchema, input);
|
|
1259
|
+
const namespace = validatedInput.namespace || 'default';
|
|
1260
|
+
const id = randomUUID();
|
|
1261
|
+
const now = Date.now();
|
|
1262
|
+
// Build metadata with decay_factor
|
|
1263
|
+
const metadata = {
|
|
1264
|
+
...(validatedInput.metadata || {}),
|
|
1265
|
+
decay_factor: 0.25,
|
|
1266
|
+
supersedes: validatedInput.supersedes || null,
|
|
1267
|
+
};
|
|
1268
|
+
// Store as decision memory type
|
|
1269
|
+
connections.sqlite.prepare(`
|
|
1270
|
+
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace, agent_id, agent_role, task_id)
|
|
1271
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1272
|
+
`).run(id, validatedInput.content, 'decision', now, now, JSON.stringify(metadata), namespace, validatedInput.agent_id || null, validatedInput.agent_role || null, validatedInput.task_id || null);
|
|
1273
|
+
// Generate embedding and store in Qdrant
|
|
1274
|
+
let hasEmbedding = false;
|
|
1275
|
+
if (connections.embeddingClient && connections.hybridSearch) {
|
|
1276
|
+
try {
|
|
1277
|
+
const embedding = await connections.embeddingClient.embed(validatedInput.content);
|
|
1278
|
+
await connections.hybridSearch.upsertMemory(id, embedding, {
|
|
1279
|
+
content: validatedInput.content,
|
|
1280
|
+
memory_type: 'decision',
|
|
1281
|
+
created_at: now,
|
|
1282
|
+
namespace,
|
|
1283
|
+
agent_id: validatedInput.agent_id,
|
|
1284
|
+
task_id: validatedInput.task_id,
|
|
1285
|
+
decay_factor: 0.25,
|
|
1286
|
+
});
|
|
1287
|
+
hasEmbedding = true;
|
|
1288
|
+
}
|
|
1289
|
+
catch (err) {
|
|
1290
|
+
console.error("[WARN] Failed to generate embedding for decision:", err instanceof Error ? err.message : String(err));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
// If supersedes, mark the old decision's metadata
|
|
1294
|
+
if (validatedInput.supersedes) {
|
|
1295
|
+
const oldMem = connections.sqlite.prepare("SELECT metadata FROM memories WHERE id = ? AND namespace = ?").get(validatedInput.supersedes, namespace);
|
|
1296
|
+
if (oldMem) {
|
|
1297
|
+
const oldMeta = JSON.parse(oldMem.metadata || '{}');
|
|
1298
|
+
oldMeta.superseded_by = id;
|
|
1299
|
+
connections.sqlite.prepare("UPDATE memories SET metadata = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(oldMeta), now, validatedInput.supersedes);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
// Check for conflicting decisions in same namespace
|
|
1303
|
+
const existingDecisions = connections.sqlite.prepare("SELECT id, content FROM memories WHERE namespace = ? AND memory_type = 'decision' AND id != ? ORDER BY created_at DESC LIMIT 10").all(namespace, id);
|
|
1304
|
+
console.error(`[INFO] Decision stored: ${id} (ns: ${namespace}, supersedes: ${validatedInput.supersedes || 'none'})`);
|
|
1305
|
+
return {
|
|
1306
|
+
success: true,
|
|
1307
|
+
decision_id: id,
|
|
1308
|
+
namespace,
|
|
1309
|
+
timestamp: now,
|
|
1310
|
+
has_embedding: hasEmbedding,
|
|
1311
|
+
supersedes: validatedInput.supersedes || null,
|
|
1312
|
+
existing_decisions_count: existingDecisions.length,
|
|
1313
|
+
message: `Decision stored successfully`,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
catch (err) {
|
|
1317
|
+
throw new McpError(ErrorCode.InternalError, `Failed to store decision: ${err instanceof Error ? err.message : String(err)}`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Handle list_namespaces tool call
|
|
1322
|
+
*/
|
|
1323
|
+
async function handleListNamespaces() {
|
|
1324
|
+
if (!connections.sqlite) {
|
|
1325
|
+
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
1326
|
+
}
|
|
1327
|
+
try {
|
|
1328
|
+
const namespaces = connections.sqlite.prepare(`
|
|
1329
|
+
SELECT namespace, COUNT(*) as memory_count,
|
|
1330
|
+
COUNT(DISTINCT agent_id) as agent_count,
|
|
1331
|
+
MAX(created_at) as last_activity
|
|
1332
|
+
FROM memories
|
|
1333
|
+
WHERE namespace IS NOT NULL
|
|
1334
|
+
GROUP BY namespace
|
|
1335
|
+
ORDER BY last_activity DESC
|
|
1336
|
+
`).all();
|
|
1337
|
+
return {
|
|
1338
|
+
success: true,
|
|
1339
|
+
namespaces: namespaces.map(ns => ({
|
|
1340
|
+
namespace: ns.namespace,
|
|
1341
|
+
memory_count: ns.memory_count,
|
|
1342
|
+
agent_count: ns.agent_count,
|
|
1343
|
+
last_activity: ns.last_activity,
|
|
1344
|
+
})),
|
|
1345
|
+
count: namespaces.length,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
catch (err) {
|
|
1349
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list namespaces: ${err instanceof Error ? err.message : String(err)}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Handle namespace_stats tool call
|
|
1354
|
+
*/
|
|
1355
|
+
async function handleNamespaceStats(input) {
|
|
1356
|
+
if (!connections.sqlite) {
|
|
1357
|
+
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
1358
|
+
}
|
|
1359
|
+
try {
|
|
1360
|
+
const validatedInput = validateInput(NamespaceStatsInputSchema, input);
|
|
1361
|
+
const namespace = validatedInput.namespace;
|
|
1362
|
+
const memoryCount = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories WHERE namespace = ?").get(namespace).count;
|
|
1363
|
+
const typeBreakdown = connections.sqlite.prepare("SELECT memory_type, COUNT(*) as count FROM memories WHERE namespace = ? GROUP BY memory_type").all(namespace);
|
|
1364
|
+
const entityCount = connections.sqlite.prepare("SELECT COUNT(*) as count FROM entities WHERE namespace = ?").get(namespace).count;
|
|
1365
|
+
const skillCount = connections.sqlite.prepare("SELECT COUNT(*) as count FROM skills WHERE namespace = ?").get(namespace).count;
|
|
1366
|
+
const agents = connections.sqlite.prepare("SELECT DISTINCT agent_id, agent_role FROM memories WHERE namespace = ? AND agent_id IS NOT NULL").all(namespace);
|
|
1367
|
+
const decisionCount = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories WHERE namespace = ? AND memory_type = 'decision'").get(namespace).count;
|
|
1368
|
+
return {
|
|
1369
|
+
success: true,
|
|
1370
|
+
namespace,
|
|
1371
|
+
memories: memoryCount,
|
|
1372
|
+
memory_type_breakdown: Object.fromEntries(typeBreakdown.map(t => [t.memory_type, t.count])),
|
|
1373
|
+
entities: entityCount,
|
|
1374
|
+
skills: skillCount,
|
|
1375
|
+
decisions: decisionCount,
|
|
1376
|
+
agents: agents.map(a => ({ agent_id: a.agent_id, agent_role: a.agent_role })),
|
|
1377
|
+
agent_count: agents.length,
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
catch (err) {
|
|
1381
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get namespace stats: ${err instanceof Error ? err.message : String(err)}`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
559
1384
|
/**
|
|
560
1385
|
* Process tool calls
|
|
561
1386
|
*/
|
|
@@ -589,6 +1414,18 @@ async function processTool(name, input) {
|
|
|
589
1414
|
return await handleListRecent(input);
|
|
590
1415
|
case "get_stats":
|
|
591
1416
|
return await handleGetStats(input);
|
|
1417
|
+
case "record_skill_outcome":
|
|
1418
|
+
return await handleRecordSkillOutcome(input);
|
|
1419
|
+
case "load_skill":
|
|
1420
|
+
return await handleLoadSkill(input);
|
|
1421
|
+
case "share_context":
|
|
1422
|
+
return await handleShareContext(input);
|
|
1423
|
+
case "store_decision":
|
|
1424
|
+
return await handleStoreDecision(input);
|
|
1425
|
+
case "list_namespaces":
|
|
1426
|
+
return await handleListNamespaces();
|
|
1427
|
+
case "namespace_stats":
|
|
1428
|
+
return await handleNamespaceStats(input);
|
|
592
1429
|
default:
|
|
593
1430
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
594
1431
|
}
|
|
@@ -603,7 +1440,7 @@ async function main() {
|
|
|
603
1440
|
// Create MCP server
|
|
604
1441
|
const server = new Server({
|
|
605
1442
|
name: "vesper",
|
|
606
|
-
version: "0.
|
|
1443
|
+
version: "0.5.0",
|
|
607
1444
|
}, {
|
|
608
1445
|
capabilities: {
|
|
609
1446
|
tools: {},
|
|
@@ -657,8 +1494,22 @@ async function main() {
|
|
|
657
1494
|
console.error("[INFO] Starting stdio transport...");
|
|
658
1495
|
await server.connect(transport);
|
|
659
1496
|
console.error("[INFO] Vesper running on stdio transport");
|
|
660
|
-
console.error("[INFO] Available tools: store_memory, retrieve_memory, list_recent, get_stats, vesper_enable, vesper_disable, vesper_status");
|
|
1497
|
+
console.error("[INFO] Available tools: store_memory, retrieve_memory, list_recent, get_stats, record_skill_outcome, load_skill, vesper_enable, vesper_disable, vesper_status");
|
|
1498
|
+
}
|
|
1499
|
+
// Cleanup handler for graceful shutdown
|
|
1500
|
+
function cleanup() {
|
|
1501
|
+
console.error('[INFO] Shutting down Vesper...');
|
|
1502
|
+
if (connections.redis) {
|
|
1503
|
+
connections.redis.disconnect();
|
|
1504
|
+
}
|
|
1505
|
+
if (connections.sqlite) {
|
|
1506
|
+
connections.sqlite.close();
|
|
1507
|
+
}
|
|
1508
|
+
console.error('[INFO] Vesper shutdown complete');
|
|
1509
|
+
process.exit(0);
|
|
661
1510
|
}
|
|
1511
|
+
process.on('SIGINT', cleanup);
|
|
1512
|
+
process.on('SIGTERM', cleanup);
|
|
662
1513
|
// Run the server
|
|
663
1514
|
main().catch((err) => {
|
|
664
1515
|
console.error("[FATAL] Server error:", err instanceof Error ? err.message : String(err));
|