agentdb 3.0.0 → 3.2.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.
Files changed (57) hide show
  1. package/README.md +13 -0
  2. package/dist/src/backends/ExactSearch.d.ts +22 -0
  3. package/dist/src/backends/ExactSearch.d.ts.map +1 -0
  4. package/dist/src/backends/ExactSearch.js +36 -0
  5. package/dist/src/backends/ExactSearch.js.map +1 -0
  6. package/dist/src/backends/VectorBackend.d.ts +33 -1
  7. package/dist/src/backends/VectorBackend.d.ts.map +1 -1
  8. package/dist/src/backends/VectorBackend.js +58 -4
  9. package/dist/src/backends/VectorBackend.js.map +1 -1
  10. package/dist/src/backends/rvf/SqlJsRvfBackend.d.ts.map +1 -1
  11. package/dist/src/backends/rvf/SqlJsRvfBackend.js +22 -12
  12. package/dist/src/backends/rvf/SqlJsRvfBackend.js.map +1 -1
  13. package/dist/src/controllers/EmbeddingService.d.ts +10 -0
  14. package/dist/src/controllers/EmbeddingService.d.ts.map +1 -1
  15. package/dist/src/controllers/EmbeddingService.js +12 -0
  16. package/dist/src/controllers/EmbeddingService.js.map +1 -1
  17. package/dist/src/controllers/ReasoningBank.d.ts +40 -4
  18. package/dist/src/controllers/ReasoningBank.d.ts.map +1 -1
  19. package/dist/src/controllers/ReasoningBank.js +124 -57
  20. package/dist/src/controllers/ReasoningBank.js.map +1 -1
  21. package/dist/src/controllers/ReflexionMemory.d.ts +20 -0
  22. package/dist/src/controllers/ReflexionMemory.d.ts.map +1 -1
  23. package/dist/src/controllers/ReflexionMemory.js +104 -17
  24. package/dist/src/controllers/ReflexionMemory.js.map +1 -1
  25. package/dist/src/controllers/SkillLibrary.d.ts +9 -0
  26. package/dist/src/controllers/SkillLibrary.d.ts.map +1 -1
  27. package/dist/src/controllers/SkillLibrary.js +60 -20
  28. package/dist/src/controllers/SkillLibrary.js.map +1 -1
  29. package/dist/src/controllers/WASMVectorSearch.d.ts.map +1 -1
  30. package/dist/src/controllers/WASMVectorSearch.js +48 -17
  31. package/dist/src/controllers/WASMVectorSearch.js.map +1 -1
  32. package/dist/src/controllers/episode-hybrid.d.ts +63 -0
  33. package/dist/src/controllers/episode-hybrid.d.ts.map +1 -0
  34. package/dist/src/controllers/episode-hybrid.js +117 -0
  35. package/dist/src/controllers/episode-hybrid.js.map +1 -0
  36. package/dist/src/mcp/agentdb-mcp-server.js +146 -3
  37. package/dist/src/mcp/agentdb-mcp-server.js.map +1 -1
  38. package/dist/src/search/conversation-retrieval.d.ts +56 -0
  39. package/dist/src/search/conversation-retrieval.d.ts.map +1 -0
  40. package/dist/src/search/conversation-retrieval.js +161 -0
  41. package/dist/src/search/conversation-retrieval.js.map +1 -0
  42. package/dist/src/search/cross-encoder.d.ts +17 -0
  43. package/dist/src/search/cross-encoder.d.ts.map +1 -0
  44. package/dist/src/search/cross-encoder.js +52 -0
  45. package/dist/src/search/cross-encoder.js.map +1 -0
  46. package/dist/src/security/mcp-policy.d.ts.map +1 -1
  47. package/dist/src/security/mcp-policy.js +4 -0
  48. package/dist/src/security/mcp-policy.js.map +1 -1
  49. package/dist/src/simd/simd-vector-ops.d.ts +11 -0
  50. package/dist/src/simd/simd-vector-ops.d.ts.map +1 -1
  51. package/dist/src/simd/simd-vector-ops.js +67 -0
  52. package/dist/src/simd/simd-vector-ops.js.map +1 -1
  53. package/dist/src/utils/top-k.d.ts +44 -0
  54. package/dist/src/utils/top-k.d.ts.map +1 -0
  55. package/dist/src/utils/top-k.js +111 -0
  56. package/dist/src/utils/top-k.js.map +1 -0
  57. package/package.json +2 -2
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Hybrid (dense + BM25) episode retrieval for ReflexionMemory (ADR-016).
3
+ *
4
+ * Dense search alone misses episodes whose wording matches the query but whose embedding does not (names, error
5
+ * codes, numbers). This keeps a BM25 index over the most recent episodes, rebuilt only when the episodes table
6
+ * changes, and fuses its ranking with the dense ranking by reciprocal-rank fusion. On LongMemEval session retrieval
7
+ * (benchmarks/longmemeval, which runs these ranking functions over turn chunks, not the episode store), the same
8
+ * fusion lifts recall_all@5 over dense-only ranking of the same chunks from 0.883 to 0.900 (S) and from 0.630 to
9
+ * 0.719 (M), with the default MiniLM embedder. See ADR-016.
10
+ */
11
+ import type { IDatabaseConnection } from '../types/database.types.js';
12
+ /**
13
+ * Episodes indexed for BM25, newest first; older episodes are still found by dense search. The first search in a
14
+ * process builds the index (about 200 ms at 10k episodes, 1 s at 50k); later writes are appended incrementally.
15
+ */
16
+ export declare const HYBRID_MAX_INDEXED: number;
17
+ /** The candidate pool each side contributes before fusion. */
18
+ export declare const HYBRID_MAX_POOL = 500;
19
+ /** Weight of the BM25 ranking against the dense ranking. Equal weights won on LongMemEval_M; see ADR-016. */
20
+ export declare const HYBRID_LEXICAL_WEIGHT = 1;
21
+ /**
22
+ * A BM25 index over episode text. New episodes are appended incrementally (the common write-then-read pattern);
23
+ * the index is rebuilt only when rows were deleted or replaced, or it has grown past 1.25x its cap.
24
+ */
25
+ export declare class EpisodeLexicalIndex {
26
+ private readonly db;
27
+ private readonly maxDocs;
28
+ private count;
29
+ private maxId;
30
+ private ids;
31
+ private index;
32
+ constructor(db: IDatabaseConnection, maxDocs?: number);
33
+ private static textOf;
34
+ private refresh;
35
+ /** Episode ids with a BM25 match, best first, at most `limit`. */
36
+ search(query: string, limit: number): {
37
+ id: number;
38
+ score: number;
39
+ }[];
40
+ }
41
+ /**
42
+ * Weighted RRF scores of a dense ranking (best first) and a lexical ranking (best first) over episode ids.
43
+ * Episodes only in the lexical list are ranked just past the dense pool on the dense side.
44
+ */
45
+ export declare function fuseEpisodeScores(denseIds: readonly number[], lexical: readonly {
46
+ id: number;
47
+ }[], lexicalWeight?: number): Map<number, number>;
48
+ /** Ids ordered by score, ties broken by dense order, then by newer id. */
49
+ export declare function orderByScore(score: ReadonlyMap<number, number>, denseIds?: readonly number[]): number[];
50
+ /** Fused ranking of episode ids, best first. */
51
+ export declare function fuseEpisodeRankings(denseIds: readonly number[], lexical: readonly {
52
+ id: number;
53
+ }[], lexicalWeight?: number): number[];
54
+ /** Candidates the cross-encoder reads: the top `RERANK_DEPTH` of each ranking (the configuration benchmarked in ADR-016). */
55
+ export declare const RERANK_DEPTH = 60;
56
+ /** Weight of the cross-encoder rank in the fusion; best on LongMemEval_M (ADR-016). */
57
+ export declare const RERANK_WEIGHT = 8;
58
+ /**
59
+ * Adds `RERANK_WEIGHT`/(k + cross-encoder rank) to each candidate's fused score, in place. Candidates without text
60
+ * are skipped. Throws if the model cannot be loaded; the caller keeps the hybrid order.
61
+ */
62
+ export declare function applyCrossEncoder(query: string, score: Map<number, number>, candidates: readonly number[], textOf: (id: number) => string | undefined, model?: string): Promise<void>;
63
+ //# sourceMappingURL=episode-hybrid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"episode-hybrid.d.ts","sourceRoot":"","sources":["../../../src/controllers/episode-hybrid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAItE;;;GAGG;AACH,eAAO,MAAM,kBAAkB,QAA0E,CAAC;AAM1G,8DAA8D;AAC9D,eAAO,MAAM,eAAe,MAAM,CAAC;AACnC,6GAA6G;AAC7G,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAWvC;;;GAGG;AACH,qBAAa,mBAAmB;IAO5B,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAP1B,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,GAAG,CAAgB;IAC3B,OAAO,CAAC,KAAK,CAA0B;gBAGpB,EAAE,EAAE,mBAAmB,EACvB,OAAO,SAAqB;IAG/C,OAAO,CAAC,MAAM,CAAC,MAAM;IAIrB,OAAO,CAAC,OAAO;IA4Bf,kEAAkE;IAClE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE;CAStE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,aAAa,SAAwB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAS7J;AAED,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,GAAE,SAAS,MAAM,EAAO,GAAG,MAAM,EAAE,CAM3G;AAED,gDAAgD;AAChD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAAE,EAAE,aAAa,SAAwB,GAAG,MAAM,EAAE,CAEpJ;AAED,6HAA6H;AAC7H,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,uFAAuF;AACvF,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAC1B,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,EAC1C,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAKf"}
@@ -0,0 +1,117 @@
1
+ import { Bm25Index, tokenize } from '../search/conversation-retrieval.js';
2
+ import { crossEncoderScores } from '../search/cross-encoder.js';
3
+ /**
4
+ * Episodes indexed for BM25, newest first; older episodes are still found by dense search. The first search in a
5
+ * process builds the index (about 200 ms at 10k episodes, 1 s at 50k); later writes are appended incrementally.
6
+ */
7
+ export const HYBRID_MAX_INDEXED = clampInt(process.env.AGENTDB_HYBRID_MAX_EPISODES, 10_000, 1, 1_000_000);
8
+ function clampInt(raw, fallback, min, max) {
9
+ const n = Math.floor(Number(raw));
10
+ return Number.isFinite(n) && n >= min ? Math.min(n, max) : fallback;
11
+ }
12
+ /** The candidate pool each side contributes before fusion. */
13
+ export const HYBRID_MAX_POOL = 500;
14
+ /** Weight of the BM25 ranking against the dense ranking. Equal weights won on LongMemEval_M; see ADR-016. */
15
+ export const HYBRID_LEXICAL_WEIGHT = 1;
16
+ const RRF_K = 60;
17
+ /**
18
+ * A BM25 index over episode text. New episodes are appended incrementally (the common write-then-read pattern);
19
+ * the index is rebuilt only when rows were deleted or replaced, or it has grown past 1.25x its cap.
20
+ */
21
+ export class EpisodeLexicalIndex {
22
+ db;
23
+ maxDocs;
24
+ count = -1;
25
+ maxId = 0;
26
+ ids = [];
27
+ index = null;
28
+ constructor(db, maxDocs = HYBRID_MAX_INDEXED) {
29
+ this.db = db;
30
+ this.maxDocs = maxDocs;
31
+ }
32
+ static textOf(r) {
33
+ return tokenize([r.task, r.input, r.output, r.critique].filter(Boolean).join('\n'));
34
+ }
35
+ refresh() {
36
+ const sig = this.db.prepare('SELECT COUNT(*) AS c, MAX(id) AS m FROM episodes').get();
37
+ const count = sig?.c ?? 0;
38
+ const maxId = sig?.m ?? 0;
39
+ if (this.index && count === this.count && maxId === this.maxId)
40
+ return;
41
+ if (this.index && count > this.count && maxId > this.maxId && this.ids.length < this.maxDocs * 1.25) {
42
+ const added = this.db
43
+ .prepare('SELECT id, task, input, output, critique FROM episodes WHERE id > ? ORDER BY id ASC')
44
+ .all(this.maxId);
45
+ if (this.count + added.length === count) {
46
+ for (const r of added) {
47
+ this.index.add(EpisodeLexicalIndex.textOf(r));
48
+ this.ids.push(r.id);
49
+ }
50
+ this.count = count;
51
+ this.maxId = maxId;
52
+ return;
53
+ }
54
+ }
55
+ const rows = this.db
56
+ .prepare('SELECT id, task, input, output, critique FROM episodes ORDER BY id DESC LIMIT ?')
57
+ .all(this.maxDocs);
58
+ this.index = new Bm25Index(rows.map(r => EpisodeLexicalIndex.textOf(r)));
59
+ this.ids = rows.map(r => r.id);
60
+ this.count = count;
61
+ this.maxId = maxId;
62
+ }
63
+ /** Episode ids with a BM25 match, best first, at most `limit`. */
64
+ search(query, limit) {
65
+ this.refresh();
66
+ if (!this.index || this.ids.length === 0)
67
+ return [];
68
+ const scores = this.index.score(tokenize(query, 256));
69
+ const hits = [];
70
+ for (let i = 0; i < scores.length; i++)
71
+ if (scores[i] > 0)
72
+ hits.push({ id: this.ids[i], score: scores[i] });
73
+ hits.sort((a, z) => z.score - a.score || z.id - a.id);
74
+ return hits.slice(0, limit);
75
+ }
76
+ }
77
+ /**
78
+ * Weighted RRF scores of a dense ranking (best first) and a lexical ranking (best first) over episode ids.
79
+ * Episodes only in the lexical list are ranked just past the dense pool on the dense side.
80
+ */
81
+ export function fuseEpisodeScores(denseIds, lexical, lexicalWeight = HYBRID_LEXICAL_WEIGHT) {
82
+ const score = new Map();
83
+ const denseMiss = denseIds.length + 1;
84
+ denseIds.forEach((id, r) => score.set(id, 1 / (RRF_K + r + 1)));
85
+ lexical.forEach(({ id }, r) => {
86
+ const base = score.get(id) ?? 1 / (RRF_K + denseMiss);
87
+ score.set(id, base + lexicalWeight / (RRF_K + r + 1));
88
+ });
89
+ return score;
90
+ }
91
+ /** Ids ordered by score, ties broken by dense order, then by newer id. */
92
+ export function orderByScore(score, denseIds = []) {
93
+ const order = new Map();
94
+ denseIds.forEach((id, i) => order.set(id, i));
95
+ return [...score.entries()]
96
+ .sort((a, z) => z[1] - a[1] || (order.get(a[0]) ?? Infinity) - (order.get(z[0]) ?? Infinity) || z[0] - a[0])
97
+ .map(([id]) => id);
98
+ }
99
+ /** Fused ranking of episode ids, best first. */
100
+ export function fuseEpisodeRankings(denseIds, lexical, lexicalWeight = HYBRID_LEXICAL_WEIGHT) {
101
+ return orderByScore(fuseEpisodeScores(denseIds, lexical, lexicalWeight), denseIds);
102
+ }
103
+ /** Candidates the cross-encoder reads: the top `RERANK_DEPTH` of each ranking (the configuration benchmarked in ADR-016). */
104
+ export const RERANK_DEPTH = 60;
105
+ /** Weight of the cross-encoder rank in the fusion; best on LongMemEval_M (ADR-016). */
106
+ export const RERANK_WEIGHT = 8;
107
+ /**
108
+ * Adds `RERANK_WEIGHT`/(k + cross-encoder rank) to each candidate's fused score, in place. Candidates without text
109
+ * are skipped. Throws if the model cannot be loaded; the caller keeps the hybrid order.
110
+ */
111
+ export async function applyCrossEncoder(query, score, candidates, textOf, model) {
112
+ const ids = candidates.filter(id => score.has(id) && textOf(id));
113
+ const ce = await crossEncoderScores(query, ids.map(id => textOf(id)), model);
114
+ const byCe = ids.map((_, i) => i).sort((a, z) => ce[z] - ce[a] || a - z);
115
+ byCe.forEach((i, r) => score.set(ids[i], score.get(ids[i]) + RERANK_WEIGHT / (RRF_K + r + 1)));
116
+ }
117
+ //# sourceMappingURL=episode-hybrid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"episode-hybrid.js","sourceRoot":"","sources":["../../../src/controllers/episode-hybrid.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,11 @@
7
7
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
8
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
9
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
10
- import { createDatabase } from '../db-fallback.js';
10
+ import { createDatabase, getDatabaseInfo } from '../db-fallback.js';
11
+ import { NativeAccelerator } from '../backends/rvf/NativeAccelerator.js';
12
+ import { detectBackends } from '../backends/factory.js';
13
+ import { DEFAULT_EXACT_SEARCH_THRESHOLD } from '../backends/VectorBackend.js';
14
+ import { DEFAULT_RERANK_WEIGHTS } from '../controllers/ReasoningBank.js';
11
15
  import { CausalMemoryGraph } from '../controllers/CausalMemoryGraph.js';
