@sonzai-labs/openclaw-context 1.0.3
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/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/cli.cjs +169 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +145 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +670 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +282 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +638 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var agents = require('@sonzai-labs/agents');
|
|
6
|
+
require('readline');
|
|
7
|
+
var fs = require('fs');
|
|
8
|
+
var path = require('path');
|
|
9
|
+
|
|
10
|
+
function _interopNamespace(e) {
|
|
11
|
+
if (e && e.__esModule) return e;
|
|
12
|
+
var n = Object.create(null);
|
|
13
|
+
if (e) {
|
|
14
|
+
Object.keys(e).forEach(function (k) {
|
|
15
|
+
if (k !== 'default') {
|
|
16
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
17
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function () { return e[k]; }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
n.default = e;
|
|
25
|
+
return Object.freeze(n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
29
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
30
|
+
|
|
31
|
+
// src/plugin.ts
|
|
32
|
+
|
|
33
|
+
// src/config.ts
|
|
34
|
+
var DEFAULTS = {
|
|
35
|
+
baseUrl: "https://api.sonz.ai",
|
|
36
|
+
agentName: "openclaw-agent",
|
|
37
|
+
defaultUserId: "owner",
|
|
38
|
+
contextTokenBudget: 2e3
|
|
39
|
+
};
|
|
40
|
+
function resolveConfig(raw) {
|
|
41
|
+
const apiKey = raw?.apiKey || env("SONZAI_API_KEY") || env("SONZAI_OPENCLAW_API_KEY");
|
|
42
|
+
if (!apiKey) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"[@sonzai-labs/openclaw-context] Missing API key. Set apiKey in plugin config or SONZAI_API_KEY environment variable."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
apiKey,
|
|
49
|
+
agentId: raw?.agentId || env("SONZAI_AGENT_ID") || void 0,
|
|
50
|
+
baseUrl: raw?.baseUrl || env("SONZAI_BASE_URL") || DEFAULTS.baseUrl,
|
|
51
|
+
agentName: raw?.agentName || env("SONZAI_AGENT_NAME") || DEFAULTS.agentName,
|
|
52
|
+
defaultUserId: raw?.defaultUserId || DEFAULTS.defaultUserId,
|
|
53
|
+
contextTokenBudget: raw?.contextTokenBudget ?? DEFAULTS.contextTokenBudget,
|
|
54
|
+
disable: raw?.disable ?? {},
|
|
55
|
+
extractionProvider: raw?.extractionProvider || env("SONZAI_EXTRACTION_PROVIDER") || void 0,
|
|
56
|
+
extractionModel: raw?.extractionModel || env("SONZAI_EXTRACTION_MODEL") || void 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function env(key) {
|
|
60
|
+
try {
|
|
61
|
+
return typeof process !== "undefined" ? process.env[key] : void 0;
|
|
62
|
+
} catch {
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/cache.ts
|
|
68
|
+
var SessionCache = class {
|
|
69
|
+
constructor(ttlMs) {
|
|
70
|
+
this.ttlMs = ttlMs;
|
|
71
|
+
}
|
|
72
|
+
store = /* @__PURE__ */ new Map();
|
|
73
|
+
get(key) {
|
|
74
|
+
const entry = this.store.get(key);
|
|
75
|
+
if (!entry) return void 0;
|
|
76
|
+
if (Date.now() > entry.expiresAt) {
|
|
77
|
+
this.store.delete(key);
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
return entry.value;
|
|
81
|
+
}
|
|
82
|
+
set(key, value) {
|
|
83
|
+
this.store.set(key, {
|
|
84
|
+
value,
|
|
85
|
+
expiresAt: Date.now() + this.ttlMs
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
delete(key) {
|
|
89
|
+
this.store.delete(key);
|
|
90
|
+
}
|
|
91
|
+
clear() {
|
|
92
|
+
this.store.clear();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/context-builder.ts
|
|
97
|
+
var SECTION_ORDER = [
|
|
98
|
+
"habits",
|
|
99
|
+
"interests",
|
|
100
|
+
"goals",
|
|
101
|
+
"relationships",
|
|
102
|
+
"mood",
|
|
103
|
+
"memories",
|
|
104
|
+
"personality"
|
|
105
|
+
];
|
|
106
|
+
var formatters = {
|
|
107
|
+
personality: formatPersonality,
|
|
108
|
+
mood: formatMood,
|
|
109
|
+
relationships: formatRelationships,
|
|
110
|
+
memories: formatMemories,
|
|
111
|
+
goals: formatGoals,
|
|
112
|
+
interests: formatInterests,
|
|
113
|
+
habits: formatHabits
|
|
114
|
+
};
|
|
115
|
+
function buildSystemPromptAddition(sources, tokenBudget) {
|
|
116
|
+
const sections = [];
|
|
117
|
+
for (const key of [...SECTION_ORDER].reverse()) {
|
|
118
|
+
const data = sources[key];
|
|
119
|
+
if (!data) continue;
|
|
120
|
+
const text = formatters[key](data);
|
|
121
|
+
if (text) sections.push({ key, text });
|
|
122
|
+
}
|
|
123
|
+
if (sections.length === 0) return "";
|
|
124
|
+
let result = "<sonzai-context>\n" + sections.map((s) => s.text).join("\n\n") + "\n</sonzai-context>";
|
|
125
|
+
while (estimateTokens(result) > tokenBudget && sections.length > 1) {
|
|
126
|
+
const dropKey = SECTION_ORDER.find(
|
|
127
|
+
(k) => sections.some((s) => s.key === k)
|
|
128
|
+
);
|
|
129
|
+
if (!dropKey) break;
|
|
130
|
+
const idx = sections.findIndex((s) => s.key === dropKey);
|
|
131
|
+
if (idx === -1) break;
|
|
132
|
+
sections.splice(idx, 1);
|
|
133
|
+
result = "<sonzai-context>\n" + sections.map((s) => s.text).join("\n\n") + "\n</sonzai-context>";
|
|
134
|
+
}
|
|
135
|
+
if (estimateTokens(result) > tokenBudget) {
|
|
136
|
+
const maxChars = tokenBudget * 4;
|
|
137
|
+
result = result.slice(0, maxChars) + "\n[...truncated]\n</sonzai-context>";
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
function estimateTokens(text) {
|
|
142
|
+
return Math.ceil(text.length / 4);
|
|
143
|
+
}
|
|
144
|
+
function formatPersonality(data) {
|
|
145
|
+
const resp = data;
|
|
146
|
+
const p = resp?.profile;
|
|
147
|
+
if (!p) return null;
|
|
148
|
+
const lines = ["## Personality"];
|
|
149
|
+
lines.push(`Name: ${p.name}`);
|
|
150
|
+
if (p.bio) lines.push(`Bio: ${p.bio}`);
|
|
151
|
+
if (p.primary_traits?.length) lines.push(`Traits: ${p.primary_traits.join(", ")}`);
|
|
152
|
+
if (p.speech_patterns?.length) lines.push(`Speech patterns: ${p.speech_patterns.join(", ")}`);
|
|
153
|
+
if (p.personality_prompt) lines.push(`Character: ${p.personality_prompt}`);
|
|
154
|
+
if (p.big5) {
|
|
155
|
+
const b5 = p.big5;
|
|
156
|
+
const traits = [
|
|
157
|
+
`O:${b5.openness?.score ?? "?"}`,
|
|
158
|
+
`C:${b5.conscientiousness?.score ?? "?"}`,
|
|
159
|
+
`E:${b5.extraversion?.score ?? "?"}`,
|
|
160
|
+
`A:${b5.agreeableness?.score ?? "?"}`,
|
|
161
|
+
`N:${b5.neuroticism?.score ?? "?"}`
|
|
162
|
+
];
|
|
163
|
+
lines.push(`Big5: ${traits.join(" ")}`);
|
|
164
|
+
}
|
|
165
|
+
if (p.preferences) {
|
|
166
|
+
const pref = p.preferences;
|
|
167
|
+
lines.push(
|
|
168
|
+
`Style: pace=${pref.pace}, formality=${pref.formality}, humor=${pref.humor_style}, expression=${pref.emotional_expression}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const evo = resp?.evolution;
|
|
172
|
+
if (evo?.length) {
|
|
173
|
+
lines.push("Recent shifts:");
|
|
174
|
+
for (const d of evo.slice(0, 3)) {
|
|
175
|
+
lines.push(`- ${d.change} (${d.reason})`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
function formatMood(data) {
|
|
181
|
+
const mood = data;
|
|
182
|
+
if (!mood || Object.keys(mood).length === 0) return null;
|
|
183
|
+
const lines = ["## Current Mood"];
|
|
184
|
+
for (const [key, value] of Object.entries(mood)) {
|
|
185
|
+
if (key.startsWith("_") || value === null || value === void 0) continue;
|
|
186
|
+
if (typeof value === "object") {
|
|
187
|
+
lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
188
|
+
} else {
|
|
189
|
+
lines.push(`${key}: ${value}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return lines.length > 1 ? lines.join("\n") : null;
|
|
193
|
+
}
|
|
194
|
+
function formatRelationships(data) {
|
|
195
|
+
const rels = data;
|
|
196
|
+
if (!rels || Object.keys(rels).length === 0) return null;
|
|
197
|
+
const lines = ["## Relationship"];
|
|
198
|
+
for (const [key, value] of Object.entries(rels)) {
|
|
199
|
+
if (key.startsWith("_") || value === null || value === void 0) continue;
|
|
200
|
+
if (typeof value === "object") {
|
|
201
|
+
lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
202
|
+
} else {
|
|
203
|
+
lines.push(`${key}: ${value}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return lines.length > 1 ? lines.join("\n") : null;
|
|
207
|
+
}
|
|
208
|
+
function formatMemories(data) {
|
|
209
|
+
const resp = data;
|
|
210
|
+
if (!resp?.results?.length) return null;
|
|
211
|
+
const lines = ["## Relevant Memories"];
|
|
212
|
+
for (const mem of resp.results.slice(0, 10)) {
|
|
213
|
+
const score = mem.score ? ` (score: ${mem.score.toFixed(2)})` : "";
|
|
214
|
+
lines.push(`- ${mem.content}${score}`);
|
|
215
|
+
}
|
|
216
|
+
return lines.join("\n");
|
|
217
|
+
}
|
|
218
|
+
function formatGoals(data) {
|
|
219
|
+
const resp = data;
|
|
220
|
+
if (!resp?.goals?.length) return null;
|
|
221
|
+
const lines = ["## Goals"];
|
|
222
|
+
for (const g of resp.goals.filter((g2) => g2.status === "active").slice(0, 5)) {
|
|
223
|
+
lines.push(`- ${g.title}: ${g.description} [${g.type}, priority=${g.priority}]`);
|
|
224
|
+
}
|
|
225
|
+
return lines.length > 1 ? lines.join("\n") : null;
|
|
226
|
+
}
|
|
227
|
+
function formatInterests(data) {
|
|
228
|
+
const interests = data;
|
|
229
|
+
if (!interests || Object.keys(interests).length === 0) return null;
|
|
230
|
+
const lines = ["## Interests"];
|
|
231
|
+
for (const [key, value] of Object.entries(interests)) {
|
|
232
|
+
if (key.startsWith("_") || value === null || value === void 0) continue;
|
|
233
|
+
if (typeof value === "object") {
|
|
234
|
+
lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
235
|
+
} else {
|
|
236
|
+
lines.push(`${key}: ${value}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return lines.length > 1 ? lines.join("\n") : null;
|
|
240
|
+
}
|
|
241
|
+
function formatHabits(data) {
|
|
242
|
+
const habits = data;
|
|
243
|
+
if (!habits || Object.keys(habits).length === 0) return null;
|
|
244
|
+
const lines = ["## Habits"];
|
|
245
|
+
for (const [key, value] of Object.entries(habits)) {
|
|
246
|
+
if (key.startsWith("_") || value === null || value === void 0) continue;
|
|
247
|
+
if (typeof value === "object") {
|
|
248
|
+
lines.push(`${key}: ${JSON.stringify(value)}`);
|
|
249
|
+
} else {
|
|
250
|
+
lines.push(`${key}: ${value}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return lines.length > 1 ? lines.join("\n") : null;
|
|
254
|
+
}
|
|
255
|
+
function buildSystemPromptFromContext(ctx, tokenBudget, disable = {}) {
|
|
256
|
+
const sections = [];
|
|
257
|
+
if (!disable.personality && ctx.personality_prompt) {
|
|
258
|
+
const lines = ["## Personality"];
|
|
259
|
+
lines.push(`Character: ${ctx.personality_prompt}`);
|
|
260
|
+
if (ctx.primary_traits?.length) lines.push(`Traits: ${ctx.primary_traits.join(", ")}`);
|
|
261
|
+
if (ctx.speech_patterns?.length) lines.push(`Speech patterns: ${ctx.speech_patterns.join(", ")}`);
|
|
262
|
+
if (ctx.big5) {
|
|
263
|
+
const b5 = ctx.big5;
|
|
264
|
+
const traits = ["openness", "conscientiousness", "extraversion", "agreeableness", "neuroticism"].map((t) => `${t[0].toUpperCase()}:${b5[t]?.score ?? "?"}`).join(" ");
|
|
265
|
+
lines.push(`Big5: ${traits}`);
|
|
266
|
+
}
|
|
267
|
+
sections.push({ key: "personality", text: lines.join("\n"), priority: 7 });
|
|
268
|
+
}
|
|
269
|
+
if (!disable.memory) {
|
|
270
|
+
const facts = ctx.loaded_facts;
|
|
271
|
+
if (facts?.length) {
|
|
272
|
+
const lines = ["## Relevant Memories"];
|
|
273
|
+
for (const f of facts.slice(0, 10)) {
|
|
274
|
+
const text = f.atomic_text || f.content || JSON.stringify(f);
|
|
275
|
+
lines.push(`- ${text}`);
|
|
276
|
+
}
|
|
277
|
+
sections.push({ key: "memories", text: lines.join("\n"), priority: 6 });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!disable.mood && ctx.current_mood) {
|
|
281
|
+
const mood = ctx.current_mood;
|
|
282
|
+
const lines = ["## Current Mood"];
|
|
283
|
+
for (const [key, value] of Object.entries(mood)) {
|
|
284
|
+
if (key.startsWith("_") || value === null || value === void 0) continue;
|
|
285
|
+
lines.push(`${key}: ${typeof value === "object" ? JSON.stringify(value) : value}`);
|
|
286
|
+
}
|
|
287
|
+
if (lines.length > 1) sections.push({ key: "mood", text: lines.join("\n"), priority: 5 });
|
|
288
|
+
}
|
|
289
|
+
if (!disable.relationships && ctx.relationship_narrative) {
|
|
290
|
+
const lines = ["## Relationship"];
|
|
291
|
+
lines.push(ctx.relationship_narrative);
|
|
292
|
+
if (ctx.love_from_agent !== void 0) lines.push(`Love (agent->user): ${ctx.love_from_agent}`);
|
|
293
|
+
if (ctx.love_from_user !== void 0) lines.push(`Love (user->agent): ${ctx.love_from_user}`);
|
|
294
|
+
if (ctx.relationship_status) lines.push(`Status: ${ctx.relationship_status}`);
|
|
295
|
+
sections.push({ key: "relationships", text: lines.join("\n"), priority: 4 });
|
|
296
|
+
}
|
|
297
|
+
if (!disable.goals) {
|
|
298
|
+
const goals = ctx.active_goals;
|
|
299
|
+
if (goals?.length) {
|
|
300
|
+
const lines = ["## Goals"];
|
|
301
|
+
for (const g of goals.slice(0, 5)) {
|
|
302
|
+
lines.push(`- ${g.title}: ${g.description} [${g.type}]`);
|
|
303
|
+
}
|
|
304
|
+
sections.push({ key: "goals", text: lines.join("\n"), priority: 3 });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!disable.interests && ctx.true_interests?.length) {
|
|
308
|
+
sections.push({ key: "interests", text: `## Interests
|
|
309
|
+
${ctx.true_interests.join(", ")}`, priority: 2 });
|
|
310
|
+
}
|
|
311
|
+
if (!disable.habits) {
|
|
312
|
+
const ctxHabits = ctx.habits;
|
|
313
|
+
if (ctxHabits?.length) {
|
|
314
|
+
const lines = ["## Habits"];
|
|
315
|
+
for (const h of ctxHabits.slice(0, 5)) {
|
|
316
|
+
lines.push(`- ${h.name}: ${h.description || h.category || ""}`);
|
|
317
|
+
}
|
|
318
|
+
sections.push({ key: "habits", text: lines.join("\n"), priority: 1 });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (sections.length === 0) return "";
|
|
322
|
+
sections.sort((a, b) => b.priority - a.priority);
|
|
323
|
+
let result = "<sonzai-context>\n" + sections.map((s) => s.text).join("\n\n") + "\n</sonzai-context>";
|
|
324
|
+
while (estimateTokens(result) > tokenBudget && sections.length > 1) {
|
|
325
|
+
sections.pop();
|
|
326
|
+
result = "<sonzai-context>\n" + sections.map((s) => s.text).join("\n\n") + "\n</sonzai-context>";
|
|
327
|
+
}
|
|
328
|
+
if (estimateTokens(result) > tokenBudget) {
|
|
329
|
+
const maxChars = tokenBudget * 4;
|
|
330
|
+
result = result.slice(0, maxChars) + "\n[...truncated]\n</sonzai-context>";
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/session-key.ts
|
|
336
|
+
function parseSessionKey(sessionId, defaultUserId) {
|
|
337
|
+
const parts = sessionId.split(":");
|
|
338
|
+
if (parts[0] === "agent" && parts.length >= 3) {
|
|
339
|
+
const agentId = parts[1] ?? null;
|
|
340
|
+
const directIdx = parts.indexOf("direct");
|
|
341
|
+
if (directIdx !== -1 && directIdx + 1 < parts.length) {
|
|
342
|
+
const channel = directIdx >= 3 ? parts[2] : null;
|
|
343
|
+
const peerId = parts.slice(directIdx + 1).join(":");
|
|
344
|
+
return {
|
|
345
|
+
agentId,
|
|
346
|
+
userId: peerId,
|
|
347
|
+
channel,
|
|
348
|
+
sessionType: "dm"
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const groupIdx = parts.indexOf("group");
|
|
352
|
+
if (groupIdx !== -1 && groupIdx + 1 < parts.length) {
|
|
353
|
+
const channel = groupIdx >= 3 ? parts[2] : null;
|
|
354
|
+
const groupId = parts.slice(groupIdx + 1).join(":");
|
|
355
|
+
return {
|
|
356
|
+
agentId,
|
|
357
|
+
userId: groupId,
|
|
358
|
+
channel,
|
|
359
|
+
sessionType: "group"
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
agentId,
|
|
364
|
+
userId: defaultUserId,
|
|
365
|
+
channel: null,
|
|
366
|
+
sessionType: "cli"
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (parts[0] === "cron") {
|
|
370
|
+
return {
|
|
371
|
+
agentId: null,
|
|
372
|
+
userId: defaultUserId,
|
|
373
|
+
channel: null,
|
|
374
|
+
sessionType: "cron"
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
if (parts[0] === "hook") {
|
|
378
|
+
return {
|
|
379
|
+
agentId: null,
|
|
380
|
+
userId: defaultUserId,
|
|
381
|
+
channel: null,
|
|
382
|
+
sessionType: "webhook"
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
agentId: null,
|
|
387
|
+
userId: defaultUserId,
|
|
388
|
+
channel: null,
|
|
389
|
+
sessionType: "unknown"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// src/engine.ts
|
|
394
|
+
var AGENT_CACHE_TTL = 30 * 60 * 1e3;
|
|
395
|
+
var SonzaiContextEngine = class {
|
|
396
|
+
constructor(client, config) {
|
|
397
|
+
this.client = client;
|
|
398
|
+
this.config = config;
|
|
399
|
+
}
|
|
400
|
+
info = {
|
|
401
|
+
id: "sonzai",
|
|
402
|
+
name: "Sonzai Mind Layer",
|
|
403
|
+
ownsCompaction: true
|
|
404
|
+
};
|
|
405
|
+
sessions = /* @__PURE__ */ new Map();
|
|
406
|
+
agentCache = new SessionCache(AGENT_CACHE_TTL);
|
|
407
|
+
// -------------------------------------------------------------------------
|
|
408
|
+
// bootstrap — initialize session, optionally auto-provision agent
|
|
409
|
+
// -------------------------------------------------------------------------
|
|
410
|
+
async bootstrap({ sessionId }) {
|
|
411
|
+
try {
|
|
412
|
+
const parsed = parseSessionKey(sessionId, this.config.defaultUserId);
|
|
413
|
+
const agentId = await this.resolveAgentId(parsed.agentId);
|
|
414
|
+
await this.client.agents.sessions.start(agentId, {
|
|
415
|
+
userId: parsed.userId,
|
|
416
|
+
sessionId
|
|
417
|
+
});
|
|
418
|
+
this.sessions.set(sessionId, {
|
|
419
|
+
agentId,
|
|
420
|
+
userId: parsed.userId,
|
|
421
|
+
sonzaiSessionId: sessionId,
|
|
422
|
+
startedAt: Date.now(),
|
|
423
|
+
turnCount: 0,
|
|
424
|
+
lastProcessedIndex: 0,
|
|
425
|
+
lastMessages: []
|
|
426
|
+
});
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.warn("[@sonzai-labs/openclaw-context] bootstrap failed:", err);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// -------------------------------------------------------------------------
|
|
432
|
+
// ingest — no-op; processing happens in afterTurn with full conversation
|
|
433
|
+
// -------------------------------------------------------------------------
|
|
434
|
+
async ingest(_args) {
|
|
435
|
+
return { ingested: true };
|
|
436
|
+
}
|
|
437
|
+
// -------------------------------------------------------------------------
|
|
438
|
+
// assemble — fetch enriched context and inject as systemPromptAddition
|
|
439
|
+
// -------------------------------------------------------------------------
|
|
440
|
+
async assemble({
|
|
441
|
+
sessionId,
|
|
442
|
+
messages,
|
|
443
|
+
tokenBudget,
|
|
444
|
+
origin
|
|
445
|
+
}) {
|
|
446
|
+
const session = this.sessions.get(sessionId);
|
|
447
|
+
if (!session) {
|
|
448
|
+
return { messages, estimatedTokens: 0 };
|
|
449
|
+
}
|
|
450
|
+
session.lastMessages = messages;
|
|
451
|
+
try {
|
|
452
|
+
const { agentId, userId } = session;
|
|
453
|
+
const lastUserMsg = findLastUserMessage(messages);
|
|
454
|
+
const budget = Math.min(tokenBudget, this.config.contextTokenBudget);
|
|
455
|
+
const effectiveUserId = origin?.from && origin?.provider ? `${origin.provider}:${origin.from}` : userId;
|
|
456
|
+
const context = await this.client.agents.getContext(agentId, {
|
|
457
|
+
userId: effectiveUserId,
|
|
458
|
+
sessionId: session.sonzaiSessionId,
|
|
459
|
+
query: lastUserMsg
|
|
460
|
+
});
|
|
461
|
+
const systemPromptAddition = buildSystemPromptFromContext(
|
|
462
|
+
context,
|
|
463
|
+
budget,
|
|
464
|
+
this.config.disable
|
|
465
|
+
);
|
|
466
|
+
return {
|
|
467
|
+
messages,
|
|
468
|
+
estimatedTokens: estimateTokens(systemPromptAddition),
|
|
469
|
+
systemPromptAddition: systemPromptAddition || void 0
|
|
470
|
+
};
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.warn("[@sonzai-labs/openclaw-context] assemble failed:", err);
|
|
473
|
+
return { messages, estimatedTokens: 0 };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// -------------------------------------------------------------------------
|
|
477
|
+
// compact — trigger Sonzai's consolidation pipeline
|
|
478
|
+
// -------------------------------------------------------------------------
|
|
479
|
+
async compact({ sessionId }) {
|
|
480
|
+
const session = this.sessions.get(sessionId);
|
|
481
|
+
if (!session) {
|
|
482
|
+
return { ok: false, compacted: false };
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
await this.client.agents.consolidate(session.agentId);
|
|
486
|
+
return { ok: true, compacted: true };
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.warn("[@sonzai-labs/openclaw-context] compact failed:", err);
|
|
489
|
+
return { ok: false, compacted: false };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// -------------------------------------------------------------------------
|
|
493
|
+
// afterTurn — send new messages for fact extraction
|
|
494
|
+
// -------------------------------------------------------------------------
|
|
495
|
+
async afterTurn({ sessionId }) {
|
|
496
|
+
const session = this.sessions.get(sessionId);
|
|
497
|
+
if (!session) return;
|
|
498
|
+
try {
|
|
499
|
+
session.turnCount++;
|
|
500
|
+
const newMessages = session.lastMessages.slice(session.lastProcessedIndex);
|
|
501
|
+
session.lastProcessedIndex = session.lastMessages.length;
|
|
502
|
+
if (newMessages.length === 0) return;
|
|
503
|
+
await this.client.agents.process(session.agentId, {
|
|
504
|
+
userId: session.userId,
|
|
505
|
+
sessionId: session.sonzaiSessionId,
|
|
506
|
+
messages: newMessages.map((m) => ({
|
|
507
|
+
role: m.role,
|
|
508
|
+
content: m.content
|
|
509
|
+
})),
|
|
510
|
+
provider: this.config.extractionProvider,
|
|
511
|
+
model: this.config.extractionModel
|
|
512
|
+
});
|
|
513
|
+
} catch (err) {
|
|
514
|
+
console.warn("[@sonzai-labs/openclaw-context] afterTurn failed:", err);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// -------------------------------------------------------------------------
|
|
518
|
+
// dispose — end all active sessions and clear caches
|
|
519
|
+
// -------------------------------------------------------------------------
|
|
520
|
+
async dispose() {
|
|
521
|
+
const endPromises = [...this.sessions.entries()].map(
|
|
522
|
+
async ([_sessionId, session]) => {
|
|
523
|
+
try {
|
|
524
|
+
await this.client.agents.sessions.end(session.agentId, {
|
|
525
|
+
userId: session.userId,
|
|
526
|
+
sessionId: session.sonzaiSessionId,
|
|
527
|
+
totalMessages: session.turnCount,
|
|
528
|
+
durationSeconds: Math.floor(
|
|
529
|
+
(Date.now() - session.startedAt) / 1e3
|
|
530
|
+
)
|
|
531
|
+
});
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
);
|
|
536
|
+
await Promise.allSettled(endPromises);
|
|
537
|
+
this.sessions.clear();
|
|
538
|
+
this.agentCache.clear();
|
|
539
|
+
}
|
|
540
|
+
// -------------------------------------------------------------------------
|
|
541
|
+
// Private helpers
|
|
542
|
+
// -------------------------------------------------------------------------
|
|
543
|
+
/**
|
|
544
|
+
* Resolve the Sonzai agent ID: use config if provided, otherwise
|
|
545
|
+
* auto-provision via the idempotent create endpoint.
|
|
546
|
+
*
|
|
547
|
+
* Idempotency guarantee: the backend derives a deterministic UUID v5 from
|
|
548
|
+
* `SHA1(namespace, tenantID + "/" + lowercase(name))`. Same API key (tenant)
|
|
549
|
+
* + same name → same UUID on every call, even across restarts or pod
|
|
550
|
+
* replacements.
|
|
551
|
+
*
|
|
552
|
+
* The name used is always `config.agentName` (default: "openclaw-agent").
|
|
553
|
+
* This is stable because it comes from the plugin config / env var, not
|
|
554
|
+
* from any runtime state like pod IDs or session UUIDs.
|
|
555
|
+
*
|
|
556
|
+
* If a B2B client needs multiple distinct agents under one API key, they
|
|
557
|
+
* set a unique `agentName` per OpenClaw instance in their config.
|
|
558
|
+
*/
|
|
559
|
+
async resolveAgentId(_sessionAgentId) {
|
|
560
|
+
if (this.config.agentId) {
|
|
561
|
+
return this.config.agentId;
|
|
562
|
+
}
|
|
563
|
+
const name = this.config.agentName;
|
|
564
|
+
const cached = this.agentCache.get(name);
|
|
565
|
+
if (cached) return cached;
|
|
566
|
+
const agent = await this.client.agents.create({ name });
|
|
567
|
+
this.agentCache.set(name, agent.agent_id);
|
|
568
|
+
return agent.agent_id;
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
function findLastUserMessage(messages) {
|
|
572
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
573
|
+
if (messages[i].role === "user") {
|
|
574
|
+
return messages[i].content;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return void 0;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/plugin.ts
|
|
581
|
+
function register(api) {
|
|
582
|
+
api.registerContextEngine("sonzai", () => {
|
|
583
|
+
const config = resolveConfig();
|
|
584
|
+
const client = new agents.Sonzai({
|
|
585
|
+
apiKey: config.apiKey,
|
|
586
|
+
baseUrl: config.baseUrl
|
|
587
|
+
});
|
|
588
|
+
return new SonzaiContextEngine(client, config);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
async function setup(options) {
|
|
592
|
+
const baseUrl = options.baseUrl || "https://api.sonz.ai";
|
|
593
|
+
const agentName = options.agentName || "openclaw-agent";
|
|
594
|
+
const writeConfig = options.writeConfig ?? true;
|
|
595
|
+
const configPath = options.configPath || "./openclaw.json";
|
|
596
|
+
const client = new agents.Sonzai({ apiKey: options.apiKey, baseUrl });
|
|
597
|
+
let agentId;
|
|
598
|
+
if (options.agentId) {
|
|
599
|
+
const agent = await client.agents.get(options.agentId);
|
|
600
|
+
agentId = agent.agent_id;
|
|
601
|
+
} else {
|
|
602
|
+
const agent = await client.agents.create({ name: agentName });
|
|
603
|
+
agentId = agent.agent_id;
|
|
604
|
+
}
|
|
605
|
+
const pluginConfig = buildOpenClawConfig(options.apiKey, agentId);
|
|
606
|
+
let written = false;
|
|
607
|
+
if (writeConfig) {
|
|
608
|
+
mergeOpenClawConfig(configPath, pluginConfig);
|
|
609
|
+
written = true;
|
|
610
|
+
}
|
|
611
|
+
return {
|
|
612
|
+
agentId,
|
|
613
|
+
agentName,
|
|
614
|
+
config: pluginConfig,
|
|
615
|
+
configPath: written ? path__namespace.resolve(configPath) : void 0,
|
|
616
|
+
written
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
function buildOpenClawConfig(_apiKey, agentId) {
|
|
620
|
+
return {
|
|
621
|
+
plugins: {
|
|
622
|
+
slots: {
|
|
623
|
+
contextEngine: "sonzai"
|
|
624
|
+
},
|
|
625
|
+
entries: {
|
|
626
|
+
sonzai: {
|
|
627
|
+
enabled: true,
|
|
628
|
+
agentId
|
|
629
|
+
// apiKey is intentionally NOT written to the config file —
|
|
630
|
+
// it should be set via SONZAI_API_KEY env var for security.
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function mergeOpenClawConfig(configPath, pluginConfig) {
|
|
637
|
+
let existing = {};
|
|
638
|
+
const resolvedPath = path__namespace.resolve(configPath);
|
|
639
|
+
if (fs__namespace.existsSync(resolvedPath)) {
|
|
640
|
+
try {
|
|
641
|
+
const raw = fs__namespace.readFileSync(resolvedPath, "utf-8");
|
|
642
|
+
existing = JSON.parse(raw);
|
|
643
|
+
} catch {
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const merged = { ...existing };
|
|
647
|
+
const existingPlugins = merged.plugins || {};
|
|
648
|
+
const newPlugins = pluginConfig.plugins;
|
|
649
|
+
const existingSlots = existingPlugins.slots || {};
|
|
650
|
+
const newSlots = newPlugins.slots || {};
|
|
651
|
+
const existingEntries = existingPlugins.entries || {};
|
|
652
|
+
const newEntries = newPlugins.entries || {};
|
|
653
|
+
merged.plugins = {
|
|
654
|
+
...existingPlugins,
|
|
655
|
+
slots: { ...existingSlots, ...newSlots },
|
|
656
|
+
entries: { ...existingEntries, ...newEntries }
|
|
657
|
+
};
|
|
658
|
+
fs__namespace.writeFileSync(resolvedPath, JSON.stringify(merged, null, 2) + "\n");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
exports.SessionCache = SessionCache;
|
|
662
|
+
exports.SonzaiContextEngine = SonzaiContextEngine;
|
|
663
|
+
exports.buildSystemPromptAddition = buildSystemPromptAddition;
|
|
664
|
+
exports.default = register;
|
|
665
|
+
exports.estimateTokens = estimateTokens;
|
|
666
|
+
exports.parseSessionKey = parseSessionKey;
|
|
667
|
+
exports.resolveConfig = resolveConfig;
|
|
668
|
+
exports.setup = setup;
|
|
669
|
+
//# sourceMappingURL=index.cjs.map
|
|
670
|
+
//# sourceMappingURL=index.cjs.map
|