@agentmemory/agentmemory 0.8.6 → 0.8.8
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/README.md +5 -9
- package/dist/cli.mjs +4 -4
- package/dist/index.mjs +281 -157
- package/dist/index.mjs.map +1 -1
- package/dist/{src-DjVaT_sI.mjs → src-CneY0pgf.mjs} +279 -158
- package/dist/src-CneY0pgf.mjs.map +1 -0
- package/dist/{standalone-C7tRDaiu.mjs → standalone-Qmvspmgi.mjs} +2 -2
- package/dist/{standalone-C7tRDaiu.mjs.map → standalone-Qmvspmgi.mjs.map} +1 -1
- package/dist/standalone.mjs +1 -1
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-BYzapMJi.mjs → tools-registry-BuDo4gKj.mjs} +6 -3
- package/dist/tools-registry-BuDo4gKj.mjs.map +1 -0
- package/iii-config.docker.yaml +51 -0
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/src-DjVaT_sI.mjs.map +0 -1
- package/dist/tools-registry-BYzapMJi.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -181,15 +181,6 @@ npx @agentmemory/agentmemory
|
|
|
181
181
|
</tr>
|
|
182
182
|
</table>
|
|
183
183
|
|
|
184
|
-
<p align="center">
|
|
185
|
-
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-recall.svg"><img src="assets/tags/stat-recall.svg" alt="95.2% retrieval R@5" height="48" /></picture>
|
|
186
|
-
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-tokens.svg"><img src="assets/tags/stat-tokens.svg" alt="92% fewer tokens" height="48" /></picture>
|
|
187
|
-
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-tools.svg"><img src="assets/tags/stat-tools.svg" alt="43 MCP tools" height="48" /></picture>
|
|
188
|
-
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-hooks.svg"><img src="assets/tags/stat-hooks.svg" alt="12 auto hooks" height="48" /></picture>
|
|
189
|
-
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-deps.svg"><img src="assets/tags/stat-deps.svg" alt="0 external DBs" height="48" /></picture>
|
|
190
|
-
<picture><source media="(prefers-color-scheme: dark)" srcset="assets/tags/light/stat-tests.svg"><img src="assets/tags/stat-tests.svg" alt="654 tests passing" height="48" /></picture>
|
|
191
|
-
</p>
|
|
192
|
-
|
|
193
184
|
> Embedding model: `all-MiniLM-L6-v2` (local, free, no API key). Full reports: [`benchmark/LONGMEMEVAL.md`](benchmark/LONGMEMEVAL.md), [`benchmark/QUALITY.md`](benchmark/QUALITY.md), [`benchmark/SCALE.md`](benchmark/SCALE.md). Competitor comparison: [`benchmark/COMPARISON.md`](benchmark/COMPARISON.md) — agentmemory vs mem0, Letta, Khoj, claude-mem, Hippo.
|
|
194
185
|
|
|
195
186
|
---
|
|
@@ -727,6 +718,11 @@ Create `~/.agentmemory/.env`:
|
|
|
727
718
|
# III_REST_PORT=3111
|
|
728
719
|
|
|
729
720
|
# Features
|
|
721
|
+
# AGENTMEMORY_AUTO_COMPRESS=false # OFF by default (#138). When on,
|
|
722
|
+
# every PostToolUse hook calls your
|
|
723
|
+
# LLM provider to compress the
|
|
724
|
+
# observation — expect significant
|
|
725
|
+
# token spend on active sessions.
|
|
730
726
|
# GRAPH_EXTRACTION_ENABLED=false
|
|
731
727
|
# CONSOLIDATION_ENABLED=true
|
|
732
728
|
# LESSON_DECAY_ENABLED=true
|
package/dist/cli.mjs
CHANGED
|
@@ -284,12 +284,12 @@ async function main() {
|
|
|
284
284
|
p.intro("agentmemory");
|
|
285
285
|
if (skipEngine) {
|
|
286
286
|
p.log.info("Skipping engine check (--no-engine)");
|
|
287
|
-
await import("./src-
|
|
287
|
+
await import("./src-CneY0pgf.mjs");
|
|
288
288
|
return;
|
|
289
289
|
}
|
|
290
290
|
if (await isEngineRunning()) {
|
|
291
291
|
p.log.success("iii-engine is running");
|
|
292
|
-
await import("./src-
|
|
292
|
+
await import("./src-CneY0pgf.mjs");
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
295
|
if (!await startEngine()) {
|
|
@@ -333,7 +333,7 @@ async function main() {
|
|
|
333
333
|
process.exit(1);
|
|
334
334
|
}
|
|
335
335
|
s.stop("iii-engine is ready");
|
|
336
|
-
await import("./src-
|
|
336
|
+
await import("./src-CneY0pgf.mjs");
|
|
337
337
|
}
|
|
338
338
|
async function runStatus() {
|
|
339
339
|
const port = getRestPort();
|
|
@@ -557,7 +557,7 @@ async function runDemo() {
|
|
|
557
557
|
p.log.success("agentmemory is working. Point your agent at it and get back to coding.");
|
|
558
558
|
}
|
|
559
559
|
async function runMcp() {
|
|
560
|
-
await import("./standalone-
|
|
560
|
+
await import("./standalone-Qmvspmgi.mjs");
|
|
561
561
|
}
|
|
562
562
|
({
|
|
563
563
|
status: runStatus,
|
package/dist/index.mjs
CHANGED
|
@@ -156,6 +156,9 @@ function isGraphExtractionEnabled() {
|
|
|
156
156
|
function isConsolidationEnabled() {
|
|
157
157
|
return getMergedEnv()["CONSOLIDATION_ENABLED"] === "true";
|
|
158
158
|
}
|
|
159
|
+
function isAutoCompressEnabled() {
|
|
160
|
+
return getMergedEnv()["AGENTMEMORY_AUTO_COMPRESS"] === "true";
|
|
161
|
+
}
|
|
159
162
|
function getConsolidationDecayDays() {
|
|
160
163
|
return safeParseInt(getMergedEnv()["CONSOLIDATION_DECAY_DAYS"], 30);
|
|
161
164
|
}
|
|
@@ -1997,164 +2000,95 @@ function withKeyedLock(key, fn) {
|
|
|
1997
2000
|
}
|
|
1998
2001
|
|
|
1999
2002
|
//#endregion
|
|
2000
|
-
//#region src/functions/
|
|
2001
|
-
function
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
}
|
|
2062
|
-
});
|
|
2063
|
-
sdk.triggerVoid("stream::set", {
|
|
2064
|
-
stream_name: STREAM.name,
|
|
2065
|
-
group_id: STREAM.viewerGroup,
|
|
2066
|
-
item_id: obsId,
|
|
2067
|
-
data: {
|
|
2068
|
-
type: "raw",
|
|
2069
|
-
observation: raw,
|
|
2070
|
-
sessionId: payload.sessionId
|
|
2071
|
-
}
|
|
2072
|
-
});
|
|
2073
|
-
const session = await kv.get(KV.sessions, payload.sessionId);
|
|
2074
|
-
if (session) await kv.set(KV.sessions, payload.sessionId, {
|
|
2075
|
-
...session,
|
|
2076
|
-
observationCount: (session.observationCount || 0) + 1
|
|
2077
|
-
});
|
|
2078
|
-
sdk.triggerVoid("mem::compress", {
|
|
2079
|
-
observationId: obsId,
|
|
2080
|
-
sessionId: payload.sessionId,
|
|
2081
|
-
raw
|
|
2082
|
-
});
|
|
2083
|
-
ctx.logger.info("Observation captured", {
|
|
2084
|
-
obsId,
|
|
2085
|
-
sessionId: payload.sessionId,
|
|
2086
|
-
hook: payload.hookType
|
|
2087
|
-
});
|
|
2088
|
-
return { observationId: obsId };
|
|
2089
|
-
});
|
|
2090
|
-
});
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
//#endregion
|
|
2094
|
-
//#region src/prompts/compression.ts
|
|
2095
|
-
const COMPRESSION_SYSTEM = `You are a memory compression engine for an AI coding agent. Your job is to extract the essential information from a tool usage observation and compress it into structured data.
|
|
2096
|
-
|
|
2097
|
-
Output EXACTLY this XML format with no additional text:
|
|
2098
|
-
|
|
2099
|
-
<observation>
|
|
2100
|
-
<type>one of: file_read, file_write, file_edit, command_run, search, web_fetch, conversation, error, decision, discovery, subagent, notification, task, other</type>
|
|
2101
|
-
<title>Short descriptive title (max 80 chars)</title>
|
|
2102
|
-
<subtitle>One-line context (optional)</subtitle>
|
|
2103
|
-
<facts>
|
|
2104
|
-
<fact>Specific factual detail 1</fact>
|
|
2105
|
-
<fact>Specific factual detail 2</fact>
|
|
2106
|
-
</facts>
|
|
2107
|
-
<narrative>2-3 sentence summary of what happened and why it matters</narrative>
|
|
2108
|
-
<concepts>
|
|
2109
|
-
<concept>technical concept or pattern</concept>
|
|
2110
|
-
</concepts>
|
|
2111
|
-
<files>
|
|
2112
|
-
<file>path/to/file</file>
|
|
2113
|
-
</files>
|
|
2114
|
-
<importance>1-10 scale, 10 being critical architectural decision</importance>
|
|
2115
|
-
</observation>
|
|
2116
|
-
|
|
2117
|
-
Rules:
|
|
2118
|
-
- Be concise but preserve ALL technically relevant details
|
|
2119
|
-
- File paths must be exact
|
|
2120
|
-
- Importance: 1-3 for routine reads, 4-6 for edits/commands, 7-9 for architectural decisions, 10 for breaking changes
|
|
2121
|
-
- Concepts should be reusable search terms (e.g., "React hooks", "SQL migration", "auth middleware")
|
|
2122
|
-
- Strip any secrets, tokens, or credentials from the output`;
|
|
2123
|
-
function buildCompressionPrompt(observation) {
|
|
2124
|
-
const parts = [`Timestamp: ${observation.timestamp}`, `Hook: ${observation.hookType}`];
|
|
2125
|
-
if (observation.toolName) parts.push(`Tool: ${observation.toolName}`);
|
|
2126
|
-
if (observation.toolInput) {
|
|
2127
|
-
const input = typeof observation.toolInput === "string" ? observation.toolInput : JSON.stringify(observation.toolInput, null, 2);
|
|
2128
|
-
parts.push(`Input:\n${truncate(input, 4e3)}`);
|
|
2129
|
-
}
|
|
2130
|
-
if (observation.toolOutput) {
|
|
2131
|
-
const output = typeof observation.toolOutput === "string" ? observation.toolOutput : JSON.stringify(observation.toolOutput, null, 2);
|
|
2132
|
-
parts.push(`Output:\n${truncate(output, 4e3)}`);
|
|
2003
|
+
//#region src/functions/compress-synthetic.ts
|
|
2004
|
+
function inferType(toolName, hookType) {
|
|
2005
|
+
if (hookType === "post_tool_failure") return "error";
|
|
2006
|
+
if (hookType === "prompt_submit") return "conversation";
|
|
2007
|
+
if (hookType === "subagent_stop" || hookType === "task_completed") return "subagent";
|
|
2008
|
+
if (hookType === "notification") return "notification";
|
|
2009
|
+
if (!toolName) return "other";
|
|
2010
|
+
const n = toolName.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
2011
|
+
const hasWord = (word) => new RegExp(`(^|_)${word}(_|$)`).test(n) || n === word || n.endsWith(word) || n.startsWith(word);
|
|
2012
|
+
if ([
|
|
2013
|
+
"fetch",
|
|
2014
|
+
"http",
|
|
2015
|
+
"web"
|
|
2016
|
+
].some(hasWord)) return "web_fetch";
|
|
2017
|
+
if ([
|
|
2018
|
+
"grep",
|
|
2019
|
+
"search",
|
|
2020
|
+
"glob",
|
|
2021
|
+
"find"
|
|
2022
|
+
].some(hasWord)) return "search";
|
|
2023
|
+
if ([
|
|
2024
|
+
"bash",
|
|
2025
|
+
"shell",
|
|
2026
|
+
"exec",
|
|
2027
|
+
"run"
|
|
2028
|
+
].some(hasWord)) return "command_run";
|
|
2029
|
+
if ([
|
|
2030
|
+
"edit",
|
|
2031
|
+
"update",
|
|
2032
|
+
"patch",
|
|
2033
|
+
"replace"
|
|
2034
|
+
].some(hasWord)) return "file_edit";
|
|
2035
|
+
if (["write", "create"].some(hasWord)) return "file_write";
|
|
2036
|
+
if (["read", "view"].some(hasWord)) return "file_read";
|
|
2037
|
+
if (["task", "agent"].some(hasWord)) return "subagent";
|
|
2038
|
+
return "other";
|
|
2039
|
+
}
|
|
2040
|
+
function extractFiles$1(input) {
|
|
2041
|
+
if (!input || typeof input !== "object") return [];
|
|
2042
|
+
const o = input;
|
|
2043
|
+
const out = /* @__PURE__ */ new Set();
|
|
2044
|
+
for (const key of [
|
|
2045
|
+
"file_path",
|
|
2046
|
+
"filepath",
|
|
2047
|
+
"path",
|
|
2048
|
+
"filePath",
|
|
2049
|
+
"file",
|
|
2050
|
+
"pattern"
|
|
2051
|
+
]) {
|
|
2052
|
+
const v = o[key];
|
|
2053
|
+
if (typeof v === "string" && v.length > 0 && v.length < 512) out.add(v);
|
|
2054
|
+
}
|
|
2055
|
+
return [...out];
|
|
2056
|
+
}
|
|
2057
|
+
function stringifyForNarrative(v) {
|
|
2058
|
+
if (v == null) return "";
|
|
2059
|
+
if (typeof v === "string") return v;
|
|
2060
|
+
try {
|
|
2061
|
+
return JSON.stringify(v);
|
|
2062
|
+
} catch {
|
|
2063
|
+
return String(v);
|
|
2133
2064
|
}
|
|
2134
|
-
if (observation.userPrompt) parts.push(`User prompt:\n${truncate(observation.userPrompt, 2e3)}`);
|
|
2135
|
-
return parts.join("\n\n");
|
|
2136
2065
|
}
|
|
2137
|
-
function truncate(s,
|
|
2138
|
-
return s.length >
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
//#endregion
|
|
2142
|
-
//#region src/prompts/xml.ts
|
|
2143
|
-
const VALID_TAG = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
2144
|
-
function getXmlTag(xml, tag) {
|
|
2145
|
-
if (!VALID_TAG.test(tag)) return "";
|
|
2146
|
-
const match = xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
|
|
2147
|
-
return match ? match[1].trim() : "";
|
|
2066
|
+
function truncate$1(s, n) {
|
|
2067
|
+
return s.length > n ? s.slice(0, n - 1) + "…" : s;
|
|
2148
2068
|
}
|
|
2149
|
-
function
|
|
2150
|
-
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2069
|
+
function buildSyntheticCompression(raw) {
|
|
2070
|
+
const toolName = raw.toolName ?? raw.hookType;
|
|
2071
|
+
const inputStr = stringifyForNarrative(raw.toolInput);
|
|
2072
|
+
const outputStr = stringifyForNarrative(raw.toolOutput);
|
|
2073
|
+
const narrativeParts = [
|
|
2074
|
+
raw.userPrompt ?? "",
|
|
2075
|
+
inputStr,
|
|
2076
|
+
outputStr
|
|
2077
|
+
].filter((s) => s.length > 0);
|
|
2078
|
+
return {
|
|
2079
|
+
id: raw.id,
|
|
2080
|
+
sessionId: raw.sessionId,
|
|
2081
|
+
timestamp: raw.timestamp,
|
|
2082
|
+
type: inferType(toolName, raw.hookType),
|
|
2083
|
+
title: truncate$1(toolName || "observation", 80),
|
|
2084
|
+
subtitle: inputStr ? truncate$1(inputStr, 120) : void 0,
|
|
2085
|
+
facts: [],
|
|
2086
|
+
narrative: truncate$1(narrativeParts.join(" | "), 400),
|
|
2087
|
+
concepts: [],
|
|
2088
|
+
files: extractFiles$1(raw.toolInput),
|
|
2089
|
+
importance: 5,
|
|
2090
|
+
confidence: .3
|
|
2091
|
+
};
|
|
2158
2092
|
}
|
|
2159
2093
|
|
|
2160
2094
|
//#endregion
|
|
@@ -2325,6 +2259,192 @@ function registerSearchFunction(sdk, kv) {
|
|
|
2325
2259
|
});
|
|
2326
2260
|
}
|
|
2327
2261
|
|
|
2262
|
+
//#endregion
|
|
2263
|
+
//#region src/functions/observe.ts
|
|
2264
|
+
function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
2265
|
+
sdk.registerFunction({
|
|
2266
|
+
id: "mem::observe",
|
|
2267
|
+
description: "Capture and store a tool-use observation"
|
|
2268
|
+
}, async (payload) => {
|
|
2269
|
+
const ctx = getContext();
|
|
2270
|
+
if (!payload?.sessionId || typeof payload.sessionId !== "string" || !payload.hookType || typeof payload.hookType !== "string" || !payload.timestamp || typeof payload.timestamp !== "string") return {
|
|
2271
|
+
success: false,
|
|
2272
|
+
error: "Invalid payload: sessionId, hookType, and timestamp are required"
|
|
2273
|
+
};
|
|
2274
|
+
const obsId = generateId("obs");
|
|
2275
|
+
let dedupHash;
|
|
2276
|
+
if (dedupMap) {
|
|
2277
|
+
const d = typeof payload.data === "object" && payload.data !== null ? payload.data : {};
|
|
2278
|
+
const toolName = d["tool_name"] || payload.hookType;
|
|
2279
|
+
dedupHash = dedupMap.computeHash(payload.sessionId, toolName, d["tool_input"]);
|
|
2280
|
+
if (dedupMap.isDuplicate(dedupHash)) return {
|
|
2281
|
+
deduplicated: true,
|
|
2282
|
+
sessionId: payload.sessionId
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
let sanitizedRaw = payload.data;
|
|
2286
|
+
try {
|
|
2287
|
+
const sanitized = stripPrivateData(JSON.stringify(payload.data));
|
|
2288
|
+
sanitizedRaw = JSON.parse(sanitized);
|
|
2289
|
+
} catch {
|
|
2290
|
+
sanitizedRaw = stripPrivateData(String(payload.data));
|
|
2291
|
+
}
|
|
2292
|
+
const raw = {
|
|
2293
|
+
id: obsId,
|
|
2294
|
+
sessionId: payload.sessionId,
|
|
2295
|
+
timestamp: payload.timestamp,
|
|
2296
|
+
hookType: payload.hookType,
|
|
2297
|
+
raw: sanitizedRaw
|
|
2298
|
+
};
|
|
2299
|
+
if (typeof sanitizedRaw === "object" && sanitizedRaw !== null) {
|
|
2300
|
+
const d = sanitizedRaw;
|
|
2301
|
+
if (payload.hookType === "post_tool_use" || payload.hookType === "post_tool_failure") {
|
|
2302
|
+
raw.toolName = d["tool_name"];
|
|
2303
|
+
raw.toolInput = d["tool_input"];
|
|
2304
|
+
raw.toolOutput = d["tool_output"] || d["error"];
|
|
2305
|
+
}
|
|
2306
|
+
if (payload.hookType === "prompt_submit") raw.userPrompt = d["prompt"];
|
|
2307
|
+
}
|
|
2308
|
+
return withKeyedLock(`obs:${payload.sessionId}`, async () => {
|
|
2309
|
+
if (maxObservationsPerSession && maxObservationsPerSession > 0) {
|
|
2310
|
+
if ((await kv.list(KV.observations(payload.sessionId))).length >= maxObservationsPerSession) return {
|
|
2311
|
+
success: false,
|
|
2312
|
+
error: `Session observation limit reached (${maxObservationsPerSession})`
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
await kv.set(KV.observations(payload.sessionId), obsId, raw);
|
|
2316
|
+
if (dedupMap && dedupHash) dedupMap.record(dedupHash);
|
|
2317
|
+
sdk.triggerVoid("stream::set", {
|
|
2318
|
+
stream_name: STREAM.name,
|
|
2319
|
+
group_id: STREAM.group(payload.sessionId),
|
|
2320
|
+
item_id: obsId,
|
|
2321
|
+
data: {
|
|
2322
|
+
type: "raw",
|
|
2323
|
+
observation: raw
|
|
2324
|
+
}
|
|
2325
|
+
});
|
|
2326
|
+
sdk.triggerVoid("stream::set", {
|
|
2327
|
+
stream_name: STREAM.name,
|
|
2328
|
+
group_id: STREAM.viewerGroup,
|
|
2329
|
+
item_id: obsId,
|
|
2330
|
+
data: {
|
|
2331
|
+
type: "raw",
|
|
2332
|
+
observation: raw,
|
|
2333
|
+
sessionId: payload.sessionId
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
const session = await kv.get(KV.sessions, payload.sessionId);
|
|
2337
|
+
if (session) await kv.set(KV.sessions, payload.sessionId, {
|
|
2338
|
+
...session,
|
|
2339
|
+
observationCount: (session.observationCount || 0) + 1
|
|
2340
|
+
});
|
|
2341
|
+
if (isAutoCompressEnabled()) sdk.triggerVoid("mem::compress", {
|
|
2342
|
+
observationId: obsId,
|
|
2343
|
+
sessionId: payload.sessionId,
|
|
2344
|
+
raw
|
|
2345
|
+
});
|
|
2346
|
+
else {
|
|
2347
|
+
const synthetic = buildSyntheticCompression(raw);
|
|
2348
|
+
await kv.set(KV.observations(payload.sessionId), obsId, synthetic);
|
|
2349
|
+
getSearchIndex().add(synthetic);
|
|
2350
|
+
sdk.triggerVoid("stream::set", {
|
|
2351
|
+
stream_name: STREAM.name,
|
|
2352
|
+
group_id: STREAM.group(payload.sessionId),
|
|
2353
|
+
item_id: obsId,
|
|
2354
|
+
data: {
|
|
2355
|
+
type: "compressed",
|
|
2356
|
+
observation: synthetic
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
sdk.triggerVoid("stream::set", {
|
|
2360
|
+
stream_name: STREAM.name,
|
|
2361
|
+
group_id: STREAM.viewerGroup,
|
|
2362
|
+
item_id: obsId,
|
|
2363
|
+
data: {
|
|
2364
|
+
type: "compressed",
|
|
2365
|
+
observation: synthetic,
|
|
2366
|
+
sessionId: payload.sessionId
|
|
2367
|
+
}
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
ctx.logger.info("Observation captured", {
|
|
2371
|
+
obsId,
|
|
2372
|
+
sessionId: payload.sessionId,
|
|
2373
|
+
hook: payload.hookType,
|
|
2374
|
+
compress: isAutoCompressEnabled() ? "llm" : "synthetic"
|
|
2375
|
+
});
|
|
2376
|
+
return { observationId: obsId };
|
|
2377
|
+
});
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
//#endregion
|
|
2382
|
+
//#region src/prompts/compression.ts
|
|
2383
|
+
const COMPRESSION_SYSTEM = `You are a memory compression engine for an AI coding agent. Your job is to extract the essential information from a tool usage observation and compress it into structured data.
|
|
2384
|
+
|
|
2385
|
+
Output EXACTLY this XML format with no additional text:
|
|
2386
|
+
|
|
2387
|
+
<observation>
|
|
2388
|
+
<type>one of: file_read, file_write, file_edit, command_run, search, web_fetch, conversation, error, decision, discovery, subagent, notification, task, other</type>
|
|
2389
|
+
<title>Short descriptive title (max 80 chars)</title>
|
|
2390
|
+
<subtitle>One-line context (optional)</subtitle>
|
|
2391
|
+
<facts>
|
|
2392
|
+
<fact>Specific factual detail 1</fact>
|
|
2393
|
+
<fact>Specific factual detail 2</fact>
|
|
2394
|
+
</facts>
|
|
2395
|
+
<narrative>2-3 sentence summary of what happened and why it matters</narrative>
|
|
2396
|
+
<concepts>
|
|
2397
|
+
<concept>technical concept or pattern</concept>
|
|
2398
|
+
</concepts>
|
|
2399
|
+
<files>
|
|
2400
|
+
<file>path/to/file</file>
|
|
2401
|
+
</files>
|
|
2402
|
+
<importance>1-10 scale, 10 being critical architectural decision</importance>
|
|
2403
|
+
</observation>
|
|
2404
|
+
|
|
2405
|
+
Rules:
|
|
2406
|
+
- Be concise but preserve ALL technically relevant details
|
|
2407
|
+
- File paths must be exact
|
|
2408
|
+
- Importance: 1-3 for routine reads, 4-6 for edits/commands, 7-9 for architectural decisions, 10 for breaking changes
|
|
2409
|
+
- Concepts should be reusable search terms (e.g., "React hooks", "SQL migration", "auth middleware")
|
|
2410
|
+
- Strip any secrets, tokens, or credentials from the output`;
|
|
2411
|
+
function buildCompressionPrompt(observation) {
|
|
2412
|
+
const parts = [`Timestamp: ${observation.timestamp}`, `Hook: ${observation.hookType}`];
|
|
2413
|
+
if (observation.toolName) parts.push(`Tool: ${observation.toolName}`);
|
|
2414
|
+
if (observation.toolInput) {
|
|
2415
|
+
const input = typeof observation.toolInput === "string" ? observation.toolInput : JSON.stringify(observation.toolInput, null, 2);
|
|
2416
|
+
parts.push(`Input:\n${truncate(input, 4e3)}`);
|
|
2417
|
+
}
|
|
2418
|
+
if (observation.toolOutput) {
|
|
2419
|
+
const output = typeof observation.toolOutput === "string" ? observation.toolOutput : JSON.stringify(observation.toolOutput, null, 2);
|
|
2420
|
+
parts.push(`Output:\n${truncate(output, 4e3)}`);
|
|
2421
|
+
}
|
|
2422
|
+
if (observation.userPrompt) parts.push(`User prompt:\n${truncate(observation.userPrompt, 2e3)}`);
|
|
2423
|
+
return parts.join("\n\n");
|
|
2424
|
+
}
|
|
2425
|
+
function truncate(s, max) {
|
|
2426
|
+
return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
//#endregion
|
|
2430
|
+
//#region src/prompts/xml.ts
|
|
2431
|
+
const VALID_TAG = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
2432
|
+
function getXmlTag(xml, tag) {
|
|
2433
|
+
if (!VALID_TAG.test(tag)) return "";
|
|
2434
|
+
const match = xml.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
|
|
2435
|
+
return match ? match[1].trim() : "";
|
|
2436
|
+
}
|
|
2437
|
+
function getXmlChildren(xml, parentTag, childTag) {
|
|
2438
|
+
if (!VALID_TAG.test(parentTag) || !VALID_TAG.test(childTag)) return [];
|
|
2439
|
+
const parentMatch = xml.match(new RegExp(`<${parentTag}>([\\s\\S]*?)</${parentTag}>`));
|
|
2440
|
+
if (!parentMatch) return [];
|
|
2441
|
+
const items = [];
|
|
2442
|
+
const re = new RegExp(`<${childTag}>([\\s\\S]*?)</${childTag}>`, "g");
|
|
2443
|
+
let m;
|
|
2444
|
+
while ((m = re.exec(parentMatch[1])) !== null) items.push(m[1].trim());
|
|
2445
|
+
return items;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2328
2448
|
//#endregion
|
|
2329
2449
|
//#region src/eval/schemas.ts
|
|
2330
2450
|
const HookTypeEnum = z.enum([
|
|
@@ -4007,7 +4127,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
4007
4127
|
|
|
4008
4128
|
//#endregion
|
|
4009
4129
|
//#region src/version.ts
|
|
4010
|
-
const VERSION = "0.8.
|
|
4130
|
+
const VERSION = "0.8.8";
|
|
4011
4131
|
|
|
4012
4132
|
//#endregion
|
|
4013
4133
|
//#region src/functions/export-import.ts
|
|
@@ -4121,7 +4241,9 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
4121
4241
|
"0.8.3",
|
|
4122
4242
|
"0.8.4",
|
|
4123
4243
|
"0.8.5",
|
|
4124
|
-
"0.8.6"
|
|
4244
|
+
"0.8.6",
|
|
4245
|
+
"0.8.7",
|
|
4246
|
+
"0.8.8"
|
|
4125
4247
|
]).has(importData.version)) return {
|
|
4126
4248
|
success: false,
|
|
4127
4249
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -14957,6 +15079,8 @@ async function main() {
|
|
|
14957
15079
|
}
|
|
14958
15080
|
registerConsolidationPipelineFunction(sdk, kv, provider);
|
|
14959
15081
|
console.log(`[agentmemory] Consolidation pipeline: registered (CONSOLIDATION_ENABLED=${isConsolidationEnabled() ? "true" : "false"})`);
|
|
15082
|
+
if (isAutoCompressEnabled()) console.log(`[agentmemory] WARNING: AGENTMEMORY_AUTO_COMPRESS=true — every PostToolUse observation will be sent to your LLM provider for compression. This spends API tokens proportional to your session tool-use frequency (see #138). Set AGENTMEMORY_AUTO_COMPRESS=false to disable.`);
|
|
15083
|
+
else console.log(`[agentmemory] Auto-compress: OFF (default, #138) — observations indexed via zero-LLM synthetic compression. Set AGENTMEMORY_AUTO_COMPRESS=true to opt-in to LLM-powered summaries (uses your API key).`);
|
|
14960
15084
|
const teamConfig = loadTeamConfig();
|
|
14961
15085
|
if (teamConfig) {
|
|
14962
15086
|
registerTeamFunction(sdk, kv, teamConfig);
|