@agentmemory/agentmemory 0.7.4 → 0.7.6

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
@@ -1183,12 +1183,66 @@ function extractEntitiesFromQuery(query) {
1183
1183
  return [...new Set(entities)];
1184
1184
  }
1185
1185
 
1186
+ //#endregion
1187
+ //#region src/state/reranker.ts
1188
+ let pipeline = null;
1189
+ let pipelineLoading = null;
1190
+ let pipelineUnavailable = false;
1191
+ async function loadPipeline() {
1192
+ if (pipelineUnavailable) return null;
1193
+ if (pipeline) return pipeline;
1194
+ if (pipelineLoading) return pipelineLoading;
1195
+ pipelineLoading = (async () => {
1196
+ try {
1197
+ const { pipeline: createPipeline } = await import("./transformers-BX_tgxdO.mjs");
1198
+ pipeline = await createPipeline("text-classification", "Xenova/ms-marco-MiniLM-L-6-v2", { quantized: true });
1199
+ return pipeline;
1200
+ } catch {
1201
+ pipeline = null;
1202
+ pipelineUnavailable = true;
1203
+ return null;
1204
+ } finally {
1205
+ pipelineLoading = null;
1206
+ }
1207
+ })();
1208
+ return pipelineLoading;
1209
+ }
1210
+ async function rerank(query, results, topK = 20) {
1211
+ if (results.length <= 1) return results;
1212
+ const reranker = await loadPipeline();
1213
+ if (!reranker) return results;
1214
+ const pairs = results.slice(0, Math.min(results.length, topK)).map((r) => ({
1215
+ text: `${query} [SEP] ${r.observation.title || ""} ${r.observation.narrative || ""}`.slice(0, 512),
1216
+ result: r
1217
+ }));
1218
+ const scores = [];
1219
+ for (const pair of pairs) try {
1220
+ const output = await reranker(pair.text);
1221
+ const score = Array.isArray(output) ? output[0]?.score ?? 0 : 0;
1222
+ scores.push({
1223
+ result: pair.result,
1224
+ rerankScore: score
1225
+ });
1226
+ } catch {
1227
+ scores.push({
1228
+ result: pair.result,
1229
+ rerankScore: pair.result.combinedScore
1230
+ });
1231
+ }
1232
+ scores.sort((a, b) => b.rerankScore - a.rerankScore);
1233
+ return scores.map((s, i) => ({
1234
+ ...s.result,
1235
+ combinedScore: s.rerankScore,
1236
+ rerankPosition: i + 1
1237
+ }));
1238
+ }
1239
+
1186
1240
  //#endregion
1187
1241
  //#region src/state/hybrid-search.ts
1188
1242
  const RRF_K = 60;