12
16
  import { CausalRecall } from '../controllers/CausalRecall.js';
13
17
  import { ReflexionMemory } from '../controllers/ReflexionMemory.js';
@@ -32,6 +36,21 @@ const toolPolicy = buildMcpToolPolicy();
32
36
  // Request-size limits at the MCP boundary
33
37
  const MAX_INSERT_BATCH_ITEMS = 1000;
34
38
  const MAX_SEARCH_K = 100;
39
+ const MAX_FEEDBACK_LATENCY_MS = 24 * 60 * 60 * 1000;
40
+ /**
41
+ * Outcome feedback at the MCP boundary (ADR-015): a boolean success, a reward
42
+ * in [0, 1], or both. A reward alone counts as a success when >= 0.5.
43
+ */
44
+ function validateFeedback(args) {
45
+ const hasReward = args?.reward !== undefined && args?.reward !== null;
46
+ const hasSuccess = args?.success !== undefined && args?.success !== null;
47
+ if (!hasReward && !hasSuccess) {
48
+ throw new ValidationError('success or reward is required', 'MISSING_REQUIRED_FIELD', 'success');
49
+ }
50
+ const reward = hasReward ? validateNumericRange(args.reward, 'reward', 0, 1) : undefined;
51
+ const success = hasSuccess ? validateBoolean(args.success, 'success') : reward >= 0.5;
52
+ return { success, reward };
53
+ }
35
54
  // stdout belongs exclusively to JSON-RPC when running over stdio. Route every
