@smyslenny/agent-memory 2.1.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 +51 -27
- package/README.en.md +153 -0
- package/README.md +95 -122
- package/dist/bin/agent-memory.js +28 -485
- package/dist/bin/agent-memory.js.map +1 -1
- package/dist/index.d.ts +53 -143
- package/dist/index.js +289 -574
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +585 -631
- package/dist/mcp/server.js.map +1 -1
- package/docs/design/0004-agent-memory-integration.md +316 -0
- package/docs/design/0005-reranker-api-integration.md +276 -0
- package/docs/design/0006-multi-provider-embedding.md +196 -0
- package/docs/design/0014-memory-core-dedup.md +722 -0
- package/docs/design/TEMPLATE.md +67 -0
- package/docs/roadmap/integration-plan-v1.md +139 -0
- package/docs/roadmap/memory-architecture.md +168 -0
- package/docs/roadmap/warm-boot.md +135 -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,246 +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 qVec = Float32Array.from(await provider.embed(query));
|
|
857
|
-
const embeddings = listEmbeddings(db, agentId, model);
|
|
858
|
-
const scored = [];
|
|
859
|
-
for (const e of embeddings) {
|
|
860
|
-
scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
|
|
861
|
-
}
|
|
862
|
-
scored.sort((a, b) => b.score - a.score);
|
|
863
|
-
const semanticTop = scored.slice(0, semanticCandidates);
|
|
864
|
-
const fused = fuseRrf(
|
|
865
|
-
[
|
|
866
|
-
{ name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
|
|
867
|
-
{ name: "semantic", items: semanticTop }
|
|
868
|
-
],
|
|
869
|
-
rrfK
|
|
870
|
-
);
|
|
871
|
-
const ids = [...fused.keys()];
|
|
872
|
-
const memories = fetchMemories(db, ids, agentId);
|
|
873
|
-
const byId = new Map(memories.map((m) => [m.id, m]));
|
|
874
|
-
const out = [];
|
|
875
|
-
for (const [id, meta] of fused) {
|
|
876
|
-
const mem = byId.get(id);
|
|
877
|
-
if (!mem) continue;
|
|
878
|
-
out.push({
|
|
879
|
-
memory: mem,
|
|
880
|
-
score: meta.score,
|
|
881
|
-
matchReason: meta.sources.sort().join("+")
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
out.sort((a, b) => b.score - a.score);
|
|
885
|
-
return out.slice(0, limit);
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// src/search/providers.ts
|
|
889
|
-
function getEmbeddingProviderFromEnv() {
|
|
890
|
-
const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
|
|
891
|
-
if (provider === "none" || provider === "off" || provider === "false") return null;
|
|
892
|
-
if (provider === "openai") {
|
|
893
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
894
|
-
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
|
|
895
|
-
const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
|
|
896
|
-
if (!apiKey) return null;
|
|
897
|
-
return createOpenAIProvider({ apiKey, model, baseUrl });
|
|
898
|
-
}
|
|
899
|
-
if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
|
|
900
|
-
const apiKey = process.env.DASHSCOPE_API_KEY;
|
|
901
|
-
const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
|
|
902
|
-
const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
|
|
903
|
-
if (!apiKey) return null;
|
|
904
|
-
return createDashScopeProvider({ apiKey, model, baseUrl });
|
|
905
|
-
}
|
|
906
|
-
return null;
|
|
907
|
-
}
|
|
908
|
-
function authHeader(apiKey) {
|
|
909
|
-
return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
|
|
910
|
-
}
|
|
911
|
-
function normalizeEmbedding(e) {
|
|
912
|
-
if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
|
|
913
|
-
if (e.length === 0) throw new Error("Invalid embedding: empty");
|
|
914
|
-
return e.map((x) => {
|
|
915
|
-
if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
|
|
916
|
-
return x;
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
function createOpenAIProvider(opts) {
|
|
920
|
-
const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
|
|
921
|
-
return {
|
|
922
|
-
id: "openai",
|
|
923
|
-
model: opts.model,
|
|
924
|
-
async embed(text) {
|
|
925
|
-
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
|
|
926
|
-
method: "POST",
|
|
927
|
-
headers: {
|
|
928
|
-
"content-type": "application/json",
|
|
929
|
-
authorization: authHeader(opts.apiKey)
|
|
930
|
-
},
|
|
931
|
-
body: JSON.stringify({ model: opts.model, input: text })
|
|
932
|
-
});
|
|
933
|
-
if (!resp.ok) {
|
|
934
|
-
const body = await resp.text().catch(() => "");
|
|
935
|
-
throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
936
|
-
}
|
|
937
|
-
const data = await resp.json();
|
|
938
|
-
const emb = data.data?.[0]?.embedding;
|
|
939
|
-
return normalizeEmbedding(emb);
|
|
940
|
-
}
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
function createDashScopeProvider(opts) {
|
|
944
|
-
const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
|
|
945
|
-
return {
|
|
946
|
-
id: "dashscope",
|
|
947
|
-
model: opts.model,
|
|
948
|
-
async embed(text) {
|
|
949
|
-
const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
|
|
950
|
-
method: "POST",
|
|
951
|
-
headers: {
|
|
952
|
-
"content-type": "application/json",
|
|
953
|
-
authorization: authHeader(opts.apiKey)
|
|
954
|
-
},
|
|
955
|
-
body: JSON.stringify({
|
|
956
|
-
model: opts.model,
|
|
957
|
-
input: { texts: [text] }
|
|
958
|
-
})
|
|
959
|
-
});
|
|
960
|
-
if (!resp.ok) {
|
|
961
|
-
const body = await resp.text().catch(() => "");
|
|
962
|
-
throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
|
|
963
|
-
}
|
|
964
|
-
const data = await resp.json();
|
|
965
|
-
const emb = data?.output?.embeddings?.[0]?.embedding ?? data?.output?.embeddings?.[0]?.vector ?? data?.output?.embedding ?? data?.data?.[0]?.embedding;
|
|
966
|
-
return normalizeEmbedding(emb);
|
|
967
|
-
}
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// src/search/embed.ts
|
|
972
|
-
async function embedMemory(db, memoryId, provider, opts) {
|
|
973
|
-
const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
|
|
974
|
-
if (!row) return false;
|
|
975
|
-
if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
|
|
976
|
-
const model = opts?.model ?? provider.model;
|
|
977
|
-
const maxChars = opts?.maxChars ?? 2e3;
|
|
978
|
-
const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
|
|
979
|
-
const vector = await provider.embed(text);
|
|
980
|
-
upsertEmbedding(db, {
|
|
981
|
-
agent_id: row.agent_id,
|
|
982
|
-
memory_id: row.id,
|
|
983
|
-
model,
|
|
984
|
-
vector
|
|
985
|
-
});
|
|
986
|
-
return true;
|
|
987
|
-
}
|
|
988
|
-
async function embedMissingForAgent(db, provider, opts) {
|
|
989
|
-
const agentId = opts?.agent_id ?? "default";
|
|
990
|
-
const model = opts?.model ?? provider.model;
|
|
991
|
-
const limit = opts?.limit ?? 1e3;
|
|
992
|
-
const rows = db.prepare(
|
|
993
|
-
`SELECT m.id
|
|
994
|
-
FROM memories m
|
|
995
|
-
LEFT JOIN embeddings e
|
|
996
|
-
ON e.memory_id = m.id AND e.agent_id = m.agent_id AND e.model = ?
|
|
997
|
-
WHERE m.agent_id = ? AND e.memory_id IS NULL
|
|
998
|
-
ORDER BY m.updated_at DESC
|
|
999
|
-
LIMIT ?`
|
|
1000
|
-
).all(model, agentId, limit);
|
|
1001
|
-
let embedded = 0;
|
|
1002
|
-
for (const r of rows) {
|
|
1003
|
-
const ok = await embedMemory(db, r.id, provider, { agent_id: agentId, model, maxChars: opts?.maxChars });
|
|
1004
|
-
if (ok) embedded++;
|
|
1005
|
-
}
|
|
1006
|
-
return { embedded, scanned: rows.length };
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
631
|
// src/core/path.ts
|
|
1010
632
|
var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
|
|
1011
633
|
function parseUri(uri) {
|
|
@@ -1154,33 +776,15 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
|
|
|
1154
776
|
).all(...agentId ? [threshold, agentId] : [threshold]);
|
|
1155
777
|
}
|
|
1156
778
|
|
|
1157
|
-
// src/core/snapshot.ts
|
|
1158
|
-
function createSnapshot(db, memoryId, action, changedBy) {
|
|
1159
|
-
const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
|
|
1160
|
-
if (!memory) throw new Error(`Memory not found: ${memoryId}`);
|
|
1161
|
-
const id = newId();
|
|
1162
|
-
db.prepare(
|
|
1163
|
-
`INSERT INTO snapshots (id, memory_id, content, changed_by, action, created_at)
|
|
1164
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1165
|
-
).run(id, memoryId, memory.content, changedBy ?? null, action, now());
|
|
1166
|
-
return { id, memory_id: memoryId, content: memory.content, changed_by: changedBy ?? null, action, created_at: now() };
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
779
|
// src/sleep/tidy.ts
|
|
1170
780
|
function runTidy(db, opts) {
|
|
1171
781
|
const threshold = opts?.vitalityThreshold ?? 0.05;
|
|
1172
|
-
const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
|
|
1173
782
|
const agentId = opts?.agent_id;
|
|
1174
783
|
let archived = 0;
|
|
1175
784
|
let orphansCleaned = 0;
|
|
1176
|
-
let snapshotsPruned = 0;
|
|
1177
785
|
const transaction = db.transaction(() => {
|
|
1178
786
|
const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
|
|
1179
787
|
for (const mem of decayed) {
|
|
1180
|
-
try {
|
|
1181
|
-
createSnapshot(db, mem.id, "delete", "tidy");
|
|
1182
|
-
} catch {
|
|
1183
|
-
}
|
|
1184
788
|
deleteMemory(db, mem.id);
|
|
1185
789
|
archived++;
|
|
1186
790
|
}
|
|
@@ -1192,35 +796,15 @@ function runTidy(db, opts) {
|
|
|
1192
796
|
"DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
|
|
1193
797
|
).run();
|
|
1194
798
|
orphansCleaned = orphans.changes;
|
|
1195
|
-
const memoriesWithSnapshots = agentId ? db.prepare(
|
|
1196
|
-
`SELECT s.memory_id, COUNT(*) as cnt
|
|
1197
|
-
FROM snapshots s
|
|
1198
|
-
JOIN memories m ON m.id = s.memory_id
|
|
1199
|
-
WHERE m.agent_id = ?
|
|
1200
|
-
GROUP BY s.memory_id HAVING cnt > ?`
|
|
1201
|
-
).all(agentId, maxSnapshots) : db.prepare(
|
|
1202
|
-
`SELECT memory_id, COUNT(*) as cnt FROM snapshots
|
|
1203
|
-
GROUP BY memory_id HAVING cnt > ?`
|
|
1204
|
-
).all(maxSnapshots);
|
|
1205
|
-
for (const { memory_id } of memoriesWithSnapshots) {
|
|
1206
|
-
const pruned = db.prepare(
|
|
1207
|
-
`DELETE FROM snapshots WHERE id NOT IN (
|
|
1208
|
-
SELECT id FROM snapshots WHERE memory_id = ?
|
|
1209
|
-
ORDER BY created_at DESC LIMIT ?
|
|
1210
|
-
) AND memory_id = ?`
|
|
1211
|
-
).run(memory_id, maxSnapshots, memory_id);
|
|
1212
|
-
snapshotsPruned += pruned.changes;
|
|
1213
|
-
}
|
|
1214
799
|
});
|
|
1215
800
|
transaction();
|
|
1216
|
-
return { archived, orphansCleaned
|
|
801
|
+
return { archived, orphansCleaned };
|
|
1217
802
|
}
|
|
1218
803
|
|
|
1219
804
|
// src/sleep/govern.ts
|
|
1220
805
|
function runGovern(db, opts) {
|
|
1221
806
|
const agentId = opts?.agent_id;
|
|
1222
807
|
let orphanPaths = 0;
|
|
1223
|
-
let orphanLinks = 0;
|
|
1224
808
|
let emptyMemories = 0;
|
|
1225
809
|
const transaction = db.transaction(() => {
|
|
1226
810
|
const pathResult = agentId ? db.prepare(
|
|
@@ -1229,23 +813,11 @@ function runGovern(db, opts) {
|
|
|
1229
813
|
AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
|
|
1230
814
|
).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
|
|
1231
815
|
orphanPaths = pathResult.changes;
|
|
1232
|
-
const linkResult = agentId ? db.prepare(
|
|
1233
|
-
`DELETE FROM links WHERE
|
|
1234
|
-
agent_id = ? AND (
|
|
1235
|
-
source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
|
|
1236
|
-
target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
|
|
1237
|
-
)`
|
|
1238
|
-
).run(agentId, agentId, agentId) : db.prepare(
|
|
1239
|
-
`DELETE FROM links WHERE
|
|
1240
|
-
source_id NOT IN (SELECT id FROM memories) OR
|
|
1241
|
-
target_id NOT IN (SELECT id FROM memories)`
|
|
1242
|
-
).run();
|
|
1243
|
-
orphanLinks = linkResult.changes;
|
|
1244
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();
|
|
1245
817
|
emptyMemories = emptyResult.changes;
|
|
1246
818
|
});
|
|
1247
819
|
transaction();
|
|
1248
|
-
return { orphanPaths,
|
|
820
|
+
return { orphanPaths, emptyMemories };
|
|
1249
821
|
}
|
|
1250
822
|
|
|
1251
823
|
// src/core/guard.ts
|
|
@@ -1375,7 +947,6 @@ function syncOne(db, input) {
|
|
|
1375
947
|
}
|
|
1376
948
|
case "update": {
|
|
1377
949
|
if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
|
|
1378
|
-
createSnapshot(db, guardResult.existingId, "update", "sync");
|
|
1379
950
|
updateMemory(db, guardResult.existingId, { content: input.content });
|
|
1380
951
|
return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
1381
952
|
}
|
|
@@ -1383,7 +954,6 @@ function syncOne(db, input) {
|
|
|
1383
954
|
if (!guardResult.existingId || !guardResult.mergedContent) {
|
|
1384
955
|
return { action: "skipped", reason: "Missing merge data" };
|
|
1385
956
|
}
|
|
1386
|
-
createSnapshot(db, guardResult.existingId, "merge", "sync");
|
|
1387
957
|
updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
|
|
1388
958
|
return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
|
|
1389
959
|
}
|
|
@@ -1403,27 +973,26 @@ function getAgentId() {
|
|
|
1403
973
|
}
|
|
1404
974
|
function printHelp() {
|
|
1405
975
|
console.log(`
|
|
1406
|
-
\u{1F9E0} AgentMemory
|
|
976
|
+
\u{1F9E0} AgentMemory v3 \u2014 Sleep-cycle memory for AI agents
|
|
1407
977
|
|
|
1408
978
|
Usage: agent-memory <command> [options]
|
|
1409
979
|
|
|
1410
980
|
Commands:
|
|
1411
|
-
init
|
|
1412
|
-
db:migrate
|
|
1413
|
-
embed [--limit N] Embed missing memories (requires provider)
|
|
981
|
+
init Create database
|
|
982
|
+
db:migrate Run schema migrations (no-op if up-to-date)
|
|
1414
983
|
remember <content> [--uri X] [--type T] Store a memory
|
|
1415
|
-
recall <query> [--limit N]
|
|
1416
|
-
boot
|
|
1417
|
-
status
|
|
1418
|
-
reflect [decay|tidy|govern|all]
|
|
1419
|
-
reindex
|
|
1420
|
-
migrate <dir>
|
|
1421
|
-
export <dir>
|
|
1422
|
-
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
|
|
1423
992
|
|
|
1424
993
|
Environment:
|
|
1425
|
-
AGENT_MEMORY_DB
|
|
1426
|
-
AGENT_MEMORY_AGENT_ID
|
|
994
|
+
AGENT_MEMORY_DB Database path (default: ./agent-memory.db)
|
|
995
|
+
AGENT_MEMORY_AGENT_ID Agent ID (default: "default")
|
|
1427
996
|
`);
|
|
1428
997
|
}
|
|
1429
998
|
function getFlag(flag) {
|
|
@@ -1448,20 +1017,6 @@ async function main() {
|
|
|
1448
1017
|
db.close();
|
|
1449
1018
|
break;
|
|
1450
1019
|
}
|
|
1451
|
-
case "embed": {
|
|
1452
|
-
const provider = getEmbeddingProviderFromEnv();
|
|
1453
|
-
if (!provider) {
|
|
1454
|
-
console.error("Embedding provider not configured. Set AGENT_MEMORY_EMBEDDINGS_PROVIDER=openai|qwen and the corresponding API key.");
|
|
1455
|
-
process.exit(1);
|
|
1456
|
-
}
|
|
1457
|
-
const db = openDatabase({ path: getDbPath() });
|
|
1458
|
-
const agentId = getAgentId();
|
|
1459
|
-
const limit = parseInt(getFlag("--limit") ?? "200");
|
|
1460
|
-
const r = await embedMissingForAgent(db, provider, { agent_id: agentId, limit });
|
|
1461
|
-
console.log(`\u2705 Embedded: ${r.embedded}/${r.scanned} (agent_id=${agentId}, model=${provider.model})`);
|
|
1462
|
-
db.close();
|
|
1463
|
-
break;
|
|
1464
|
-
}
|
|
1465
1020
|
case "remember": {
|
|
1466
1021
|
const content = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
|
|
1467
1022
|
if (!content) {
|
|
@@ -1472,14 +1027,7 @@ async function main() {
|
|
|
1472
1027
|
const uri = getFlag("--uri");
|
|
1473
1028
|
const type = getFlag("--type") ?? "knowledge";
|
|
1474
1029
|
const agentId = getAgentId();
|
|
1475
|
-
const
|
|
1476
|
-
const result = syncOne(db, { content, type, uri, agent_id: agentId });
|
|
1477
|
-
if (provider && result.memoryId && (result.action === "added" || result.action === "updated" || result.action === "merged")) {
|
|
1478
|
-
try {
|
|
1479
|
-
await embedMemory(db, result.memoryId, provider, { agent_id: agentId });
|
|
1480
|
-
} catch {
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1030
|
+
const result = syncOne(db, { content, type, uri, source: "manual", agent_id: agentId });
|
|
1483
1031
|
console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
|
|
1484
1032
|
db.close();
|
|
1485
1033
|
break;
|
|
@@ -1491,16 +1039,17 @@ async function main() {
|
|
|
1491
1039
|
process.exit(1);
|
|
1492
1040
|
}
|
|
1493
1041
|
const db = openDatabase({ path: getDbPath() });
|
|
1494
|
-
const limit = parseInt(getFlag("--limit") ?? "10");
|
|
1495
|
-
const { intent } = classifyIntent(query);
|
|
1496
|
-
const strategy = getStrategy(intent);
|
|
1042
|
+
const limit = parseInt(getFlag("--limit") ?? "10", 10);
|
|
1497
1043
|
const agentId = getAgentId();
|
|
1498
|
-
const
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
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}
|
|
1502
1051
|
`);
|
|
1503
|
-
for (const r of
|
|
1052
|
+
for (const r of weighted) {
|
|
1504
1053
|
const p = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][r.memory.priority];
|
|
1505
1054
|
const v = (r.memory.vitality * 100).toFixed(0);
|
|
1506
1055
|
console.log(`${p} P${r.memory.priority} [${v}%] ${r.memory.content.slice(0, 80)}`);
|
|
@@ -1529,17 +1078,11 @@ async function main() {
|
|
|
1529
1078
|
const stats = countMemories(db, agentId);
|
|
1530
1079
|
const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(agentId).c;
|
|
1531
1080
|
const paths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(agentId).c;
|
|
1532
|
-
const links = db.prepare("SELECT COUNT(*) as c FROM links WHERE agent_id = ?").get(agentId).c;
|
|
1533
|
-
const snaps = db.prepare(
|
|
1534
|
-
`SELECT COUNT(*) as c FROM snapshots s
|
|
1535
|
-
JOIN memories m ON m.id = s.memory_id
|
|
1536
|
-
WHERE m.agent_id = ?`
|
|
1537
|
-
).get(agentId).c;
|
|
1538
1081
|
console.log("\u{1F9E0} AgentMemory Status\n");
|
|
1539
1082
|
console.log(` Total memories: ${stats.total}`);
|
|
1540
1083
|
console.log(` By type: ${Object.entries(stats.by_type).map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
1541
1084
|
console.log(` By priority: ${Object.entries(stats.by_priority).map(([k, v]) => `${k}=${v}`).join(", ")}`);
|
|
1542
|
-
console.log(` Paths: ${paths}
|
|
1085
|
+
console.log(` Paths: ${paths}`);
|
|
1543
1086
|
console.log(` Low vitality (<10%): ${lowVit}`);
|
|
1544
1087
|
db.close();
|
|
1545
1088
|
break;
|
|
@@ -1556,11 +1099,11 @@ async function main() {
|
|
|
1556
1099
|
}
|
|
1557
1100
|
if (phase === "tidy" || phase === "all") {
|
|
1558
1101
|
const r = runTidy(db, { agent_id: agentId });
|
|
1559
|
-
console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans
|
|
1102
|
+
console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans`);
|
|
1560
1103
|
}
|
|
1561
1104
|
if (phase === "govern" || phase === "all") {
|
|
1562
1105
|
const r = runGovern(db, { agent_id: agentId });
|
|
1563
|
-
console.log(` Govern: ${r.orphanPaths} paths, ${r.
|
|
1106
|
+
console.log(` Govern: ${r.orphanPaths} paths, ${r.emptyMemories} empty cleaned`);
|
|
1564
1107
|
}
|
|
1565
1108
|
db.close();
|
|
1566
1109
|
break;
|