@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/README.md +400 -0
- package/README.zh.md +400 -0
- package/index.js +397 -0
- package/openclaw.plugin.json +50 -0
- package/package.json +40 -0
- package/src/assembler.js +96 -0
- package/src/compaction.js +85 -0
- package/src/config.js +14 -0
- package/src/context-engine.js +283 -0
- package/src/formatter.js +152 -0
- package/src/http-client.js +46 -0
- package/src/lifecycle.js +65 -0
- package/src/memory-api.js +77 -0
- package/src/message-utils.js +163 -0
- package/src/subagent.js +116 -0
- package/src/types.js +107 -0
|
@@ -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
|
+
}
|
package/src/subagent.js
ADDED
|
@@ -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
|
+
*/
|