@pentatonic-ai/ai-agent-sdk 0.5.5 → 0.5.7
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/package.json +7 -4
- package/packages/memory/openclaw-plugin/__tests__/sanitizer.test.js +135 -0
- package/packages/memory/openclaw-plugin/index.js +56 -2
- package/packages/memory/openclaw-plugin/openclaw.plugin.json +1 -1
- package/packages/memory/openclaw-plugin/package.json +2 -2
- package/packages/memory/src/__tests__/api-contract.test.js +56 -0
- package/packages/memory/src/__tests__/hosted.test.js +253 -0
- package/packages/memory/src/__tests__/sanitize.test.js +103 -0
- package/packages/memory/src/ai.js +52 -25
- package/packages/memory/src/distill.js +29 -4
- package/packages/memory/src/hosted.js +372 -0
- package/packages/memory/src/openclaw/index.js +37 -136
- package/packages/memory/src/sanitize.js +61 -0
- package/packages/memory/src/server.js +1 -1
|
@@ -38,6 +38,40 @@
|
|
|
38
38
|
import pg from "pg";
|
|
39
39
|
import { createMemorySystem } from "../index.js";
|
|
40
40
|
import { createContextEngine } from "./context-engine.js";
|
|
41
|
+
import { sanitizeMemoryContent } from "../sanitize.js";
|
|
42
|
+
import {
|
|
43
|
+
hostedSearch as _hostedSearch,
|
|
44
|
+
hostedEmitChatTurn as _hostedEmitChatTurn,
|
|
45
|
+
hostedStoreMemory as _hostedStoreMemory,
|
|
46
|
+
} from "../hosted.js";
|
|
47
|
+
|
|
48
|
+
// --- Hosted-mode adapters ---
|
|
49
|
+
//
|
|
50
|
+
// The OpenClaw plugin predates the public hosted-helper API (`packages/
|
|
51
|
+
// memory/src/hosted.js`). The wrappers below adapt the plugin's existing
|
|
52
|
+
// call shape to the public API so other consumers (the LLM proxy worker,
|
|
53
|
+
// custom integrations) hit the same code path. Adapters are tiny — they
|
|
54
|
+
// translate args and unwrap the result envelope. New code should import
|
|
55
|
+
// from `@pentatonic-ai/ai-agent-sdk/memory/hosted` directly.
|
|
56
|
+
|
|
57
|
+
async function hostedSearch(config, query, limit = 5, minScore = 0.3) {
|
|
58
|
+
const { memories } = await _hostedSearch(config, query, { limit, minScore });
|
|
59
|
+
return memories;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function hostedEmitChatTurn(config, sessionId, turn) {
|
|
63
|
+
return _hostedEmitChatTurn(
|
|
64
|
+
config,
|
|
65
|
+
{ ...turn, sessionId },
|
|
66
|
+
{ source: "openclaw-plugin" }
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function hostedStore(config, content, metadata = {}) {
|
|
71
|
+
return _hostedStoreMemory(config, content, metadata, {
|
|
72
|
+
source: metadata.source || "openclaw-plugin",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
41
75
|
|
|
42
76
|
const { Pool } = pg;
|
|
43
77
|
|
|
@@ -74,139 +108,6 @@ function getLocalMemory(config) {
|
|
|
74
108
|
return memory;
|
|
75
109
|
}
|
|
76
110
|
|
|
77
|
-
// --- Hosted mode helpers ---
|
|
78
|
-
|
|
79
|
-
function tesHeaders(config) {
|
|
80
|
-
const headers = {
|
|
81
|
-
"Content-Type": "application/json",
|
|
82
|
-
"x-client-id": config.tes_client_id,
|
|
83
|
-
};
|
|
84
|
-
if (config.tes_api_key?.startsWith("tes_")) {
|
|
85
|
-
headers["Authorization"] = `Bearer ${config.tes_api_key}`;
|
|
86
|
-
} else {
|
|
87
|
-
headers["x-service-key"] = config.tes_api_key;
|
|
88
|
-
}
|
|
89
|
-
return headers;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function hostedSearch(config, query, limit = 5, minScore = 0.3) {
|
|
93
|
-
try {
|
|
94
|
-
const response = await fetch(`${config.tes_endpoint}/api/graphql`, {
|
|
95
|
-
method: "POST",
|
|
96
|
-
headers: tesHeaders(config),
|
|
97
|
-
body: JSON.stringify({
|
|
98
|
-
query: `query($clientId: String!, $query: String!, $limit: Int, $minScore: Float) {
|
|
99
|
-
semanticSearchMemories(clientId: $clientId, query: $query, limit: $limit, minScore: $minScore) {
|
|
100
|
-
id content similarity
|
|
101
|
-
}
|
|
102
|
-
}`,
|
|
103
|
-
variables: {
|
|
104
|
-
clientId: config.tes_client_id,
|
|
105
|
-
query,
|
|
106
|
-
limit,
|
|
107
|
-
minScore,
|
|
108
|
-
},
|
|
109
|
-
}),
|
|
110
|
-
signal: AbortSignal.timeout(5000),
|
|
111
|
-
});
|
|
112
|
-
if (!response.ok) return [];
|
|
113
|
-
const json = await response.json();
|
|
114
|
-
return json.data?.semanticSearchMemories || [];
|
|
115
|
-
} catch {
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Emit a CHAT_TURN event to TES so the conversation-analytics dashboard
|
|
122
|
-
* (Token Universe + Tools tabs) can render. Without this, the dashboard
|
|
123
|
-
* filters on eventType=CHAT_TURN and shows nothing for OpenClaw users
|
|
124
|
-
* because the only events emitted are STORE_MEMORY.
|
|
125
|
-
*
|
|
126
|
-
* Anything missing from the message metadata is omitted rather than
|
|
127
|
-
* defaulted to zero — that way the dashboard can distinguish "no data"
|
|
128
|
-
* from "zero usage".
|
|
129
|
-
*/
|
|
130
|
-
async function hostedEmitChatTurn(config, sessionId, turn) {
|
|
131
|
-
const attributes = {
|
|
132
|
-
source: "openclaw-plugin",
|
|
133
|
-
user_message: turn.userMessage,
|
|
134
|
-
assistant_response: turn.assistantResponse,
|
|
135
|
-
};
|
|
136
|
-
if (turn.model) attributes.model = turn.model;
|
|
137
|
-
if (turn.usage) attributes.usage = turn.usage;
|
|
138
|
-
if (turn.toolCalls?.length) attributes.tool_calls = turn.toolCalls;
|
|
139
|
-
if (turn.turnNumber !== undefined) attributes.turn_number = turn.turnNumber;
|
|
140
|
-
if (turn.systemPrompt) attributes.system_prompt = turn.systemPrompt;
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const response = await fetch(`${config.tes_endpoint}/api/graphql`, {
|
|
144
|
-
method: "POST",
|
|
145
|
-
headers: tesHeaders(config),
|
|
146
|
-
// Route through createModuleEvent on the conversation-analytics
|
|
147
|
-
// module rather than the top-level emitEvent. The latter requires
|
|
148
|
-
// a permission most client API keys don't have ("Access denied:
|
|
149
|
-
// You don't have permission to update emitEvent"), but the
|
|
150
|
-
// module's manifest declares CHAT_TURN as a registered event
|
|
151
|
-
// type, so the module-scoped path is both authorised and
|
|
152
|
-
// consistent with how STORE_MEMORY is emitted.
|
|
153
|
-
body: JSON.stringify({
|
|
154
|
-
query: `mutation Cme($moduleId: String!, $input: ModuleEventInput!) {
|
|
155
|
-
createModuleEvent(moduleId: $moduleId, input: $input) { success eventId }
|
|
156
|
-
}`,
|
|
157
|
-
variables: {
|
|
158
|
-
moduleId: "conversation-analytics",
|
|
159
|
-
input: {
|
|
160
|
-
eventType: "CHAT_TURN",
|
|
161
|
-
data: {
|
|
162
|
-
entity_id: sessionId,
|
|
163
|
-
attributes,
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
}),
|
|
168
|
-
signal: AbortSignal.timeout(10000),
|
|
169
|
-
});
|
|
170
|
-
if (!response.ok) return null;
|
|
171
|
-
return response.json();
|
|
172
|
-
} catch {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function hostedStore(config, content, metadata = {}) {
|
|
178
|
-
try {
|
|
179
|
-
const response = await fetch(`${config.tes_endpoint}/api/graphql`, {
|
|
180
|
-
method: "POST",
|
|
181
|
-
headers: tesHeaders(config),
|
|
182
|
-
body: JSON.stringify({
|
|
183
|
-
query: `mutation CreateModuleEvent($moduleId: String!, $input: ModuleEventInput!) {
|
|
184
|
-
createModuleEvent(moduleId: $moduleId, input: $input) { success eventId }
|
|
185
|
-
}`,
|
|
186
|
-
variables: {
|
|
187
|
-
moduleId: "deep-memory",
|
|
188
|
-
input: {
|
|
189
|
-
eventType: "STORE_MEMORY",
|
|
190
|
-
data: {
|
|
191
|
-
entity_id: metadata.session_id || "openclaw",
|
|
192
|
-
attributes: {
|
|
193
|
-
...metadata,
|
|
194
|
-
content,
|
|
195
|
-
source: "openclaw-plugin",
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
}),
|
|
201
|
-
signal: AbortSignal.timeout(10000),
|
|
202
|
-
});
|
|
203
|
-
if (!response.ok) return null;
|
|
204
|
-
return response.json();
|
|
205
|
-
} catch {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
111
|
// --- Hosted context engine ---
|
|
211
112
|
|
|
212
113
|
// Per-session turn buffer. Holds the user message until the matching
|
|
@@ -440,7 +341,7 @@ function createHostedContextEngine(config, opts = {}) {
|
|
|
440
341
|
const memoryText = results
|
|
441
342
|
.map(
|
|
442
343
|
(m) =>
|
|
443
|
-
`- [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`
|
|
344
|
+
`- [${Math.round((m.similarity || 0) * 100)}%] ${sanitizeMemoryContent(m.content)}`
|
|
444
345
|
)
|
|
445
346
|
.join("\n");
|
|
446
347
|
|
|
@@ -638,7 +539,7 @@ Tell the user to run step 1 first, then help them fill in the config with the cr
|
|
|
638
539
|
return results
|
|
639
540
|
.map(
|
|
640
541
|
(m, i) =>
|
|
641
|
-
`${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`
|
|
542
|
+
`${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${sanitizeMemoryContent(m.content)}`
|
|
642
543
|
)
|
|
643
544
|
.join("\n\n");
|
|
644
545
|
},
|
|
@@ -705,7 +606,7 @@ Tell the user to run step 1 first, then help them fill in the config with the cr
|
|
|
705
606
|
return results
|
|
706
607
|
.map(
|
|
707
608
|
(m, i) =>
|
|
708
|
-
`${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`
|
|
609
|
+
`${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${sanitizeMemoryContent(m.content)}`
|
|
709
610
|
)
|
|
710
611
|
.join("\n\n");
|
|
711
612
|
},
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-content sanitizer.
|
|
3
|
+
*
|
|
4
|
+
* Stored memories from TES often contain dashboard-UI noise (leading
|
|
5
|
+
* timestamps, layer IDs, confidence/decay metadata, trailing JSON
|
|
6
|
+
* blobs). This strips them before showing content to the model — the
|
|
7
|
+
* fact-bearing text is what matters, the metadata just dilutes the
|
|
8
|
+
* signal and burns context budget.
|
|
9
|
+
*
|
|
10
|
+
* Conservative: if stripping would leave no real words, fall back to
|
|
11
|
+
* the original content. Better a noisy signal than none.
|
|
12
|
+
*
|
|
13
|
+
* Canonical implementation. The Claude Code hook (`hooks/scripts/
|
|
14
|
+
* shared.js`) and the published openclaw-plugin (`openclaw-plugin/
|
|
15
|
+
* index.js`) each inline the same logic — they're published
|
|
16
|
+
* standalone and can't cross-import. Update all three if changing.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const TES_META_FIELDS =
|
|
20
|
+
"event_id|event_type|entity_type|source|clientId|correlationId|timestamp|session_id|layer_id|confidence|decay_rate|user_id";
|
|
21
|
+
|
|
22
|
+
export const MEMORY_MAX_LEN = 600;
|
|
23
|
+
|
|
24
|
+
export function sanitizeMemoryContent(content) {
|
|
25
|
+
if (typeof content !== "string") return content;
|
|
26
|
+
let out = content;
|
|
27
|
+
// Trailing JSON metadata blob (no `m` flag — `$` = end-of-string).
|
|
28
|
+
out = out.replace(/\n\{\s*\n[\s\S]*?\n\s*\}\s*$/, "");
|
|
29
|
+
// Inline JSON metadata blobs (2+ consecutive TES metadata fields).
|
|
30
|
+
out = out.replace(
|
|
31
|
+
new RegExp(
|
|
32
|
+
`\\{\\s*\\n(\\s*"(?:${TES_META_FIELDS})"[^\\n]*\\n){2,}\\s*\\}`,
|
|
33
|
+
"g"
|
|
34
|
+
),
|
|
35
|
+
""
|
|
36
|
+
);
|
|
37
|
+
// Dashboard-UI standalone lines.
|
|
38
|
+
const linePatterns = [
|
|
39
|
+
/^\s*anonymous\s*$/gm,
|
|
40
|
+
/^\s*ml_[a-z0-9_-]+_(episodic|semantic|procedural|working)\s*$/gm,
|
|
41
|
+
/^\s*\d+%\s*match\s*$/gm,
|
|
42
|
+
/^\s*Confidence:\s*\d+%\s*$/gm,
|
|
43
|
+
/^\s*Accessed:\s*\d+x?\s*$/gm,
|
|
44
|
+
/^\s*<?\s*\d+[smhd]\s*ago\s*$/gm,
|
|
45
|
+
/^\s*Decay:\s*[\d.]+\s*$/gm,
|
|
46
|
+
/^\s*Metadata\s*$/gm,
|
|
47
|
+
];
|
|
48
|
+
for (const pat of linePatterns) out = out.replace(pat, "");
|
|
49
|
+
// Leading ISO timestamps — strip prefix, keep line content.
|
|
50
|
+
out = out.replace(/^\s*\[\d{4}-\d{2}-\d{2}T[\d:.]+Z\]\s*/gm, "");
|
|
51
|
+
// Collapse consecutive blank lines.
|
|
52
|
+
out = out.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
|
|
53
|
+
// Cap verbose transcript dumps.
|
|
54
|
+
if (out.length > MEMORY_MAX_LEN) {
|
|
55
|
+
out = out.slice(0, MEMORY_MAX_LEN).trimEnd() + "…";
|
|
56
|
+
}
|
|
57
|
+
// Fallback to original if we stripped everything meaningful.
|
|
58
|
+
const wordCount = (out.match(/\b\w{2,}\b/g) || []).length;
|
|
59
|
+
if (wordCount < 2) return content;
|
|
60
|
+
return out;
|
|
61
|
+
}
|