@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.
@@ -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
+ }
@@ -347,7 +347,7 @@ async function main() {
347
347
  const health = {
348
348
  status: "ok",
349
349
  client: CLIENT_ID,
350
- version: "0.5.5",
350
+ version: "0.5.6",
351
351
  search: "text",
352
352
  db: false,
353
353
  ollama: false,