36
55
  // transitive diagnostic (including legacy controller console.log calls) to
37
56
  // stderr before database and embedding initialization begins.
@@ -242,6 +261,40 @@ const learningSystem = new LearningSystem(db, embeddingService);
242
261
  const batchOps = new BatchOperations(db, embeddingService);
243
262
  const reasoningBank = new ReasoningBank(db, embeddingService);
244
263
  const caches = new MCPToolCaches();
264
+ // agentdb_capabilities: native probing is done once, on first request
265
+ let acceleratorProbe = null;
266
+ async function describeCapabilities() {
267
+ acceleratorProbe ??= (async () => {
268
+ const accelerator = new NativeAccelerator();
269
+ await accelerator.initialize();
270
+ return accelerator;
271
+ })();
272
+ const [accelerator, detected] = await Promise.all([acceleratorProbe, detectBackends()]);
273
+ const exactThreshold = Number(process.env.AGENTDB_EXACT_SEARCH_THRESHOLD);
274
+ return {
275
+ nativeAccelerator: accelerator.capabilities(),
276
+ vectorBackend: {
277
+ // This server's controllers are built without a vector index: every
278
+ // search here is an exact cosine scan over the SQL embedding tables.
279
+ inUse: 'sql-exact-scan',
280
+ available: detected.available,
281
+ ruvector: detected.ruvector,
282
+ rvf: detected.rvf,
283
+ hnswlib: detected.hnswlib,
284
+ sqljsRvf: detected.sqljsRvf,
285
+ ruvectorBackendEnv: process.env.RUVECTOR_BACKEND ?? null,
286
+ },
287
+ sqlDriver: getDatabaseInfo(),
288
+ embedding: embeddingService.getRuntimeInfo(),
289
+ retrieval: {
290
+ exactSearchThreshold: Number.isFinite(exactThreshold) && exactThreshold >= 0
291
+ ? exactThreshold
292
+ : DEFAULT_EXACT_SEARCH_THRESHOLD,
293
+ patternRerankWeights: DEFAULT_RERANK_WEIGHTS,
294
+ },
295
+ mcpProfile: toolPolicy.profile,
296
+ };
297
+ }
245
298
  // ============================================================================
