@agentmemory/agentmemory 0.9.1 → 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 +40 -13
- package/dist/cli.mjs +20 -8
- 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 +2034 -174
- package/dist/index.mjs.map +1 -1
- package/dist/{src-Dw_gJcCy.mjs → src-tmuZyobT.mjs} +1879 -208
- package/dist/src-tmuZyobT.mjs.map +1 -0
- package/dist/{standalone-BEWvWM5P.mjs → standalone-BiwX0rdC.mjs} +2 -2
- package/dist/{standalone-BEWvWM5P.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-BvWNlj6u.mjs → tools-registry-CHH84gIQ.mjs} +166 -12
- package/dist/tools-registry-CHH84gIQ.mjs.map +1 -0
- package/dist/viewer/index.html +248 -61
- 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-Dw_gJcCy.mjs.map +0 -1
- package/dist/tools-registry-BvWNlj6u.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
|
@@ -1,20 +1,127 @@
|
|
|
1
1
|
import { a as jaccardSimilarity, i as generateId, n as STREAM, r as fingerprintId, t as KV } from "./cli.mjs";
|
|
2
|
-
import { a as
|
|
2
|
+
import { _ as loadTeamConfig, a as getConsolidationDecayDays, c as isAutoCompressEnabled, d as isGraphExtractionEnabled, f as loadClaudeBridgeConfig, g as loadSnapshotConfig, h as loadFallbackConfig, i as detectEmbeddingProvider, l as isConsolidationEnabled, m as loadEmbeddingConfig, n as getVisibleTools, o as getEnvVar, p as loadConfig, r as VERSION, t as getAllTools, u as isContextInjectionEnabled } from "./tools-registry-CHH84gIQ.mjs";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { constants, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, extname, join, resolve, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
9
|
+
import { lstat, mkdir, open, readFile, readdir, stat, unlink, utimes, writeFile } from "node:fs/promises";
|
|
9
10
|
import { TriggerAction, registerWorker } from "iii-sdk";
|
|
10
11
|
import Anthropic from "@anthropic-ai/sdk";
|
|
11
12
|
import { z } from "zod";
|
|
12
13
|
import { promisify } from "node:util";
|
|
13
14
|
import { lookup } from "node:dns/promises";
|
|
14
15
|
import { isIP } from "node:net";
|
|
15
|
-
import { lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
|
|
16
16
|
import { createServer } from "node:http";
|
|
17
17
|
|
|
18
|
+
//#region src/utils/image-store.ts
|
|
19
|
+
const IMAGES_DIR = join(homedir(), ".agentmemory", "images");
|
|
20
|
+
const DEFAULT_MAX_BYTES = 500 * 1024 * 1024;
|
|
21
|
+
function getMaxBytes() {
|
|
22
|
+
return Number(process.env.AGENTMEMORY_IMAGE_STORE_MAX_BYTES) || DEFAULT_MAX_BYTES;
|
|
23
|
+
}
|
|
24
|
+
function isManagedImagePath(filePath) {
|
|
25
|
+
const resolved = resolve(filePath);
|
|
26
|
+
const normalizedImagesDir = resolve(IMAGES_DIR);
|
|
27
|
+
return resolved.startsWith(normalizedImagesDir + sep) || resolved === normalizedImagesDir;
|
|
28
|
+
}
|
|
29
|
+
function contentHash(data) {
|
|
30
|
+
return createHash("sha256").update(data).digest("hex");
|
|
31
|
+
}
|
|
32
|
+
async function saveImageToDisk(base64Data) {
|
|
33
|
+
if (!base64Data) return {
|
|
34
|
+
filePath: "",
|
|
35
|
+
bytesWritten: 0
|
|
36
|
+
};
|
|
37
|
+
if (!existsSync(IMAGES_DIR)) await mkdir(IMAGES_DIR, { recursive: true });
|
|
38
|
+
let cleanBase64 = base64Data;
|
|
39
|
+
let ext = "png";
|
|
40
|
+
if (base64Data.startsWith("data:image/")) {
|
|
41
|
+
const commaIdx = base64Data.indexOf(",");
|
|
42
|
+
if (commaIdx !== -1) {
|
|
43
|
+
const meta = base64Data.substring(0, commaIdx);
|
|
44
|
+
if (meta.includes("jpeg") || meta.includes("jpg")) ext = "jpg";
|
|
45
|
+
else if (meta.includes("webp")) ext = "webp";
|
|
46
|
+
else if (meta.includes("gif")) ext = "gif";
|
|
47
|
+
cleanBase64 = base64Data.substring(commaIdx + 1);
|
|
48
|
+
}
|
|
49
|
+
} else if (base64Data.startsWith("/9j/")) ext = "jpg";
|
|
50
|
+
const filePath = join(IMAGES_DIR, `${contentHash(cleanBase64)}.${ext}`);
|
|
51
|
+
if (existsSync(filePath)) return {
|
|
52
|
+
filePath,
|
|
53
|
+
bytesWritten: 0
|
|
54
|
+
};
|
|
55
|
+
await writeFile(filePath, Buffer.from(cleanBase64, "base64"));
|
|
56
|
+
return {
|
|
57
|
+
filePath,
|
|
58
|
+
bytesWritten: (await stat(filePath)).size
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function deleteImage(filePath) {
|
|
62
|
+
if (!filePath) return { deletedBytes: 0 };
|
|
63
|
+
if (!isManagedImagePath(filePath)) return { deletedBytes: 0 };
|
|
64
|
+
try {
|
|
65
|
+
if (existsSync(filePath)) {
|
|
66
|
+
const size = (await stat(filePath)).size;
|
|
67
|
+
await unlink(filePath);
|
|
68
|
+
return { deletedBytes: size };
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error("[agentmemory] Failed to delete image context:", err);
|
|
72
|
+
}
|
|
73
|
+
return { deletedBytes: 0 };
|
|
74
|
+
}
|
|
75
|
+
/** Touch an image file to update its mtime (marking it as recently used for LRU eviction) */
|
|
76
|
+
async function touchImage(filePath) {
|
|
77
|
+
if (!filePath || !isManagedImagePath(filePath)) return;
|
|
78
|
+
try {
|
|
79
|
+
if (existsSync(filePath)) {
|
|
80
|
+
const now = /* @__PURE__ */ new Date();
|
|
81
|
+
await utimes(filePath, now, now);
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/state/keyed-mutex.ts
|
|
88
|
+
const locks = /* @__PURE__ */ new Map();
|
|
89
|
+
function withKeyedLock(key, fn) {
|
|
90
|
+
const next = (locks.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
91
|
+
const cleanup = next.then(() => {}, () => {});
|
|
92
|
+
locks.set(key, cleanup);
|
|
93
|
+
cleanup.then(() => {
|
|
94
|
+
if (locks.get(key) === cleanup) locks.delete(key);
|
|
95
|
+
});
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/functions/image-refs.ts
|
|
101
|
+
async function getImageRefCount(kv, filePath) {
|
|
102
|
+
const count = await kv.get(KV.imageRefs, filePath);
|
|
103
|
+
return count ? Number(count) : 0;
|
|
104
|
+
}
|
|
105
|
+
async function incrementImageRef(kv, filePath) {
|
|
106
|
+
return withKeyedLock(`imgRef:${filePath}`, async () => {
|
|
107
|
+
const current = await getImageRefCount(kv, filePath);
|
|
108
|
+
await kv.set(KV.imageRefs, filePath, current + 1);
|
|
109
|
+
await touchImage(filePath);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
async function decrementImageRef(kv, sdk, filePath) {
|
|
113
|
+
return withKeyedLock(`imgRef:${filePath}`, async () => {
|
|
114
|
+
const current = await getImageRefCount(kv, filePath);
|
|
115
|
+
if (current <= 1) {
|
|
116
|
+
await kv.delete(KV.imageEmbeddings, filePath);
|
|
117
|
+
await kv.delete(KV.imageRefs, filePath);
|
|
118
|
+
const { deletedBytes } = await deleteImage(filePath);
|
|
119
|
+
if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
|
|
120
|
+
} else await kv.set(KV.imageRefs, filePath, current - 1);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
18
125
|
//#region src/providers/agent-sdk.ts
|
|
19
126
|
var AgentSDKProvider = class {
|
|
20
127
|
name = "agent-sdk";
|
|
@@ -25,18 +132,26 @@ var AgentSDKProvider = class {
|
|
|
25
132
|
return this.query(systemPrompt, userPrompt);
|
|
26
133
|
}
|
|
27
134
|
async query(systemPrompt, userPrompt) {
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
135
|
+
if (process.env.AGENTMEMORY_SDK_CHILD === "1") return "";
|
|
136
|
+
const prev = process.env.AGENTMEMORY_SDK_CHILD;
|
|
137
|
+
process.env.AGENTMEMORY_SDK_CHILD = "1";
|
|
138
|
+
try {
|
|
139
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
140
|
+
const messages = query({
|
|
141
|
+
prompt: userPrompt,
|
|
142
|
+
options: {
|
|
143
|
+
systemPrompt,
|
|
144
|
+
maxTurns: 1,
|
|
145
|
+
allowedTools: []
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
let result = "";
|
|
149
|
+
for await (const msg of messages) if (msg.type === "result") result = msg.result ?? "";
|
|
150
|
+
return result;
|
|
151
|
+
} finally {
|
|
152
|
+
if (prev === void 0) delete process.env.AGENTMEMORY_SDK_CHILD;
|
|
153
|
+
else process.env.AGENTMEMORY_SDK_CHILD = prev;
|
|
154
|
+
}
|
|
40
155
|
}
|
|
41
156
|
};
|
|
42
157
|
|
|
@@ -61,6 +176,26 @@ var AnthropicProvider = class {
|
|
|
61
176
|
async summarize(systemPrompt, userPrompt) {
|
|
62
177
|
return this.call(systemPrompt, userPrompt);
|
|
63
178
|
}
|
|
179
|
+
async describeImage(imageData, mimeType, prompt) {
|
|
180
|
+
return (await this.client.messages.create({
|
|
181
|
+
model: this.model,
|
|
182
|
+
max_tokens: this.maxTokens,
|
|
183
|
+
messages: [{
|
|
184
|
+
role: "user",
|
|
185
|
+
content: [{
|
|
186
|
+
type: "image",
|
|
187
|
+
source: {
|
|
188
|
+
type: "base64",
|
|
189
|
+
media_type: mimeType,
|
|
190
|
+
data: imageData
|
|
191
|
+
}
|
|
192
|
+
}, {
|
|
193
|
+
type: "text",
|
|
194
|
+
text: prompt
|
|
195
|
+
}]
|
|
196
|
+
}]
|
|
197
|
+
})).content.find((b) => b.type === "text")?.text ?? "";
|
|
198
|
+
}
|
|
64
199
|
async call(systemPrompt, userPrompt) {
|
|
65
200
|
return (await this.client.messages.create({
|
|
66
201
|
model: this.model,
|
|
@@ -135,6 +270,25 @@ var MinimaxProvider = class {
|
|
|
135
270
|
}
|
|
136
271
|
};
|
|
137
272
|
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/providers/noop.ts
|
|
275
|
+
/**
|
|
276
|
+
* Returns empty strings for every call. Used when no LLM API key is set
|
|
277
|
+
* AND the user has not opted into the agent-sdk fallback via
|
|
278
|
+
* AGENTMEMORY_ALLOW_AGENT_SDK=true. Callers (compress, summarize) must
|
|
279
|
+
* detect the empty result and short-circuit instead of spawning a
|
|
280
|
+
* provider session (#149 / Stop-hook recursion loop fix).
|
|
281
|
+
*/
|
|
282
|
+
var NoopProvider = class {
|
|
283
|
+
name = "noop";
|
|
284
|
+
async compress() {
|
|
285
|
+
return "";
|
|
286
|
+
}
|
|
287
|
+
async summarize() {
|
|
288
|
+
return "";
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
138
292
|
//#endregion
|
|
139
293
|
//#region src/providers/openrouter.ts
|
|
140
294
|
var OpenRouterProvider = class {
|
|
@@ -346,28 +500,67 @@ var GeminiEmbeddingProvider = class {
|
|
|
346
500
|
|
|
347
501
|
//#endregion
|
|
348
502
|
//#region src/providers/embedding/openai.ts
|
|
349
|
-
const
|
|
503
|
+
const DEFAULT_BASE_URL = "https://api.openai.com";
|
|
504
|
+
const DEFAULT_MODEL$1 = "text-embedding-3-small";
|
|
505
|
+
/**
|
|
506
|
+
* Known OpenAI embedding model dimensions. Extend as new models ship.
|
|
507
|
+
* Override in any case via OPENAI_EMBEDDING_DIMENSIONS for custom or
|
|
508
|
+
* self-hosted OpenAI-compatible endpoints returning non-standard sizes.
|
|
509
|
+
*/
|
|
510
|
+
const MODEL_DIMENSIONS = {
|
|
511
|
+
"text-embedding-3-small": 1536,
|
|
512
|
+
"text-embedding-3-large": 3072,
|
|
513
|
+
"text-embedding-ada-002": 1536
|
|
514
|
+
};
|
|
515
|
+
const DEFAULT_DIMENSIONS = MODEL_DIMENSIONS[DEFAULT_MODEL$1] ?? 1536;
|
|
516
|
+
function resolveDimensions(model, override) {
|
|
517
|
+
if (override !== void 0 && override.trim().length > 0) {
|
|
518
|
+
const parsed = parseInt(override, 10);
|
|
519
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`OPENAI_EMBEDDING_DIMENSIONS must be a positive integer, got: ${override}`);
|
|
520
|
+
return parsed;
|
|
521
|
+
}
|
|
522
|
+
return MODEL_DIMENSIONS[model] ?? DEFAULT_DIMENSIONS;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* OpenAI-compatible embedding provider.
|
|
526
|
+
*
|
|
527
|
+
* Required env vars:
|
|
528
|
+
* OPENAI_API_KEY — API key
|
|
529
|
+
*
|
|
530
|
+
* Optional:
|
|
531
|
+
* OPENAI_BASE_URL — base URL without path (default: https://api.openai.com)
|
|
532
|
+
* OPENAI_EMBEDDING_MODEL — model name (default: text-embedding-3-small)
|
|
533
|
+
* OPENAI_EMBEDDING_DIMENSIONS — override reported dimensions (required for
|
|
534
|
+
* custom / self-hosted models not in the
|
|
535
|
+
* MODEL_DIMENSIONS table above)
|
|
536
|
+
*/
|
|
350
537
|
var OpenAIEmbeddingProvider = class {
|
|
351
538
|
name = "openai";
|
|
352
|
-
dimensions
|
|
539
|
+
dimensions;
|
|
353
540
|
apiKey;
|
|
541
|
+
baseUrl;
|
|
542
|
+
model;
|
|
354
543
|
constructor(apiKey) {
|
|
355
544
|
this.apiKey = apiKey || getEnvVar("OPENAI_API_KEY") || "";
|
|
356
545
|
if (!this.apiKey) throw new Error("OPENAI_API_KEY is required");
|
|
546
|
+
this.baseUrl = getEnvVar("OPENAI_BASE_URL") || DEFAULT_BASE_URL;
|
|
547
|
+
this.model = getEnvVar("OPENAI_EMBEDDING_MODEL") || DEFAULT_MODEL$1;
|
|
548
|
+
this.dimensions = resolveDimensions(this.model, getEnvVar("OPENAI_EMBEDDING_DIMENSIONS"));
|
|
357
549
|
}
|
|
358
550
|
async embed(text) {
|
|
359
551
|
const [result] = await this.embedBatch([text]);
|
|
360
552
|
return result;
|
|
361
553
|
}
|
|
362
554
|
async embedBatch(texts) {
|
|
363
|
-
const
|
|
555
|
+
const url = `${this.baseUrl}/v1/embeddings`;
|
|
556
|
+
const response = await fetch(url, {
|
|
364
557
|
method: "POST",
|
|
365
558
|
headers: {
|
|
366
559
|
Authorization: `Bearer ${this.apiKey}`,
|
|
367
560
|
"Content-Type": "application/json"
|
|
368
561
|
},
|
|
369
562
|
body: JSON.stringify({
|
|
370
|
-
model:
|
|
563
|
+
model: this.model,
|
|
371
564
|
input: texts
|
|
372
565
|
})
|
|
373
566
|
});
|
|
@@ -508,7 +701,7 @@ var LocalEmbeddingProvider = class {
|
|
|
508
701
|
if (this.extractor) return this.extractor;
|
|
509
702
|
let transformers;
|
|
510
703
|
try {
|
|
511
|
-
transformers = await import("
|
|
704
|
+
transformers = await import("@xenova/transformers");
|
|
512
705
|
} catch {
|
|
513
706
|
throw new Error("Install @xenova/transformers for local embeddings: npm install @xenova/transformers");
|
|
514
707
|
}
|
|
@@ -517,8 +710,86 @@ var LocalEmbeddingProvider = class {
|
|
|
517
710
|
}
|
|
518
711
|
};
|
|
519
712
|
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region src/providers/embedding/clip.ts
|
|
715
|
+
const DEFAULT_MODEL = "Xenova/clip-vit-base-patch32";
|
|
716
|
+
const DIMENSIONS = 512;
|
|
717
|
+
var ClipEmbeddingProvider = class {
|
|
718
|
+
name = "clip";
|
|
719
|
+
dimensions = DIMENSIONS;
|
|
720
|
+
textExtractor = null;
|
|
721
|
+
imageExtractor = null;
|
|
722
|
+
transformers = null;
|
|
723
|
+
modelId;
|
|
724
|
+
constructor(modelId = DEFAULT_MODEL) {
|
|
725
|
+
this.modelId = modelId;
|
|
726
|
+
}
|
|
727
|
+
async embed(text) {
|
|
728
|
+
const [vec] = await this.embedBatch([text]);
|
|
729
|
+
return vec;
|
|
730
|
+
}
|
|
731
|
+
async embedBatch(texts) {
|
|
732
|
+
return (await (await this.getTextExtractor())(texts, {
|
|
733
|
+
pooling: "mean",
|
|
734
|
+
normalize: true
|
|
735
|
+
})).tolist().map((v) => new Float32Array(v));
|
|
736
|
+
}
|
|
737
|
+
async embedImage(src) {
|
|
738
|
+
const image = await loadImage(await this.getTransformers(), src);
|
|
739
|
+
const output = await (await this.getImageExtractor())(image);
|
|
740
|
+
return normalize(output.data ?? new Float32Array(output.tolist()[0] || []));
|
|
741
|
+
}
|
|
742
|
+
async getTransformers() {
|
|
743
|
+
if (this.transformers) return this.transformers;
|
|
744
|
+
try {
|
|
745
|
+
this.transformers = await import("@xenova/transformers");
|
|
746
|
+
} catch {
|
|
747
|
+
throw new Error("Install @xenova/transformers for CLIP image embeddings: npm install @xenova/transformers");
|
|
748
|
+
}
|
|
749
|
+
return this.transformers;
|
|
750
|
+
}
|
|
751
|
+
async getTextExtractor() {
|
|
752
|
+
if (this.textExtractor) return this.textExtractor;
|
|
753
|
+
this.textExtractor = await (await this.getTransformers()).pipeline("feature-extraction", this.modelId);
|
|
754
|
+
return this.textExtractor;
|
|
755
|
+
}
|
|
756
|
+
async getImageExtractor() {
|
|
757
|
+
if (this.imageExtractor) return this.imageExtractor;
|
|
758
|
+
this.imageExtractor = await (await this.getTransformers()).pipeline("image-feature-extraction", this.modelId);
|
|
759
|
+
return this.imageExtractor;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
async function loadImage(t, src) {
|
|
763
|
+
if (src.startsWith("data:")) {
|
|
764
|
+
const comma = src.indexOf(",");
|
|
765
|
+
const b64 = comma >= 0 ? src.slice(comma + 1) : src;
|
|
766
|
+
const buf = Buffer.from(b64, "base64");
|
|
767
|
+
const blob = new Blob([buf]);
|
|
768
|
+
return t.RawImage.fromBlob(blob);
|
|
769
|
+
}
|
|
770
|
+
const data = await readFile(src);
|
|
771
|
+
const blob = new Blob([data]);
|
|
772
|
+
return t.RawImage.fromBlob(blob);
|
|
773
|
+
}
|
|
774
|
+
function normalize(vec) {
|
|
775
|
+
let sum = 0;
|
|
776
|
+
for (let i = 0; i < vec.length; i++) sum += vec[i] * vec[i];
|
|
777
|
+
const norm = Math.sqrt(sum);
|
|
778
|
+
if (norm === 0) return vec;
|
|
779
|
+
const out = new Float32Array(vec.length);
|
|
780
|
+
for (let i = 0; i < vec.length; i++) out[i] = vec[i] / norm;
|
|
781
|
+
return out;
|
|
782
|
+
}
|
|
783
|
+
|
|
520
784
|
//#endregion
|
|
521
785
|
//#region src/providers/embedding/index.ts
|
|
786
|
+
let imageEmbeddingProvider = null;
|
|
787
|
+
function createImageEmbeddingProvider() {
|
|
788
|
+
if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] !== "true") return null;
|
|
789
|
+
if (imageEmbeddingProvider) return imageEmbeddingProvider;
|
|
790
|
+
imageEmbeddingProvider = new ClipEmbeddingProvider();
|
|
791
|
+
return imageEmbeddingProvider;
|
|
792
|
+
}
|
|
522
793
|
function createEmbeddingProvider() {
|
|
523
794
|
const detected = detectEmbeddingProvider();
|
|
524
795
|
if (!detected) return null;
|
|
@@ -570,6 +841,7 @@ function createBaseProvider(config) {
|
|
|
570
841
|
return new OpenRouterProvider(geminiKey, config.model, config.maxTokens, "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions");
|
|
571
842
|
}
|
|
572
843
|
case "openrouter": return new OpenRouterProvider(requireEnvVar("OPENROUTER_API_KEY"), config.model, config.maxTokens, "https://openrouter.ai/api/v1/chat/completions");
|
|
844
|
+
case "noop": return new NoopProvider();
|
|
573
845
|
default: return new AgentSDKProvider();
|
|
574
846
|
}
|
|
575
847
|
}
|
|
@@ -1105,7 +1377,7 @@ async function loadPipeline() {
|
|
|
1105
1377
|
if (pipelineLoading) return pipelineLoading;
|
|
1106
1378
|
pipelineLoading = (async () => {
|
|
1107
1379
|
try {
|
|
1108
|
-
const { pipeline: createPipeline } = await import("
|
|
1380
|
+
const { pipeline: createPipeline } = await import("@xenova/transformers");
|
|
1109
1381
|
pipeline = await createPipeline("text-classification", "Xenova/ms-marco-MiniLM-L-6-v2", { quantized: true });
|
|
1110
1382
|
return pipeline;
|
|
1111
1383
|
} catch {
|
|
@@ -1822,19 +2094,6 @@ function registerPrivacyFunction(sdk) {
|
|
|
1822
2094
|
});
|
|
1823
2095
|
}
|
|
1824
2096
|
|
|
1825
|
-
//#endregion
|
|
1826
|
-
//#region src/state/keyed-mutex.ts
|
|
1827
|
-
const locks = /* @__PURE__ */ new Map();
|
|
1828
|
-
function withKeyedLock(key, fn) {
|
|
1829
|
-
const next = (locks.get(key) ?? Promise.resolve()).then(fn, fn);
|
|
1830
|
-
const cleanup = next.then(() => {}, () => {});
|
|
1831
|
-
locks.set(key, cleanup);
|
|
1832
|
-
cleanup.then(() => {
|
|
1833
|
-
if (locks.get(key) === cleanup) locks.delete(key);
|
|
1834
|
-
});
|
|
1835
|
-
return next;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
2097
|
//#endregion
|
|
1839
2098
|
//#region src/functions/compress-synthetic.ts
|
|
1840
2099
|
function inferType(toolName, hookType) {
|
|
@@ -1911,7 +2170,7 @@ function buildSyntheticCompression(raw) {
|
|
|
1911
2170
|
inputStr,
|
|
1912
2171
|
outputStr
|
|
1913
2172
|
].filter((s) => s.length > 0);
|
|
1914
|
-
|
|
2173
|
+
const result = {
|
|
1915
2174
|
id: raw.id,
|
|
1916
2175
|
sessionId: raw.sessionId,
|
|
1917
2176
|
timestamp: raw.timestamp,
|
|
@@ -1925,6 +2184,9 @@ function buildSyntheticCompression(raw) {
|
|
|
1925
2184
|
importance: 5,
|
|
1926
2185
|
confidence: .3
|
|
1927
2186
|
};
|
|
2187
|
+
if (raw.modality) result.modality = raw.modality;
|
|
2188
|
+
if (raw.imageData) result.imageData = raw.imageData;
|
|
2189
|
+
return result;
|
|
1928
2190
|
}
|
|
1929
2191
|
|
|
1930
2192
|
//#endregion
|
|
@@ -2172,6 +2434,24 @@ function registerSearchFunction(sdk, kv) {
|
|
|
2172
2434
|
|
|
2173
2435
|
//#endregion
|
|
2174
2436
|
//#region src/functions/observe.ts
|
|
2437
|
+
function extractImage(d) {
|
|
2438
|
+
if (!d) return void 0;
|
|
2439
|
+
if (typeof d === "string") {
|
|
2440
|
+
if (d.startsWith("data:image/") || d.startsWith("iVBORw0KGgo") || d.startsWith("/9j/")) return d;
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
if (typeof d === "object" && d !== null) {
|
|
2444
|
+
const obj = d;
|
|
2445
|
+
if (typeof obj["image_data"] === "string") return obj["image_data"];
|
|
2446
|
+
if (typeof obj["image_path"] === "string") return obj["image_path"];
|
|
2447
|
+
if (typeof obj["imageBase64"] === "string") return obj["imageBase64"];
|
|
2448
|
+
if (typeof obj["imagePath"] === "string") return obj["imagePath"];
|
|
2449
|
+
for (const key of Object.keys(obj)) {
|
|
2450
|
+
const match = extractImage(obj[key]);
|
|
2451
|
+
if (match) return match;
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2175
2455
|
function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
2176
2456
|
sdk.registerFunction("mem::observe", async (payload) => {
|
|
2177
2457
|
if (!payload?.sessionId || typeof payload.sessionId !== "string" || !payload.hookType || typeof payload.hookType !== "string" || !payload.timestamp || typeof payload.timestamp !== "string") return {
|
|
@@ -2203,6 +2483,7 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2203
2483
|
hookType: payload.hookType,
|
|
2204
2484
|
raw: sanitizedRaw
|
|
2205
2485
|
};
|
|
2486
|
+
let extractedImage;
|
|
2206
2487
|
if (typeof sanitizedRaw === "object" && sanitizedRaw !== null) {
|
|
2207
2488
|
const d = sanitizedRaw;
|
|
2208
2489
|
if (payload.hookType === "post_tool_use" || payload.hookType === "post_tool_failure") {
|
|
@@ -2211,7 +2492,13 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2211
2492
|
raw.toolOutput = d["tool_output"] || d["error"];
|
|
2212
2493
|
}
|
|
2213
2494
|
if (payload.hookType === "prompt_submit") raw.userPrompt = d["prompt"];
|
|
2495
|
+
extractedImage = extractImage(sanitizedRaw);
|
|
2496
|
+
if (extractedImage) raw.modality = raw.toolInput || raw.toolOutput || raw.userPrompt ? "mixed" : "image";
|
|
2497
|
+
} else if (typeof sanitizedRaw === "string") {
|
|
2498
|
+
extractedImage = extractImage(sanitizedRaw);
|
|
2499
|
+
if (extractedImage) raw.modality = "image";
|
|
2214
2500
|
}
|
|
2501
|
+
const pendingImageData = extractedImage;
|
|
2215
2502
|
return withKeyedLock(`obs:${payload.sessionId}`, async () => {
|
|
2216
2503
|
if (maxObservationsPerSession && maxObservationsPerSession > 0) {
|
|
2217
2504
|
if ((await kv.list(KV.observations(payload.sessionId))).length >= maxObservationsPerSession) return {
|
|
@@ -2219,7 +2506,29 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2219
2506
|
error: `Session observation limit reached (${maxObservationsPerSession})`
|
|
2220
2507
|
};
|
|
2221
2508
|
}
|
|
2222
|
-
|
|
2509
|
+
if (pendingImageData && (pendingImageData.startsWith("data:image/") || pendingImageData.startsWith("iVBORw0KGgo") || pendingImageData.startsWith("/9j/"))) {
|
|
2510
|
+
const { saveImageToDisk } = await import("./image-store-BLOkD0xV.mjs");
|
|
2511
|
+
const { filePath, bytesWritten } = await saveImageToDisk(pendingImageData);
|
|
2512
|
+
raw.imageData = filePath;
|
|
2513
|
+
const { incrementImageRef } = await import("./image-refs-Dq5wcV-a.mjs");
|
|
2514
|
+
await incrementImageRef(kv, filePath);
|
|
2515
|
+
sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: bytesWritten });
|
|
2516
|
+
if (process.env["AGENTMEMORY_IMAGE_EMBEDDINGS"] === "true") sdk.triggerVoid("mem::vision-embed", {
|
|
2517
|
+
imageRef: filePath,
|
|
2518
|
+
sessionId: payload.sessionId,
|
|
2519
|
+
observationId: obsId
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
try {
|
|
2523
|
+
await kv.set(KV.observations(payload.sessionId), obsId, raw);
|
|
2524
|
+
} catch (error) {
|
|
2525
|
+
if (raw.imageData) {
|
|
2526
|
+
const { deleteImage } = await import("./image-store-BLOkD0xV.mjs");
|
|
2527
|
+
const { deletedBytes } = await deleteImage(raw.imageData);
|
|
2528
|
+
if (deletedBytes > 0) sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
|
|
2529
|
+
}
|
|
2530
|
+
throw error;
|
|
2531
|
+
}
|
|
2223
2532
|
if (dedupMap && dedupHash) dedupMap.record(dedupHash);
|
|
2224
2533
|
await sdk.trigger({
|
|
2225
2534
|
function_id: "stream::set",
|
|
@@ -2249,15 +2558,26 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2249
2558
|
action: TriggerAction.Void()
|
|
2250
2559
|
});
|
|
2251
2560
|
const session = await kv.get(KV.sessions, payload.sessionId);
|
|
2252
|
-
if (session)
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2561
|
+
if (session) {
|
|
2562
|
+
const updates = [{
|
|
2563
|
+
type: "set",
|
|
2564
|
+
path: "updatedAt",
|
|
2565
|
+
value: (/* @__PURE__ */ new Date()).toISOString()
|
|
2566
|
+
}, {
|
|
2567
|
+
type: "set",
|
|
2568
|
+
path: "observationCount",
|
|
2569
|
+
value: (session.observationCount || 0) + 1
|
|
2570
|
+
}];
|
|
2571
|
+
if (!session.firstPrompt && typeof raw.userPrompt === "string") {
|
|
2572
|
+
const trimmed = raw.userPrompt.replace(/\s+/g, " ").trim();
|
|
2573
|
+
if (trimmed.length > 0) updates.push({
|
|
2574
|
+
type: "set",
|
|
2575
|
+
path: "firstPrompt",
|
|
2576
|
+
value: trimmed.slice(0, 200)
|
|
2577
|
+
});
|
|
2578
|
+
}
|
|
2579
|
+
await kv.update(KV.sessions, payload.sessionId, updates);
|
|
2580
|
+
}
|
|
2261
2581
|
if (isAutoCompressEnabled()) await sdk.trigger({
|
|
2262
2582
|
function_id: "mem::compress",
|
|
2263
2583
|
payload: {
|
|
@@ -2308,6 +2628,708 @@ function registerObserveFunction(sdk, kv, dedupMap, maxObservationsPerSession) {
|
|
|
2308
2628
|
});
|
|
2309
2629
|
}
|
|
2310
2630
|
|
|
2631
|
+
//#endregion
|
|
2632
|
+
//#region src/functions/image-quota-cleanup.ts
|
|
2633
|
+
const GRACE_PERIOD_MS = 3e4;
|
|
2634
|
+
function registerImageQuotaCleanup(sdk, kv) {
|
|
2635
|
+
sdk.registerFunction("mem::image-quota-cleanup", async () => {
|
|
2636
|
+
const now = Date.now();
|
|
2637
|
+
return withKeyedLock("system:cleanupLock", async () => {
|
|
2638
|
+
let totalSize = 0;
|
|
2639
|
+
const fileStats = [];
|
|
2640
|
+
try {
|
|
2641
|
+
const files = await readdir(IMAGES_DIR);
|
|
2642
|
+
for (const file of files) {
|
|
2643
|
+
if (file.startsWith(".")) continue;
|
|
2644
|
+
const filePath = join(IMAGES_DIR, file);
|
|
2645
|
+
const s = await stat(filePath);
|
|
2646
|
+
if (s.isFile()) {
|
|
2647
|
+
fileStats.push({
|
|
2648
|
+
filePath,
|
|
2649
|
+
size: s.size,
|
|
2650
|
+
mtimeMs: s.mtimeMs
|
|
2651
|
+
});
|
|
2652
|
+
totalSize += s.size;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
} catch {
|
|
2656
|
+
return {
|
|
2657
|
+
success: true,
|
|
2658
|
+
evicted: 0,
|
|
2659
|
+
freedBytes: 0
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
const limit = getMaxBytes();
|
|
2663
|
+
if (totalSize <= limit) return {
|
|
2664
|
+
success: true,
|
|
2665
|
+
evicted: 0,
|
|
2666
|
+
freedBytes: 0,
|
|
2667
|
+
underQuota: true
|
|
2668
|
+
};
|
|
2669
|
+
fileStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2670
|
+
let totalToFree = totalSize - limit;
|
|
2671
|
+
let evicted = 0;
|
|
2672
|
+
let freedBytes = 0;
|
|
2673
|
+
for (const f of fileStats) {
|
|
2674
|
+
if (totalToFree <= 0) break;
|
|
2675
|
+
if (now - f.mtimeMs < GRACE_PERIOD_MS) continue;
|
|
2676
|
+
await withKeyedLock(`imgRef:${f.filePath}`, async () => {
|
|
2677
|
+
let refCount;
|
|
2678
|
+
try {
|
|
2679
|
+
refCount = await getImageRefCount(kv, f.filePath);
|
|
2680
|
+
} catch (err) {
|
|
2681
|
+
logger.error("Failed to read refCount; skipping eviction", {
|
|
2682
|
+
filePath: f.filePath,
|
|
2683
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2684
|
+
});
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
if (refCount > 0) return;
|
|
2688
|
+
const { deletedBytes } = await deleteImage(f.filePath);
|
|
2689
|
+
if (deletedBytes > 0) {
|
|
2690
|
+
sdk.triggerVoid("mem::disk-size-delta", { deltaBytes: -deletedBytes });
|
|
2691
|
+
totalToFree -= deletedBytes;
|
|
2692
|
+
freedBytes += deletedBytes;
|
|
2693
|
+
evicted++;
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
}
|
|
2697
|
+
if (evicted > 0) {
|
|
2698
|
+
const freedMb = (freedBytes / (1024 * 1024)).toFixed(1);
|
|
2699
|
+
logger.info("Image quota cleanup complete", {
|
|
2700
|
+
evicted,
|
|
2701
|
+
freedMb
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
return {
|
|
2705
|
+
success: true,
|
|
2706
|
+
evicted,
|
|
2707
|
+
freedBytes
|
|
2708
|
+
};
|
|
2709
|
+
});
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
//#endregion
|
|
2714
|
+
//#region src/functions/audit.ts
|
|
2715
|
+
async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
|
|
2716
|
+
const entry = {
|
|
2717
|
+
id: generateId("aud"),
|
|
2718
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2719
|
+
operation,
|
|
2720
|
+
userId,
|
|
2721
|
+
functionId,
|
|
2722
|
+
targetIds,
|
|
2723
|
+
details,
|
|
2724
|
+
qualityScore
|
|
2725
|
+
};
|
|
2726
|
+
await kv.set(KV.audit, entry.id, entry);
|
|
2727
|
+
return entry;
|
|
2728
|
+
}
|
|
2729
|
+
async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
|
|
2730
|
+
try {
|
|
2731
|
+
await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
|
|
2732
|
+
} catch (err) {
|
|
2733
|
+
try {
|
|
2734
|
+
logger.warn("audit write failed", {
|
|
2735
|
+
functionId,
|
|
2736
|
+
operation,
|
|
2737
|
+
targetIds,
|
|
2738
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2739
|
+
});
|
|
2740
|
+
} catch {}
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
async function queryAudit(kv, filter) {
|
|
2744
|
+
let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
2745
|
+
if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
|
|
2746
|
+
if (filter?.dateFrom) {
|
|
2747
|
+
const from = new Date(filter.dateFrom).getTime();
|
|
2748
|
+
if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
|
|
2749
|
+
entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
|
|
2750
|
+
}
|
|
2751
|
+
if (filter?.dateTo) {
|
|
2752
|
+
const to = new Date(filter.dateTo).getTime();
|
|
2753
|
+
if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
|
|
2754
|
+
entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
|
|
2755
|
+
}
|
|
2756
|
+
return entries.slice(0, filter?.limit || 100);
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
//#endregion
|
|
2760
|
+
//#region src/functions/vision-search.ts
|
|
2761
|
+
function registerVisionSearchFunctions(sdk, kv, imageProvider) {
|
|
2762
|
+
sdk.registerFunction("mem::vision-embed", async (data) => {
|
|
2763
|
+
if (!imageProvider?.embedImage) return {
|
|
2764
|
+
success: false,
|
|
2765
|
+
error: "image embeddings disabled (set AGENTMEMORY_IMAGE_EMBEDDINGS=true)"
|
|
2766
|
+
};
|
|
2767
|
+
if (!data?.imageRef || typeof data.imageRef !== "string") return {
|
|
2768
|
+
success: false,
|
|
2769
|
+
error: "imageRef required"
|
|
2770
|
+
};
|
|
2771
|
+
if (!isManagedImagePath(data.imageRef)) return {
|
|
2772
|
+
success: false,
|
|
2773
|
+
error: "imageRef must point to a file under the managed image store"
|
|
2774
|
+
};
|
|
2775
|
+
const refCount = await kv.get(KV.imageRefs, data.imageRef);
|
|
2776
|
+
if (!refCount || Number(refCount) < 1) return {
|
|
2777
|
+
success: false,
|
|
2778
|
+
error: "imageRef not registered in mem:image-refs"
|
|
2779
|
+
};
|
|
2780
|
+
try {
|
|
2781
|
+
const vec = await imageProvider.embedImage(data.imageRef);
|
|
2782
|
+
const stored = {
|
|
2783
|
+
imageRef: data.imageRef,
|
|
2784
|
+
vector: Array.from(vec),
|
|
2785
|
+
modelName: imageProvider.name,
|
|
2786
|
+
dimensions: imageProvider.dimensions,
|
|
2787
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2788
|
+
sessionId: data.sessionId,
|
|
2789
|
+
observationId: data.observationId
|
|
2790
|
+
};
|
|
2791
|
+
await kv.set(KV.imageEmbeddings, data.imageRef, stored);
|
|
2792
|
+
await recordAudit(kv, "vision_embed", "mem::vision-embed", [data.imageRef], {
|
|
2793
|
+
modelName: imageProvider.name,
|
|
2794
|
+
dimensions: stored.dimensions,
|
|
2795
|
+
sessionId: data.sessionId,
|
|
2796
|
+
observationId: data.observationId
|
|
2797
|
+
});
|
|
2798
|
+
return {
|
|
2799
|
+
success: true,
|
|
2800
|
+
imageRef: data.imageRef,
|
|
2801
|
+
dimensions: stored.dimensions
|
|
2802
|
+
};
|
|
2803
|
+
} catch (err) {
|
|
2804
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2805
|
+
logger.warn("vision-embed failed", {
|
|
2806
|
+
imageRef: data.imageRef,
|
|
2807
|
+
error: msg
|
|
2808
|
+
});
|
|
2809
|
+
return {
|
|
2810
|
+
success: false,
|
|
2811
|
+
error: msg
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
sdk.registerFunction("mem::vision-search", async (data) => {
|
|
2816
|
+
if (!imageProvider?.embedImage) return {
|
|
2817
|
+
success: false,
|
|
2818
|
+
error: "image embeddings disabled (set AGENTMEMORY_IMAGE_EMBEDDINGS=true)"
|
|
2819
|
+
};
|
|
2820
|
+
const requestedTopK = typeof data?.topK === "number" && Number.isFinite(data.topK) ? Math.trunc(data.topK) : 10;
|
|
2821
|
+
const topK = Math.min(50, Math.max(1, requestedTopK));
|
|
2822
|
+
let queryVec = null;
|
|
2823
|
+
try {
|
|
2824
|
+
if (data?.queryText) queryVec = await imageProvider.embed(data.queryText);
|
|
2825
|
+
else if (data?.queryImageBase64) {
|
|
2826
|
+
const b64 = data.queryImageBase64.startsWith("data:") ? data.queryImageBase64 : `data:image/png;base64,${data.queryImageBase64}`;
|
|
2827
|
+
queryVec = await imageProvider.embedImage(b64);
|
|
2828
|
+
} else if (data?.queryImageRef) {
|
|
2829
|
+
if (!isManagedImagePath(data.queryImageRef)) return {
|
|
2830
|
+
success: false,
|
|
2831
|
+
error: "queryImageRef must point to a file under the managed image store"
|
|
2832
|
+
};
|
|
2833
|
+
const refCount = await kv.get(KV.imageRefs, data.queryImageRef);
|
|
2834
|
+
if (!refCount || Number(refCount) < 1) return {
|
|
2835
|
+
success: false,
|
|
2836
|
+
error: "queryImageRef not registered in mem:image-refs"
|
|
2837
|
+
};
|
|
2838
|
+
queryVec = await imageProvider.embedImage(data.queryImageRef);
|
|
2839
|
+
} else return {
|
|
2840
|
+
success: false,
|
|
2841
|
+
error: "queryText, queryImageRef, or queryImageBase64 required"
|
|
2842
|
+
};
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
return {
|
|
2845
|
+
success: false,
|
|
2846
|
+
error: `query embed failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
if (!queryVec) return {
|
|
2850
|
+
success: false,
|
|
2851
|
+
error: "failed to build query vector"
|
|
2852
|
+
};
|
|
2853
|
+
const stored = await kv.list(KV.imageEmbeddings);
|
|
2854
|
+
const scored = (data?.sessionId ? stored.filter((s) => s.sessionId === data.sessionId) : stored).map((s) => ({
|
|
2855
|
+
imageRef: s.imageRef,
|
|
2856
|
+
score: cosine(queryVec, s.vector),
|
|
2857
|
+
sessionId: s.sessionId,
|
|
2858
|
+
observationId: s.observationId,
|
|
2859
|
+
updatedAt: s.updatedAt
|
|
2860
|
+
}));
|
|
2861
|
+
scored.sort((a, b) => b.score - a.score);
|
|
2862
|
+
return {
|
|
2863
|
+
success: true,
|
|
2864
|
+
results: scored.slice(0, topK),
|
|
2865
|
+
total: scored.length
|
|
2866
|
+
};
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
function cosine(a, b) {
|
|
2870
|
+
if (a.length !== b.length) return 0;
|
|
2871
|
+
let dot = 0;
|
|
2872
|
+
let normA = 0;
|
|
2873
|
+
let normB = 0;
|
|
2874
|
+
for (let i = 0; i < a.length; i++) {
|
|
2875
|
+
dot += a[i] * b[i];
|
|
2876
|
+
normA += a[i] * a[i];
|
|
2877
|
+
normB += b[i] * b[i];
|
|
2878
|
+
}
|
|
2879
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
2880
|
+
return denom === 0 ? 0 : dot / denom;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
//#endregion
|
|
2884
|
+
//#region src/functions/slots.ts
|
|
2885
|
+
const DEFAULT_SIZE_LIMIT = 2e3;
|
|
2886
|
+
const DEFAULT_SLOTS = [
|
|
2887
|
+
{
|
|
2888
|
+
label: "persona",
|
|
2889
|
+
content: "",
|
|
2890
|
+
sizeLimit: 1e3,
|
|
2891
|
+
description: "How the agent should see itself: role, tone, behavioural guidelines.",
|
|
2892
|
+
pinned: true,
|
|
2893
|
+
readOnly: false,
|
|
2894
|
+
scope: "global"
|
|
2895
|
+
},
|
|
2896
|
+
{
|
|
2897
|
+
label: "user_preferences",
|
|
2898
|
+
content: "",
|
|
2899
|
+
sizeLimit: 2e3,
|
|
2900
|
+
description: "Coding style, tool preferences, naming conventions, and other habits the user wants preserved across sessions.",
|
|
2901
|
+
pinned: true,
|
|
2902
|
+
readOnly: false,
|
|
2903
|
+
scope: "global"
|
|
2904
|
+
},
|
|
2905
|
+
{
|
|
2906
|
+
label: "tool_guidelines",
|
|
2907
|
+
content: "",
|
|
2908
|
+
sizeLimit: 1500,
|
|
2909
|
+
description: "Rules the agent should follow when picking or sequencing tools (e.g. prefer X over Y, never run Z without confirmation).",
|
|
2910
|
+
pinned: true,
|
|
2911
|
+
readOnly: false,
|
|
2912
|
+
scope: "global"
|
|
2913
|
+
},
|
|
2914
|
+
{
|
|
2915
|
+
label: "project_context",
|
|
2916
|
+
content: "",
|
|
2917
|
+
sizeLimit: 3e3,
|
|
2918
|
+
description: "Architecture decisions, codebase conventions, build/test commands, and cross-cutting constraints for the current project.",
|
|
2919
|
+
pinned: true,
|
|
2920
|
+
readOnly: false,
|
|
2921
|
+
scope: "project"
|
|
2922
|
+
},
|
|
2923
|
+
{
|
|
2924
|
+
label: "guidance",
|
|
2925
|
+
content: "",
|
|
2926
|
+
sizeLimit: 1500,
|
|
2927
|
+
description: "Active advice for the next session: what to focus on, what to avoid, open risks.",
|
|
2928
|
+
pinned: true,
|
|
2929
|
+
readOnly: false,
|
|
2930
|
+
scope: "project"
|
|
2931
|
+
},
|
|
2932
|
+
{
|
|
2933
|
+
label: "pending_items",
|
|
2934
|
+
content: "",
|
|
2935
|
+
sizeLimit: 2e3,
|
|
2936
|
+
description: "Unfinished work, explicit TODOs, and promises made but not yet delivered.",
|
|
2937
|
+
pinned: true,
|
|
2938
|
+
readOnly: false,
|
|
2939
|
+
scope: "project"
|
|
2940
|
+
},
|
|
2941
|
+
{
|
|
2942
|
+
label: "session_patterns",
|
|
2943
|
+
content: "",
|
|
2944
|
+
sizeLimit: 1500,
|
|
2945
|
+
description: "Recurring behaviours and common struggles observed across recent sessions.",
|
|
2946
|
+
pinned: false,
|
|
2947
|
+
readOnly: false,
|
|
2948
|
+
scope: "project"
|
|
2949
|
+
},
|
|
2950
|
+
{
|
|
2951
|
+
label: "self_notes",
|
|
2952
|
+
content: "",
|
|
2953
|
+
sizeLimit: 1500,
|
|
2954
|
+
description: "Free-form notes the agent keeps for itself: hypotheses, dead ends, things to revisit.",
|
|
2955
|
+
pinned: false,
|
|
2956
|
+
readOnly: false,
|
|
2957
|
+
scope: "project"
|
|
2958
|
+
}
|
|
2959
|
+
];
|
|
2960
|
+
function isSlotsEnabled() {
|
|
2961
|
+
return process.env["AGENTMEMORY_SLOTS"] === "true";
|
|
2962
|
+
}
|
|
2963
|
+
function isReflectEnabled() {
|
|
2964
|
+
return process.env["AGENTMEMORY_REFLECT"] === "true";
|
|
2965
|
+
}
|
|
2966
|
+
function scopeKv(scope) {
|
|
2967
|
+
return scope === "global" ? KV.globalSlots : KV.slots;
|
|
2968
|
+
}
|
|
2969
|
+
function nowIso() {
|
|
2970
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
2971
|
+
}
|
|
2972
|
+
function validateLabel(label) {
|
|
2973
|
+
if (typeof label !== "string") return null;
|
|
2974
|
+
const trimmed = label.trim();
|
|
2975
|
+
if (!trimmed || trimmed.length > 64) return null;
|
|
2976
|
+
if (!/^[a-z][a-z0-9_]*$/.test(trimmed)) return null;
|
|
2977
|
+
return trimmed;
|
|
2978
|
+
}
|
|
2979
|
+
async function readSlot(kv, label) {
|
|
2980
|
+
const project = await kv.get(KV.slots, label);
|
|
2981
|
+
if (project) return {
|
|
2982
|
+
slot: project,
|
|
2983
|
+
scope: "project"
|
|
2984
|
+
};
|
|
2985
|
+
const global = await kv.get(KV.globalSlots, label);
|
|
2986
|
+
if (global) return {
|
|
2987
|
+
slot: global,
|
|
2988
|
+
scope: "global"
|
|
2989
|
+
};
|
|
2990
|
+
return {
|
|
2991
|
+
slot: null,
|
|
2992
|
+
scope: "project"
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
async function readSlotInScope(kv, label, scope) {
|
|
2996
|
+
return kv.get(scopeKv(scope), label);
|
|
2997
|
+
}
|
|
2998
|
+
function validateScope(raw) {
|
|
2999
|
+
if (raw === void 0 || raw === null) return "project";
|
|
3000
|
+
if (raw === "project" || raw === "global") return raw;
|
|
3001
|
+
return null;
|
|
3002
|
+
}
|
|
3003
|
+
function validateSizeLimit(raw) {
|
|
3004
|
+
if (raw === void 0 || raw === null) return DEFAULT_SIZE_LIMIT;
|
|
3005
|
+
if (typeof raw !== "number") return null;
|
|
3006
|
+
if (!Number.isInteger(raw) || raw < 1 || raw > 2e4) return null;
|
|
3007
|
+
return raw;
|
|
3008
|
+
}
|
|
3009
|
+
async function seedDefaults(kv) {
|
|
3010
|
+
const ts = nowIso();
|
|
3011
|
+
for (const tmpl of DEFAULT_SLOTS) {
|
|
3012
|
+
const target = scopeKv(tmpl.scope);
|
|
3013
|
+
if (await kv.get(target, tmpl.label)) continue;
|
|
3014
|
+
const slot = {
|
|
3015
|
+
...tmpl,
|
|
3016
|
+
createdAt: ts,
|
|
3017
|
+
updatedAt: ts
|
|
3018
|
+
};
|
|
3019
|
+
await kv.set(target, tmpl.label, slot);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
function registerSlotsFunctions(sdk, kv) {
|
|
3023
|
+
seedDefaults(kv).catch((err) => {
|
|
3024
|
+
logger.warn("slot defaults seed failed", { error: err instanceof Error ? err.message : String(err) });
|
|
3025
|
+
});
|
|
3026
|
+
sdk.registerFunction("mem::slot-list", async () => {
|
|
3027
|
+
const [project, global] = await Promise.all([kv.list(KV.slots), kv.list(KV.globalSlots)]);
|
|
3028
|
+
const merged = /* @__PURE__ */ new Map();
|
|
3029
|
+
for (const s of global) merged.set(s.label, s);
|
|
3030
|
+
for (const s of project) merged.set(s.label, s);
|
|
3031
|
+
return {
|
|
3032
|
+
success: true,
|
|
3033
|
+
slots: Array.from(merged.values()).sort((a, b) => a.label.localeCompare(b.label))
|
|
3034
|
+
};
|
|
3035
|
+
});
|
|
3036
|
+
sdk.registerFunction("mem::slot-get", async (data) => {
|
|
3037
|
+
const label = validateLabel(data?.label);
|
|
3038
|
+
if (!label) return {
|
|
3039
|
+
success: false,
|
|
3040
|
+
error: "label required (lowercase, starts with letter, [a-z0-9_])"
|
|
3041
|
+
};
|
|
3042
|
+
const { slot, scope } = await readSlot(kv, label);
|
|
3043
|
+
if (!slot) return {
|
|
3044
|
+
success: false,
|
|
3045
|
+
error: "slot not found"
|
|
3046
|
+
};
|
|
3047
|
+
return {
|
|
3048
|
+
success: true,
|
|
3049
|
+
slot,
|
|
3050
|
+
scope
|
|
3051
|
+
};
|
|
3052
|
+
});
|
|
3053
|
+
sdk.registerFunction("mem::slot-create", async (data) => {
|
|
3054
|
+
const label = validateLabel(data?.label);
|
|
3055
|
+
if (!label) return {
|
|
3056
|
+
success: false,
|
|
3057
|
+
error: "label required (lowercase, starts with letter, [a-z0-9_])"
|
|
3058
|
+
};
|
|
3059
|
+
const scope = validateScope(data?.scope);
|
|
3060
|
+
if (!scope) return {
|
|
3061
|
+
success: false,
|
|
3062
|
+
error: "scope must be 'project' or 'global'"
|
|
3063
|
+
};
|
|
3064
|
+
const sizeLimit = validateSizeLimit(data?.sizeLimit);
|
|
3065
|
+
if (sizeLimit === null) return {
|
|
3066
|
+
success: false,
|
|
3067
|
+
error: "sizeLimit must be an integer between 1 and 20000"
|
|
3068
|
+
};
|
|
3069
|
+
const content = typeof data?.content === "string" ? data.content : "";
|
|
3070
|
+
if (content.length > sizeLimit) return {
|
|
3071
|
+
success: false,
|
|
3072
|
+
error: `content exceeds sizeLimit (${content.length} > ${sizeLimit})`
|
|
3073
|
+
};
|
|
3074
|
+
const description = typeof data?.description === "string" ? data.description : "";
|
|
3075
|
+
const pinned = typeof data?.pinned === "boolean" ? data.pinned : true;
|
|
3076
|
+
return withKeyedLock(`slot:${label}`, async () => {
|
|
3077
|
+
if (await readSlotInScope(kv, label, scope)) return {
|
|
3078
|
+
success: false,
|
|
3079
|
+
error: `slot already exists in ${scope} scope`
|
|
3080
|
+
};
|
|
3081
|
+
const ts = nowIso();
|
|
3082
|
+
const slot = {
|
|
3083
|
+
label,
|
|
3084
|
+
content,
|
|
3085
|
+
sizeLimit,
|
|
3086
|
+
description,
|
|
3087
|
+
pinned,
|
|
3088
|
+
readOnly: false,
|
|
3089
|
+
scope,
|
|
3090
|
+
createdAt: ts,
|
|
3091
|
+
updatedAt: ts
|
|
3092
|
+
};
|
|
3093
|
+
await kv.set(scopeKv(scope), label, slot);
|
|
3094
|
+
await recordAudit(kv, "slot_create", "mem::slot-create", [label], {
|
|
3095
|
+
scope,
|
|
3096
|
+
sizeLimit: slot.sizeLimit,
|
|
3097
|
+
pinned: slot.pinned
|
|
3098
|
+
});
|
|
3099
|
+
return {
|
|
3100
|
+
success: true,
|
|
3101
|
+
slot
|
|
3102
|
+
};
|
|
3103
|
+
});
|
|
3104
|
+
});
|
|
3105
|
+
sdk.registerFunction("mem::slot-append", async (data) => {
|
|
3106
|
+
const label = validateLabel(data?.label);
|
|
3107
|
+
if (!label) return {
|
|
3108
|
+
success: false,
|
|
3109
|
+
error: "label required"
|
|
3110
|
+
};
|
|
3111
|
+
const text = typeof data?.text === "string" ? data.text : "";
|
|
3112
|
+
if (!text) return {
|
|
3113
|
+
success: false,
|
|
3114
|
+
error: "text required"
|
|
3115
|
+
};
|
|
3116
|
+
return withKeyedLock(`slot:${label}`, async () => {
|
|
3117
|
+
const { slot, scope } = await readSlot(kv, label);
|
|
3118
|
+
if (!slot) return {
|
|
3119
|
+
success: false,
|
|
3120
|
+
error: "slot not found (use mem::slot-create first)"
|
|
3121
|
+
};
|
|
3122
|
+
if (slot.readOnly) return {
|
|
3123
|
+
success: false,
|
|
3124
|
+
error: "slot is read-only"
|
|
3125
|
+
};
|
|
3126
|
+
const sep = slot.content && !slot.content.endsWith("\n") ? "\n" : "";
|
|
3127
|
+
const next = `${slot.content}${sep}${text}`;
|
|
3128
|
+
if (next.length > slot.sizeLimit) return {
|
|
3129
|
+
success: false,
|
|
3130
|
+
error: `append would exceed sizeLimit (${next.length} > ${slot.sizeLimit}). Use mem::slot-replace to compact first.`,
|
|
3131
|
+
currentSize: slot.content.length,
|
|
3132
|
+
sizeLimit: slot.sizeLimit
|
|
3133
|
+
};
|
|
3134
|
+
const updated = {
|
|
3135
|
+
...slot,
|
|
3136
|
+
content: next,
|
|
3137
|
+
updatedAt: nowIso()
|
|
3138
|
+
};
|
|
3139
|
+
await kv.set(scopeKv(scope), label, updated);
|
|
3140
|
+
await recordAudit(kv, "slot_append", "mem::slot-append", [label], {
|
|
3141
|
+
scope,
|
|
3142
|
+
added: text.length,
|
|
3143
|
+
total: next.length
|
|
3144
|
+
});
|
|
3145
|
+
return {
|
|
3146
|
+
success: true,
|
|
3147
|
+
slot: updated,
|
|
3148
|
+
size: next.length
|
|
3149
|
+
};
|
|
3150
|
+
});
|
|
3151
|
+
});
|
|
3152
|
+
sdk.registerFunction("mem::slot-replace", async (data) => {
|
|
3153
|
+
const label = validateLabel(data?.label);
|
|
3154
|
+
if (!label) return {
|
|
3155
|
+
success: false,
|
|
3156
|
+
error: "label required"
|
|
3157
|
+
};
|
|
3158
|
+
if (typeof data?.content !== "string") return {
|
|
3159
|
+
success: false,
|
|
3160
|
+
error: "content required (string)"
|
|
3161
|
+
};
|
|
3162
|
+
return withKeyedLock(`slot:${label}`, async () => {
|
|
3163
|
+
const { slot, scope } = await readSlot(kv, label);
|
|
3164
|
+
if (!slot) return {
|
|
3165
|
+
success: false,
|
|
3166
|
+
error: "slot not found (use mem::slot-create first)"
|
|
3167
|
+
};
|
|
3168
|
+
if (slot.readOnly) return {
|
|
3169
|
+
success: false,
|
|
3170
|
+
error: "slot is read-only"
|
|
3171
|
+
};
|
|
3172
|
+
if (data.content.length > slot.sizeLimit) return {
|
|
3173
|
+
success: false,
|
|
3174
|
+
error: `content exceeds sizeLimit (${data.content.length} > ${slot.sizeLimit})`,
|
|
3175
|
+
sizeLimit: slot.sizeLimit
|
|
3176
|
+
};
|
|
3177
|
+
const updated = {
|
|
3178
|
+
...slot,
|
|
3179
|
+
content: data.content,
|
|
3180
|
+
updatedAt: nowIso()
|
|
3181
|
+
};
|
|
3182
|
+
await kv.set(scopeKv(scope), label, updated);
|
|
3183
|
+
await recordAudit(kv, "slot_replace", "mem::slot-replace", [label], {
|
|
3184
|
+
scope,
|
|
3185
|
+
before: slot.content.length,
|
|
3186
|
+
after: data.content.length
|
|
3187
|
+
});
|
|
3188
|
+
return {
|
|
3189
|
+
success: true,
|
|
3190
|
+
slot: updated,
|
|
3191
|
+
size: data.content.length
|
|
3192
|
+
};
|
|
3193
|
+
});
|
|
3194
|
+
});
|
|
3195
|
+
sdk.registerFunction("mem::slot-delete", async (data) => {
|
|
3196
|
+
const label = validateLabel(data?.label);
|
|
3197
|
+
if (!label) return {
|
|
3198
|
+
success: false,
|
|
3199
|
+
error: "label required"
|
|
3200
|
+
};
|
|
3201
|
+
return withKeyedLock(`slot:${label}`, async () => {
|
|
3202
|
+
const { slot, scope } = await readSlot(kv, label);
|
|
3203
|
+
if (!slot) return {
|
|
3204
|
+
success: false,
|
|
3205
|
+
error: "slot not found"
|
|
3206
|
+
};
|
|
3207
|
+
if (slot.readOnly) return {
|
|
3208
|
+
success: false,
|
|
3209
|
+
error: "slot is read-only"
|
|
3210
|
+
};
|
|
3211
|
+
await kv.delete(scopeKv(scope), label);
|
|
3212
|
+
await recordAudit(kv, "slot_delete", "mem::slot-delete", [label], {
|
|
3213
|
+
scope,
|
|
3214
|
+
size: slot.content.length
|
|
3215
|
+
});
|
|
3216
|
+
return { success: true };
|
|
3217
|
+
});
|
|
3218
|
+
});
|
|
3219
|
+
sdk.registerFunction("mem::slot-reflect", async (data) => {
|
|
3220
|
+
if (!data?.sessionId || typeof data.sessionId !== "string") return {
|
|
3221
|
+
success: false,
|
|
3222
|
+
error: "sessionId required"
|
|
3223
|
+
};
|
|
3224
|
+
const max = typeof data.maxObservations === "number" && Number.isInteger(data.maxObservations) && data.maxObservations > 0 ? Math.min(200, data.maxObservations) : 50;
|
|
3225
|
+
const observations = await kv.list(KV.observations(data.sessionId));
|
|
3226
|
+
if (observations.length === 0) return {
|
|
3227
|
+
success: true,
|
|
3228
|
+
applied: 0,
|
|
3229
|
+
reason: "no observations for session"
|
|
3230
|
+
};
|
|
3231
|
+
const recent = observations.slice().sort((a, b) => (b.timestamp || "").localeCompare(a.timestamp || "")).slice(0, max);
|
|
3232
|
+
const pendingLines = [];
|
|
3233
|
+
const patternCounts = /* @__PURE__ */ new Map();
|
|
3234
|
+
const files = /* @__PURE__ */ new Set();
|
|
3235
|
+
for (const obs of recent) {
|
|
3236
|
+
const title = (obs.title || "").toLowerCase();
|
|
3237
|
+
if ((obs.narrative || "").toLowerCase().includes("todo") || title.includes("todo")) pendingLines.push(`- ${obs.title || obs.id}`);
|
|
3238
|
+
if (obs.type === "error") patternCounts.set("errors", (patternCounts.get("errors") ?? 0) + 1);
|
|
3239
|
+
if (obs.type === "command_run") patternCounts.set("commands", (patternCounts.get("commands") ?? 0) + 1);
|
|
3240
|
+
if (obs.files) for (const f of obs.files) files.add(f);
|
|
3241
|
+
}
|
|
3242
|
+
let applied = 0;
|
|
3243
|
+
if (pendingLines.length > 0) {
|
|
3244
|
+
if (await withKeyedLock(`slot:pending_items`, async () => {
|
|
3245
|
+
const { slot, scope } = await readSlot(kv, "pending_items");
|
|
3246
|
+
if (!slot) return false;
|
|
3247
|
+
const already = new Set(slot.content.split("\n"));
|
|
3248
|
+
const fresh = pendingLines.filter((line) => !already.has(line));
|
|
3249
|
+
if (fresh.length === 0) return false;
|
|
3250
|
+
const sep = slot.content && !slot.content.endsWith("\n") ? "\n" : "";
|
|
3251
|
+
const next = `${slot.content}${sep}${fresh.join("\n")}`;
|
|
3252
|
+
const truncated = next.length > slot.sizeLimit ? next.slice(next.length - slot.sizeLimit) : next;
|
|
3253
|
+
await kv.set(scopeKv(scope), "pending_items", {
|
|
3254
|
+
...slot,
|
|
3255
|
+
content: truncated,
|
|
3256
|
+
updatedAt: nowIso()
|
|
3257
|
+
});
|
|
3258
|
+
return true;
|
|
3259
|
+
})) applied++;
|
|
3260
|
+
}
|
|
3261
|
+
if (patternCounts.size > 0) {
|
|
3262
|
+
if (await withKeyedLock(`slot:session_patterns`, async () => {
|
|
3263
|
+
const { slot, scope } = await readSlot(kv, "session_patterns");
|
|
3264
|
+
if (!slot) return false;
|
|
3265
|
+
const summary = [`last reflection: ${nowIso()}`, ...Array.from(patternCounts.entries()).map(([kind, count]) => `- ${kind}: ${count} in last ${recent.length} observations`)].join("\n");
|
|
3266
|
+
const next = summary.length > slot.sizeLimit ? summary.slice(0, slot.sizeLimit) : summary;
|
|
3267
|
+
await kv.set(scopeKv(scope), "session_patterns", {
|
|
3268
|
+
...slot,
|
|
3269
|
+
content: next,
|
|
3270
|
+
updatedAt: nowIso()
|
|
3271
|
+
});
|
|
3272
|
+
return true;
|
|
3273
|
+
})) applied++;
|
|
3274
|
+
}
|
|
3275
|
+
if (files.size > 0) {
|
|
3276
|
+
if (await withKeyedLock(`slot:project_context`, async () => {
|
|
3277
|
+
const { slot, scope } = await readSlot(kv, "project_context");
|
|
3278
|
+
if (!slot) return false;
|
|
3279
|
+
const already = slot.content;
|
|
3280
|
+
const fresh = Array.from(files).filter((f) => !already.includes(f)).slice(0, 20);
|
|
3281
|
+
if (fresh.length === 0) return false;
|
|
3282
|
+
const header = already.length === 0 ? "Files touched in recent sessions:" : "";
|
|
3283
|
+
const nextRaw = `${already}${already && !already.endsWith("\n") ? "\n" : ""}${header ? header + "\n" : ""}${fresh.map((f) => `- ${f}`).join("\n")}`;
|
|
3284
|
+
const next = nextRaw.length > slot.sizeLimit ? nextRaw.slice(nextRaw.length - slot.sizeLimit) : nextRaw;
|
|
3285
|
+
await kv.set(scopeKv(scope), "project_context", {
|
|
3286
|
+
...slot,
|
|
3287
|
+
content: next,
|
|
3288
|
+
updatedAt: nowIso()
|
|
3289
|
+
});
|
|
3290
|
+
return true;
|
|
3291
|
+
})) applied++;
|
|
3292
|
+
}
|
|
3293
|
+
if (applied > 0) await recordAudit(kv, "slot_reflect", "mem::slot-reflect", [data.sessionId], {
|
|
3294
|
+
observationCount: recent.length,
|
|
3295
|
+
slotsUpdated: applied
|
|
3296
|
+
});
|
|
3297
|
+
return {
|
|
3298
|
+
success: true,
|
|
3299
|
+
applied,
|
|
3300
|
+
observationsReviewed: recent.length
|
|
3301
|
+
};
|
|
3302
|
+
});
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
//#endregion
|
|
3306
|
+
//#region src/functions/disk-size-manager.ts
|
|
3307
|
+
const DISK_SIZE_KEY = "system:currentDiskSize";
|
|
3308
|
+
function registerDiskSizeManager(sdk, kv) {
|
|
3309
|
+
sdk.registerFunction("mem::disk-size-delta", async (data) => {
|
|
3310
|
+
if (typeof data?.deltaBytes !== "number" || !isFinite(data.deltaBytes)) return {
|
|
3311
|
+
success: false,
|
|
3312
|
+
error: "deltaBytes must be a finite number"
|
|
3313
|
+
};
|
|
3314
|
+
return withKeyedLock(DISK_SIZE_KEY, async () => {
|
|
3315
|
+
let newTotal = (await kv.get(KV.state, DISK_SIZE_KEY) || 0) + data.deltaBytes;
|
|
3316
|
+
if (newTotal < 0) newTotal = 0;
|
|
3317
|
+
await kv.set(KV.state, DISK_SIZE_KEY, newTotal);
|
|
3318
|
+
if (data.deltaBytes > 0 && newTotal > getMaxBytes()) {
|
|
3319
|
+
sdk.triggerVoid("mem::image-quota-cleanup", {});
|
|
3320
|
+
logger.info("Disk quota exceeded, cleanup triggered", {
|
|
3321
|
+
currentBytes: newTotal,
|
|
3322
|
+
maxBytes: getMaxBytes()
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
return {
|
|
3326
|
+
success: true,
|
|
3327
|
+
currentTotal: newTotal
|
|
3328
|
+
};
|
|
3329
|
+
});
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
|
|
2311
3333
|
//#endregion
|
|
2312
3334
|
//#region src/prompts/compression.ts
|
|
2313
3335
|
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.
|
|
@@ -2356,6 +3378,17 @@ function truncate$1(s, max) {
|
|
|
2356
3378
|
return s.length > max ? s.slice(0, max) + "\n[...truncated]" : s;
|
|
2357
3379
|
}
|
|
2358
3380
|
|
|
3381
|
+
//#endregion
|
|
3382
|
+
//#region src/prompts/vision.ts
|
|
3383
|
+
const VISION_DESCRIPTION_PROMPT = `Describe what this image shows in the context of software development. Extract:
|
|
3384
|
+
- What type of image this is (screenshot, diagram, mockup, terminal output, error, etc.)
|
|
3385
|
+
- Key entities visible (files, components, UI elements, error messages)
|
|
3386
|
+
- Relationships or flow shown
|
|
3387
|
+
- Any decisions, errors, or state visible
|
|
3388
|
+
- Text content visible in the image
|
|
3389
|
+
|
|
3390
|
+
Be concise but preserve all technically relevant details. Output plain text, no XML.`;
|
|
3391
|
+
|
|
2359
3392
|
//#endregion
|
|
2360
3393
|
//#region src/prompts/xml.ts
|
|
2361
3394
|
const VALID_TAG = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
|
|
@@ -2589,6 +3622,7 @@ const VALID_TYPES$1 = new Set([
|
|
|
2589
3622
|
"subagent",
|
|
2590
3623
|
"notification",
|
|
2591
3624
|
"task",
|
|
3625
|
+
"image",
|
|
2592
3626
|
"other"
|
|
2593
3627
|
]);
|
|
2594
3628
|
function parseCompressionXml(xml) {
|
|
@@ -2609,11 +3643,32 @@ function parseCompressionXml(xml) {
|
|
|
2609
3643
|
function registerCompressFunction(sdk, kv, provider, metricsStore) {
|
|
2610
3644
|
sdk.registerFunction("mem::compress", async (data) => {
|
|
2611
3645
|
const startMs = Date.now();
|
|
3646
|
+
let imageDescription;
|
|
3647
|
+
const hasImage = data.raw.modality === "image" || data.raw.modality === "mixed";
|
|
3648
|
+
if (hasImage && data.raw.imageData && provider.describeImage) try {
|
|
3649
|
+
let base64Data = data.raw.imageData;
|
|
3650
|
+
let mimeType = "image/png";
|
|
3651
|
+
if (!data.raw.imageData.startsWith("/9j/") && !data.raw.imageData.startsWith("iVBOR")) {
|
|
3652
|
+
if (!isManagedImagePath(data.raw.imageData)) throw new Error(`Refusing to read image outside managed store: ${data.raw.imageData}`);
|
|
3653
|
+
base64Data = readFileSync(data.raw.imageData).toString("base64");
|
|
3654
|
+
if (data.raw.imageData.endsWith(".jpg") || data.raw.imageData.endsWith(".jpeg")) mimeType = "image/jpeg";
|
|
3655
|
+
else if (data.raw.imageData.endsWith(".webp")) mimeType = "image/webp";
|
|
3656
|
+
else if (data.raw.imageData.endsWith(".gif")) mimeType = "image/gif";
|
|
3657
|
+
}
|
|
3658
|
+
imageDescription = await provider.describeImage(base64Data, mimeType, VISION_DESCRIPTION_PROMPT);
|
|
3659
|
+
logger.info("Image described by vision model", { obsId: data.observationId });
|
|
3660
|
+
} catch (err) {
|
|
3661
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3662
|
+
logger.warn("Vision model call failed, falling back to text-only compression", {
|
|
3663
|
+
obsId: data.observationId,
|
|
3664
|
+
error: msg
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
2612
3667
|
const prompt = buildCompressionPrompt({
|
|
2613
3668
|
hookType: data.raw.hookType,
|
|
2614
3669
|
toolName: data.raw.toolName,
|
|
2615
3670
|
toolInput: data.raw.toolInput,
|
|
2616
|
-
toolOutput: data.raw.toolOutput,
|
|
3671
|
+
toolOutput: imageDescription ? `[Image Description]: ${imageDescription}\n\n${data.raw.toolOutput ?? ""}` : data.raw.toolOutput,
|
|
2617
3672
|
userPrompt: data.raw.userPrompt,
|
|
2618
3673
|
timestamp: data.raw.timestamp
|
|
2619
3674
|
});
|
|
@@ -2650,7 +3705,10 @@ function registerCompressFunction(sdk, kv, provider, metricsStore) {
|
|
|
2650
3705
|
sessionId: data.sessionId,
|
|
2651
3706
|
timestamp: data.raw.timestamp,
|
|
2652
3707
|
...parsed,
|
|
2653
|
-
confidence: qualityScore / 100
|
|
3708
|
+
confidence: qualityScore / 100,
|
|
3709
|
+
...hasImage ? { modality: data.raw.modality } : {},
|
|
3710
|
+
...imageDescription ? { imageDescription } : {},
|
|
3711
|
+
...data.raw.imageData ? { imageRef: data.raw.imageData } : {}
|
|
2654
3712
|
};
|
|
2655
3713
|
await kv.set(KV.observations(data.sessionId), data.observationId, compressed);
|
|
2656
3714
|
getSearchIndex().add(compressed);
|
|
@@ -2833,61 +3891,15 @@ Output EXACTLY this XML format with no additional text:
|
|
|
2833
3891
|
|
|
2834
3892
|
Rules:
|
|
2835
3893
|
- Focus on outcomes, not individual tool calls
|
|
2836
|
-
- Highlight decisions and their rationale
|
|
2837
|
-
- List all files that were created or modified
|
|
2838
|
-
- Concepts should be searchable terms for future context retrieval`;
|
|
2839
|
-
function buildSummaryPrompt(observations) {
|
|
2840
|
-
const lines = observations.map((obs, i) => {
|
|
2841
|
-
const facts = obs.facts.map((f) => ` - ${f}`).join("\n");
|
|
2842
|
-
return `[${i + 1}] ${obs.type}: ${obs.title}\n${obs.narrative}\nFacts:\n${facts}\nFiles: ${obs.files.join(", ")}`;
|
|
2843
|
-
});
|
|
2844
|
-
return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
|
|
2845
|
-
}
|
|
2846
|
-
|
|
2847
|
-
//#endregion
|
|
2848
|
-
//#region src/functions/audit.ts
|
|
2849
|
-
async function recordAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
|
|
2850
|
-
const entry = {
|
|
2851
|
-
id: generateId("aud"),
|
|
2852
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2853
|
-
operation,
|
|
2854
|
-
userId,
|
|
2855
|
-
functionId,
|
|
2856
|
-
targetIds,
|
|
2857
|
-
details,
|
|
2858
|
-
qualityScore
|
|
2859
|
-
};
|
|
2860
|
-
await kv.set(KV.audit, entry.id, entry);
|
|
2861
|
-
return entry;
|
|
2862
|
-
}
|
|
2863
|
-
async function safeAudit(kv, operation, functionId, targetIds, details = {}, qualityScore, userId) {
|
|
2864
|
-
try {
|
|
2865
|
-
await recordAudit(kv, operation, functionId, targetIds, details, qualityScore, userId);
|
|
2866
|
-
} catch (err) {
|
|
2867
|
-
try {
|
|
2868
|
-
logger.warn("audit write failed", {
|
|
2869
|
-
functionId,
|
|
2870
|
-
operation,
|
|
2871
|
-
targetIds,
|
|
2872
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2873
|
-
});
|
|
2874
|
-
} catch {}
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
async function queryAudit(kv, filter) {
|
|
2878
|
-
let entries = [...await kv.list(KV.audit)].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
2879
|
-
if (filter?.operation) entries = entries.filter((e) => e.operation === filter.operation);
|
|
2880
|
-
if (filter?.dateFrom) {
|
|
2881
|
-
const from = new Date(filter.dateFrom).getTime();
|
|
2882
|
-
if (Number.isNaN(from)) throw new Error(`Invalid dateFrom: ${filter.dateFrom}`);
|
|
2883
|
-
entries = entries.filter((e) => new Date(e.timestamp).getTime() >= from);
|
|
2884
|
-
}
|
|
2885
|
-
if (filter?.dateTo) {
|
|
2886
|
-
const to = new Date(filter.dateTo).getTime();
|
|
2887
|
-
if (Number.isNaN(to)) throw new Error(`Invalid dateTo: ${filter.dateTo}`);
|
|
2888
|
-
entries = entries.filter((e) => new Date(e.timestamp).getTime() <= to);
|
|
2889
|
-
}
|
|
2890
|
-
return entries.slice(0, filter?.limit || 100);
|
|
3894
|
+
- Highlight decisions and their rationale
|
|
3895
|
+
- List all files that were created or modified
|
|
3896
|
+
- Concepts should be searchable terms for future context retrieval`;
|
|
3897
|
+
function buildSummaryPrompt(observations) {
|
|
3898
|
+
const lines = observations.map((obs, i) => {
|
|
3899
|
+
const facts = obs.facts.map((f) => ` - ${f}`).join("\n");
|
|
3900
|
+
return `[${i + 1}] ${obs.type}: ${obs.title}\n${obs.narrative}\nFacts:\n${facts}\nFiles: ${obs.files.join(", ")}`;
|
|
3901
|
+
});
|
|
3902
|
+
return `Session observations (${observations.length} total):\n\n${lines.join("\n\n---\n\n")}`;
|
|
2891
3903
|
}
|
|
2892
3904
|
|
|
2893
3905
|
//#endregion
|
|
@@ -2931,9 +3943,33 @@ function registerSummarizeFunction(sdk, kv, provider, metricsStore) {
|
|
|
2931
3943
|
error: "no_observations"
|
|
2932
3944
|
};
|
|
2933
3945
|
}
|
|
3946
|
+
if (provider.name === "noop") {
|
|
3947
|
+
logger.info("Summarize skipped — no LLM provider configured", { sessionId });
|
|
3948
|
+
return {
|
|
3949
|
+
success: false,
|
|
3950
|
+
error: "no_provider",
|
|
3951
|
+
reason: "No LLM provider key set; Summarize is a no-op. Set ANTHROPIC_API_KEY (or GEMINI/OPENROUTER/MINIMAX) in ~/.agentmemory/.env to enable."
|
|
3952
|
+
};
|
|
3953
|
+
}
|
|
2934
3954
|
try {
|
|
2935
3955
|
const prompt = buildSummaryPrompt(compressed);
|
|
2936
|
-
const
|
|
3956
|
+
const response = await provider.summarize(SUMMARY_SYSTEM, prompt);
|
|
3957
|
+
if (!response || !response.trim()) {
|
|
3958
|
+
const latencyMs = Date.now() - startMs;
|
|
3959
|
+
if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
|
|
3960
|
+
logger.warn("Empty provider response on summarize", {
|
|
3961
|
+
sessionId,
|
|
3962
|
+
provider: provider.name,
|
|
3963
|
+
promptBytes: prompt.length,
|
|
3964
|
+
systemBytes: SUMMARY_SYSTEM.length,
|
|
3965
|
+
observationCount: compressed.length
|
|
3966
|
+
});
|
|
3967
|
+
return {
|
|
3968
|
+
success: false,
|
|
3969
|
+
error: "empty_provider_response"
|
|
3970
|
+
};
|
|
3971
|
+
}
|
|
3972
|
+
const summary = parseSummaryXml(response, sessionId, session.project, compressed.length);
|
|
2937
3973
|
if (!summary) {
|
|
2938
3974
|
const latencyMs = Date.now() - startMs;
|
|
2939
3975
|
if (metricsStore) await metricsStore.record("mem::summarize", latencyMs, false);
|
|
@@ -3520,14 +4556,20 @@ function registerRememberFunction(sdk, kv) {
|
|
|
3520
4556
|
const deletedMemoryIds = [];
|
|
3521
4557
|
const deletedObservationIds = [];
|
|
3522
4558
|
let deletedSession = false;
|
|
4559
|
+
const { decrementImageRef } = await import("./image-refs-Dq5wcV-a.mjs");
|
|
3523
4560
|
if (data.memoryId) {
|
|
4561
|
+
const mem = await kv.get(KV.memories, data.memoryId);
|
|
3524
4562
|
await kv.delete(KV.memories, data.memoryId);
|
|
4563
|
+
if (mem?.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
3525
4564
|
await deleteAccessLog(kv, data.memoryId);
|
|
3526
4565
|
deletedMemoryIds.push(data.memoryId);
|
|
3527
4566
|
deleted++;
|
|
3528
4567
|
}
|
|
3529
4568
|
if (data.sessionId && data.observationIds && data.observationIds.length > 0) for (const obsId of data.observationIds) {
|
|
4569
|
+
const obs = await kv.get(KV.observations(data.sessionId), obsId);
|
|
3530
4570
|
await kv.delete(KV.observations(data.sessionId), obsId);
|
|
4571
|
+
if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
4572
|
+
if (obs?.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
3531
4573
|
deletedObservationIds.push(obsId);
|
|
3532
4574
|
deleted++;
|
|
3533
4575
|
}
|
|
@@ -3535,6 +4577,8 @@ function registerRememberFunction(sdk, kv) {
|
|
|
3535
4577
|
const observations = await kv.list(KV.observations(data.sessionId));
|
|
3536
4578
|
for (const obs of observations) {
|
|
3537
4579
|
await kv.delete(KV.observations(data.sessionId), obs.id);
|
|
4580
|
+
if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
4581
|
+
if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
3538
4582
|
deletedObservationIds.push(obs.id);
|
|
3539
4583
|
deleted++;
|
|
3540
4584
|
}
|
|
@@ -3571,6 +4615,7 @@ const DEFAULTS$1 = {
|
|
|
3571
4615
|
function registerEvictFunction(sdk, kv) {
|
|
3572
4616
|
sdk.registerFunction("mem::evict", async (data) => {
|
|
3573
4617
|
const dryRun = data?.dryRun ?? false;
|
|
4618
|
+
const { decrementImageRef } = await import("./image-refs-Dq5wcV-a.mjs");
|
|
3574
4619
|
const configOverride = await kv.get(KV.config, "eviction").catch(() => null);
|
|
3575
4620
|
const cfg = {
|
|
3576
4621
|
...DEFAULTS$1,
|
|
@@ -3629,6 +4674,8 @@ function registerEvictFunction(sdk, kv) {
|
|
|
3629
4674
|
});
|
|
3630
4675
|
continue;
|
|
3631
4676
|
}
|
|
4677
|
+
if (o.imageData) await decrementImageRef(kv, sdk, o.imageData);
|
|
4678
|
+
if (o.imageRef && o.imageRef !== o.imageData) await decrementImageRef(kv, sdk, o.imageRef);
|
|
3632
4679
|
await recordAudit(kv, "delete", "mem::evict", [o.id], {
|
|
3633
4680
|
resource: "observation",
|
|
3634
4681
|
reason: "low_importance_old_observation",
|
|
@@ -3658,6 +4705,8 @@ function registerEvictFunction(sdk, kv) {
|
|
|
3658
4705
|
});
|
|
3659
4706
|
continue;
|
|
3660
4707
|
}
|
|
4708
|
+
if (o.imageData) await decrementImageRef(kv, sdk, o.imageData);
|
|
4709
|
+
if (o.imageRef && o.imageRef !== o.imageData) await decrementImageRef(kv, sdk, o.imageRef);
|
|
3661
4710
|
await recordAudit(kv, "delete", "mem::evict", [o.id], {
|
|
3662
4711
|
resource: "observation",
|
|
3663
4712
|
reason: "project_observation_cap",
|
|
@@ -3687,6 +4736,7 @@ function registerEvictFunction(sdk, kv) {
|
|
|
3687
4736
|
});
|
|
3688
4737
|
continue;
|
|
3689
4738
|
}
|
|
4739
|
+
if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
3690
4740
|
await recordAudit(kv, "delete", "mem::evict", [mem.id], {
|
|
3691
4741
|
resource: "memory",
|
|
3692
4742
|
reason: "expired_memory",
|
|
@@ -3710,6 +4760,7 @@ function registerEvictFunction(sdk, kv) {
|
|
|
3710
4760
|
});
|
|
3711
4761
|
continue;
|
|
3712
4762
|
}
|
|
4763
|
+
if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
3713
4764
|
await recordAudit(kv, "delete", "mem::evict", [mem.id], {
|
|
3714
4765
|
resource: "memory",
|
|
3715
4766
|
reason: "old_non_latest_memory",
|
|
@@ -4167,6 +5218,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
4167
5218
|
sdk.registerFunction("mem::auto-forget", async (data) => {
|
|
4168
5219
|
const dryRun = data?.dryRun ?? false;
|
|
4169
5220
|
const now = Date.now();
|
|
5221
|
+
const { decrementImageRef } = await import("./image-refs-Dq5wcV-a.mjs");
|
|
4170
5222
|
const result = {
|
|
4171
5223
|
ttlExpired: [],
|
|
4172
5224
|
contradictions: [],
|
|
@@ -4180,6 +5232,7 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
4180
5232
|
result.ttlExpired.push(mem.id);
|
|
4181
5233
|
deletedIds.add(mem.id);
|
|
4182
5234
|
if (!dryRun) {
|
|
5235
|
+
if (mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
4183
5236
|
await kv.delete(KV.memories, mem.id);
|
|
4184
5237
|
await recordAudit(kv, "delete", "mem::auto-forget", [mem.id], {
|
|
4185
5238
|
resource: "memory",
|
|
@@ -4248,13 +5301,23 @@ function registerAutoForgetFunction(sdk, kv) {
|
|
|
4248
5301
|
if (now - new Date(obs.timestamp).getTime() > 180 * MS_PER_DAY && (obs.importance ?? 5) <= 2) {
|
|
4249
5302
|
result.lowValueObs.push(obs.id);
|
|
4250
5303
|
if (!dryRun) {
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
}
|
|
5304
|
+
let deletedOk = false;
|
|
5305
|
+
try {
|
|
5306
|
+
await kv.delete(KV.observations(sessions[i].id), obs.id);
|
|
5307
|
+
deletedOk = true;
|
|
5308
|
+
} catch {
|
|
5309
|
+
deletedOk = false;
|
|
5310
|
+
}
|
|
5311
|
+
if (deletedOk) {
|
|
5312
|
+
if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
|
|
5313
|
+
if (obs.imageRef && obs.imageRef !== obs.imageData) await decrementImageRef(kv, sdk, obs.imageRef);
|
|
5314
|
+
await recordAudit(kv, "delete", "mem::auto-forget", [obs.id], {
|
|
5315
|
+
resource: "observation",
|
|
5316
|
+
reason: "auto-forget low-value observation",
|
|
5317
|
+
sessionId: sessions[i].id,
|
|
5318
|
+
timestamp: obs.timestamp
|
|
5319
|
+
});
|
|
5320
|
+
}
|
|
4258
5321
|
}
|
|
4259
5322
|
}
|
|
4260
5323
|
}
|
|
@@ -4385,7 +5448,8 @@ function registerExportImportFunction(sdk, kv) {
|
|
|
4385
5448
|
"0.8.12",
|
|
4386
5449
|
"0.8.13",
|
|
4387
5450
|
"0.9.0",
|
|
4388
|
-
"0.9.1"
|
|
5451
|
+
"0.9.1",
|
|
5452
|
+
"0.9.2"
|
|
4389
5453
|
]).has(importData.version)) return {
|
|
4390
5454
|
success: false,
|
|
4391
5455
|
error: `Unsupported export version: ${importData.version}`
|
|
@@ -10742,6 +11806,7 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
10742
11806
|
const threshold = typeof data?.threshold === "number" && Number.isFinite(data.threshold) ? data.threshold : DEFAULT_DECAY.tierThresholds.cold;
|
|
10743
11807
|
const maxEvictRaw = typeof data?.maxEvict === "number" && Number.isInteger(data.maxEvict) ? data.maxEvict : 50;
|
|
10744
11808
|
const maxEvict = Math.min(1e3, Math.max(0, maxEvictRaw));
|
|
11809
|
+
const { decrementImageRef } = await import("./image-refs-Dq5wcV-a.mjs");
|
|
10745
11810
|
const candidates = (await kv.list(KV.retentionScores)).filter((s) => s.score < threshold).sort((a, b) => a.score - b.score).slice(0, maxEvict);
|
|
10746
11811
|
if (data?.dryRun) return {
|
|
10747
11812
|
success: true,
|
|
@@ -10773,6 +11838,8 @@ function registerRetentionFunctions(sdk, kv) {
|
|
|
10773
11838
|
resolvedSource = "semantic";
|
|
10774
11839
|
}
|
|
10775
11840
|
if (!scope || !resolvedSource) continue;
|
|
11841
|
+
const mem = await kv.get(scope, candidate.memoryId);
|
|
11842
|
+
if (mem && mem.imageRef) await decrementImageRef(kv, sdk, mem.imageRef);
|
|
10776
11843
|
await kv.delete(scope, candidate.memoryId);
|
|
10777
11844
|
await kv.delete(KV.retentionScores, candidate.memoryId);
|
|
10778
11845
|
await deleteAccessLog(kv, candidate.memoryId);
|
|
@@ -11210,6 +12277,92 @@ function rawFromCompressed(obs) {
|
|
|
11210
12277
|
}
|
|
11211
12278
|
};
|
|
11212
12279
|
}
|
|
12280
|
+
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];
|
|
12281
|
+
async function deriveCrystalAndLessons(kv, sessionId, project, rawObs, compressed, firstPrompt) {
|
|
12282
|
+
if (rawObs.length === 0) return;
|
|
12283
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
12284
|
+
const files = /* @__PURE__ */ new Set();
|
|
12285
|
+
const tools = /* @__PURE__ */ new Set();
|
|
12286
|
+
for (const c of compressed) {
|
|
12287
|
+
for (const f of c.files || []) files.add(f);
|
|
12288
|
+
if (c.type && c.type !== "conversation" && c.title) tools.add(c.title);
|
|
12289
|
+
}
|
|
12290
|
+
const assistantTexts = [];
|
|
12291
|
+
const userPrompts = [];
|
|
12292
|
+
for (const r of rawObs) {
|
|
12293
|
+
if (typeof r.assistantResponse === "string" && r.assistantResponse.trim()) assistantTexts.push(r.assistantResponse);
|
|
12294
|
+
if (typeof r.userPrompt === "string" && r.userPrompt.trim()) userPrompts.push(r.userPrompt);
|
|
12295
|
+
}
|
|
12296
|
+
const lessonMatches = /* @__PURE__ */ new Map();
|
|
12297
|
+
for (const text of assistantTexts.concat(userPrompts).slice(0, 200)) for (const pat of LESSON_PATTERNS) {
|
|
12298
|
+
pat.lastIndex = 0;
|
|
12299
|
+
let m;
|
|
12300
|
+
while ((m = pat.exec(text)) !== null && lessonMatches.size < 40) {
|
|
12301
|
+
const snippet = m[0].replace(/\s+/g, " ").trim();
|
|
12302
|
+
if (snippet.length >= 20 && snippet.length <= 220) {
|
|
12303
|
+
const key = snippet.toLowerCase();
|
|
12304
|
+
if (!lessonMatches.has(key)) lessonMatches.set(key, snippet);
|
|
12305
|
+
}
|
|
12306
|
+
}
|
|
12307
|
+
}
|
|
12308
|
+
const lessonEntries = Array.from(lessonMatches.values()).slice(0, 20);
|
|
12309
|
+
const lessonIds = [];
|
|
12310
|
+
for (const content of lessonEntries) {
|
|
12311
|
+
const lessonId = fingerprintId("lesson", content.trim().toLowerCase());
|
|
12312
|
+
try {
|
|
12313
|
+
const existing = await kv.get(KV.lessons, lessonId);
|
|
12314
|
+
if (existing) {
|
|
12315
|
+
const existingSources = existing.sourceIds || [];
|
|
12316
|
+
const mergedSources = existingSources.includes(sessionId) ? existingSources : [...existingSources, sessionId];
|
|
12317
|
+
const existingTags = existing.tags || [];
|
|
12318
|
+
const mergedTags = existingTags.includes("auto-import") ? existingTags : [...existingTags, "auto-import"];
|
|
12319
|
+
const merged = {
|
|
12320
|
+
...existing,
|
|
12321
|
+
sourceIds: mergedSources,
|
|
12322
|
+
tags: mergedTags,
|
|
12323
|
+
reinforcements: (existing.reinforcements || 0) + 1,
|
|
12324
|
+
updatedAt: createdAt,
|
|
12325
|
+
lastReinforcedAt: createdAt
|
|
12326
|
+
};
|
|
12327
|
+
await kv.set(KV.lessons, lessonId, merged);
|
|
12328
|
+
} else {
|
|
12329
|
+
const lesson = {
|
|
12330
|
+
id: lessonId,
|
|
12331
|
+
content,
|
|
12332
|
+
context: firstPrompt || project,
|
|
12333
|
+
confidence: .4,
|
|
12334
|
+
reinforcements: 0,
|
|
12335
|
+
source: "consolidation",
|
|
12336
|
+
sourceIds: [sessionId],
|
|
12337
|
+
project,
|
|
12338
|
+
tags: ["auto-import"],
|
|
12339
|
+
createdAt,
|
|
12340
|
+
updatedAt: createdAt,
|
|
12341
|
+
decayRate: .05
|
|
12342
|
+
};
|
|
12343
|
+
await kv.set(KV.lessons, lessonId, lesson);
|
|
12344
|
+
}
|
|
12345
|
+
lessonIds.push(lessonId);
|
|
12346
|
+
} catch {}
|
|
12347
|
+
}
|
|
12348
|
+
const crystalId = fingerprintId("crystal", sessionId);
|
|
12349
|
+
const narrativePreview = firstPrompt ? firstPrompt.slice(0, 300) : compressed.slice(0, 5).map((c) => c.narrative || c.title).filter(Boolean).join(" · ").slice(0, 300);
|
|
12350
|
+
try {
|
|
12351
|
+
const existingCrystal = await kv.get(KV.crystals, crystalId);
|
|
12352
|
+
const crystal = {
|
|
12353
|
+
id: crystalId,
|
|
12354
|
+
narrative: narrativePreview || `Session ${sessionId.slice(0, 12)} (${rawObs.length} observations)`,
|
|
12355
|
+
keyOutcomes: Array.from(tools).slice(0, 8),
|
|
12356
|
+
filesAffected: Array.from(files).slice(0, 20),
|
|
12357
|
+
lessons: lessonIds,
|
|
12358
|
+
sourceActionIds: existingCrystal?.sourceActionIds ?? [],
|
|
12359
|
+
sessionId,
|
|
12360
|
+
project,
|
|
12361
|
+
createdAt: existingCrystal?.createdAt ?? createdAt
|
|
12362
|
+
};
|
|
12363
|
+
await kv.set(KV.crystals, crystalId, crystal);
|
|
12364
|
+
} catch {}
|
|
12365
|
+
}
|
|
11213
12366
|
function isRawShape(o) {
|
|
11214
12367
|
if (!o || typeof o !== "object") return false;
|
|
11215
12368
|
return typeof o.hookType === "string";
|
|
@@ -11320,6 +12473,8 @@ function registerReplayFunctions(sdk, kv) {
|
|
|
11320
12473
|
}
|
|
11321
12474
|
const parsed = parseJsonlText(text, generateId("sess"));
|
|
11322
12475
|
if (parsed.observations.length === 0) continue;
|
|
12476
|
+
const firstPromptObs = parsed.observations.find((o) => typeof o.userPrompt === "string" && o.userPrompt.trim().length > 0);
|
|
12477
|
+
const firstPrompt = firstPromptObs?.userPrompt ? firstPromptObs.userPrompt.replace(/\s+/g, " ").trim().slice(0, 200) : void 0;
|
|
11323
12478
|
const existing = await kv.get(KV.sessions, parsed.sessionId);
|
|
11324
12479
|
if (existing) {
|
|
11325
12480
|
existing.observationCount = (existing.observationCount || 0) + parsed.observations.length;
|
|
@@ -11327,6 +12482,7 @@ function registerReplayFunctions(sdk, kv) {
|
|
|
11327
12482
|
if (existing.status === "active") existing.status = "completed";
|
|
11328
12483
|
const existingTags = existing.tags || [];
|
|
11329
12484
|
if (!existingTags.includes("jsonl-import")) existing.tags = [...existingTags, "jsonl-import"];
|
|
12485
|
+
if (!existing.firstPrompt && firstPrompt) existing.firstPrompt = firstPrompt;
|
|
11330
12486
|
await kv.set(KV.sessions, existing.id, existing);
|
|
11331
12487
|
} else {
|
|
11332
12488
|
const session = {
|
|
@@ -11337,13 +12493,22 @@ function registerReplayFunctions(sdk, kv) {
|
|
|
11337
12493
|
endedAt: parsed.endedAt,
|
|
11338
12494
|
status: "completed",
|
|
11339
12495
|
observationCount: parsed.observations.length,
|
|
11340
|
-
tags: ["jsonl-import"]
|
|
12496
|
+
tags: ["jsonl-import"],
|
|
12497
|
+
firstPrompt
|
|
11341
12498
|
};
|
|
11342
12499
|
await kv.set(KV.sessions, session.id, session);
|
|
11343
12500
|
}
|
|
11344
|
-
|
|
12501
|
+
const searchIndex = getSearchIndex();
|
|
12502
|
+
const compressed = [];
|
|
12503
|
+
await Promise.all(parsed.observations.map(async (obs) => {
|
|
12504
|
+
const synthetic = buildSyntheticCompression(obs);
|
|
12505
|
+
compressed.push(synthetic);
|
|
12506
|
+
await kv.set(KV.observations(parsed.sessionId), obs.id, synthetic);
|
|
12507
|
+
searchIndex.add(synthetic);
|
|
12508
|
+
}));
|
|
11345
12509
|
observationCount += parsed.observations.length;
|
|
11346
12510
|
sessionIds.push(parsed.sessionId);
|
|
12511
|
+
await deriveCrystalAndLessons(kv, parsed.sessionId, parsed.project, parsed.observations, compressed, firstPrompt);
|
|
11347
12512
|
}
|
|
11348
12513
|
await safeAudit(kv, "import", "mem::replay::import-jsonl", sessionIds, {
|
|
11349
12514
|
source: "jsonl",
|
|
@@ -11377,6 +12542,7 @@ function evaluateHealth(snapshot, config = {}) {
|
|
|
11377
12542
|
...config
|
|
11378
12543
|
};
|
|
11379
12544
|
const alerts = [];
|
|
12545
|
+
const notes = [];
|
|
11380
12546
|
let critical = false;
|
|
11381
12547
|
let degraded = false;
|
|
11382
12548
|
if (snapshot.connectionState === "disconnected" || snapshot.connectionState === "failed") {
|
|
@@ -11410,10 +12576,11 @@ function evaluateHealth(snapshot, config = {}) {
|
|
|
11410
12576
|
} else if (memPercent > cfg.memoryWarnPercent && rssAboveFloor) {
|
|
11411
12577
|
alerts.push(`memory_warn_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
11412
12578
|
degraded = true;
|
|
11413
|
-
} else if (memPercent > cfg.memoryWarnPercent)
|
|
12579
|
+
} else if (memPercent > cfg.memoryWarnPercent) notes.push(`memory_heap_tight_${Math.round(memPercent)}%_rss${memMb}mb`);
|
|
11414
12580
|
return {
|
|
11415
12581
|
status: critical ? "critical" : degraded ? "degraded" : "healthy",
|
|
11416
|
-
alerts
|
|
12582
|
+
alerts,
|
|
12583
|
+
notes
|
|
11417
12584
|
};
|
|
11418
12585
|
}
|
|
11419
12586
|
|
|
@@ -11490,6 +12657,7 @@ function registerHealthMonitor(sdk, kv) {
|
|
|
11490
12657
|
const evaluated = evaluateHealth(snapshot);
|
|
11491
12658
|
snapshot.status = evaluated.status;
|
|
11492
12659
|
snapshot.alerts = evaluated.alerts;
|
|
12660
|
+
snapshot.notes = evaluated.notes;
|
|
11493
12661
|
await kv.set(KV.health, "latest", snapshot).catch(() => {});
|
|
11494
12662
|
return snapshot;
|
|
11495
12663
|
}
|
|
@@ -11833,9 +13001,14 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
11833
13001
|
sdk.registerFunction("api::replay::sessions", async (req) => {
|
|
11834
13002
|
const authErr = checkAuth(req, secret);
|
|
11835
13003
|
if (authErr) return authErr;
|
|
13004
|
+
const sessions = await kv.list(KV.sessions);
|
|
13005
|
+
sessions.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
|
|
11836
13006
|
return {
|
|
11837
13007
|
status_code: 200,
|
|
11838
|
-
body:
|
|
13008
|
+
body: {
|
|
13009
|
+
success: true,
|
|
13010
|
+
sessions
|
|
13011
|
+
}
|
|
11839
13012
|
};
|
|
11840
13013
|
});
|
|
11841
13014
|
sdk.registerTrigger({
|
|
@@ -12630,13 +13803,16 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
12630
13803
|
const parsedLimit = parseOptionalInt(req.query_params?.["limit"]);
|
|
12631
13804
|
return {
|
|
12632
13805
|
status_code: 200,
|
|
12633
|
-
body:
|
|
12634
|
-
|
|
12635
|
-
|
|
12636
|
-
|
|
12637
|
-
|
|
12638
|
-
|
|
12639
|
-
|
|
13806
|
+
body: {
|
|
13807
|
+
entries: await sdk.trigger({
|
|
13808
|
+
function_id: "mem::audit-query",
|
|
13809
|
+
payload: {
|
|
13810
|
+
operation: req.query_params?.["operation"],
|
|
13811
|
+
limit: parsedLimit ?? 50
|
|
13812
|
+
}
|
|
13813
|
+
}),
|
|
13814
|
+
success: true
|
|
13815
|
+
}
|
|
12640
13816
|
};
|
|
12641
13817
|
});
|
|
12642
13818
|
sdk.registerTrigger({
|
|
@@ -12683,157 +13859,489 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
12683
13859
|
});
|
|
12684
13860
|
sdk.registerTrigger({
|
|
12685
13861
|
type: "http",
|
|
12686
|
-
function_id: "api::governance-bulk",
|
|
13862
|
+
function_id: "api::governance-bulk",
|
|
13863
|
+
config: {
|
|
13864
|
+
api_path: "/agentmemory/governance/bulk-delete",
|
|
13865
|
+
http_method: "POST"
|
|
13866
|
+
}
|
|
13867
|
+
});
|
|
13868
|
+
sdk.registerFunction("api::snapshots", async (req) => {
|
|
13869
|
+
const authErr = checkAuth(req, secret);
|
|
13870
|
+
if (authErr) return authErr;
|
|
13871
|
+
try {
|
|
13872
|
+
return {
|
|
13873
|
+
status_code: 200,
|
|
13874
|
+
body: await sdk.trigger({
|
|
13875
|
+
function_id: "mem::snapshot-list",
|
|
13876
|
+
payload: {}
|
|
13877
|
+
})
|
|
13878
|
+
};
|
|
13879
|
+
} catch {
|
|
13880
|
+
return {
|
|
13881
|
+
status_code: 404,
|
|
13882
|
+
body: { error: "Snapshots not enabled" }
|
|
13883
|
+
};
|
|
13884
|
+
}
|
|
13885
|
+
});
|
|
13886
|
+
sdk.registerTrigger({
|
|
13887
|
+
type: "http",
|
|
13888
|
+
function_id: "api::snapshots",
|
|
13889
|
+
config: {
|
|
13890
|
+
api_path: "/agentmemory/snapshots",
|
|
13891
|
+
http_method: "GET"
|
|
13892
|
+
}
|
|
13893
|
+
});
|
|
13894
|
+
sdk.registerFunction("api::snapshot-create", async (req) => {
|
|
13895
|
+
const authErr = checkAuth(req, secret);
|
|
13896
|
+
if (authErr) return authErr;
|
|
13897
|
+
try {
|
|
13898
|
+
return {
|
|
13899
|
+
status_code: 201,
|
|
13900
|
+
body: await sdk.trigger({
|
|
13901
|
+
function_id: "mem::snapshot-create",
|
|
13902
|
+
payload: req.body || {}
|
|
13903
|
+
})
|
|
13904
|
+
};
|
|
13905
|
+
} catch {
|
|
13906
|
+
return {
|
|
13907
|
+
status_code: 404,
|
|
13908
|
+
body: { error: "Snapshots not enabled" }
|
|
13909
|
+
};
|
|
13910
|
+
}
|
|
13911
|
+
});
|
|
13912
|
+
sdk.registerTrigger({
|
|
13913
|
+
type: "http",
|
|
13914
|
+
function_id: "api::snapshot-create",
|
|
13915
|
+
config: {
|
|
13916
|
+
api_path: "/agentmemory/snapshot/create",
|
|
13917
|
+
http_method: "POST"
|
|
13918
|
+
}
|
|
13919
|
+
});
|
|
13920
|
+
sdk.registerFunction("api::snapshot-restore", async (req) => {
|
|
13921
|
+
const authErr = checkAuth(req, secret);
|
|
13922
|
+
if (authErr) return authErr;
|
|
13923
|
+
if (!req.body?.commitHash) return {
|
|
13924
|
+
status_code: 400,
|
|
13925
|
+
body: { error: "commitHash is required" }
|
|
13926
|
+
};
|
|
13927
|
+
try {
|
|
13928
|
+
return {
|
|
13929
|
+
status_code: 200,
|
|
13930
|
+
body: await sdk.trigger({
|
|
13931
|
+
function_id: "mem::snapshot-restore",
|
|
13932
|
+
payload: req.body
|
|
13933
|
+
})
|
|
13934
|
+
};
|
|
13935
|
+
} catch {
|
|
13936
|
+
return {
|
|
13937
|
+
status_code: 404,
|
|
13938
|
+
body: { error: "Snapshots not enabled" }
|
|
13939
|
+
};
|
|
13940
|
+
}
|
|
13941
|
+
});
|
|
13942
|
+
sdk.registerTrigger({
|
|
13943
|
+
type: "http",
|
|
13944
|
+
function_id: "api::snapshot-restore",
|
|
13945
|
+
config: {
|
|
13946
|
+
api_path: "/agentmemory/snapshot/restore",
|
|
13947
|
+
http_method: "POST"
|
|
13948
|
+
}
|
|
13949
|
+
});
|
|
13950
|
+
sdk.registerFunction("api::memories", async (req) => {
|
|
13951
|
+
const authErr = checkAuth(req, secret);
|
|
13952
|
+
if (authErr) return authErr;
|
|
13953
|
+
const memories = await kv.list(KV.memories);
|
|
13954
|
+
return {
|
|
13955
|
+
status_code: 200,
|
|
13956
|
+
body: { memories: req.query_params?.["latest"] === "true" ? memories.filter((m) => m.isLatest) : memories }
|
|
13957
|
+
};
|
|
13958
|
+
});
|
|
13959
|
+
sdk.registerTrigger({
|
|
13960
|
+
type: "http",
|
|
13961
|
+
function_id: "api::memories",
|
|
13962
|
+
config: {
|
|
13963
|
+
api_path: "/agentmemory/memories",
|
|
13964
|
+
http_method: "GET"
|
|
13965
|
+
}
|
|
13966
|
+
});
|
|
13967
|
+
sdk.registerFunction("api::semantic-list", async (req) => {
|
|
13968
|
+
const authErr = checkAuth(req, secret);
|
|
13969
|
+
if (authErr) return authErr;
|
|
13970
|
+
return {
|
|
13971
|
+
status_code: 200,
|
|
13972
|
+
body: { semantic: await kv.list(KV.semantic) }
|
|
13973
|
+
};
|
|
13974
|
+
});
|
|
13975
|
+
sdk.registerTrigger({
|
|
13976
|
+
type: "http",
|
|
13977
|
+
function_id: "api::semantic-list",
|
|
13978
|
+
config: {
|
|
13979
|
+
api_path: "/agentmemory/semantic",
|
|
13980
|
+
http_method: "GET"
|
|
13981
|
+
}
|
|
13982
|
+
});
|
|
13983
|
+
sdk.registerFunction("api::procedural-list", async (req) => {
|
|
13984
|
+
const authErr = checkAuth(req, secret);
|
|
13985
|
+
if (authErr) return authErr;
|
|
13986
|
+
return {
|
|
13987
|
+
status_code: 200,
|
|
13988
|
+
body: { procedural: await kv.list(KV.procedural) }
|
|
13989
|
+
};
|
|
13990
|
+
});
|
|
13991
|
+
sdk.registerTrigger({
|
|
13992
|
+
type: "http",
|
|
13993
|
+
function_id: "api::procedural-list",
|
|
13994
|
+
config: {
|
|
13995
|
+
api_path: "/agentmemory/procedural",
|
|
13996
|
+
http_method: "GET"
|
|
13997
|
+
}
|
|
13998
|
+
});
|
|
13999
|
+
sdk.registerFunction("api::relations-list", async (req) => {
|
|
14000
|
+
const authErr = checkAuth(req, secret);
|
|
14001
|
+
if (authErr) return authErr;
|
|
14002
|
+
return {
|
|
14003
|
+
status_code: 200,
|
|
14004
|
+
body: { relations: await kv.list(KV.relations) }
|
|
14005
|
+
};
|
|
14006
|
+
});
|
|
14007
|
+
sdk.registerTrigger({
|
|
14008
|
+
type: "http",
|
|
14009
|
+
function_id: "api::relations-list",
|
|
14010
|
+
config: {
|
|
14011
|
+
api_path: "/agentmemory/relations",
|
|
14012
|
+
http_method: "GET"
|
|
14013
|
+
}
|
|
14014
|
+
});
|
|
14015
|
+
sdk.registerFunction("api::vision-search", async (req) => {
|
|
14016
|
+
const authErr = checkAuth(req, secret);
|
|
14017
|
+
if (authErr) return authErr;
|
|
14018
|
+
const body = req.body ?? {};
|
|
14019
|
+
const queryText = asNonEmptyString$1(body["queryText"]);
|
|
14020
|
+
const queryImageRef = asNonEmptyString$1(body["queryImageRef"]);
|
|
14021
|
+
const queryImageBase64 = asNonEmptyString$1(body["queryImageBase64"]);
|
|
14022
|
+
const sessionId = asNonEmptyString$1(body["sessionId"]);
|
|
14023
|
+
if (!queryText && !queryImageRef && !queryImageBase64) return {
|
|
14024
|
+
status_code: 400,
|
|
14025
|
+
body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
|
|
14026
|
+
};
|
|
14027
|
+
const topKParsed = parseOptionalPositiveInt(body["topK"]);
|
|
14028
|
+
if (topKParsed === null) return {
|
|
14029
|
+
status_code: 400,
|
|
14030
|
+
body: { error: "topK must be a positive integer" }
|
|
14031
|
+
};
|
|
14032
|
+
const payload = {};
|
|
14033
|
+
if (queryText) payload["queryText"] = queryText;
|
|
14034
|
+
if (queryImageRef) payload["queryImageRef"] = queryImageRef;
|
|
14035
|
+
if (queryImageBase64) payload["queryImageBase64"] = queryImageBase64;
|
|
14036
|
+
if (sessionId) payload["sessionId"] = sessionId;
|
|
14037
|
+
if (topKParsed !== void 0) payload["topK"] = Math.min(50, topKParsed);
|
|
14038
|
+
const result = await sdk.trigger({
|
|
14039
|
+
function_id: "mem::vision-search",
|
|
14040
|
+
payload
|
|
14041
|
+
});
|
|
14042
|
+
const resp = result;
|
|
14043
|
+
if (resp?.success === false) return {
|
|
14044
|
+
status_code: resp.error?.includes("disabled") ? 503 : 400,
|
|
14045
|
+
body: resp
|
|
14046
|
+
};
|
|
14047
|
+
return {
|
|
14048
|
+
status_code: 200,
|
|
14049
|
+
body: result
|
|
14050
|
+
};
|
|
14051
|
+
});
|
|
14052
|
+
sdk.registerTrigger({
|
|
14053
|
+
type: "http",
|
|
14054
|
+
function_id: "api::vision-search",
|
|
14055
|
+
config: {
|
|
14056
|
+
api_path: "/agentmemory/vision-search",
|
|
14057
|
+
http_method: "POST"
|
|
14058
|
+
}
|
|
14059
|
+
});
|
|
14060
|
+
sdk.registerFunction("api::vision-embed", async (req) => {
|
|
14061
|
+
const authErr = checkAuth(req, secret);
|
|
14062
|
+
if (authErr) return authErr;
|
|
14063
|
+
const body = req.body ?? {};
|
|
14064
|
+
const imageRef = asNonEmptyString$1(body["imageRef"]);
|
|
14065
|
+
const sessionId = asNonEmptyString$1(body["sessionId"]);
|
|
14066
|
+
const observationId = asNonEmptyString$1(body["observationId"]);
|
|
14067
|
+
if (!imageRef) return {
|
|
14068
|
+
status_code: 400,
|
|
14069
|
+
body: { error: "imageRef is required" }
|
|
14070
|
+
};
|
|
14071
|
+
const payload = { imageRef };
|
|
14072
|
+
if (sessionId) payload["sessionId"] = sessionId;
|
|
14073
|
+
if (observationId) payload["observationId"] = observationId;
|
|
14074
|
+
const result = await sdk.trigger({
|
|
14075
|
+
function_id: "mem::vision-embed",
|
|
14076
|
+
payload
|
|
14077
|
+
});
|
|
14078
|
+
const resp = result;
|
|
14079
|
+
if (resp?.success === false) return {
|
|
14080
|
+
status_code: resp.error?.includes("disabled") ? 503 : 400,
|
|
14081
|
+
body: resp
|
|
14082
|
+
};
|
|
14083
|
+
return {
|
|
14084
|
+
status_code: 200,
|
|
14085
|
+
body: result
|
|
14086
|
+
};
|
|
14087
|
+
});
|
|
14088
|
+
sdk.registerTrigger({
|
|
14089
|
+
type: "http",
|
|
14090
|
+
function_id: "api::vision-embed",
|
|
12687
14091
|
config: {
|
|
12688
|
-
api_path: "/agentmemory/
|
|
14092
|
+
api_path: "/agentmemory/vision-embed",
|
|
12689
14093
|
http_method: "POST"
|
|
12690
14094
|
}
|
|
12691
14095
|
});
|
|
12692
|
-
sdk.registerFunction("api::
|
|
14096
|
+
sdk.registerFunction("api::slot-list", async (req) => {
|
|
12693
14097
|
const authErr = checkAuth(req, secret);
|
|
12694
14098
|
if (authErr) return authErr;
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
};
|
|
12703
|
-
} catch {
|
|
12704
|
-
return {
|
|
12705
|
-
status_code: 404,
|
|
12706
|
-
body: { error: "Snapshots not enabled" }
|
|
12707
|
-
};
|
|
12708
|
-
}
|
|
14099
|
+
return {
|
|
14100
|
+
status_code: 200,
|
|
14101
|
+
body: await sdk.trigger({
|
|
14102
|
+
function_id: "mem::slot-list",
|
|
14103
|
+
payload: {}
|
|
14104
|
+
})
|
|
14105
|
+
};
|
|
12709
14106
|
});
|
|
12710
14107
|
sdk.registerTrigger({
|
|
12711
14108
|
type: "http",
|
|
12712
|
-
function_id: "api::
|
|
14109
|
+
function_id: "api::slot-list",
|
|
12713
14110
|
config: {
|
|
12714
|
-
api_path: "/agentmemory/
|
|
14111
|
+
api_path: "/agentmemory/slots",
|
|
12715
14112
|
http_method: "GET"
|
|
12716
14113
|
}
|
|
12717
14114
|
});
|
|
12718
|
-
sdk.registerFunction("api::
|
|
14115
|
+
sdk.registerFunction("api::slot-get", async (req) => {
|
|
12719
14116
|
const authErr = checkAuth(req, secret);
|
|
12720
14117
|
if (authErr) return authErr;
|
|
12721
|
-
|
|
12722
|
-
|
|
12723
|
-
|
|
12724
|
-
|
|
12725
|
-
|
|
12726
|
-
|
|
12727
|
-
|
|
12728
|
-
}
|
|
12729
|
-
}
|
|
12730
|
-
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
}
|
|
14118
|
+
const label = asNonEmptyString$1(req.query_params?.["label"]);
|
|
14119
|
+
if (!label) return {
|
|
14120
|
+
status_code: 400,
|
|
14121
|
+
body: { error: "label query param required" }
|
|
14122
|
+
};
|
|
14123
|
+
const result = await sdk.trigger({
|
|
14124
|
+
function_id: "mem::slot-get",
|
|
14125
|
+
payload: { label }
|
|
14126
|
+
});
|
|
14127
|
+
const resp = result;
|
|
14128
|
+
if (resp?.success === false) return {
|
|
14129
|
+
status_code: resp.error?.includes("not found") ? 404 : 400,
|
|
14130
|
+
body: resp
|
|
14131
|
+
};
|
|
14132
|
+
return {
|
|
14133
|
+
status_code: 200,
|
|
14134
|
+
body: result
|
|
14135
|
+
};
|
|
12735
14136
|
});
|
|
12736
14137
|
sdk.registerTrigger({
|
|
12737
14138
|
type: "http",
|
|
12738
|
-
function_id: "api::
|
|
14139
|
+
function_id: "api::slot-get",
|
|
12739
14140
|
config: {
|
|
12740
|
-
api_path: "/agentmemory/
|
|
12741
|
-
http_method: "
|
|
14141
|
+
api_path: "/agentmemory/slot",
|
|
14142
|
+
http_method: "GET"
|
|
12742
14143
|
}
|
|
12743
14144
|
});
|
|
12744
|
-
sdk.registerFunction("api::
|
|
14145
|
+
sdk.registerFunction("api::slot-create", async (req) => {
|
|
12745
14146
|
const authErr = checkAuth(req, secret);
|
|
12746
14147
|
if (authErr) return authErr;
|
|
12747
|
-
|
|
14148
|
+
const body = req.body ?? {};
|
|
14149
|
+
const label = asNonEmptyString$1(body["label"]);
|
|
14150
|
+
if (!label) return {
|
|
12748
14151
|
status_code: 400,
|
|
12749
|
-
body: { error: "
|
|
14152
|
+
body: { error: "label required" }
|
|
14153
|
+
};
|
|
14154
|
+
if (body["content"] !== void 0 && typeof body["content"] !== "string") return {
|
|
14155
|
+
status_code: 400,
|
|
14156
|
+
body: { error: "content must be a string" }
|
|
14157
|
+
};
|
|
14158
|
+
if (body["description"] !== void 0 && typeof body["description"] !== "string") return {
|
|
14159
|
+
status_code: 400,
|
|
14160
|
+
body: { error: "description must be a string" }
|
|
14161
|
+
};
|
|
14162
|
+
if (body["pinned"] !== void 0 && typeof body["pinned"] !== "boolean") return {
|
|
14163
|
+
status_code: 400,
|
|
14164
|
+
body: { error: "pinned must be a boolean" }
|
|
14165
|
+
};
|
|
14166
|
+
if (body["scope"] !== void 0 && body["scope"] !== "project" && body["scope"] !== "global") return {
|
|
14167
|
+
status_code: 400,
|
|
14168
|
+
body: { error: "scope must be 'project' or 'global'" }
|
|
14169
|
+
};
|
|
14170
|
+
const sizeLimit = parseOptionalPositiveInt(body["sizeLimit"]);
|
|
14171
|
+
if (sizeLimit === null) return {
|
|
14172
|
+
status_code: 400,
|
|
14173
|
+
body: { error: "sizeLimit must be a positive integer" }
|
|
14174
|
+
};
|
|
14175
|
+
if (sizeLimit !== void 0 && sizeLimit > 2e4) return {
|
|
14176
|
+
status_code: 400,
|
|
14177
|
+
body: { error: "sizeLimit must be <= 20000" }
|
|
14178
|
+
};
|
|
14179
|
+
const payload = { label };
|
|
14180
|
+
if (typeof body["content"] === "string") payload["content"] = body["content"];
|
|
14181
|
+
if (typeof body["description"] === "string") payload["description"] = body["description"];
|
|
14182
|
+
if (sizeLimit !== void 0) payload["sizeLimit"] = sizeLimit;
|
|
14183
|
+
if (typeof body["pinned"] === "boolean") payload["pinned"] = body["pinned"];
|
|
14184
|
+
if (body["scope"] === "project" || body["scope"] === "global") payload["scope"] = body["scope"];
|
|
14185
|
+
const result = await sdk.trigger({
|
|
14186
|
+
function_id: "mem::slot-create",
|
|
14187
|
+
payload
|
|
14188
|
+
});
|
|
14189
|
+
const resp = result;
|
|
14190
|
+
if (resp?.success === false) return {
|
|
14191
|
+
status_code: resp.error?.includes("exists") ? 409 : 400,
|
|
14192
|
+
body: resp
|
|
14193
|
+
};
|
|
14194
|
+
return {
|
|
14195
|
+
status_code: 201,
|
|
14196
|
+
body: result
|
|
12750
14197
|
};
|
|
12751
|
-
try {
|
|
12752
|
-
return {
|
|
12753
|
-
status_code: 200,
|
|
12754
|
-
body: await sdk.trigger({
|
|
12755
|
-
function_id: "mem::snapshot-restore",
|
|
12756
|
-
payload: req.body
|
|
12757
|
-
})
|
|
12758
|
-
};
|
|
12759
|
-
} catch {
|
|
12760
|
-
return {
|
|
12761
|
-
status_code: 404,
|
|
12762
|
-
body: { error: "Snapshots not enabled" }
|
|
12763
|
-
};
|
|
12764
|
-
}
|
|
12765
14198
|
});
|
|
12766
14199
|
sdk.registerTrigger({
|
|
12767
14200
|
type: "http",
|
|
12768
|
-
function_id: "api::
|
|
14201
|
+
function_id: "api::slot-create",
|
|
12769
14202
|
config: {
|
|
12770
|
-
api_path: "/agentmemory/
|
|
14203
|
+
api_path: "/agentmemory/slot",
|
|
12771
14204
|
http_method: "POST"
|
|
12772
14205
|
}
|
|
12773
14206
|
});
|
|
12774
|
-
sdk.registerFunction("api::
|
|
14207
|
+
sdk.registerFunction("api::slot-append", async (req) => {
|
|
12775
14208
|
const authErr = checkAuth(req, secret);
|
|
12776
14209
|
if (authErr) return authErr;
|
|
12777
|
-
const
|
|
14210
|
+
const body = req.body ?? {};
|
|
14211
|
+
const label = asNonEmptyString$1(body["label"]);
|
|
14212
|
+
const text = typeof body["text"] === "string" ? body["text"] : null;
|
|
14213
|
+
if (!label || !text) return {
|
|
14214
|
+
status_code: 400,
|
|
14215
|
+
body: { error: "label and text required" }
|
|
14216
|
+
};
|
|
14217
|
+
const result = await sdk.trigger({
|
|
14218
|
+
function_id: "mem::slot-append",
|
|
14219
|
+
payload: {
|
|
14220
|
+
label,
|
|
14221
|
+
text
|
|
14222
|
+
}
|
|
14223
|
+
});
|
|
14224
|
+
const resp = result;
|
|
14225
|
+
if (resp?.success === false) {
|
|
14226
|
+
const notFound = resp.error?.includes("not found");
|
|
14227
|
+
const overLimit = resp.error?.includes("exceed");
|
|
14228
|
+
return {
|
|
14229
|
+
status_code: notFound ? 404 : overLimit ? 413 : 400,
|
|
14230
|
+
body: resp
|
|
14231
|
+
};
|
|
14232
|
+
}
|
|
12778
14233
|
return {
|
|
12779
14234
|
status_code: 200,
|
|
12780
|
-
body:
|
|
14235
|
+
body: result
|
|
12781
14236
|
};
|
|
12782
14237
|
});
|
|
12783
14238
|
sdk.registerTrigger({
|
|
12784
14239
|
type: "http",
|
|
12785
|
-
function_id: "api::
|
|
14240
|
+
function_id: "api::slot-append",
|
|
12786
14241
|
config: {
|
|
12787
|
-
api_path: "/agentmemory/
|
|
12788
|
-
http_method: "
|
|
14242
|
+
api_path: "/agentmemory/slot/append",
|
|
14243
|
+
http_method: "POST"
|
|
12789
14244
|
}
|
|
12790
14245
|
});
|
|
12791
|
-
sdk.registerFunction("api::
|
|
14246
|
+
sdk.registerFunction("api::slot-replace", async (req) => {
|
|
12792
14247
|
const authErr = checkAuth(req, secret);
|
|
12793
14248
|
if (authErr) return authErr;
|
|
14249
|
+
const body = req.body ?? {};
|
|
14250
|
+
const label = asNonEmptyString$1(body["label"]);
|
|
14251
|
+
const content = body["content"];
|
|
14252
|
+
if (!label || typeof content !== "string") return {
|
|
14253
|
+
status_code: 400,
|
|
14254
|
+
body: { error: "label and content (string) required" }
|
|
14255
|
+
};
|
|
14256
|
+
const result = await sdk.trigger({
|
|
14257
|
+
function_id: "mem::slot-replace",
|
|
14258
|
+
payload: {
|
|
14259
|
+
label,
|
|
14260
|
+
content
|
|
14261
|
+
}
|
|
14262
|
+
});
|
|
14263
|
+
const resp = result;
|
|
14264
|
+
if (resp?.success === false) {
|
|
14265
|
+
const notFound = resp.error?.includes("not found");
|
|
14266
|
+
const overLimit = resp.error?.includes("exceed");
|
|
14267
|
+
return {
|
|
14268
|
+
status_code: notFound ? 404 : overLimit ? 413 : 400,
|
|
14269
|
+
body: resp
|
|
14270
|
+
};
|
|
14271
|
+
}
|
|
12794
14272
|
return {
|
|
12795
14273
|
status_code: 200,
|
|
12796
|
-
body:
|
|
14274
|
+
body: result
|
|
12797
14275
|
};
|
|
12798
14276
|
});
|
|
12799
14277
|
sdk.registerTrigger({
|
|
12800
14278
|
type: "http",
|
|
12801
|
-
function_id: "api::
|
|
14279
|
+
function_id: "api::slot-replace",
|
|
12802
14280
|
config: {
|
|
12803
|
-
api_path: "/agentmemory/
|
|
12804
|
-
http_method: "
|
|
14281
|
+
api_path: "/agentmemory/slot/replace",
|
|
14282
|
+
http_method: "POST"
|
|
12805
14283
|
}
|
|
12806
14284
|
});
|
|
12807
|
-
sdk.registerFunction("api::
|
|
14285
|
+
sdk.registerFunction("api::slot-delete", async (req) => {
|
|
12808
14286
|
const authErr = checkAuth(req, secret);
|
|
12809
14287
|
if (authErr) return authErr;
|
|
14288
|
+
const label = asNonEmptyString$1(req.query_params?.["label"]);
|
|
14289
|
+
if (!label) return {
|
|
14290
|
+
status_code: 400,
|
|
14291
|
+
body: { error: "label query param required" }
|
|
14292
|
+
};
|
|
14293
|
+
const result = await sdk.trigger({
|
|
14294
|
+
function_id: "mem::slot-delete",
|
|
14295
|
+
payload: { label }
|
|
14296
|
+
});
|
|
14297
|
+
const resp = result;
|
|
14298
|
+
if (resp?.success === false) return {
|
|
14299
|
+
status_code: resp.error?.includes("not found") ? 404 : 400,
|
|
14300
|
+
body: resp
|
|
14301
|
+
};
|
|
12810
14302
|
return {
|
|
12811
14303
|
status_code: 200,
|
|
12812
|
-
body:
|
|
14304
|
+
body: result
|
|
12813
14305
|
};
|
|
12814
14306
|
});
|
|
12815
14307
|
sdk.registerTrigger({
|
|
12816
14308
|
type: "http",
|
|
12817
|
-
function_id: "api::
|
|
14309
|
+
function_id: "api::slot-delete",
|
|
12818
14310
|
config: {
|
|
12819
|
-
api_path: "/agentmemory/
|
|
12820
|
-
http_method: "
|
|
14311
|
+
api_path: "/agentmemory/slot",
|
|
14312
|
+
http_method: "DELETE"
|
|
12821
14313
|
}
|
|
12822
14314
|
});
|
|
12823
|
-
sdk.registerFunction("api::
|
|
14315
|
+
sdk.registerFunction("api::slot-reflect", async (req) => {
|
|
12824
14316
|
const authErr = checkAuth(req, secret);
|
|
12825
14317
|
if (authErr) return authErr;
|
|
14318
|
+
const body = req.body ?? {};
|
|
14319
|
+
const sessionId = asNonEmptyString$1(body["sessionId"]);
|
|
14320
|
+
if (!sessionId) return {
|
|
14321
|
+
status_code: 400,
|
|
14322
|
+
body: { error: "sessionId required" }
|
|
14323
|
+
};
|
|
14324
|
+
const maxObservations = parseOptionalPositiveInt(body["maxObservations"]);
|
|
14325
|
+
if (maxObservations === null) return {
|
|
14326
|
+
status_code: 400,
|
|
14327
|
+
body: { error: "maxObservations must be a positive integer" }
|
|
14328
|
+
};
|
|
14329
|
+
const payload = { sessionId };
|
|
14330
|
+
if (maxObservations !== void 0) payload["maxObservations"] = maxObservations;
|
|
12826
14331
|
return {
|
|
12827
14332
|
status_code: 200,
|
|
12828
|
-
body:
|
|
14333
|
+
body: await sdk.trigger({
|
|
14334
|
+
function_id: "mem::slot-reflect",
|
|
14335
|
+
payload
|
|
14336
|
+
})
|
|
12829
14337
|
};
|
|
12830
14338
|
});
|
|
12831
14339
|
sdk.registerTrigger({
|
|
12832
14340
|
type: "http",
|
|
12833
|
-
function_id: "api::
|
|
14341
|
+
function_id: "api::slot-reflect",
|
|
12834
14342
|
config: {
|
|
12835
|
-
api_path: "/agentmemory/
|
|
12836
|
-
http_method: "
|
|
14343
|
+
api_path: "/agentmemory/slot/reflect",
|
|
14344
|
+
http_method: "POST"
|
|
12837
14345
|
}
|
|
12838
14346
|
});
|
|
12839
14347
|
sdk.registerFunction("api::action-create", async (req) => {
|
|
@@ -13070,9 +14578,9 @@ function registerApiTriggers(sdk, kv, secret, metricsStore, provider) {
|
|
|
13070
14578
|
sdk.registerFunction("api::routine-create", async (req) => {
|
|
13071
14579
|
const authErr = checkAuth(req, secret);
|
|
13072
14580
|
if (authErr) return authErr;
|
|
13073
|
-
if (!req.body?.name) return {
|
|
14581
|
+
if (!req.body?.name || !req.body?.steps) return {
|
|
13074
14582
|
status_code: 400,
|
|
13075
|
-
body: { error: "name
|
|
14583
|
+
body: { error: "name and steps are required" }
|
|
13076
14584
|
};
|
|
13077
14585
|
return {
|
|
13078
14586
|
status_code: 201,
|
|
@@ -14321,10 +15829,21 @@ function registerEventTriggers(sdk, kv) {
|
|
|
14321
15829
|
function_id: "event::observation",
|
|
14322
15830
|
config: { topic: "agentmemory.observation" }
|
|
14323
15831
|
});
|
|
14324
|
-
sdk.registerFunction("event::session::stopped", async (data) =>
|
|
14325
|
-
|
|
14326
|
-
|
|
14327
|
-
|
|
15832
|
+
sdk.registerFunction("event::session::stopped", async (data) => {
|
|
15833
|
+
const summary = await sdk.trigger({
|
|
15834
|
+
function_id: "mem::summarize",
|
|
15835
|
+
payload: data
|
|
15836
|
+
});
|
|
15837
|
+
if (isReflectEnabled()) try {
|
|
15838
|
+
sdk.triggerVoid("mem::slot-reflect", { sessionId: data.sessionId });
|
|
15839
|
+
} catch (err) {
|
|
15840
|
+
logger.warn("slot-reflect triggerVoid failed", {
|
|
15841
|
+
sessionId: data.sessionId,
|
|
15842
|
+
error: err instanceof Error ? err.message : String(err)
|
|
15843
|
+
});
|
|
15844
|
+
}
|
|
15845
|
+
return summary;
|
|
15846
|
+
});
|
|
14328
15847
|
sdk.registerTrigger({
|
|
14329
15848
|
type: "durable:subscriber",
|
|
14330
15849
|
function_id: "event::session::stopped",
|
|
@@ -14576,6 +16095,34 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
14576
16095
|
}] }
|
|
14577
16096
|
};
|
|
14578
16097
|
}
|
|
16098
|
+
case "memory_vision_search": {
|
|
16099
|
+
const queryText = typeof args.queryText === "string" ? args.queryText : void 0;
|
|
16100
|
+
const queryImageRef = typeof args.queryImageRef === "string" ? args.queryImageRef : void 0;
|
|
16101
|
+
const queryImageBase64 = typeof args.queryImageBase64 === "string" ? args.queryImageBase64 : void 0;
|
|
16102
|
+
if (!queryText && !queryImageRef && !queryImageBase64) return {
|
|
16103
|
+
status_code: 400,
|
|
16104
|
+
body: { error: "queryText, queryImageRef, or queryImageBase64 required" }
|
|
16105
|
+
};
|
|
16106
|
+
const topK = Math.max(1, Math.min(50, asNumber(args.topK, 10) ?? 10));
|
|
16107
|
+
const sessionId = typeof args.sessionId === "string" ? args.sessionId : void 0;
|
|
16108
|
+
const result = await sdk.trigger({
|
|
16109
|
+
function_id: "mem::vision-search",
|
|
16110
|
+
payload: {
|
|
16111
|
+
queryText,
|
|
16112
|
+
queryImageRef,
|
|
16113
|
+
queryImageBase64,
|
|
16114
|
+
topK,
|
|
16115
|
+
sessionId
|
|
16116
|
+
}
|
|
16117
|
+
});
|
|
16118
|
+
return {
|
|
16119
|
+
status_code: 200,
|
|
16120
|
+
body: { content: [{
|
|
16121
|
+
type: "text",
|
|
16122
|
+
text: JSON.stringify(result, null, 2)
|
|
16123
|
+
}] }
|
|
16124
|
+
};
|
|
16125
|
+
}
|
|
14579
16126
|
case "memory_timeline": {
|
|
14580
16127
|
if (typeof args.anchor !== "string" || !args.anchor.trim()) return {
|
|
14581
16128
|
status_code: 400,
|
|
@@ -15441,6 +16988,123 @@ function registerMcpEndpoints(sdk, kv, secret) {
|
|
|
15441
16988
|
}] }
|
|
15442
16989
|
};
|
|
15443
16990
|
}
|
|
16991
|
+
case "memory_slot_list": {
|
|
16992
|
+
const result = await sdk.trigger({
|
|
16993
|
+
function_id: "mem::slot-list",
|
|
16994
|
+
payload: {}
|
|
16995
|
+
});
|
|
16996
|
+
return {
|
|
16997
|
+
status_code: 200,
|
|
16998
|
+
body: { content: [{
|
|
16999
|
+
type: "text",
|
|
17000
|
+
text: JSON.stringify(result, null, 2)
|
|
17001
|
+
}] }
|
|
17002
|
+
};
|
|
17003
|
+
}
|
|
17004
|
+
case "memory_slot_get": {
|
|
17005
|
+
const label = asNonEmptyString(args.label);
|
|
17006
|
+
if (!label) return {
|
|
17007
|
+
status_code: 400,
|
|
17008
|
+
body: { error: "label required" }
|
|
17009
|
+
};
|
|
17010
|
+
const result = await sdk.trigger({
|
|
17011
|
+
function_id: "mem::slot-get",
|
|
17012
|
+
payload: { label }
|
|
17013
|
+
});
|
|
17014
|
+
return {
|
|
17015
|
+
status_code: 200,
|
|
17016
|
+
body: { content: [{
|
|
17017
|
+
type: "text",
|
|
17018
|
+
text: JSON.stringify(result, null, 2)
|
|
17019
|
+
}] }
|
|
17020
|
+
};
|
|
17021
|
+
}
|
|
17022
|
+
case "memory_slot_create": {
|
|
17023
|
+
const label = asNonEmptyString(args.label);
|
|
17024
|
+
if (!label) return {
|
|
17025
|
+
status_code: 400,
|
|
17026
|
+
body: { error: "label required" }
|
|
17027
|
+
};
|
|
17028
|
+
const payload = { label };
|
|
17029
|
+
if (typeof args.content === "string") payload.content = args.content;
|
|
17030
|
+
if (typeof args.description === "string") payload.description = args.description;
|
|
17031
|
+
if (typeof args.sizeLimit === "number") payload.sizeLimit = args.sizeLimit;
|
|
17032
|
+
if (args.pinned === false || args.pinned === "false") payload.pinned = false;
|
|
17033
|
+
else if (args.pinned === true || args.pinned === "true") payload.pinned = true;
|
|
17034
|
+
if (args.scope === "global" || args.scope === "project") payload.scope = args.scope;
|
|
17035
|
+
const result = await sdk.trigger({
|
|
17036
|
+
function_id: "mem::slot-create",
|
|
17037
|
+
payload
|
|
17038
|
+
});
|
|
17039
|
+
return {
|
|
17040
|
+
status_code: 200,
|
|
17041
|
+
body: { content: [{
|
|
17042
|
+
type: "text",
|
|
17043
|
+
text: JSON.stringify(result, null, 2)
|
|
17044
|
+
}] }
|
|
17045
|
+
};
|
|
17046
|
+
}
|
|
17047
|
+
case "memory_slot_append": {
|
|
17048
|
+
const label = asNonEmptyString(args.label);
|
|
17049
|
+
const text = typeof args.text === "string" ? args.text : null;
|
|
17050
|
+
if (!label || !text) return {
|
|
17051
|
+
status_code: 400,
|
|
17052
|
+
body: { error: "label and text required" }
|
|
17053
|
+
};
|
|
17054
|
+
const result = await sdk.trigger({
|
|
17055
|
+
function_id: "mem::slot-append",
|
|
17056
|
+
payload: {
|
|
17057
|
+
label,
|
|
17058
|
+
text
|
|
17059
|
+
}
|
|
17060
|
+
});
|
|
17061
|
+
return {
|
|
17062
|
+
status_code: 200,
|
|
17063
|
+
body: { content: [{
|
|
17064
|
+
type: "text",
|
|
17065
|
+
text: JSON.stringify(result, null, 2)
|
|
17066
|
+
}] }
|
|
17067
|
+
};
|
|
17068
|
+
}
|
|
17069
|
+
case "memory_slot_replace": {
|
|
17070
|
+
const label = asNonEmptyString(args.label);
|
|
17071
|
+
if (!label || typeof args.content !== "string") return {
|
|
17072
|
+
status_code: 400,
|
|
17073
|
+
body: { error: "label and content (string) required" }
|
|
17074
|
+
};
|
|
17075
|
+
const result = await sdk.trigger({
|
|
17076
|
+
function_id: "mem::slot-replace",
|
|
17077
|
+
payload: {
|
|
17078
|
+
label,
|
|
17079
|
+
content: args.content
|
|
17080
|
+
}
|
|
17081
|
+
});
|
|
17082
|
+
return {
|
|
17083
|
+
status_code: 200,
|
|
17084
|
+
body: { content: [{
|
|
17085
|
+
type: "text",
|
|
17086
|
+
text: JSON.stringify(result, null, 2)
|
|
17087
|
+
}] }
|
|
17088
|
+
};
|
|
17089
|
+
}
|
|
17090
|
+
case "memory_slot_delete": {
|
|
17091
|
+
const label = asNonEmptyString(args.label);
|
|
17092
|
+
if (!label) return {
|
|
17093
|
+
status_code: 400,
|
|
17094
|
+
body: { error: "label required" }
|
|
17095
|
+
};
|
|
17096
|
+
const result = await sdk.trigger({
|
|
17097
|
+
function_id: "mem::slot-delete",
|
|
17098
|
+
payload: { label }
|
|
17099
|
+
});
|
|
17100
|
+
return {
|
|
17101
|
+
status_code: 200,
|
|
17102
|
+
body: { content: [{
|
|
17103
|
+
type: "text",
|
|
17104
|
+
text: JSON.stringify(result, null, 2)
|
|
17105
|
+
}] }
|
|
17106
|
+
};
|
|
17107
|
+
}
|
|
15444
17108
|
default: return {
|
|
15445
17109
|
status_code: 400,
|
|
15446
17110
|
body: { error: `Unknown tool: ${name}` }
|
|
@@ -16095,11 +17759,13 @@ async function main() {
|
|
|
16095
17759
|
const fallbackConfig = loadFallbackConfig();
|
|
16096
17760
|
const provider = fallbackConfig.providers.length > 0 ? createFallbackProvider(config.provider, fallbackConfig) : createProvider(config.provider);
|
|
16097
17761
|
const embeddingProvider = createEmbeddingProvider();
|
|
17762
|
+
const imageEmbeddingProvider = createImageEmbeddingProvider();
|
|
16098
17763
|
console.log(`[agentmemory] Starting worker v${VERSION}...`);
|
|
16099
17764
|
console.log(`[agentmemory] Engine: ${config.engineUrl}`);
|
|
16100
17765
|
console.log(`[agentmemory] Provider: ${config.provider.provider} (${config.provider.model})`);
|
|
16101
17766
|
if (embeddingProvider) console.log(`[agentmemory] Embedding provider: ${embeddingProvider.name} (${embeddingProvider.dimensions} dims)`);
|
|
16102
17767
|
else console.log(`[agentmemory] Embedding provider: none (BM25-only mode)`);
|
|
17768
|
+
if (imageEmbeddingProvider) console.log(`[agentmemory] Image embedding provider: ${imageEmbeddingProvider.name} (${imageEmbeddingProvider.dimensions} dims) — vision-search active`);
|
|
16103
17769
|
console.log(`[agentmemory] REST API: http://localhost:${config.restPort}/agentmemory/*`);
|
|
16104
17770
|
console.log(`[agentmemory] Streams: ws://localhost:${config.streamsPort}`);
|
|
16105
17771
|
const sdk = registerWorker(config.engineUrl, {
|
|
@@ -16118,6 +17784,10 @@ async function main() {
|
|
|
16118
17784
|
initMetrics(hasGetMeter(sdk) ? sdk.getMeter.bind(sdk) : void 0);
|
|
16119
17785
|
registerPrivacyFunction(sdk);
|
|
16120
17786
|
registerObserveFunction(sdk, kv, dedupMap, config.maxObservationsPerSession);
|
|
17787
|
+
registerImageQuotaCleanup(sdk, kv);
|
|
17788
|
+
registerVisionSearchFunctions(sdk, kv, imageEmbeddingProvider);
|
|
17789
|
+
if (isSlotsEnabled()) registerSlotsFunctions(sdk, kv);
|
|
17790
|
+
registerDiskSizeManager(sdk, kv);
|
|
16121
17791
|
registerCompressFunction(sdk, kv, provider, metricsStore);
|
|
16122
17792
|
registerSearchFunction(sdk, kv);
|
|
16123
17793
|
registerContextFunction(sdk, kv, config.tokenBudget);
|
|
@@ -16184,6 +17854,7 @@ async function main() {
|
|
|
16184
17854
|
registerReplayFunctions(sdk, kv);
|
|
16185
17855
|
console.log(`[agentmemory] v0.6 advanced retrieval: sliding-window, query-expansion, temporal-graph, retention-scoring`);
|
|
16186
17856
|
console.log(`[agentmemory] Orchestration layer: actions, frontier, leases, routines, signals, checkpoints, flow-compress, mesh, branch-aware, sentinels, sketches, crystallize, diagnostics, facets`);
|
|
17857
|
+
if (isSlotsEnabled()) console.log(`[agentmemory] Slots: enabled (pinned editable memory). Reflect on Stop hook: ${isReflectEnabled() ? "on" : "off"}`);
|
|
16187
17858
|
const snapshotConfig = loadSnapshotConfig();
|
|
16188
17859
|
if (snapshotConfig.enabled) {
|
|
16189
17860
|
registerSnapshotFunction(sdk, kv, snapshotConfig.dir);
|
|
@@ -16221,7 +17892,7 @@ async function main() {
|
|
|
16221
17892
|
}
|
|
16222
17893
|
}
|
|
16223
17894
|
console.log(`[agentmemory] Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`);
|
|
16224
|
-
console.log(`[agentmemory] Endpoints: 107 REST +
|
|
17895
|
+
console.log(`[agentmemory] Endpoints: 107 REST + ${getAllTools().length} MCP tools + 6 MCP resources + 3 MCP prompts`);
|
|
16225
17896
|
const viewerServer = startViewerServer(config.restPort + 2, kv, sdk, secret, config.restPort);
|
|
16226
17897
|
const autoForgetIntervalMs = parseInt(process.env.AUTO_FORGET_INTERVAL_MS || "3600000", 10);
|
|
16227
17898
|
const consolidationIntervalMs = parseInt(process.env.CONSOLIDATION_INTERVAL_MS || "7200000", 10);
|
|
@@ -16287,5 +17958,5 @@ main().catch((err) => {
|
|
|
16287
17958
|
});
|
|
16288
17959
|
|
|
16289
17960
|
//#endregion
|
|
16290
|
-
export {
|
|
16291
|
-
//# sourceMappingURL=src-
|
|
17961
|
+
export { deleteImage as a, saveImageToDisk as c, IMAGES_DIR as i, touchImage as l, getImageRefCount as n, getMaxBytes as o, incrementImageRef as r, isManagedImagePath as s, decrementImageRef as t };
|
|
17962
|
+
//# sourceMappingURL=src-tmuZyobT.mjs.map
|