@smyslenny/agent-memory 2.2.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -41
- package/README.en.md +153 -0
- package/README.md +86 -153
- package/dist/bin/agent-memory.js +28 -534
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +50 -167
- package/dist/index.js +289 -692
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +584 -748
- package/dist/mcp/server.js.map +1 -1
- package/docs/design/0014-memory-core-dedup.md +722 -0
- package/docs/design/TEMPLATE.md +67 -0
- package/package.json +2 -3
- package/README.zh-CN.md +0 -170
package/dist/bin/agent-memory.js
CHANGED
|
@@ -577,144 +577,6 @@ function exportMemories(db, dirPath, opts) {
|
|
|
577
577
|
return { exported, files };
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
-
// src/search/intent.ts
|
|
581
|
-
var INTENT_PATTERNS = {
|
|
582
|
-
factual: [
|
|
583
|
-
// English
|
|
584
|
-
/^(what|who|where|which|how much|how many)\b/i,
|
|
585
|
-
/\b(name|address|number|password|config|setting)\b/i,
|
|
586
|
-
// Chinese - questions about facts
|
|
587
|
-
/是(什么|谁|哪|啥)/,
|
|
588
|
-
/叫(什么|啥)/,
|
|
589
|
-
/(名字|地址|号码|密码|配置|设置|账号|邮箱|链接|版本)/,
|
|
590
|
-
/(多少|几个|哪个|哪些|哪里)/,
|
|
591
|
-
// Chinese - lookup patterns
|
|
592
|
-
/(查一下|找一下|看看|搜一下)/,
|
|
593
|
-
/(.+)是什么$/
|
|
594
|
-
],
|
|
595
|
-
temporal: [
|
|
596
|
-
// English
|
|
597
|
-
/^(when|what time|how long)\b/i,
|
|
598
|
-
/\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
|
|
599
|
-
/\b(first|latest|newest|oldest|previous|next)\b/i,
|
|
600
|
-
// Chinese - time expressions
|
|
601
|
-
/什么时候/,
|
|
602
|
-
/(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
|
|
603
|
-
/(几月|几号|几点|多久|多长时间)/,
|
|
604
|
-
/(上次|下次|第一次|最后一次|那天|那时)/,
|
|
605
|
-
// Date patterns
|
|
606
|
-
/\d{4}[-/.]\d{1,2}/,
|
|
607
|
-
/\d{1,2}月\d{1,2}[日号]/,
|
|
608
|
-
// Chinese - temporal context
|
|
609
|
-
/(历史|记录|日志|以来|至今|期间)/
|
|
610
|
-
],
|
|
611
|
-
causal: [
|
|
612
|
-
// English
|
|
613
|
-
/^(why|how come|what caused)\b/i,
|
|
614
|
-
/\b(because|due to|reason|cause|result)\b/i,
|
|
615
|
-
// Chinese - causal questions
|
|
616
|
-
/为(什么|啥|何)/,
|
|
617
|
-
/(原因|导致|造成|引起|因为|所以|结果)/,
|
|
618
|
-
/(怎么回事|怎么了|咋回事|咋了)/,
|
|
619
|
-
/(为啥|凭啥|凭什么)/,
|
|
620
|
-
// Chinese - problem/diagnosis
|
|
621
|
-
/(出(了|了什么)?问题|报错|失败|出错|bug)/
|
|
622
|
-
],
|
|
623
|
-
exploratory: [
|
|
624
|
-
// English
|
|
625
|
-
/^(how|tell me about|explain|describe|show me)\b/i,
|
|
626
|
-
/^(what do you think|what about|any)\b/i,
|
|
627
|
-
/\b(overview|summary|list|compare)\b/i,
|
|
628
|
-
// Chinese - exploratory
|
|
629
|
-
/(怎么样|怎样|如何)/,
|
|
630
|
-
/(介绍|说说|讲讲|聊聊|谈谈)/,
|
|
631
|
-
/(有哪些|有什么|有没有)/,
|
|
632
|
-
/(关于|对于|至于|关联)/,
|
|
633
|
-
/(总结|概括|梳理|回顾|盘点)/,
|
|
634
|
-
// Chinese - opinion/analysis
|
|
635
|
-
/(看法|想法|意见|建议|评价|感觉|觉得)/,
|
|
636
|
-
/(对比|比较|区别|差异|优缺点)/
|
|
637
|
-
]
|
|
638
|
-
};
|
|
639
|
-
var CN_STRUCTURE_BOOSTS = {
|
|
640
|
-
factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
|
|
641
|
-
temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
|
|
642
|
-
causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
|
|
643
|
-
exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
|
|
644
|
-
};
|
|
645
|
-
function classifyIntent(query) {
|
|
646
|
-
const scores = {
|
|
647
|
-
factual: 0,
|
|
648
|
-
exploratory: 0,
|
|
649
|
-
temporal: 0,
|
|
650
|
-
causal: 0
|
|
651
|
-
};
|
|
652
|
-
for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
|
|
653
|
-
for (const pattern of patterns) {
|
|
654
|
-
if (pattern.test(query)) {
|
|
655
|
-
scores[intent] += 1;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
|
|
660
|
-
for (const pattern of patterns) {
|
|
661
|
-
if (pattern.test(query)) {
|
|
662
|
-
scores[intent] += 0.5;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
const tokens = tokenize(query);
|
|
667
|
-
const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
668
|
-
if (totalPatternScore === 0 && tokens.length <= 3) {
|
|
669
|
-
scores.factual += 1;
|
|
670
|
-
}
|
|
671
|
-
let maxIntent = "factual";
|
|
672
|
-
let maxScore = 0;
|
|
673
|
-
for (const [intent, score] of Object.entries(scores)) {
|
|
674
|
-
if (score > maxScore) {
|
|
675
|
-
maxScore = score;
|
|
676
|
-
maxIntent = intent;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
|
|
680
|
-
const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
|
|
681
|
-
return { intent: maxIntent, confidence };
|
|
682
|
-
}
|
|
683
|
-
function getStrategy(intent) {
|
|
684
|
-
switch (intent) {
|
|
685
|
-
case "factual":
|
|
686
|
-
return { boostRecent: false, boostPriority: true, limit: 5 };
|
|
687
|
-
case "temporal":
|
|
688
|
-
return { boostRecent: true, boostPriority: false, limit: 10 };
|
|
689
|
-
case "causal":
|
|
690
|
-
return { boostRecent: false, boostPriority: false, limit: 10 };
|
|
691
|
-
case "exploratory":
|
|
692
|
-
return { boostRecent: false, boostPriority: false, limit: 15 };
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// src/search/rerank.ts
|
|
697
|
-
function rerank(results, opts) {
|
|
698
|
-
const now2 = Date.now();
|
|
699
|
-
const scored = results.map((r) => {
|
|
700
|
-
let finalScore = r.score;
|
|
701
|
-
if (opts.boostPriority) {
|
|
702
|
-
const priorityMultiplier = [4, 3, 2, 1][r.memory.priority] ?? 1;
|
|
703
|
-
finalScore *= priorityMultiplier;
|
|
704
|
-
}
|
|
705
|
-
if (opts.boostRecent && r.memory.updated_at) {
|
|
706
|
-
const age = now2 - new Date(r.memory.updated_at).getTime();
|
|
707
|
-
const daysSinceUpdate = age / (1e3 * 60 * 60 * 24);
|
|
708
|
-
const recencyBoost = Math.max(0.1, 1 / (1 + daysSinceUpdate * 0.1));
|
|
709
|
-
finalScore *= recencyBoost;
|
|
710
|
-
}
|
|
711
|
-
finalScore *= Math.max(0.1, r.memory.vitality);
|
|
712
|
-
return { ...r, score: finalScore };
|
|
713
|
-
});
|
|
714
|
-
scored.sort((a, b) => b.score - a.score);
|
|
715
|
-
return scored.slice(0, opts.limit);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
580
|
// src/search/bm25.ts
|
|
719
581
|
function searchBM25(db, query, opts) {
|
|
720
582
|
const limit = opts?.limit ?? 20;
|
|
@@ -766,295 +628,6 @@ function buildFtsQuery(text) {
|
|
|
766
628
|
return tokens.map((w) => `"${w}"`).join(" OR ");
|
|
767
629
|
}
|
|
768
630
|
|
|
769
|
-
// src/search/embeddings.ts
|
|
770
|
-
function encodeEmbedding(vector) {
|
|
771
|
-
const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
|
|
772
|
-
return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
773
|
-
}
|
|
774
|
-
function decodeEmbedding(buf) {
|
|
775
|
-
const copy = Buffer.from(buf);
|
|
776
|
-
return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
|
|
777
|
-
}
|
|
778
|
-
function upsertEmbedding(db, input) {
|
|
779
|
-
const ts = now();
|
|
780
|
-
const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
|
|
781
|
-
const blob = encodeEmbedding(vec);
|
|
782
|
-
db.prepare(
|
|
783
|
-
`INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
|
|
784
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
785
|
-
ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
|
|
786
|
-
dim = excluded.dim,
|
|
787
|
-
vector = excluded.vector,
|
|
788
|
-
updated_at = excluded.updated_at`
|
|
789
|
-
).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
|
|
790
|
-
}
|
|
791
|
-
function listEmbeddings(db, agent_id, model) {
|
|
792
|
-
const rows = db.prepare(
|
|
793
|
-
"SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
|
|
794
|
-
).all(agent_id, model);
|
|
795
|
-
return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// src/search/hybrid.ts
|
|
799
|
-
function cosine(a, b) {
|
|
800
|
-
const n = Math.min(a.length, b.length);
|
|
801
|
-
let dot = 0;
|
|
802
|
-
let na = 0;
|
|
803
|
-
let nb = 0;
|
|
804
|
-
for (let i = 0; i < n; i++) {
|
|
805
|
-
const x = a[i];
|
|
806
|
-
const y = b[i];
|
|
807
|
-
dot += x * y;
|
|
808
|
-
na += x * x;
|
|
809
|
-
nb += y * y;
|
|
810
|
-
}
|
|
811
|
-
if (na === 0 || nb === 0) return 0;
|
|
812
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
813
|
-
}
|
|
814
|
-
function rrfScore(rank, k) {
|
|
815
|
-
return 1 / (k + rank);
|
|
816
|
-
}
|
|
817
|
-
function fuseRrf(lists, k) {
|
|
818
|
-
const out = /* @__PURE__ */ new Map();
|
|
819
|
-
for (const list of lists) {
|
|
820
|
-
for (let i = 0; i < list.items.length; i++) {
|
|
821
|
-
const it = list.items[i];
|
|
822
|
-
const rank = i + 1;
|
|
823
|
-
const add = rrfScore(rank, k);
|
|
824
|
-
const prev = out.get(it.id);
|
|
825
|
-
if (!prev) out.set(it.id, { score: add, sources: [list.name] });
|
|
826
|
-
else {
|
|
827
|
-
prev.score += add;
|
|
828
|
-
if (!prev.sources.includes(list.name)) prev.sources.push(list.name);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
return out;
|
|
833
|
-
}
|
|
834
|
-
function fetchMemories(db, ids, agentId) {
|
|
835
|
-
if (ids.length === 0) return [];
|
|
836
|
-
const placeholders = ids.map(() => "?").join(", ");
|
|
837
|
-
const sql = agentId ? `SELECT * FROM memories WHERE id IN (${placeholders}) AND agent_id = ?` : `SELECT * FROM memories WHERE id IN (${placeholders})`;
|
|
838
|
-
const rows = db.prepare(sql).all(...agentId ? [...ids, agentId] : ids);
|
|
839
|
-
return rows;
|
|
840
|
-
}
|
|
841
|
-
async function searchHybrid(db, query, opts) {
|
|
842
|
-
const agentId = opts?.agent_id ?? "default";
|
|
843
|
-
const limit = opts?.limit ?? 10;
|
|
844
|
-
const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
|
|
845
|
-
const semanticCandidates = opts?.semanticCandidates ?? 50;
|
|
846
|
-
const rrfK = opts?.rrfK ?? 60;
|
|
847
|
-
const bm25 = searchBM25(db, query, {
|
|
848
|
-
agent_id: agentId,
|
|
849
|
-
limit: limit * bm25Mult
|
|
850
|
-
});
|
|
851
|
-
const provider = opts?.embeddingProvider ?? null;
|
|
852
|
-
const model = opts?.embeddingModel ?? provider?.model;
|
|
853
|
-
if (!provider || !model) {
|
|
854
|
-
return bm25.slice(0, limit);
|
|
855
|
-
}
|
|
856
|
-
const embedFn = provider.embedQuery ?? provider.embed;
|
|
857
|
-
const qVec = Float32Array.from(await embedFn.call(provider, query));
|
|
858
|
-
const embeddings = listEmbeddings(db, agentId, model);
|
|
859
|
-
const scored = [];
|
|
860
|
-
for (const e of embeddings) {
|
|
861
|
-
scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
|
|
862
|
-
}
|
|
863
|
-
scored.sort((a, b) => b.score - a.score);
|
|
864
|
-
const semanticTop = scored.slice(0, semanticCandidates);
|
|
865
|
-
const fused = fuseRrf(
|
|
866
|
-
[
|
|
867
|
-
{ name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
|
|
868
|
-
{ name: "semantic", items: semanticTop }
|
|
869
|
-
],
|
|
870
|
-
rrfK
|
|
871
|
-
);
|
|
872
|
-
const ids = [...fused.keys()];
|
|
873
|
-
const memories = fetchMemories(db, ids, agentId);
|
|
874
|
-
const byId = new Map(memories.map((m) => [m.id, m]));
|
|
875
|
-
const out = [];
|
|
876
|
-
for (const [id, meta] of fused) {
|
|
877
|
-
const mem = byId.get(id);
|
|
878
|
-
if (!mem) continue;
|
|
879
|
-
out.push({
|
|
880
|
-
memory: mem,
|
|
881
|
-
score: meta.score,
|
|
882
|
-
matchReason: meta.sources.sort().join("+")
|
|
883
|
-
});
|
|
884
|
-
}
|
|
885
|
-
out.sort((a, b) => b.score - a.score);
|
|
886
|
-
return out.slice(0, limit);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// src/search/providers.ts
|
|
890
|
-
var QWEN_DEFAULT_INSTRUCTION = "Given a query, retrieve the most semantically relevant document";
|
|
891
|
-
function getDefaultInstruction(model) {
|
|
892
|
-
const m = model.toLowerCase();
|
|
893
|
-
if (m.includes("qwen")) return QWEN_DEFAULT_INSTRUCTION;
|
|
894
|
-
if (m.includes("gemini")) return null;
|
|
895
|
-
return null;
|
|
896
|
-
}
|
|
897
|
-
function resolveInstruction(model) {
|
|
898
|
-
const override = process.env.AGENT_MEMORY_EMBEDDINGS_INSTRUCTION;
|
|
899
|
-
if (override !== void 0) {
|
|
900
|
-
const normalized = override.trim();
|
|
901
|
-
if (!normalized) return null;
|
|
902
|
-
const lowered = normalized.toLowerCase();
|
|
903
|
-
if (lowered === "none" || lowered === "off" || lowered === "false" || lowered === "null") return null;
|
|
904
|
-
return normalized;
|
|
905
|
-
}
|
|
906
|
-
return getDefaultInstruction(model);
|
|
907
|
-
}
|
|
908
|
-
function buildQueryInput(query, instructionPrefix) {
|
|
909
|
-
if (!instructionPrefix) return query;
|
|
910
|
-
return `Instruct: ${instructionPrefix}
|
|
911
|
-
Query: ${query}`;
|
|
912
|
-
}
|
|
913
|
-
function getEmbeddingProviderFromEnv() {
|
|
914
|
-
const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
|
|
915
|
-
if (provider === "none" || provider === "off" || provider === "false") return null;
|
|
916
|
-
if (provider === "openai") {
|
|
917
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
918
|
-
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
|
|
919
|
-
const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
920
|
-
if (!apiKey) return null;
|
|
921
|
-
const instruction = resolveInstruction(model);
|
|
922
|
-
return createOpenAIProvider({ apiKey, model, baseUrl, instruction });
|
|
923
|
-
}
|
|
924
|
-
if (provider === "gemini" || provider === "google") {
|
|
925
|
-
const apiKey = process.env.GEMINI_API_KEY ?? process.env.OPENAI_API_KEY;
|
|
926
|
-
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "gemini-embedding-001";
|
|
927
|
-
const baseUrl = process.env.GEMINI_BASE_URL ?? process.env.OPENAI_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta";
|
|
928
|
-
if (!apiKey) return null;
|
|
929
|
-
const instruction = resolveInstruction(model);
|
|
930
|
-
return createOpenAIProvider({ id: "gemini", apiKey, model, baseUrl, instruction });
|
|
931
|
-
}
|
|
932
|
-
if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
|
|
933
|
-
const apiKey = process.env.DASHSCOPE_API_KEY;
|
|
934
|
-
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
|
|
935
|
-
const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
|
|
936
|
-
if (!apiKey) return null;
|
|
937
|
-
const instruction = resolveInstruction(model);
|
|
938
|
-
return createDashScopeProvider({ apiKey, model, baseUrl, instruction });
|
|
939
|
-
}
|
|
940
|
-
return null;
|
|
941
|
-
}
|
|
942
|
-
function authHeader(apiKey) {
|
|
943
|
-
return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
|
|
944
|
-
}
|
|
945
|
-
function normalizeEmbedding(e) {
|
|
946
|
-
if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
|
|
947
|
-
if (e.length === 0) throw new Error("Invalid embedding: empty");
|
|
948
|
-
return e.map((x) => {
|
|
949
|
-
if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
|
|
950
|
-
return x;
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
function createOpenAIProvider(opts) {
|
|
954
|
-
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
955
|
-
const instructionPrefix = opts.instruction ?? null;
|
|
956
|
-
async function requestEmbedding(input) {
|
|
957
|
-
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
|
|
958
|
-
method: "POST",
|
|
959
|
-
headers: {
|
|
960
|
-
"content-type": "application/json",
|
|
961
|
-
authorization: authHeader(opts.apiKey)
|
|
962
|
-
},
|
|
963
|
-
body: JSON.stringify({ model: opts.model, input })
|
|
964
|
-
});
|
|
965
|
-
if (!resp.ok) {
|
|
966
|
-
const body = await resp.text().catch(() => "");
|
|
967
|
-
throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
968
|
-
}
|
|
969
|
-
const data = await resp.json();
|
|
970
|
-
return normalizeEmbedding(data.data?.[0]?.embedding);
|
|
971
|
-
}
|
|
972
|
-
return {
|
|
973
|
-
id: opts.id ?? "openai",
|
|
974
|
-
model: opts.model,
|
|
975
|
-
instructionPrefix,
|
|
976
|
-
async embed(text) {
|
|
977
|
-
return requestEmbedding(text);
|
|
978
|
-
},
|
|
979
|
-
async embedQuery(query) {
|
|
980
|
-
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
981
|
-
}
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
function createDashScopeProvider(opts) {
|
|
985
|
-
const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
|
|
986
|
-
const instructionPrefix = opts.instruction ?? null;
|
|
987
|
-
async function requestEmbedding(text) {
|
|
988
|
-
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
|
|
989
|
-
method: "POST",
|
|
990
|
-
headers: {
|
|
991
|
-
"content-type": "application/json",
|
|
992
|
-
authorization: authHeader(opts.apiKey)
|
|
993
|
-
},
|
|
994
|
-
body: JSON.stringify({
|
|
995
|
-
model: opts.model,
|
|
996
|
-
input: { texts: [text] }
|
|
997
|
-
})
|
|
998
|
-
});
|
|
999
|
-
if (!resp.ok) {
|
|
1000
|
-
const body = await resp.text().catch(() => "");
|
|
1001
|
-
throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
1002
|
-
}
|
|
1003
|
-
const data = await resp.json();
|
|
1004
|
-
const emb = data.output?.embeddings?.[0]?.embedding ?? data.output?.embeddings?.[0]?.vector ?? data.output?.embedding ?? data.data?.[0]?.embedding;
|
|
1005
|
-
return normalizeEmbedding(emb);
|
|
1006
|
-
}
|
|
1007
|
-
return {
|
|
1008
|
-
id: "dashscope",
|
|
1009
|
-
model: opts.model,
|
|
1010
|
-
instructionPrefix,
|
|
1011
|
-
async embed(text) {
|
|
1012
|
-
return requestEmbedding(text);
|
|
1013
|
-
},
|
|
1014
|
-
async embedQuery(query) {
|
|
1015
|
-
return requestEmbedding(buildQueryInput(query, instructionPrefix));
|
|
1016
|
-
}
|
|
1017
|
-
};
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// src/search/embed.ts
|
|
1021
|
-
async function embedMemory(db, memoryId, provider, opts) {
|
|
1022
|
-
const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
|
|
1023
|
-
if (!row) return false;
|
|
1024
|
-
if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
|
|
1025
|
-
const model = opts?.model ?? provider.model;
|
|
1026
|
-
const maxChars = opts?.maxChars ?? 2e3;
|
|
1027
|
-
const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
|
|
1028
|
-
const vector = await provider.embed(text);
|
|
1029
|
-
upsertEmbedding(db, {
|
|
1030
|
-
agent_id: row.agent_id,
|
|
1031
|
-
memory_id: row.id,
|
|
1032
|
-
model,
|
|
1033
|
-
vector
|
|
1034
|
-
});
|
|
1035
|
-
return true;
|
|
1036
|
-
}
|
|
1037
|
-
async function embedMissingForAgent(db, provider, opts) {
|
|
1038
|
-
const agentId = opts?.agent_id ?? "default";
|
|
1039
|
-
const model = opts?.model ?? provider.model;
|
|
1040
|
-
const limit = opts?.limit ?? 1e3;
|
|
1041
|
-
const rows = db.prepare(
|
|
1042
|
-
`SELECT m.id
|
|
1043
|
-
FROM memories m
|
|
1044
|
-
LEFT JOIN embeddings e
|
|
1045
|
-
ON e.memory_id = m.id AND e.agent_id = m.agent_id AND e.model = ?
|
|
1046
|
-
WHERE m.agent_id = ? AND e.memory_id IS NULL
|
|
1047
|
-
ORDER BY m.updated_at DESC
|
|
1048
|
-
LIMIT ?`
|
|
1049
|
-
).all(model, agentId, limit);
|
|
1050
|
-
let embedded = 0;
|
|
1051
|
-
for (const r of rows) {
|
|
1052
|
-
const ok = await embedMemory(db, r.id, provider, { agent_id: agentId, model, maxChars: opts?.maxChars });
|
|
1053
|
-
if (ok) embedded++;
|
|
1054
|
-
}
|
|
1055
|
-
return { embedded, scanned: rows.length };
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
631
|
// src/core/path.ts
|
|
1059
632
|
var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
|
|
1060
633
|
function parseUri(uri) {
|
|
@@ -1203,33 +776,15 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
1203
776
|
).all(...agentId ? [threshold, agentId] : [threshold]);
|
|
1204
777
|
}
|
|
1205
778
|
|
|
1206
|
-
// src/core/snapshot.ts
|
|
1207
|
-
function createSnapshot(db, memoryId, action, changedBy) {
|
|
1208
|
-
const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
|
|
1209
|
-
if (!memory) throw new Error(`Memory not found: ${memoryId}`);
|
|
1210
|
-
const id = newId();
|
|
1211
|
-
db.prepare(
|
|
1212
|
-
`INSERT INTO snapshots (id, memory_id, content, changed_by, action, created_at)
|
|
1213
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1214
|
-
).run(id, memoryId, memory.content, changedBy ?? null, action, now());
|
|
1215
|
-
return { id, memory_id: memoryId, content: memory.content, changed_by: changedBy ?? null, action, created_at: now() };
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
779
|
// src/sleep/tidy.ts
|
|
1219
780
|
function runTidy(db, opts) {
|
|
1220
781
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
1221
|
-
const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
|
|
1222
782
|
const agentId = opts?.agent_id;
|
|
1223
783
|
let archived = 0;
|
|
1224
784
|
let orphansCleaned = 0;
|
|
1225
|
-
let snapshotsPruned = 0;
|
|
1226
785
|
const transaction = db.transaction(() => {
|
|
1227
786
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
1228
787
|
for (const mem of decayed) {
|
|
1229
|
-
try {
|
|
1230
|
-
createSnapshot(db, mem.id, "delete", "tidy");
|
|
1231
|
-
} catch {
|
|
1232
|
-
}
|
|
1233
788
|
deleteMemory(db, mem.id);
|
|
1234
789
|
archived++;
|
|
1235
790
|
}
|
|
@@ -1241,35 +796,15 @@ function runTidy(db, opts) {
|
|
|
1241
796
|
"DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
|
|
1242
797
|
).run();
|
|
1243
798
|
orphansCleaned = orphans.changes;
|
|
1244
|
-
const memoriesWithSnapshots = agentId ? db.prepare(
|
|
1245
|
-
`SELECT s.memory_id, COUNT(*) as cnt
|
|
1246
|
-
FROM snapshots s
|
|
1247
|
-
JOIN memories m ON m.id = s.memory_id
|
|
1248
|
-
WHERE m.agent_id = ?
|
|
1249
|
-
GROUP BY s.memory_id HAVING cnt > ?`
|
|
1250
|
-
).all(agentId, maxSnapshots) : db.prepare(
|
|
1251
|
-
`SELECT memory_id, COUNT(*) as cnt FROM snapshots
|
|
1252
|
-
GROUP BY memory_id HAVING cnt > ?`
|
|
1253
|
-
).all(maxSnapshots);
|
|
1254
|
-
for (const { memory_id } of memoriesWithSnapshots) {
|
|
1255
|
-
const pruned = db.prepare(
|
|
1256
|
-
`DELETE FROM snapshots WHERE id NOT IN (
|
|
1257
|
-
SELECT id FROM snapshots WHERE memory_id = ?
|
|
1258
|
-
ORDER BY created_at DESC LIMIT ?
|
|
1259
|
-
) AND memory_id = ?`
|
|
1260
|
-
).run(memory_id, maxSnapshots, memory_id);
|
|
1261
|
-
snapshotsPruned += pruned.changes;
|
|
1262
|
-
}
|
|
1263
799
|
});
|
|
1264
800
|
transaction();
|
|
1265
|
-
return { archived, orphansCleaned
|
|
801
|
+
return { archived, orphansCleaned };
|
|
1266
802
|
}
|
|
1267
803
|
|
|
1268
804
|
// src/sleep/govern.ts
|
|
1269
805
|
function runGovern(db, opts) {
|
|
1270
806
|
const agentId = opts?.agent_id;
|
|
1271
807
|
let orphanPaths = 0;
|
|
1272
|
-
let orphanLinks = 0;
|
|
1273
808
|
let emptyMemories = 0;
|
|
1274
809
|
const transaction = db.transaction(() => {
|
|
1275
810
|
const pathResult = agentId ? db.prepare(
|
|
@@ -1278,23 +813,11 @@ function runGovern(db, opts) {
|
|
|
1278
813
|
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
1279
814
|
).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
|
|
1280
815
|
orphanPaths = pathResult.changes;
|
|
1281
|
-
const linkResult = agentId ? db.prepare(
|
|
1282
|
-
`DELETE FROM links WHERE
|
|
1283
|
-
agent_id = ? AND (
|
|
1284
|
-
source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
|
|
1285
|
-
target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
|
|
1286
|
-
)`
|
|
1287
|
-
).run(agentId, agentId, agentId) : db.prepare(
|
|
1288
|
-
`DELETE FROM links WHERE
|
|
1289
|
-
source_id NOT IN (SELECT id FROM memories) OR
|
|
1290
|
-
target_id NOT IN (SELECT id FROM memories)`
|
|
1291
|
-
).run();
|
|
1292
|
-
orphanLinks = linkResult.changes;
|
|
1293
816
|
const emptyResult = agentId ? db.prepare("DELETE FROM memories WHERE agent_id = ? AND TRIM(content) = ''").run(agentId) : db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
|
|
1294
817
|
emptyMemories = emptyResult.changes;
|
|
1295
818
|
});
|
|
1296
819
|
transaction();
|
|
1297
|
-
return { orphanPaths,
|
|
820
|
+
return { orphanPaths, emptyMemories };
|
|
1298
821
|
}
|
|
1299
822
|
|
|
1300
823
|
// src/core/guard.ts
|
|
@@ -1424,7 +947,6 @@ function syncOne(db, input) {
|
|
|
1424
947
|
}
|
|
1425
948
|
case "update": {
|
|
1426
949
|
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
1427
|
-
createSnapshot(db, guardResult.existingId, "update", "sync");
|
|
1428
950
|
updateMemory(db, guardResult.existingId, { content: input.content });
|
|
1429
951
|
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
1430
952
|
}
|
|
@@ -1432,7 +954,6 @@ function syncOne(db, input) {
|
|
|
1432
954
|
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
1433
955
|
return { action: "skipped", reason: "Missing merge data" };
|
|
1434
956
|
}
|
|
1435
|
-
createSnapshot(db, guardResult.existingId, "merge", "sync");
|
|
1436
957
|
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
1437
958
|
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
1438
959
|
}
|
|
@@ -1452,27 +973,26 @@ function getAgentId() {
|
|
|
1452
973
|
}
|
|
1453
974
|
function printHelp() {
|
|
1454
975
|
console.log(`
|
|
1455
|
-
\u{1F9E0} AgentMemory
|
|
976
|
+
\u{1F9E0} AgentMemory v3 \u2014 Sleep-cycle memory for AI agents
|
|
1456
977
|
|
|
1457
978
|
Usage: agent-memory <command> [options]
|
|
1458
979
|
|
|
1459
980
|
Commands:
|
|
1460
|
-
init
|
|
1461
|
-
db:migrate
|
|
1462
|
-
embed [--limit N] Embed missing memories (requires provider)
|
|
981
|
+
init Create database
|
|
982
|
+
db:migrate Run schema migrations (no-op if up-to-date)
|
|
1463
983
|
remember <content> [--uri X] [--type T] Store a memory
|
|
1464
|
-
recall <query> [--limit N]
|
|
1465
|
-
boot
|
|
1466
|
-
status
|
|
1467
|
-
reflect [decay|tidy|govern|all]
|
|
1468
|
-
reindex
|
|
1469
|
-
migrate <dir>
|
|
1470
|
-
export <dir>
|
|
1471
|
-
help
|
|
984
|
+
recall <query> [--limit N] Search memories (BM25 + priority\xD7vitality)
|
|
985
|
+
boot Load identity memories
|
|
986
|
+
status Show statistics
|
|
987
|
+
reflect [decay|tidy|govern|all] Run sleep cycle
|
|
988
|
+
reindex Rebuild FTS index with jieba tokenizer
|
|
989
|
+
migrate <dir> Import from Markdown files
|
|
990
|
+
export <dir> Export memories to Markdown files
|
|
991
|
+
help Show this help
|
|
1472
992
|
|
|
1473
993
|
Environment:
|
|
1474
|
-
AGENT_MEMORY_DB
|
|
1475
|
-
AGENT_MEMORY_AGENT_ID
|
|
994
|
+
AGENT_MEMORY_DB Database path (default: ./agent-memory.db)
|
|
995
|
+
AGENT_MEMORY_AGENT_ID Agent ID (default: "default")
|
|
1476
996
|
`);
|
|
1477
997
|
}
|
|
1478
998
|
function getFlag(flag) {
|
|
@@ -1497,20 +1017,6 @@ async function main() {
|
|
|
1497
1017
|
db.close();
|
|
1498
1018
|
break;
|
|
1499
1019
|
}
|
|
1500
|
-
case "embed": {
|
|
1501
|
-
const provider = getEmbeddingProviderFromEnv();
|
|
1502
|
-
if (!provider) {
|
|
1503
|
-
console.error("Embedding provider not configured. Set AGENT_MEMORY_EMBEDDINGS_PROVIDER=openai|qwen and the corresponding API key.");
|
|
1504
|
-
process.exit(1);
|
|
1505
|
-
}
|
|
1506
|
-
const db = openDatabase({ path: getDbPath() });
|
|
1507
|
-
const agentId = getAgentId();
|
|
1508
|
-
const limit = parseInt(getFlag("--limit") ?? "200");
|
|
1509
|
-
const r = await embedMissingForAgent(db, provider, { agent_id: agentId, limit });
|
|
1510
|
-
console.log(`\u2705 Embedded: ${r.embedded}/${r.scanned} (agent_id=${agentId}, model=${provider.model})`);
|
|
1511
|
-
db.close();
|
|
1512
|
-
break;
|
|
1513
|
-
}
|
|
1514
1020
|
case "remember": {
|
|
1515
1021
|
const content = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
|
|
1516
1022
|
if (!content) {
|
|
@@ -1521,14 +1027,7 @@ async function main() {
|
|
|
1521
1027
|
const uri = getFlag("--uri");
|
|
1522
1028
|
const type = getFlag("--type") ?? "knowledge";
|
|
1523
1029
|
const agentId = getAgentId();
|
|
1524
|
-
const
|
|
1525
|
-
const result = syncOne(db, { content, type, uri, agent_id: agentId });
|
|
1526
|
-
if (provider && result.memoryId && (result.action === "added" || result.action === "updated" || result.action === "merged")) {
|
|
1527
|
-
try {
|
|
1528
|
-
await embedMemory(db, result.memoryId, provider, { agent_id: agentId });
|
|
1529
|
-
} catch {
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1030
|
+
const result = syncOne(db, { content, type, uri, source: "manual", agent_id: agentId });
|
|
1532
1031
|
console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
|
|
1533
1032
|
db.close();
|
|
1534
1033
|
break;
|
|
@@ -1540,16 +1039,17 @@ async function main() {
|
|
|
1540
1039
|
process.exit(1);
|
|
1541
1040
|
}
|
|
1542
1041
|
const db = openDatabase({ path: getDbPath() });
|
|
1543
|
-
const limit = parseInt(getFlag("--limit") ?? "10");
|
|
1544
|
-
const { intent } = classifyIntent(query);
|
|
1545
|
-
const strategy = getStrategy(intent);
|
|
1042
|
+
const limit = parseInt(getFlag("--limit") ?? "10", 10);
|
|
1546
1043
|
const agentId = getAgentId();
|
|
1547
|
-
const
|
|
1548
|
-
const
|
|
1549
|
-
|
|
1550
|
-
|
|
1044
|
+
const raw = searchBM25(db, query, { agent_id: agentId, limit: Math.max(limit * 2, limit) });
|
|
1045
|
+
const weighted = raw.map((r) => {
|
|
1046
|
+
const weight = [4, 3, 2, 1][r.memory.priority] ?? 1;
|
|
1047
|
+
const vitality = Math.max(0.1, r.memory.vitality);
|
|
1048
|
+
return { ...r, score: r.score * weight * vitality };
|
|
1049
|
+
}).sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1050
|
+
console.log(`\u{1F50D} Results: ${weighted.length}
|
|
1551
1051
|
`);
|
|
1552
|
-
for (const r of
|
|
1052
|
+
for (const r of weighted) {
|
|
1553
1053
|
const p = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][r.memory.priority];
|
|
1554
1054
|
const v = (r.memory.vitality * 100).toFixed(0);
|
|
1555
1055
|
console.log(`${p} P${r.memory.priority} [${v}%] ${r.memory.content.slice(0, 80)}`);
|
|
@@ -1578,17 +1078,11 @@ async function main() {
|
|
|
1578
1078
|
const stats = countMemories(db, agentId);
|
|
1579
1079
|
const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(agentId).c;
|
|
1580
1080
|
const paths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(agentId).c;
|
|
1581
|
-
const links = db.prepare("SELECT COUNT(*) as c FROM links WHERE agent_id = ?").get(agentId).c;
|
|
1582
|
-
const snaps = db.prepare(
|
|
1583
|
-
`SELECT COUNT(*) as c FROM snapshots s
|
|
1584
|
-
JOIN memories m ON m.id = s.memory_id
|
|
1585
|
-
WHERE m.agent_id = ?`
|
|
1586
|
-
).get(agentId).c;
|
|
1587
1081
|
console.log("\u{1F9E0} AgentMemory Status\n");
|
|
1588
1082
|
console.log(` Total memories: ${stats.total}`);
|
|
1589
1083
|
console.log(` By type: ${Object.entries(stats.by_type).map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
1590
1084
|
console.log(` By priority: ${Object.entries(stats.by_priority).map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
1591
|
-
console.log(` Paths: ${paths}
|
|
1085
|
+
console.log(` Paths: ${paths}`);
|
|
1592
1086
|
console.log(` Low vitality (<10%): ${lowVit}`);
|
|
1593
1087
|
db.close();
|
|
1594
1088
|
break;
|
|
@@ -1605,11 +1099,11 @@ async function main() {
|
|
|
1605
1099
|
}
|
|
1606
1100
|
if (phase === "tidy" || phase === "all") {
|
|
1607
1101
|
const r = runTidy(db, { agent_id: agentId });
|
|
1608
|
-
console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans
|
|
1102
|
+
console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans`);
|
|
1609
1103
|
}
|
|
1610
1104
|
if (phase === "govern" || phase === "all") {
|
|
1611
1105
|
const r = runGovern(db, { agent_id: agentId });
|
|
1612
|
-
console.log(` Govern: ${r.orphanPaths} paths, ${r.
|
|
1106
|
+
console.log(` Govern: ${r.orphanPaths} paths, ${r.emptyMemories} empty cleaned`);
|
|
1613
1107
|
}
|
|
1614
1108
|
db.close();
|
|
1615
1109
|
break;
|