@agentmemory/agentmemory 0.7.5 → 0.7.7

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/cli.mjs CHANGED
@@ -160,12 +160,12 @@ async function main() {
160
160
  p.intro("agentmemory");
161
161
  if (skipEngine) {
162
162
  p.log.info("Skipping engine check (--no-engine)");
163
- await import("./src-Dflj3k63.mjs");
163
+ await import("./src-C_TC9frp.mjs");
164
164
  return;
165
165
  }
166
166
  if (await isEngineRunning()) {
167
167
  p.log.success("iii-engine is running");
168
- await import("./src-Dflj3k63.mjs");
168
+ await import("./src-C_TC9frp.mjs");
169
169
  return;
170
170
  }
171
171
  if (!await startEngine()) {
@@ -193,7 +193,7 @@ async function main() {
193
193
  process.exit(1);
194
194
  }
195
195
  s.stop("iii-engine is ready");
196
- await import("./src-Dflj3k63.mjs");
196
+ await import("./src-C_TC9frp.mjs");
197
197
  }
198
198
  main().catch((err) => {
199
199
  p.log.error(err instanceof Error ? err.message : String(err));
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.5";
3808
+ const VERSION = "0.7.7";
3744
3809
 
3745
3810
  //#endregion
3746
3811
  //#region src/functions/export-import.ts
@@ -3842,7 +3907,9 @@ function registerExportImportFunction(sdk, kv) {
3842
3907
  "0.7.2",
3843
3908
  "0.7.3",
3844
3909
  "0.7.4",
3845
- "0.7.5"
3910
+ "0.7.5",
3911
+ "0.7.6",
3912
+ "0.7.7"
3846
3913
  ]).has(importData.version)) return {
3847
3914
  success: false,
3848
3915
  error: `Unsupported export version: ${importData.version}`
@@ -8744,6 +8811,407 @@ function registerReflectFunctions(sdk, kv, provider) {
8744
8811
  });
8745
8812
  }
8746
8813
 
