@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.
@@ -0,0 +1,163 @@
1
+ import { CONTEXT_BOUNDARY } from "./formatter.js";
2
+
3
+ const MAX_CHARS = 20000;
4
+
5
+ /* ------------------------------------------------------------------ */
6
+ /* Helpers */
7
+ /* ------------------------------------------------------------------ */
8
+
9
+ export function toText(content) {
10
+ if (!content) return "";
11
+ if (typeof content === "string") return content;
12
+ if (!Array.isArray(content)) return "";
13
+ return content.reduce((out, block) => {
14
+ if (block?.type !== "text" || !block.text) return out;
15
+ return out ? `${out} ${block.text}` : block.text;
16
+ }, "");
17
+ }
18
+
19
+ function stripContext(text) {
20
+ if (!text) return text;
21
+ const cut = text.lastIndexOf(CONTEXT_BOUNDARY);
22
+ return cut < 0 ? text : text.slice(cut + CONTEXT_BOUNDARY.length).replace(/^\s+/, "");
23
+ }
24
+
25
+ function cap(s) {
26
+ return s && s.length > MAX_CHARS ? `${s.slice(0, MAX_CHARS)}…` : (s || "");
27
+ }
28
+
29
+ /* ------------------------------------------------------------------ */
30
+ /* OpenClaw unified format helpers */
31
+ /* */
32
+ /* Tool call: assistant content block type:"toolCall" */
33
+ /* Tool result: standalone message role:"toolResult" */
34
+ /* ------------------------------------------------------------------ */
35
+
36
+ /** Convert an OpenClaw toolCall block to EverMemOS tool_calls item */
37
+ function toToolCallItem(block) {
38
+ const name = block.name || "unknown";
39
+ const args = block.arguments;
40
+ let argsStr = "";
41
+ if (args != null) {
42
+ try {
43
+ argsStr = typeof args === "string" ? args : JSON.stringify(args);
44
+ } catch {
45
+ argsStr = String(args);
46
+ }
47
+ }
48
+ return {
49
+ id: block.id || undefined,
50
+ type: "function",
51
+ function: { name, arguments: argsStr },
52
+ };
53
+ }
54
+
55
+ function toolResultContent(msg) {
56
+ const c = msg.content;
57
+ if (!c) return "";
58
+ if (typeof c === "string") return c;
59
+ if (Array.isArray(c)) {
60
+ return c.reduce((out, b) => {
61
+ if (b?.type === "text" && b.text) return out ? `${out}\n${b.text}` : b.text;
62
+ return out;
63
+ }, "");
64
+ }
65
+ return "";
66
+ }
67
+
68
+ /* ------------------------------------------------------------------ */
69
+ /* Entry conversion */
70
+ /* ------------------------------------------------------------------ */
71
+
72
+ function toEntries(msg) {
73
+ if (!msg?.role) return [];
74
+
75
+ /* --- toolResult message --- */
76
+ if (msg.role === "toolResult") {
77
+ const text = cap(toolResultContent(msg) || toText(msg.content));
78
+ if (!text) return [];
79
+ return [{
80
+ role: "tool",
81
+ tool_call_id: msg.toolCallId || undefined,
82
+ content: text,
83
+ }];
84
+ }
85
+
86
+ /* --- user message --- */
87
+ if (msg.role === "user") {
88
+ const text = cap(stripContext(toText(msg.content)));
89
+ return text ? [{ role: "user", content: text }] : [];
90
+ }
91
+
92
+ /* --- assistant message --- */
93
+ if (msg.role === "assistant") {
94
+ const text = cap(toText(msg.content));
95
+ const toolCalls = Array.isArray(msg.content)
96
+ ? msg.content.filter((b) => b?.type === "toolCall").map(toToolCallItem)
97
+ : [];
98
+ if (!text && !toolCalls.length) return [];
99
+ const effectiveText = text || toolCalls.map((tc) => `[tool_call: ${tc.function.name}]`).join(", ");
100
+ const entry = { role: "assistant", content: effectiveText };
101
+ if (toolCalls.length) entry.tool_calls = toolCalls;
102
+ return [entry];
103
+ }
104
+
105
+ return [];
106
+ }
107
+
108
+ /* ------------------------------------------------------------------ */
109
+ /* Public API */
110
+ /* ------------------------------------------------------------------ */
111
+
112
+ /**
113
+ * Collect the last turn (final user message and everything after it).
114
+ * Skips over toolResult messages when searching for the real user message.
115
+ */
116
+ export function collectMessages(messages) {
117
+ let pivot = -1;
118
+ for (let i = messages.length - 1; i >= 0; i--) {
119
+ if (messages[i]?.role === "user") { pivot = i; break; }
120
+ }
121
+ if (pivot < 0) return [];
122
+
123
+ return messages.slice(pivot).flatMap((m) => toEntries(m));
124
+ }
125
+
126
+ /* ------------------------------------------------------------------ */
127
+ /* Session reset prompt detection */
128
+ /* ------------------------------------------------------------------ */
129
+
130
+ export const BARE_SESSION_RESET_PROMPT =
131
+ "A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. If the runtime model differs from default_model in the system prompt, mention the default model. Do not mention internal steps, files, tools, or reasoning.";
132
+
133
+ /** Levenshtein edit distance (space-optimised O(m*n) time, O(n) space) */
134
+ function levenshtein(a, b) {
135
+ const m = a.length;
136
+ const n = b.length;
137
+ const prev = Array.from({ length: n + 1 }, (_, i) => i);
138
+ const curr = new Array(n + 1);
139
+ for (let i = 1; i <= m; i++) {
140
+ curr[0] = i;
141
+ for (let j = 1; j <= n; j++) {
142
+ curr[j] = a[i - 1] === b[j - 1]
143
+ ? prev[j - 1]
144
+ : 1 + Math.min(prev[j - 1], prev[j], curr[j - 1]);
145
+ }
146
+ prev.splice(0, n + 1, ...curr);
147
+ }
148
+ return prev[n];
149
+ }
150
+
151
+ /**
152
+ * Returns true when the query is within 20% length of BARE_SESSION_RESET_PROMPT
153
+ * AND the edit-distance ratio is below 0.20 (i.e. ≥80% similar).
154
+ */
155
+ export function isSessionResetPrompt(query) {
156
+ if (!query) return false;
157
+ const promptLen = BARE_SESSION_RESET_PROMPT.length;
158
+ const queryLen = query.length;
159
+ // Fast path: length must be within ±20% of the prompt
160
+ if (Math.abs(queryLen - promptLen) / promptLen > 0.20) return false;
161
+ const dist = levenshtein(query, BARE_SESSION_RESET_PROMPT);
162
+ return dist / Math.max(queryLen, promptLen) < 0.20;
163
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Subagent Tracker Module
3
+ * Tracks subagent lifecycles for hierarchical memory management
4
+ */
5
+
6
+ /**
7
+ * @typedef {import("./types.js").EverMemOSConfig} EverMemOSConfig
8
+ * @typedef {import("./types.js").Logger} Logger
9
+ * @typedef {import("./types.js").SubagentInfo} SubagentInfo
10
+ */
11
+
12
+ /**
13
+ * Handles subagent lifecycle tracking
14
+ * Enables hierarchical memory management across parent and child agent conversations
15
+ */
16
+ export class SubagentTracker {
17
+ /**
18
+ * @param {EverMemOSConfig} cfg
19
+ * @param {Logger} logger
20
+ */
21
+ constructor(cfg, logger) {
22
+ this.cfg = cfg;
23
+ this.log = logger;
24
+
25
+ /** @type {Map<string, SubagentInfo>} */
26
+ this.activeSubagents = new Map();
27
+ }
28
+
29
+ /**
30
+ * Register a new subagent
31
+ * @param {string} subagentId
32
+ * @param {Object} metadata
33
+ * @param {string} [metadata.subagentType]
34
+ * @param {number} [metadata.parentTurnCount]
35
+ */
36
+ register(subagentId, metadata = {}) {
37
+ this.activeSubagents.set(subagentId, {
38
+ startTime: Date.now(),
39
+ ...metadata,
40
+ });
41
+ this.log(`[evermemos] subagent tracker: registered ${subagentId}`);
42
+ }
43
+
44
+ /**
45
+ * Unregister a subagent
46
+ * @param {string} subagentId
47
+ * @returns {boolean} - True if subagent was found and removed
48
+ */
49
+ unregister(subagentId) {
50
+ const removed = this.activeSubagents.delete(subagentId);
51
+ if (removed) {
52
+ this.log(`[evermemos] subagent tracker: unregistered ${subagentId}`);
53
+ }
54
+ return removed;
55
+ }
56
+
57
+ /**
58
+ * Get active subagent count
59
+ * @returns {number}
60
+ */
61
+ getActiveCount() {
62
+ return this.activeSubagents.size;
63
+ }
64
+
65
+ /**
66
+ * Check if a subagent is active
67
+ * @param {string} subagentId
68
+ * @returns {boolean}
69
+ */
70
+ isActive(subagentId) {
71
+ return this.activeSubagents.has(subagentId);
72
+ }
73
+
74
+ /**
75
+ * Get info for a specific subagent
76
+ * @param {string} subagentId
77
+ * @returns {SubagentInfo|undefined}
78
+ */
79
+ getInfo(subagentId) {
80
+ return this.activeSubagents.get(subagentId);
81
+ }
82
+
83
+ /**
84
+ * Get all active subagent IDs
85
+ * @returns {string[]}
86
+ */
87
+ getActiveIds() {
88
+ return Array.from(this.activeSubagents.keys());
89
+ }
90
+
91
+ /**
92
+ * Clean up stale subagents (older than specified duration)
93
+ * @param {number} maxAgeMs - Maximum age in milliseconds (default: 1 hour)
94
+ * @returns {string[]} - List of cleaned up subagent IDs
95
+ */
96
+ cleanup(maxAgeMs = 60 * 60 * 1000) {
97
+ const now = Date.now();
98
+ const stale = [];
99
+
100
+ for (const [id, info] of this.activeSubagents.entries()) {
101
+ if (now - info.startTime > maxAgeMs) {
102
+ stale.push(id);
103
+ }
104
+ }
105
+
106
+ for (const id of stale) {
107
+ this.unregister(id);
108
+ }
109
+
110
+ if (stale.length > 0) {
111
+ this.log(`[evermemos] subagent tracker: cleaned up ${stale.length} stale subagents`);
112
+ }
113
+
114
+ return stale;
115
+ }
116
+ }
package/src/types.js ADDED
@@ -0,0 +1,107 @@
1
+ /**
2
+ * JSDoc type definitions for EverMemOS ContextEngine
3
+ * This file contains type definitions used across all ContextEngine modules
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} EverMemOSConfig
8
+ * @property {string} serverUrl - EverMemOS server URL (e.g., "http://localhost:1995")
9
+ * @property {string} userId - User ID for memory storage
10
+ * @property {string} groupId - Group ID for shared memory
11
+ * @property {number} topK - Number of memories to retrieve
12
+ * @property {string[]} memoryTypes - Memory types to retrieve (episodic_memory, profile, agent_skill, agent_case)
13
+ * @property {string} retrieveMethod - Retrieval strategy (keyword, vector, hybrid, agentic)
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} Logger
18
+ * @property {(...args: any[]) => void} log - Info level logging
19
+ * @property {(...args: any[]) => void} warn - Warning level logging
20
+ * @property {(...args: any[]) => void} error - Error level logging
21
+ */
22
+
23
+ /**
24
+ * @typedef {Object} BootstrapContext
25
+ * @property {Object} api - OpenClaw API object
26
+ * @property {EverMemOSConfig} pluginConfig - Plugin configuration
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} AssembleContext
31
+ * @property {string|Array} prompt - Current user prompt (can be string or content blocks)
32
+ * @property {Array} messages - Full conversation history
33
+ * @property {string} [sessionId] - Optional session identifier
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} AssembleResult
38
+ * @property {string} context - Formatted context string to prepend
39
+ * @property {Object} metadata - Metadata about the assembly
40
+ * @property {number} [metadata.memoryCount] - Number of memories retrieved
41
+ * @property {string} [metadata.retrieveMethod] - Method used for retrieval
42
+ * @property {number} [metadata.turnCount] - Current turn number
43
+ * @property {string} [metadata.skipped] - Reason if assembly was skipped
44
+ * @property {string} [metadata.error] - Error message if failed
45
+ */
46
+
47
+ /**
48
+ * @typedef {Object} AfterTurnContext
49
+ * @property {Array} messages - Messages from the completed turn
50
+ * @property {boolean} success - Whether the turn completed successfully
51
+ * @property {string} [errorMessage] - Error message if turn failed
52
+ */
53
+
54
+ /**
55
+ * @typedef {Object} CompactContext
56
+ * @property {Array} messages - Current session messages
57
+ * @property {number} tokenCount - Estimated token count of context
58
+ * @property {string} [sessionId] - Optional session identifier
59
+ */
60
+
61
+ /**
62
+ * @typedef {Object} CompactResult
63
+ * @property {boolean} shouldCompact - Whether compaction is recommended
64
+ * @property {string} reason - Explanation of the decision
65
+ * @property {Object} [metadata] - Additional metadata
66
+ * @property {string} [metadata.memoryStrategy] - Suggested memory consolidation strategy
67
+ * @property {number} [metadata.turnCount] - Turn count at evaluation time
68
+ */
69
+
70
+ /**
71
+ * @typedef {Object} PrepareSubagentContext
72
+ * @property {string} subagentId - Unique identifier for the subagent
73
+ * @property {string|Array} prompt - Prompt for the subagent
74
+ * @property {Array} parentMessages - Parent agent's conversation history
75
+ * @property {string} [subagentType] - Type of subagent being spawned
76
+ */
77
+
78
+ /**
79
+ * @typedef {Object} PrepareSubagentResult
80
+ * @property {string} prependContext - Context to prepend to subagent prompt
81
+ * @property {Object} metadata - Metadata about the preparation
82
+ * @property {string} [metadata.subagentId] - Subagent identifier
83
+ * @property {number} [metadata.parentTurnCount] - Parent's turn count
84
+ */
85
+
86
+ /**
87
+ * @typedef {Object} SubagentEndedContext
88
+ * @property {string} subagentId - Unique identifier for the subagent
89
+ * @property {Array} messages - Messages from the subagent conversation
90
+ * @property {boolean} success - Whether the subagent completed successfully
91
+ * @property {string} [errorMessage] - Error message if subagent failed
92
+ */
93
+
94
+ /**
95
+ * @typedef {Object} ParsedMemoryResponse
96
+ * @property {Array<{text: string, timestamp: number|string|null}>} episodic - Episodic memories
97
+ * @property {Array<{text: string, kind: string}>} traits - Profile traits
98
+ * @property {Object|null} case - Top agent case memory
99
+ * @property {Object|null} skill - Top agent skill memory
100
+ */
101
+
102
+ /**
103
+ * @typedef {Object} SubagentInfo
104
+ * @property {number} startTime - Subagent spawn timestamp
105
+ * @property {string} [subagentType] - Type of subagent
106
+ * @property {number} [parentTurnCount] - Parent's turn count at spawn
107
+ */