@agentmemory/agentmemory 0.9.11 → 0.9.13

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/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  import { TriggerAction, registerWorker } from "iii-sdk";
3
4
  import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
5
  import { basename, dirname, extname, join, resolve, sep } from "node:path";
@@ -82,7 +83,7 @@ function detectProvider(env) {
82
83
  if (!hasRealValue(env["GEMINI_API_KEY"]) && hasRealValue(env["GOOGLE_API_KEY"])) process.stderr.write("[agentmemory] GOOGLE_API_KEY detected — treating as GEMINI_API_KEY. Set GEMINI_API_KEY in ~/.agentmemory/.env to silence this warning.\n");
83
84
  return {
84
85
  provider: "gemini",
85
- model: env["GEMINI_MODEL"] || "gemini-2.0-flash",
86
+ model: env["GEMINI_MODEL"] || "gemini-2.5-flash",
86
87
  maxTokens
87
88
  };
88
89
  }
@@ -570,7 +571,8 @@ var FallbackChainProvider = class {
570
571
  //#endregion
571
572
  //#region src/providers/embedding/gemini.ts
572
573
  const BATCH_LIMIT = 100;
573
- const API_BASE = "https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:batchEmbedContent";
574
+ const MODEL = "models/gemini-embedding-001";
575
+ const API_BASE = `https://generativelanguage.googleapis.com/v1beta/${MODEL}:batchEmbedContents`;
574
576
  var GeminiEmbeddingProvider = class {
575
577
  name = "gemini";
576
578
  dimensions = 768;
@@ -591,8 +593,9 @@ var GeminiEmbeddingProvider = class {
591
593
  method: "POST",
592
594
  headers: { "Content-Type": "application/json" },
593
595
  body: JSON.stringify({ requests: chunk.map((t) => ({
594
- model: "models/text-embedding-004",
595
- content: { parts: [{ text: t }] }
596
+ model: MODEL,
597
+ content: { parts: [{ text: t }] },
598
+ outputDimensionality: this.dimensions
596
599
  })) })
597
600
  });
598
601
  if (!response.ok) {
@@ -600,11 +603,26 @@ var GeminiEmbeddingProvider = class {
600
603
  throw new Error(`Gemini embedding failed (${response.status}): ${err}`);
601
604
  }
602
605
  const data = await response.json();
603
- for (const emb of data.embeddings) results.push(new Float32Array(emb.values));
606
+ for (const emb of data.embeddings) results.push(l2Normalize(new Float32Array(emb.values)));
604
607
  }
605
608
  return results;
606
609
  }
607
610
  };
611
+ let zeroNormWarned = false;
612
+ function l2Normalize(vec) {
613
+ let sum = 0;
614
+ for (let i = 0; i < vec.length; i++) sum += vec[i] * vec[i];
615
+ const norm = Math.sqrt(sum);
616
+ if (norm === 0) {
617
+ if (!zeroNormWarned) {
618
+ zeroNormWarned = true;
619
+ process.stderr.write(`[agentmemory] warn: gemini-embedding-001 returned a zero-norm embedding (length=${vec.length}); leaving it un-normalized. Subsequent zero-norm vectors will not be reported.\n`);
620
+ }
621
+ return vec;
622
+ }
623
+ for (let i = 0; i < vec.length; i++) vec[i] = vec[i] / norm;
624
+ return vec;
625
+ }
608
626
 
609
627
  //#endregion
610
628
  //#region src/providers/embedding/openai.ts
@@ -2075,6 +2093,110 @@ function getSynonyms(stemmedTerm) {
2075
2093
  return syns ? [...syns] : [];
2076
2094
  }
2077
2095
 