8814
+ //#endregion
8815
+ //#region src/functions/working-memory.ts
8816
+ const CORE_SCOPE = "mem:core-memory";
8817
+ function estimateTokens(text) {
8818
+ return Math.ceil(text.length / 3);
8819
+ }
8820
+ function scoreEntry(entry, now) {
8821
+ const recencyScore = 1 / (1 + (now - new Date(entry.lastAccessedAt).getTime()) / (1e3 * 60 * 60 * 24) * .1);
8822
+ const accessScore = Math.log2(entry.accessCount + 1) / 10;
8823
+ return entry.importance / 10 * .5 + recencyScore * .3 + accessScore * .2;
8824
+ }
8825
+ function registerWorkingMemoryFunctions(sdk, kv, tokenBudget) {
8826
+ sdk.registerFunction({
8827
+ id: "mem::core-add",
8828
+ description: "Add a fact to core memory (always included in context)"
8829
+ }, async (data) => {
8830
+ if (!data?.content?.trim()) return {
8831
+ success: false,
8832
+ error: "content is required"
8833
+ };
8834
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8835
+ const entry = {
8836
+ id: generateId("core"),
8837
+ content: data.content.trim(),
8838
+ importance: Math.min(10, Math.max(1, data.importance ?? 7)),
8839
+ pinned: data.pinned ?? false,
8840
+ accessCount: 0,
8841
+ lastAccessedAt: now,
8842
+ createdAt: now
8843
+ };
8844
+ await kv.set(CORE_SCOPE, entry.id, entry);
8845
+ try {
8846
+ await recordAudit(kv, "core_add", "mem::core-add", [entry.id], {
8847
+ content: entry.content.slice(0, 100),
8848
+ importance: entry.importance,
8849
+ pinned: entry.pinned
8850
+ });
8851
+ } catch {}
8852
+ return {
8853
+ success: true,
8854
+ id: entry.id
8855
+ };
8856
+ });
8857
+ sdk.registerFunction({
8858
+ id: "mem::core-remove",
8859
+ description: "Remove a fact from core memory"
8860
+ }, async (data) => {
8861
+ if (!data?.id) return {
8862
+ success: false,
8863
+ error: "id is required"
8864
+ };
8865
+ await kv.delete(CORE_SCOPE, data.id);
8866
+ try {
8867
+ await recordAudit(kv, "core_remove", "mem::core-remove", [data.id], {});
8868
+ } catch {}
8869
+ return { success: true };
8870
+ });
8871
+ sdk.registerFunction({
8872
+ id: "mem::core-list",
8873
+ description: "List all core memory entries"
8874
+ }, async () => {
8875
+ const entries = await kv.list(CORE_SCOPE);
8876
+ entries.sort((a, b) => b.importance - a.importance);
8877
+ return {
8878
+ success: true,
8879
+ entries,
8880
+ totalTokens: entries.reduce((sum, e) => sum + estimateTokens(e.content), 0)
8881
+ };
8882
+ });
8883
+ sdk.registerFunction({
8884
+ id: "mem::working-context",
8885
+ description: "Build working context: core memory (pinned) + paged archival (by relevance/recency)"
8886
+ }, async (data) => {
8887
+ const ctx = getContext();
8888
+ const budget = data.budget || tokenBudget;
8889
+ const now = Date.now();
8890
+ let usedTokens = 0;
8891
+ const coreEntries = await kv.list(CORE_SCOPE);
8892
+ const pinned = coreEntries.filter((e) => e.pinned);
8893
+ const unpinned = coreEntries.filter((e) => !e.pinned).sort((a, b) => scoreEntry(b, now) - scoreEntry(a, now));
8894
+ const coreLines = [];
8895
+ const coreBudget = Math.floor(budget * .3);
8896
+ const accessUpdates = [];
8897
+ const accessTimestamp = (/* @__PURE__ */ new Date()).toISOString();
8898
+ for (const entry of [...pinned, ...unpinned]) {
8899
+ const tokens = estimateTokens(entry.content);
8900
+ if (usedTokens + tokens > coreBudget && !entry.pinned) continue;
8901
+ coreLines.push(`- ${entry.content}`);
8902
+ usedTokens += tokens;
8903
+ entry.accessCount++;
8904
+ entry.lastAccessedAt = accessTimestamp;
8905
+ accessUpdates.push({
8906
+ id: entry.id,
8907
+ entry
8908
+ });
8909
+ }
8910
+ Promise.allSettled(accessUpdates.map(({ id, entry }) => kv.set(CORE_SCOPE, id, entry))).catch(() => {});
8911
+ const archivalLines = [];
8912
+ const active = (await kv.list(KV.memories)).filter((m) => m.isLatest !== false).sort((a, b) => {
8913
+ const strengthDiff = b.strength - a.strength;
8914
+ if (Math.abs(strengthDiff) > .2) return strengthDiff;
8915
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
8916
+ });
8917
+ for (const mem of active) {
8918
+ const tokens = estimateTokens(mem.content);
8919
+ if (usedTokens + tokens > budget) continue;
8920
+ archivalLines.push(`- [${mem.type}] ${mem.title}: ${mem.content}`);
8921
+ usedTokens += tokens;
8922
+ }
8923
+ const pagedOut = active.length - archivalLines.length;
8924
+ const sections = [];
8925
+ if (coreLines.length > 0) sections.push(`## Core Memory\n${coreLines.join("\n")}`);
8926
+ if (archivalLines.length > 0) sections.push(`## Archival Memory\n${archivalLines.join("\n")}`);
8927
+ if (pagedOut > 0) sections.push(`_${pagedOut} memories paged to archival (use mem::search to retrieve)_`);
8928
+ const context = sections.join("\n\n");
8929
+ ctx.logger.info("Working context built", {
8930
+ coreEntries: coreLines.length,
8931
+ archivalEntries: archivalLines.length,
8932
+ pagedOut,
8933
+ tokens: usedTokens,
8934
+ budget
8935
+ });
8936
+ return {
8937
+ success: true,
8938
+ context,
8939
+ coreEntries: coreLines.length,
8940
+ archivalEntries: archivalLines.length,
8941
+ pagedOut,
8942
+ tokens: usedTokens,
8943
+ budget
8944
+ };
8945
+ });
8946
+ sdk.registerFunction({
8947
+ id: "mem::auto-page",
8948
+ description: "Automatically page low-value core memory entries to archival when over budget"
8949
+ }, async (data) => {
8950
+ const budget = data?.budget || tokenBudget;
8951
+ const coreBudget = Math.floor(budget * .3);
8952
+ const entries = await kv.list(CORE_SCOPE);
8953
+ let totalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
8954
+ if (totalTokens <= coreBudget) return {
8955
+ success: true,
8956
+ paged: 0,
8957
+ totalTokens,
8958
+ budget: coreBudget
8959
+ };
8960
+ const now = Date.now();
8961
+ const unpinned = entries.filter((e) => !e.pinned).sort((a, b) => scoreEntry(a, now) - scoreEntry(b, now));
8962
+ let paged = 0;
8963
+ const pagedIds = [];
8964
+ for (const entry of unpinned) {
8965
+ if (totalTokens <= coreBudget) break;
8966
+ const tokens = estimateTokens(entry.content);
8967
+ const archivalMemory = {
8968
+ id: generateId("mem"),
8969
+ createdAt: entry.createdAt,
8970
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8971
+ type: "fact",
8972
+ title: entry.content.slice(0, 80),
8973
+ content: entry.content,
8974
+ concepts: [],
8975
+ files: [],
8976
+ sessionIds: [],
8977
+ strength: entry.importance / 10,
8978
+ version: 1,
8979
+ isLatest: true
8980
+ };
8981
+ await kv.set(KV.memories, archivalMemory.id, archivalMemory);
8982
+ await kv.delete(CORE_SCOPE, entry.id);
8983
+ totalTokens -= tokens;
8984
+ paged++;
8985
+ pagedIds.push(entry.id);
8986
+ }
8987
+ if (paged > 0) try {
8988
+ await recordAudit(kv, "auto_page", "mem::auto-page", pagedIds, {
8989
+ paged,
8990
+ budget: coreBudget
8991
+ });
8992
+ } catch {}
8993
+ return {
8994
+ success: true,
8995
+ paged,
8996
+ totalTokens,
8997
+ budget: coreBudget
8998
+ };
8999
+ });
9000
+ }
9001
+
9002
+ //#endregion
9003
+ //#region src/functions/skill-extract.ts
9004
+ const SKILL_EXTRACT_SYSTEM = `You are a skill extraction engine. Given a completed multi-step task session, extract a reusable procedural skill document.
9005
+
9006
+ Output format:
9007
+ <skill>
9008
+ <trigger>When the agent encounters [specific situation/pattern]</trigger>
9009
+ <title>Short skill title</title>
9010
+ <steps>
9011
+ <step>First concrete action</step>
9012
+ <step>Second concrete action</step>
9013
+ </steps>
9014
+ <expected_outcome>What success looks like</expected_outcome>
9015
+ <tags>comma,separated,tags</tags>
9016
+ </skill>
9017
+
9018
+ Rules:
9019
+ - Extract ONLY if the session shows a clear multi-step procedure that succeeded
9020
+ - Steps must be concrete and actionable, not vague
9021
+ - The trigger should describe WHEN to apply this skill
9022
+ - If the session is exploratory with no clear procedure, output <no-skill/>
9023
+ - Maximum 10 steps per skill`;
9024
+ function buildSkillPrompt(summary, observations) {
9025
+ 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");
9026
+ return `## Session Summary
9027
+ Title: ${summary.title}
9028
+ Narrative: ${summary.narrative}
9029
+ Key Decisions: ${summary.keyDecisions.join("; ")}
9030
+ Files Modified: ${summary.filesModified.join(", ")}
9031
+ Concepts: ${summary.concepts.join(", ")}
9032
+
9033
+ ## Observations (${observations.length} total, showing top by importance)
9034
+ ${obsText}`;
9035
+ }
9036
+ function parseSkillXml(xml) {
9037
+ if (xml.includes("<no-skill/>")) return null;
9038
+ const triggerMatch = xml.match(/<trigger>([\s\S]*?)<\/trigger>/);
9039
+ const titleMatch = xml.match(/<title>([\s\S]*?)<\/title>/);
9040
+ const stepsMatch = xml.match(/<steps>([\s\S]*?)<\/steps>/);
9041
+ const outcomeMatch = xml.match(/<expected_outcome>([\s\S]*?)<\/expected_outcome>/);
9042
+ const tagsMatch = xml.match(/<tags>([\s\S]*?)<\/tags>/);
9043
+ if (!triggerMatch || !titleMatch || !stepsMatch) return null;
9044
+ const stepRegex = /<step>([\s\S]*?)<\/step>/g;
9045
+ const steps = [];
9046
+ let match;
9047
+ while ((match = stepRegex.exec(stepsMatch[1])) !== null) {
9048
+ const step = match[1].trim();
9049
+ if (step) steps.push(step);
9050
+ }
9051
+ if (steps.length < 2) return null;
9052
+ return {
9053
+ trigger: triggerMatch[1].trim(),
9054
+ title: titleMatch[1].trim(),
9055
+ steps,
9056
+ expectedOutcome: outcomeMatch?.[1]?.trim() || "",
9057
+ tags: tagsMatch?.[1]?.split(",").map((t) => t.trim()).filter(Boolean) || []
9058
+ };
9059
+ }
9060
+ function registerSkillExtractFunctions(sdk, kv, provider) {
9061
+ sdk.registerFunction({
9062
+ id: "mem::skill-extract",
9063
+ description: "Extract a reusable skill document from a completed session"
9064
+ }, async (data) => {
9065
+ const ctx = getContext();
9066
+ if (!data?.sessionId) return {
9067
+ success: false,
9068
+ error: "sessionId is required"
9069
+ };
9070
+ const session = await kv.get(KV.sessions, data.sessionId).catch(() => null);
9071
+ if (!session) return {
9072
+ success: false,
9073
+ error: "session not found"
9074
+ };
9075
+ if (session.status !== "completed") return {
9076
+ success: false,
9077
+ error: "session must be completed before skill extraction"
9078
+ };
9079
+ const [summary, observations] = await Promise.all([kv.get(KV.summaries, data.sessionId).catch(() => null), kv.list(KV.observations(data.sessionId)).catch(() => [])]);
9080
+ if (!summary) return {
9081
+ success: false,
9082
+ error: "no summary — run mem::summarize first"
9083
+ };
9084
+ if (observations.length < 3) return {
9085
+ success: false,
9086
+ error: "too few observations for skill extraction"
9087
+ };
9088
+ try {
9089
+ const prompt = buildSkillPrompt(summary, observations);
9090
+ const parsed = parseSkillXml(await provider.summarize(SKILL_EXTRACT_SYSTEM, prompt));
9091
+ if (!parsed) {
9092
+ ctx.logger.info("No skill extracted — session was exploratory", { sessionId: data.sessionId });
9093
+ return {
9094
+ success: true,
9095
+ extracted: false,
9096
+ reason: "no clear procedure found"
9097
+ };
9098
+ }
9099
+ const fp = fingerprintId("skill", JSON.stringify({
9100
+ title: parsed.title.toLowerCase(),
9101
+ trigger: parsed.trigger.toLowerCase(),
9102
+ steps: parsed.steps.map((s) => s.toLowerCase().trim())
9103
+ }));
9104
+ const existing = await kv.get(KV.procedural, fp).catch(() => null);
9105
+ if (existing) {
9106
+ if (!existing.sourceSessionIds.includes(data.sessionId)) {
9107
+ existing.strength = Math.min(1, existing.strength + .15);
9108
+ existing.frequency++;
9109
+ existing.sourceSessionIds = [...existing.sourceSessionIds, data.sessionId];
9110
+ }
9111
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9112
+ await kv.set(KV.procedural, existing.id, existing);
9113
+ try {
9114
+ await recordAudit(kv, "skill_extract", "mem::skill-extract", [], {
9115
+ skillId: existing.id,
9116
+ reinforced: true,
9117
+ sessionId: data.sessionId
9118
+ });
9119
+ } catch {}
9120
+ ctx.logger.info("Skill reinforced", {
9121
+ id: existing.id,
9122
+ name: parsed.title
9123
+ });
9124
+ return {
9125
+ success: true,
9126
+ extracted: true,
9127
+ reinforced: true,
9128
+ skill: existing
9129
+ };
9130
+ }
9131
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9132
+ const skill = {
9133
+ id: fp,
9134
+ name: parsed.title,
9135
+ triggerCondition: parsed.trigger,
9136
+ steps: parsed.steps,
9137
+ expectedOutcome: parsed.expectedOutcome,
9138
+ strength: .6,
9139
+ frequency: 1,
9140
+ tags: parsed.tags,
9141
+ concepts: summary.concepts,
9142
+ sourceSessionIds: [data.sessionId],
9143
+ sourceObservationIds: observations.slice(0, 10).map((o) => o.id),
9144
+ createdAt: now,
9145
+ updatedAt: now
9146
+ };
9147
+ await kv.set(KV.procedural, skill.id, skill);
9148
+ try {
9149
+ await recordAudit(kv, "skill_extract", "mem::skill-extract", [], {
9150
+ skillId: skill.id,
9151
+ title: parsed.title,
9152
+ steps: parsed.steps.length,
9153
+ sessionId: data.sessionId
9154
+ });
9155
+ } catch {}
9156
+ ctx.logger.info("Skill extracted", {
9157
+ id: skill.id,
9158
+ title: parsed.title,
9159
+ steps: parsed.steps.length
9160
+ });
9161
+ return {
9162
+ success: true,
9163
+ extracted: true,
9164
+ reinforced: false,
9165
+ skill
9166
+ };
9167
+ } catch (err) {
9168
+ const msg = err instanceof Error ? err.message : String(err);
9169
+ ctx.logger.error("Skill extraction failed", { error: msg });
9170
+ return {
9171
+ success: false,
9172
+ error: msg
9173
+ };
9174
+ }
9175
+ });
9176
+ sdk.registerFunction({
9177
+ id: "mem::skill-list",
9178
+ description: "List extracted procedural skills"
9179
+ }, async (data) => {
9180
+ const limit = data?.limit ?? 50;
9181
+ const sorted = (await kv.list(KV.procedural)).sort((a, b) => b.strength - a.strength);
9182
+ return {
9183
+ success: true,
9184
+ skills: sorted.slice(0, limit),
9185
+ total: sorted.length
9186
+ };
9187
+ });
9188
+ sdk.registerFunction({
9189
+ id: "mem::skill-match",
9190
+ description: "Find skills relevant to a given task description"
9191
+ }, async (data) => {
9192
+ if (!data?.query?.trim()) return {
9193
+ success: false,
9194
+ error: "query is required"
9195
+ };
9196
+ const limit = data.limit ?? 5;
9197
+ const terms = data.query.toLowerCase().split(/\s+/).filter((t) => t.length > 2);
9198
+ const scored = (await kv.list(KV.procedural)).map((skill) => {
9199
+ const text = `${skill.name} ${skill.triggerCondition} ${(skill.tags || []).join(" ")} ${skill.steps.join(" ")}`.toLowerCase();
9200
+ const matchCount = terms.filter((t) => text.includes(t)).length;
9201
+ if (matchCount === 0) return null;
9202
+ return {
9203
+ skill,
9204
+ score: matchCount / terms.length * skill.strength
9205
+ };
9206
+ }).filter(Boolean);
9207
+ scored.sort((a, b) => b.score - a.score);
9208
+ return {
9209
+ success: true,
9210
+ matches: scored.slice(0, limit)
9211
+ };
9212
+ });
9213
+ }
9214
+
8747
9215
  //#endregion
8748
9216
  //#region src/functions/sliding-window.ts
8749
9217
  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.
@@ -14158,6 +14626,8 @@ async function main() {
14158
14626
  registerLessonsFunctions(sdk, kv);
14159
14627
  registerObsidianExportFunction(sdk, kv);
14160
14628
  registerReflectFunctions(sdk, kv, provider);
14629
+ registerWorkingMemoryFunctions(sdk, kv, config.tokenBudget);
14630
+ registerSkillExtractFunctions(sdk, kv, provider);
14161
14631
  registerCascadeFunction(sdk, kv);
14162
14632
  registerSlidingWindowFunction(sdk, kv, provider);
14163
14633
  registerQueryExpansionFunction(sdk, provider);