@pentatonic-ai/openclaw-memory-plugin 0.4.4

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 ADDED
@@ -0,0 +1,60 @@
1
+ # @pentatonic-ai/openclaw-memory
2
+
3
+ Persistent, searchable memory for OpenClaw. Local (Docker + Ollama) or hosted (Pentatonic TES).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ openclaw plugins install @pentatonic-ai/openclaw-memory
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ Tell OpenClaw:
14
+
15
+ ```
16
+ Set up pentatonic memory
17
+ ```
18
+
19
+ Or configure manually in `openclaw.json`:
20
+
21
+ ```json
22
+ {
23
+ "plugins": {
24
+ "slots": { "contextEngine": "pentatonic-memory" },
25
+ "entries": {
26
+ "pentatonic-memory": {
27
+ "enabled": true,
28
+ "config": {
29
+ "database_url": "postgres://memory:memory@localhost:5433/memory",
30
+ "embedding_url": "http://localhost:11435/v1",
31
+ "embedding_model": "nomic-embed-text",
32
+ "llm_url": "http://localhost:11435/v1",
33
+ "llm_model": "llama3.2:3b"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## What it does
42
+
43
+ Every lifecycle event is handled automatically:
44
+
45
+ - **Ingest** — every message stored with embeddings + HyDE query expansion
46
+ - **Assemble** — relevant memories injected as context before every prompt
47
+ - **Compact** — decay cycle when context window fills
48
+ - **After turn** — high-access memories consolidated to semantic layer
49
+
50
+ Plus tools: `memory_search`, `memory_store`, `memory_layers`
51
+
52
+ ## Local vs Hosted
53
+
54
+ **Local**: Fully private. Requires Docker (Postgres + pgvector + Ollama). Run `npx @pentatonic-ai/ai-agent-sdk memory` to set up.
55
+
56
+ **Hosted**: Connect to Pentatonic TES for higher-dimensional embeddings, team memory, and analytics. Run `npx @pentatonic-ai/ai-agent-sdk init`.
57
+
58
+ ## License
59
+
60
+ MIT
package/index.js ADDED
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Pentatonic Memory — OpenClaw Context Engine Plugin
3
+ *
4
+ * Install: openclaw plugins install @pentatonic-ai/openclaw-memory
5
+ *
6
+ * Provides persistent, searchable memory via the ContextEngine lifecycle:
7
+ * ingest — every message stored with embedding + HyDE
8
+ * assemble — relevant memories injected before every prompt
9
+ * compact — decay cycle on context overflow
10
+ * afterTurn — consolidation check
11
+ *
12
+ * Plus agent-callable tools: memory_search, memory_store, pentatonic_memory_setup
13
+ *
14
+ * Two modes:
15
+ * - Local: HTTP calls to the memory server (localhost:3333)
16
+ * - Hosted: HTTP calls to TES GraphQL API
17
+ *
18
+ * No native modules, no child_process, no filesystem access.
19
+ * All config comes from OpenClaw's plugin config system.
20
+ */
21
+
22
+ // --- Local mode: HTTP to memory server ---
23
+
24
+ async function localSearch(baseUrl, query, limit = 5, minScore = 0.3) {
25
+ try {
26
+ const res = await fetch(`${baseUrl}/search`, {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/json" },
29
+ body: JSON.stringify({ query, limit, min_score: minScore }),
30
+ signal: AbortSignal.timeout(5000),
31
+ });
32
+ if (!res.ok) return [];
33
+ const data = await res.json();
34
+ return data.results || [];
35
+ } catch {
36
+ return [];
37
+ }
38
+ }
39
+
40
+ async function localStore(baseUrl, content, metadata = {}) {
41
+ try {
42
+ const res = await fetch(`${baseUrl}/store`, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify({ content, metadata }),
46
+ signal: AbortSignal.timeout(10000),
47
+ });
48
+ if (!res.ok) return null;
49
+ return res.json();
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ async function localHealth(baseUrl) {
56
+ try {
57
+ const res = await fetch(`${baseUrl}/health`, {
58
+ signal: AbortSignal.timeout(3000),
59
+ });
60
+ return res.ok;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ // --- Hosted mode: TES GraphQL ---
67
+
68
+ function tesHeaders(config) {
69
+ const headers = {
70
+ "Content-Type": "application/json",
71
+ "x-client-id": config.tes_client_id,
72
+ };
73
+ if (config.tes_api_key?.startsWith("tes_")) {
74
+ headers["Authorization"] = `Bearer ${config.tes_api_key}`;
75
+ } else {
76
+ headers["x-service-key"] = config.tes_api_key;
77
+ }
78
+ return headers;
79
+ }
80
+
81
+ async function hostedSearch(config, query, limit = 5, minScore = 0.3) {
82
+ try {
83
+ const res = await fetch(`${config.tes_endpoint}/api/graphql`, {
84
+ method: "POST",
85
+ headers: tesHeaders(config),
86
+ body: JSON.stringify({
87
+ query: `query($clientId: String!, $query: String!, $limit: Int, $minScore: Float) {
88
+ semanticSearchMemories(clientId: $clientId, query: $query, limit: $limit, minScore: $minScore) {
89
+ id content similarity
90
+ }
91
+ }`,
92
+ variables: { clientId: config.tes_client_id, query, limit, minScore },
93
+ }),
94
+ signal: AbortSignal.timeout(5000),
95
+ });
96
+ if (!res.ok) return [];
97
+ const json = await res.json();
98
+ return json.data?.semanticSearchMemories || [];
99
+ } catch {
100
+ return [];
101
+ }
102
+ }
103
+
104
+ async function hostedStore(config, content, metadata = {}) {
105
+ try {
106
+ const res = await fetch(`${config.tes_endpoint}/api/graphql`, {
107
+ method: "POST",
108
+ headers: tesHeaders(config),
109
+ body: JSON.stringify({
110
+ query: `mutation CreateModuleEvent($moduleId: String!, $input: ModuleEventInput!) {
111
+ createModuleEvent(moduleId: $moduleId, input: $input) { success eventId }
112
+ }`,
113
+ variables: {
114
+ moduleId: "deep-memory",
115
+ input: {
116
+ eventType: "STORE_MEMORY",
117
+ data: {
118
+ entity_id: metadata.session_id || "openclaw",
119
+ attributes: { ...metadata, content, source: "openclaw-plugin" },
120
+ },
121
+ },
122
+ },
123
+ }),
124
+ signal: AbortSignal.timeout(10000),
125
+ });
126
+ if (!res.ok) return null;
127
+ return res.json();
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ // --- Context engines ---
134
+
135
+ function createLocalContextEngine(baseUrl, opts = {}) {
136
+ const searchLimit = opts.searchLimit || 5;
137
+ const minScore = opts.minScore || 0.3;
138
+ const log = opts.logger || (() => {});
139
+
140
+ return {
141
+ info: { id: "pentatonic-memory", name: "Pentatonic Memory (Local)", ownsCompaction: false },
142
+
143
+ async ingest({ sessionId, message }) {
144
+ if (!message?.content) return { ingested: false };
145
+ const role = message.role || message.type;
146
+ if (role !== "user" && role !== "assistant") return { ingested: false };
147
+ try {
148
+ await localStore(baseUrl, message.content, { session_id: sessionId, role });
149
+ log(`Ingested ${role} message`);
150
+ return { ingested: true };
151
+ } catch (err) {
152
+ log(`Ingest failed: ${err.message}`);
153
+ return { ingested: false };
154
+ }
155
+ },
156
+
157
+ async assemble({ sessionId, messages }) {
158
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user" || m.type === "user");
159
+ if (!lastUserMsg?.content) return { messages, estimatedTokens: 0 };
160
+ try {
161
+ const results = await localSearch(baseUrl, lastUserMsg.content, searchLimit, minScore);
162
+ if (!results.length) return { messages, estimatedTokens: 0 };
163
+ const memoryText = results
164
+ .map((m) => `- [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`)
165
+ .join("\n");
166
+ const addition = `[Memory] Relevant context from past conversations:\n${memoryText}`;
167
+ log(`Assembled ${results.length} memories`);
168
+ return { messages, estimatedTokens: Math.ceil(addition.length / 4), systemPromptAddition: addition };
169
+ } catch (err) {
170
+ log(`Assemble failed: ${err.message}`);
171
+ return { messages, estimatedTokens: 0 };
172
+ }
173
+ },
174
+
175
+ async compact() { return { ok: true, compacted: false }; },
176
+ async afterTurn() {},
177
+ };
178
+ }
179
+
180
+ function createHostedContextEngine(config, opts = {}) {
181
+ const searchLimit = opts.searchLimit || 5;
182
+ const minScore = opts.minScore || 0.3;
183
+ const log = opts.logger || (() => {});
184
+
185
+ return {
186
+ info: { id: "pentatonic-memory", name: "Pentatonic Memory (Hosted)", ownsCompaction: false },
187
+
188
+ async ingest({ sessionId, message }) {
189
+ if (!message?.content) return { ingested: false };
190
+ const role = message.role || message.type;
191
+ if (role !== "user" && role !== "assistant") return { ingested: false };
192
+ try {
193
+ await hostedStore(config, message.content, { session_id: sessionId, role });
194
+ log(`Ingested ${role} message via TES`);
195
+ return { ingested: true };
196
+ } catch (err) {
197
+ log(`Hosted ingest failed: ${err.message}`);
198
+ return { ingested: false };
199
+ }
200
+ },
201
+
202
+ async assemble({ sessionId, messages }) {
203
+ const lastUserMsg = [...messages].reverse().find((m) => m.role === "user" || m.type === "user");
204
+ if (!lastUserMsg?.content) return { messages, estimatedTokens: 0 };
205
+ try {
206
+ const results = await hostedSearch(config, lastUserMsg.content, searchLimit, minScore);
207
+ if (!results.length) return { messages, estimatedTokens: 0 };
208
+ const memoryText = results
209
+ .map((m) => `- [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`)
210
+ .join("\n");
211
+ const addition = `[Memory] Relevant context from past conversations:\n${memoryText}`;
212
+ log(`Assembled ${results.length} memories via TES`);
213
+ return { messages, estimatedTokens: Math.ceil(addition.length / 4), systemPromptAddition: addition };
214
+ } catch (err) {
215
+ log(`Hosted assemble failed: ${err.message}`);
216
+ return { messages, estimatedTokens: 0 };
217
+ }
218
+ },
219
+
220
+ async compact() { return { ok: true, compacted: false }; },
221
+ async afterTurn() {},
222
+ };
223
+ }
224
+
225
+ // --- Format helpers ---
226
+
227
+ function formatResults(results) {
228
+ if (!results.length) return "No relevant memories found.";
229
+ return results
230
+ .map((m, i) => `${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`)
231
+ .join("\n\n");
232
+ }
233
+
234
+ // --- Plugin entry ---
235
+
236
+ export default {
237
+ id: "pentatonic-memory",
238
+ name: "Pentatonic Memory",
239
+ description: "Persistent, searchable memory with multi-signal retrieval and HyDE query expansion",
240
+ kind: "context-engine",
241
+
242
+ register(api) {
243
+ const config = api.config || {};
244
+ const hosted = !!(config.tes_endpoint && config.tes_api_key);
245
+ const baseUrl = config.memory_url || "http://localhost:3333";
246
+ const log = (msg) => process.stderr.write(`[pentatonic-memory] ${msg}\n`);
247
+
248
+ // --- Setup tool (always registered) ---
249
+
250
+ api.registerTool({
251
+ name: "pentatonic_memory_setup",
252
+ description: `Guide the user through setting up Pentatonic Memory.
253
+
254
+ Two modes:
255
+ 1. "local" — fully private, Docker-based (PostgreSQL + pgvector + Ollama). No cloud.
256
+ 2. "hosted" — Pentatonic TES cloud. Team-wide shared memory, analytics, higher-dimensional embeddings.
257
+
258
+ Call this to get instructions for the user's chosen mode.`,
259
+ parameters: {
260
+ type: "object",
261
+ properties: {
262
+ mode: { type: "string", enum: ["local", "hosted"], description: "Which mode the user wants" },
263
+ },
264
+ required: ["mode"],
265
+ },
266
+ async execute({ mode }) {
267
+ if (mode === "local") {
268
+ return `## Local Memory Setup
269
+
270
+ Run in terminal:
271
+ \`\`\`
272
+ npx @pentatonic-ai/ai-agent-sdk memory
273
+ \`\`\`
274
+
275
+ This starts PostgreSQL + pgvector, Ollama, and the memory server via Docker.
276
+
277
+ Then add to openclaw.json:
278
+ \`\`\`json
279
+ {
280
+ "plugins": {
281
+ "slots": { "contextEngine": "pentatonic-memory" },
282
+ "entries": {
283
+ "pentatonic-memory": {
284
+ "enabled": true,
285
+ "config": { "memory_url": "http://localhost:3333" }
286
+ }
287
+ }
288
+ }
289
+ }
290
+ \`\`\`
291
+
292
+ Restart OpenClaw to activate.`;
293
+ }
294
+ return `## Hosted TES Setup
295
+
296
+ Run in terminal:
297
+ \`\`\`
298
+ npx @pentatonic-ai/ai-agent-sdk init
299
+ \`\`\`
300
+
301
+ This creates a TES account and generates API credentials.
302
+
303
+ Then add to openclaw.json:
304
+ \`\`\`json
305
+ {
306
+ "plugins": {
307
+ "slots": { "contextEngine": "pentatonic-memory" },
308
+ "entries": {
309
+ "pentatonic-memory": {
310
+ "enabled": true,
311
+ "config": {
312
+ "tes_endpoint": "https://your-company.api.pentatonic.com",
313
+ "tes_client_id": "your-company",
314
+ "tes_api_key": "tes_your-company_xxxxx"
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+ \`\`\`
321
+
322
+ Restart OpenClaw to activate.`;
323
+ },
324
+ });
325
+
326
+ // --- Mode-specific registration ---
327
+
328
+ if (hosted) {
329
+ log("Hosted mode — routing through TES");
330
+
331
+ api.registerContextEngine("pentatonic-memory", () =>
332
+ createHostedContextEngine(config, {
333
+ searchLimit: config.search_limit || 5,
334
+ minScore: config.min_score || 0.3,
335
+ logger: log,
336
+ })
337
+ );
338
+
339
+ api.registerTool({
340
+ name: "memory_search",
341
+ description: "Search memories for relevant context.",
342
+ parameters: {
343
+ type: "object",
344
+ properties: {
345
+ query: { type: "string", description: "What to search for" },
346
+ limit: { type: "number", description: "Max results (default 5)" },
347
+ },
348
+ required: ["query"],
349
+ },
350
+ async execute({ query, limit }) {
351
+ return formatResults(await hostedSearch(config, query, limit || 5, 0.3));
352
+ },
353
+ });
354
+
355
+ api.registerTool({
356
+ name: "memory_store",
357
+ description: "Explicitly store something important.",
358
+ parameters: {
359
+ type: "object",
360
+ properties: { content: { type: "string", description: "What to remember" } },
361
+ required: ["content"],
362
+ },
363
+ async execute({ content }) {
364
+ const result = await hostedStore(config, content, { source: "openclaw-tool" });
365
+ return result ? "Memory stored." : "Failed to store memory.";
366
+ },
367
+ });
368
+ } else {
369
+ // Local mode — HTTP to memory server
370
+ const isConfigured = config.memory_url || config.database_url;
371
+
372
+ if (isConfigured) {
373
+ log(`Local mode — ${baseUrl}`);
374
+
375
+ // Check if server is reachable on startup
376
+ localHealth(baseUrl).then((ok) => {
377
+ if (!ok) log(`Warning: memory server not reachable at ${baseUrl}. Is Docker running?`);
378
+ });
379
+
380
+ api.registerContextEngine("pentatonic-memory", () =>
381
+ createLocalContextEngine(baseUrl, {
382
+ searchLimit: config.search_limit || 5,
383
+ minScore: config.min_score || 0.3,
384
+ logger: log,
385
+ })
386
+ );
387
+
388
+ api.registerTool({
389
+ name: "memory_search",
390
+ description: "Search memories for relevant context.",
391
+ parameters: {
392
+ type: "object",
393
+ properties: {
394
+ query: { type: "string", description: "What to search for" },
395
+ limit: { type: "number", description: "Max results (default 5)" },
396
+ },
397
+ required: ["query"],
398
+ },
399
+ async execute({ query, limit }) {
400
+ return formatResults(await localSearch(baseUrl, query, limit || 5, 0.3));
401
+ },
402
+ });
403
+
404
+ api.registerTool({
405
+ name: "memory_store",
406
+ description: "Explicitly store something important.",
407
+ parameters: {
408
+ type: "object",
409
+ properties: { content: { type: "string", description: "What to remember" } },
410
+ required: ["content"],
411
+ },
412
+ async execute({ content }) {
413
+ const result = await localStore(baseUrl, content, { source: "openclaw-tool" });
414
+ return result ? `Stored: ${result.id}` : "Failed to store memory.";
415
+ },
416
+ });
417
+ } else {
418
+ log("No config — setup tool available. Tell OpenClaw: 'set up pentatonic memory'");
419
+ }
420
+ }
421
+
422
+ log(`Plugin registered (${hosted ? "hosted" : isConfigured ? "local" : "unconfigured"})`);
423
+ },
424
+ };
@@ -0,0 +1,69 @@
1
+ {
2
+ "id": "pentatonic-memory",
3
+ "name": "Pentatonic Memory",
4
+ "description": "Persistent, searchable memory with multi-signal retrieval and HyDE query expansion. Local (Docker + Ollama) or hosted (Pentatonic TES).",
5
+ "version": "0.4.4",
6
+ "kind": "context-engine",
7
+ "configSchema": {
8
+ "type": "object",
9
+ "additionalProperties": false,
10
+ "properties": {
11
+ "mode": {
12
+ "type": "string",
13
+ "enum": ["local", "hosted"],
14
+ "default": "local",
15
+ "description": "Local (Docker + Ollama) or hosted (Pentatonic TES)"
16
+ },
17
+ "database_url": {
18
+ "type": "string",
19
+ "description": "PostgreSQL connection string (local mode)"
20
+ },
21
+ "embedding_url": {
22
+ "type": "string",
23
+ "description": "OpenAI-compatible embeddings endpoint (local mode)"
24
+ },
25
+ "embedding_model": {
26
+ "type": "string",
27
+ "default": "nomic-embed-text"
28
+ },
29
+ "llm_url": {
30
+ "type": "string",
31
+ "description": "OpenAI-compatible chat endpoint for HyDE (local mode)"
32
+ },
33
+ "llm_model": {
34
+ "type": "string",
35
+ "default": "llama3.2:3b"
36
+ },
37
+ "tes_endpoint": {
38
+ "type": "string",
39
+ "description": "TES API endpoint (hosted mode)"
40
+ },
41
+ "tes_client_id": {
42
+ "type": "string",
43
+ "description": "TES client ID (hosted mode)"
44
+ },
45
+ "tes_api_key": {
46
+ "type": "string",
47
+ "description": "TES API key (hosted mode)"
48
+ },
49
+ "client_id": {
50
+ "type": "string",
51
+ "default": "default",
52
+ "description": "Memory namespace"
53
+ },
54
+ "search_limit": {
55
+ "type": "number",
56
+ "default": 5,
57
+ "description": "Memories to inject per prompt"
58
+ },
59
+ "min_score": {
60
+ "type": "number",
61
+ "default": 0.3,
62
+ "description": "Minimum relevance threshold"
63
+ }
64
+ }
65
+ },
66
+ "setup": {
67
+ "description": "Set up persistent memory — local (Docker + Ollama, fully private) or hosted (Pentatonic TES, team-wide)."
68
+ }
69
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@pentatonic-ai/openclaw-memory-plugin",
3
+ "version": "0.4.4",
4
+ "description": "Pentatonic Memory plugin for OpenClaw — persistent, searchable memory with multi-signal retrieval and HyDE query expansion",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "openclaw": {
8
+ "extensions": [
9
+ {
10
+ "id": "pentatonic-memory",
11
+ "entrypoint": "./index.js"
12
+ }
13
+ ],
14
+ "install": {
15
+ "npmSpec": "@pentatonic-ai/openclaw-memory",
16
+ "minHostVersion": ">=2026.3.7"
17
+ }
18
+ },
19
+ "dependencies": {},
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/Pentatonic-Ltd/ai-agent-sdk.git",
24
+ "directory": "packages/memory/openclaw-plugin"
25
+ },
26
+ "keywords": ["openclaw", "plugin", "memory", "context-engine", "pentatonic", "tes"]
27
+ }