2096
+ //#endregion
2097
+ //#region src/state/cjk-segmenter.ts
2098
+ const cjkRequire = createRequire(import.meta.url);
2099
+ const CJK_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
2100
+ const KANA_RE = /[\p{Script=Hiragana}\p{Script=Katakana}]/u;
2101
+ const HANGUL_RE = /\p{Script=Hangul}/u;
2102
+ const CJK_RUN_RE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+/gu;
2103
+ const HANGUL_BLOCK_RE = /[가-힯]+/g;
2104
+ const hintShown = /* @__PURE__ */ new Set();
2105
+ function hasCjk(text) {
2106
+ return CJK_RE.test(text);
2107
+ }
2108
+ function showHintOnce(key, message) {
2109
+ if (hintShown.has(key)) return;
2110
+ hintShown.add(key);
2111
+ if (typeof process !== "undefined" && process.stderr?.write) process.stderr.write(`agentmemory: ${message}\n`);
2112
+ }
2113
+ let jiebaInstance = null;
2114
+ let jiebaLoaded = false;
2115
+ function getJieba() {
2116
+ if (jiebaLoaded) return jiebaInstance;
2117
+ jiebaLoaded = true;
2118
+ try {
2119
+ const mod = cjkRequire("@node-rs/jieba");
2120
+ try {
2121
+ const dictMod = cjkRequire("@node-rs/jieba/dict");
2122
+ jiebaInstance = mod.Jieba.withDict(dictMod.dict);
2123
+ } catch {
2124
+ jiebaInstance = new mod.Jieba();
2125
+ }
2126
+ return jiebaInstance;
2127
+ } catch {
2128
+ showHintOnce("jieba", "install @node-rs/jieba to improve Chinese search; falling back to whole-string tokenization");
2129
+ return null;
2130
+ }
2131
+ }
2132
+ let jaSegmenterInstance = null;
2133
+ let jaSegmenterLoaded = false;
2134
+ function getJaSegmenter() {
2135
+ if (jaSegmenterLoaded) return jaSegmenterInstance;
2136
+ jaSegmenterLoaded = true;
2137
+ try {
2138
+ jaSegmenterInstance = new (cjkRequire("tiny-segmenter"))();
2139
+ return jaSegmenterInstance;
2140
+ } catch {
2141
+ showHintOnce("tiny-segmenter", "install tiny-segmenter to improve Japanese search; falling back to whole-string tokenization");
2142
+ return null;
2143
+ }
2144
+ }
2145
+ function cleanTokens(tokens) {
2146
+ const out = [];
2147
+ for (const t of tokens) {
2148
+ const trimmed = t.trim();
2149
+ if (trimmed) out.push(trimmed);
2150
+ }
2151
+ return out;
2152
+ }
2153
+ function segmentHan(text) {
2154
+ const j = getJieba();
2155
+ if (!j) return [text];
2156
+ try {
2157
+ return cleanTokens(j.cut(text, true));
2158
+ } catch {
2159
+ return [text];
2160
+ }
2161
+ }
2162
+ function segmentKana(text) {
2163
+ const s = getJaSegmenter();
2164
+ if (!s) return [text];
2165
+ try {
2166
+ return cleanTokens(s.segment(text));
2167
+ } catch {
2168
+ return [text];
2169
+ }
2170
+ }
2171
+ function segmentHangul(text) {
2172
+ const out = [];
2173
+ for (const m of text.matchAll(HANGUL_BLOCK_RE)) if (m[0]) out.push(m[0]);
2174
+ return out;
2175
+ }
2176
+ function segmentCjk(text) {
2177
+ if (!hasCjk(text)) return [text];
2178
+ const out = [];
2179
+ let cursor = 0;
2180
+ for (const m of text.matchAll(CJK_RUN_RE)) {
2181
+ const start = m.index ?? 0;
2182
+ const run = m[0];
2183
+ const end = start + run.length;
2184
+ if (start > cursor) {
2185
+ const piece = text.slice(cursor, start).trim();
2186
+ if (piece) out.push(piece);
2187
+ }
2188
+ if (HANGUL_RE.test(run)) out.push(...segmentHangul(run));
2189
+ else if (KANA_RE.test(run)) out.push(...segmentKana(run));
2190
+ else out.push(...segmentHan(run));
2191
+ cursor = end;
2192
+ }
2193
+ if (cursor < text.length) {
2194
+ const trailing = text.slice(cursor).trim();
2195
+ if (trailing) out.push(trailing);
2196
+ }
2197
+ return out;
2198
+ }
2199
+
2078
2200
  //#endregion
2079
2201
  //#region src/state/search-index.ts
