@evermind-ai/openclaw-plugin 1.1.0

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/index.js ADDED
@@ -0,0 +1,397 @@
1
+ /**
2
+ * EverMemOS ContextEngine Plugin for OpenClaw
3
+ * Registers EverMemOS as the context engine for memory management
4
+ */
5
+
6
+ import { resolveConfig } from "./src/config.js";
7
+ import { searchMemories, saveMemories } from "./src/memory-api.js";
8
+ import { buildMemoryPrompt, parseSearchResponse } from "./src/formatter.js";
9
+ import { collectMessages, toText, isSessionResetPrompt } from "./src/message-utils.js";
10
+
11
+ const PLUGIN_ID = "evermemos-openclaw-plugin";
12
+
13
+ /**
14
+ * Convert OpenClaw AgentMessage to EverMemOS message format
15
+ * Handles both OpenClaw format (toolCall/toolResult) and Anthropic format (tool_use/tool_result)
16
+ * @param {Object} msg - OpenClaw AgentMessage
17
+ * @returns {Object} - EverMemOS message format
18
+ */
19
+ function convertMessage(msg) {
20
+ const content = msg.content;
21
+ const originalRole = msg.role;
22
+ let role = originalRole;
23
+ let toolCalls = undefined;
24
+ let toolCallId = undefined;
25
+ let textContent = "";
26
+
27
+ // Normalize role - preserve toolResult for EverMemOS
28
+ // OpenClaw uses "toolResult", we map it to "tool" for EverMemOS
29
+ if (role === "toolResult") {
30
+ role = "tool";
31
+ toolCallId = msg.toolCallId || undefined;
32
+ } else if (role !== "user" && role !== "assistant" && role !== "tool") {
33
+ role = "user";
34
+ }
35
+
36
+ // Handle text content (simple string)
37
+ if (typeof content === "string") {
38
+ const result = { role, content };
39
+ if (toolCallId) result.tool_call_id = toolCallId;
40
+ return result;
41
+ }
42
+
43
+ // Handle content blocks (array of {type, ...})
44
+ if (Array.isArray(content)) {
45
+ for (const block of content) {
46
+ if (!block || !block.type) continue;
47
+
48
+ if (block.type === "text") {
49
+ const text = block.text ?? "";
50
+ textContent += (textContent ? "\n" : "") + text;
51
+ }
52
+ // OpenClaw format: toolCall
53
+ else if (block.type === "toolCall") {
54
+ toolCalls = toolCalls || [];
55
+ const tc = {
56
+ id: block.id,
57
+ type: "function",
58
+ function: {
59
+ name: block.name || "unknown",
60
+ arguments: typeof block.arguments === "string"
61
+ ? block.arguments
62
+ : JSON.stringify(block.arguments ?? {}),
63
+ },
64
+ };
65
+ toolCalls.push(tc);
66
+ textContent += (textContent ? "\n" : "") + `[Tool: ${block.name || "unknown"}]`;
67
+ }
68
+ // Anthropic format: tool_use
69
+ else if (block.type === "tool_use") {
70
+ toolCalls = toolCalls || [];
71
+ const tc = {
72
+ id: block.id,
73
+ type: "function",
74
+ function: {
75
+ name: block.name || "unknown",
76
+ arguments: typeof block.input === "string"
77
+ ? block.input
78
+ : JSON.stringify(block.input ?? {}),
79
+ },
80
+ };
81
+ toolCalls.push(tc);
82
+ textContent += (textContent ? "\n" : "") + `[Tool: ${block.name || "unknown"}]`;
83
+ }
84
+ // Anthropic format: tool_result (in assistant content array)
85
+ else if (block.type === "tool_result") {
86
+ toolCallId = block.tool_use_id;
87
+ const preview = safePreview(block.content);
88
+ textContent += (textContent ? "\n" : "") + `[Tool Result: ${preview}]`;
89
+ }
90
+ }
91
+
92
+ const result = { role, content: textContent || "" };
93
+ if (toolCalls) result.tool_calls = toolCalls;
94
+ if (toolCallId) result.tool_call_id = toolCallId;
95
+ return result;
96
+ }
97
+
98
+ // Fallback for unexpected content types
99
+ const fallbackContent = content == null ? "" : String(content);
100
+ const result = { role, content: fallbackContent };
101
+ if (toolCallId) result.tool_call_id = toolCallId;
102
+ return result;
103
+ }
104
+
105
+ /**
106
+ * Safely create a preview of tool result content
107
+ * @param {*} content - Tool result content (string, object, array, null, undefined, Symbol, function)
108
+ * @param {number} maxLength - Maximum preview length
109
+ * @returns {string} - Safe preview string
110
+ */
111
+ function safePreview(content, maxLength = 200) {
112
+ if (content == null) return "(empty)";
113
+
114
+ let str = "";
115
+ if (typeof content === "string") {
116
+ str = content;
117
+ } else {
118
+ try {
119
+ str = JSON.stringify(content);
120
+ // JSON.stringify returns undefined for Symbol/function/undefined values
121
+ if (typeof str !== "string") {
122
+ str = String(content);
123
+ }
124
+ } catch {
125
+ str = String(content);
126
+ }
127
+ }
128
+
129
+ if (str.length <= maxLength) return str;
130
+ return str.slice(0, maxLength) + "...";
131
+ }
132
+
133
+ /**
134
+ * Create EverMemOS ContextEngine instance
135
+ * @param {Object} pluginConfig - Plugin configuration
136
+ * @param {Object} logger - Logger instance
137
+ * @returns {Object} - ContextEngine implementation
138
+ */
139
+ function createContextEngineInstance(pluginConfig, logger) {
140
+ const cfg = resolveConfig(pluginConfig);
141
+ const log = logger || { info: (...a) => console.log(...a), warn: (...a) => console.warn(...a) };
142
+
143
+ log.info(`[evermemos] ContextEngine config: baseUrl=${cfg.serverUrl}, userId=${cfg.userId}`);
144
+
145
+ // Session state - shared across all sessions for this engine instance
146
+ const sessionState = new Map();
147
+
148
+ return {
149
+ info: {
150
+ id: PLUGIN_ID,
151
+ name: "EverMemOS ContextEngine",
152
+ version: "1.0.0",
153
+ ownsCompaction: false,
154
+ },
155
+
156
+ async bootstrap({ sessionId, sessionKey }) {
157
+ log.info(`[evermemos] bootstrap: session=${sessionId}, key=${sessionKey}`);
158
+
159
+ // Verify EverMemOS backend health
160
+ try {
161
+ const response = await fetch(`${cfg.serverUrl}/health`, {
162
+ signal: AbortSignal.timeout(5000),
163
+ });
164
+ if (response.ok) {
165
+ const result = await response.json();
166
+ log.info(`[evermemos] bootstrap: backend healthy, status=${result.status}`);
167
+ } else {
168
+ log.warn(`[evermemos] bootstrap: backend unhealthy, status=${response.status}`);
169
+ }
170
+ } catch (err) {
171
+ log.warn(`[evermemos] bootstrap: health check failed: ${err.message}`);
172
+ }
173
+
174
+ // Initialize or get session state
175
+ if (!sessionState.has(sessionKey)) {
176
+ sessionState.set(sessionKey, {
177
+ turnCount: 0,
178
+ lastAssembleTime: 0,
179
+ pendingFlush: false,
180
+ pendingMessages: [],
181
+ });
182
+ log.info(`[evermemos] bootstrap: initialized state for ${sessionKey}`);
183
+ } else {
184
+ log.info(`[evermemos] bootstrap: reusing existing state for ${sessionKey}, turn=${sessionState.get(sessionKey).turnCount}`);
185
+ }
186
+
187
+ return { bootstrapped: true };
188
+ },
189
+
190
+ async ingest({ sessionId, sessionKey, message }) {
191
+ log.info(`[evermemos] ingest: session=${sessionKey}, role=${message?.role}, isHeartbeat=${message?.isHeartbeat}`);
192
+
193
+ const state = sessionState.get(sessionKey);
194
+ if (!state) {
195
+ log.warn(`[evermemos] ingest: no state for session=${sessionKey}`);
196
+ return { ingested: false };
197
+ }
198
+
199
+ // Don't ingest heartbeats
200
+ if (message.isHeartbeat) {
201
+ return { ingested: false };
202
+ }
203
+
204
+ // Store for batch processing in afterTurn
205
+ state.pendingMessages.push(message);
206
+ log.info(`[evermemos] ingest: collected ${state.pendingMessages.length} messages so far`);
207
+
208
+ return { ingested: true };
209
+ },
210
+
211
+ async ingestBatch({ sessionId, sessionKey, messages, isHeartbeat }) {
212
+ log.info(`[evermemos] ingestBatch: session=${sessionKey}, count=${messages?.length}, isHeartbeat=${isHeartbeat}`);
213
+
214
+ if (isHeartbeat) {
215
+ return { ingestedCount: 0 };
216
+ }
217
+
218
+ const state = sessionState.get(sessionKey);
219
+ if (!state) {
220
+ log.warn(`[evermemos] ingestBatch: no state for session=${sessionKey}`);
221
+ return { ingestedCount: 0 };
222
+ }
223
+
224
+ // Store messages for later processing
225
+ state.pendingMessages.push(...messages);
226
+ log.info(`[evermemos] ingestBatch: collected ${state.pendingMessages.length} messages so far`);
227
+
228
+ return { ingestedCount: messages.length };
229
+ },
230
+
231
+ async afterTurn({ sessionId, sessionKey, messages, prePromptMessageCount }) {
232
+ const state = sessionState.get(sessionKey);
233
+ if (!state) {
234
+ log.warn(`[evermemos] afterTurn: no state for session=${sessionKey}`);
235
+ return;
236
+ }
237
+
238
+ state.turnCount++;
239
+
240
+ // Get new messages (those added after prePromptMessageCount)
241
+ const newMessages = prePromptMessageCount !== undefined
242
+ ? messages.slice(prePromptMessageCount)
243
+ : messages;
244
+
245
+ log.info(`[evermemos] afterTurn: session=${sessionKey}, turn=${state.turnCount}, totalMessages=${messages.length}, newMessages=${newMessages.length}`);
246
+
247
+ if (newMessages.length === 0) {
248
+ log.info(`[evermemos] afterTurn: session=${sessionKey}, turn=${state.turnCount}, no new messages to save`);
249
+ return;
250
+ }
251
+
252
+ try {
253
+ const evermemosMessages = newMessages.map(convertMessage).filter((m) => m.content);
254
+ if (evermemosMessages.length === 0) {
255
+ log.info(`[evermemos] afterTurn: session=${sessionKey}, turn=${state.turnCount}, no valid messages to save`);
256
+ return;
257
+ }
258
+ await saveMemories(cfg, {
259
+ userId: cfg.userId,
260
+ groupId: cfg.groupId,
261
+ messages: evermemosMessages,
262
+ flush: state.pendingFlush || false,
263
+ });
264
+ log.info(`[evermemos] afterTurn: session=${sessionKey}, turn=${state.turnCount}, saved ${evermemosMessages.length} messages`);
265
+
266
+ if (state.pendingFlush) {
267
+ state.pendingFlush = false;
268
+ log.info(`[evermemos] afterTurn: flush flag consumed`);
269
+ }
270
+ } catch (err) {
271
+ log.warn(`[evermemos] afterTurn: save failed: ${err.message}`);
272
+ }
273
+ },
274
+
275
+ async assemble({ sessionId, sessionKey, messages, tokenBudget }) {
276
+ // Initialize state if not exists (assemble can be called before bootstrap)
277
+ if (!sessionState.has(sessionKey)) {
278
+ sessionState.set(sessionKey, {
279
+ turnCount: 0,
280
+ lastAssembleTime: 0,
281
+ pendingFlush: false,
282
+ pendingMessages: [],
283
+ });
284
+ log.info(`[evermemos] assemble: initialized state for ${sessionKey}`);
285
+ }
286
+
287
+ const state = sessionState.get(sessionKey);
288
+
289
+ // Get the last user message as query
290
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
291
+ const query = lastUserMsg ? toText(lastUserMsg) : "";
292
+
293
+ if (!query || query.length < 3) {
294
+ return { messages, estimatedTokens: 0 };
295
+ }
296
+
297
+ // Detect session reset - flush memory but keep current messages
298
+ if (isSessionResetPrompt(query)) {
299
+ log.info(`[evermemos] assemble: session reset detected, keeping current messages`);
300
+ state.pendingFlush = true;
301
+ // Return original messages without memory injection
302
+ // The reset intent is captured in the query itself
303
+ return { messages, estimatedTokens: 0 };
304
+ }
305
+
306
+ try {
307
+ state.lastAssembleTime = Date.now();
308
+
309
+ // Early turns: retrieve more context
310
+ const earlyTurnMultiplier = state.turnCount <= 2 ? 2 : 1;
311
+ const topK = Math.min(cfg.topK * earlyTurnMultiplier, 20);
312
+
313
+ const params = {
314
+ query,
315
+ user_id: cfg.userId,
316
+ group_ids: cfg.groupId ? [cfg.groupId] : undefined,
317
+ memory_types: cfg.memoryTypes,
318
+ retrieve_method: cfg.retrieveMethod,
319
+ top_k,
320
+ };
321
+
322
+ const result = await searchMemories(cfg, params);
323
+ const parsed = parseSearchResponse(result);
324
+
325
+ const memoryCount =
326
+ (parsed.episodic?.length || 0) +
327
+ (parsed.traits?.length || 0) +
328
+ (parsed.case ? 1 : 0) +
329
+ (parsed.skill ? 1 : 0);
330
+
331
+ if (memoryCount === 0) {
332
+ return { messages, estimatedTokens: 0 };
333
+ }
334
+
335
+ // Build memory context as a system message
336
+ const memoryText = buildMemoryPrompt(parsed, { wrapInCodeBlock: true });
337
+ const memoryMessage = {
338
+ role: "system",
339
+ content: `[Relevant Memory]\n${memoryText}`,
340
+ _memory: true,
341
+ };
342
+
343
+ log.info(`[evermemos] assemble: session=${sessionKey}, retrieved ${memoryCount} memories`);
344
+
345
+ // Return memory message + existing messages
346
+ return {
347
+ messages: [memoryMessage, ...messages],
348
+ estimatedTokens: Math.floor((memoryText.length + JSON.stringify(messages).length) / 3),
349
+ };
350
+ } catch (err) {
351
+ log.warn(`[evermemos] assemble: ${err.message}`);
352
+ return { messages, estimatedTokens: 0 };
353
+ }
354
+ },
355
+
356
+ async compact({ sessionId, sessionKey, tokenBudget, currentTokenCount }) {
357
+ const state = sessionState.get(sessionKey);
358
+ if (!state) {
359
+ return { ok: true, compacted: false, reason: "no session state" };
360
+ }
361
+
362
+ log.info(`[evermemos] compact: session=${sessionKey}, tokens=${currentTokenCount}, budget=${tokenBudget}`);
363
+
364
+ // Simple compaction strategy: if over 80% of budget, recommend compaction
365
+ const threshold = tokenBudget ? tokenBudget * 0.8 : 8000;
366
+ if (currentTokenCount && currentTokenCount > threshold) {
367
+ return {
368
+ ok: true,
369
+ compacted: false,
370
+ reason: `token count (${currentTokenCount}) exceeds threshold (${threshold})`,
371
+ };
372
+ }
373
+
374
+ return { ok: true, compacted: false, reason: "within threshold" };
375
+ },
376
+
377
+ async dispose() {
378
+ // Clean up session states
379
+ sessionState.clear();
380
+ },
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Plugin entry point - registers the ContextEngine
386
+ * @param {Object} api - OpenClaw API
387
+ */
388
+ export default function register(api) {
389
+ const log = api.logger || { info: (...a) => console.log(...a), warn: (...a) => console.warn(...a) };
390
+
391
+ log.info(`[evermemos] Registering EverMemOS ContextEngine plugin`);
392
+
393
+ // Register the ContextEngine factory
394
+ api.registerContextEngine(PLUGIN_ID, (pluginConfig) => {
395
+ return createContextEngineInstance(pluginConfig, api.logger);
396
+ });
397
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "id": "@evermind-ai/openclaw-plugin",
3
+ "name": "EverMemOS ContextEngine",
4
+ "description": "Full-lifecycle memory management with EverMemOS - supports bootstrap, assemble, afterTurn, compact, and subagent tracking",
5
+ "version": "1.0.0",
6
+ "kind": "context-engine",
7
+ "contextEngine": true,
8
+ "main": "./index.js",
9
+ "configSchema": {
10
+ "type": "object",
11
+ "additionalProperties": false,
12
+ "properties": {
13
+ "baseUrl": {
14
+ "type": "string",
15
+ "description": "EverMemOS server base URL",
16
+ "default": "http://localhost:1995"
17
+ },
18
+ "userId": {
19
+ "type": "string",
20
+ "description": "Identity used for memory ownership and as message sender",
21
+ "default": "evermemos-user"
22
+ },
23
+ "groupId": {
24
+ "type": "string",
25
+ "description": "Group id for shared memory",
26
+ "default": "evermemos-group"
27
+ },
28
+ "topK": {
29
+ "type": "integer",
30
+ "description": "Maximum number of memory entries to retrieve",
31
+ "default": 5
32
+ },
33
+ "memoryTypes": {
34
+ "type": "array",
35
+ "description": "EverMemOS memory types to search",
36
+ "items": {
37
+ "type": "string",
38
+ "enum": ["episodic_memory", "profile", "agent_skill", "agent_case"]
39
+ },
40
+ "default": ["episodic_memory", "profile", "agent_skill", "agent_case"]
41
+ },
42
+ "retrieveMethod": {
43
+ "type": "string",
44
+ "description": "Retrieval strategy used by EverMemOS",
45
+ "enum": ["keyword", "vector", "hybrid", "rrf", "agentic"],
46
+ "default": "hybrid"
47
+ }
48
+ }
49
+ }
50
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@evermind-ai/openclaw-plugin",
3
+ "version": "1.1.0",
4
+ "description": "EverMemOS ContextEngine integration for OpenClaw 3.8+",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "openclaw.plugin.json",
13
+ "README.md",
14
+ "README.zh.md",
15
+ "src/"
16
+ ],
17
+ "keywords": [
18
+ "openclaw",
19
+ "plugin",
20
+ "context-engine",
21
+ "memory",
22
+ "evermemos",
23
+ "ai",
24
+ "agent"
25
+ ],
26
+ "author": "EverMind",
27
+ "license": "Apache-2.0",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/evermind-ai/evermemos-openclaw-plugin"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ },
35
+ "openclaw": {
36
+ "id": "@evermind-ai/openclaw-plugin",
37
+ "kind": "context-engine",
38
+ "contextEngine": true
39
+ }
40
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Context Assembler Module
3
+ * Handles query-aware context assembly from EverMemOS memories
4
+ */
5
+
6
+ import { searchMemories } from "./memory-api.js";
7
+ import { buildMemoryPrompt, parseSearchResponse } from "./formatter.js";
8
+
9
+ /**
10
+ * @typedef {import("./types.js").EverMemOSConfig} EverMemOSConfig
11
+ * @typedef {import("./types.js").Logger} Logger
12
+ * @typedef {import("./types.js").ParsedMemoryResponse} ParsedMemoryResponse
13
+ */
14
+
15
+ /**
16
+ * Handles query-aware context assembly
17
+ * Retrieves relevant memories based on current query and conversation state
18
+ */
19
+ export class ContextAssembler {
20
+ /**
21
+ * @param {EverMemOSConfig} cfg
22
+ * @param {Logger} logger
23
+ */
24
+ constructor(cfg, logger) {
25
+ this.cfg = cfg;
26
+ this.log = logger;
27
+ }
28
+
29
+ /**
30
+ * Assemble context from memories based on current query and conversation state
31
+ * @param {string} query - Current user query
32
+ * @param {Array} messages - Full conversation history
33
+ * @param {number} turnCount - Current turn number
34
+ * @returns {Promise<{context: string, memoryCount: number}>}
35
+ */
36
+ async assemble(query, messages, turnCount) {
37
+ // Early turns: retrieve more context for grounding
38
+ const earlyTurnMultiplier = turnCount <= 2 ? 2 : 1;
39
+ const topK = Math.min(this.cfg.topK * earlyTurnMultiplier, 20);
40
+
41
+ /** @type {Object} */
42
+ const params = {
43
+ query,
44
+ user_id: this.cfg.userId,
45
+ group_ids: this.cfg.groupId ? [this.cfg.groupId] : undefined,
46
+ memory_types: this.cfg.memoryTypes,
47
+ retrieve_method: this.cfg.retrieveMethod,
48
+ top_k: topK,
49
+ };
50
+
51
+ /** @type {any} */
52
+ const result = await searchMemories(this.cfg, params);
53
+ /** @type {ParsedMemoryResponse} */
54
+ const parsed = parseSearchResponse(result);
55
+
56
+ // Count total memories
57
+ const memoryCount =
58
+ (parsed.episodic?.length || 0) +
59
+ (parsed.traits?.length || 0) +
60
+ (parsed.case ? 1 : 0) +
61
+ (parsed.skill ? 1 : 0);
62
+
63
+ const context = buildMemoryPrompt(parsed, { wrapInCodeBlock: true });
64
+
65
+ return { context, memoryCount };
66
+ }
67
+
68
+ /**
69
+ * Build minimal context for subagents (smaller context window)
70
+ * @param {string} query - Subagent query
71
+ * @returns {Promise<string>}
72
+ */
73
+ async assembleForSubagent(query) {
74
+ if (!query || query.length < 3) return "";
75
+
76
+ const topK = Math.min(this.cfg.topK, 3);
77
+
78
+ /** @type {Object} */
79
+ const params = {
80
+ query,
81
+ user_id: this.cfg.userId,
82
+ group_ids: this.cfg.groupId ? [this.cfg.groupId] : undefined,
83
+ memory_types: this.cfg.memoryTypes,
84
+ retrieve_method: this.cfg.retrieveMethod,
85
+ top_k: topK,
86
+ };
87
+
88
+ /** @type {any} */
89
+ const result = await searchMemories(this.cfg, params);
90
+ /** @type {ParsedMemoryResponse} */
91
+ const parsed = parseSearchResponse(result);
92
+
93
+ // Use no code block for subagents (cleaner format)
94
+ return buildMemoryPrompt(parsed, { wrapInCodeBlock: false });
95
+ }
96
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Compaction Handler Module
3
+ * Handles session compaction evaluation and participation
4
+ */
5
+
6
+ /**
7
+ * @typedef {import("./types.js").EverMemOSConfig} EverMemOSConfig
8
+ * @typedef {import("./types.js").Logger} Logger
9
+ */
10
+
11
+ /**
12
+ * Compaction evaluation parameters
13
+ * @typedef {Object} CompactionEvalParams
14
+ * @property {Array} messages
15
+ * @property {number} tokenCount
16
+ * @property {number} turnCount
17
+ * @property {number} lastAssembleTime
18
+ */
19
+
20
+ /**
21
+ * Compaction decision result
22
+ * @typedef {Object} CompactionDecision
23
+ * @property {boolean} shouldCompact
24
+ * @property {string} reason
25
+ * @property {string} [memoryStrategy]
26
+ */
27
+
28
+ /**
29
+ * Handles session compaction evaluation and participation
30
+ * Evaluates when to compact the conversation context
31
+ */
32
+ export class CompactionHandler {
33
+ /**
34
+ * @param {EverMemOSConfig} cfg
35
+ * @param {Logger} logger
36
+ */
37
+ constructor(cfg, logger) {
38
+ this.cfg = cfg;
39
+ this.log = logger;
40
+
41
+ // Configurable thresholds
42
+ this.compactThresholdTokens = 8000;
43
+ this.compactThresholdTurns = 10;
44
+ }
45
+
46
+ /**
47
+ * Evaluate whether session should be compacted
48
+ * @param {CompactionEvalParams} params
49
+ * @returns {Promise<CompactionDecision>}
50
+ */
51
+ async evaluate({ messages, tokenCount, turnCount, lastAssembleTime }) {
52
+ // Compact if token count exceeds threshold
53
+ if (tokenCount > this.compactThresholdTokens) {
54
+ return {
55
+ shouldCompact: true,
56
+ reason: `token count (${tokenCount}) exceeds threshold (${this.compactThresholdTokens})`,
57
+ memoryStrategy: "consolidate_to_long_term",
58
+ };
59
+ }
60
+
61
+ // Compact if turn count exceeds threshold
62
+ if (turnCount > this.compactThresholdTurns) {
63
+ return {
64
+ shouldCompact: true,
65
+ reason: `turn count (${turnCount}) exceeds threshold (${this.compactThresholdTurns})`,
66
+ memoryStrategy: "consolidate_to_long_term",
67
+ };
68
+ }
69
+
70
+ // Compact if no recent memory assembly (stale session)
71
+ const timeSinceAssemble = Date.now() - lastAssembleTime;
72
+ if (lastAssembleTime > 0 && timeSinceAssemble > 30 * 60 * 1000) { // 30 minutes
73
+ return {
74
+ shouldCompact: true,
75
+ reason: `session inactive for ${Math.round(timeSinceAssemble / 60000)} minutes`,
76
+ memoryStrategy: "consolidate_to_long_term",
77
+ };
78
+ }
79
+
80
+ return {
81
+ shouldCompact: false,
82
+ reason: "token and turn counts within acceptable range",
83
+ };
84
+ }
85
+ }
package/src/config.js ADDED
@@ -0,0 +1,14 @@
1
+ const DEFAULT_URL = "http://localhost:1995";
2
+
3
+ export const TIMEOUT_MS = 60000;
4
+
5
+ export function resolveConfig(pc = {}) {
6
+ return {
7
+ serverUrl: (pc.baseUrl || DEFAULT_URL).replace(/\/*$/, ""),
8
+ userId: pc.userId || "evermemos-user",
9
+ groupId: pc.groupId || "evermemos-group",
10
+ topK: pc.topK ?? 5,
11
+ memoryTypes: pc.memoryTypes ?? ["episodic_memory", "profile", "agent_skill", "agent_case"],
12
+ retrieveMethod: pc.retrieveMethod ?? "hybrid",
13
+ };
14
+ }