246
299
  // MCP Server Setup
247
300
  // ============================================================================
@@ -312,13 +365,15 @@ const tools = [
312
365
  },
313
366
  {
314
367
  name: 'agentdb_search',
315
- description: 'Semantic k-NN vector search using cosine similarity. Returns the most relevant results ranked by similarity score.',
368
+ description: 'Hybrid episode search: dense k-NN (cosine) fused with BM25 keyword ranking by reciprocal-rank fusion. Set hybrid=false for pure vector order.',
316
369
  inputSchema: {
317
370
  type: 'object',
318
371
  properties: {
319
372
  query: { type: 'string', description: 'Search query text' },
320
373
  k: { type: 'number', description: 'Number of results to return (max 100)', default: 10, minimum: 1, maximum: 100 },
321
374
  min_similarity: { type: 'number', description: 'Minimum similarity threshold (0-1)', default: 0.0 },
375
+ hybrid: { type: 'boolean', description: 'Fuse BM25 keyword ranking with the vector ranking (ADR-016)', default: true },
376
+ 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 },
322
377
  filters: {
323
378
  type: 'object',
324
379
  properties: {
@@ -708,6 +763,41 @@ const tools = [
708
763
  properties: {},
709
764
  },
710
765
  },
766
+ {
767
+ name: 'agentdb_pattern_feedback',
768
+ description: 'Record the outcome of using a reasoning pattern. Updates its success rate and average reward, which agentdb_pattern_search ranks by (0.7 similarity + 0.2 success rate + 0.1 reward). Give success, reward, or both.',
769
+ inputSchema: {
770
+ type: 'object',
771
+ properties: {
772
+ pattern_id: { type: 'integer', minimum: 0, description: 'Pattern ID from agentdb_pattern_store or agentdb_pattern_search' },
773
+ success: { type: 'boolean', description: 'Whether applying the pattern succeeded' },
774
+ reward: { type: 'number', minimum: 0, maximum: 1, description: 'Reward in [0, 1]; defaults to 1 for success, 0 for failure. A reward alone counts as success when >= 0.5' },
775
+ },
776
+ required: ['pattern_id'],
777
+ },
778
+ },
779
+ {
780
+ name: 'agentdb_skill_feedback',
781
+ description: 'Record the outcome of using a skill. Updates its uses, success rate, average reward and (optionally) latency, which skill_search ranks by. Give success, reward, or both.',
782
+ inputSchema: {
783
+ type: 'object',
784
+ properties: {
785
+ skill_id: { type: 'integer', minimum: 0, description: 'Skill ID from skill_create or skill_search' },
786
+ success: { type: 'boolean', description: 'Whether the skill succeeded' },
787
+ reward: { type: 'number', minimum: 0, maximum: 1, description: 'Reward in [0, 1]; defaults to 1 for success, 0 for failure. A reward alone counts as success when >= 0.5' },
788
+ latency_ms: { type: 'number', minimum: 0, description: 'Observed latency in milliseconds (optional)' },
789
+ },
790
+ required: ['skill_id'],
791
+ },
792
+ },
793
+ {
794
+ name: 'agentdb_capabilities',
795
+ description: 'Report what this AgentDB server is running on: native accelerator features that loaded, the vector backend in use and the ones available, the SQL driver, the embedding model and dimension (and whether it is the hash fallback), and the ranking defaults. Read-only.',
796
+ inputSchema: {
797
+ type: 'object',
798
+ properties: {},
799
+ },
800
+ },
711
801
  {
712
802
  name: 'agentdb_clear_cache',
713
803
  description: 'Clear query cache to refresh statistics and search results',
@@ -981,13 +1071,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
981
1071
  };
982
1072
  }
983
1073
  case 'agentdb_search': {
984
- const queryText = args?.query;
1074
+ const queryText = validateTaskString(args?.query, 'query');
985
1075
  const k = Math.floor(validateNumericRange(args?.k ?? 10, 'k', 1, MAX_SEARCH_K));
986
1076
  const minSimilarity = args?.min_similarity || 0.0;
987
1077
  const filters = args?.filters;
988
1078
  const query = {
989
1079
  task: queryText,
990
1080
  k,
1081
+ hybrid: args?.hybrid !== false,
1082
+ rerank: args?.rerank === true,
991
1083
  };
992
1084
  if (filters) {
993
1085
  if (filters.min_reward !== undefined) {
@@ -1403,6 +1495,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1403
1495
  `Top Results:\n` +
1404
1496
  patterns.slice(0, 5).map((p, i) => `${i + 1}. [ID: ${p.id}] ${p.taskType}\n` +
1405
1497
  ` Similarity: ${(p.similarity || 0).toFixed(3)}\n` +
1498
+ (p.score !== undefined ? ` Rank score: ${p.score.toFixed(3)} (similarity + outcome)\n` : '') +
1406
1499
  ` Success Rate: ${(p.successRate * 100).toFixed(1)}%\n` +
1407
1500
  ` Approach: ${p.approach.substring(0, 80)}${p.approach.length > 80 ? '...' : ''}\n` +
1408
1501
  ` Uses: ${p.uses || 0}`).join('\n\n') +
@@ -1447,6 +1540,56 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1447
1540
  ],
1448
1541
  };
1449
1542
  }
1543
+ case 'agentdb_pattern_feedback': {
1544
+ const patternId = validateId(args?.pattern_id, 'pattern_id');
1545
+ const { success, reward } = validateFeedback(args);
1546
+ if (!(await reasoningBank.recordOutcome(patternId, success, reward))) {
1547
+ throw new ValidationError(`Pattern ${patternId} not found`, 'NOT_FOUND', 'pattern_id');
1548
+ }
1549
+ caches.stats.clear();
1550
+ const pattern = reasoningBank.getPattern(patternId);
1551
+ return {
1552
+ content: [{
1553
+ type: 'text',
1554
+ text: JSON.stringify({
1555
+ patternId,
1556
+ recorded: { success, reward: reward ?? (success ? 1 : 0) },
1557
+ uses: pattern.uses,
1558
+ successRate: pattern.successRate,
1559
+ avgReward: pattern.avgReward,
1560
+ }, null, 2),
1561
+ }],
1562
+ };
1563
+ }
1564
+ case 'agentdb_skill_feedback': {
1565
+ const skillId = validateId(args?.skill_id, 'skill_id');
1566
+ const { success, reward } = validateFeedback(args);
1567
+ const latencyMs = args?.latency_ms === undefined || args?.latency_ms === null
1568
+ ? undefined
1569
+ : validateNumericRange(args.latency_ms, 'latency_ms', 0, MAX_FEEDBACK_LATENCY_MS);
1570
+ if (!skills.recordOutcome(skillId, success, reward, latencyMs)) {
1571
+ throw new ValidationError(`Skill ${skillId} not found`, 'NOT_FOUND', 'skill_id');
1572
+ }
1573
+ const row = db.prepare('SELECT uses, success_rate, avg_reward, avg_latency_ms FROM skills WHERE id = ?').get(skillId);
1574
+ return {
1575
+ content: [{
1576
+ type: 'text',
1577
+ text: JSON.stringify({
1578
+ skillId,
1579
+ recorded: { success, reward: reward ?? (success ? 1 : 0), latencyMs },
1580
+ uses: row.uses,
1581
+ successRate: row.success_rate,
1582
+ avgReward: row.avg_reward,
1583
+ avgLatencyMs: row.avg_latency_ms,
1584
+ }, null, 2),
1585
+ }],
1586
+ };
1587
+ }
1588
+ case 'agentdb_capabilities': {
1589
+ return {
1590
+ content: [{ type: 'text', text: JSON.stringify(await describeCapabilities(), null, 2) }],
1591
+ };
1592
+ }
1450
1593
  case 'agentdb_clear_cache': {
1451
1594
  const cacheType = args?.cache_type || 'all';
1452
1595
  let cleared = 0;