2080
2202
  var SearchIndex = class SearchIndex {
@@ -2231,7 +2353,15 @@ var SearchIndex = class SearchIndex {
2231
2353
  return this.tokenize(parts.join(" ").toLowerCase());
2232
2354
  }
2233
2355
  tokenize(text) {
2234
- return text.replace(/[^\w\s/.\-_]/g, " ").split(/\s+/).filter((t) => t.length > 1).map((t) => stem(t));
2356
+ const cleaned = text.replace(/[^\p{L}\p{N}\s/.\\-_]/gu, " ");
2357
+ const out = [];
2358
+ for (const raw of cleaned.split(/\s+/)) {
2359
+ if (raw.length < 2) continue;
2360
+ if (hasCjk(raw)) {
2361
+ for (const seg of segmentCjk(raw)) if (seg.length >= 1) out.push(seg);
2362
+ } else out.push(stem(raw));
2363
+ }
2364
+ return out;
2235
2365
  }
2236
2366
  getSortedTerms() {
2237
2367
  if (!this.sortedTerms) this.sortedTerms = Array.from(this.invertedIndex.keys()).sort();
@@ -2530,13 +2660,55 @@ async function deleteAccessLog(kv, memoryId) {
2530
2660
  //#endregion
2531
2661
  //#region src/functions/search.ts
2532
2662
  let index = null;
2663
+ let vectorIndex = null;
2664
+ let currentEmbeddingProvider = null;
2533
2665
  function getSearchIndex() {
2534
2666
  if (!index) index = new SearchIndex();
2535
2667
  return index;
2536
2668
  }
2669
+ function setVectorIndex(idx) {
2670
+ vectorIndex = idx;
2671
+ }
2672
+ function setEmbeddingProvider(provider) {
2673
+ currentEmbeddingProvider = provider;
2674
+ }
2675
+ const EMBED_MAX_CHARS = 16e3;
2676
+ function clipEmbedInput(text) {
2677
+ if (text.length <= EMBED_MAX_CHARS) return text;
2678
+ return text.slice(0, EMBED_MAX_CHARS);
2679
+ }
2680
+ async function vectorIndexAddGuarded(id, sessionId, text, context) {
2681
+ const vi = vectorIndex;
2682
+ const ep = currentEmbeddingProvider;
2683
+ if (!vi || !ep) return false;
2684
+ try {
2685
+ const embedding = await ep.embed(clipEmbedInput(text));
2686
+ if (embedding.length !== ep.dimensions) {
2687
+ logger.warn("vector-index add: dimension mismatch — skipping", {
2688
+ kind: context.kind,
2689
+ id: context.logId,
2690
+ provider: ep.name,
2691
+ expected: ep.dimensions,
2692
+ received: embedding.length
2693
+ });
2694
+ return false;
2695
+ }
2696
+ vi.add(id, sessionId, embedding);
2697
+ return true;
2698
+ } catch (err) {
2699
+ logger.warn("vector-index add: embed failed — skipping", {
2700
+ kind: context.kind,
2701
+ id: context.logId,
2702
+ provider: ep.name,
2703
+ error: err instanceof Error ? err.message : String(err)
2704
+ });
2705
+ return false;
2706
+ }
2707
+ }
2537
2708
  async function rebuildIndex(kv) {
2538
2709
  const idx = getSearchIndex();
2539
2710
  idx.clear();
2711
+ vectorIndex?.clear();
2540
2712
  let count = 0;
2541
2713
  try {
2542
2714
  const memories = await kv.list(KV.memories);
@@ -2544,6 +2716,10 @@ async function rebuildIndex(kv) {
2544
2716
  if (memory.isLatest === false) continue;
2545
2717
  if (!memory.title || !memory.content) continue;
2546
2718
  idx.add(memoryToObservation(memory));
2719
+ await vectorIndexAddGuarded(memory.id, memory.sessionIds[0] ?? "memory", memory.title + " " + memory.content, {
2720
+ kind: "memory",
2721
+ logId: memory.id
2722
+ });
2547
2723
  count++;
2548
2724
  }
2549
2725
  } catch (err) {
@@ -2568,6 +2744,10 @@ async function rebuildIndex(kv) {
2568
2744
  if (failedSessions.length > 0) logger.warn("rebuildIndex: failed to load observations for sessions", { failedSessions });
2569
2745
  for (const observations of obsPerSession) for (const obs of observations) if (obs.title && obs.narrative) {
2570
2746
  idx.add(obs);
2747
+ await vectorIndexAddGuarded(obs.id, obs.sessionId, obs.title + " " + obs.narrative, {
2748
+ kind: "observation",
2749
+ logId: obs.id
2750
+ });
2571
2751
  count++;
2572
2752
  }
2573
2753
  return count;
@@ -2874,6 +3054,10 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
2874
3054
  const synthetic = buildSyntheticCompression(raw);
2875
3055
  await kv.set(KV.observations(payload.sessionId), obsId, synthetic);
2876
3056
  getSearchIndex().add(synthetic);
3057
+ await vectorIndexAddGuarded(synthetic.id, synthetic.sessionId, synthetic.title + " " + (synthetic.narrative || ""), {
3058
+ kind: "synthetic",
3059
+ logId: synthetic.id
3060
+ });
2877
3061
  await sdk.trigger({
2878
3062
  function_id: "stream::set",
2879
3063
  payload: {
@@ -4118,7 +4302,20 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
4118
4302
  ...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
4119
4303
  };
4120
4304
  await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
4121
- getSearchIndex().add(compressed);
4305
+ try {
4306
+ getSearchIndex().add(compressed);
4307
+ } catch (err) {
4308
+ logger.warn("Failed to index compressed observation into BM25", {
4309
+ obsId: compressed.id,
4310
+ sessionId: compressed.sessionId,
4311
+ title: compressed.title,
4312
+ error: err instanceof Error ? err.message : String(err)
4313
+ });
4314
+ }
4315
+ await vectorIndexAddGuarded(compressed.id, compressed.sessionId, compressed.title + " " + (compressed.narrative || ""), {
4316
+ kind: "observation",
4317
+ logId: compressed.id
4318
+ });
4122
4319
  const streamResults = await Promise.allSettled([sdk.trigger({
4123
4320
  function_id: "stream::set",
4124
4321
  payload: {
@@ -4958,6 +5155,10 @@ function registerRememberFunction(sdk, kv) {
4958
5155
  error: err instanceof Error ? err.message : String(err)
4959
5156
  });
4960
5157
  }
5158
+ await vectorIndexAddGuarded(memory.id, memory.sessionIds[0] ?? "memory", memory.title + " " + memory.content, {
5159
+ kind: "memory",
5160
+ logId: memory.id
5161
+ });
4961
5162
  if (supersededId) await sdk.trigger({
4962
5163
  function_id: "mem::cascade-update",
4963
5164
  payload: { supersededMemoryId: supersededId },
@@ -5755,7 +5956,7 @@ function registerAutoForgetFunction(sdk, kv) {
5755
5956
 
5756
5957
  //#endregion
5757
5958
  //#region src/version.ts
5758
- const VERSION = "0.9.11";
5959
+ const VERSION = "0.9.13";
5759
5960
 
5760
5961
  //#endregion
5761
5962
  //#region src/functions/export-import.ts
@@ -5884,7 +6085,9 @@ function registerExportImportFunction(sdk, kv) {
5884
6085
  "0.9.8",
5885
6086
  "0.9.9",
5886
6087
  "0.9.10",
5887
- "0.9.11"
6088
+ "0.9.11",
6089
+ "0.9.12",
6090
+ "0.9.13"
5888
6091
  ]).has(importData.version)) return {
5889
6092
  success: false,
5890
6093
  error: `Unsupported export version: ${importData.version}`
@@ -19451,6 +19654,8 @@ async function main() {
19451
19654
  const metricsStore = new MetricsStore(kv);
19452
19655
  const dedupMap = new DedupMap();
19453
19656
  const vectorIndex = embeddingProvider ? new VectorIndex() : null;
19657
+ setVectorIndex(vectorIndex);
19658
+ setEmbeddingProvider(embeddingProvider);
19454
19659
  initMetrics(hasGetMeter(sdk) ? sdk.getMeter.bind(sdk) : void 0);
19455
19660
  registerPrivacyFunction(sdk);
19456
19661
  registerObserveFunction(sdk, kv, dedupMap, config.maxObservationsPerSession);