agentdb 3.1.0 → 3.3.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/README.md +13 -0
- package/dist/src/controllers/ReasoningBank.d.ts +36 -0
- package/dist/src/controllers/ReasoningBank.d.ts.map +1 -1
- package/dist/src/controllers/ReasoningBank.js +123 -1
- package/dist/src/controllers/ReasoningBank.js.map +1 -1
- package/dist/src/controllers/ReflexionMemory.d.ts +75 -0
- package/dist/src/controllers/ReflexionMemory.d.ts.map +1 -1
- package/dist/src/controllers/ReflexionMemory.js +292 -10
- package/dist/src/controllers/ReflexionMemory.js.map +1 -1
- package/dist/src/controllers/SkillLibrary.d.ts +50 -0
- package/dist/src/controllers/SkillLibrary.d.ts.map +1 -1
- package/dist/src/controllers/SkillLibrary.js +220 -9
- package/dist/src/controllers/SkillLibrary.js.map +1 -1
- package/dist/src/controllers/crud-helpers.d.ts +45 -0
- package/dist/src/controllers/crud-helpers.d.ts.map +1 -0
- package/dist/src/controllers/crud-helpers.js +130 -0
- package/dist/src/controllers/crud-helpers.js.map +1 -0
- package/dist/src/controllers/episode-hybrid.d.ts +82 -0
- package/dist/src/controllers/episode-hybrid.d.ts.map +1 -0
- package/dist/src/controllers/episode-hybrid.js +140 -0
- package/dist/src/controllers/episode-hybrid.js.map +1 -0
- package/dist/src/mcp/agentdb-mcp-server.js +373 -47
- package/dist/src/mcp/agentdb-mcp-server.js.map +1 -1
- package/dist/src/search/conversation-retrieval.d.ts +61 -0
- package/dist/src/search/conversation-retrieval.d.ts.map +1 -0
- package/dist/src/search/conversation-retrieval.js +190 -0
- package/dist/src/search/conversation-retrieval.js.map +1 -0
- package/dist/src/search/cross-encoder.d.ts +17 -0
- package/dist/src/search/cross-encoder.d.ts.map +1 -0
- package/dist/src/search/cross-encoder.js +52 -0
- package/dist/src/search/cross-encoder.js.map +1 -0
- package/dist/src/security/mcp-policy.d.ts.map +1 -1
- package/dist/src/security/mcp-policy.js +13 -0
- package/dist/src/security/mcp-policy.js.map +1 -1
- package/package.json +6 -4
|
@@ -22,7 +22,8 @@ import { EmbeddingService } from '../controllers/EmbeddingService.js';
|
|
|
22
22
|
import { BatchOperations } from '../optimizations/BatchOperations.js';
|
|
23
23
|
import { ReasoningBank } from '../controllers/ReasoningBank.js';
|
|
24
24
|
import { MCPToolCaches } from '../optimizations/ToolCache.js';
|
|
25
|
-
import { validateId, validateTimestamp, validateSessionId, validateTaskString, validateNumericRange, validateArrayLength, validateBoolean, validateEnum, ValidationError, handleSecurityError, } from '../security/input-validation.js';
|
|
25
|
+
import { validateId, validateTimestamp, validateSessionId, validateTaskString, validateNumericRange, validateArrayLength, validateBoolean, validateEnum, ValidationError, handleSecurityError, sanitizeText, } from '../security/input-validation.js';
|
|
26
|
+
import { assertMetadata, assertTags, inTransaction, toRecordId, CrudValidationError } from '../controllers/crud-helpers.js';
|
|
26
27
|
import * as path from 'path';
|
|
27
28
|
import * as fs from 'fs';
|
|
28
29
|
import { fileURLToPath } from 'url';
|
|
@@ -365,13 +366,15 @@ const tools = [
|
|
|
365
366
|
},
|
|
366
367
|
{
|
|
367
368
|
name: 'agentdb_search',
|
|
368
|
-
description: '
|
|
369
|
+
description: 'Hybrid episode search: dense k-NN (cosine) fused with BM25 keyword ranking by reciprocal-rank fusion. Set hybrid=false for pure vector order.',
|
|
369
370
|
inputSchema: {
|
|
370
371
|
type: 'object',
|
|
371
372
|
properties: {
|
|
372
373
|
query: { type: 'string', description: 'Search query text' },
|
|
373
374
|
k: { type: 'number', description: 'Number of results to return (max 100)', default: 10, minimum: 1, maximum: 100 },
|
|
374
375
|
min_similarity: { type: 'number', description: 'Minimum similarity threshold (0-1)', default: 0.0 },
|
|
376
|
+
hybrid: { type: 'boolean', description: 'Fuse BM25 keyword ranking with the vector ranking (ADR-016)', default: true },
|
|
377
|
+
rerank: { type: 'boolean', description: 'Rerank the top candidates with a local cross-encoder (ADR-016; ~200 ms on CPU, 23 MB model on first use)', default: false },
|
|
375
378
|
filters: {
|
|
376
379
|
type: 'object',
|
|
377
380
|
properties: {
|
|
@@ -406,6 +409,141 @@ const tools = [
|
|
|
406
409
|
// ==========================================================================
|
|
407
410
|
// FRONTIER MEMORY FEATURES (EXISTING)
|
|
408
411
|
// ==========================================================================
|
|
412
|
+
{
|
|
413
|
+
name: 'agentdb_get',
|
|
414
|
+
description: 'Get one stored vector record (episode) by id. Returns null when it does not exist. The embedding is omitted unless include_embedding is true.',
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: 'object',
|
|
417
|
+
properties: {
|
|
418
|
+
id: { type: 'number', description: 'Record id returned by agentdb_insert' },
|
|
419
|
+
include_embedding: { type: 'boolean', description: 'Also return the stored embedding vector', default: false },
|
|
420
|
+
},
|
|
421
|
+
required: ['id'],
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: 'agentdb_list',
|
|
426
|
+
description: 'Page through stored vector records (episodes), newest first. Returns items plus the total count.',
|
|
427
|
+
inputSchema: {
|
|
428
|
+
type: 'object',
|
|
429
|
+
properties: {
|
|
430
|
+
session_id: { type: 'string', description: 'Only records from this session' },
|
|
431
|
+
limit: { type: 'number', description: 'Page size (1-500)', default: 50 },
|
|
432
|
+
offset: { type: 'number', description: 'Records to skip', default: 0 },
|
|
433
|
+
order: { type: 'string', enum: ['newest', 'oldest'], default: 'newest' },
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'agentdb_update',
|
|
439
|
+
description: 'Update a stored vector record in place, keeping its id. Changing text re-embeds it and refreshes the keyword index; tag or metadata changes do not re-embed. Metadata is merged (null removes a key) unless replace_metadata is true.',
|
|
440
|
+
inputSchema: {
|
|
441
|
+
type: 'object',
|
|
442
|
+
properties: {
|
|
443
|
+
id: { type: 'number', description: 'Record id' },
|
|
444
|
+
text: { type: 'string', description: 'New text content (re-embedded)' },
|
|
445
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Replacement tag list' },
|
|
446
|
+
metadata: { type: 'object', description: 'Metadata patch (merged by default)' },
|
|
447
|
+
replace_metadata: { type: 'boolean', description: 'Replace metadata instead of merging', default: false },
|
|
448
|
+
reward: { type: 'number', description: 'New reward value' },
|
|
449
|
+
success: { type: 'boolean', description: 'New success flag' },
|
|
450
|
+
},
|
|
451
|
+
required: ['id'],
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: 'agentdb_pattern_get',
|
|
456
|
+
description: 'Get one reasoning pattern by id.',
|
|
457
|
+
inputSchema: {
|
|
458
|
+
type: 'object',
|
|
459
|
+
properties: { pattern_id: { type: 'number', description: 'Pattern id' } },
|
|
460
|
+
required: ['pattern_id'],
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
name: 'agentdb_pattern_list',
|
|
465
|
+
description: 'Page through reasoning patterns, newest first, optionally filtered by task type.',
|
|
466
|
+
inputSchema: {
|
|
467
|
+
type: 'object',
|
|
468
|
+
properties: {
|
|
469
|
+
task_type: { type: 'string', description: 'Only patterns of this task type' },
|
|
470
|
+
limit: { type: 'number', default: 50 },
|
|
471
|
+
offset: { type: 'number', default: 0 },
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'agentdb_pattern_update',
|
|
477
|
+
description: 'Update a reasoning pattern in place. Changing taskType or approach re-embeds it. Usage statistics are not writable here (use agentdb_pattern_feedback).',
|
|
478
|
+
inputSchema: {
|
|
479
|
+
type: 'object',
|
|
480
|
+
properties: {
|
|
481
|
+
pattern_id: { type: 'number', description: 'Pattern id' },
|
|
482
|
+
taskType: { type: 'string' },
|
|
483
|
+
approach: { type: 'string' },
|
|
484
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
485
|
+
metadata: { type: 'object', description: 'Metadata patch (merged by default)' },
|
|
486
|
+
replace_metadata: { type: 'boolean', default: false },
|
|
487
|
+
},
|
|
488
|
+
required: ['pattern_id'],
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: 'agentdb_pattern_delete',
|
|
493
|
+
description: 'Delete a reasoning pattern, its embedding and its vector-index entry.',
|
|
494
|
+
inputSchema: {
|
|
495
|
+
type: 'object',
|
|
496
|
+
properties: { pattern_id: { type: 'number', description: 'Pattern id' } },
|
|
497
|
+
required: ['pattern_id'],
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'skill_get',
|
|
502
|
+
description: 'Get one skill by id.',
|
|
503
|
+
inputSchema: {
|
|
504
|
+
type: 'object',
|
|
505
|
+
properties: { skill_id: { type: 'number', description: 'Skill id' } },
|
|
506
|
+
required: ['skill_id'],
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: 'skill_list',
|
|
511
|
+
description: 'Page through skills, newest first.',
|
|
512
|
+
inputSchema: {
|
|
513
|
+
type: 'object',
|
|
514
|
+
properties: {
|
|
515
|
+
min_success_rate: { type: 'number', description: 'Only skills at or above this success rate' },
|
|
516
|
+
limit: { type: 'number', default: 50 },
|
|
517
|
+
offset: { type: 'number', default: 0 },
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
name: 'skill_update',
|
|
523
|
+
description: 'Update a skill in place. Changing name, description or signature re-embeds it; renaming onto an existing name is rejected. Usage statistics are not writable here (use agentdb_skill_feedback).',
|
|
524
|
+
inputSchema: {
|
|
525
|
+
type: 'object',
|
|
526
|
+
properties: {
|
|
527
|
+
skill_id: { type: 'number', description: 'Skill id' },
|
|
528
|
+
name: { type: 'string' },
|
|
529
|
+
description: { type: 'string' },
|
|
530
|
+
code: { type: 'string' },
|
|
531
|
+
signature: { type: 'object', description: '{ inputs: {...}, outputs: {...} }' },
|
|
532
|
+
metadata: { type: 'object', description: 'Metadata patch (merged by default)' },
|
|
533
|
+
replace_metadata: { type: 'boolean', default: false },
|
|
534
|
+
},
|
|
535
|
+
required: ['skill_id'],
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: 'skill_delete',
|
|
540
|
+
description: 'Delete a skill with its embedding, links and vector-index entry.',
|
|
541
|
+
inputSchema: {
|
|
542
|
+
type: 'object',
|
|
543
|
+
properties: { skill_id: { type: 'number', description: 'Skill id' } },
|
|
544
|
+
required: ['skill_id'],
|
|
545
|
+
},
|
|
546
|
+
},
|
|
409
547
|
{
|
|
410
548
|
name: 'reflexion_store',
|
|
411
549
|
description: 'Store an episode with self-critique for reflexion-based learning',
|
|
@@ -972,6 +1110,157 @@ const tools = [
|
|
|
972
1110
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
973
1111
|
return { tools: filterMcpTools(tools, toolPolicy) };
|
|
974
1112
|
});
|
|
1113
|
+
/** Bulk-delete episodes together with their embeddings (FK cascades are off by default). */
|
|
1114
|
+
function deleteEpisodesWhere(condition, value) {
|
|
1115
|
+
const changes = inTransaction(db, () => {
|
|
1116
|
+
db.prepare(`DELETE FROM episode_embeddings WHERE episode_id IN (SELECT id FROM episodes WHERE ${condition})`).run(value);
|
|
1117
|
+
return db.prepare(`DELETE FROM episodes WHERE ${condition}`).run(value).changes;
|
|
1118
|
+
});
|
|
1119
|
+
if (changes > 0) {
|
|
1120
|
+
reflexion.clearCache();
|
|
1121
|
+
caches.stats.clear();
|
|
1122
|
+
}
|
|
1123
|
+
return changes;
|
|
1124
|
+
}
|
|
1125
|
+
/** Reject arguments a tool does not define, so a typo can never silently become a no-op update. */
|
|
1126
|
+
function rejectUnknownArgs(args, allowed, tool) {
|
|
1127
|
+
for (const key of Object.keys(args)) {
|
|
1128
|
+
if (!allowed.includes(key))
|
|
1129
|
+
throw new CrudValidationError(`${tool} does not accept "${key}"`, key);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function optionalString(value, field, maxLength = 100_000) {
|
|
1133
|
+
if (value === undefined)
|
|
1134
|
+
return undefined;
|
|
1135
|
+
if (typeof value !== 'string')
|
|
1136
|
+
throw new CrudValidationError(`${field} must be a string`, field);
|
|
1137
|
+
if (value.length > maxLength)
|
|
1138
|
+
throw new CrudValidationError(`${field} exceeds ${maxLength} characters`, field);
|
|
1139
|
+
return value;
|
|
1140
|
+
}
|
|
1141
|
+
/** Dispatch for the ADR-017 CRUD tools. Returns a JSON-serialisable result. */
|
|
1142
|
+
async function handleCrudTool(name, args) {
|
|
1143
|
+
switch (name) {
|
|
1144
|
+
case 'agentdb_get': {
|
|
1145
|
+
rejectUnknownArgs(args, ['id', 'include_embedding'], name);
|
|
1146
|
+
const episode = reflexion.getEpisode(toRecordId(args.id), { includeEmbedding: args.include_embedding === true });
|
|
1147
|
+
if (!episode)
|
|
1148
|
+
return null;
|
|
1149
|
+
const { embedding, ...rest } = episode;
|
|
1150
|
+
return embedding ? { ...rest, embedding: Array.from(embedding) } : rest;
|
|
1151
|
+
}
|
|
1152
|
+
case 'agentdb_list': {
|
|
1153
|
+
rejectUnknownArgs(args, ['session_id', 'limit', 'offset', 'order'], name);
|
|
1154
|
+
const order = args.order === undefined ? 'newest' : validateEnum(args.order, 'order', ['newest', 'oldest']);
|
|
1155
|
+
return reflexion.listEpisodes({
|
|
1156
|
+
sessionId: args.session_id === undefined ? undefined : validateSessionId(args.session_id),
|
|
1157
|
+
limit: args.limit,
|
|
1158
|
+
offset: args.offset,
|
|
1159
|
+
order,
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
case 'agentdb_update': {
|
|
1163
|
+
rejectUnknownArgs(args, ['id', 'text', 'tags', 'metadata', 'replace_metadata', 'reward', 'success'], name);
|
|
1164
|
+
// agentdb_insert stores the text as both task and input, so an update keeps them in step.
|
|
1165
|
+
const text = args.text === undefined ? undefined : requireText(args.text, 'text');
|
|
1166
|
+
const result = await reflexion.updateEpisode(toRecordId(args.id), {
|
|
1167
|
+
task: text,
|
|
1168
|
+
input: text,
|
|
1169
|
+
tags: args.tags === undefined ? undefined : assertTags(args.tags),
|
|
1170
|
+
metadata: args.metadata === undefined ? undefined : assertMetadata(args.metadata),
|
|
1171
|
+
reward: args.reward === undefined ? undefined : validateNumericRange(args.reward, 'reward', -1e6, 1e6),
|
|
1172
|
+
success: args.success === undefined ? undefined : validateBoolean(args.success, 'success'),
|
|
1173
|
+
}, { replaceMetadata: args.replace_metadata === true });
|
|
1174
|
+
if (result)
|
|
1175
|
+
caches.stats.clear();
|
|
1176
|
+
return result ? { updated: true, ...result } : { updated: false, reason: 'not found' };
|
|
1177
|
+
}
|
|
1178
|
+
case 'agentdb_pattern_get': {
|
|
1179
|
+
rejectUnknownArgs(args, ['pattern_id'], name);
|
|
1180
|
+
const pattern = reasoningBank.getPattern(toRecordId(args.pattern_id, 'pattern_id'));
|
|
1181
|
+
if (!pattern)
|
|
1182
|
+
return null;
|
|
1183
|
+
const { embedding: _omit, ...rest } = pattern;
|
|
1184
|
+
return rest;
|
|
1185
|
+
}
|
|
1186
|
+
case 'agentdb_pattern_list': {
|
|
1187
|
+
rejectUnknownArgs(args, ['task_type', 'limit', 'offset'], name);
|
|
1188
|
+
return reasoningBank.listPatterns({
|
|
1189
|
+
taskType: optionalString(args.task_type, 'task_type', 1000),
|
|
1190
|
+
limit: args.limit,
|
|
1191
|
+
offset: args.offset,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
case 'agentdb_pattern_update': {
|
|
1195
|
+
rejectUnknownArgs(args, ['pattern_id', 'taskType', 'approach', 'tags', 'metadata', 'replace_metadata'], name);
|
|
1196
|
+
const result = await reasoningBank.updatePattern(toRecordId(args.pattern_id, 'pattern_id'), {
|
|
1197
|
+
taskType: args.taskType === undefined ? undefined : requireText(args.taskType, 'taskType'),
|
|
1198
|
+
approach: args.approach === undefined ? undefined : requireText(args.approach, 'approach'),
|
|
1199
|
+
tags: args.tags === undefined ? undefined : assertTags(args.tags),
|
|
1200
|
+
metadata: args.metadata === undefined ? undefined : assertMetadata(args.metadata),
|
|
1201
|
+
}, { replaceMetadata: args.replace_metadata === true });
|
|
1202
|
+
if (result) {
|
|
1203
|
+
caches.patterns.clear();
|
|
1204
|
+
caches.stats.clear();
|
|
1205
|
+
}
|
|
1206
|
+
if (!result)
|
|
1207
|
+
return { updated: false, reason: 'not found' };
|
|
1208
|
+
const { embedding: _omit, ...pattern } = result.pattern;
|
|
1209
|
+
return { updated: true, ...result, pattern };
|
|
1210
|
+
}
|
|
1211
|
+
case 'agentdb_pattern_delete': {
|
|
1212
|
+
rejectUnknownArgs(args, ['pattern_id'], name);
|
|
1213
|
+
const deleted = reasoningBank.deletePattern(toRecordId(args.pattern_id, 'pattern_id'));
|
|
1214
|
+
if (deleted) {
|
|
1215
|
+
caches.patterns.clear();
|
|
1216
|
+
caches.stats.clear();
|
|
1217
|
+
}
|
|
1218
|
+
return { deleted };
|
|
1219
|
+
}
|
|
1220
|
+
case 'skill_get': {
|
|
1221
|
+
rejectUnknownArgs(args, ['skill_id'], name);
|
|
1222
|
+
return skills.getSkill(toRecordId(args.skill_id, 'skill_id'));
|
|
1223
|
+
}
|
|
1224
|
+
case 'skill_list': {
|
|
1225
|
+
rejectUnknownArgs(args, ['min_success_rate', 'limit', 'offset'], name);
|
|
1226
|
+
return skills.listSkills({
|
|
1227
|
+
minSuccessRate: args.min_success_rate === undefined ? undefined : validateNumericRange(args.min_success_rate, 'min_success_rate', 0, 1),
|
|
1228
|
+
limit: args.limit,
|
|
1229
|
+
offset: args.offset,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
case 'skill_update': {
|
|
1233
|
+
rejectUnknownArgs(args, ['skill_id', 'name', 'description', 'code', 'signature', 'metadata', 'replace_metadata'], name);
|
|
1234
|
+
const result = await skills.updateSkill(toRecordId(args.skill_id, 'skill_id'), {
|
|
1235
|
+
name: args.name === undefined ? undefined : requireText(args.name, 'name'),
|
|
1236
|
+
description: args.description === undefined ? undefined : requireText(args.description, 'description'),
|
|
1237
|
+
code: optionalString(args.code, 'code'),
|
|
1238
|
+
signature: args.signature === undefined ? undefined
|
|
1239
|
+
: assertMetadata(args.signature, 'signature'),
|
|
1240
|
+
metadata: args.metadata === undefined ? undefined : assertMetadata(args.metadata),
|
|
1241
|
+
}, { replaceMetadata: args.replace_metadata === true });
|
|
1242
|
+
if (result)
|
|
1243
|
+
caches.stats.clear();
|
|
1244
|
+
return result ? { updated: true, ...result } : { updated: false, reason: 'not found' };
|
|
1245
|
+
}
|
|
1246
|
+
case 'skill_delete': {
|
|
1247
|
+
rejectUnknownArgs(args, ['skill_id'], name);
|
|
1248
|
+
const deleted = skills.deleteSkill(toRecordId(args.skill_id, 'skill_id'));
|
|
1249
|
+
if (deleted)
|
|
1250
|
+
caches.stats.clear();
|
|
1251
|
+
return { deleted };
|
|
1252
|
+
}
|
|
1253
|
+
default:
|
|
1254
|
+
throw new CrudValidationError(`unknown CRUD tool ${name}`, 'name');
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
/** Non-empty text up to `maxLength`, null bytes stripped. Deliberately no content heuristics: stored text may be code. */
|
|
1258
|
+
function requireText(value, field, maxLength = 100_000) {
|
|
1259
|
+
const text = sanitizeText(value, maxLength, field);
|
|
1260
|
+
if (!text.trim())
|
|
1261
|
+
throw new CrudValidationError(`${field} cannot be empty`, field);
|
|
1262
|
+
return text;
|
|
1263
|
+
}
|
|
975
1264
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
976
1265
|
const { name, arguments: args } = request.params;
|
|
977
1266
|
try {
|
|
@@ -989,7 +1278,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
989
1278
|
}
|
|
990
1279
|
// Initialize schema
|
|
991
1280
|
initializeSchema(db);
|
|
992
|
-
const stats = db.prepare(
|
|
1281
|
+
const stats = db.prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type = 'table'").get();
|
|
993
1282
|
return {
|
|
994
1283
|
content: [
|
|
995
1284
|
{
|
|
@@ -1004,10 +1293,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1004
1293
|
};
|
|
1005
1294
|
}
|
|
1006
1295
|
case 'agentdb_insert': {
|
|
1007
|
-
const text = args?.text;
|
|
1008
|
-
const sessionId = args?.session_id
|
|
1009
|
-
const tags = args?.tags
|
|
1010
|
-
const metadata = args?.metadata
|
|
1296
|
+
const text = requireText(args?.text, 'text');
|
|
1297
|
+
const sessionId = args?.session_id === undefined ? 'default' : validateSessionId(args.session_id);
|
|
1298
|
+
const tags = args?.tags === undefined ? [] : assertTags(args.tags);
|
|
1299
|
+
const metadata = args?.metadata === undefined ? {} : assertMetadata(args.metadata);
|
|
1011
1300
|
const episodeId = await reflexion.storeEpisode({
|
|
1012
1301
|
sessionId,
|
|
1013
1302
|
task: text,
|
|
@@ -1037,19 +1326,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1037
1326
|
case 'agentdb_insert_batch': {
|
|
1038
1327
|
const items = validateArrayLength(args?.items ?? [], 'items', 0, MAX_INSERT_BATCH_ITEMS);
|
|
1039
1328
|
const batchSize = args?.batch_size || 100;
|
|
1040
|
-
const episodes = items.map((item) =>
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1329
|
+
const episodes = items.map((item, i) => {
|
|
1330
|
+
if (!item || typeof item !== 'object')
|
|
1331
|
+
throw new ValidationError(`items[${i}] must be an object`, 'INVALID_TYPE', 'items');
|
|
1332
|
+
const text = requireText(item.text, `items[${i}].text`);
|
|
1333
|
+
return {
|
|
1334
|
+
sessionId: item.session_id === undefined ? 'default' : validateSessionId(item.session_id),
|
|
1335
|
+
task: text,
|
|
1336
|
+
reward: 1.0,
|
|
1337
|
+
success: true,
|
|
1338
|
+
input: text,
|
|
1339
|
+
output: '',
|
|
1340
|
+
critique: '',
|
|
1341
|
+
latencyMs: 0,
|
|
1342
|
+
tokensUsed: 0,
|
|
1343
|
+
tags: item.tags === undefined ? [] : assertTags(item.tags, `items[${i}].tags`),
|
|
1344
|
+
metadata: item.metadata === undefined ? {} : assertMetadata(item.metadata, `items[${i}].metadata`),
|
|
1345
|
+
};
|
|
1346
|
+
});
|
|
1053
1347
|
const batchOpsConfig = new BatchOperations(db, embeddingService, {
|
|
1054
1348
|
batchSize,
|
|
1055
1349
|
parallelism: 4,
|
|
@@ -1069,13 +1363,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1069
1363
|
};
|
|
1070
1364
|
}
|
|
1071
1365
|
case 'agentdb_search': {
|
|
1072
|
-
const queryText = args?.query;
|
|
1366
|
+
const queryText = validateTaskString(args?.query, 'query');
|
|
1073
1367
|
const k = Math.floor(validateNumericRange(args?.k ?? 10, 'k', 1, MAX_SEARCH_K));
|
|
1074
1368
|
const minSimilarity = args?.min_similarity || 0.0;
|
|
1075
1369
|
const filters = args?.filters;
|
|
1076
1370
|
const query = {
|
|
1077
1371
|
task: queryText,
|
|
1078
1372
|
k,
|
|
1373
|
+
hybrid: args?.hybrid !== false,
|
|
1374
|
+
rerank: args?.rerank === true,
|
|
1079
1375
|
};
|
|
1080
1376
|
if (filters) {
|
|
1081
1377
|
if (filters.min_reward !== undefined) {
|
|
@@ -1113,10 +1409,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1113
1409
|
if (id !== undefined) {
|
|
1114
1410
|
// Validate ID
|
|
1115
1411
|
const validatedId = validateId(id, 'id');
|
|
1116
|
-
//
|
|
1117
|
-
|
|
1118
|
-
const result = stmt.run(validatedId);
|
|
1119
|
-
deleted = result.changes;
|
|
1412
|
+
// Through the controller so the embedding, vector index and caches go with the row
|
|
1413
|
+
deleted = (await reflexion.deleteEpisode(validatedId)) ? 1 : 0;
|
|
1120
1414
|
}
|
|
1121
1415
|
else if (filters) {
|
|
1122
1416
|
// Bulk delete with validated filters
|
|
@@ -1124,17 +1418,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1124
1418
|
// Validate session_id
|
|
1125
1419
|
const validatedSessionId = validateSessionId(filters.session_id);
|
|
1126
1420
|
// Use parameterized query
|
|
1127
|
-
|
|
1128
|
-
const result = stmt.run(validatedSessionId);
|
|
1129
|
-
deleted = result.changes;
|
|
1421
|
+
deleted = deleteEpisodesWhere('session_id = ?', validatedSessionId);
|
|
1130
1422
|
}
|
|
1131
1423
|
else if (filters.before_timestamp) {
|
|
1132
1424
|
// Validate timestamp
|
|
1133
1425
|
const validatedTimestamp = validateTimestamp(filters.before_timestamp, 'before_timestamp');
|
|
1134
1426
|
// Use parameterized query
|
|
1135
|
-
|
|
1136
|
-
const result = stmt.run(validatedTimestamp);
|
|
1137
|
-
deleted = result.changes;
|
|
1427
|
+
deleted = deleteEpisodesWhere('ts < ?', validatedTimestamp);
|
|
1138
1428
|
}
|
|
1139
1429
|
else {
|
|
1140
1430
|
throw new ValidationError('Invalid or missing filter criteria', 'INVALID_FILTER');
|
|
@@ -1168,6 +1458,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1168
1458
|
}
|
|
1169
1459
|
}
|
|
1170
1460
|
// ======================================================================
|
|
1461
|
+
// CRUD: get / list / update / delete (ADR-017)
|
|
1462
|
+
// ======================================================================
|
|
1463
|
+
case 'agentdb_get':
|
|
1464
|
+
case 'agentdb_list':
|
|
1465
|
+
case 'agentdb_update':
|
|
1466
|
+
case 'agentdb_pattern_get':
|
|
1467
|
+
case 'agentdb_pattern_list':
|
|
1468
|
+
case 'agentdb_pattern_update':
|
|
1469
|
+
case 'agentdb_pattern_delete':
|
|
1470
|
+
case 'skill_get':
|
|
1471
|
+
case 'skill_list':
|
|
1472
|
+
case 'skill_update':
|
|
1473
|
+
case 'skill_delete': {
|
|
1474
|
+
try {
|
|
1475
|
+
const result = await handleCrudTool(name, (args ?? {}));
|
|
1476
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1477
|
+
}
|
|
1478
|
+
catch (error) {
|
|
1479
|
+
const message = error instanceof CrudValidationError ? error.message : handleSecurityError(error);
|
|
1480
|
+
return { content: [{ type: 'text', text: `❌ ${name} failed: ${message}` }], isError: true };
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
// ======================================================================
|
|
1171
1484
|
// FRONTIER MEMORY FEATURES (EXISTING)
|
|
1172
1485
|
// ======================================================================
|
|
1173
1486
|
case 'reflexion_store': {
|
|
@@ -2273,30 +2586,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2273
2586
|
};
|
|
2274
2587
|
}
|
|
2275
2588
|
case 'agentdb_delete_batch': {
|
|
2276
|
-
const ids = args?.ids
|
|
2589
|
+
const ids = validateArrayLength(args?.ids ?? [], 'ids', 0, MAX_INSERT_BATCH_ITEMS);
|
|
2277
2590
|
const requestedTable = args?.table || 'episodes';
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2591
|
+
// Each table with the embedding table that must be cleaned alongside it (FK cascades are off by default)
|
|
2592
|
+
const embeddingTables = {
|
|
2593
|
+
episodes: ['episode_embeddings', 'episode_id'],
|
|
2594
|
+
reasoning_patterns: ['pattern_embeddings', 'pattern_id'],
|
|
2595
|
+
};
|
|
2596
|
+
if (!Object.prototype.hasOwnProperty.call(embeddingTables, requestedTable)) {
|
|
2597
|
+
throw new ValidationError(`Invalid table: ${requestedTable}`, 'INVALID_TABLE', 'table');
|
|
2281
2598
|
}
|
|
2282
2599
|
if (ids.length === 0) {
|
|
2283
2600
|
return { content: [{ type: 'text', text: '⚠️ No IDs supplied — nothing to delete.' }] };
|
|
2284
2601
|
}
|
|
2285
|
-
|
|
2286
|
-
const safeIds = ids.map((raw) => {
|
|
2287
|
-
const n = Number(raw);
|
|
2288
|
-
if (!Number.isInteger(n) || n <= 0)
|
|
2289
|
-
throw new Error(`Invalid id: ${raw}`);
|
|
2290
|
-
return n;
|
|
2291
|
-
});
|
|
2602
|
+
const safeIds = ids.map((raw, i) => toRecordId(raw, `ids[${i}]`));
|
|
2292
2603
|
const placeholders = safeIds.map(() => '?').join(',');
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
const
|
|
2604
|
+
const [embTable, embColumn] = embeddingTables[requestedTable];
|
|
2605
|
+
// Table names come from the fixed map above; values are bound parameters.
|
|
2606
|
+
const changes = inTransaction(db, () => {
|
|
2607
|
+
db.prepare(`DELETE FROM ${embTable} WHERE ${embColumn} IN (${placeholders})`).run(...safeIds);
|
|
2608
|
+
return db.prepare(`DELETE FROM ${requestedTable} WHERE id IN (${placeholders})`).run(...safeIds).changes;
|
|
2609
|
+
});
|
|
2610
|
+
if (changes > 0) {
|
|
2611
|
+
if (requestedTable === 'episodes')
|
|
2612
|
+
reflexion.clearCache();
|
|
2613
|
+
else {
|
|
2614
|
+
reasoningBank.clearCache();
|
|
2615
|
+
caches.patterns.clear();
|
|
2616
|
+
}
|
|
2617
|
+
caches.stats.clear();
|
|
2618
|
+
}
|
|
2296
2619
|
return {
|
|
2297
2620
|
content: [{
|
|
2298
2621
|
type: 'text',
|
|
2299
|
-
text: `🗑️ Batch delete from \`${requestedTable}\`: ${
|
|
2622
|
+
text: `🗑️ Batch delete from \`${requestedTable}\`: ${changes}/${safeIds.length} rows removed (atomic).`,
|
|
2300
2623
|
}],
|
|
2301
2624
|
};
|
|
2302
2625
|
}
|
|
@@ -2321,11 +2644,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2321
2644
|
}
|
|
2322
2645
|
}
|
|
2323
2646
|
catch (error) {
|
|
2647
|
+
if (!(error instanceof ValidationError))
|
|
2648
|
+
console.error(`[agentdb-mcp] ${name} failed:`, error);
|
|
2324
2649
|
return {
|
|
2325
2650
|
content: [
|
|
2326
2651
|
{
|
|
2327
2652
|
type: 'text',
|
|
2328
|
-
|
|
2653
|
+
// The stack trace stays in the server log; clients only see the message.
|
|
2654
|
+
text: `❌ Error: ${error?.message ?? String(error)}`,
|
|
2329
2655
|
},
|
|
2330
2656
|
],
|
|
2331
2657
|
isError: true,
|