1189
1243
  var HybridSearch = class {
1190
1244
  graphRetrieval;
1191
- constructor(bm25, vector, embeddingProvider, kv, bm25Weight = .4, vectorWeight = .6, graphWeight = .3) {
1245
+ constructor(bm25, vector, embeddingProvider, kv, bm25Weight = .4, vectorWeight = .6, graphWeight = .3, rerankEnabled = process.env.RERANK_ENABLED === "true") {
1192
1246
  this.bm25 = bm25;
1193
1247
  this.vector = vector;
1194
1248
  this.embeddingProvider = embeddingProvider;
@@ -1196,6 +1250,7 @@ var HybridSearch = class {
1196
1250
  this.bm25Weight = bm25Weight;
1197
1251
  this.vectorWeight = vectorWeight;
1198
1252
  this.graphWeight = graphWeight;
1253
+ this.rerankEnabled = rerankEnabled;
1199
1254
  this.graphRetrieval = new GraphRetrieval(kv);
1200
1255
  }
1201
1256
  async search(query, limit = 20) {
@@ -1299,8 +1354,18 @@ var HybridSearch = class {
1299
1354
  combinedScore: effectiveBm25W * (1 / (RRF_K + s.bm25Rank)) + effectiveVectorW * (1 / (RRF_K + s.vectorRank)) + effectiveGraphW * (1 / (RRF_K + s.graphRank))
1300
1355
  }));
1301
1356
  combined.sort((a, b) => b.combinedScore - a.combinedScore);
1302
- const diversified = this.diversifyBySession(combined, limit);
1303
- return this.enrichResults(diversified, limit);
1357
+ const retrievalDepth = Math.max(limit, 20);
1358
+ const rerankWindow = 20;
1359
+ const diversified = this.diversifyBySession(combined, retrievalDepth);
1360
+ const enriched = await this.enrichResults(diversified, retrievalDepth);
1361
+ if (this.rerankEnabled && enriched.length > 1) try {
1362
+ const head = enriched.slice(0, rerankWindow);
1363
+ const tail = enriched.slice(rerankWindow);
1364
+ return (await rerank(query, head, rerankWindow)).concat(tail).slice(0, limit);
1365
+ } catch {
1366
+ return enriched.slice(0, limit);
1367
+ }
1368
+ return enriched.slice(0, limit);
1304
1369
  }
1305
1370
  diversifyBySession(results, limit, maxPerSession = 3) {
1306
1371
  const selected = [];
@@ -2414,7 +2479,7 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
2414
2479
 
2415
2480
  //#endregion
2416
2481
  //#region src/functions/context.ts
2417
- function estimateTokens(text) {
2482
+ function estimateTokens$1(text) {
2418
2483
  return Math.ceil(text.length / 3);
2419
2484
  }
2420
2485
  function escapeXmlAttr(s) {
@@ -2440,7 +2505,7 @@ function registerContextFunction(sdk, kv, tokenBudget) {
2440
2505
  blocks.push({
2441
2506
  type: "memory",
2442
2507
  content: profileContent,
2443
- tokens: estimateTokens(profileContent),
2508
+ tokens: estimateTokens$1(profileContent),
2444
2509
  recency: new Date(profile.updatedAt).getTime()
2445
2510
  });
2446
2511
  }
@@ -2455,7 +2520,7 @@ function registerContextFunction(sdk, kv, tokenBudget) {
2455
2520
  blocks.push({
2456
2521
  type: "summary",
2457
2522
  content,
2458
- tokens: estimateTokens(content),
2523
+ tokens: estimateTokens$1(content),
2459
2524
  recency: new Date(summary.createdAt).getTime()
2460
2525
  });
2461
2526
  } else sessionsNeedingObs.push(i);
@@ -2470,7 +2535,7 @@ function registerContextFunction(sdk, kv, tokenBudget) {
2470
2535
  blocks.push({
2471
2536
  type: "observation",
2472
2537
  content,
2473
- tokens: estimateTokens(content),
2538
+ tokens: estimateTokens$1(content),
2474
2539
  recency: new Date(sessions[i].startedAt).getTime()
2475
2540
  });
2476
2541
  }
@@ -2480,7 +2545,7 @@ function registerContextFunction(sdk, kv, tokenBudget) {
2480
2545
  const selected = [];
2481
2546
  const header = `<agentmemory-context project="${escapeXmlAttr(data.project)}">`;
2482
2547
  const footer = `</agentmemory-context>`;
2483
- usedTokens += estimateTokens(header) + estimateTokens(footer);
2548
+ usedTokens += estimateTokens$1(header) + estimateTokens$1(footer);
2484
2549
  for (const block of blocks) {
2485
2550
  if (usedTokens + block.tokens > budget) break;
2486
2551
  selected.push(block.content);
@@ -3740,7 +3805,7 @@ function registerAutoForgetFunction(sdk, kv) {
3740
3805
 
3741
3806
  //#endregion
3742
3807
  //#region src/version.ts
3743
- const VERSION = "0.7.4";
3808
+ const VERSION = "0.7.6";
3744
3809
 
3745
3810
  //#endregion
3746
3811
  //#region src/functions/export-import.ts
@@ -3841,7 +3906,9 @@ function registerExportImportFunction(sdk, kv) {
3841
3906
  "0.7.0",
3842
3907
  "0.7.2",
3843
3908
  "0.7.3",
3844
- "0.7.4"
3909
+ "0.7.4",
3910
+ "0.7.5",
3911
+ "0.7.6"
3845
3912
  ]).has(importData.version)) return {
3846
3913
  success: false,
3847
3914
  error: `Unsupported export version: ${importData.version}`
@@ -8743,6 +8810,407 @@ function registerReflectFunctions(sdk, kv, provider) {
8743
8810
  });
8744
8811
  }
8745
8812
 
8813
+ //#endregion
8814
+ //#region src/functions/working-memory.ts
8815
+ const CORE_SCOPE = "mem:core-memory";
8816
+ function estimateTokens(text) {
8817
+ return Math.ceil(text.length / 3);
8818
+ }
8819
+ function scoreEntry(entry, now) {
8820
+ const recencyScore = 1 / (1 + (now - new Date(entry.lastAccessedAt).getTime()) / (1e3 * 60 * 60 * 24) * .1);
8821
+ const accessScore = Math.log2(entry.accessCount + 1) / 10;
8822
+ return entry.importance / 10 * .5 + recencyScore * .3 + accessScore * .2;
8823
+ }
8824
+ function registerWorkingMemoryFunctions(sdk, kv, tokenBudget) {
8825
+ sdk.registerFunction({
8826
+ id: "mem::core-add",
8827
+ description: "Add a fact to core memory (always included in context)"
8828
+ }, async (data) => {
8829
+ if (!data?.content?.trim()) return {
8830
+ success: false,
8831
+ error: "content is required"
8832
+ };
8833
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8834
+ const entry = {
8835
+ id: generateId("core"),
8836
+ content: data.content.trim(),
8837
+ importance: Math.min(10, Math.max(1, data.importance ?? 7)),
8838
+ pinned: data.pinned ?? false,
8839
+ accessCount: 0,
8840
+ lastAccessedAt: now,
8841
+ createdAt: now
8842
+ };
8843
+ await kv.set(CORE_SCOPE, entry.id, entry);
8844
+ try {
8845
+ await recordAudit(kv, "core_add", "mem::core-add", [entry.id], {
8846
+ content: entry.content.slice(0, 100),
8847
+ importance: entry.importance,
8848
+ pinned: entry.pinned
8849
+ });
8850
+ } catch {}
8851
+ return {
8852
+ success: true,
8853
+ id: entry.id
8854
+ };
8855
+ });
8856
+ sdk.registerFunction({
8857
+ id: "mem::core-remove",
8858
+ description: "Remove a fact from core memory"
8859
+ }, async (data) => {
8860
+ if (!data?.id) return {
8861
+ success: false,
8862
+ error: "id is required"
8863
+ };
8864
+ await kv.delete(CORE_SCOPE, data.id);
8865
+ try {
8866
+ await recordAudit(kv, "core_remove", "mem::core-remove", [data.id], {});
8867
+ } catch {}
8868
+ return { success: true };
8869
+ });
8870
+ sdk.registerFunction({
8871
+ id: "mem::core-list",
8872
+ description: "List all core memory entries"
8873
+ }, async () => {
8874
+ const entries = await kv.list(CORE_SCOPE);
8875
+ entries.sort((a, b) => b.importance - a.importance);
8876
+ return {
8877
+ success: true,
8878
+ entries,
8879
+ totalTokens: entries.reduce((sum, e) => sum + estimateTokens(e.content), 0)
8880
+ };
8881
+ });
8882
+ sdk.registerFunction({
8883
+ id: "mem::working-context",
8884
+ description: "Build working context: core memory (pinned) + paged archival (by relevance/recency)"
8885
+ }, async (data) => {
8886
+ const ctx = getContext();
8887
+ const budget = data.budget || tokenBudget;
8888
+ const now = Date.now();
8889
+ let usedTokens = 0;
8890
+ const coreEntries = await kv.list(CORE_SCOPE);
8891
+ const pinned = coreEntries.filter((e) => e.pinned);
8892
+ const unpinned = coreEntries.filter((e) => !e.pinned).sort((a, b) => scoreEntry(b, now) - scoreEntry(a, now));
8893
+ const coreLines = [];
8894
+ const coreBudget = Math.floor(budget * .3);
8895
+ const accessUpdates = [];
8896
+ const accessTimestamp = (/* @__PURE__ */ new Date()).toISOString();
8897
+ for (const entry of [...pinned, ...unpinned]) {
8898
+ const tokens = estimateTokens(entry.content);
8899
+ if (usedTokens + tokens > coreBudget && !entry.pinned) continue;
8900
+ coreLines.push(`- ${entry.content}`);
8901
+ usedTokens += tokens;
8902
+ entry.accessCount++;
8903
+ entry.lastAccessedAt = accessTimestamp;
8904
+ accessUpdates.push({
8905
+ id: entry.id,
8906
+ entry
8907
+ });
8908
+ }
8909
+ Promise.allSettled(accessUpdates.map(({ id, entry }) => kv.set(CORE_SCOPE, id, entry))).catch(() => {});
8910
+ const archivalLines = [];
8911
+ const active = (await kv.list(KV.memories)).filter((m) => m.isLatest !== false).sort((a, b) => {
8912
+ const strengthDiff = b.strength - a.strength;
8913
+ if (Math.abs(strengthDiff) > .2) return strengthDiff;
8914
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
8915
+ });
8916
+ for (const mem of active) {
8917
+ const tokens = estimateTokens(mem.content);
8918
+ if (usedTokens + tokens > budget) continue;
8919
+ archivalLines.push(`- [${mem.type}] ${mem.title}: ${mem.content}`);
8920
+ usedTokens += tokens;
8921
+ }
8922
+ const pagedOut = active.length - archivalLines.length;
8923
+ const sections = [];
8924
+ if (coreLines.length > 0) sections.push(`## Core Memory\n${coreLines.join("\n")}`);
8925
+ if (archivalLines.length > 0) sections.push(`## Archival Memory\n${archivalLines.join("\n")}`);
8926
+ if (pagedOut > 0) sections.push(`_${pagedOut} memories paged to archival (use mem::search to retrieve)_`);
8927
+ const context = sections.join("\n\n");
8928
+ ctx.logger.info("Working context built", {
8929
+ coreEntries: coreLines.length,
8930
+ archivalEntries: archivalLines.length,
8931
+ pagedOut,
8932
+ tokens: usedTokens,
8933
+ budget
8934
+ });
8935
+ return {
8936
+ success: true,
8937
+ context,
8938
+ coreEntries: coreLines.length,
8939
+ archivalEntries: archivalLines.length,
8940
+ pagedOut,
8941
+ tokens: usedTokens,
8942
+ budget
8943
+ };
8944
+ });
8945
+ sdk.registerFunction({
8946
+ id: "mem::auto-page",
8947
+ description: "Automatically page low-value core memory entries to archival when over budget"
8948
+ }, async (data) => {
8949
+ const budget = data?.budget || tokenBudget;
8950
+ const coreBudget = Math.floor(budget * .3);
8951
+ const entries = await kv.list(CORE_SCOPE);
8952
+ let totalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
8953
+ if (totalTokens <= coreBudget) return {
8954
+ success: true,
8955
+ paged: 0,
8956
+ totalTokens,
8957
+ budget: coreBudget
8958
+ };
8959
+ const now = Date.now();
8960
+ const unpinned = entries.filter((e) => !e.pinned).sort((a, b) => scoreEntry(a, now) - scoreEntry(b, now));
8961
+ let paged = 0;
8962
+ const pagedIds = [];
8963
+ for (const entry of unpinned) {
8964
+ if (totalTokens <= coreBudget) break;
8965
+ const tokens = estimateTokens(entry.content);
8966
+ const archivalMemory = {
8967
+ id: generateId("mem"),
8968
+ createdAt: entry.createdAt,
8969
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8970
+ type: "fact",
8971
+ title: entry.content.slice(0, 80),
8972
+ content: entry.content,
8973
+ concepts: [],
8974
+ files: [],
8975
+ sessionIds: [],
8976
+ strength: entry.importance / 10,
8977
+ version: 1,
8978
+ isLatest: true
8979
+ };
8980
+ await kv.set(KV.memories, archivalMemory.id, archivalMemory);
8981
+ await kv.delete(CORE_SCOPE, entry.id);
8982
+ totalTokens -= tokens;
8983
+ paged++;
8984
+ pagedIds.push(entry.id);
8985
+ }
8986
+ if (paged > 0) try {
8987
+ await recordAudit(kv, "auto_page", "mem::auto-page", pagedIds, {
8988
+ paged,
8989
+ budget: coreBudget
8990
+ });
8991
+ } catch {}
8992
+ return {
8993
+ success: true,
8994
+ paged,
8995
+ totalTokens,
8996
+ budget: coreBudget
8997
+ };
8998
+ });
8999
+ }
9000
+
9001
+ //#endregion
9002
+ //#region src/functions/skill-extract.ts
9003
+ const SKILL_EXTRACT_SYSTEM = `You are a skill extraction engine. Given a completed multi-step task session, extract a reusable procedural skill document.
9004
+
9005
+ Output format:
9006
+ <skill>
9007
+ <trigger>When the agent encounters [specific situation/pattern]</trigger>
9008
+ <title>Short skill title</title>
9009
+ <steps>
9010
+ <step>First concrete action</step>
9011
+ <step>Second concrete action</step>
9012
+ </steps>
9013
+ <expected_outcome>What success looks like</expected_outcome>
9014
+ <tags>comma,separated,tags</tags>
9015
+ </skill>
9016
+
9017
+ Rules:
9018
+ - Extract ONLY if the session shows a clear multi-step procedure that succeeded
9019
+ - Steps must be concrete and actionable, not vague
9020
+ - The trigger should describe WHEN to apply this skill
9021
+ - If the session is exploratory with no clear procedure, output <no-skill/>
9022
+ - Maximum 10 steps per skill`;
9023
+ function buildSkillPrompt(summary, observations) {
9024
+ const obsText = observations.filter((o) => o.importance >= 4).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()).slice(0, 30).map((o) => `[${o.type}] ${o.title}${o.narrative ? ": " + o.narrative : ""}`).join("\n");
9025
+ return `## Session Summary
9026
+ Title: ${summary.title}
9027
+ Narrative: ${summary.narrative}
9028
+ Key Decisions: ${summary.keyDecisions.join("; ")}
9029
+ Files Modified: ${summary.filesModified.join(", ")}
9030
+ Concepts: ${summary.concepts.join(", ")}
9031
+
9032
+ ## Observations (${observations.length} total, showing top by importance)
9033
+ ${obsText}`;
9034
+ }
9035
+ function parseSkillXml(xml) {
9036
+ if (xml.includes("<no-skill/>")) return null;
9037
+ const triggerMatch = xml.match(/<trigger>([\s\S]*?)<\/trigger>/);
9038
+ const titleMatch = xml.match(/<title>([\s\S]*?)<\/title>/);
9039
+ const stepsMatch = xml.match(/<steps>([\s\S]*?)<\/steps>/);
9040
+ const outcomeMatch = xml.match(/<expected_outcome>([\s\S]*?)<\/expected_outcome>/);
9041
+ const tagsMatch = xml.match(/<tags>([\s\S]*?)<\/tags>/);
9042
+ if (!triggerMatch || !titleMatch || !stepsMatch) return null;
9043
+ const stepRegex = /<step>([\s\S]*?)<\/step>/g;
9044
+ const steps = [];
9045
+ let match;
9046
+ while ((match = stepRegex.exec(stepsMatch[1])) !== null) {
9047
+ const step = match[1].trim();
9048
+ if (step) steps.push(step);
9049
+ }
9050
+ if (steps.length < 2) return null;
9051
+ return {
9052
+ trigger: triggerMatch[1].trim(),
9053
+ title: titleMatch[1].trim(),
9054
+ steps,
9055
+ expectedOutcome: outcomeMatch?.[1]?.trim() || "",
9056
+ tags: tagsMatch?.[1]?.split(",").map((t) => t.trim()).filter(Boolean) || []
9057
+ };
9058
+ }
9059
+ function registerSkillExtractFunctions(sdk, kv, provider) {
9060
+ sdk.registerFunction({
9061
+ id: "mem::skill-extract",
9062
+ description: "Extract a reusable skill document from a completed session"
9063
+ }, async (data) => {
9064
+ const ctx = getContext();
9065
+ if (!data?.sessionId) return {
9066
+ success: false,
9067
+ error: "sessionId is required"
9068
+ };
9069
+ const session = await kv.get(KV.sessions, data.sessionId).catch(() => null);
9070
+ if (!session) return {
9071
+ success: false,
9072
+ error: "session not found"
9073
+ };
9074
+ if (session.status !== "completed") return {
9075
+ success: false,
9076
+ error: "session must be completed before skill extraction"
9077
+ };
9078
+ const [summary, observations] = await Promise.all([kv.get(KV.summaries, data.sessionId).catch(() => null), kv.list(KV.observations(data.sessionId)).catch(() => [])]);
9079
+ if (!summary) return {
9080
+ success: false,
9081
+ error: "no summary — run mem::summarize first"
9082
+ };
9083
+ if (observations.length < 3) return {
9084
+ success: false,
9085
+ error: "too few observations for skill extraction"
9086
+ };
9087
+ try {
9088
+ const prompt = buildSkillPrompt(summary, observations);
9089
+ const parsed = parseSkillXml(await provider.summarize(SKILL_EXTRACT_SYSTEM, prompt));
9090
+ if (!parsed) {
9091
+ ctx.logger.info("No skill extracted — session was exploratory", { sessionId: data.sessionId });
9092
+ return {
9093
+ success: true,
9094
+ extracted: false,
9095
+ reason: "no clear procedure found"
9096
+ };
9097
+ }
9098
+ const fp = fingerprintId("skill", JSON.stringify({
9099
+ title: parsed.title.toLowerCase(),
9100
+ trigger: parsed.trigger.toLowerCase(),
9101
+ steps: parsed.steps.map((s) => s.toLowerCase().trim())
9102
+ }));
9103
+ const existing = await kv.get(KV.procedural, fp).catch(() => null);
9104
+ if (existing) {
9105
+ if (!existing.sourceSessionIds.includes(data.sessionId)) {
9106
+ existing.strength = Math.min(1, existing.strength + .15);
9107
+ existing.frequency++;
9108
+ existing.sourceSessionIds = [...existing.sourceSessionIds, data.sessionId];
9109
+ }
9110
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9111
+ await kv.set(KV.procedural, existing.id, existing);
9112
+ try {
9113
+ await recordAudit(kv, "skill_extract", "mem::skill-extract", [], {
9114
+ skillId: existing.id,
9115
+ reinforced: true,
9116
+ sessionId: data.sessionId
9117
+ });
9118
+ } catch {}
9119
+ ctx.logger.info("Skill reinforced", {
9120
+ id: existing.id,
9121
+ name: parsed.title
9122
+ });
9123
+ return {
9124
+ success: true,
9125
+ extracted: true,
9126
+ reinforced: true,
9127
+ skill: existing
9128
+ };
9129
+ }
9130
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9131
+ const skill = {
9132
+ id: fp,
9133
+ name: parsed.title,
9134
+ triggerCondition: parsed.trigger,
9135
+ steps: parsed.steps,
9136
+ expectedOutcome: parsed.expectedOutcome,
9137
+ strength: .6,
9138
+ frequency: 1,
9139
+ tags: parsed.tags,
9140
+ concepts: summary.concepts,
9141
+ sourceSessionIds: [data.sessionId],
9142
+ sourceObservationIds: observations.slice(0, 10).map((o) => o.id),
9143
+ createdAt: now,
9144
+ updatedAt: now
9145
+ };
9146
+ await kv.set(KV.procedural, skill.id, skill);
9147
+ try {
9148
+ await recordAudit(kv, "skill_extract", "mem::skill-extract", [], {
9149
+ skillId: skill.id,
9150
+ title: parsed.title,
9151
+ steps: parsed.steps.length,
9152
+ sessionId: data.sessionId
9153
+ });
9154
+ } catch {}
9155
+ ctx.logger.info("Skill extracted", {
9156
+ id: skill.id,
9157
+ title: parsed.title,
9158
+ steps: parsed.steps.length
9159
+ });
9160
+ return {
9161
+ success: true,
9162
+ extracted: true,
9163
+ reinforced: false,
9164
+ skill
9165
+ };
9166
+ } catch (err) {
9167
+ const msg = err instanceof Error ? err.message : String(err);
9168
+ ctx.logger.error("Skill extraction failed", { error: msg });
9169
+ return {
9170
+ success: false,
9171
+ error: msg
9172
+ };
9173
+ }
9174
+ });
9175
+ sdk.registerFunction({
9176
+ id: "mem::skill-list",
9177
+ description: "List extracted procedural skills"
9178
+ }, async (data) => {
9179
+ const limit = data?.limit ?? 50;
9180
+ const sorted = (await kv.list(KV.procedural)).sort((a, b) => b.strength - a.strength);
9181
+ return {
9182
+ success: true,
9183
+ skills: sorted.slice(0, limit),
9184
+ total: sorted.length
9185
+ };
9186
+ });
9187
+ sdk.registerFunction({
9188
+ id: "mem::skill-match",
9189
+ description: "Find skills relevant to a given task description"
9190
+ }, async (data) => {
9191
+ if (!data?.query?.trim()) return {
9192
+ success: false,
9193
+ error: "query is required"
9194
+ };
9195
+ const limit = data.limit ?? 5;
9196
+ const terms = data.query.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
9197
+ const scored = (await kv.list(KV.procedural)).map((skill) => {
9198
+ const text = `${skill.name} ${skill.triggerCondition} ${(skill.tags || []).join(" ")} ${skill.steps.join(" ")}`.toLowerCase();
9199
+ const matchCount = terms.filter((t) => text.includes(t)).length;
9200
+ if (matchCount === 0) return null;
9201
+ return {
9202
+ skill,
9203
+ score: matchCount / terms.length * skill.strength
9204
+ };
9205
+ }).filter(Boolean);
9206
+ scored.sort((a, b) => b.score - a.score);
9207
+ return {
9208
+ success: true,
9209
+ matches: scored.slice(0, limit)
9210
+ };
9211
+ });
9212
+ }
9213
+
8746
9214
  //#endregion
8747
9215
  //#region src/functions/sliding-window.ts
8748
9216
  const SLIDING_WINDOW_SYSTEM = `You are a contextual enrichment engine. Given a primary observation and its surrounding context window (previous and next observations from the same session), produce an enriched version.
@@ -13895,6 +14363,10 @@ function startViewerServer(port, _kv, _sdk, secret, restPort) {
13895
14363
  json(res, 502, { error: "upstream error" }, req);
13896
14364
  }
13897
14365
  });
14366
+ server.on("error", (err) => {
14367
+ if (err.code === "EADDRINUSE") console.warn(`[agentmemory] Viewer port ${port} already in use, skipping viewer.`);
14368
+ else console.error(`[agentmemory] Viewer error:`, err.message);
14369
+ });
13898
14370
  server.listen(port, "127.0.0.1", () => {
13899
14371
  console.log(`[agentmemory] Viewer: http://localhost:${port}`);
13900
14372
  });
@@ -14153,6 +14625,8 @@ async function main() {
14153
14625
  registerLessonsFunctions(sdk, kv);
14154
14626
  registerObsidianExportFunction(sdk, kv);
14155
14627
  registerReflectFunctions(sdk, kv, provider);
14628
+ registerWorkingMemoryFunctions(sdk, kv, config.tokenBudget);
14629
+ registerSkillExtractFunctions(sdk, kv, provider);
14156
14630
  registerCascadeFunction(sdk, kv);
14157
14631
  registerSlidingWindowFunction(sdk, kv, provider);
14158
14632
  registerQueryExpansionFunction(sdk, provider);