@agentmemory/agentmemory 0.9.0 → 0.9.2
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 +42 -15
- package/dist/cli.mjs +60 -25
- package/dist/cli.mjs.map +1 -1
- package/dist/hooks/notification.mjs +6 -0
- package/dist/hooks/notification.mjs.map +1 -1
- package/dist/hooks/post-tool-failure.mjs +6 -0
- package/dist/hooks/post-tool-failure.mjs.map +1 -1
- package/dist/hooks/post-tool-use.mjs +35 -1
- package/dist/hooks/post-tool-use.mjs.map +1 -1
- package/dist/hooks/pre-compact.mjs +6 -0
- package/dist/hooks/pre-compact.mjs.map +1 -1
- package/dist/hooks/pre-tool-use.mjs +6 -0
- package/dist/hooks/pre-tool-use.mjs.map +1 -1
- package/dist/hooks/prompt-submit.mjs +6 -0
- package/dist/hooks/prompt-submit.mjs.map +1 -1
- package/dist/hooks/session-end.mjs +6 -0
- package/dist/hooks/session-end.mjs.map +1 -1
- package/dist/hooks/session-start.mjs +6 -0
- package/dist/hooks/session-start.mjs.map +1 -1
- package/dist/hooks/stop.mjs +6 -0
- package/dist/hooks/stop.mjs.map +1 -1
- package/dist/hooks/subagent-start.mjs +6 -0
- package/dist/hooks/subagent-start.mjs.map +1 -1
- package/dist/hooks/subagent-stop.mjs +6 -0
- package/dist/hooks/subagent-stop.mjs.map +1 -1
- package/dist/hooks/task-completed.mjs +6 -0
- package/dist/hooks/task-completed.mjs.map +1 -1
- package/dist/image-refs-Dq5wcV-a.mjs +3 -0
- package/dist/image-store-BLOkD0xV.mjs +3 -0
- package/dist/index.mjs +2054 -144
- package/dist/index.mjs.map +1 -1
- package/dist/{src-B3pEsBSb.mjs → src-tmuZyobT.mjs} +1974 -253
- package/dist/src-tmuZyobT.mjs.map +1 -0
- package/dist/{standalone-DXc-BEqr.mjs → standalone-BiwX0rdC.mjs} +2 -2
- package/dist/{standalone-DXc-BEqr.mjs.map → standalone-BiwX0rdC.mjs.map} +1 -1
- package/dist/standalone.mjs +136 -2
- package/dist/standalone.mjs.map +1 -1
- package/dist/{tools-registry-DXIK5CxQ.mjs → tools-registry-CHH84gIQ.mjs} +166 -12
- package/dist/tools-registry-CHH84gIQ.mjs.map +1 -0
- package/dist/viewer/index.html +249 -62
- package/package.json +5 -3
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/scripts/notification.mjs +6 -0
- package/plugin/scripts/notification.mjs.map +1 -1
- package/plugin/scripts/post-tool-failure.mjs +6 -0
- package/plugin/scripts/post-tool-failure.mjs.map +1 -1
- package/plugin/scripts/post-tool-use.mjs +35 -1
- package/plugin/scripts/post-tool-use.mjs.map +1 -1
- package/plugin/scripts/pre-compact.mjs +6 -0
- package/plugin/scripts/pre-compact.mjs.map +1 -1
- package/plugin/scripts/pre-tool-use.mjs +6 -0
- package/plugin/scripts/pre-tool-use.mjs.map +1 -1
- package/plugin/scripts/prompt-submit.mjs +6 -0
- package/plugin/scripts/prompt-submit.mjs.map +1 -1
- package/plugin/scripts/session-end.mjs +6 -0
- package/plugin/scripts/session-end.mjs.map +1 -1
- package/plugin/scripts/session-start.mjs +6 -0
- package/plugin/scripts/session-start.mjs.map +1 -1
- package/plugin/scripts/stop.mjs +6 -0
- package/plugin/scripts/stop.mjs.map +1 -1
- package/plugin/scripts/subagent-start.mjs +6 -0
- package/plugin/scripts/subagent-start.mjs.map +1 -1
- package/plugin/scripts/subagent-stop.mjs +6 -0
- package/plugin/scripts/subagent-stop.mjs.map +1 -1
- package/plugin/scripts/task-completed.mjs +6 -0
- package/plugin/scripts/task-completed.mjs.map +1 -1
- package/dist/src-B3pEsBSb.mjs.map +0 -1
- package/dist/tools-registry-DXIK5CxQ.mjs.map +0 -1
- package/dist/transformers-BX_tgxdO.mjs +0 -38684
- package/dist/transformers-BX_tgxdO.mjs.map +0 -1
- package/dist/transformers-KMm1i9no.mjs +0 -38683
- package/dist/transformers-KMm1i9no.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -4,16 +4,33 @@ import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "n
|
|
|
4
4
|
import { basename, dirname, extname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import Anthropic from "@anthropic-ai/sdk";
|
|
7
|
+
import { lstat, mkdir, open, readFile, readdir, stat, unlink, utimes, writeFile } from "node:fs/promises";
|
|
7
8
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
import { execFile } from "node:child_process";
|
|
10
11
|
import { promisify } from "node:util";
|
|
11
12
|
import { lookup } from "node:dns/promises";
|
|
12
13
|
import { isIP } from "node:net";
|
|
13
|
-
import { lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { createServer } from "node:http";
|
|
16
16
|
|
|
17
|
+
//#region \0rolldown/runtime.js
|
|
18
|
+
var __defProp = Object.defineProperty;
|
|
19
|
+
var __exportAll = (all, no_symbols) => {
|
|
20
|
+
let target = {};
|
|
21
|
+
for (var name in all) {
|
|
22
|
+
__defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (!no_symbols) {
|
|
28
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
29
|
+
}
|
|
30
|
+
return target;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
17
34
|
//#region src/config.ts
|
|
18
35
|
function safeParseInt(value, fallback) {
|
|
19
36
|
if (!value) return fallback;
|
|
@@ -38,37 +55,48 @@ function loadEnvFile() {
|
|
|
38
55
|
}
|
|
39
56
|
return vars;
|
|
40
57
|
}
|
|
58
|
+
function hasRealValue(v) {
|
|
59
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
60
|
+
}
|
|
41
61
|
function detectProvider(env) {
|
|
42
62
|
const maxTokens = parseInt(env["MAX_TOKENS"] || "4096", 10);
|
|
43
|
-
if (env["MINIMAX_API_KEY"]) return {
|
|
63
|
+
if (hasRealValue(env["MINIMAX_API_KEY"])) return {
|
|
44
64
|
provider: "minimax",
|
|
45
65
|
model: env["MINIMAX_MODEL"] || "MiniMax-M2.7",
|
|
46
66
|
maxTokens
|
|
47
67
|
};
|
|
48
|
-
if (env["ANTHROPIC_API_KEY"]) return {
|
|
68
|
+
if (hasRealValue(env["ANTHROPIC_API_KEY"])) return {
|
|
49
69
|
provider: "anthropic",
|
|
50
70
|
model: env["ANTHROPIC_MODEL"] || "claude-sonnet-4-20250514",
|
|
51
71
|
maxTokens,
|
|
52
72
|
baseURL: env["ANTHROPIC_BASE_URL"]
|
|
53
73
|
};
|
|
54
|
-
if (env["GEMINI_API_KEY"] || env["GOOGLE_API_KEY"]) {
|
|
55
|
-
if (!env["GEMINI_API_KEY"] && env["GOOGLE_API_KEY"]) process.stderr.write("[agentmemory] GOOGLE_API_KEY detected — treating as GEMINI_API_KEY. Set GEMINI_API_KEY in ~/.agentmemory/.env to silence this warning.\n");
|
|
74
|
+
if (hasRealValue(env["GEMINI_API_KEY"]) || hasRealValue(env["GOOGLE_API_KEY"])) {
|
|
75
|
+
if (!hasRealValue(env["GEMINI_API_KEY"]) && hasRealValue(env["GOOGLE_API_KEY"])) process.stderr.write("[agentmemory] GOOGLE_API_KEY detected — treating as GEMINI_API_KEY. Set GEMINI_API_KEY in ~/.agentmemory/.env to silence this warning.\n");
|
|
56
76
|
return {
|
|
57
77
|
provider: "gemini",
|
|
58
78
|
model: env["GEMINI_MODEL"] || "gemini-2.0-flash",
|
|
59
79
|
maxTokens
|
|
60
80
|
};
|
|
61
81
|
}
|
|
62
|
-
if (env["OPENROUTER_API_KEY"]) return {
|
|
82
|
+
if (hasRealValue(env["OPENROUTER_API_KEY"])) return {
|
|
63
83
|
provider: "openrouter",
|
|
64
84
|
model: env["OPENROUTER_MODEL"] || "anthropic/claude-sonnet-4-20250514",
|
|
65
85
|
maxTokens
|
|
66
86
|
};
|
|
67
|
-
if (env["
|
|
87
|
+
if (!(env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true")) {
|
|
88
|
+
process.stderr.write("[agentmemory] No LLM provider key found (ANTHROPIC_API_KEY, GEMINI_API_KEY, OPENROUTER_API_KEY, MINIMAX_API_KEY). LLM-backed compression and summarization are DISABLED — using no-op provider. This is the safe default: the agent-sdk fallback used to spawn Claude Agent SDK child sessions which inherit Claude Code's plugin hooks and cause infinite Stop-hook recursion (#149 follow-up). To opt in to the agent-sdk fallback anyway, set both AGENTMEMORY_AUTO_COMPRESS=true AND AGENTMEMORY_ALLOW_AGENT_SDK=true — but be aware it will burn your Claude Pro allocation and may still recurse if you use it from inside Claude Code itself.\n");
|
|
89
|
+
return {
|
|
90
|
+
provider: "noop",
|
|
91
|
+
model: "noop",
|
|
92
|
+
maxTokens
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
process.stderr.write("[agentmemory] WARNING: agent-sdk fallback enabled via AGENTMEMORY_ALLOW_AGENT_SDK=true. This spawns @anthropic-ai/claude-agent-sdk child sessions that can trigger the Stop-hook recursion loop (#149 follow-up). A SDK-child env marker is set to block re-entry, but prefer setting a real API key in ~/.agentmemory/.env instead.\n");
|
|
68
96
|
return {
|
|
69
97
|
provider: "agent-sdk",
|
|
70
98
|
model: "claude-sonnet-4-20250514",
|
|
71
|
-
maxTokens
|
|
99
|
+
maxTokens
|
|
72
100
|
};
|
|
73
101
|
}
|
|
74
102
|
function loadConfig() {
|
|
@@ -177,7 +205,16 @@ const VALID_PROVIDERS = new Set([
|
|
|
177
205
|
"minimax"
|
|
178
206
|
]);
|
|
179
207
|
function loadFallbackConfig() {
|
|
180
|
-
|
|
208
|
+
const env = getMergedEnv();
|
|
209
|
+
const raw = env["FALLBACK_PROVIDERS"] || "";
|
|
210
|
+
const allowAgentSdk = env["AGENTMEMORY_ALLOW_AGENT_SDK"] === "true";
|
|
211
|
+
return { providers: raw.split(",").map((p) => p.trim()).filter((p) => Boolean(p) && VALID_PROVIDERS.has(p)).filter((p) => {
|
|
212
|
+
if (p === "agent-sdk" && !allowAgentSdk) {
|
|
213
|
+
process.stderr.write("[agentmemory] Ignoring FALLBACK_PROVIDERS entry 'agent-sdk' (AGENTMEMORY_ALLOW_AGENT_SDK is not 'true'). The agent-sdk fallback can spawn Claude Agent SDK child sessions that trigger the Stop-hook recursion loop (#149 follow-up). Opt in explicitly with AGENTMEMORY_ALLOW_AGENT_SDK=true if this is intentional.\n");
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}) };
|
|
181
218
|
}
|
|
182
219
|
|
|
183
220
|
//#endregion
|
|
@@ -191,18 +228,26 @@ var AgentSDKProvider = class {
|
|
|
191
228
|
return this.query(systemPrompt, userPrompt);
|
|
192
229
|
}
|
|
193
230
|
async query(systemPrompt, userPrompt) {
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
231
|
+
if (process.env.AGENTMEMORY_SDK_CHILD === "1") return "";
|
|
232
|
+
const prev = process.env.AGENTMEMORY_SDK_CHILD;
|
|
233
|
+
process.env.AGENTMEMORY_SDK_CHILD = "1";
|
|
234
|
+
try {
|
|
235
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
236
|
+
const messages = query({
|
|
237
|
+
prompt: userPrompt,
|
|
238
|
+
options: {
|
|
239
|
+
systemPrompt,
|
|
240
|
+
maxTurns: 1,
|
|
241
|
+
allowedTools: []
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
let result = "";
|
|
245
|
+
for await (const msg of messages) if (msg.type === "result") result = msg.result ?? "";
|
|
246
|
+
return result;
|
|
247
|
+
} finally {
|
|
248
|
+
if (prev === void 0) delete process.env.AGENTMEMORY_SDK_CHILD;
|
|
249
|
+
else process.env.AGENTMEMORY_SDK_CHILD = prev;
|
|
250
|
+
}
|
|
206
251
|
}
|
|
207
252
|
};
|
|
208
253
|
|
|
@@ -227,6 +272,26 @@ var AnthropicProvider = class {
|
|
|
227
272
|
async summarize(systemPrompt, userPrompt) {
|
|
228
273
|
return this.call(systemPrompt, userPrompt);
|
|
229
274
|
}
|
|
275
|
+
async describeImage(imageData, mimeType, prompt) {
|
|
276
|
+
return (await this.client.messages.create({
|
|
277
|
+
model: this.model,
|
|
278
|
+
max_tokens: this.maxTokens,
|
|
279
|
+
messages: [{
|
|
280
|
+
role: "user",
|
|
281
|
+
content: [{
|
|
282
|
+
type: "image",
|
|
283
|
+
source: {
|
|
284
|
+
type: "base64",
|
|
285
|
+
media_type: mimeType,
|
|
286
|
+
data: imageData
|
|
287
|
+
}
|
|
288
|
+
}, {
|
|
289
|
+
type: "text",
|
|
290
|
+
text: prompt
|
|
291
|
+
}]
|
|
292
|
+
}]
|
|
293
|
+
})).content.find((b) => b.type === "text")?.text ?? "";
|
|
294
|
+
}
|
|
230
295
|
async call(systemPrompt, userPrompt) {
|
|
231
296
|
return (await this.client.messages.create({
|
|
232
297
|
model: this.model,
|
|
@@ -301,6 +366,25 @@ var MinimaxProvider = class {
|
|
|
301
366
|
}
|
|
302
367
|
};
|
|
303
368
|
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/providers/noop.ts
|
|
371
|
+
/**
|
|
372
|
+
* Returns empty strings for every call. Used when no LLM API key is set
|
|
373
|
+
* AND the user has not opted into the agent-sdk fallback via
|
|
374
|
+
* AGENTMEMORY_ALLOW_AGENT_SDK=true. Callers (compress, summarize) must
|
|
375
|
+
* detect the empty result and short-circuit instead of spawning a
|
|
376
|
+
* provider session (#149 / Stop-hook recursion loop fix).
|
|
377
|
+
*/
|
|
378
|
+
var NoopProvider = class {
|
|
379
|
+
name = "noop";
|
|
380
|
+
async compress() {
|
|
381
|
+
return "";
|
|
382
|
+
}
|
|
383
|
+
async summarize() {
|
|
384
|
+
return "";
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
304
388
|
//#endregion
|
|
305
389
|
//#region src/providers/openrouter.ts
|
|
306
390
|
var OpenRouterProvider = class {
|
|
@@ -512,28 +596,67 @@ var GeminiEmbeddingProvider = class {
|
|
|
512
596
|
|
|
513
597
|
//#endregion
|
|
514
598
|
//#region src/providers/embedding/openai.ts
|
|
515
|
-
const
|
|
599
|
+
const DEFAULT_BASE_URL = "https://api.openai.com";
|
|
600
|
+
const DEFAULT_MODEL$1 = "text-embedding-3-small";
|
|
601
|
+
/**
|
|
602
|
+
* Known OpenAI embedding model dimensions. Extend as new models ship.
|
|
603
|
+
* Override in any case via OPENAI_EMBEDDING_DIMENSIONS for custom or
|
|
604
|
+
* self-hosted OpenAI-compatible endpoints returning non-standard sizes.
|
|
605
|
+
*/
|
|
606
|
+
const MODEL_DIMENSIONS = {
|
|
607
|
+
"text-embedding-3-small": 1536,
|
|
608
|
+
"text-embedding-3-large": 3072,
|
|
609
|
+
"text-embedding-ada-002": 1536
|
|
610
|
+
};
|
|
611
|
+
const DEFAULT_DIMENSIONS = MODEL_DIMENSIONS[DEFAULT_MODEL$1] ?? 1536;
|
|
612
|
+
function resolveDimensions(model, override) {
|
|
613
|
+
if (override !== void 0 && override.trim().length > 0) {
|
|
614
|
+
const parsed = parseInt(override, 10);
|
|
615
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`OPENAI_EMBEDDING_DIMENSIONS must be a positive integer, got: ${override}`);
|
|
616
|
+
return parsed;
|
|
617
|
+
}
|
|
618
|
+
return MODEL_DIMENSIONS[model] ?? DEFAULT_DIMENSIONS;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* OpenAI-compatible embedding provider.
|
|
622
|
+
*
|
|
623
|
+
* Required env vars:
|
|
624
|
+
* OPENAI_API_KEY — API key
|
|
625
|
+
*
|
|
626
|
+
* Optional:
|
|
627
|
+
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com)
|
|
628
|
+
* OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
|
|
629
|
+
* OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
|
|
630
|
+
* custom / self-hosted models not in the
|
|
631
|
+
* MODEL_DIMENSIONS table above)
|
|
632
|
+
*/
|
|
516
633
|
var OpenAIEmbeddingProvider = class {
|
|
517
634
|
name = "openai";
|
|
518
|
-
dimensions
|
|
635
|
+
dimensions;
|
|
519
636
|
apiKey;
|
|
637
|
+
baseUrl;
|
|
638
|
+
model;
|
|
520
639
|
constructor(apiKey) {
|
|
521
640
|
this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
|
|
522
641
|
if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
|
|
642
|
+
this.baseUrl = getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL;
|
|
643
|
+
this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
|
|
644
|
+
this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
|
|
523
645
|
}
|
|
524
646
|
async embed(text) {
|
|
525
647
|
const [result] = await this.embedBatch([text]);
|
|
526
648
|
return result;
|
|
527
649
|
}
|
|
528
650
|
async embedBatch(texts) {
|
|
529
|
-
const
|
|
651
|
+
const url = `${this.baseUrl}/v1/embeddings`;
|
|
652
|
+
const response = await fetch(url, {
|
|
530
653
|
method: "POST",
|
|
531
654
|
headers: {
|
|
532
655
|
Authorization: `Bearer ${this.apiKey}`,
|
|
533
656
|
"Content-Type": "application/json"
|
|
534
657
|
},
|
|
535
658
|
body: JSON.stringify({
|
|
536
|
-
model:
|
|
659
|
+
model: this.model,
|
|
537
660
|
input: texts
|
|
538
661
|
})
|
|
539
662
|
});
|
|
@@ -674,7 +797,7 @@ var LocalEmbeddingProvider = class {
|
|
|
674
797
|
if (this.extractor) return this.extractor;
|
|
675
798
|
let transformers;
|
|
676
799
|
try {
|
|
677
|
-
transformers = await import("
|
|
800
|
+
transformers = await import("@xenova/transformers");
|
|
678
801
|
} catch {
|
|
679
802
|
throw new Error("Install @xenova/transformers for local embeddings: npm install @xenova/transformers");
|
|
680
803
|
}
|
|
@@ -683,8 +806,86 @@ var LocalEmbeddingProvider = class {
|
|
|
683
806
|
}
|
|
684
807
|
};
|
|
685
808
|
|
|
809
|
+
//#endregion
|
|
810
|
+
//#region src/providers/embedding/clip.ts
|
|
811
|
+
const DEFAULT_MODEL = "Xenova/clip-vit-base-patch32";
|
|
812
|
+
const DIMENSIONS = 512;
|
|
813
|
+
var ClipEmbeddingProvider = class {
|
|
814
|
+
name = "clip";
|
|
815
|
+
dimensions = DIMENSIONS;
|
|
816
|
+
textExtractor = null;
|
|
817
|
+
imageExtractor = null;
|
|
818
|
+
transformers = null;
|
|
819
|
+
modelId;
|
|
820
|
+
constructor(modelId = DEFAULT_MODEL) {
|
|
821
|
+
this.modelId = modelId;
|
|
822
|
+
}
|
|
823
|
+
async embed(text) {
|
|
824
|
+
const [vec] = await this.embedBatch([text]);
|
|
825
|
+
return vec;
|
|
826
|
+
}
|
|
827
|
+
async embedBatch(texts) {
|
|
828
|
+
return (await (await this.getTextExtractor())(texts, {
|
|
829
|
+
pooling: "mean",
|
|
830
|
+
normalize: true
|
|
831
|
+
})).tolist().map((v) => new Float32Array(v));
|
|
832
|
+
}
|
|
833
|
+
async embedImage(src) {
|
|
834
|
+
const image = await loadImage(await this.getTransformers(), src);
|
|
835
|
+
const output = await (await this.getImageExtractor())(image);
|
|
836
|
+
return normalize(output.data ?? new Float32Array(output.tolist()[0] || []));
|
|
837
|
+
}
|
|
838
|
+
async getTransformers() {
|
|
839
|
+
if (this.transformers) return this.transformers;
|
|
840
|
+
try {
|
|
841
|
+
this.transformers = await import("@xenova/transformers");
|
|
842
|
+
} catch {
|
|
843
|
+
throw new Error("Install @xenova/transformers for CLIP image embeddings: npm install @xenova/transformers");
|
|
844
|
+
}
|
|
845
|
+
return this.transformers;
|
|
846
|
+
}
|
|
847
|
+
async getTextExtractor() {
|
|
848
|
+
if (this.textExtractor) return this.textExtractor;
|
|
849
|
+
this.textExtractor = await (await this.getTransformers()).pipeline("feature-extraction", this.modelId);
|
|
850
|
+
return this.textExtractor;
|
|
851
|
+
}
|
|
852
|
+
async getImageExtractor() {
|
|
853
|
+
if (this.imageExtractor) return this.imageExtractor;
|
|
854
|
+
this.imageExtractor = await (await this.getTransformers()).pipeline("image-feature-extraction", this.modelId);
|
|
855
|
+
return this.imageExtractor;
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
async function loadImage(t, src) {
|
|
859
|
+
if (src.startsWith("data:")) {
|
|
860
|
+
const comma = src.indexOf(",");
|
|
861
|
+
const b64 = comma >= 0 ? src.slice(comma + 1) : src;
|
|
862
|
+
const buf = Buffer.from(b64, "base64");
|
|
863
|
+
const blob = new Blob([buf]);
|
|
864
|
+
return t.RawImage.fromBlob(blob);
|
|
865
|
+
}
|
|
866
|
+
const data = await readFile(src);
|
|
867
|
+
const blob = new Blob([data]);
|
|
868
|
+
return t.RawImage.fromBlob(blob);
|
|
869
|
+
}
|
|
870
|
+
function normalize(vec) {
|
|
871
|
+
let sum = 0;
|
|
872
|
+
for (let i = 0; i < vec.length; i++) sum += vec[i] * vec[i];
|
|
873
|
+
const norm = Math.sqrt(sum);
|
|
874
|
+
if (norm === 0) return vec;
|
|
875
|
+
const out = new Float32Array(vec.length);
|
|
876
|
+
for (let i = 0; i < vec.length; i++) out[i] = vec[i] / norm;
|
|
877
|
+
return out;
|
|
878
|
+
}
|
|
879
|
+
|
|
686
880
|
//#endregion
|
|
687
881
|
//#region src/providers/embedding/index.ts
|
|
882
|
+
let imageEmbeddingProvider = null;
|
|
883
|
+
function createImageEmbeddingProvider() {
|
|
884
|
+
if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] !== "true") return null;
|
|
885
|
+
if (imageEmbeddingProvider) return imageEmbeddingProvider;
|
|
886
|
+
imageEmbeddingProvider = new ClipEmbeddingProvider();
|
|
887
|
+
return imageEmbeddingProvider;
|
|
888
|
+
}
|
|
688
889
|
function createEmbeddingProvider() {
|
|
689
890
|
const detected = detectEmbeddingProvider();
|
|
690
891
|
if (!detected) return null;
|
|
@@ -736,6 +937,7 @@ function createBaseProvider(config) {
|
|
|
736
937
|
return new OpenRouterProvider(geminiKey, config.model, config.maxTokens, "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions");
|
|
737
938
|
}
|
|
738
939
|
case "openrouter": return new OpenRouterProvider(requireEnvVar("OPENROUTER_API_KEY"), config.model, config.maxTokens, "https://openrouter.ai/api/v1/chat/completions");
|
|
940
|
+
case "noop": return new NoopProvider();
|
|
739
941
|
default: return new AgentSDKProvider();
|
|
740
942
|
}
|
|
741
943
|
}
|
|
@@ -939,7 +1141,12 @@ const KV = {
|
|
|
939
1141
|
enrichedChunks: (sessionId) => `mem:enriched:${sessionId}`,
|
|
940
1142
|
latentEmbeddings: (obsId) => `mem:latent:${obsId}`,
|
|
941
1143
|
retentionScores: "mem:retention",
|
|
942
|
-
accessLog: "mem:access"
|
|
1144
|
+
accessLog: "mem:access",
|
|
1145
|
+
imageRefs: "mem:image-refs",
|
|
1146
|
+
imageEmbeddings: "mem:image-embeddings",
|
|
1147
|
+
slots: "mem:slots",
|
|
1148
|
+
globalSlots: "mem:slots:global",
|
|
1149
|
+
state: "mem:state"
|
|
943
1150
|
};
|
|
944
1151
|
const STREAM = {
|
|
945
1152
|
name: "mem-live",
|
|
@@ -1335,7 +1542,7 @@ async function loadPipeline() {
|
|
|
1335
1542
|
if (pipelineLoading) return pipelineLoading;
|
|
1336
1543
|
pipelineLoading = (async () => {
|
|
1337
1544
|
try {
|
|
1338
|
-
const { pipeline: createPipeline } = await import("
|
|
1545
|
+
const { pipeline: createPipeline } = await import("@xenova/transformers");
|
|
1339
1546
|
pipeline = await createPipeline("text-classification", "Xenova/ms-marco-MiniLM-L-6-v2", { quantized: true });
|
|
1340
1547
|
return pipeline;
|
|
1341
1548
|
} catch {
|
|
@@ -2141,7 +2348,7 @@ function buildSyntheticCompression(raw) {
|
|
|
2141
2348
|
inputStr,
|
|
2142
2349
|
outputStr
|
|
2143
2350
|
].filter((s) => s.length > 0);
|
|
2144
|
-
|
|
2351
|
+
const result = {
|
|
2145
2352
|
id: raw.id,
|
|
2146
2353
|
sessionId: raw.sessionId,
|
|
2147
2354
|
timestamp: raw.timestamp,
|
|
@@ -2155,6 +2362,9 @@ function buildSyntheticCompression(raw) {
|
|
|
2155
2362
|
importance: 5,
|
|
2156
2363
|
confidence: .3
|
|
2157
2364
|
};
|
|
2365
|
+
if (raw.modality) result.modality = raw.modality;
|
|
2366
|
+
if (raw.imageData) result.imageData = raw.imageData;
|
|
2367
|
+
return result;
|
|
2158
2368
|
}
|
|
2159
2369
|
|
|
2160
2370
|
//#endregion
|
|
@@ -2402,6 +2612,24 @@ function registerSearchFunction(sdk, kv) {
|
|
|
2402
2612
|
|
|
2403
2613
|
//#endregion
|
|
2404
2614
|
//#region src/functions/observe.ts
|
|
2615
|
+
function extractImage(d) {
|
|
2616
|
+
if (!d) return void 0;
|
|
2617
|
+
if (typeof d === "string") {
|
|
2618
|
+
if (d.startsWith("data:image/") || d.startsWith("iVBORw0KGgo") || d.startsWith("/9j/")) return d;
|
|
2619
|
+
return;
|
|
2620
|
+
}
|
|
2621
|
+
if (typeof d === "object" && d !== null) {
|
|
2622
|
+
const obj = d;
|
|
2623
|
+
if (typeof obj["image_data"] === "string") return obj["image_data"];
|
|
2624
|
+
if (typeof obj["image_path"] === "string") return obj["image_path"];
|
|
2625
|
+
if (typeof obj["imageBase64"] === "string") return obj["imageBase64"];
|
|
2626
|
+
if (typeof obj["imagePath"] === "string") return obj["imagePath"];
|
|
2627
|
+
for (const key of Object.keys(obj)) {
|
|
2628
|
+
const match = extractImage(obj[key]);
|
|
2629
|
+
if (match) return match;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2405
2633
|
function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
2406
2634
|
sdk.registerFunction("mem::observe", async (payload) => {
|
|
2407
2635
|
if (!payload?.sessionId || typeof payload.sessionId !== "string" || !payload.hookType || typeof payload.hookType !== "string" || !payload.timestamp || typeof payload.timestamp !== "string") return {
|
|
@@ -2433,6 +2661,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2433
2661
|
hookType: payload.hookType,
|
|
2434
2662
|
raw: sanitizedRaw
|
|
2435
2663
|
};
|
|
2664
|
+
let extractedImage;
|
|
2436
2665
|
if (typeof sanitizedRaw === "object" && sanitizedRaw !== null) {
|
|
2437
2666
|
const d = sanitizedRaw;
|
|
2438
2667
|
if (payload.hookType === "post_tool_use" || payload.hookType === "post_tool_failure") {
|
|
@@ -2441,7 +2670,13 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2441
2670
|
raw.toolOutput = d["tool_output"] || d["error"];
|
|
2442
2671
|
}
|
|
2443
2672
|
if (payload.hookType === "prompt_submit") raw.userPrompt = d["prompt"];
|
|
2673
|
+
extractedImage = extractImage(sanitizedRaw);
|
|
2674
|
+
if (extractedImage) raw.modality = raw.toolInput || raw.toolOutput || raw.userPrompt ? "mixed" : "image";
|
|
2675
|
+
} else if (typeof sanitizedRaw === "string") {
|
|
2676
|
+
extractedImage = extractImage(sanitizedRaw);
|
|
2677
|
+
if (extractedImage) raw.modality = "image";
|
|
2444
2678
|
}
|
|
2679
|
+
const pendingImageData = extractedImage;
|
|
2445
2680
|
return withKeyedLock(`obs:${payload.sessionId}`, async () => {
|
|
2446
2681
|
if (maxObservationsPerSession && maxObservationsPerSession > 0) {
|
|
2447
2682
|
if ((await kv.list(KV.observations(payload.sessionId))).length >= maxObservationsPerSession) return {
|
|
@@ -2449,7 +2684,29 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2449
2684
|
error: `Session observation limit reached (${maxObservationsPerSession})`
|
|
2450
2685
|
};
|
|
2451
2686
|
}
|
|
2452
|
-
|
|
2687
|
+
if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
|
|
2688
|
+
const { saveImageToDisk } = await Promise.resolve().then(() => image_store_exports);
|
|
2689
|
+
const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
|
|
2690
|
+
raw.imageData = filePath;
|
|
2691
|
+
const { incrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
|
|
2692
|
+
await incrementImageRef(kv, filePath);
|
|
2693
|
+
sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: bytesWritten });
|
|
2694
|
+
if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] === "true") sdk.triggerVoid("mem::vision-embed", {
|
|
2695
|
+
imageRef: filePath,
|
|
2696
|
+
sessionId: payload.sessionId,
|
|
2697
|
+
observationId: obsId
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
try {
|
|
2701
|
+
await kv.set(KV.observations(payload.sessionId), obsId, raw);
|
|
2702
|
+
} catch (error) {
|
|
2703
|
+
if (raw.imageData) {
|
|
2704
|
+
const { deleteImage } = await Promise.resolve().then(() => image_store_exports);
|
|
2705
|
+
const { deletedBytes } = await deleteImage(raw.imageData);
|
|
2706
|
+
if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
|
|
2707
|
+
}
|
|
2708
|
+
throw error;
|
|
2709
|
+
}
|
|
2453
2710
|
if (dedupMap && dedupHash) dedupMap.record(dedupHash);
|
|
2454
2711
|
await sdk.trigger({
|
|
2455
2712
|
function_id: "stream::set",
|
|
@@ -2479,15 +2736,26 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2479
2736
|
action: TriggerAction.Void()
|
|
2480
2737
|
});
|
|
2481
2738
|
const session = await kv.get(KV.sessions, payload.sessionId);
|
|
2482
|
-
if (session)
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2739
|
+
if (session) {
|
|
2740
|
+
const updates = [{
|
|
2741
|
+
type: "set",
|
|
2742
|
+
path: "updatedAt",
|
|
2743
|
+
value: (/* @__PURE__ */ new Date()).toISOString()
|
|
2744
|
+
}, {
|
|
2745
|
+
type: "set",
|
|
2746
|
+
path: "observationCount",
|
|
2747
|
+
value: (session.observationCount || 0) + 1
|
|
2748
|
+
}];
|
|
2749
|
+
if (!session.firstPrompt && typeof raw.userPrompt === "string") {
|
|
2750
|
+
const trimmed = raw.userPrompt.replace(/\s+/g, " ").trim();
|
|
2751
|
+
if (trimmed.length > 0) updates.push({
|
|
2752
|
+
type: "set",
|
|
2753
|
+
path: "firstPrompt",
|
|
2754
|
+
value: trimmed.slice(0, 200)
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
await kv.update(KV.sessions, payload.sessionId, updates);
|
|
2758
|
+
}
|
|
2491
2759
|
if (isAutoCompressEnabled()) await sdk.trigger({
|
|
2492
2760
|
function_id: "mem::compress",
|
|
2493
2761
|
payload: {
|
|
@@ -2538,6 +2806,815 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2538
2806
|
});
|
|
2539
2807
|
}
|
|
2540
2808
|
|
|
2809
|
+
//#endregion
|
|
2810
|
+
//#region src/utils/image-store.ts
|
|
2811
|
+
var image_store_exports = /* @__PURE__ */ __exportAll({
|
|
2812
|
+
IMAGES_DIR: () => IMAGES_DIR,
|
|
2813
|
+
deleteImage: () => deleteImage,
|
|
2814
|
+
getMaxBytes: () => getMaxBytes,
|
|
2815
|
+
isManagedImagePath: () => isManagedImagePath,
|
|
2816
|
+
saveImageToDisk: () => saveImageToDisk,
|
|
2817
|
+
touchImage: () => touchImage
|
|
2818
|
+
});
|
|
2819
|
+
const IMAGES_DIR = join(homedir(), ".agentmemory", "images");
|
|
2820
|
+
const DEFAULT_MAX_BYTES = 500 * 1024 * 1024;
|
|
2821
|
+
function getMaxBytes() {
|
|
2822
|
+
return Number(process.env.AGENTMEMORY_IMAGE_STORE_MAX_BYTES) || DEFAULT_MAX_BYTES;
|
|
2823
|
+
}
|
|
2824
|
+
function isManagedImagePath(filePath) {
|
|
2825
|
+
const resolved = resolve(filePath);
|
|
2826
|
+
const normalizedImagesDir = resolve(IMAGES_DIR);
|
|
2827
|
+
return resolved.startsWith(normalizedImagesDir + sep) || resolved === normalizedImagesDir;
|
|
2828
|
+
}
|
|
2829
|
+
function contentHash(data) {
|
|
2830
|
+
return createHash("sha256").update(data).digest("hex");
|
|
2831
|
+
}
|
|
2832
|
+
async function saveImageToDisk(base64Data) {
|
|
2833
|
+
if (!base64Data) return {
|
|
2834
|
+
filePath: "",
|
|
2835
|
+
bytesWritten: 0
|
|
2836
|
+
};
|
|
2837
|
+
if (!existsSync(IMAGES_DIR)) await mkdir(IMAGES_DIR, { recursive: true });
|
|
2838
|
+
let cleanBase64 = base64Data;
|
|
2839
|
+
let ext = "png";
|
|
2840
|
+
if (base64Data.startsWith("data:image/")) {
|
|
2841
|
+
const commaIdx = base64Data.indexOf(",");
|
|
2842
|
+
if (commaIdx !== -1) {
|
|
2843
|
+
const meta = base64Data.substring(0, commaIdx);
|
|
2844
|
+
if (meta.includes("jpeg") || meta.includes("jpg")) ext = "jpg";
|
|
2845
|
+
else if (meta.includes("webp")) ext = "webp";
|
|
2846
|
+
else if (meta.includes("gif")) ext = "gif";
|
|
2847
|
+
cleanBase64 = base64Data.substring(commaIdx + 1);
|
|
2848
|
+
}
|
|
2849
|
+
} else if (base64Data.startsWith("/9j/")) ext = "jpg";
|
|
2850
|
+
const filePath = join(IMAGES_DIR, `${contentHash(cleanBase64)}.${ext}`);
|
|
2851
|
+
if (existsSync(filePath)) return {
|
|
2852
|
+
filePath,
|
|
2853
|
+
bytesWritten: 0
|
|
2854
|
+
};
|
|
2855
|
+
await writeFile(filePath, Buffer.from(cleanBase64, "base64"));
|
|
2856
|
+
return {
|
|
2857
|
+
filePath,
|
|
2858
|
+
bytesWritten: (await stat(filePath)).size
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
async function deleteImage(filePath) {
|
|
2862
|
+
if (!filePath) return { deletedBytes: 0 };
|
|
2863
|
+
if (!isManagedImagePath(filePath)) return { deletedBytes: 0 };
|
|
2864
|
+
try {
|
|
2865
|
+
if (existsSync(filePath)) {
|
|
2866
|
+
const size = (await stat(filePath)).size;
|
|
2867
|
+
await unlink(filePath);
|
|
2868
|
+
return { deletedBytes: size };
|
|
2869
|
+
}
|
|
2870
|
+
} catch (err) {
|
|
2871
|
+
console.error("[agentmemory] Failed to delete image context:", err);
|
|
2872
|
+
}
|
|
2873
|
+
return { deletedBytes: 0 };
|
|
2874
|
+
}
|
|
2875
|
+
/** Touch an image file to update its mtime (marking it as recently used for LRU eviction) */
|
|
2876
|
+
async function touchImage(filePath) {
|
|
2877
|
+
if (!filePath || !isManagedImagePath(filePath)) return;
|
|
2878
|
+
try {
|
|
2879
|
+
if (existsSync(filePath)) {
|
|
2880
|
+
const now = /* @__PURE__ */ new Date();
|
|
2881
|
+
await utimes(filePath, now, now);
|
|
2882
|
+
}
|
|
2883
|
+
} catch (err) {}
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
//#endregion
|
|
2887
|
+
//#region src/functions/image-refs.ts
|
|
2888
|
+
var image_refs_exports = /* @__PURE__ */ __exportAll({
|
|
2889
|
+
decrementImageRef: () => decrementImageRef,
|
|
2890
|
+
getImageRefCount: () => getImageRefCount,
|
|
2891
|
+
incrementImageRef: () => incrementImageRef
|
|
2892
|
+
});
|
|
2893
|
+
async function getImageRefCount(kv, filePath) {
|
|
2894
|
+
const count = await kv.get(KV.imageRefs, filePath);
|
|
2895
|
+
return count ? Number(count) : 0;
|
|
2896
|
+
}
|
|
2897
|
+
async function incrementImageRef(kv, filePath) {
|
|
2898
|
+
return withKeyedLock(`imgRef:${filePath}`, async () => {
|
|
2899
|
+
const current = await getImageRefCount(kv, filePath);
|
|
2900
|
+
await kv.set(KV.imageRefs, filePath, current + 1);
|
|
2901
|
+
await touchImage(filePath);
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
async function decrementImageRef(kv, sdk, filePath) {
|
|
2905
|
+
return withKeyedLock(`imgRef:${filePath}`, async () => {
|
|
2906
|
+
const current = await getImageRefCount(kv, filePath);
|
|
2907
|
+
if (current <= 1) {
|
|
2908
|
+
await kv.delete(KV.imageEmbeddings, filePath);
|
|
2909
|
+
await kv.delete(KV.imageRefs, filePath);
|
|
2910
|
+
const { deletedBytes } = await deleteImage(filePath);
|
|
2911
|
+
if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
|
|
2912
|
+
} else await kv.set(KV.imageRefs, filePath, current - 1);
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
//#endregion
|
|
2917
|
+
//#region src/functions/image-quota-cleanup.ts
|
|
2918
|
+
const GRACE_PERIOD_MS = 3e4;
|
|
2919
|
+
function registerImageQuotaCleanup(sdk, kv) {
|
|
2920
|
+
sdk.registerFunction("mem::image-quota-cleanup", async () => {
|
|
2921
|
+
const now = Date.now();
|
|
2922
|
+
return withKeyedLock("system:cleanupLock", async () => {
|
|
2923
|
+
let totalSize = 0;
|
|
2924
|
+
const fileStats = [];
|
|
2925
|
+
try {
|
|
2926
|
+
const files = await readdir(IMAGES_DIR);
|
|
2927
|
+
for (const file of files) {
|
|
2928
|
+
if (file.startsWith(".")) continue;
|
|
2929
|
+
const filePath = join(IMAGES_DIR, file);
|
|
2930
|
+
const s = await stat(filePath);
|
|
2931
|
+
if (s.isFile()) {
|
|
2932
|
+
fileStats.push({
|
|
2933
|
+
filePath,
|
|
2934
|
+
size: s.size,
|
|
2935
|
+
mtimeMs: s.mtimeMs
|
|
2936
|
+
});
|
|
2937
|
+
totalSize += s.size;
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
} catch {
|
|
2941
|
+
return {
|
|
2942
|
+
success: true,
|
|
2943
|
+
evicted: 0,
|
|
2944
|
+
freedBytes: 0
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
const limit = getMaxBytes();
|
|
2948
|
+
if (totalSize <= limit) return {
|
|
2949
|
+
success: true,
|
|
2950
|
+
evicted: 0,
|
|
2951
|
+
freedBytes: 0,
|
|
2952
|
+
underQuota: true
|
|
2953
|
+
};
|
|
2954
|
+
fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2955
|
+
let totalToFree = totalSize - limit;
|
|
2956
|
+
let evicted = 0;
|
|
2957
|
+
let freedBytes = 0;
|
|
2958
|
+
for (const f of fileStats) {
|
|
2959
|
+
if (totalToFree <= 0) break;
|
|
2960
|
+
if (now - f.mtimeMs < GRACE_PERIOD_MS) continue;
|
|
2961
|
+
await withKeyedLock(`imgRef:${f.filePath}`, async () => {
|
|
2962
|
+
let refCount;
|
|
2963
|
+
try {
|
|
2964
|
+
refCount = await getImageRefCount(kv, f.filePath);
|
|
2965
|
+
} catch (err) {
|
|
2966
|
+
logger.error("Failed to read refCount; skipping eviction", {
|
|
2967
|
+
filePath: f.filePath,
|
|
2968
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2969
|
+
});
|
|
2970
|
+
return;
|
|
2971
|
+
}
|
|
2972
|
+
if (refCount > 0) return;
|
|
2973
|
+
const { deletedBytes } = await deleteImage(f.filePath);
|
|
2974
|
+
if (deletedBytes > 0) {
|
|
2975
|
+
sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
|
|
2976
|
+
totalToFree -= deletedBytes;
|
|
2977
|
+
freedBytes += deletedBytes;
|
|
2978
|
+
evicted++;
|
|
2979
|
+
}
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2982
|
+
if (evicted > 0) {
|
|
2983
|
+
const freedMb = (freedBytes / (1024 * 1024)).toFixed(1);
|
|
2984
|
+
logger.info("Image quota cleanup complete", {
|
|
2985
|
+
evicted,
|
|
2986
|
+
freedMb
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
return {
|
|
2990
|
+
success: true,
|
|
2991
|
+
evicted,
|
|
2992
|
+
freedBytes
|
|
2993
|
+
};
|
|
2994
|
+
});
|
|
2995
|
+
});
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
//#endregion
|
|
2999
|
+
//#region src/functions/audit.ts
|
|
3000
|
+
async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
|
|
3001
|
+
const entry = {
|
|
3002
|
+
id: generateId("aud"),
|
|
3003
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3004
|
+
operation,
|
|
3005
|
+
userId,
|
|
3006
|
+
functionId,
|
|
3007
|
+
targetIds,
|
|
3008
|
+
details,
|
|
3009
|
+
qualityScore
|
|
3010
|
+
};
|
|
3011
|
+
await kv.set(KV.audit, entry.id, entry);
|
|
3012
|
+
return entry;
|
|
3013
|
+
}
|
|
3014
|
+
async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
|
|
3015
|
+
try {
|
|
3016
|
+
await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
|
|
3017
|
+
} catch (err) {
|
|
3018
|
+
try {
|
|
3019
|
+
logger.warn("audit write failed", {
|
|
3020
|
+
functionId,
|
|
3021
|
+
operation,
|
|
3022
|
+
targetIds,
|
|
3023
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3024
|
+
});
|
|
3025
|
+
} catch {}
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
async function queryAudit(kv, filter) {
|
|
3029
|
+
let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
3030
|
+
if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
|
|
3031
|
+
if (filter?.dateFrom) {
|
|
3032
|
+
const from = new Date(filter.dateFrom).getTime();
|
|
3033
|
+
if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
|
|
3034
|
+
entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
|
|
3035
|
+
}
|
|
3036
|
+
if (filter?.dateTo) {
|
|
3037
|
+
const to = new Date(filter.dateTo).getTime();
|
|
3038
|
+
if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
|
|
3039
|
+
entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
|
|
3040
|
+
}
|
|
3041
|
+
return entries.slice(0, filter?.limit || 100);
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
//#endregion
|
|
3045
|
+
//#region src/functions/vision-search.ts
|
|
3046
|
+
function registerVisionSearchFunctions(sdk, kv, imageProvider) {
|
|
3047
|
+
sdk.registerFunction("mem::vision-embed", async (data) => {
|
|
3048
|
+
if (!imageProvider?.embedImage) return {
|
|
3049
|
+
success: false,
|
|
3050
|
+
error: "image embeddings disabled (set AGENTMEMORY_IMAGE_EMBEDDINGS=true)"
|
|
3051
|
+
};
|
|
3052
|
+
if (!data?.imageRef || typeof data.imageRef !== "string") return {
|
|
3053
|
+
success: false,
|
|
3054
|
+
error: "imageRef required"
|
|
3055
|
+
};
|
|
3056
|
+
if (!isManagedImagePath(data.imageRef)) return {
|
|
3057
|
+
success: false,
|
|
3058
|
+
error: "imageRef must point to a file under the managed image store"
|
|
3059
|
+
};
|
|
3060
|
+
const refCount = await kv.get(KV.imageRefs, data.imageRef);
|
|
3061
|
+
if (!refCount || Number(refCount) < 1) return {
|
|
3062
|
+
success: false,
|
|
3063
|
+
error: "imageRef not registered in mem:image-refs"
|
|
3064
|
+
};
|
|
3065
|
+
try {
|
|
3066
|
+
const vec = await imageProvider.embedImage(data.imageRef);
|
|
3067
|
+
const stored = {
|
|
3068
|
+
imageRef: data.imageRef,
|
|
3069
|
+
vector: Array.from(vec),
|
|
3070
|
+
modelName: imageProvider.name,
|
|
3071
|
+
dimensions: imageProvider.dimensions,
|
|
3072
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3073
|
+
sessionId: data.sessionId,
|
|
3074
|
+
observationId: data.observationId
|
|
3075
|
+
};
|
|
3076
|
+
await kv.set(KV.imageEmbeddings, data.imageRef, stored);
|
|
3077
|
+
await recordAudit(kv, "vision_embed", "mem::vision-embed", [data.imageRef], {
|
|
3078
|
+
modelName: imageProvider.name,
|
|
3079
|
+
dimensions: stored.dimensions,
|
|
3080
|
+
sessionId: data.sessionId,
|
|
3081
|
+
observationId: data.observationId
|
|
3082
|
+
});
|
|
3083
|
+
return {
|
|
3084
|
+
success: true,
|
|
3085
|
+
imageRef: data.imageRef,
|
|
3086
|
+
dimensions: stored.dimensions
|
|
3087
|
+
};
|
|
3088
|
+
} catch (err) {
|
|
3089
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3090
|
+
logger.warn("vision-embed failed", {
|
|
3091
|
+
imageRef: data.imageRef,
|
|
3092
|
+
error: msg
|
|
3093
|
+
});
|
|
3094
|
+
return {
|
|
3095
|
+
success: false,
|
|
3096
|
+
error: msg
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
});
|
|
3100
|
+
sdk.registerFunction("mem::vision-search", async (data) => {
|
|
3101
|
+
if (!imageProvider?.embedImage) return {
|
|
3102
|
+
success: false,
|
|
3103
|
+
error: "image embeddings disabled (set AGENTMEMORY_IMAGE_EMBEDDINGS=true)"
|
|
3104
|
+
};
|
|
3105
|
+
const requestedTopK = typeof data?.topK === "number" && Number.isFinite(data.topK) ? Math.trunc(data.topK) : 10;
|
|
3106
|
+
const topK = Math.min(50, Math.max(1, requestedTopK));
|
|
3107
|
+
let queryVec = null;
|
|
3108
|
+
try {
|
|
3109
|
+
if (data?.queryText) queryVec = await imageProvider.embed(data.queryText);
|
|
3110
|
+
else if (data?.queryImageBase64) {
|
|
3111
|
+
const b64 = data.queryImageBase64.startsWith("data:") ? data.queryImageBase64 : `data:image/png;base64,${data.queryImageBase64}`;
|
|
3112
|
+
queryVec = await imageProvider.embedImage(b64);
|
|
3113
|
+
} else if (data?.queryImageRef) {
|
|
3114
|
+
if (!isManagedImagePath(data.queryImageRef)) return {
|
|
3115
|
+
success: false,
|
|
3116
|
+
error: "queryImageRef must point to a file under the managed image store"
|
|
3117
|
+
};
|
|
3118
|
+
const refCount = await kv.get(KV.imageRefs, data.queryImageRef);
|
|
3119
|
+
if (!refCount || Number(refCount) < 1) return {
|
|
3120
|
+
success: false,
|
|
3121
|
+
error: "queryImageRef not registered in mem:image-refs"
|
|
3122
|
+
};
|
|
3123
|
+
queryVec = await imageProvider.embedImage(data.queryImageRef);
|
|
3124
|
+
} else return {
|
|
3125
|
+
success: false,
|
|
3126
|
+
error: "queryText, queryImageRef, or queryImageBase64 required"
|
|
3127
|
+
};
|
|
3128
|
+
} catch (err) {
|
|
3129
|
+
return {
|
|
3130
|
+
success: false,
|
|
3131
|
+
error: `query embed failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
if (!queryVec) return {
|
|
3135
|
+
success: false,
|
|
3136
|
+
error: "failed to build query vector"
|
|
3137
|
+
};
|
|
3138
|
+
const stored = await kv.list(KV.imageEmbeddings);
|
|
3139
|
+
const scored = (data?.sessionId ? stored.filter((s) => s.sessionId === data.sessionId) : stored).map((s) => ({
|
|
3140
|
+
imageRef: s.imageRef,
|
|
3141
|
+
score: cosine(queryVec, s.vector),
|
|
3142
|
+
sessionId: s.sessionId,
|
|
3143
|
+
observationId: s.observationId,
|
|
3144
|
+
updatedAt: s.updatedAt
|
|
3145
|
+
}));
|
|
3146
|
+
scored.sort((a, b) => b.score - a.score);
|
|
3147
|
+
return {
|
|
3148
|
+
success: true,
|
|
3149
|
+
results: scored.slice(0, topK),
|
|
3150
|
+
total: scored.length
|
|
3151
|
+
};
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
function cosine(a, b) {
|
|
3155
|
+
if (a.length !== b.length) return 0;
|
|
3156
|
+
let dot = 0;
|
|
3157
|
+
let normA = 0;
|
|
3158
|
+
let normB = 0;
|
|
3159
|
+
for (let i = 0; i < a.length; i++) {
|
|
3160
|
+
dot += a[i] * b[i];
|
|
3161
|
+
normA += a[i] * a[i];
|
|
3162
|
+
normB += b[i] * b[i];
|
|
3163
|
+
}
|
|
3164
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
3165
|
+
return denom === 0 ? 0 : dot / denom;
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
//#endregion
|
|
3169
|
+
//#region src/functions/slots.ts
|
|
3170
|
+
const DEFAULT_SIZE_LIMIT = 2e3;
|
|
3171
|
+
const DEFAULT_SLOTS = [
|
|
3172
|
+
{
|
|
3173
|
+
label: "persona",
|
|
3174
|
+
content: "",
|
|
3175
|
+
sizeLimit: 1e3,
|
|
3176
|
+
description: "How the agent should see itself: role, tone, behavioural guidelines.",
|
|
3177
|
+
pinned: true,
|
|
3178
|
+
readOnly: false,
|
|
3179
|
+
scope: "global"
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
label: "user_preferences",
|
|
3183
|
+
content: "",
|
|
3184
|
+
sizeLimit: 2e3,
|
|
3185
|
+
description: "Coding style, tool preferences, naming conventions, and other habits the user wants preserved across sessions.",
|
|
3186
|
+
pinned: true,
|
|
3187
|
+
readOnly: false,
|
|
3188
|
+
scope: "global"
|
|
3189
|
+
},
|
|
3190
|
+
{
|
|
3191
|
+
label: "tool_guidelines",
|
|
3192
|
+
content: "",
|
|
3193
|
+
sizeLimit: 1500,
|
|
3194
|
+
description: "Rules the agent should follow when picking or sequencing tools (e.g. prefer X over Y, never run Z without confirmation).",
|
|
3195
|
+
pinned: true,
|
|
3196
|
+
readOnly: false,
|
|
3197
|
+
scope: "global"
|
|
3198
|
+
},
|
|
3199
|
+
{
|
|
3200
|
+
label: "project_context",
|
|
3201
|
+
content: "",
|
|
3202
|
+
sizeLimit: 3e3,
|
|
3203
|
+
description: "Architecture decisions, codebase conventions, build/test commands, and cross-cutting constraints for the current project.",
|
|
3204
|
+
pinned: true,
|
|
3205
|
+
readOnly: false,
|
|
3206
|
+
scope: "project"
|
|
3207
|
+
},
|
|
3208
|
+
{
|
|
3209
|
+
label: "guidance",
|
|
3210
|
+
content: "",
|
|
3211
|
+
sizeLimit: 1500,
|
|
3212
|
+
description: "Active advice for the next session: what to focus on, what to avoid, open risks.",
|
|
3213
|
+
pinned: true,
|
|
3214
|
+
readOnly: false,
|
|
3215
|
+
scope: "project"
|
|
3216
|
+
},
|
|
3217
|
+
{
|
|
3218
|
+
label: "pending_items",
|
|
3219
|
+
content: "",
|
|
3220
|
+
sizeLimit: 2e3,
|
|
3221
|
+
description: "Unfinished work, explicit TODOs, and promises made but not yet delivered.",
|
|
3222
|
+
pinned: true,
|
|
3223
|
+
readOnly: false,
|
|
3224
|
+
scope: "project"
|
|
3225
|
+
},
|
|
3226
|
+
{
|
|
3227
|
+
label: "session_patterns",
|
|
3228
|
+
content: "",
|
|
3229
|
+
sizeLimit: 1500,
|
|
3230
|
+
description: "Recurring behaviours and common struggles observed across recent sessions.",
|
|
3231
|
+
pinned: false,
|
|
3232
|
+
readOnly: false,
|
|
3233
|
+
scope: "project"
|
|
3234
|
+
},
|
|
3235
|
+
{
|
|
3236
|
+
label: "self_notes",
|
|
3237
|
+
content: "",
|
|
3238
|
+
sizeLimit: 1500,
|
|
3239
|
+
description: "Free-form notes the agent keeps for itself: hypotheses, dead ends, things to revisit.",
|
|
3240
|
+
pinned: false,
|
|
3241
|
+
readOnly: false,
|
|
3242
|
+
scope: "project"
|
|
3243
|
+
}
|
|
3244
|
+
];
|
|
3245
|
+
function isSlotsEnabled() {
|
|
3246
|
+
return process.env["AGENTMEMORY_SLOTS"] === "true";
|
|
3247
|
+
}
|
|
3248
|
+
function isReflectEnabled() {
|
|
3249
|
+
return process.env["AGENTMEMORY_REFLECT"] === "true";
|
|
3250
|
+
}
|
|
3251
|
+
function scopeKv(scope) {
|
|
3252
|
+
return scope === "global" ? KV.globalSlots : KV.slots;
|
|
3253
|
+
}
|
|
3254
|
+
function nowIso() {
|
|
3255
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3256
|
+
}
|
|
3257
|
+
function validateLabel(label) {
|
|
3258
|
+
if (typeof label !== "string") return null;
|
|
3259
|
+
const trimmed = label.trim();
|
|
3260
|
+
if (!trimmed || trimmed.length > 64) return null;
|
|
3261
|
+
if (!/^[a-z][a-z0-9_]*$/.test(trimmed)) return null;
|
|
3262
|
+
return trimmed;
|
|
3263
|
+
}
|
|
3264
|
+
async function readSlot(kv, label) {
|
|
3265
|
+
const project = await kv.get(KV.slots, label);
|
|
3266
|
+
if (project) return {
|
|
3267
|
+
slot: project,
|
|
3268
|
+
scope: "project"
|
|
3269
|
+
};
|
|
3270
|
+
const global = await kv.get(KV.globalSlots, label);
|
|
3271
|
+
if (global) return {
|
|
3272
|
+
slot: global,
|
|
3273
|
+
scope: "global"
|
|
3274
|
+
};
|
|
3275
|
+
return {
|
|
3276
|
+
slot: null,
|
|
3277
|
+
scope: "project"
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
async function readSlotInScope(kv, label, scope) {
|
|
3281
|
+
return kv.get(scopeKv(scope), label);
|
|
3282
|
+
}
|
|
3283
|
+
function validateScope(raw) {
|
|
3284
|
+
if (raw === void 0 || raw === null) return "project";
|
|
3285
|
+
if (raw === "project" || raw === "global") return raw;
|
|
3286
|
+
return null;
|
|
3287
|
+
}
|
|
3288
|
+
function validateSizeLimit(raw) {
|
|
3289
|
+
if (raw === void 0 || raw === null) return DEFAULT_SIZE_LIMIT;
|
|
3290
|
+
if (typeof raw !== "number") return null;
|
|
3291
|
+
if (!Number.isInteger(raw) || raw < 1 || raw > 2e4) return null;
|
|
3292
|
+
return raw;
|
|
3293
|
+
}
|
|
3294
|
+
async function seedDefaults(kv) {
|
|
3295
|
+
const ts = nowIso();
|
|
3296
|
+
for (const tmpl of DEFAULT_SLOTS) {
|
|
3297
|
+
const target = scopeKv(tmpl.scope);
|
|
3298
|
+
if (await kv.get(target, tmpl.label)) continue;
|
|
3299
|
+
const slot = {
|
|
3300
|
+
...tmpl,
|
|
3301
|
+
createdAt: ts,
|
|
3302
|
+
updatedAt: ts
|
|
3303
|
+
};
|
|
3304
|
+
await kv.set(target, tmpl.label, slot);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
function registerSlotsFunctions(sdk, kv) {
|
|
3308
|
+
seedDefaults(kv).catch((err) => {
|
|
3309
|
+
logger.warn("slot defaults seed failed", { error: err instanceof Error ? err.message : String(err) });
|
|
3310
|
+
});
|
|
3311
|
+
sdk.registerFunction("mem::slot-list", async () => {
|
|
3312
|
+
const [project, global] = await Promise.all([kv.list(KV.slots), kv.list(KV.globalSlots)]);
|
|
3313
|
+
const merged = /* @__PURE__ */ new Map();
|
|
3314
|
+
for (const s of global) merged.set(s.label, s);
|
|
3315
|
+
for (const s of project) merged.set(s.label, s);
|
|
3316
|
+
return {
|
|
3317
|
+
success: true,
|
|
3318
|
+
slots: Array.from(merged.values()).sort((a, b) => a.label.localeCompare(b.label))
|
|
3319
|
+
};
|
|
3320
|
+
});
|
|
3321
|
+
sdk.registerFunction("mem::slot-get", async (data) => {
|
|
3322
|
+
const label = validateLabel(data?.label);
|
|
3323
|
+
if (!label) return {
|
|
3324
|
+
success: false,
|
|
3325
|
+
error: "label required (lowercase, starts with letter, [a-z0-9_])"
|
|
3326
|
+
};
|
|
3327
|
+
const { slot, scope } = await readSlot(kv, label);
|
|
3328
|
+
if (!slot) return {
|
|
3329
|
+
success: false,
|
|
3330
|
+
error: "slot not found"
|
|
3331
|
+
};
|
|
3332
|
+
return {
|
|
3333
|
+
success: true,
|
|
3334
|
+
slot,
|
|
3335
|
+
scope
|
|
3336
|
+
};
|
|
3337
|
+
});
|
|
3338
|
+
sdk.registerFunction("mem::slot-create", async (data) => {
|
|
3339
|
+
const label = validateLabel(data?.label);
|
|
3340
|
+
if (!label) return {
|
|
3341
|
+
success: false,
|
|
3342
|
+
error: "label required (lowercase, starts with letter, [a-z0-9_])"
|
|
3343
|
+
};
|
|
3344
|
+
const scope = validateScope(data?.scope);
|
|
3345
|
+
if (!scope) return {
|
|
3346
|
+
success: false,
|
|
3347
|
+
error: "scope must be 'project' or 'global'"
|
|
3348
|
+
};
|
|
3349
|
+
const sizeLimit = validateSizeLimit(data?.sizeLimit);
|
|
3350
|
+
if (sizeLimit === null) return {
|
|
3351
|
+
success: false,
|
|
3352
|
+
error: "sizeLimit must be an integer between 1 and 20000"
|
|
3353
|
+
};
|
|
3354
|
+
const content = typeof data?.content === "string" ? data.content : "";
|
|
3355
|
+
if (content.length > sizeLimit) return {
|
|
3356
|
+
success: false,
|
|
3357
|
+
error: `content exceeds sizeLimit (${content.length} > ${sizeLimit})`
|
|
3358
|
+
};
|
|
3359
|
+
const description = typeof data?.description === "string" ? data.description : "";
|
|
3360
|
+
const pinned = typeof data?.pinned === "boolean" ? data.pinned : true;
|
|
3361
|
+
return withKeyedLock(`slot:${label}`, async () => {
|
|
3362
|
+
if (await readSlotInScope(kv, label, scope)) return {
|
|
3363
|
+
success: false,
|
|
3364
|
+
error: `slot already exists in ${scope} scope`
|
|
3365
|
+
};
|
|
3366
|
+
const ts = nowIso();
|
|
3367
|
+
const slot = {
|
|
3368
|
+
label,
|
|
3369
|
+
content,
|
|
3370
|
+
sizeLimit,
|
|
3371
|
+
description,
|
|
3372
|
+
pinned,
|
|
3373
|
+
readOnly: false,
|
|
3374
|
+
scope,
|
|
3375
|
+
createdAt: ts,
|
|
3376
|
+
updatedAt: ts
|
|
3377
|
+
};
|
|
3378
|
+
await kv.set(scopeKv(scope), label, slot);
|
|
3379
|
+
await recordAudit(kv, "slot_create", "mem::slot-create", [label], {
|
|
3380
|
+
scope,
|
|
3381
|
+
sizeLimit: slot.sizeLimit,
|
|
3382
|
+
pinned: slot.pinned
|
|
3383
|
+
});
|
|
3384
|
+
return {
|
|
3385
|
+
success: true,
|
|
3386
|
+
slot
|
|
3387
|
+
};
|
|
3388
|
+
});
|
|
3389
|
+
});
|
|
3390
|
+
sdk.registerFunction("mem::slot-append", async (data) => {
|
|
3391
|
+
const label = validateLabel(data?.label);
|
|
3392
|
+
if (!label) return {
|
|
3393
|
+
success: false,
|
|
3394
|
+
error: "label required"
|
|
3395
|
+
};
|
|
3396
|
+
const text = typeof data?.text === "string" ? data.text : "";
|
|
3397
|
+
if (!text) return {
|
|
3398
|
+
success: false,
|
|
3399
|
+
error: "text required"
|
|
3400
|
+
};
|
|
3401
|
+
return withKeyedLock(`slot:${label}`, async () => {
|
|
3402
|
+
const { slot, scope } = await readSlot(kv, label);
|
|
3403
|
+
if (!slot) return {
|
|
3404
|
+
success: false,
|
|
3405
|
+
error: "slot not found (use mem::slot-create first)"
|
|
3406
|
+
};
|
|
3407
|
+
if (slot.readOnly) return {
|
|
3408
|
+
success: false,
|
|
3409
|
+
error: "slot is read-only"
|
|
3410
|
+
};
|
|
3411
|
+
const sep = slot.content && !slot.content.endsWith("\n") ? "\n" : "";
|
|
3412
|
+
const next = `${slot.content}${sep}${text}`;
|
|
3413
|
+
if (next.length > slot.sizeLimit) return {
|
|
3414
|
+
success: false,
|
|
3415
|
+
error: `append would exceed sizeLimit (${next.length} > ${slot.sizeLimit}). Use mem::slot-replace to compact first.`,
|
|
3416
|
+
currentSize: slot.content.length,
|
|
3417
|
+
sizeLimit: slot.sizeLimit
|
|
3418
|
+
};
|
|
3419
|
+
const updated = {
|
|
3420
|
+
...slot,
|
|
3421
|
+
content: next,
|
|
3422
|
+
updatedAt: nowIso()
|
|
3423
|
+
};
|
|
3424
|
+
await kv.set(scopeKv(scope), label, updated);
|
|
3425
|
+
await recordAudit(kv, "slot_append", "mem::slot-append", [label], {
|
|
3426
|
+
scope,
|
|
3427
|
+
added: text.length,
|
|
3428
|
+
total: next.length
|
|
3429
|
+
});
|
|
3430
|
+
return {
|
|
3431
|
+
success: true,
|
|
3432
|
+
slot: updated,
|
|
3433
|
+
size: next.length
|
|
3434
|
+
};
|
|
3435
|
+
});
|
|
3436
|
+
});
|
|
3437
|
+
sdk.registerFunction("mem::slot-replace", async (data) => {
|
|
3438
|
+
const label = validateLabel(data?.label);
|
|
3439
|
+
if (!label) return {
|
|
3440
|
+
success: false,
|
|
3441
|
+
error: "label required"
|
|
3442
|
+
};
|
|
3443
|
+
if (typeof data?.content !== "string") return {
|
|
3444
|
+
success: false,
|
|
3445
|
+
error: "content required (string)"
|
|
3446
|
+
};
|
|
3447
|
+
return withKeyedLock(`slot:${label}`, async () => {
|
|
3448
|
+
const { slot, scope } = await readSlot(kv, label);
|
|
3449
|
+
if (!slot) return {
|
|
3450
|
+
success: false,
|
|
3451
|
+
error: "slot not found (use mem::slot-create first)"
|
|
3452
|
+
};
|
|
3453
|
+
if (slot.readOnly) return {
|
|
3454
|
+
success: false,
|
|
3455
|
+
error: "slot is read-only"
|
|
3456
|
+
};
|
|
3457
|
+
if (data.content.length > slot.sizeLimit) return {
|
|
3458
|
+
success: false,
|
|
3459
|
+
error: `content exceeds sizeLimit (${data.content.length} > ${slot.sizeLimit})`,
|
|
3460
|
+
sizeLimit: slot.sizeLimit
|
|
3461
|
+
};
|
|
3462
|
+
const updated = {
|
|
3463
|
+
...slot,
|
|
3464
|
+
content: data.content,
|
|
3465
|
+
updatedAt: nowIso()
|
|
3466
|
+
};
|
|
3467
|
+
await kv.set(scopeKv(scope), label, updated);
|
|
3468
|
+
await recordAudit(kv, "slot_replace", "mem::slot-replace", [label], {
|
|
3469
|
+
scope,
|
|
3470
|
+
before: slot.content.length,
|
|
3471
|
+
after: data.content.length
|
|
3472
|
+
});
|
|
3473
|
+
return {
|
|
3474
|
+
success: true,
|
|
3475
|
+
slot: updated,
|
|
3476
|
+
size: data.content.length
|
|
3477
|
+
};
|
|
3478
|
+
});
|
|
3479
|
+
});
|
|
3480
|
+
sdk.registerFunction("mem::slot-delete", async (data) => {
|
|
3481
|
+
const label = validateLabel(data?.label);
|
|
3482
|
+
if (!label) return {
|
|
3483
|
+
success: false,
|
|
3484
|
+
error: "label required"
|
|
3485
|
+
};
|
|
3486
|
+
return withKeyedLock(`slot:${label}`, async () => {
|
|
3487
|
+
const { slot, scope } = await readSlot(kv, label);
|
|
3488
|
+
if (!slot) return {
|
|
3489
|
+
success: false,
|
|
3490
|
+
error: "slot not found"
|
|
3491
|
+
};
|
|
3492
|
+
if (slot.readOnly) return {
|
|
3493
|
+
success: false,
|
|
3494
|
+
error: "slot is read-only"
|
|
3495
|
+
};
|
|
3496
|
+
await kv.delete(scopeKv(scope), label);
|
|
3497
|
+
await recordAudit(kv, "slot_delete", "mem::slot-delete", [label], {
|
|
3498
|
+
scope,
|
|
3499
|
+
size: slot.content.length
|
|
3500
|
+
});
|
|
3501
|
+
return { success: true };
|
|
3502
|
+
});
|
|
3503
|
+
});
|
|
3504
|
+
sdk.registerFunction("mem::slot-reflect", async (data) => {
|
|
3505
|
+
if (!data?.sessionId || typeof data.sessionId !== "string") return {
|
|
3506
|
+
success: false,
|
|
3507
|
+
error: "sessionId required"
|
|
3508
|
+
};
|
|
3509
|
+
const max = typeof data.maxObservations === "number" && Number.isInteger(data.maxObservations) && data.maxObservations > 0 ? Math.min(200, data.maxObservations) : 50;
|
|
3510
|
+
const observations = await kv.list(KV.observations(data.sessionId));
|
|
3511
|
+
if (observations.length === 0) return {
|
|
3512
|
+
success: true,
|
|
3513
|
+
applied: 0,
|
|
3514
|
+
reason: "no observations for session"
|
|
3515
|
+
};
|
|
3516
|
+
const recent = observations.slice().sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || "")).slice(0, max);
|
|
3517
|
+
const pendingLines = [];
|
|
3518
|
+
const patternCounts = /* @__PURE__ */ new Map();
|
|
3519
|
+
const files = /* @__PURE__ */ new Set();
|
|
3520
|
+
for (const obs of recent) {
|
|
3521
|
+
const title = (obs.title || "").toLowerCase();
|
|
3522
|
+
if ((obs.narrative || "").toLowerCase().includes("todo") || title.includes("todo")) pendingLines.push(`- ${obs.title || obs.id}`);
|
|
3523
|
+
if (obs.type === "error") patternCounts.set("errors", (patternCounts.get("errors") ?? 0) + 1);
|
|
3524
|
+
if (obs.type === "command_run") patternCounts.set("commands", (patternCounts.get("commands") ?? 0) + 1);
|
|
3525
|
+
if (obs.files) for (const f of obs.files) files.add(f);
|
|
3526
|
+
}
|
|
3527
|
+
let applied = 0;
|
|
3528
|
+
if (pendingLines.length > 0) {
|
|
3529
|
+
if (await withKeyedLock(`slot:pending_items`, async () => {
|
|
3530
|
+
const { slot, scope } = await readSlot(kv, "pending_items");
|
|
3531
|
+
if (!slot) return false;
|
|
3532
|
+
const already = new Set(slot.content.split("\n"));
|
|
3533
|
+
const fresh = pendingLines.filter((line) => !already.has(line));
|
|
3534
|
+
if (fresh.length === 0) return false;
|
|
3535
|
+
const sep = slot.content && !slot.content.endsWith("\n") ? "\n" : "";
|
|
3536
|
+
const next = `${slot.content}${sep}${fresh.join("\n")}`;
|
|
3537
|
+
const truncated = next.length > slot.sizeLimit ? next.slice(next.length - slot.sizeLimit) : next;
|
|
3538
|
+
await kv.set(scopeKv(scope), "pending_items", {
|
|
3539
|
+
...slot,
|
|
3540
|
+
content: truncated,
|
|
3541
|
+
updatedAt: nowIso()
|
|
3542
|
+
});
|
|
3543
|
+
return true;
|
|
3544
|
+
})) applied++;
|
|
3545
|
+
}
|
|
3546
|
+
if (patternCounts.size > 0) {
|
|
3547
|
+
if (await withKeyedLock(`slot:session_patterns`, async () => {
|
|
3548
|
+
const { slot, scope } = await readSlot(kv, "session_patterns");
|
|
3549
|
+
if (!slot) return false;
|
|
3550
|
+
const summary = [`last reflection: ${nowIso()}`, ...Array.from(patternCounts.entries()).map(([kind, count]) => `- ${kind}: ${count} in last ${recent.length} observations`)].join("\n");
|
|
3551
|
+
const next = summary.length > slot.sizeLimit ? summary.slice(0, slot.sizeLimit) : summary;
|
|
3552
|
+
await kv.set(scopeKv(scope), "session_patterns", {
|
|
3553
|
+
...slot,
|
|
3554
|
+
content: next,
|
|
3555
|
+
updatedAt: nowIso()
|
|
3556
|
+
});
|
|
3557
|
+
return true;
|
|
3558
|
+
})) applied++;
|
|
3559
|
+
}
|
|
3560
|
+
if (files.size > 0) {
|
|
3561
|
+
if (await withKeyedLock(`slot:project_context`, async () => {
|
|
3562
|
+
const { slot, scope } = await readSlot(kv, "project_context");
|
|
3563
|
+
if (!slot) return false;
|
|
3564
|
+
const already = slot.content;
|
|
3565
|
+
const fresh = Array.from(files).filter((f) => !already.includes(f)).slice(0, 20);
|
|
3566
|
+
if (fresh.length === 0) return false;
|
|
3567
|
+
const header = already.length === 0 ? "Files touched in recent sessions:" : "";
|
|
3568
|
+
const nextRaw = `${already}${already && !already.endsWith("\n") ? "\n" : ""}${header ? header + "\n" : ""}${fresh.map((f) => `- ${f}`).join("\n")}`;
|
|
3569
|
+
const next = nextRaw.length > slot.sizeLimit ? nextRaw.slice(nextRaw.length - slot.sizeLimit) : nextRaw;
|
|
3570
|
+
await kv.set(scopeKv(scope), "project_context", {
|
|
3571
|
+
...slot,
|
|
3572
|
+
content: next,
|
|
3573
|
+
updatedAt: nowIso()
|
|
3574
|
+
});
|
|
3575
|
+
return true;
|
|
3576
|
+
})) applied++;
|
|
3577
|
+
}
|
|
3578
|
+
if (applied > 0) await recordAudit(kv, "slot_reflect", "mem::slot-reflect", [data.sessionId], {
|
|
3579
|
+
observationCount: recent.length,
|
|
3580
|
+
slotsUpdated: applied
|
|
3581
|
+
});
|
|
3582
|
+
return {
|
|
3583
|
+
success: true,
|
|
3584
|
+
applied,
|
|
3585
|
+
observationsReviewed: recent.length
|
|
3586
|
+
};
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
//#endregion
|
|
3591
|
+
//#region src/functions/disk-size-manager.ts
|
|
3592
|
+
const DISK_SIZE_KEY = "system:currentDiskSize";
|
|
3593
|
+
function registerDiskSizeManager(sdk, kv) {
|
|
3594
|
+
sdk.registerFunction("mem::disk-size-delta", async (data) => {
|
|
3595
|
+
if (typeof data?.deltaBytes !== "number" || !isFinite(data.deltaBytes)) return {
|
|
3596
|
+
success: false,
|
|
3597
|
+
error: "deltaBytes must be a finite number"
|
|
3598
|
+
};
|
|
3599
|
+
return withKeyedLock(DISK_SIZE_KEY, async () => {
|
|
3600
|
+
let newTotal = (await kv.get(KV.state, DISK_SIZE_KEY) || 0) + data.deltaBytes;
|
|
3601
|
+
if (newTotal < 0) newTotal = 0;
|
|
3602
|
+
await kv.set(KV.state, DISK_SIZE_KEY, newTotal);
|
|
3603
|
+
if (data.deltaBytes > 0 && newTotal > getMaxBytes()) {
|
|
3604
|
+
sdk.triggerVoid("mem::image-quota-cleanup", {});
|
|
3605
|
+
logger.info("Disk quota exceeded, cleanup triggered", {
|
|
3606
|
+
currentBytes: newTotal,
|
|
3607
|
+
maxBytes: getMaxBytes()
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
return {
|
|
3611
|
+
success: true,
|
|
3612
|
+
currentTotal: newTotal
|
|
3613
|
+
};
|
|
3614
|
+
});
|
|
3615
|
+
});
|
|
3616
|
+
}
|
|
3617
|
+
|
|
2541
3618
|
//#endregion
|
|
2542
3619
|
//#region src/prompts/compression.ts
|
|
2543
3620
|
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.
|
|
@@ -2586,6 +3663,17 @@ function truncate$1(s, max) {
|
|
|
2586
3663
|
return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
|
|
2587
3664
|
}
|
|
2588
3665
|
|
|
3666
|
+
//#endregion
|
|
3667
|
+
//#region src/prompts/vision.ts
|
|
3668
|
+
const VISION_DESCRIPTION_PROMPT = `Describe what this image shows in the context of software development. Extract:
|
|
3669
|
+
- What type of image this is (screenshot, diagram, mockup, terminal output, error, etc.)
|
|
3670
|
+
- Key entities visible (files, components, UI elements, error messages)
|
|
3671
|
+
- Relationships or flow shown
|
|
3672
|
+
- Any decisions, errors, or state visible
|
|
3673
|
+
- Text content visible in the image
|
|
3674
|
+
|
|
3675
|
+
Be concise but preserve all technically relevant details. Output plain text, no XML.`;
|
|
3676
|
+
|
|
2589
3677
|
//#endregion
|
|
2590
3678
|
//#region src/prompts/xml.ts
|
|
2591
3679
|
const VALID_TAG = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
@@ -2819,6 +3907,7 @@ const VALID_TYPES$1 = new Set([
|
|
|
2819
3907
|
"subagent",
|
|
2820
3908
|
"notification",
|
|
2821
3909
|
"task",
|
|
3910
|
+
"image",
|
|
2822
3911
|
"other"
|
|
2823
3912
|
]);
|
|
2824
3913
|
function parseCompressionXml(xml) {
|
|
@@ -2839,11 +3928,32 @@ function parseCompressionXml(xml) {
|
|
|
2839
3928
|
function registerCompressFunction(sdk, kv, provider, metricsStore) {
|
|
2840
3929
|
sdk.registerFunction("mem::compress", async (data) => {
|
|
2841
3930
|
const startMs = Date.now();
|
|
3931
|
+
let imageDescription;
|
|
3932
|
+
const hasImage = data.raw.modality === "image" || data.raw.modality === "mixed";
|
|
3933
|
+
if (hasImage && data.raw.imageData && provider.describeImage) try {
|
|
3934
|
+
let base64Data = data.raw.imageData;
|
|
3935
|
+
let mimeType = "image/png";
|
|
3936
|
+
if (!data.raw.imageData.startsWith("/9j/") && !data.raw.imageData.startsWith("iVBOR")) {
|
|
3937
|
+
if (!isManagedImagePath(data.raw.imageData)) throw new Error(`Refusing to read image outside managed store: ${data.raw.imageData}`);
|
|
3938
|
+
base64Data = readFileSync(data.raw.imageData).toString("base64");
|
|
3939
|
+
if (data.raw.imageData.endsWith(".jpg") || data.raw.imageData.endsWith(".jpeg")) mimeType = "image/jpeg";
|
|
3940
|
+
else if (data.raw.imageData.endsWith(".webp")) mimeType = "image/webp";
|
|
3941
|
+
else if (data.raw.imageData.endsWith(".gif")) mimeType = "image/gif";
|
|
3942
|
+
}
|
|
3943
|
+
imageDescription = await provider.describeImage(base64Data, mimeType, VISION_DESCRIPTION_PROMPT);
|
|
3944
|
+
logger.info("Image described by vision model", { obsId: data.observationId });
|
|
3945
|
+
} catch (err) {
|
|
3946
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3947
|
+
logger.warn("Vision model call failed, falling back to text-only compression", {
|
|
3948
|
+
obsId: data.observationId,
|
|
3949
|
+
error: msg
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
2842
3952
|
const prompt = buildCompressionPrompt({
|
|
2843
3953
|
hookType: data.raw.hookType,
|
|
2844
3954
|
toolName: data.raw.toolName,
|
|
2845
3955
|
toolInput: data.raw.toolInput,
|
|
2846
|
-
toolOutput: data.raw.toolOutput,
|
|
3956
|
+
toolOutput: imageDescription ? `[Image Description]: ${imageDescription}\n\n${data.raw.toolOutput ?? ""}` : data.raw.toolOutput,
|
|
2847
3957
|
userPrompt: data.raw.userPrompt,
|
|
2848
3958
|
timestamp: data.raw.timestamp
|
|
2849
3959
|
});
|
|
@@ -2880,7 +3990,10 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
|
|
|
2880
3990
|
sessionId: data.sessionId,
|
|
2881
3991
|
timestamp: data.raw.timestamp,
|
|
2882
3992
|
...parsed,
|
|
2883
|
-
confidence: qualityScore / 100
|
|
3993
|
+
confidence: qualityScore / 100,
|
|
3994
|
+
...hasImage ? { modality: data.raw.modality } : {},
|
|
3995
|
+
...imageDescription ? { imageDescription } : {},
|
|
3996
|
+
...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
|
|
2884
3997
|
};
|
|
2885
3998
|
await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
|
|
2886
3999
|
getSearchIndex().add(compressed);
|
|
@@ -3074,52 +4187,6 @@ function buildSummaryPrompt(observations) {
|
|
|
3074
4187
|
return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
|
|
3075
4188
|
}
|
|
3076
4189
|
|
|
3077
|
-
//#endregion
|
|
3078
|
-
//#region src/functions/audit.ts
|
|
3079
|
-
async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
|
|
3080
|
-
const entry = {
|
|
3081
|
-
id: generateId("aud"),
|
|
3082
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3083
|
-
operation,
|
|
3084
|
-
userId,
|
|
3085
|
-
functionId,
|
|
3086
|
-
targetIds,
|
|
3087
|
-
details,
|
|
3088
|
-
qualityScore
|
|
3089
|
-
};
|
|
3090
|
-
await kv.set(KV.audit, entry.id, entry);
|
|
3091
|
-
return entry;
|
|
3092
|
-
}
|
|
3093
|
-
async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
|
|
3094
|
-
try {
|
|
3095
|
-
await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
|
|
3096
|
-
} catch (err) {
|
|
3097
|
-
try {
|
|
3098
|
-
logger.warn("audit write failed", {
|
|
3099
|
-
functionId,
|
|
3100
|
-
operation,
|
|
3101
|
-
targetIds,
|
|
3102
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3103
|
-
});
|
|
3104
|
-
} catch {}
|
|
3105
|
-
}
|
|
3106
|
-
}
|
|
3107
|
-
async function queryAudit(kv, filter) {
|
|
3108
|
-
let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
3109
|
-
if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
|
|
3110
|
-
if (filter?.dateFrom) {
|
|
3111
|
-
const from = new Date(filter.dateFrom).getTime();
|
|
3112
|
-
if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
|
|
3113
|
-
entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
|
|
3114
|
-
}
|
|
3115
|
-
if (filter?.dateTo) {
|
|
3116
|
-
const to = new Date(filter.dateTo).getTime();
|
|
3117
|
-
if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
|
|
3118
|
-
entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
|
|
3119
|
-
}
|
|
3120
|
-
return entries.slice(0, filter?.limit || 100);
|
|
3121
|
-
}
|
|
3122
|
-
|
|
3123
4190
|
//#endregion
|
|
3124
4191
|
//#region src/functions/summarize.ts
|
|
3125
4192
|
function parseSummaryXml(xml, sessionId, project, obsCount) {
|
|
@@ -3161,9 +4228,33 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
|
|
|
3161
4228
|
error: "no_observations"
|
|
3162
4229
|
};
|
|
3163
4230
|
}
|
|
4231
|
+
if (provider.name === "noop") {
|
|
4232
|
+
logger.info("Summarize skipped — no LLM provider configured", { sessionId });
|
|
4233
|
+
return {
|
|
4234
|
+
success: false,
|
|
4235
|
+
error: "no_provider",
|
|
4236
|
+
reason: "No LLM provider key set; Summarize is a no-op. Set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env to enable."
|
|
4237
|
+
};
|
|
4238
|
+
}
|
|
3164
4239
|
try {
|
|
3165
4240
|
const prompt = buildSummaryPrompt(compressed);
|
|
3166
|
-
const
|
|
4241
|
+
const response = await provider.summarize(SUMMARY_SYSTEM, prompt);
|
|
4242
|
+
if (!response || !response.trim()) {
|
|
4243
|
+
const latencyMs = Date.now() - startMs;
|
|
4244
|
+
if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
|
|
4245
|
+
logger.warn("Empty provider response on summarize", {
|
|
4246
|
+
sessionId,
|
|
4247
|
+
provider: provider.name,
|
|
4248
|
+
promptBytes: prompt.length,
|
|
4249
|
+
systemBytes: SUMMARY_SYSTEM.length,
|
|
4250
|
+
observationCount: compressed.length
|
|
4251
|
+
});
|
|
4252
|
+
return {
|
|
4253
|
+
success: false,
|
|
4254
|
+
error: "empty_provider_response"
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
const summary = parseSummaryXml(response, sessionId, session.project, compressed.length);
|
|
3167
4258
|
if (!summary) {
|
|
3168
4259
|
const latencyMs = Date.now() - startMs;
|
|
3169
4260
|
if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
|
|
@@ -3750,14 +4841,20 @@ function registerRememberFunction(sdk, kv) {
|
|
|
3750
4841
|
const deletedMemoryIds = [];
|
|
3751
4842
|
const deletedObservationIds = [];
|
|
3752
4843
|
let deletedSession = false;
|
|
4844
|
+
const { decrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
|
|
3753
4845
|
if (data.memoryId) {
|
|
4846
|
+
const mem = await kv.get(KV.memories, data.memoryId);
|
|
3754
4847
|
await kv.delete(KV.memories, data.memoryId);
|
|
4848
|
+
if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
3755
4849
|
await deleteAccessLog(kv, data.memoryId);
|
|
3756
4850
|
deletedMemoryIds.push(data.memoryId);
|
|
3757
4851
|
deleted++;
|
|
3758
4852
|
}
|
|
3759
4853
|
if (data.sessionId && data.observationIds && data.observationIds.length > 0) for (const obsId of data.observationIds) {
|
|
4854
|
+
const obs = await kv.get(KV.observations(data.sessionId), obsId);
|
|
3760
4855
|
await kv.delete(KV.observations(data.sessionId), obsId);
|
|
4856
|
+
if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
4857
|
+
if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
3761
4858
|
deletedObservationIds.push(obsId);
|
|
3762
4859
|
deleted++;
|
|
3763
4860
|
}
|
|
@@ -3765,6 +4862,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
3765
4862
|
const observations = await kv.list(KV.observations(data.sessionId));
|
|
3766
4863
|
for (const obs of observations) {
|
|
3767
4864
|
await kv.delete(KV.observations(data.sessionId), obs.id);
|
|
4865
|
+
if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
4866
|
+
if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
3768
4867
|
deletedObservationIds.push(obs.id);
|
|
3769
4868
|
deleted++;
|
|
3770
4869
|
}
|
|
@@ -3801,6 +4900,7 @@ const DEFAULTS$1 = {
|
|
|
3801
4900
|
function registerEvictFunction(sdk, kv) {
|
|
3802
4901
|
sdk.registerFunction("mem::evict", async (data) => {
|
|
3803
4902
|
const dryRun = data?.dryRun ?? false;
|
|
4903
|
+
const { decrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
|
|
3804
4904
|
const configOverride = await kv.get(KV.config, "eviction").catch(() => null);
|
|
3805
4905
|
const cfg = {
|
|
3806
4906
|
...DEFAULTS$1,
|
|
@@ -3859,6 +4959,8 @@ function registerEvictFunction(sdk, kv) {
|
|
|
3859
4959
|
});
|
|
3860
4960
|
continue;
|
|
3861
4961
|
}
|
|
4962
|
+
if (o.imageData) await decrementImageRef(kv, sdk, o.imageData);
|
|
4963
|
+
if (o.imageRef && o.imageRef !== o.imageData) await decrementImageRef(kv, sdk, o.imageRef);
|
|
3862
4964
|
await recordAudit(kv, "delete", "mem::evict", [o.id], {
|
|
3863
4965
|
resource: "observation",
|
|
3864
4966
|
reason: "low_importance_old_observation",
|
|
@@ -3888,6 +4990,8 @@ function registerEvictFunction(sdk, kv) {
|
|
|
3888
4990
|
});
|
|
3889
4991
|
continue;
|
|
3890
4992
|
}
|
|
4993
|
+
if (o.imageData) await decrementImageRef(kv, sdk, o.imageData);
|
|
4994
|
+
if (o.imageRef && o.imageRef !== o.imageData) await decrementImageRef(kv, sdk, o.imageRef);
|
|
3891
4995
|
await recordAudit(kv, "delete", "mem::evict", [o.id], {
|
|
3892
4996
|
resource: "observation",
|
|
3893
4997
|
reason: "project_observation_cap",
|
|
@@ -3917,6 +5021,7 @@ function registerEvictFunction(sdk, kv) {
|
|
|
3917
5021
|
});
|
|
3918
5022
|
continue;
|
|
3919
5023
|
}
|
|
5024
|
+
if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
3920
5025
|
await recordAudit(kv, "delete", "mem::evict", [mem.id], {
|
|
3921
5026
|
resource: "memory",
|
|
3922
5027
|
reason: "expired_memory",
|
|
@@ -3940,6 +5045,7 @@ function registerEvictFunction(sdk, kv) {
|
|
|
3940
5045
|
});
|
|
3941
5046
|
continue;
|
|
3942
5047
|
}
|
|
5048
|
+
if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
3943
5049
|
await recordAudit(kv, "delete", "mem::evict", [mem.id], {
|
|
3944
5050
|
resource: "memory",
|
|
3945
5051
|
reason: "old_non_latest_memory",
|
|
@@ -4397,6 +5503,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
4397
5503
|
sdk.registerFunction("mem::auto-forget", async (data) => {
|
|
4398
5504
|
const dryRun = data?.dryRun ?? false;
|
|
4399
5505
|
const now = Date.now();
|
|
5506
|
+
const { decrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
|
|
4400
5507
|
const result = {
|
|
4401
5508
|
ttlExpired: [],
|
|
4402
5509
|
contradictions: [],
|
|
@@ -4410,6 +5517,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
4410
5517
|
result.ttlExpired.push(mem.id);
|
|
4411
5518
|
deletedIds.add(mem.id);
|
|
4412
5519
|
if (!dryRun) {
|
|
5520
|
+
if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
4413
5521
|
await kv.delete(KV.memories, mem.id);
|
|
4414
5522
|
await recordAudit(kv, "delete", "mem::auto-forget", [mem.id], {
|
|
4415
5523
|
resource: "memory",
|
|
@@ -4478,13 +5586,23 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
4478
5586
|
if (now - new Date(obs.timestamp).getTime() > 180 * MS_PER_DAY && (obs.importance ?? 5) <= 2) {
|
|
4479
5587
|
result.lowValueObs.push(obs.id);
|
|
4480
5588
|
if (!dryRun) {
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
}
|
|
5589
|
+
let deletedOk = false;
|
|
5590
|
+
try {
|
|
5591
|
+
await kv.delete(KV.observations(sessions[i].id), obs.id);
|
|
5592
|
+
deletedOk = true;
|
|
5593
|
+
} catch {
|
|
5594
|
+
deletedOk = false;
|
|
5595
|
+
}
|
|
5596
|
+
if (deletedOk) {
|
|
5597
|
+
if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
5598
|
+
if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
5599
|
+
await recordAudit(kv, "delete", "mem::auto-forget", [obs.id], {
|
|
5600
|
+
resource: "observation",
|
|
5601
|
+
reason: "auto-forget low-value observation",
|
|
5602
|
+
sessionId: sessions[i].id,
|
|
5603
|
+
timestamp: obs.timestamp
|
|
5604
|
+
});
|
|
5605
|
+
}
|
|
4488
5606
|
}
|
|
4489
5607
|
}
|
|
4490
5608
|
}
|
|
@@ -4500,7 +5618,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
4500
5618
|
|
|
4501
5619
|
//#endregion
|
|
4502
5620
|
//#region src/version.ts
|
|
4503
|
-
const VERSION = "0.9.
|
|
5621
|
+
const VERSION = "0.9.2";
|
|
4504
5622
|
|
|
4505
5623
|
//#endregion
|
|
4506
5624
|
//#region src/functions/export-import.ts
|
|
@@ -4618,7 +5736,9 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
4618
5736
|
"0.8.11",
|
|
4619
5737
|
"0.8.12",
|
|
4620
5738
|
"0.8.13",
|
|
4621
|
-
"0.9.0"
|
|
5739
|
+
"0.9.0",
|
|
5740
|
+
"0.9.1",
|
|
5741
|
+
"0.9.2"
|
|
4622
5742
|
]).has(importData.version)) return {
|
|
4623
5743
|
success: false,
|
|
4624
5744
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -10975,6 +12095,7 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
10975
12095
|
const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
|
|
10976
12096
|
const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
|
|
10977
12097
|
const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
|
|
12098
|
+
const { decrementImageRef } = await Promise.resolve().then(() => image_refs_exports);
|
|
10978
12099
|
const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
|
|
10979
12100
|
if (data?.dryRun) return {
|
|
10980
12101
|
success: true,
|
|
@@ -11006,6 +12127,8 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
11006
12127
|
resolvedSource = "semantic";
|
|
11007
12128
|
}
|
|
11008
12129
|
if (!scope || !resolvedSource) continue;
|
|
12130
|
+
const mem = await kv.get(scope, candidate.memoryId);
|
|
12131
|
+
if (mem && mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
11009
12132
|
await kv.delete(scope, candidate.memoryId);
|
|
11010
12133
|
await kv.delete(KV.retentionScores, candidate.memoryId);
|
|
11011
12134
|
await deleteAccessLog(kv, candidate.memoryId);
|
|
@@ -11443,6 +12566,92 @@ function rawFromCompressed(obs) {
|
|
|
11443
12566
|
}
|
|
11444
12567
|
};
|
|
11445
12568
|
}
|
|
12569
|
+
const LESSON_PATTERNS = [/\b(always|never|don'?t|do not|make sure|remember to|note:|caveat:|warning:)\b[^.\n]{10,200}[.!\n]/gi, /\b(prefer|avoid)\s[^.\n]{10,200}[.!\n]/gi];
|
|
12570
|
+
async function deriveCrystalAndLessons(kv, sessionId, project, rawObs, compressed, firstPrompt) {
|
|
12571
|
+
if (rawObs.length === 0) return;
|
|
12572
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12573
|
+
const files = /* @__PURE__ */ new Set();
|
|
12574
|
+
const tools = /* @__PURE__ */ new Set();
|
|
12575
|
+
for (const c of compressed) {
|
|
12576
|
+
for (const f of c.files || []) files.add(f);
|
|
12577
|
+
if (c.type && c.type !== "conversation" && c.title) tools.add(c.title);
|
|
12578
|
+
}
|
|
12579
|
+
const assistantTexts = [];
|
|
12580
|
+
const userPrompts = [];
|
|
12581
|
+
for (const r of rawObs) {
|
|
12582
|
+
if (typeof r.assistantResponse === "string" && r.assistantResponse.trim()) assistantTexts.push(r.assistantResponse);
|
|
12583
|
+
if (typeof r.userPrompt === "string" && r.userPrompt.trim()) userPrompts.push(r.userPrompt);
|
|
12584
|
+
}
|
|
12585
|
+
const lessonMatches = /* @__PURE__ */ new Map();
|
|
12586
|
+
for (const text of assistantTexts.concat(userPrompts).slice(0, 200)) for (const pat of LESSON_PATTERNS) {
|
|
12587
|
+
pat.lastIndex = 0;
|
|
12588
|
+
let m;
|
|
12589
|
+
while ((m = pat.exec(text)) !== null && lessonMatches.size < 40) {
|
|
12590
|
+
const snippet = m[0].replace(/\s+/g, " ").trim();
|
|
12591
|
+
if (snippet.length >= 20 && snippet.length <= 220) {
|
|
12592
|
+
const key = snippet.toLowerCase();
|
|
12593
|
+
if (!lessonMatches.has(key)) lessonMatches.set(key, snippet);
|
|
12594
|
+
}
|
|
12595
|
+
}
|
|
12596
|
+
}
|
|
12597
|
+
const lessonEntries = Array.from(lessonMatches.values()).slice(0, 20);
|
|
12598
|
+
const lessonIds = [];
|
|
12599
|
+
for (const content of lessonEntries) {
|
|
12600
|
+
const lessonId = fingerprintId("lesson", content.trim().toLowerCase());
|
|
12601
|
+
try {
|
|
12602
|
+
const existing = await kv.get(KV.lessons, lessonId);
|
|
12603
|
+
if (existing) {
|
|
12604
|
+
const existingSources = existing.sourceIds || [];
|
|
12605
|
+
const mergedSources = existingSources.includes(sessionId) ? existingSources : [...existingSources, sessionId];
|
|
12606
|
+
const existingTags = existing.tags || [];
|
|
12607
|
+
const mergedTags = existingTags.includes("auto-import") ? existingTags : [...existingTags, "auto-import"];
|
|
12608
|
+
const merged = {
|
|
12609
|
+
...existing,
|
|
12610
|
+
sourceIds: mergedSources,
|
|
12611
|
+
tags: mergedTags,
|
|
12612
|
+
reinforcements: (existing.reinforcements || 0) + 1,
|
|
12613
|
+
updatedAt: createdAt,
|
|
12614
|
+
lastReinforcedAt: createdAt
|
|
12615
|
+
};
|
|
12616
|
+
await kv.set(KV.lessons, lessonId, merged);
|
|
12617
|
+
} else {
|
|
12618
|
+
const lesson = {
|
|
12619
|
+
id: lessonId,
|
|
12620
|
+
content,
|
|
12621
|
+
context: firstPrompt || project,
|
|
12622
|
+
confidence: .4,
|
|
12623
|
+
reinforcements: 0,
|
|
12624
|
+
source: "consolidation",
|
|
12625
|
+
sourceIds: [sessionId],
|
|
12626
|
+
project,
|
|
12627
|
+
tags: ["auto-import"],
|
|
12628
|
+
createdAt,
|
|
12629
|
+
updatedAt: createdAt,
|
|
12630
|
+
decayRate: .05
|
|
12631
|
+
};
|
|
12632
|
+
await kv.set(KV.lessons, lessonId, lesson);
|
|
12633
|
+
}
|
|
12634
|
+
lessonIds.push(lessonId);
|
|
12635
|
+
} catch {}
|
|
12636
|
+
}
|
|
12637
|
+
const crystalId = fingerprintId("crystal", sessionId);
|
|
12638
|
+
const narrativePreview = firstPrompt ? firstPrompt.slice(0, 300) : compressed.slice(0, 5).map((c) => c.narrative || c.title).filter(Boolean).join(" · ").slice(0, 300);
|
|
12639
|
+
try {
|
|
12640
|
+
const existingCrystal = await kv.get(KV.crystals, crystalId);
|
|
12641
|
+
const crystal = {
|
|
12642
|
+
id: crystalId,
|
|
12643
|
+
narrative: narrativePreview || `Session ${sessionId.slice(0, 12)} (${rawObs.length} observations)`,
|
|
12644
|
+
keyOutcomes: Array.from(tools).slice(0, 8),
|
|
12645
|
+
filesAffected: Array.from(files).slice(0, 20),
|
|
12646
|
+
lessons: lessonIds,
|
|
12647
|
+
sourceActionIds: existingCrystal?.sourceActionIds ?? [],
|
|
12648
|
+
sessionId,
|
|
12649
|
+
project,
|
|
12650
|
+
createdAt: existingCrystal?.createdAt ?? createdAt
|
|
12651
|
+
};
|
|
12652
|
+
await kv.set(KV.crystals, crystalId, crystal);
|
|
12653
|
+
} catch {}
|
|
12654
|
+
}
|
|
11446
12655
|
function isRawShape(o) {
|
|
11447
12656
|
if (!o || typeof o !== "object") return false;
|
|
11448
12657
|
return typeof o.hookType === "string";
|
|
@@ -11553,6 +12762,8 @@ function registerReplayFunctions(sdk, kv) {
|
|
|
11553
12762
|
}
|
|
11554
12763
|
const parsed = parseJsonlText(text, generateId("sess"));
|
|
11555
12764
|
if (parsed.observations.length === 0) continue;
|
|
12765
|
+
const firstPromptObs = parsed.observations.find((o) => typeof o.userPrompt === "string" && o.userPrompt.trim().length > 0);
|
|
12766
|
+
const firstPrompt = firstPromptObs?.userPrompt ? firstPromptObs.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
|
|
11556
12767
|
const existing = await kv.get(KV.sessions, parsed.sessionId);
|
|
11557
12768
|
if (existing) {
|
|
11558
12769
|
existing.observationCount = (existing.observationCount || 0) + parsed.observations.length;
|
|
@@ -11560,6 +12771,7 @@ function registerReplayFunctions(sdk, kv) {
|
|
|
11560
12771
|
if (existing.status === "active") existing.status = "completed";
|
|
11561
12772
|
const existingTags = existing.tags || [];
|
|
11562
12773
|
if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
|
|
12774
|
+
if (!existing.firstPrompt && firstPrompt) existing.firstPrompt = firstPrompt;
|
|
11563
12775
|
await kv.set(KV.sessions, existing.id, existing);
|
|
11564
12776
|
} else {
|
|
11565
12777
|
const session = {
|
|
@@ -11570,13 +12782,22 @@ function registerReplayFunctions(sdk, kv) {
|
|
|
11570
12782
|
endedAt: parsed.endedAt,
|
|
11571
12783
|
status: "completed",
|
|
11572
12784
|
observationCount: parsed.observations.length,
|
|
11573
|
-
tags: ["jsonl-import"]
|
|
12785
|
+
tags: ["jsonl-import"],
|
|
12786
|
+
firstPrompt
|
|
11574
12787
|
};
|
|
11575
12788
|
await kv.set(KV.sessions, session.id, session);
|
|
11576
12789
|
}
|
|
11577
|
-
|
|
12790
|
+
const searchIndex = getSearchIndex();
|
|
12791
|
+
const compressed = [];
|
|
12792
|
+
await Promise.all(parsed.observations.map(async (obs) => {
|
|
12793
|
+
const synthetic = buildSyntheticCompression(obs);
|
|
12794
|
+
compressed.push(synthetic);
|
|
12795
|
+
await kv.set(KV.observations(parsed.sessionId), obs.id, synthetic);
|
|
12796
|
+
searchIndex.add(synthetic);
|
|
12797
|
+
}));
|
|
11578
12798
|
observationCount += parsed.observations.length;
|
|
11579
12799
|
sessionIds.push(parsed.sessionId);
|
|
12800
|
+
await deriveCrystalAndLessons(kv, parsed.sessionId, parsed.project, parsed.observations, compressed, firstPrompt);
|
|
11580
12801
|
}
|
|
11581
12802
|
await safeAudit(kv, "import", "mem::replay::import-jsonl", sessionIds, {
|
|
11582
12803
|
source: "jsonl",
|
|
@@ -11610,6 +12831,7 @@ function evaluateHealth(snapshot, config = {}) {
|
|
|
11610
12831
|
...config
|
|
11611
12832
|
};
|
|
11612
12833
|
const alerts = [];
|
|
12834
|
+
const notes = [];
|
|
11613
12835
|
let critical = false;
|
|
11614
12836
|
let degraded = false;
|
|
11615
12837
|
if (snapshot.connectionState === "disconnected" || snapshot.connectionState === "failed") {
|
|
@@ -11643,10 +12865,11 @@ function evaluateHealth(snapshot, config = {}) {
|
|
|
11643
12865
|
} else if (memPercent > cfg.memoryWarnPercent && rssAboveFloor) {
|
|
11644
12866
|
alerts.push(`memory_warn_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
11645
12867
|
degraded = true;
|
|
11646
|
-
} else if (memPercent > cfg.memoryWarnPercent)
|
|
12868
|
+
} else if (memPercent > cfg.memoryWarnPercent) notes.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
11647
12869
|
return {
|
|
11648
12870
|
status: critical ? "critical" : degraded ? "degraded" : "healthy",
|
|
11649
|
-
alerts
|
|
12871
|
+
alerts,
|
|
12872
|
+
notes
|
|
11650
12873
|
};
|
|
11651
12874
|
}
|
|
11652
12875
|
|
|
@@ -11723,6 +12946,7 @@ function registerHealthMonitor(sdk, kv) {
|
|
|
11723
12946
|
const evaluated = evaluateHealth(snapshot);
|
|
11724
12947
|
snapshot.status = evaluated.status;
|
|
11725
12948
|
snapshot.alerts = evaluated.alerts;
|
|
12949
|
+
snapshot.notes = evaluated.notes;
|
|
11726
12950
|
await kv.set(KV.health, "latest", snapshot).catch(() => {});
|
|
11727
12951
|
return snapshot;
|
|
11728
12952
|
}
|
|
@@ -11765,6 +12989,7 @@ function buildViewerCsp(nonce) {
|
|
|
11765
12989
|
|
|
11766
12990
|
//#endregion
|
|
11767
12991
|
//#region src/viewer/document.ts
|
|
12992
|
+
const VIEWER_VERSION_PLACEHOLDER = "__AGENTMEMORY_VERSION__";
|
|
11768
12993
|
function loadViewerTemplate() {
|
|
11769
12994
|
const base = dirname(fileURLToPath(import.meta.url));
|
|
11770
12995
|
const candidates = [
|
|
@@ -11783,7 +13008,7 @@ function renderViewerDocument() {
|
|
|
11783
13008
|
const nonce = createViewerNonce();
|
|
11784
13009
|
return {
|
|
11785
13010
|
found: true,
|
|
11786
|
-
html: template.replaceAll(VIEWER_NONCE_PLACEHOLDER, nonce),
|
|
13011
|
+
html: template.replaceAll(VIEWER_NONCE_PLACEHOLDER, nonce).replaceAll(VIEWER_VERSION_PLACEHOLDER, VERSION),
|
|
11787
13012
|
csp: buildViewerCsp(nonce)
|
|
11788
13013
|
};
|
|
11789
13014
|
}
|
|
@@ -12065,9 +13290,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
12065
13290
|
sdk.registerFunction("api::replay::sessions", async (req) => {
|
|
12066
13291
|
const authErr = checkAuth(req, secret);
|
|
12067
13292
|
if (authErr) return authErr;
|
|
13293
|
+
const sessions = await kv.list(KV.sessions);
|
|
13294
|
+
sessions.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
|
|
12068
13295
|
return {
|
|
12069
13296
|
status_code: 200,
|
|
12070
|
-
body:
|
|
13297
|
+
body: {
|
|
13298
|
+
success: true,
|
|
13299
|
+
sessions
|
|
13300
|
+
}
|
|
12071
13301
|
};
|
|
12072
13302
|
});
|
|
12073
13303
|
sdk.registerTrigger({
|
|
@@ -12862,13 +14092,16 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
12862
14092
|
const parsedLimit = parseOptionalInt(req.query_params?.["limit"]);
|
|
12863
14093
|
return {
|
|
12864
14094
|
status_code: 200,
|
|
12865
|
-
body:
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
|
|
14095
|
+
body: {
|
|
14096
|
+
entries: await sdk.trigger({
|
|
14097
|
+
function_id: "mem::audit-query",
|
|
14098
|
+
payload: {
|
|
14099
|
+
operation: req.query_params?.["operation"],
|
|
14100
|
+
limit: parsedLimit ?? 50
|
|
14101
|
+
}
|
|
14102
|
+
}),
|
|
14103
|
+
success: true
|
|
14104
|
+
}
|
|
12872
14105
|
};
|
|
12873
14106
|
});
|
|
12874
14107
|
sdk.registerTrigger({
|
|
@@ -12960,64 +14193,444 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
12960
14193
|
};
|
|
12961
14194
|
} catch {
|
|
12962
14195
|
return {
|
|
12963
|
-
status_code: 404,
|
|
12964
|
-
body: { error: "Snapshots not enabled" }
|
|
14196
|
+
status_code: 404,
|
|
14197
|
+
body: { error: "Snapshots not enabled" }
|
|
14198
|
+
};
|
|
14199
|
+
}
|
|
14200
|
+
});
|
|
14201
|
+
sdk.registerTrigger({
|
|
14202
|
+
type: "http",
|
|
14203
|
+
function_id: "api::snapshot-create",
|
|
14204
|
+
config: {
|
|
14205
|
+
api_path: "/agentmemory/snapshot/create",
|
|
14206
|
+
http_method: "POST"
|
|
14207
|
+
}
|
|
14208
|
+
});
|
|
14209
|
+
sdk.registerFunction("api::snapshot-restore", async (req) => {
|
|
14210
|
+
const authErr = checkAuth(req, secret);
|
|
14211
|
+
if (authErr) return authErr;
|
|
14212
|
+
if (!req.body?.commitHash) return {
|
|
14213
|
+
status_code: 400,
|
|
14214
|
+
body: { error: "commitHash is required" }
|
|
14215
|
+
};
|
|
14216
|
+
try {
|
|
14217
|
+
return {
|
|
14218
|
+
status_code: 200,
|
|
14219
|
+
body: await sdk.trigger({
|
|
14220
|
+
function_id: "mem::snapshot-restore",
|
|
14221
|
+
payload: req.body
|
|
14222
|
+
})
|
|
14223
|
+
};
|
|
14224
|
+
} catch {
|
|
14225
|
+
return {
|
|
14226
|
+
status_code: 404,
|
|
14227
|
+
body: { error: "Snapshots not enabled" }
|
|
14228
|
+
};
|
|
14229
|
+
}
|
|
14230
|
+
});
|
|
14231
|
+
sdk.registerTrigger({
|
|
14232
|
+
type: "http",
|
|
14233
|
+
function_id: "api::snapshot-restore",
|
|
14234
|
+
config: {
|
|
14235
|
+
api_path: "/agentmemory/snapshot/restore",
|
|
14236
|
+
http_method: "POST"
|
|
14237
|
+
}
|
|
14238
|
+
});
|
|
14239
|
+
sdk.registerFunction("api::memories", async (req) => {
|
|
14240
|
+
const authErr = checkAuth(req, secret);
|
|
14241
|
+
if (authErr) return authErr;
|
|
14242
|
+
const memories = await kv.list(KV.memories);
|
|
14243
|
+
return {
|
|
14244
|
+
status_code: 200,
|
|
14245
|
+
body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
|
|
14246
|
+
};
|
|
14247
|
+
});
|
|
14248
|
+
sdk.registerTrigger({
|
|
14249
|
+
type: "http",
|
|
14250
|
+
function_id: "api::memories",
|
|
14251
|
+
config: {
|
|
14252
|
+
api_path: "/agentmemory/memories",
|
|
14253
|
+
http_method: "GET"
|
|
14254
|
+
}
|
|
14255
|
+
});
|
|
14256
|
+
sdk.registerFunction("api::semantic-list", async (req) => {
|
|
14257
|
+
const authErr = checkAuth(req, secret);
|
|
14258
|
+
if (authErr) return authErr;
|
|
14259
|
+
return {
|
|
14260
|
+
status_code: 200,
|
|
14261
|
+
body: { semantic: await kv.list(KV.semantic) }
|
|
14262
|
+
};
|
|
14263
|
+
});
|
|
14264
|
+
sdk.registerTrigger({
|
|
14265
|
+
type: "http",
|
|
14266
|
+
function_id: "api::semantic-list",
|
|
14267
|
+
config: {
|
|
14268
|
+
api_path: "/agentmemory/semantic",
|
|
14269
|
+
http_method: "GET"
|
|
14270
|
+
}
|
|
14271
|
+
});
|
|
14272
|
+
sdk.registerFunction("api::procedural-list", async (req) => {
|
|
14273
|
+
const authErr = checkAuth(req, secret);
|
|
14274
|
+
if (authErr) return authErr;
|
|
14275
|
+
return {
|
|
14276
|
+
status_code: 200,
|
|
14277
|
+
body: { procedural: await kv.list(KV.procedural) }
|
|
14278
|
+
};
|
|
14279
|
+
});
|
|
14280
|
+
sdk.registerTrigger({
|
|
14281
|
+
type: "http",
|
|
14282
|
+
function_id: "api::procedural-list",
|
|
14283
|
+
config: {
|
|
14284
|
+
api_path: "/agentmemory/procedural",
|
|
14285
|
+
http_method: "GET"
|
|
14286
|
+
}
|
|
14287
|
+
});
|
|
14288
|
+
sdk.registerFunction("api::relations-list", async (req) => {
|
|
14289
|
+
const authErr = checkAuth(req, secret);
|
|
14290
|
+
if (authErr) return authErr;
|
|
14291
|
+
return {
|
|
14292
|
+
status_code: 200,
|
|
14293
|
+
body: { relations: await kv.list(KV.relations) }
|
|
14294
|
+
};
|
|
14295
|
+
});
|
|
14296
|
+
sdk.registerTrigger({
|
|
14297
|
+
type: "http",
|
|
14298
|
+
function_id: "api::relations-list",
|
|
14299
|
+
config: {
|
|
14300
|
+
api_path: "/agentmemory/relations",
|
|
14301
|
+
http_method: "GET"
|
|
14302
|
+
}
|
|
14303
|
+
});
|
|
14304
|
+
sdk.registerFunction("api::vision-search", async (req) => {
|
|
14305
|
+
const authErr = checkAuth(req, secret);
|
|
14306
|
+
if (authErr) return authErr;
|
|
14307
|
+
const body = req.body ?? {};
|
|
14308
|
+
const queryText = asNonEmptyString$1(body["queryText"]);
|
|
14309
|
+
const queryImageRef = asNonEmptyString$1(body["queryImageRef"]);
|
|
14310
|
+
const queryImageBase64 = asNonEmptyString$1(body["queryImageBase64"]);
|
|
14311
|
+
const sessionId = asNonEmptyString$1(body["sessionId"]);
|
|
14312
|
+
if (!queryText && !queryImageRef && !queryImageBase64) return {
|
|
14313
|
+
status_code: 400,
|
|
14314
|
+
body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
|
|
14315
|
+
};
|
|
14316
|
+
const topKParsed = parseOptionalPositiveInt(body["topK"]);
|
|
14317
|
+
if (topKParsed === null) return {
|
|
14318
|
+
status_code: 400,
|
|
14319
|
+
body: { error: "topK must be a positive integer" }
|
|
14320
|
+
};
|
|
14321
|
+
const payload = {};
|
|
14322
|
+
if (queryText) payload["queryText"] = queryText;
|
|
14323
|
+
if (queryImageRef) payload["queryImageRef"] = queryImageRef;
|
|
14324
|
+
if (queryImageBase64) payload["queryImageBase64"] = queryImageBase64;
|
|
14325
|
+
if (sessionId) payload["sessionId"] = sessionId;
|
|
14326
|
+
if (topKParsed !== void 0) payload["topK"] = Math.min(50, topKParsed);
|
|
14327
|
+
const result = await sdk.trigger({
|
|
14328
|
+
function_id: "mem::vision-search",
|
|
14329
|
+
payload
|
|
14330
|
+
});
|
|
14331
|
+
const resp = result;
|
|
14332
|
+
if (resp?.success === false) return {
|
|
14333
|
+
status_code: resp.error?.includes("disabled") ? 503 : 400,
|
|
14334
|
+
body: resp
|
|
14335
|
+
};
|
|
14336
|
+
return {
|
|
14337
|
+
status_code: 200,
|
|
14338
|
+
body: result
|
|
14339
|
+
};
|
|
14340
|
+
});
|
|
14341
|
+
sdk.registerTrigger({
|
|
14342
|
+
type: "http",
|
|
14343
|
+
function_id: "api::vision-search",
|
|
14344
|
+
config: {
|
|
14345
|
+
api_path: "/agentmemory/vision-search",
|
|
14346
|
+
http_method: "POST"
|
|
14347
|
+
}
|
|
14348
|
+
});
|
|
14349
|
+
sdk.registerFunction("api::vision-embed", async (req) => {
|
|
14350
|
+
const authErr = checkAuth(req, secret);
|
|
14351
|
+
if (authErr) return authErr;
|
|
14352
|
+
const body = req.body ?? {};
|
|
14353
|
+
const imageRef = asNonEmptyString$1(body["imageRef"]);
|
|
14354
|
+
const sessionId = asNonEmptyString$1(body["sessionId"]);
|
|
14355
|
+
const observationId = asNonEmptyString$1(body["observationId"]);
|
|
14356
|
+
if (!imageRef) return {
|
|
14357
|
+
status_code: 400,
|
|
14358
|
+
body: { error: "imageRef is required" }
|
|
14359
|
+
};
|
|
14360
|
+
const payload = { imageRef };
|
|
14361
|
+
if (sessionId) payload["sessionId"] = sessionId;
|
|
14362
|
+
if (observationId) payload["observationId"] = observationId;
|
|
14363
|
+
const result = await sdk.trigger({
|
|
14364
|
+
function_id: "mem::vision-embed",
|
|
14365
|
+
payload
|
|
14366
|
+
});
|
|
14367
|
+
const resp = result;
|
|
14368
|
+
if (resp?.success === false) return {
|
|
14369
|
+
status_code: resp.error?.includes("disabled") ? 503 : 400,
|
|
14370
|
+
body: resp
|
|
14371
|
+
};
|
|
14372
|
+
return {
|
|
14373
|
+
status_code: 200,
|
|
14374
|
+
body: result
|
|
14375
|
+
};
|
|
14376
|
+
});
|
|
14377
|
+
sdk.registerTrigger({
|
|
14378
|
+
type: "http",
|
|
14379
|
+
function_id: "api::vision-embed",
|
|
14380
|
+
config: {
|
|
14381
|
+
api_path: "/agentmemory/vision-embed",
|
|
14382
|
+
http_method: "POST"
|
|
14383
|
+
}
|
|
14384
|
+
});
|
|
14385
|
+
sdk.registerFunction("api::slot-list", async (req) => {
|
|
14386
|
+
const authErr = checkAuth(req, secret);
|
|
14387
|
+
if (authErr) return authErr;
|
|
14388
|
+
return {
|
|
14389
|
+
status_code: 200,
|
|
14390
|
+
body: await sdk.trigger({
|
|
14391
|
+
function_id: "mem::slot-list",
|
|
14392
|
+
payload: {}
|
|
14393
|
+
})
|
|
14394
|
+
};
|
|
14395
|
+
});
|
|
14396
|
+
sdk.registerTrigger({
|
|
14397
|
+
type: "http",
|
|
14398
|
+
function_id: "api::slot-list",
|
|
14399
|
+
config: {
|
|
14400
|
+
api_path: "/agentmemory/slots",
|
|
14401
|
+
http_method: "GET"
|
|
14402
|
+
}
|
|
14403
|
+
});
|
|
14404
|
+
sdk.registerFunction("api::slot-get", async (req) => {
|
|
14405
|
+
const authErr = checkAuth(req, secret);
|
|
14406
|
+
if (authErr) return authErr;
|
|
14407
|
+
const label = asNonEmptyString$1(req.query_params?.["label"]);
|
|
14408
|
+
if (!label) return {
|
|
14409
|
+
status_code: 400,
|
|
14410
|
+
body: { error: "label query param required" }
|
|
14411
|
+
};
|
|
14412
|
+
const result = await sdk.trigger({
|
|
14413
|
+
function_id: "mem::slot-get",
|
|
14414
|
+
payload: { label }
|
|
14415
|
+
});
|
|
14416
|
+
const resp = result;
|
|
14417
|
+
if (resp?.success === false) return {
|
|
14418
|
+
status_code: resp.error?.includes("not found") ? 404 : 400,
|
|
14419
|
+
body: resp
|
|
14420
|
+
};
|
|
14421
|
+
return {
|
|
14422
|
+
status_code: 200,
|
|
14423
|
+
body: result
|
|
14424
|
+
};
|
|
14425
|
+
});
|
|
14426
|
+
sdk.registerTrigger({
|
|
14427
|
+
type: "http",
|
|
14428
|
+
function_id: "api::slot-get",
|
|
14429
|
+
config: {
|
|
14430
|
+
api_path: "/agentmemory/slot",
|
|
14431
|
+
http_method: "GET"
|
|
14432
|
+
}
|
|
14433
|
+
});
|
|
14434
|
+
sdk.registerFunction("api::slot-create", async (req) => {
|
|
14435
|
+
const authErr = checkAuth(req, secret);
|
|
14436
|
+
if (authErr) return authErr;
|
|
14437
|
+
const body = req.body ?? {};
|
|
14438
|
+
const label = asNonEmptyString$1(body["label"]);
|
|
14439
|
+
if (!label) return {
|
|
14440
|
+
status_code: 400,
|
|
14441
|
+
body: { error: "label required" }
|
|
14442
|
+
};
|
|
14443
|
+
if (body["content"] !== void 0 && typeof body["content"] !== "string") return {
|
|
14444
|
+
status_code: 400,
|
|
14445
|
+
body: { error: "content must be a string" }
|
|
14446
|
+
};
|
|
14447
|
+
if (body["description"] !== void 0 && typeof body["description"] !== "string") return {
|
|
14448
|
+
status_code: 400,
|
|
14449
|
+
body: { error: "description must be a string" }
|
|
14450
|
+
};
|
|
14451
|
+
if (body["pinned"] !== void 0 && typeof body["pinned"] !== "boolean") return {
|
|
14452
|
+
status_code: 400,
|
|
14453
|
+
body: { error: "pinned must be a boolean" }
|
|
14454
|
+
};
|
|
14455
|
+
if (body["scope"] !== void 0 && body["scope"] !== "project" && body["scope"] !== "global") return {
|
|
14456
|
+
status_code: 400,
|
|
14457
|
+
body: { error: "scope must be 'project' or 'global'" }
|
|
14458
|
+
};
|
|
14459
|
+
const sizeLimit = parseOptionalPositiveInt(body["sizeLimit"]);
|
|
14460
|
+
if (sizeLimit === null) return {
|
|
14461
|
+
status_code: 400,
|
|
14462
|
+
body: { error: "sizeLimit must be a positive integer" }
|
|
14463
|
+
};
|
|
14464
|
+
if (sizeLimit !== void 0 && sizeLimit > 2e4) return {
|
|
14465
|
+
status_code: 400,
|
|
14466
|
+
body: { error: "sizeLimit must be <= 20000" }
|
|
14467
|
+
};
|
|
14468
|
+
const payload = { label };
|
|
14469
|
+
if (typeof body["content"] === "string") payload["content"] = body["content"];
|
|
14470
|
+
if (typeof body["description"] === "string") payload["description"] = body["description"];
|
|
14471
|
+
if (sizeLimit !== void 0) payload["sizeLimit"] = sizeLimit;
|
|
14472
|
+
if (typeof body["pinned"] === "boolean") payload["pinned"] = body["pinned"];
|
|
14473
|
+
if (body["scope"] === "project" || body["scope"] === "global") payload["scope"] = body["scope"];
|
|
14474
|
+
const result = await sdk.trigger({
|
|
14475
|
+
function_id: "mem::slot-create",
|
|
14476
|
+
payload
|
|
14477
|
+
});
|
|
14478
|
+
const resp = result;
|
|
14479
|
+
if (resp?.success === false) return {
|
|
14480
|
+
status_code: resp.error?.includes("exists") ? 409 : 400,
|
|
14481
|
+
body: resp
|
|
14482
|
+
};
|
|
14483
|
+
return {
|
|
14484
|
+
status_code: 201,
|
|
14485
|
+
body: result
|
|
14486
|
+
};
|
|
14487
|
+
});
|
|
14488
|
+
sdk.registerTrigger({
|
|
14489
|
+
type: "http",
|
|
14490
|
+
function_id: "api::slot-create",
|
|
14491
|
+
config: {
|
|
14492
|
+
api_path: "/agentmemory/slot",
|
|
14493
|
+
http_method: "POST"
|
|
14494
|
+
}
|
|
14495
|
+
});
|
|
14496
|
+
sdk.registerFunction("api::slot-append", async (req) => {
|
|
14497
|
+
const authErr = checkAuth(req, secret);
|
|
14498
|
+
if (authErr) return authErr;
|
|
14499
|
+
const body = req.body ?? {};
|
|
14500
|
+
const label = asNonEmptyString$1(body["label"]);
|
|
14501
|
+
const text = typeof body["text"] === "string" ? body["text"] : null;
|
|
14502
|
+
if (!label || !text) return {
|
|
14503
|
+
status_code: 400,
|
|
14504
|
+
body: { error: "label and text required" }
|
|
14505
|
+
};
|
|
14506
|
+
const result = await sdk.trigger({
|
|
14507
|
+
function_id: "mem::slot-append",
|
|
14508
|
+
payload: {
|
|
14509
|
+
label,
|
|
14510
|
+
text
|
|
14511
|
+
}
|
|
14512
|
+
});
|
|
14513
|
+
const resp = result;
|
|
14514
|
+
if (resp?.success === false) {
|
|
14515
|
+
const notFound = resp.error?.includes("not found");
|
|
14516
|
+
const overLimit = resp.error?.includes("exceed");
|
|
14517
|
+
return {
|
|
14518
|
+
status_code: notFound ? 404 : overLimit ? 413 : 400,
|
|
14519
|
+
body: resp
|
|
12965
14520
|
};
|
|
12966
14521
|
}
|
|
14522
|
+
return {
|
|
14523
|
+
status_code: 200,
|
|
14524
|
+
body: result
|
|
14525
|
+
};
|
|
12967
14526
|
});
|
|
12968
14527
|
sdk.registerTrigger({
|
|
12969
14528
|
type: "http",
|
|
12970
|
-
function_id: "api::
|
|
14529
|
+
function_id: "api::slot-append",
|
|
12971
14530
|
config: {
|
|
12972
|
-
api_path: "/agentmemory/
|
|
14531
|
+
api_path: "/agentmemory/slot/append",
|
|
12973
14532
|
http_method: "POST"
|
|
12974
14533
|
}
|
|
12975
14534
|
});
|
|
12976
|
-
sdk.registerFunction("api::
|
|
14535
|
+
sdk.registerFunction("api::slot-replace", async (req) => {
|
|
12977
14536
|
const authErr = checkAuth(req, secret);
|
|
12978
14537
|
if (authErr) return authErr;
|
|
12979
|
-
|
|
14538
|
+
const body = req.body ?? {};
|
|
14539
|
+
const label = asNonEmptyString$1(body["label"]);
|
|
14540
|
+
const content = body["content"];
|
|
14541
|
+
if (!label || typeof content !== "string") return {
|
|
12980
14542
|
status_code: 400,
|
|
12981
|
-
body: { error: "
|
|
14543
|
+
body: { error: "label and content (string) required" }
|
|
12982
14544
|
};
|
|
12983
|
-
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
|
|
12988
|
-
|
|
12989
|
-
|
|
12990
|
-
|
|
12991
|
-
|
|
14545
|
+
const result = await sdk.trigger({
|
|
14546
|
+
function_id: "mem::slot-replace",
|
|
14547
|
+
payload: {
|
|
14548
|
+
label,
|
|
14549
|
+
content
|
|
14550
|
+
}
|
|
14551
|
+
});
|
|
14552
|
+
const resp = result;
|
|
14553
|
+
if (resp?.success === false) {
|
|
14554
|
+
const notFound = resp.error?.includes("not found");
|
|
14555
|
+
const overLimit = resp.error?.includes("exceed");
|
|
12992
14556
|
return {
|
|
12993
|
-
status_code: 404,
|
|
12994
|
-
body:
|
|
14557
|
+
status_code: notFound ? 404 : overLimit ? 413 : 400,
|
|
14558
|
+
body: resp
|
|
12995
14559
|
};
|
|
12996
14560
|
}
|
|
14561
|
+
return {
|
|
14562
|
+
status_code: 200,
|
|
14563
|
+
body: result
|
|
14564
|
+
};
|
|
12997
14565
|
});
|
|
12998
14566
|
sdk.registerTrigger({
|
|
12999
14567
|
type: "http",
|
|
13000
|
-
function_id: "api::
|
|
14568
|
+
function_id: "api::slot-replace",
|
|
13001
14569
|
config: {
|
|
13002
|
-
api_path: "/agentmemory/
|
|
14570
|
+
api_path: "/agentmemory/slot/replace",
|
|
13003
14571
|
http_method: "POST"
|
|
13004
14572
|
}
|
|
13005
14573
|
});
|
|
13006
|
-
sdk.registerFunction("api::
|
|
14574
|
+
sdk.registerFunction("api::slot-delete", async (req) => {
|
|
13007
14575
|
const authErr = checkAuth(req, secret);
|
|
13008
14576
|
if (authErr) return authErr;
|
|
13009
|
-
const
|
|
14577
|
+
const label = asNonEmptyString$1(req.query_params?.["label"]);
|
|
14578
|
+
if (!label) return {
|
|
14579
|
+
status_code: 400,
|
|
14580
|
+
body: { error: "label query param required" }
|
|
14581
|
+
};
|
|
14582
|
+
const result = await sdk.trigger({
|
|
14583
|
+
function_id: "mem::slot-delete",
|
|
14584
|
+
payload: { label }
|
|
14585
|
+
});
|
|
14586
|
+
const resp = result;
|
|
14587
|
+
if (resp?.success === false) return {
|
|
14588
|
+
status_code: resp.error?.includes("not found") ? 404 : 400,
|
|
14589
|
+
body: resp
|
|
14590
|
+
};
|
|
13010
14591
|
return {
|
|
13011
14592
|
status_code: 200,
|
|
13012
|
-
body:
|
|
14593
|
+
body: result
|
|
13013
14594
|
};
|
|
13014
14595
|
});
|
|
13015
14596
|
sdk.registerTrigger({
|
|
13016
14597
|
type: "http",
|
|
13017
|
-
function_id: "api::
|
|
14598
|
+
function_id: "api::slot-delete",
|
|
13018
14599
|
config: {
|
|
13019
|
-
api_path: "/agentmemory/
|
|
13020
|
-
http_method: "
|
|
14600
|
+
api_path: "/agentmemory/slot",
|
|
14601
|
+
http_method: "DELETE"
|
|
14602
|
+
}
|
|
14603
|
+
});
|
|
14604
|
+
sdk.registerFunction("api::slot-reflect", async (req) => {
|
|
14605
|
+
const authErr = checkAuth(req, secret);
|
|
14606
|
+
if (authErr) return authErr;
|
|
14607
|
+
const body = req.body ?? {};
|
|
14608
|
+
const sessionId = asNonEmptyString$1(body["sessionId"]);
|
|
14609
|
+
if (!sessionId) return {
|
|
14610
|
+
status_code: 400,
|
|
14611
|
+
body: { error: "sessionId required" }
|
|
14612
|
+
};
|
|
14613
|
+
const maxObservations = parseOptionalPositiveInt(body["maxObservations"]);
|
|
14614
|
+
if (maxObservations === null) return {
|
|
14615
|
+
status_code: 400,
|
|
14616
|
+
body: { error: "maxObservations must be a positive integer" }
|
|
14617
|
+
};
|
|
14618
|
+
const payload = { sessionId };
|
|
14619
|
+
if (maxObservations !== void 0) payload["maxObservations"] = maxObservations;
|
|
14620
|
+
return {
|
|
14621
|
+
status_code: 200,
|
|
14622
|
+
body: await sdk.trigger({
|
|
14623
|
+
function_id: "mem::slot-reflect",
|
|
14624
|
+
payload
|
|
14625
|
+
})
|
|
14626
|
+
};
|
|
14627
|
+
});
|
|
14628
|
+
sdk.registerTrigger({
|
|
14629
|
+
type: "http",
|
|
14630
|
+
function_id: "api::slot-reflect",
|
|
14631
|
+
config: {
|
|
14632
|
+
api_path: "/agentmemory/slot/reflect",
|
|
14633
|
+
http_method: "POST"
|
|
13021
14634
|
}
|
|
13022
14635
|
});
|
|
13023
14636
|
sdk.registerFunction("api::action-create", async (req) => {
|
|
@@ -13254,9 +14867,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13254
14867
|
sdk.registerFunction("api::routine-create", async (req) => {
|
|
13255
14868
|
const authErr = checkAuth(req, secret);
|
|
13256
14869
|
if (authErr) return authErr;
|
|
13257
|
-
if (!req.body?.name) return {
|
|
14870
|
+
if (!req.body?.name || !req.body?.steps) return {
|
|
13258
14871
|
status_code: 400,
|
|
13259
|
-
body: { error: "name
|
|
14872
|
+
body: { error: "name and steps are required" }
|
|
13260
14873
|
};
|
|
13261
14874
|
return {
|
|
13262
14875
|
status_code: 201,
|
|
@@ -14505,10 +16118,21 @@ function registerEventTriggers(sdk, kv) {
|
|
|
14505
16118
|
function_id: "event::observation",
|
|
14506
16119
|
config: { topic: "agentmemory.observation" }
|
|
14507
16120
|
});
|
|
14508
|
-
sdk.registerFunction("event::session::stopped", async (data) =>
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
16121
|
+
sdk.registerFunction("event::session::stopped", async (data) => {
|
|
16122
|
+
const summary = await sdk.trigger({
|
|
16123
|
+
function_id: "mem::summarize",
|
|
16124
|
+
payload: data
|
|
16125
|
+
});
|
|
16126
|
+
if (isReflectEnabled()) try {
|
|
16127
|
+
sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId });
|
|
16128
|
+
} catch (err) {
|
|
16129
|
+
logger.warn("slot-reflect triggerVoid failed", {
|
|
16130
|
+
sessionId: data.sessionId,
|
|
16131
|
+
error: err instanceof Error ? err.message : String(err)
|
|
16132
|
+
});
|
|
16133
|
+
}
|
|
16134
|
+
return summary;
|
|
16135
|
+
});
|
|
14512
16136
|
sdk.registerTrigger({
|
|
14513
16137
|
type: "durable:subscriber",
|
|
14514
16138
|
function_id: "event::session::stopped",
|
|
@@ -14687,6 +16311,35 @@ const CORE_TOOLS = [
|
|
|
14687
16311
|
required: ["query"]
|
|
14688
16312
|
}
|
|
14689
16313
|
},
|
|
16314
|
+
{
|
|
16315
|
+
name: "memory_vision_search",
|
|
16316
|
+
description: "Cross-modal image search via CLIP embeddings. Pass queryText to find screenshots matching a description, or queryImageBase64/queryImageRef to find similar images. Requires AGENTMEMORY_IMAGE_EMBEDDINGS=true.",
|
|
16317
|
+
inputSchema: {
|
|
16318
|
+
type: "object",
|
|
16319
|
+
properties: {
|
|
16320
|
+
queryText: {
|
|
16321
|
+
type: "string",
|
|
16322
|
+
description: "Text query (e.g. 'login form with error banner')"
|
|
16323
|
+
},
|
|
16324
|
+
queryImageRef: {
|
|
16325
|
+
type: "string",
|
|
16326
|
+
description: "Absolute path to a stored image to match against"
|
|
16327
|
+
},
|
|
16328
|
+
queryImageBase64: {
|
|
16329
|
+
type: "string",
|
|
16330
|
+
description: "Raw base64 image bytes or data URL"
|
|
16331
|
+
},
|
|
16332
|
+
topK: {
|
|
16333
|
+
type: "number",
|
|
16334
|
+
description: "Max results (default 10, max 50)"
|
|
16335
|
+
},
|
|
16336
|
+
sessionId: {
|
|
16337
|
+
type: "string",
|
|
16338
|
+
description: "Filter to a single session"
|
|
16339
|
+
}
|
|
16340
|
+
}
|
|
16341
|
+
}
|
|
16342
|
+
},
|
|
14690
16343
|
{
|
|
14691
16344
|
name: "memory_timeline",
|
|
14692
16345
|
description: "Chronological observations around an anchor point.",
|
|
@@ -15474,6 +17127,110 @@ const V073_TOOLS = [{
|
|
|
15474
17127
|
}
|
|
15475
17128
|
}
|
|
15476
17129
|
}];
|
|
17130
|
+
const V010_SLOTS_TOOLS = [
|
|
17131
|
+
{
|
|
17132
|
+
name: "memory_slot_list",
|
|
17133
|
+
description: "List all memory slots (pinned + project + global). Slots are editable, size-limited memory units the agent can read and modify across sessions.",
|
|
17134
|
+
inputSchema: {
|
|
17135
|
+
type: "object",
|
|
17136
|
+
properties: {}
|
|
17137
|
+
}
|
|
17138
|
+
},
|
|
17139
|
+
{
|
|
17140
|
+
name: "memory_slot_get",
|
|
17141
|
+
description: "Read a single slot by label.",
|
|
17142
|
+
inputSchema: {
|
|
17143
|
+
type: "object",
|
|
17144
|
+
properties: { label: {
|
|
17145
|
+
type: "string",
|
|
17146
|
+
description: "Slot label (e.g. 'persona', 'pending_items')"
|
|
17147
|
+
} },
|
|
17148
|
+
required: ["label"]
|
|
17149
|
+
}
|
|
17150
|
+
},
|
|
17151
|
+
{
|
|
17152
|
+
name: "memory_slot_create",
|
|
17153
|
+
description: "Create a new slot. Reject if a slot with the same label already exists.",
|
|
17154
|
+
inputSchema: {
|
|
17155
|
+
type: "object",
|
|
17156
|
+
properties: {
|
|
17157
|
+
label: {
|
|
17158
|
+
type: "string",
|
|
17159
|
+
description: "Slot label — lowercase, starts with letter, [a-z0-9_]"
|
|
17160
|
+
},
|
|
17161
|
+
content: {
|
|
17162
|
+
type: "string",
|
|
17163
|
+
description: "Initial content (default empty)"
|
|
17164
|
+
},
|
|
17165
|
+
sizeLimit: {
|
|
17166
|
+
type: "number",
|
|
17167
|
+
description: "Max chars (default 2000, hard cap 20000)"
|
|
17168
|
+
},
|
|
17169
|
+
description: {
|
|
17170
|
+
type: "string",
|
|
17171
|
+
description: "What this slot is for"
|
|
17172
|
+
},
|
|
17173
|
+
pinned: {
|
|
17174
|
+
type: "string",
|
|
17175
|
+
description: "'false' to exclude from context injection; default true"
|
|
17176
|
+
},
|
|
17177
|
+
scope: {
|
|
17178
|
+
type: "string",
|
|
17179
|
+
description: "'project' (default) or 'global' (shared across projects)"
|
|
17180
|
+
}
|
|
17181
|
+
},
|
|
17182
|
+
required: ["label"]
|
|
17183
|
+
}
|
|
17184
|
+
},
|
|
17185
|
+
{
|
|
17186
|
+
name: "memory_slot_append",
|
|
17187
|
+
description: "Append text to an existing slot. Fails with 413 if the append would exceed the slot's sizeLimit — agent must compact via memory_slot_replace first.",
|
|
17188
|
+
inputSchema: {
|
|
17189
|
+
type: "object",
|
|
17190
|
+
properties: {
|
|
17191
|
+
label: {
|
|
17192
|
+
type: "string",
|
|
17193
|
+
description: "Slot label"
|
|
17194
|
+
},
|
|
17195
|
+
text: {
|
|
17196
|
+
type: "string",
|
|
17197
|
+
description: "Text to append"
|
|
17198
|
+
}
|
|
17199
|
+
},
|
|
17200
|
+
required: ["label", "text"]
|
|
17201
|
+
}
|
|
17202
|
+
},
|
|
17203
|
+
{
|
|
17204
|
+
name: "memory_slot_replace",
|
|
17205
|
+
description: "Replace slot content in place. Fails if content exceeds sizeLimit.",
|
|
17206
|
+
inputSchema: {
|
|
17207
|
+
type: "object",
|
|
17208
|
+
properties: {
|
|
17209
|
+
label: {
|
|
17210
|
+
type: "string",
|
|
17211
|
+
description: "Slot label"
|
|
17212
|
+
},
|
|
17213
|
+
content: {
|
|
17214
|
+
type: "string",
|
|
17215
|
+
description: "New full content"
|
|
17216
|
+
}
|
|
17217
|
+
},
|
|
17218
|
+
required: ["label", "content"]
|
|
17219
|
+
}
|
|
17220
|
+
},
|
|
17221
|
+
{
|
|
17222
|
+
name: "memory_slot_delete",
|
|
17223
|
+
description: "Delete a slot. Seeded default slots can be deleted unless marked readOnly.",
|
|
17224
|
+
inputSchema: {
|
|
17225
|
+
type: "object",
|
|
17226
|
+
properties: { label: {
|
|
17227
|
+
type: "string",
|
|
17228
|
+
description: "Slot label"
|
|
17229
|
+
} },
|
|
17230
|
+
required: ["label"]
|
|
17231
|
+
}
|
|
17232
|
+
}
|
|
17233
|
+
];
|
|
15477
17234
|
const ESSENTIAL_TOOLS = new Set([
|
|
15478
17235
|
"memory_save",
|
|
15479
17236
|
"memory_recall",
|
|
@@ -15492,7 +17249,8 @@ function getAllTools() {
|
|
|
15492
17249
|
...V051_TOOLS,
|
|
15493
17250
|
...V061_TOOLS,
|
|
15494
17251
|
...V070_TOOLS,
|
|
15495
|
-
...V073_TOOLS
|
|
17252
|
+
...V073_TOOLS,
|
|
17253
|
+
...V010_SLOTS_TOOLS
|
|
15496
17254
|
];
|
|
15497
17255
|
}
|
|
15498
17256
|
function getVisibleTools() {
|
|
@@ -15699,6 +17457,34 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
15699
17457
|
}] }
|
|
15700
17458
|
};
|
|
15701
17459
|
}
|
|
17460
|
+
case "memory_vision_search": {
|
|
17461
|
+
const queryText = typeof args.queryText === "string" ? args.queryText : void 0;
|
|
17462
|
+
const queryImageRef = typeof args.queryImageRef === "string" ? args.queryImageRef : void 0;
|
|
17463
|
+
const queryImageBase64 = typeof args.queryImageBase64 === "string" ? args.queryImageBase64 : void 0;
|
|
17464
|
+
if (!queryText && !queryImageRef && !queryImageBase64) return {
|
|
17465
|
+
status_code: 400,
|
|
17466
|
+
body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
|
|
17467
|
+
};
|
|
17468
|
+
const topK = Math.max(1, Math.min(50, asNumber(args.topK, 10) ?? 10));
|
|
17469
|
+
const sessionId = typeof args.sessionId === "string" ? args.sessionId : void 0;
|
|
17470
|
+
const result = await sdk.trigger({
|
|
17471
|
+
function_id: "mem::vision-search",
|
|
17472
|
+
payload: {
|
|
17473
|
+
queryText,
|
|
17474
|
+
queryImageRef,
|
|
17475
|
+
queryImageBase64,
|
|
17476
|
+
topK,
|
|
17477
|
+
sessionId
|
|
17478
|
+
}
|
|
17479
|
+
});
|
|
17480
|
+
return {
|
|
17481
|
+
status_code: 200,
|
|
17482
|
+
body: { content: [{
|
|
17483
|
+
type: "text",
|
|
17484
|
+
text: JSON.stringify(result, null, 2)
|
|
17485
|
+
}] }
|
|
17486
|
+
};
|
|
17487
|
+
}
|
|
15702
17488
|
case "memory_timeline": {
|
|
15703
17489
|
if (typeof args.anchor !== "string" || !args.anchor.trim()) return {
|
|
15704
17490
|
status_code: 400,
|
|
@@ -16564,6 +18350,123 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
16564
18350
|
}] }
|
|
16565
18351
|
};
|
|
16566
18352
|
}
|
|
18353
|
+
case "memory_slot_list": {
|
|
18354
|
+
const result = await sdk.trigger({
|
|
18355
|
+
function_id: "mem::slot-list",
|
|
18356
|
+
payload: {}
|
|
18357
|
+
});
|
|
18358
|
+
return {
|
|
18359
|
+
status_code: 200,
|
|
18360
|
+
body: { content: [{
|
|
18361
|
+
type: "text",
|
|
18362
|
+
text: JSON.stringify(result, null, 2)
|
|
18363
|
+
}] }
|
|
18364
|
+
};
|
|
18365
|
+
}
|
|
18366
|
+
case "memory_slot_get": {
|
|
18367
|
+
const label = asNonEmptyString(args.label);
|
|
18368
|
+
if (!label) return {
|
|
18369
|
+
status_code: 400,
|
|
18370
|
+
body: { error: "label required" }
|
|
18371
|
+
};
|
|
18372
|
+
const result = await sdk.trigger({
|
|
18373
|
+
function_id: "mem::slot-get",
|
|
18374
|
+
payload: { label }
|
|
18375
|
+
});
|
|
18376
|
+
return {
|
|
18377
|
+
status_code: 200,
|
|
18378
|
+
body: { content: [{
|
|
18379
|
+
type: "text",
|
|
18380
|
+
text: JSON.stringify(result, null, 2)
|
|
18381
|
+
}] }
|
|
18382
|
+
};
|
|
18383
|
+
}
|
|
18384
|
+
case "memory_slot_create": {
|
|
18385
|
+
const label = asNonEmptyString(args.label);
|
|
18386
|
+
if (!label) return {
|
|
18387
|
+
status_code: 400,
|
|
18388
|
+
body: { error: "label required" }
|
|
18389
|
+
};
|
|
18390
|
+
const payload = { label };
|
|
18391
|
+
if (typeof args.content === "string") payload.content = args.content;
|
|
18392
|
+
if (typeof args.description === "string") payload.description = args.description;
|
|
18393
|
+
if (typeof args.sizeLimit === "number") payload.sizeLimit = args.sizeLimit;
|
|
18394
|
+
if (args.pinned === false || args.pinned === "false") payload.pinned = false;
|
|
18395
|
+
else if (args.pinned === true || args.pinned === "true") payload.pinned = true;
|
|
18396
|
+
if (args.scope === "global" || args.scope === "project") payload.scope = args.scope;
|
|
18397
|
+
const result = await sdk.trigger({
|
|
18398
|
+
function_id: "mem::slot-create",
|
|
18399
|
+
payload
|
|
18400
|
+
});
|
|
18401
|
+
return {
|
|
18402
|
+
status_code: 200,
|
|
18403
|
+
body: { content: [{
|
|
18404
|
+
type: "text",
|
|
18405
|
+
text: JSON.stringify(result, null, 2)
|
|
18406
|
+
}] }
|
|
18407
|
+
};
|
|
18408
|
+
}
|
|
18409
|
+
case "memory_slot_append": {
|
|
18410
|
+
const label = asNonEmptyString(args.label);
|
|
18411
|
+
const text = typeof args.text === "string" ? args.text : null;
|
|
18412
|
+
if (!label || !text) return {
|
|
18413
|
+
status_code: 400,
|
|
18414
|
+
body: { error: "label and text required" }
|
|
18415
|
+
};
|
|
18416
|
+
const result = await sdk.trigger({
|
|
18417
|
+
function_id: "mem::slot-append",
|
|
18418
|
+
payload: {
|
|
18419
|
+
label,
|
|
18420
|
+
text
|
|
18421
|
+
}
|
|
18422
|
+
});
|
|
18423
|
+
return {
|
|
18424
|
+
status_code: 200,
|
|
18425
|
+
body: { content: [{
|
|
18426
|
+
type: "text",
|
|
18427
|
+
text: JSON.stringify(result, null, 2)
|
|
18428
|
+
}] }
|
|
18429
|
+
};
|
|
18430
|
+
}
|
|
18431
|
+
case "memory_slot_replace": {
|
|
18432
|
+
const label = asNonEmptyString(args.label);
|
|
18433
|
+
if (!label || typeof args.content !== "string") return {
|
|
18434
|
+
status_code: 400,
|
|
18435
|
+
body: { error: "label and content (string) required" }
|
|
18436
|
+
};
|
|
18437
|
+
const result = await sdk.trigger({
|
|
18438
|
+
function_id: "mem::slot-replace",
|
|
18439
|
+
payload: {
|
|
18440
|
+
label,
|
|
18441
|
+
content: args.content
|
|
18442
|
+
}
|
|
18443
|
+
});
|
|
18444
|
+
return {
|
|
18445
|
+
status_code: 200,
|
|
18446
|
+
body: { content: [{
|
|
18447
|
+
type: "text",
|
|
18448
|
+
text: JSON.stringify(result, null, 2)
|
|
18449
|
+
}] }
|
|
18450
|
+
};
|
|
18451
|
+
}
|
|
18452
|
+
case "memory_slot_delete": {
|
|
18453
|
+
const label = asNonEmptyString(args.label);
|
|
18454
|
+
if (!label) return {
|
|
18455
|
+
status_code: 400,
|
|
18456
|
+
body: { error: "label required" }
|
|
18457
|
+
};
|
|
18458
|
+
const result = await sdk.trigger({
|
|
18459
|
+
function_id: "mem::slot-delete",
|
|
18460
|
+
payload: { label }
|
|
18461
|
+
});
|
|
18462
|
+
return {
|
|
18463
|
+
status_code: 200,
|
|
18464
|
+
body: { content: [{
|
|
18465
|
+
type: "text",
|
|
18466
|
+
text: JSON.stringify(result, null, 2)
|
|
18467
|
+
}] }
|
|
18468
|
+
};
|
|
18469
|
+
}
|
|
16567
18470
|
default: return {
|
|
16568
18471
|
status_code: 400,
|
|
16569
18472
|
body: { error: `Unknown tool: ${name}` }
|
|
@@ -17218,11 +19121,13 @@ async function main() {
|
|
|
17218
19121
|
const fallbackConfig = loadFallbackConfig();
|
|
17219
19122
|
const provider = fallbackConfig.providers.length > 0 ? createFallbackProvider(config.provider, fallbackConfig) : createProvider(config.provider);
|
|
17220
19123
|
const embeddingProvider = createEmbeddingProvider();
|
|
19124
|
+
const imageEmbeddingProvider = createImageEmbeddingProvider();
|
|
17221
19125
|
console.log(`[agentmemory] Starting worker v${VERSION}...`);
|
|
17222
19126
|
console.log(`[agentmemory] Engine: ${config.engineUrl}`);
|
|
17223
19127
|
console.log(`[agentmemory] Provider: ${config.provider.provider} (${config.provider.model})`);
|
|
17224
19128
|
if (embeddingProvider) console.log(`[agentmemory] Embedding provider: ${embeddingProvider.name} (${embeddingProvider.dimensions} dims)`);
|
|
17225
19129
|
else console.log(`[agentmemory] Embedding provider: none (BM25-only mode)`);
|
|
19130
|
+
if (imageEmbeddingProvider) console.log(`[agentmemory] Image embedding provider: ${imageEmbeddingProvider.name} (${imageEmbeddingProvider.dimensions} dims) — vision-search active`);
|
|
17226
19131
|
console.log(`[agentmemory] REST API: http://localhost:${config.restPort}/agentmemory/*`);
|
|
17227
19132
|
console.log(`[agentmemory] Streams: ws://localhost:${config.streamsPort}`);
|
|
17228
19133
|
const sdk = registerWorker(config.engineUrl, {
|
|
@@ -17241,6 +19146,10 @@ async function main() {
|
|
|
17241
19146
|
initMetrics(hasGetMeter(sdk) ? sdk.getMeter.bind(sdk) : void 0);
|
|
17242
19147
|
registerPrivacyFunction(sdk);
|
|
17243
19148
|
registerObserveFunction(sdk, kv, dedupMap, config.maxObservationsPerSession);
|
|
19149
|
+
registerImageQuotaCleanup(sdk, kv);
|
|
19150
|
+
registerVisionSearchFunctions(sdk, kv, imageEmbeddingProvider);
|
|
19151
|
+
if (isSlotsEnabled()) registerSlotsFunctions(sdk, kv);
|
|
19152
|
+
registerDiskSizeManager(sdk, kv);
|
|
17244
19153
|
registerCompressFunction(sdk, kv, provider, metricsStore);
|
|
17245
19154
|
registerSearchFunction(sdk, kv);
|
|
17246
19155
|
registerContextFunction(sdk, kv, config.tokenBudget);
|
|
@@ -17307,6 +19216,7 @@ async function main() {
|
|
|
17307
19216
|
registerReplayFunctions(sdk, kv);
|
|
17308
19217
|
console.log(`[agentmemory] v0.6 advanced retrieval: sliding-window, query-expansion, temporal-graph, retention-scoring`);
|
|
17309
19218
|
console.log(`[agentmemory] Orchestration layer: actions, frontier, leases, routines, signals, checkpoints, flow-compress, mesh, branch-aware, sentinels, sketches, crystallize, diagnostics, facets`);
|
|
19219
|
+
if (isSlotsEnabled()) console.log(`[agentmemory] Slots: enabled (pinned editable memory). Reflect on Stop hook: ${isReflectEnabled() ? "on" : "off"}`);
|
|
17310
19220
|
const snapshotConfig = loadSnapshotConfig();
|
|
17311
19221
|
if (snapshotConfig.enabled) {
|
|
17312
19222
|
registerSnapshotFunction(sdk, kv, snapshotConfig.dir);
|
|
@@ -17344,7 +19254,7 @@ async function main() {
|
|
|
17344
19254
|
}
|
|
17345
19255
|
}
|
|
17346
19256
|
console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
|
|
17347
|
-
console.log(`[agentmemory] Endpoints: 107 REST +
|
|
19257
|
+
console.log(`[agentmemory] Endpoints: 107 REST + ${getAllTools().length} MCP tools + 6 MCP resources + 3 MCP prompts`);
|
|
17348
19258
|
const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
|
|
17349
19259
|
const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
|
|
17350
19260
|
const consolidationIntervalMs = parseInt(process.env.CONSOLIDATION_INTERVAL_MS || "7200000", 10);
|