@melandlabs/memory-store 0.3.1 → 1.0.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 +209 -56
- package/dist/{config-2Q092MUz.d.ts → config-CnbqSek5.d.ts} +215 -2
- package/dist/contracts.d.ts +1 -0
- package/dist/contracts.js +2 -0
- package/dist/http.d.ts +21 -5
- package/dist/http.js +1047 -150
- package/dist/index.d.ts +230 -15
- package/dist/index.js +1083 -147
- package/dist/mcp.d.ts +14 -5
- package/dist/mcp.js +1184 -169
- package/dist/policies/memory-graph-correction-policy.d.ts +1 -0
- package/dist/policies/memory-graph-correction-policy.js +4 -0
- package/dist/policies/memory-graph-write-policy.d.ts +1 -0
- package/dist/policies/memory-graph-write-policy.js +2 -0
- package/dist/search/unified-search.d.ts +6 -19
- package/dist/search/unified-search.js +745 -140
- package/dist/server/cli-http.js +1047 -150
- package/dist/server/cli-mcp.js +1181 -166
- package/dist/server/cli-shared.d.ts +5 -2
- package/dist/server/cli-shared.js +3 -0
- package/dist/storage/chroma-memory-index.d.ts +1 -0
- package/dist/storage/chroma-memory-index.js +1 -2
- package/dist/storage/postgres-raw-message-factory.d.ts +4 -1
- package/dist/storage/raw-message-store.d.ts +5 -2
- package/dist/storage/raw-message-store.js +3 -0
- package/dist/storage/sqlite-raw-message-store.d.ts +5 -1
- package/dist/storage/sqlite-raw-message-store.js +3 -0
- package/dist/storage/sqlite-vector-index.d.ts +4 -1
- package/dist/storage/sqlite-vector-index.js +1 -0
- package/dist/unified-search-Bft57TRH.d.ts +122 -0
- package/package.json +9 -6
package/dist/mcp.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { vsaNormalizeVector, vsaAssertSameDim } from '@melandlabs/contracts';
|
|
1
2
|
import { buildMemoryRecordEmbeddingDocument } from '@melandlabs/ai/memory';
|
|
2
3
|
import { ChromaVectorStore } from '@melandlabs/rag';
|
|
3
4
|
import { mkdirSync } from 'fs';
|
|
4
5
|
import { dirname } from 'path';
|
|
5
6
|
import { getOpenContextPath } from '@melandlabs/env-config';
|
|
6
|
-
import { SQLiteRawMessageManager } from '@melandlabs/sqlite';
|
|
7
|
+
import { SQLiteRawMessageManager, getSQLiteVsaStore } from '@melandlabs/sqlite';
|
|
7
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
10
|
import { z } from 'zod';
|
|
11
|
+
import { applyReflectedConsolidationPlan } from '@melandlabs/memory-consolidation';
|
|
12
|
+
import { superpose, bind, unbind, cleanup, cosineSimilarity } from '@melandlabs/vsa';
|
|
10
13
|
|
|
11
14
|
var __defProp = Object.defineProperty;
|
|
12
15
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -17,8 +20,6 @@ var __export = (target, all) => {
|
|
|
17
20
|
for (var name in all)
|
|
18
21
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
22
|
};
|
|
20
|
-
|
|
21
|
-
// src/contracts.ts
|
|
22
23
|
function normalizeTimestampToMs(value) {
|
|
23
24
|
if (!Number.isFinite(value)) {
|
|
24
25
|
return 0;
|
|
@@ -325,6 +326,41 @@ var init_raw_message_store = __esm({
|
|
|
325
326
|
init_chroma_memory_index();
|
|
326
327
|
init_raw_message_store();
|
|
327
328
|
|
|
329
|
+
// src/search/reranker.ts
|
|
330
|
+
async function applyReranker(reranker, query, results) {
|
|
331
|
+
if (!reranker || results.length === 0) {
|
|
332
|
+
return results;
|
|
333
|
+
}
|
|
334
|
+
const scores = await reranker.rerank({
|
|
335
|
+
query,
|
|
336
|
+
candidates: results.map((result) => ({
|
|
337
|
+
id: result.id,
|
|
338
|
+
content: result.content,
|
|
339
|
+
metadata: result.metadata
|
|
340
|
+
}))
|
|
341
|
+
});
|
|
342
|
+
if (scores.length === 0) {
|
|
343
|
+
return results;
|
|
344
|
+
}
|
|
345
|
+
const byId = new Map(results.map((result) => [result.id, result]));
|
|
346
|
+
const reordered = [];
|
|
347
|
+
const seen = /* @__PURE__ */ new Set();
|
|
348
|
+
for (const { id, score } of scores) {
|
|
349
|
+
const hit = byId.get(id);
|
|
350
|
+
if (!hit || seen.has(id)) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
seen.add(id);
|
|
354
|
+
reordered.push({ ...hit, metadata: { ...hit.metadata, rerankerScore: score } });
|
|
355
|
+
}
|
|
356
|
+
for (const hit of results) {
|
|
357
|
+
if (!seen.has(hit.id)) {
|
|
358
|
+
reordered.push(hit);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return reordered;
|
|
362
|
+
}
|
|
363
|
+
|
|
328
364
|
// src/search/utilities.ts
|
|
329
365
|
var DEFAULT_LIMIT = 10;
|
|
330
366
|
var DEFAULT_THRESHOLD = 0.7;
|
|
@@ -446,6 +482,264 @@ function isRawMemorySemanticResult(result) {
|
|
|
446
482
|
const item = result;
|
|
447
483
|
return typeof item.id === "string" && typeof item.content === "string" && typeof item.similarity === "number" && Boolean(item.metadata) && typeof item.metadata === "object";
|
|
448
484
|
}
|
|
485
|
+
async function resolveScopePeer(input) {
|
|
486
|
+
const peers = input.peerFilter ?? [];
|
|
487
|
+
if (peers.length === 0) {
|
|
488
|
+
return { peers: [], warnings: [] };
|
|
489
|
+
}
|
|
490
|
+
if (typeof input.scopeCheck !== "function") {
|
|
491
|
+
return { peers, warnings: [] };
|
|
492
|
+
}
|
|
493
|
+
const retained = [];
|
|
494
|
+
const dropped = [];
|
|
495
|
+
for (const peer of peers) {
|
|
496
|
+
const ok = await input.scopeCheck({ userId: input.userId, peers: [peer] });
|
|
497
|
+
if (ok) {
|
|
498
|
+
retained.push(peer);
|
|
499
|
+
} else {
|
|
500
|
+
dropped.push(peer);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (dropped.length === 0) {
|
|
504
|
+
return { peers, warnings: [] };
|
|
505
|
+
}
|
|
506
|
+
if (retained.length === 0) {
|
|
507
|
+
return {
|
|
508
|
+
peers: [],
|
|
509
|
+
warnings: [
|
|
510
|
+
{
|
|
511
|
+
source: "memory",
|
|
512
|
+
code: "peer_filter_outside_user_scope",
|
|
513
|
+
message: `${dropped.map((peer) => `${peer.kind}:${peer.id}`).join(",")} ignored; falling back to userId scope`
|
|
514
|
+
}
|
|
515
|
+
]
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
peers: retained,
|
|
520
|
+
warnings: [
|
|
521
|
+
{
|
|
522
|
+
source: "memory",
|
|
523
|
+
code: "peer_filter_outside_user_scope",
|
|
524
|
+
message: `${dropped.map((peer) => `${peer.kind}:${peer.id}`).join(",")} ignored because they fall outside userId ${input.userId}`
|
|
525
|
+
}
|
|
526
|
+
]
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/search/gather-evidence.ts
|
|
531
|
+
var DEFAULT_TIERS = ["summary", "raw", "insight", "knowledge"];
|
|
532
|
+
var MAX_EVIDENCE_CHARS = 600;
|
|
533
|
+
function toSummaryResult(row) {
|
|
534
|
+
const start = typeof row.startTimestamp === "number" ? row.startTimestamp : void 0;
|
|
535
|
+
const end = typeof row.endTimestamp === "number" ? row.endTimestamp : void 0;
|
|
536
|
+
return {
|
|
537
|
+
type: "memory",
|
|
538
|
+
id: row.summaryId,
|
|
539
|
+
content: row.summaryText,
|
|
540
|
+
similarity: 1,
|
|
541
|
+
metadata: {
|
|
542
|
+
tier: "summary",
|
|
543
|
+
summaryTier: row.summaryTier,
|
|
544
|
+
keywords: row.keywords,
|
|
545
|
+
startTimestamp: start,
|
|
546
|
+
endTimestamp: end
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async function gatherSummaries(deps, input, limit, threshold, logger, peerPeers) {
|
|
551
|
+
if (typeof deps.searchSummaries !== "function") {
|
|
552
|
+
return { hits: [], warnings: [] };
|
|
553
|
+
}
|
|
554
|
+
const keywords = input.query.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length >= 2).slice(0, 16);
|
|
555
|
+
try {
|
|
556
|
+
const rows = await deps.searchSummaries({
|
|
557
|
+
userId: input.userId,
|
|
558
|
+
query: input.query,
|
|
559
|
+
keywords,
|
|
560
|
+
limit,
|
|
561
|
+
threshold,
|
|
562
|
+
authToken: input.authToken,
|
|
563
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
564
|
+
});
|
|
565
|
+
const hits = rows.map(toSummaryResult);
|
|
566
|
+
return { hits, warnings: [] };
|
|
567
|
+
} catch (error) {
|
|
568
|
+
logger.warn?.("[memory-store] gather-evidence: summary search failed:", error);
|
|
569
|
+
return {
|
|
570
|
+
hits: [],
|
|
571
|
+
warnings: [
|
|
572
|
+
{
|
|
573
|
+
source: "memory",
|
|
574
|
+
code: "reflect_summaries_failed",
|
|
575
|
+
message: error.message ?? "reflect_summaries_failed"
|
|
576
|
+
}
|
|
577
|
+
]
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function truncate(input, max = MAX_EVIDENCE_CHARS) {
|
|
582
|
+
if (input.length <= max) {
|
|
583
|
+
return input;
|
|
584
|
+
}
|
|
585
|
+
return `${input.slice(0, max - 1)}\u2026`;
|
|
586
|
+
}
|
|
587
|
+
function buildSynthesisPrompt(input) {
|
|
588
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
589
|
+
for (const item of input.evidence) {
|
|
590
|
+
const bucket = grouped.get(item.source) ?? [];
|
|
591
|
+
bucket.push(item);
|
|
592
|
+
grouped.set(item.source, bucket);
|
|
593
|
+
}
|
|
594
|
+
const sections = [];
|
|
595
|
+
for (const tier of DEFAULT_TIERS) {
|
|
596
|
+
const items = grouped.get(tier) ?? [];
|
|
597
|
+
if (items.length === 0) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const lines = items.map(
|
|
601
|
+
(item, index) => ` [${index + 1}] (${item.id}, score=${item.score.toFixed(4)}) ${item.snippet}`
|
|
602
|
+
);
|
|
603
|
+
sections.push(`## ${tier}
|
|
604
|
+
${lines.join("\n")}`);
|
|
605
|
+
}
|
|
606
|
+
const schemaDirective = input.responseSchema ? `
|
|
607
|
+
Respond with a JSON object that conforms to the following schema:
|
|
608
|
+
${JSON.stringify(input.responseSchema, null, 2)}
|
|
609
|
+
Wrap your JSON in a single fenced \`\`\`json code block; do not add any prose outside the code block.` : "\nRespond in plain prose. Be concise; cite evidence by its [n] number.";
|
|
610
|
+
if (sections.length === 0) {
|
|
611
|
+
return `You are a reflection assistant.
|
|
612
|
+
|
|
613
|
+
Question: ${input.query}
|
|
614
|
+
|
|
615
|
+
No evidence was found across any tier. State that explicitly.${schemaDirective}`;
|
|
616
|
+
}
|
|
617
|
+
return `You are a reflection assistant. Synthesise the user's question using only the evidence below. Do not invent facts.
|
|
618
|
+
|
|
619
|
+
Question: ${input.query}
|
|
620
|
+
|
|
621
|
+
Evidence (grouped by tier):
|
|
622
|
+
${sections.join("\n\n")}${schemaDirective}`;
|
|
623
|
+
}
|
|
624
|
+
function extractJsonPayload(text) {
|
|
625
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
626
|
+
if (fenced?.[1]) {
|
|
627
|
+
return fenced[1];
|
|
628
|
+
}
|
|
629
|
+
const start = text.indexOf("{");
|
|
630
|
+
if (start === -1) {
|
|
631
|
+
return void 0;
|
|
632
|
+
}
|
|
633
|
+
let depth = 0;
|
|
634
|
+
let inString = false;
|
|
635
|
+
let escaped = false;
|
|
636
|
+
for (let i = start; i < text.length; i += 1) {
|
|
637
|
+
const ch = text[i];
|
|
638
|
+
if (escaped) {
|
|
639
|
+
escaped = false;
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (inString) {
|
|
643
|
+
if (ch === "\\") {
|
|
644
|
+
escaped = true;
|
|
645
|
+
} else if (ch === '"') {
|
|
646
|
+
inString = false;
|
|
647
|
+
}
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (ch === '"') {
|
|
651
|
+
inString = true;
|
|
652
|
+
} else if (ch === "{") {
|
|
653
|
+
depth += 1;
|
|
654
|
+
} else if (ch === "}") {
|
|
655
|
+
depth -= 1;
|
|
656
|
+
if (depth === 0) {
|
|
657
|
+
return text.slice(start, i + 1);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return void 0;
|
|
662
|
+
}
|
|
663
|
+
function coerceAnswer(text, schema) {
|
|
664
|
+
if (!schema) {
|
|
665
|
+
return text;
|
|
666
|
+
}
|
|
667
|
+
const payload = extractJsonPayload(text);
|
|
668
|
+
if (!payload) {
|
|
669
|
+
return text;
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const parsed = JSON.parse(payload);
|
|
673
|
+
if (parsed && typeof parsed === "object") {
|
|
674
|
+
const answer = parsed.answer;
|
|
675
|
+
if (typeof answer === "string") {
|
|
676
|
+
return answer;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
return text;
|
|
682
|
+
}
|
|
683
|
+
async function synthesizeAnswer(input) {
|
|
684
|
+
const { query, hits, responseSchema, deps, logger } = input;
|
|
685
|
+
const evidence = hits.map((hit) => ({
|
|
686
|
+
id: hit.id,
|
|
687
|
+
source: mapHitToTier(hit),
|
|
688
|
+
snippet: truncate(hit.content),
|
|
689
|
+
score: hit.similarity
|
|
690
|
+
}));
|
|
691
|
+
const warnings = [];
|
|
692
|
+
const complete = deps.reasoning?.complete;
|
|
693
|
+
if (!complete) {
|
|
694
|
+
warnings.push({
|
|
695
|
+
source: "memory",
|
|
696
|
+
code: "reflect_llm_not_configured",
|
|
697
|
+
message: "No `reasoning.complete` callback is configured; search() returned evidence without an LLM synthesis."
|
|
698
|
+
});
|
|
699
|
+
return { answer: "", warnings };
|
|
700
|
+
}
|
|
701
|
+
const prompt = buildSynthesisPrompt({ query, evidence, responseSchema });
|
|
702
|
+
try {
|
|
703
|
+
const raw = await complete(prompt);
|
|
704
|
+
const answer = coerceAnswer(raw, responseSchema);
|
|
705
|
+
logger.log?.("[memory-store] search synthesis completed", {
|
|
706
|
+
evidenceCount: evidence.length,
|
|
707
|
+
answerChars: answer.length
|
|
708
|
+
});
|
|
709
|
+
return { answer, warnings };
|
|
710
|
+
} catch (error) {
|
|
711
|
+
logger.warn?.("[memory-store] search synthesis failed:", error);
|
|
712
|
+
warnings.push({
|
|
713
|
+
source: "memory",
|
|
714
|
+
code: "reflect_llm_failed",
|
|
715
|
+
message: error.message ?? "reflect_llm_failed"
|
|
716
|
+
});
|
|
717
|
+
return { answer: "", warnings };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function mapHitToTier(hit) {
|
|
721
|
+
const tierMarker = hit.metadata?.tier;
|
|
722
|
+
if (tierMarker === "summary") {
|
|
723
|
+
return "summary";
|
|
724
|
+
}
|
|
725
|
+
if (hit.type === "memory") {
|
|
726
|
+
return "raw";
|
|
727
|
+
}
|
|
728
|
+
if (hit.type === "insight") {
|
|
729
|
+
return "insight";
|
|
730
|
+
}
|
|
731
|
+
if (hit.type === "knowledge") {
|
|
732
|
+
return "knowledge";
|
|
733
|
+
}
|
|
734
|
+
return hit.type;
|
|
735
|
+
}
|
|
736
|
+
async function resolveSearchScopePeers(input) {
|
|
737
|
+
return resolveScopePeer({
|
|
738
|
+
userId: input.userId,
|
|
739
|
+
peerFilter: input.peerFilter,
|
|
740
|
+
scopeCheck: input.deps.peerScopeCheck
|
|
741
|
+
});
|
|
742
|
+
}
|
|
449
743
|
|
|
450
744
|
// src/search/unified-search.ts
|
|
451
745
|
function deriveLexicalKeywords(query) {
|
|
@@ -517,7 +811,7 @@ async function embedQueryVariant(embedQuery, input, query) {
|
|
|
517
811
|
authToken: input.authToken
|
|
518
812
|
});
|
|
519
813
|
}
|
|
520
|
-
async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger) {
|
|
814
|
+
async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit, threshold, logger, peerPeers = []) {
|
|
521
815
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds.map((botId) => ({ botId })) : [{}];
|
|
522
816
|
let semantic = [];
|
|
523
817
|
if (isRawMessageChromaEnabled()) {
|
|
@@ -547,6 +841,7 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
|
|
|
547
841
|
}
|
|
548
842
|
} else if (typeof deps.searchRawMessagesAnn === "function") {
|
|
549
843
|
const searchRawMessagesAnn = deps.searchRawMessagesAnn;
|
|
844
|
+
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
550
845
|
semantic = (await Promise.all(
|
|
551
846
|
filters.map(
|
|
552
847
|
(filter) => searchRawMessagesAnn({
|
|
@@ -554,7 +849,9 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
|
|
|
554
849
|
queryEmbedding,
|
|
555
850
|
limit,
|
|
556
851
|
threshold,
|
|
557
|
-
botId: "botId" in filter ? filter.botId : void 0
|
|
852
|
+
botId: "botId" in filter ? filter.botId : void 0,
|
|
853
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
854
|
+
...factTypes ? { factTypes } : {}
|
|
558
855
|
})
|
|
559
856
|
)
|
|
560
857
|
)).flat().filter(isRawMemorySemanticResult).map(toMemoryResult);
|
|
@@ -564,11 +861,14 @@ async function runSemanticSearchForEmbedding(deps, input, queryEmbedding, limit,
|
|
|
564
861
|
const { getRawMessageManager: getRawMessageManager2 } = await Promise.resolve().then(() => (init_raw_message_store(), raw_message_store_exports));
|
|
565
862
|
const manager2 = await getRawMessageManager2();
|
|
566
863
|
if (typeof manager2.searchMessagesSemantically === "function") {
|
|
864
|
+
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
567
865
|
const results = await manager2.searchMessagesSemantically({
|
|
568
866
|
userId: input.userId,
|
|
569
867
|
queryEmbedding,
|
|
570
868
|
limit,
|
|
571
|
-
threshold
|
|
869
|
+
threshold,
|
|
870
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
871
|
+
...factTypes ? { factTypes } : {}
|
|
572
872
|
});
|
|
573
873
|
semantic = results.map((r) => ({
|
|
574
874
|
type: "memory",
|
|
@@ -604,7 +904,7 @@ function toIterativeRecallCandidate(hit) {
|
|
|
604
904
|
metadata: hit.metadata
|
|
605
905
|
};
|
|
606
906
|
}
|
|
607
|
-
async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger) {
|
|
907
|
+
async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger, peerPeers = []) {
|
|
608
908
|
if (keywords.length === 0) {
|
|
609
909
|
return [];
|
|
610
910
|
}
|
|
@@ -612,13 +912,16 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger)
|
|
|
612
912
|
try {
|
|
613
913
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
614
914
|
const searchRawMessagesLexical = deps.searchRawMessagesLexical;
|
|
915
|
+
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
615
916
|
return (await Promise.all(
|
|
616
917
|
filters.map(
|
|
617
918
|
(botId) => searchRawMessagesLexical({
|
|
618
919
|
userId: input.userId,
|
|
619
920
|
keywords,
|
|
620
921
|
limit: Math.ceil(limit / filters.length),
|
|
621
|
-
botId
|
|
922
|
+
botId,
|
|
923
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
924
|
+
...factTypes ? { factTypes } : {}
|
|
622
925
|
})
|
|
623
926
|
)
|
|
624
927
|
)).flat().filter(isRawMemorySemanticResult).map((hit) => ({
|
|
@@ -632,13 +935,15 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger)
|
|
|
632
935
|
try {
|
|
633
936
|
const { lexicalSearchRawMessages: lexicalSearchRawMessages2 } = await Promise.resolve().then(() => (init_sqlite_raw_message_store(), sqlite_raw_message_store_exports));
|
|
634
937
|
const filters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
938
|
+
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
635
939
|
return (await Promise.all(
|
|
636
940
|
filters.map(
|
|
637
941
|
(botId) => lexicalSearchRawMessages2({
|
|
638
942
|
userId: input.userId,
|
|
639
943
|
keywords,
|
|
640
944
|
limit: Math.ceil(limit / filters.length),
|
|
641
|
-
botId
|
|
945
|
+
botId,
|
|
946
|
+
...factTypes ? { factTypes } : {}
|
|
642
947
|
})
|
|
643
948
|
)
|
|
644
949
|
)).flat().filter(Boolean).map(
|
|
@@ -651,6 +956,42 @@ async function runLexicalSearchForKeywords(deps, input, keywords, limit, logger)
|
|
|
651
956
|
}
|
|
652
957
|
return [];
|
|
653
958
|
}
|
|
959
|
+
function searchInputToUnified(input) {
|
|
960
|
+
return {
|
|
961
|
+
userId: input.userId,
|
|
962
|
+
query: input.query,
|
|
963
|
+
sources: input.sources ? [...input.sources] : void 0,
|
|
964
|
+
limit: input.limit,
|
|
965
|
+
threshold: input.threshold,
|
|
966
|
+
authToken: input.authToken,
|
|
967
|
+
includeArchivedInsights: input.includeArchivedInsights,
|
|
968
|
+
botIds: input.botIds,
|
|
969
|
+
documentIds: input.documentIds,
|
|
970
|
+
asOf: input.asOf,
|
|
971
|
+
dateFrom: input.dateFrom,
|
|
972
|
+
dateTo: input.dateTo,
|
|
973
|
+
mergeStrategy: input.mergeStrategy,
|
|
974
|
+
peerFilter: input.peerFilter,
|
|
975
|
+
reasoningStrategy: input.reasoningStrategy,
|
|
976
|
+
factTypes: input.factTypes
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function mapHitToEvidenceSource(hit) {
|
|
980
|
+
const tierMarker = hit.metadata?.tier;
|
|
981
|
+
if (tierMarker === "summary") {
|
|
982
|
+
return "summary";
|
|
983
|
+
}
|
|
984
|
+
if (hit.type === "memory") {
|
|
985
|
+
return "raw";
|
|
986
|
+
}
|
|
987
|
+
if (hit.type === "insight") {
|
|
988
|
+
return "insight";
|
|
989
|
+
}
|
|
990
|
+
if (hit.type === "knowledge") {
|
|
991
|
+
return "knowledge";
|
|
992
|
+
}
|
|
993
|
+
return hit.type;
|
|
994
|
+
}
|
|
654
995
|
function createUnifiedSearch(deps = {}) {
|
|
655
996
|
const logger = deps.logger ?? console;
|
|
656
997
|
function resolveReasoningStrategy(input) {
|
|
@@ -660,130 +1001,7 @@ function createUnifiedSearch(deps = {}) {
|
|
|
660
1001
|
}
|
|
661
1002
|
return normalizeUnifiedMemoryReasoningStrategy(deps.reasoning?.defaultStrategy, "none");
|
|
662
1003
|
}
|
|
663
|
-
async function
|
|
664
|
-
const query = input.query.trim();
|
|
665
|
-
const sources = normalizeUnifiedMemorySearchSources(input.sources);
|
|
666
|
-
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
667
|
-
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
668
|
-
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(input.mergeStrategy);
|
|
669
|
-
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
670
|
-
const warnings = [];
|
|
671
|
-
if (!query) {
|
|
672
|
-
return {
|
|
673
|
-
query,
|
|
674
|
-
sources,
|
|
675
|
-
results: [],
|
|
676
|
-
count: 0,
|
|
677
|
-
warnings
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
let memorySubs;
|
|
681
|
-
const reasoningInfo = { strategy: reasoningStrategy };
|
|
682
|
-
if (input.dateFrom || input.dateTo) {
|
|
683
|
-
reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
|
|
684
|
-
}
|
|
685
|
-
if (sources.includes("memory")) {
|
|
686
|
-
if (isRawMessageStorageAvailable()) {
|
|
687
|
-
try {
|
|
688
|
-
memorySubs = await runMemorySource(
|
|
689
|
-
input,
|
|
690
|
-
limit,
|
|
691
|
-
threshold,
|
|
692
|
-
warnings,
|
|
693
|
-
reasoningStrategy,
|
|
694
|
-
reasoningInfo
|
|
695
|
-
);
|
|
696
|
-
} catch (error) {
|
|
697
|
-
logger.warn?.("[memory-store] memory source failed:", error);
|
|
698
|
-
warnings.push({
|
|
699
|
-
source: "memory",
|
|
700
|
-
code: "memory_search_failed",
|
|
701
|
-
message: `Memory search failed: ${error.message ?? "Unknown error"}. Using keyword search fallback.`
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
} else {
|
|
705
|
-
warnings.push({
|
|
706
|
-
source: "memory",
|
|
707
|
-
code: "raw_message_storage_unavailable",
|
|
708
|
-
message: "Raw memory storage is not available in this environment."
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
const insightHits = [];
|
|
713
|
-
if (sources.includes("insights") && typeof deps.searchInsights === "function") {
|
|
714
|
-
try {
|
|
715
|
-
const hits = await deps.searchInsights({
|
|
716
|
-
userId: input.userId,
|
|
717
|
-
query: input.query,
|
|
718
|
-
limit,
|
|
719
|
-
threshold,
|
|
720
|
-
botIds: input.botIds,
|
|
721
|
-
includeArchived: input.includeArchivedInsights,
|
|
722
|
-
authToken: input.authToken
|
|
723
|
-
});
|
|
724
|
-
for (const hit of hits) {
|
|
725
|
-
insightHits.push({
|
|
726
|
-
type: "insight",
|
|
727
|
-
id: hit.id,
|
|
728
|
-
content: hit.content,
|
|
729
|
-
similarity: hit.similarity,
|
|
730
|
-
metadata: hit.metadata
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
} catch (error) {
|
|
734
|
-
logger.warn?.("[memory-store] insights source failed:", error);
|
|
735
|
-
warnings.push({
|
|
736
|
-
source: "insights",
|
|
737
|
-
code: "insights_search_failed",
|
|
738
|
-
message: error.message ?? "insights_search_failed"
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
} else if (sources.includes("insights")) ;
|
|
742
|
-
const knowledgeHits = [];
|
|
743
|
-
if (sources.includes("knowledge") && typeof deps.searchKnowledge === "function") {
|
|
744
|
-
try {
|
|
745
|
-
const hits = await deps.searchKnowledge({
|
|
746
|
-
userId: input.userId,
|
|
747
|
-
query: input.query,
|
|
748
|
-
options: {
|
|
749
|
-
limit,
|
|
750
|
-
threshold,
|
|
751
|
-
documentIds: input.documentIds
|
|
752
|
-
},
|
|
753
|
-
authToken: input.authToken
|
|
754
|
-
});
|
|
755
|
-
for (const hit of hits) {
|
|
756
|
-
knowledgeHits.push(toKnowledgeResult(hit));
|
|
757
|
-
}
|
|
758
|
-
} catch (error) {
|
|
759
|
-
logger.warn?.("[memory-store] knowledge source failed:", error);
|
|
760
|
-
warnings.push({
|
|
761
|
-
source: "knowledge",
|
|
762
|
-
code: "knowledge_search_failed",
|
|
763
|
-
message: error.message ?? "knowledge_search_failed"
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
} else if (sources.includes("knowledge")) ;
|
|
767
|
-
const merged = mergeAcrossSources({
|
|
768
|
-
memorySubs,
|
|
769
|
-
insightHits,
|
|
770
|
-
knowledgeHits,
|
|
771
|
-
limit,
|
|
772
|
-
strategy: mergeStrategy
|
|
773
|
-
});
|
|
774
|
-
const output = {
|
|
775
|
-
query: input.query,
|
|
776
|
-
sources,
|
|
777
|
-
results: merged,
|
|
778
|
-
count: merged.length,
|
|
779
|
-
warnings
|
|
780
|
-
};
|
|
781
|
-
if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
|
|
782
|
-
output.reasoning = reasoningInfo;
|
|
783
|
-
}
|
|
784
|
-
return output;
|
|
785
|
-
}
|
|
786
|
-
async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo) {
|
|
1004
|
+
async function runMemorySource(input, limit, threshold, warnings, reasoningStrategy = "none", reasoningInfo, peerPeers = []) {
|
|
787
1005
|
if (reasoningStrategy === "iterative" && !deps.reasoning?.iterativePlanner) {
|
|
788
1006
|
warnings.push({
|
|
789
1007
|
source: "memory",
|
|
@@ -809,7 +1027,14 @@ function createUnifiedSearch(deps = {}) {
|
|
|
809
1027
|
executor: {
|
|
810
1028
|
search: async (request) => {
|
|
811
1029
|
const keywords2 = request.keywords.length > 0 ? request.keywords : deriveLexicalKeywords(input.query);
|
|
812
|
-
const lexicalHits = await runLexicalSearchForKeywords(
|
|
1030
|
+
const lexicalHits = await runLexicalSearchForKeywords(
|
|
1031
|
+
deps,
|
|
1032
|
+
input,
|
|
1033
|
+
keywords2,
|
|
1034
|
+
limit,
|
|
1035
|
+
logger,
|
|
1036
|
+
peerPeers
|
|
1037
|
+
);
|
|
813
1038
|
let semanticHits = [];
|
|
814
1039
|
if (typeof deps.embedQuery === "function") {
|
|
815
1040
|
try {
|
|
@@ -821,7 +1046,8 @@ function createUnifiedSearch(deps = {}) {
|
|
|
821
1046
|
queryEmbedding,
|
|
822
1047
|
limit,
|
|
823
1048
|
threshold,
|
|
824
|
-
logger
|
|
1049
|
+
logger,
|
|
1050
|
+
peerPeers
|
|
825
1051
|
);
|
|
826
1052
|
} catch (error) {
|
|
827
1053
|
logger.warn?.(
|
|
@@ -866,7 +1092,7 @@ function createUnifiedSearch(deps = {}) {
|
|
|
866
1092
|
});
|
|
867
1093
|
const keywords2 = deriveLexicalKeywords(input.query);
|
|
868
1094
|
const lexical2 = filterByDateRange(
|
|
869
|
-
await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger),
|
|
1095
|
+
await runLexicalSearchForKeywords(deps, input, keywords2, limit, logger, peerPeers),
|
|
870
1096
|
input.dateFrom,
|
|
871
1097
|
input.dateTo
|
|
872
1098
|
);
|
|
@@ -892,7 +1118,7 @@ function createUnifiedSearch(deps = {}) {
|
|
|
892
1118
|
);
|
|
893
1119
|
const lists = await Promise.all(
|
|
894
1120
|
embeddings.map(
|
|
895
|
-
(embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger)
|
|
1121
|
+
(embedding) => runSemanticSearchForEmbedding(deps, input, embedding, limit, threshold, logger, peerPeers)
|
|
896
1122
|
)
|
|
897
1123
|
);
|
|
898
1124
|
semantic = mergeByMaxScore(lists);
|
|
@@ -908,7 +1134,15 @@ function createUnifiedSearch(deps = {}) {
|
|
|
908
1134
|
}
|
|
909
1135
|
if (semantic.length === 0) {
|
|
910
1136
|
const queryEmbedding = await embedQueryVariant(embedQuery, input, input.query);
|
|
911
|
-
semantic = await runSemanticSearchForEmbedding(
|
|
1137
|
+
semantic = await runSemanticSearchForEmbedding(
|
|
1138
|
+
deps,
|
|
1139
|
+
input,
|
|
1140
|
+
queryEmbedding,
|
|
1141
|
+
limit,
|
|
1142
|
+
threshold,
|
|
1143
|
+
logger,
|
|
1144
|
+
peerPeers
|
|
1145
|
+
);
|
|
912
1146
|
}
|
|
913
1147
|
let lexical = [];
|
|
914
1148
|
const keywords = deriveLexicalKeywords(input.query);
|
|
@@ -917,13 +1151,16 @@ function createUnifiedSearch(deps = {}) {
|
|
|
917
1151
|
try {
|
|
918
1152
|
const lexFilters = input.botIds && input.botIds.length > 0 ? input.botIds : [void 0];
|
|
919
1153
|
const searchRawMessagesLexical = deps.searchRawMessagesLexical;
|
|
1154
|
+
const factTypes = input.factTypes?.length ? input.factTypes : void 0;
|
|
920
1155
|
lexical = (await Promise.all(
|
|
921
1156
|
lexFilters.map(
|
|
922
1157
|
(botId) => searchRawMessagesLexical({
|
|
923
1158
|
userId: input.userId,
|
|
924
1159
|
keywords,
|
|
925
1160
|
limit,
|
|
926
|
-
botId
|
|
1161
|
+
botId,
|
|
1162
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {},
|
|
1163
|
+
...factTypes ? { factTypes } : {}
|
|
927
1164
|
})
|
|
928
1165
|
)
|
|
929
1166
|
)).flat().filter(isRawMemorySemanticResult).map((hit) => ({
|
|
@@ -951,35 +1188,287 @@ function createUnifiedSearch(deps = {}) {
|
|
|
951
1188
|
lexical: filterByDateRange(lexical, input.dateFrom, input.dateTo)
|
|
952
1189
|
};
|
|
953
1190
|
}
|
|
954
|
-
async function
|
|
1191
|
+
async function searchUnifiedMemory(input) {
|
|
1192
|
+
const query = input.query.trim();
|
|
1193
|
+
const sources = normalizeUnifiedMemorySearchSources(input.sources);
|
|
955
1194
|
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
956
1195
|
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1196
|
+
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1197
|
+
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1198
|
+
);
|
|
957
1199
|
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1200
|
+
const warnings = [];
|
|
1201
|
+
const peerScope = input.peerFilter && input.peerFilter.length > 0 ? await resolveScopePeer({
|
|
1202
|
+
userId: input.userId,
|
|
1203
|
+
peerFilter: input.peerFilter,
|
|
1204
|
+
scopeCheck: deps.peerScopeCheck
|
|
1205
|
+
}) : { peers: [], warnings: [] };
|
|
1206
|
+
warnings.push(...peerScope.warnings);
|
|
1207
|
+
const peerPeers = peerScope.peers;
|
|
1208
|
+
if (!query) {
|
|
1209
|
+
return {
|
|
1210
|
+
query,
|
|
1211
|
+
sources,
|
|
1212
|
+
results: [],
|
|
1213
|
+
count: 0,
|
|
1214
|
+
warnings
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
let memorySubs;
|
|
1218
|
+
const reasoningInfo = { strategy: reasoningStrategy };
|
|
1219
|
+
if (input.dateFrom || input.dateTo) {
|
|
1220
|
+
reasoningInfo.dateRange = { from: input.dateFrom, to: input.dateTo };
|
|
1221
|
+
}
|
|
1222
|
+
if (sources.includes("memory")) {
|
|
1223
|
+
const hasRawProviders = typeof deps.searchRawMessagesAnn === "function" || typeof deps.searchRawMessagesLexical === "function";
|
|
1224
|
+
if (isRawMessageStorageAvailable() || hasRawProviders) {
|
|
1225
|
+
try {
|
|
1226
|
+
memorySubs = await runMemorySource(
|
|
1227
|
+
input,
|
|
1228
|
+
limit,
|
|
1229
|
+
threshold,
|
|
1230
|
+
warnings,
|
|
1231
|
+
reasoningStrategy,
|
|
1232
|
+
reasoningInfo,
|
|
1233
|
+
peerPeers
|
|
1234
|
+
);
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
logger.warn?.("[memory-store] memory source failed:", error);
|
|
1237
|
+
warnings.push({
|
|
1238
|
+
source: "memory",
|
|
1239
|
+
code: "memory_search_failed",
|
|
1240
|
+
message: `Memory search failed: ${error.message ?? "Unknown error"}. Using keyword search fallback.`
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
} else {
|
|
1244
|
+
warnings.push({
|
|
1245
|
+
source: "memory",
|
|
1246
|
+
code: "raw_message_storage_unavailable",
|
|
1247
|
+
message: "Raw memory storage is not available in this environment."
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const insightHits = [];
|
|
1252
|
+
if (sources.includes("insights") && typeof deps.searchInsights === "function") {
|
|
1253
|
+
try {
|
|
1254
|
+
const hits = await deps.searchInsights({
|
|
1255
|
+
userId: input.userId,
|
|
1256
|
+
query: input.query,
|
|
1257
|
+
limit,
|
|
1258
|
+
threshold,
|
|
1259
|
+
botIds: input.botIds,
|
|
1260
|
+
includeArchived: input.includeArchivedInsights,
|
|
1261
|
+
authToken: input.authToken,
|
|
1262
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1263
|
+
});
|
|
1264
|
+
for (const hit of hits) {
|
|
1265
|
+
insightHits.push({
|
|
1266
|
+
type: "insight",
|
|
1267
|
+
id: hit.id,
|
|
1268
|
+
content: hit.content,
|
|
1269
|
+
similarity: hit.similarity,
|
|
1270
|
+
metadata: hit.metadata
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
logger.warn?.("[memory-store] insights source failed:", error);
|
|
1275
|
+
warnings.push({
|
|
1276
|
+
source: "insights",
|
|
1277
|
+
code: "insights_search_failed",
|
|
1278
|
+
message: error.message ?? "insights_search_failed"
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
} else if (sources.includes("insights")) ;
|
|
1282
|
+
const knowledgeHits = [];
|
|
1283
|
+
if (sources.includes("knowledge") && typeof deps.searchKnowledge === "function") {
|
|
1284
|
+
try {
|
|
1285
|
+
const hits = await deps.searchKnowledge({
|
|
1286
|
+
userId: input.userId,
|
|
1287
|
+
query: input.query,
|
|
1288
|
+
options: {
|
|
1289
|
+
limit,
|
|
1290
|
+
threshold,
|
|
1291
|
+
documentIds: input.documentIds
|
|
1292
|
+
},
|
|
1293
|
+
authToken: input.authToken,
|
|
1294
|
+
...peerPeers.length > 0 ? { peers: peerPeers } : {}
|
|
1295
|
+
});
|
|
1296
|
+
for (const hit of hits) {
|
|
1297
|
+
knowledgeHits.push(toKnowledgeResult(hit));
|
|
1298
|
+
}
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
logger.warn?.("[memory-store] knowledge source failed:", error);
|
|
1301
|
+
warnings.push({
|
|
1302
|
+
source: "knowledge",
|
|
1303
|
+
code: "knowledge_search_failed",
|
|
1304
|
+
message: error.message ?? "knowledge_search_failed"
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
} else if (sources.includes("knowledge")) ;
|
|
1308
|
+
const merged = mergeAcrossSources({
|
|
1309
|
+
memorySubs,
|
|
1310
|
+
insightHits,
|
|
1311
|
+
knowledgeHits,
|
|
1312
|
+
limit,
|
|
1313
|
+
strategy: mergeStrategy
|
|
1314
|
+
});
|
|
1315
|
+
const ranked = await applyReranker(deps.reranker, input.query, merged);
|
|
1316
|
+
const output = {
|
|
1317
|
+
query: input.query,
|
|
1318
|
+
sources,
|
|
1319
|
+
results: ranked,
|
|
1320
|
+
count: ranked.length,
|
|
1321
|
+
warnings
|
|
1322
|
+
};
|
|
1323
|
+
if (reasoningInfo && (reasoningInfo.strategy !== "none" || reasoningInfo.dateRange !== void 0)) {
|
|
1324
|
+
output.reasoning = reasoningInfo;
|
|
1325
|
+
}
|
|
1326
|
+
return output;
|
|
1327
|
+
}
|
|
1328
|
+
async function searchRawMemorySemantically(input) {
|
|
1329
|
+
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
1330
|
+
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1331
|
+
const reasoningStrategy = resolveReasoningStrategy(input);
|
|
1332
|
+
const mergeStrategy = normalizeUnifiedMemoryMergeStrategy(
|
|
1333
|
+
input.mergeStrategy ?? deps.reasoning?.defaultMergeStrategy ?? "rrf"
|
|
1334
|
+
);
|
|
1335
|
+
const sub = await runMemorySource(input, limit, threshold, [], reasoningStrategy);
|
|
1336
|
+
return mergeAcrossSources({
|
|
1337
|
+
memorySubs: sub,
|
|
1338
|
+
insightHits: [],
|
|
1339
|
+
knowledgeHits: [],
|
|
1340
|
+
limit,
|
|
1341
|
+
strategy: mergeStrategy
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
async function search(input) {
|
|
1345
|
+
const query = input.query.trim();
|
|
1346
|
+
const wantsSynthesis = typeof input.synthesize === "boolean" ? input.synthesize : typeof input.synthesize === "object" && input.synthesize !== null;
|
|
1347
|
+
const synthesisSchema = typeof input.synthesize === "object" && input.synthesize !== null ? input.synthesize.responseSchema ?? input.responseSchema : input.responseSchema;
|
|
1348
|
+
const peerScope = await resolveSearchScopePeers({
|
|
1349
|
+
deps,
|
|
1350
|
+
userId: input.userId,
|
|
1351
|
+
peerFilter: input.peerFilter
|
|
1352
|
+
});
|
|
1353
|
+
if (!query) {
|
|
1354
|
+
const empty = {
|
|
1355
|
+
query,
|
|
1356
|
+
sources: input.sources ? [...input.sources] : [],
|
|
1357
|
+
results: [],
|
|
1358
|
+
evidence: [],
|
|
1359
|
+
count: 0,
|
|
1360
|
+
warnings: peerScope.warnings,
|
|
1361
|
+
answer: ""
|
|
1362
|
+
};
|
|
1363
|
+
return empty;
|
|
1364
|
+
}
|
|
1365
|
+
const tiers = input.tiers ? [...input.tiers] : wantsSynthesis ? ["summary", "raw", "insight", "knowledge"] : ["raw", "insight", "knowledge"];
|
|
1366
|
+
const wantsSummaryTier = tiers.includes("summary");
|
|
1367
|
+
const wantsRawTier = tiers.includes("raw");
|
|
1368
|
+
const wantsInsightTier = tiers.includes("insight");
|
|
1369
|
+
const wantsKnowledgeTier = tiers.includes("knowledge");
|
|
1370
|
+
const warnings = [...peerScope.warnings];
|
|
1371
|
+
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
1372
|
+
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1373
|
+
let unifiedHits = [];
|
|
1374
|
+
let sources = input.sources ? [...input.sources] : [];
|
|
1375
|
+
let unifiedReasoning;
|
|
1376
|
+
if (wantsRawTier || wantsInsightTier || wantsKnowledgeTier) {
|
|
1377
|
+
const inferredSources = sources.length > 0 ? sources : [
|
|
1378
|
+
...wantsRawTier ? ["memory"] : [],
|
|
1379
|
+
...wantsInsightTier ? ["insights"] : [],
|
|
1380
|
+
...wantsKnowledgeTier ? ["knowledge"] : []
|
|
1381
|
+
];
|
|
1382
|
+
const unified = await searchUnifiedMemory({
|
|
1383
|
+
...searchInputToUnified(input),
|
|
1384
|
+
sources: inferredSources.length > 0 ? inferredSources : void 0
|
|
1385
|
+
});
|
|
1386
|
+
unifiedHits = unified.results;
|
|
1387
|
+
sources = unified.sources;
|
|
1388
|
+
unifiedReasoning = unified.reasoning;
|
|
1389
|
+
warnings.push(...unified.warnings);
|
|
1390
|
+
}
|
|
1391
|
+
let summaryHits = [];
|
|
1392
|
+
if (wantsSummaryTier && typeof deps.searchSummaries === "function") {
|
|
1393
|
+
const summaryBucket = await gatherSummaries(
|
|
1394
|
+
deps,
|
|
1395
|
+
{ userId: input.userId, query: input.query, authToken: input.authToken },
|
|
1396
|
+
limit,
|
|
1397
|
+
threshold,
|
|
1398
|
+
logger,
|
|
1399
|
+
peerScope.peers
|
|
1400
|
+
);
|
|
1401
|
+
warnings.push(...summaryBucket.warnings);
|
|
1402
|
+
summaryHits = summaryBucket.hits;
|
|
1403
|
+
}
|
|
1404
|
+
const tierLists = [];
|
|
1405
|
+
if (unifiedHits.length > 0) {
|
|
1406
|
+
tierLists.push({ name: "memory", hits: unifiedHits });
|
|
1407
|
+
}
|
|
1408
|
+
if (summaryHits.length > 0) {
|
|
1409
|
+
tierLists.push({ name: "summary", hits: summaryHits });
|
|
1410
|
+
}
|
|
1411
|
+
let hits;
|
|
1412
|
+
if (tierLists.length === 0) {
|
|
1413
|
+
hits = [];
|
|
1414
|
+
} else if (tierLists.length === 1) {
|
|
1415
|
+
hits = tierLists[0].hits.slice(0, limit);
|
|
1416
|
+
} else {
|
|
1417
|
+
hits = mergeUnifiedMemorySearchResultsRrf(tierLists, limit);
|
|
1418
|
+
}
|
|
1419
|
+
const evidence = hits.map((hit) => ({
|
|
1420
|
+
id: hit.id,
|
|
1421
|
+
source: mapHitToEvidenceSource(hit),
|
|
1422
|
+
snippet: hit.content,
|
|
1423
|
+
score: hit.similarity,
|
|
1424
|
+
timestamp: getCandidateTimestamp(hit.metadata)
|
|
1425
|
+
}));
|
|
1426
|
+
const base = {
|
|
1427
|
+
query,
|
|
1428
|
+
sources,
|
|
1429
|
+
results: hits,
|
|
1430
|
+
evidence,
|
|
1431
|
+
count: hits.length,
|
|
1432
|
+
warnings
|
|
1433
|
+
};
|
|
1434
|
+
if (unifiedReasoning) {
|
|
1435
|
+
base.reasoning = unifiedReasoning;
|
|
1436
|
+
}
|
|
1437
|
+
if (!wantsSynthesis) {
|
|
1438
|
+
return base;
|
|
1439
|
+
}
|
|
1440
|
+
const { answer, warnings: synthWarnings } = await synthesizeAnswer({
|
|
1441
|
+
query,
|
|
1442
|
+
hits,
|
|
1443
|
+
responseSchema: synthesisSchema,
|
|
1444
|
+
deps,
|
|
1445
|
+
logger
|
|
1446
|
+
});
|
|
1447
|
+
base.answer = answer;
|
|
1448
|
+
base.warnings = [...base.warnings, ...synthWarnings];
|
|
1449
|
+
return base;
|
|
1450
|
+
}
|
|
1451
|
+
return {
|
|
1452
|
+
search,
|
|
1453
|
+
searchUnifiedMemory,
|
|
1454
|
+
searchRawMemorySemantically,
|
|
1455
|
+
consolidate: (input) => applyReflectedPlan(deps, {}, input)
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
function mergeAcrossSources(input) {
|
|
1459
|
+
const memorySubs = input.memorySubs ?? { semantic: [], lexical: [] };
|
|
1460
|
+
const all = [
|
|
1461
|
+
...memorySubs.semantic,
|
|
1462
|
+
...memorySubs.lexical,
|
|
1463
|
+
...input.insightHits,
|
|
1464
|
+
...input.knowledgeHits
|
|
1465
|
+
];
|
|
1466
|
+
if (input.strategy !== "rrf") {
|
|
1467
|
+
return mergeUnifiedMemorySearchResults(all, input.limit);
|
|
1468
|
+
}
|
|
1469
|
+
const lists = [];
|
|
1470
|
+
if (memorySubs.semantic.length > 0) {
|
|
1471
|
+
lists.push({ name: "memory-semantic", hits: memorySubs.semantic });
|
|
983
1472
|
}
|
|
984
1473
|
if (memorySubs.lexical.length > 0) {
|
|
985
1474
|
lists.push({ name: "memory-bm25", hits: memorySubs.lexical });
|
|
@@ -996,9 +1485,283 @@ function mergeAcrossSources(input) {
|
|
|
996
1485
|
return mergeUnifiedMemorySearchResultsRrf(lists, input.limit);
|
|
997
1486
|
}
|
|
998
1487
|
|
|
1488
|
+
// src/search/apply-reflect.ts
|
|
1489
|
+
var DEFAULT_TIERS2 = ["summary", "raw", "insight", "knowledge"];
|
|
1490
|
+
function reflectTierToEvidenceTier(source) {
|
|
1491
|
+
switch (source) {
|
|
1492
|
+
case "raw":
|
|
1493
|
+
return "short";
|
|
1494
|
+
case "summary":
|
|
1495
|
+
return "long";
|
|
1496
|
+
default:
|
|
1497
|
+
return "mid";
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function toMemoryEvidenceRecord(input, fallbackUserId) {
|
|
1501
|
+
const tier = reflectTierToEvidenceTier(input.source);
|
|
1502
|
+
const timestamp = input.timestamp ?? Date.now();
|
|
1503
|
+
return {
|
|
1504
|
+
id: input.id,
|
|
1505
|
+
userId: fallbackUserId,
|
|
1506
|
+
timestamp,
|
|
1507
|
+
tier,
|
|
1508
|
+
text: input.snippet,
|
|
1509
|
+
metadata: {
|
|
1510
|
+
source: input.source,
|
|
1511
|
+
score: input.score,
|
|
1512
|
+
...input.timestamp !== void 0 ? { originalTimestamp: input.timestamp } : {}
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
async function applyReflectedPlan(deps, store, input, _logger) {
|
|
1517
|
+
const query = input.query.trim();
|
|
1518
|
+
const warnings = [];
|
|
1519
|
+
const limit = clampUnifiedMemorySearchLimit(input.limit);
|
|
1520
|
+
const threshold = clampUnifiedMemorySearchThreshold(input.threshold);
|
|
1521
|
+
const tiers = input.tiers && input.tiers.length > 0 ? input.tiers : DEFAULT_TIERS2;
|
|
1522
|
+
const searchInput = {
|
|
1523
|
+
userId: input.userId,
|
|
1524
|
+
query,
|
|
1525
|
+
botIds: input.botIds,
|
|
1526
|
+
dateFrom: input.dateFrom,
|
|
1527
|
+
dateTo: input.dateTo,
|
|
1528
|
+
tiers,
|
|
1529
|
+
limit,
|
|
1530
|
+
threshold,
|
|
1531
|
+
authToken: input.authToken,
|
|
1532
|
+
peerFilter: input.peerFilter,
|
|
1533
|
+
synthesize: true
|
|
1534
|
+
};
|
|
1535
|
+
const search = createUnifiedSearch(deps);
|
|
1536
|
+
const result = await search.search(searchInput);
|
|
1537
|
+
warnings.push(...result.warnings);
|
|
1538
|
+
const evidence = result.evidence.map((item) => ({
|
|
1539
|
+
id: item.id,
|
|
1540
|
+
source: item.source ?? "raw",
|
|
1541
|
+
snippet: item.snippet,
|
|
1542
|
+
score: item.score,
|
|
1543
|
+
timestamp: item.timestamp
|
|
1544
|
+
}));
|
|
1545
|
+
const records = evidence.map((item) => toMemoryEvidenceRecord(item, input.userId));
|
|
1546
|
+
const applyInput = {
|
|
1547
|
+
userId: input.userId,
|
|
1548
|
+
ownerScope: input.ownerScope,
|
|
1549
|
+
records,
|
|
1550
|
+
plan: input.plan,
|
|
1551
|
+
dryRun: input.dryRun,
|
|
1552
|
+
expectedVersion: input.expectedVersion,
|
|
1553
|
+
getClusterKey: input.getClusterKey ?? defaultClusterKey,
|
|
1554
|
+
llmReview: deps.reasoning?.complete ? {
|
|
1555
|
+
complete: deps.reasoning.complete,
|
|
1556
|
+
maxTokens: input.llmPlanReview?.maxTokens
|
|
1557
|
+
} : void 0,
|
|
1558
|
+
graphStore: input.graphStore ?? store.graphStore,
|
|
1559
|
+
storage: input.storage ?? store.storage
|
|
1560
|
+
};
|
|
1561
|
+
const applyOutput = await applyReflectedConsolidationPlan(applyInput);
|
|
1562
|
+
for (const warning of applyOutput.warnings) {
|
|
1563
|
+
warnings.push({
|
|
1564
|
+
source: "memory",
|
|
1565
|
+
code: warning.code,
|
|
1566
|
+
message: warning.message
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
const summaryAnswer = makePlanSummary(applyOutput.plan);
|
|
1570
|
+
return {
|
|
1571
|
+
answer: result.answer ? `${result.answer}
|
|
1572
|
+
|
|
1573
|
+
Plan summary:
|
|
1574
|
+
${summaryAnswer}` : summaryAnswer,
|
|
1575
|
+
plan: applyOutput.plan,
|
|
1576
|
+
persistenceResult: applyOutput.persistenceResult,
|
|
1577
|
+
deprecationCounts: applyOutput.deprecationCounts,
|
|
1578
|
+
warnings,
|
|
1579
|
+
applied: applyOutput.applied,
|
|
1580
|
+
evidence
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
function defaultClusterKey(record) {
|
|
1584
|
+
return record.text ?? record.id;
|
|
1585
|
+
}
|
|
1586
|
+
function makePlanSummary(plan) {
|
|
1587
|
+
const lines = [];
|
|
1588
|
+
const counts = {
|
|
1589
|
+
preserve: plan.actions.preserve.length,
|
|
1590
|
+
observe: plan.actions.observe.length,
|
|
1591
|
+
decay: plan.actions.decay.length,
|
|
1592
|
+
deprecate: plan.actions.deprecate.length
|
|
1593
|
+
};
|
|
1594
|
+
lines.push(
|
|
1595
|
+
`- preserve=${counts.preserve} observe=${counts.observe} decay=${counts.decay} deprecate=${counts.deprecate}`
|
|
1596
|
+
);
|
|
1597
|
+
for (const entry of plan.actions.deprecate.slice(0, 5)) {
|
|
1598
|
+
lines.push(
|
|
1599
|
+
` deprecate ${entry.recordIds.length} record(s) from cluster ${entry.clusterKey} -> ${entry.supersededBySummaryId ?? entry.winningClusterKey}`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
return lines.join("\n");
|
|
1603
|
+
}
|
|
1604
|
+
var DEFAULT_MAX_FACTS = 1e3;
|
|
1605
|
+
var LOW_CONFIDENCE_THRESHOLD = 0.1;
|
|
1606
|
+
function toHRR(data) {
|
|
1607
|
+
const arr = data instanceof Float32Array ? data : new Float32Array(data);
|
|
1608
|
+
return { dim: arr.length, data: arr };
|
|
1609
|
+
}
|
|
1610
|
+
function generateFactId() {
|
|
1611
|
+
const stamp = Date.now().toString(36);
|
|
1612
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
1613
|
+
return `vsa-${stamp}-${rand}`;
|
|
1614
|
+
}
|
|
1615
|
+
function createVsaRecall(storage) {
|
|
1616
|
+
return {
|
|
1617
|
+
async storeFact(input) {
|
|
1618
|
+
const roleVector = vsaNormalizeVector(input.roleVector, "roleVector");
|
|
1619
|
+
const fillerVector = vsaNormalizeVector(input.fillerVector, "fillerVector");
|
|
1620
|
+
vsaAssertSameDim(roleVector, fillerVector, "storeFact");
|
|
1621
|
+
const dim = input.dim ?? roleVector.length;
|
|
1622
|
+
if (roleVector.length !== dim) {
|
|
1623
|
+
throw new Error(`storeFact: roleVector.length (${roleVector.length}) must equal dim (${dim})`);
|
|
1624
|
+
}
|
|
1625
|
+
const now = Date.now();
|
|
1626
|
+
const fact = {
|
|
1627
|
+
factId: input.factId ?? generateFactId(),
|
|
1628
|
+
userId: input.userId,
|
|
1629
|
+
roleLabel: input.roleLabel,
|
|
1630
|
+
fillerLabel: input.fillerLabel,
|
|
1631
|
+
roleVector,
|
|
1632
|
+
fillerVector,
|
|
1633
|
+
dim,
|
|
1634
|
+
scopeTag: input.scopeTag ?? "default",
|
|
1635
|
+
botId: input.botId,
|
|
1636
|
+
createdAt: now
|
|
1637
|
+
};
|
|
1638
|
+
await storage.storeFact(fact);
|
|
1639
|
+
return { factId: fact.factId, createdAt: now };
|
|
1640
|
+
},
|
|
1641
|
+
async recall(input) {
|
|
1642
|
+
const roleVector = vsaNormalizeVector(input.roleVector, "roleVector");
|
|
1643
|
+
const scopeTag = input.scopeTag ?? "default";
|
|
1644
|
+
const maxFacts = input.maxFacts ?? DEFAULT_MAX_FACTS;
|
|
1645
|
+
if (input.vocabulary.length === 0) {
|
|
1646
|
+
throw new Error("vsaRecall: vocabulary must contain at least one entry");
|
|
1647
|
+
}
|
|
1648
|
+
const facts = await storage.queryFacts({
|
|
1649
|
+
userId: input.userId,
|
|
1650
|
+
scopeTag,
|
|
1651
|
+
botId: input.botId,
|
|
1652
|
+
includeDeprecated: false,
|
|
1653
|
+
limit: maxFacts
|
|
1654
|
+
});
|
|
1655
|
+
const warnings = [];
|
|
1656
|
+
if (facts.length === 0) {
|
|
1657
|
+
warnings.push({
|
|
1658
|
+
code: "vsa_no_facts",
|
|
1659
|
+
message: `No VSA facts for userId=${input.userId} scopeTag=${scopeTag}`
|
|
1660
|
+
});
|
|
1661
|
+
return {
|
|
1662
|
+
fillerLabel: "",
|
|
1663
|
+
score: 0,
|
|
1664
|
+
allScores: [],
|
|
1665
|
+
factCount: 0,
|
|
1666
|
+
warnings
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
const refDim = facts[0].dim;
|
|
1670
|
+
const dimMatchedFacts = facts.filter((fact) => {
|
|
1671
|
+
if (fact.dim === refDim) return true;
|
|
1672
|
+
warnings.push({
|
|
1673
|
+
code: "vsa_dim_mismatch",
|
|
1674
|
+
message: `Fact ${fact.factId} has dim=${fact.dim}, expected ${refDim}; skipping`
|
|
1675
|
+
});
|
|
1676
|
+
return false;
|
|
1677
|
+
});
|
|
1678
|
+
if (roleVector.length !== refDim) {
|
|
1679
|
+
throw new Error(
|
|
1680
|
+
`vsaRecall: roleVector dim (${roleVector.length}) must match stored fact dim (${refDim})`
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
if (dimMatchedFacts.length === 0) {
|
|
1684
|
+
warnings.push({
|
|
1685
|
+
code: "vsa_no_facts",
|
|
1686
|
+
message: `All stored facts for userId=${input.userId} scopeTag=${scopeTag} had a non-matching dim`
|
|
1687
|
+
});
|
|
1688
|
+
return {
|
|
1689
|
+
fillerLabel: "",
|
|
1690
|
+
score: 0,
|
|
1691
|
+
allScores: [],
|
|
1692
|
+
factCount: 0,
|
|
1693
|
+
warnings
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
const memoryVec = superpose(
|
|
1697
|
+
dimMatchedFacts.map((fact) => bind(toHRR(fact.roleVector), toHRR(fact.fillerVector)))
|
|
1698
|
+
);
|
|
1699
|
+
const candidate = unbind(memoryVec, toHRR(roleVector));
|
|
1700
|
+
const vocabVecs = input.vocabulary.map(
|
|
1701
|
+
(entry) => toHRR(vsaNormalizeVector(entry.vector, `vocabulary[${entry.label}].vector`))
|
|
1702
|
+
);
|
|
1703
|
+
const bestVec = cleanup(candidate, vocabVecs);
|
|
1704
|
+
const bestIdx = vocabVecs.indexOf(bestVec);
|
|
1705
|
+
const allScores = input.vocabulary.map((entry, idx) => ({
|
|
1706
|
+
label: entry.label,
|
|
1707
|
+
score: cosineSimilarity(candidate, vocabVecs[idx])
|
|
1708
|
+
}));
|
|
1709
|
+
allScores.sort((a, b) => b.score - a.score);
|
|
1710
|
+
const fillerLabel = bestIdx >= 0 ? input.vocabulary[bestIdx].label : "";
|
|
1711
|
+
const score = allScores[0]?.score ?? 0;
|
|
1712
|
+
if (score < LOW_CONFIDENCE_THRESHOLD) {
|
|
1713
|
+
warnings.push({
|
|
1714
|
+
code: "vsa_low_confidence",
|
|
1715
|
+
message: `Best match "${fillerLabel}" score ${score.toFixed(3)} below ${LOW_CONFIDENCE_THRESHOLD}`
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
if (facts.length >= maxFacts) {
|
|
1719
|
+
warnings.push({
|
|
1720
|
+
code: "vsa_fact_limit_reached",
|
|
1721
|
+
message: `Loaded ${facts.length} facts (cap=${maxFacts}); recall may degrade with more`
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
return {
|
|
1725
|
+
fillerLabel,
|
|
1726
|
+
score,
|
|
1727
|
+
allScores,
|
|
1728
|
+
factCount: facts.length,
|
|
1729
|
+
warnings
|
|
1730
|
+
};
|
|
1731
|
+
},
|
|
1732
|
+
async listFacts(input) {
|
|
1733
|
+
const facts = await storage.queryFacts({
|
|
1734
|
+
userId: input.userId,
|
|
1735
|
+
scopeTag: input.scopeTag,
|
|
1736
|
+
botId: input.botId,
|
|
1737
|
+
includeDeprecated: input.includeDeprecated
|
|
1738
|
+
});
|
|
1739
|
+
return facts.map((fact) => ({
|
|
1740
|
+
factId: fact.factId,
|
|
1741
|
+
userId: fact.userId,
|
|
1742
|
+
roleLabel: fact.roleLabel,
|
|
1743
|
+
fillerLabel: fact.fillerLabel,
|
|
1744
|
+
scopeTag: fact.scopeTag,
|
|
1745
|
+
botId: fact.botId,
|
|
1746
|
+
createdAt: fact.createdAt,
|
|
1747
|
+
deprecatedAt: fact.deprecatedAt
|
|
1748
|
+
}));
|
|
1749
|
+
},
|
|
1750
|
+
async forget(input) {
|
|
1751
|
+
const result = await storage.deprecateFacts({
|
|
1752
|
+
userId: input.userId,
|
|
1753
|
+
factIds: input.factIds,
|
|
1754
|
+
reason: input.reason
|
|
1755
|
+
});
|
|
1756
|
+
return { deprecatedCount: result.deprecatedCount };
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
|
|
999
1761
|
// src/mcp.ts
|
|
1000
1762
|
init_chroma_memory_index();
|
|
1001
1763
|
init_raw_message_store();
|
|
1764
|
+
init_sqlite_raw_message_store();
|
|
1002
1765
|
var DEFAULT_NAME = "@melandlabs/memory-store";
|
|
1003
1766
|
var DEFAULT_VERSION = "0.1.0";
|
|
1004
1767
|
async function embedMissingMessages(messages, deps) {
|
|
@@ -1038,6 +1801,9 @@ async function startMcpServer(options = {}) {
|
|
|
1038
1801
|
env: options.env
|
|
1039
1802
|
});
|
|
1040
1803
|
const search = createUnifiedSearch(options.unified);
|
|
1804
|
+
const vsaDbPath = resolveSQLiteRawMessageDbPath(options.dbPath);
|
|
1805
|
+
const vsaStorage = await getSQLiteVsaStore({ dbPath: vsaDbPath });
|
|
1806
|
+
const vsa = createVsaRecall(vsaStorage);
|
|
1041
1807
|
server.tool("memory.health", "Returns the health status of the memory store.", async () => ({
|
|
1042
1808
|
content: [{ type: "text", text: JSON.stringify({ ok: true }) }]
|
|
1043
1809
|
}));
|
|
@@ -1045,6 +1811,15 @@ async function startMcpServer(options = {}) {
|
|
|
1045
1811
|
userId: z.string(),
|
|
1046
1812
|
query: z.string().min(1),
|
|
1047
1813
|
sources: z.array(z.enum(["memory", "insights", "knowledge"])).optional().describe("Default: ['memory','insights','knowledge']"),
|
|
1814
|
+
tiers: z.array(z.enum(["summary", "raw", "insight", "knowledge"])).optional().describe(
|
|
1815
|
+
"Per-tier evidence scope for synthesis. Default: ['summary','raw','insight','knowledge']. Ignored when `synthesize` is falsy."
|
|
1816
|
+
),
|
|
1817
|
+
synthesize: z.union([z.boolean(), z.object({ responseSchema: z.record(z.string(), z.unknown()).optional() })]).optional().describe(
|
|
1818
|
+
"Opt-in LLM synthesis. `true` returns `{ answer, evidence, results, warnings }`. `synthesize.responseSchema` extracts `answer` from a JSON payload."
|
|
1819
|
+
),
|
|
1820
|
+
// Top-level pass-through kept for backward-shaped callers; the SDK
|
|
1821
|
+
// coerces it into `synthesize.responseSchema` when present.
|
|
1822
|
+
responseSchema: z.record(z.string(), z.unknown()).optional(),
|
|
1048
1823
|
limit: z.number().int().min(1).max(50).default(10),
|
|
1049
1824
|
threshold: z.number().min(-1).max(1).default(0.7),
|
|
1050
1825
|
botIds: z.array(z.string()).optional(),
|
|
@@ -1077,17 +1852,51 @@ async function startMcpServer(options = {}) {
|
|
|
1077
1852
|
userId: z.string(),
|
|
1078
1853
|
messageId: z.string()
|
|
1079
1854
|
};
|
|
1855
|
+
const reflectApplySchema = {
|
|
1856
|
+
userId: z.string(),
|
|
1857
|
+
query: z.string().min(1),
|
|
1858
|
+
ownerScopeUserId: z.string().describe("Required: who owns the writes."),
|
|
1859
|
+
botIds: z.array(z.string()).optional(),
|
|
1860
|
+
dateFrom: z.string().optional(),
|
|
1861
|
+
dateTo: z.string().optional(),
|
|
1862
|
+
tiers: z.array(z.enum(["summary", "raw", "insight", "knowledge"])).optional().describe("Default: ['summary','raw','insight','knowledge']"),
|
|
1863
|
+
limit: z.number().int().min(1).max(200).default(20),
|
|
1864
|
+
threshold: z.number().min(-1).max(1).default(0.7),
|
|
1865
|
+
dryRun: z.boolean().default(false),
|
|
1866
|
+
expectedVersion: z.string().optional(),
|
|
1867
|
+
llmPlanReviewMaxTokens: z.number().int().min(64).max(8192).optional(),
|
|
1868
|
+
authToken: z.string().optional()
|
|
1869
|
+
};
|
|
1080
1870
|
server.registerTool(
|
|
1081
|
-
"memory.
|
|
1871
|
+
"memory.search",
|
|
1082
1872
|
{
|
|
1083
|
-
title: "Search
|
|
1084
|
-
description: "
|
|
1873
|
+
title: "Search Memory",
|
|
1874
|
+
description: "Cross-source semantic search across raw messages, insights, and uploaded knowledge. Set `synthesize: true` to opt into LLM synthesis over the gathered evidence. With synthesis enabled, the response carries `answer`, `evidence`, `results`, and `warnings`; otherwise just `results`, `evidence`, and `count`.",
|
|
1085
1875
|
// biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
|
|
1086
1876
|
inputSchema: searchSchema
|
|
1087
1877
|
},
|
|
1088
1878
|
async (args) => {
|
|
1089
1879
|
const a = args;
|
|
1090
|
-
const
|
|
1880
|
+
const wantsSynthesis = typeof a.synthesize === "boolean" ? a.synthesize : typeof a.synthesize === "object" && a.synthesize !== null;
|
|
1881
|
+
const responseSchema = typeof a.synthesize === "object" && a.synthesize !== null ? a.synthesize.responseSchema ?? a.responseSchema : a.responseSchema;
|
|
1882
|
+
const input = {
|
|
1883
|
+
userId: a.userId,
|
|
1884
|
+
query: a.query,
|
|
1885
|
+
sources: a.sources,
|
|
1886
|
+
limit: a.limit,
|
|
1887
|
+
threshold: a.threshold,
|
|
1888
|
+
botIds: a.botIds,
|
|
1889
|
+
documentIds: a.documentIds,
|
|
1890
|
+
authToken: a.authToken,
|
|
1891
|
+
includeArchivedInsights: a.includeArchivedInsights,
|
|
1892
|
+
...a.tiers ? { tiers: a.tiers } : {},
|
|
1893
|
+
...wantsSynthesis ? {
|
|
1894
|
+
synthesize: {
|
|
1895
|
+
...responseSchema ? { responseSchema } : {}
|
|
1896
|
+
}
|
|
1897
|
+
} : {}
|
|
1898
|
+
};
|
|
1899
|
+
const result = await search.search(input);
|
|
1091
1900
|
return {
|
|
1092
1901
|
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1093
1902
|
};
|
|
@@ -1155,6 +1964,212 @@ async function startMcpServer(options = {}) {
|
|
|
1155
1964
|
};
|
|
1156
1965
|
}
|
|
1157
1966
|
);
|
|
1967
|
+
server.registerTool(
|
|
1968
|
+
"memory.consolidate",
|
|
1969
|
+
{
|
|
1970
|
+
title: "Consolidate",
|
|
1971
|
+
description: "Agentic write-back: gathers evidence like `memory.search({ synthesize: true })`, builds a memory-consolidation plan, optionally asks the LLM to veto unsafe entries, then persists via the attached graph store + soft-deprecates via the storage adapter. Set `dryRun: true` to inspect the plan without writing.",
|
|
1972
|
+
// biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
|
|
1973
|
+
inputSchema: reflectApplySchema
|
|
1974
|
+
},
|
|
1975
|
+
async (args) => {
|
|
1976
|
+
const a = args;
|
|
1977
|
+
const input = {
|
|
1978
|
+
userId: a.userId,
|
|
1979
|
+
query: a.query,
|
|
1980
|
+
ownerScope: { userId: a.ownerScopeUserId },
|
|
1981
|
+
botIds: a.botIds,
|
|
1982
|
+
dateFrom: a.dateFrom,
|
|
1983
|
+
dateTo: a.dateTo,
|
|
1984
|
+
tiers: a.tiers,
|
|
1985
|
+
limit: a.limit,
|
|
1986
|
+
threshold: a.threshold,
|
|
1987
|
+
dryRun: a.dryRun ?? false,
|
|
1988
|
+
expectedVersion: a.expectedVersion,
|
|
1989
|
+
authToken: a.authToken,
|
|
1990
|
+
llmPlanReview: a.llmPlanReviewMaxTokens !== void 0 ? { maxTokens: a.llmPlanReviewMaxTokens } : void 0
|
|
1991
|
+
};
|
|
1992
|
+
const result = await applyReflectedPlan(
|
|
1993
|
+
{ ...options.unified ?? {} },
|
|
1994
|
+
{ graphStore: options.graphStore, storage: options.storage },
|
|
1995
|
+
input);
|
|
1996
|
+
return {
|
|
1997
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
);
|
|
2001
|
+
const vsaVocabularyEntry = {
|
|
2002
|
+
label: z.string().min(1),
|
|
2003
|
+
vector: z.array(z.number()).describe("Float32 vector for the vocabulary entry.")
|
|
2004
|
+
};
|
|
2005
|
+
const vsaStoreSchema = {
|
|
2006
|
+
userId: z.string(),
|
|
2007
|
+
roleLabel: z.string().min(1),
|
|
2008
|
+
fillerLabel: z.string().min(1),
|
|
2009
|
+
roleVector: z.array(z.number()),
|
|
2010
|
+
fillerVector: z.array(z.number()),
|
|
2011
|
+
scopeTag: z.string().optional(),
|
|
2012
|
+
botId: z.string().optional(),
|
|
2013
|
+
factId: z.string().optional()
|
|
2014
|
+
};
|
|
2015
|
+
const vsaRecallSchema = {
|
|
2016
|
+
userId: z.string(),
|
|
2017
|
+
roleLabel: z.string().min(1),
|
|
2018
|
+
roleVector: z.array(z.number()),
|
|
2019
|
+
vocabulary: z.array(z.object(vsaVocabularyEntry).passthrough()),
|
|
2020
|
+
scopeTag: z.string().optional(),
|
|
2021
|
+
botId: z.string().optional(),
|
|
2022
|
+
maxFacts: z.number().int().min(1).max(1e4).optional()
|
|
2023
|
+
};
|
|
2024
|
+
const vsaListSchema = {
|
|
2025
|
+
userId: z.string(),
|
|
2026
|
+
scopeTag: z.string().optional(),
|
|
2027
|
+
botId: z.string().optional(),
|
|
2028
|
+
includeDeprecated: z.boolean().default(false)
|
|
2029
|
+
};
|
|
2030
|
+
const vsaForgetSchema = {
|
|
2031
|
+
userId: z.string(),
|
|
2032
|
+
factIds: z.array(z.string()).min(1),
|
|
2033
|
+
reason: z.string().optional()
|
|
2034
|
+
};
|
|
2035
|
+
server.registerTool(
|
|
2036
|
+
"memory.vsaStore",
|
|
2037
|
+
{
|
|
2038
|
+
title: "VSA Store Fact",
|
|
2039
|
+
description: "Persist a (role, filler) binding into the user's VSA memory. The vectors are stored as Float32 BLOBs alongside the labels; later `memory.vsaRecall` rebuilds the memory vector and picks the best vocabulary match.",
|
|
2040
|
+
// biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
|
|
2041
|
+
inputSchema: vsaStoreSchema
|
|
2042
|
+
},
|
|
2043
|
+
async (args) => {
|
|
2044
|
+
const a = args;
|
|
2045
|
+
try {
|
|
2046
|
+
const result = await vsa.storeFact({
|
|
2047
|
+
userId: a.userId,
|
|
2048
|
+
roleLabel: a.roleLabel,
|
|
2049
|
+
fillerLabel: a.fillerLabel,
|
|
2050
|
+
roleVector: a.roleVector,
|
|
2051
|
+
fillerVector: a.fillerVector,
|
|
2052
|
+
scopeTag: a.scopeTag,
|
|
2053
|
+
botId: a.botId,
|
|
2054
|
+
factId: a.factId
|
|
2055
|
+
});
|
|
2056
|
+
return {
|
|
2057
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2058
|
+
};
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
return {
|
|
2061
|
+
content: [
|
|
2062
|
+
{
|
|
2063
|
+
type: "text",
|
|
2064
|
+
text: JSON.stringify({ error: error.message ?? "vsa.store failed" })
|
|
2065
|
+
}
|
|
2066
|
+
],
|
|
2067
|
+
isError: true
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
);
|
|
2072
|
+
server.registerTool(
|
|
2073
|
+
"memory.vsaRecall",
|
|
2074
|
+
{
|
|
2075
|
+
title: "VSA Recall",
|
|
2076
|
+
description: "Rebuild the user's VSA memory vector from stored (role, filler) facts, unbind by the requested role, and cleanup against the supplied vocabulary. Returns the best-match label plus a sorted score list and warnings (e.g. low confidence, dim mismatch).",
|
|
2077
|
+
// biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
|
|
2078
|
+
inputSchema: vsaRecallSchema
|
|
2079
|
+
},
|
|
2080
|
+
async (args) => {
|
|
2081
|
+
const a = args;
|
|
2082
|
+
try {
|
|
2083
|
+
const result = await vsa.recall({
|
|
2084
|
+
userId: a.userId,
|
|
2085
|
+
roleLabel: a.roleLabel,
|
|
2086
|
+
roleVector: a.roleVector,
|
|
2087
|
+
vocabulary: a.vocabulary,
|
|
2088
|
+
scopeTag: a.scopeTag,
|
|
2089
|
+
botId: a.botId,
|
|
2090
|
+
maxFacts: a.maxFacts
|
|
2091
|
+
});
|
|
2092
|
+
return {
|
|
2093
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2094
|
+
};
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
return {
|
|
2097
|
+
content: [
|
|
2098
|
+
{
|
|
2099
|
+
type: "text",
|
|
2100
|
+
text: JSON.stringify({ error: error.message ?? "vsa.recall failed" })
|
|
2101
|
+
}
|
|
2102
|
+
],
|
|
2103
|
+
isError: true
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
);
|
|
2108
|
+
server.registerTool(
|
|
2109
|
+
"memory.vsaList",
|
|
2110
|
+
{
|
|
2111
|
+
title: "VSA List Facts",
|
|
2112
|
+
description: "List the (role, filler) facts stored for a user. Vectors are stripped from the response \u2014 for the underlying storage layer, query SQLite directly.",
|
|
2113
|
+
// biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
|
|
2114
|
+
inputSchema: vsaListSchema
|
|
2115
|
+
},
|
|
2116
|
+
async (args) => {
|
|
2117
|
+
const a = args;
|
|
2118
|
+
try {
|
|
2119
|
+
const facts = await vsa.listFacts({
|
|
2120
|
+
userId: a.userId,
|
|
2121
|
+
scopeTag: a.scopeTag,
|
|
2122
|
+
botId: a.botId,
|
|
2123
|
+
includeDeprecated: a.includeDeprecated
|
|
2124
|
+
});
|
|
2125
|
+
return {
|
|
2126
|
+
content: [{ type: "text", text: JSON.stringify({ facts }) }]
|
|
2127
|
+
};
|
|
2128
|
+
} catch (error) {
|
|
2129
|
+
return {
|
|
2130
|
+
content: [
|
|
2131
|
+
{
|
|
2132
|
+
type: "text",
|
|
2133
|
+
text: JSON.stringify({ error: error.message ?? "vsa.list failed" })
|
|
2134
|
+
}
|
|
2135
|
+
],
|
|
2136
|
+
isError: true
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
);
|
|
2141
|
+
server.registerTool(
|
|
2142
|
+
"memory.vsaForget",
|
|
2143
|
+
{
|
|
2144
|
+
title: "VSA Forget Facts",
|
|
2145
|
+
description: "Soft-delete one or more VSA facts by id. Idempotent \u2014 facts already deprecated return `deprecatedCount: 0`.",
|
|
2146
|
+
// biome-ignore lint/suspicious/noExplicitAny: MCP SDK expects Zod schema type
|
|
2147
|
+
inputSchema: vsaForgetSchema
|
|
2148
|
+
},
|
|
2149
|
+
async (args) => {
|
|
2150
|
+
const a = args;
|
|
2151
|
+
try {
|
|
2152
|
+
const result = await vsa.forget({
|
|
2153
|
+
userId: a.userId,
|
|
2154
|
+
factIds: a.factIds,
|
|
2155
|
+
reason: a.reason
|
|
2156
|
+
});
|
|
2157
|
+
return {
|
|
2158
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
2159
|
+
};
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
return {
|
|
2162
|
+
content: [
|
|
2163
|
+
{
|
|
2164
|
+
type: "text",
|
|
2165
|
+
text: JSON.stringify({ error: error.message ?? "vsa.forget failed" })
|
|
2166
|
+
}
|
|
2167
|
+
],
|
|
2168
|
+
isError: true
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
);
|
|
1158
2173
|
const transport = new StdioServerTransport();
|
|
1159
2174
|
await server.connect(transport);
|
|
1160
2175
|
return server;
|