@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 +3 -3
- package/dist/index.mjs +480 -10
- package/dist/index.mjs.map +1 -1
- package/dist/{src-Dflj3k63.mjs → src-C_TC9frp.mjs} +481 -11
- package/dist/src-C_TC9frp.mjs.map +1 -0
- package/dist/standalone.mjs +1 -1
- package/dist/standalone.mjs.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +7 -4
- package/dist/src-Dflj3k63.mjs.map +0 -1
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-
|
|
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-
|
|
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-
|
|
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
|
|
1303
|
-
|
|
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.
|
|
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);
|