agentlife 1.1.0 → 1.1.1
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/dist/index.js +4360 -0
- package/package.json +16 -2
- package/dev-dashboard.ts +0 -238
- package/index.test.ts +0 -1905
- package/index.ts +0 -1471
package/dist/index.js
ADDED
|
@@ -0,0 +1,4360 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// index.ts
|
|
5
|
+
import { homedir as homedir4 } from "node:os";
|
|
6
|
+
import * as path11 from "node:path";
|
|
7
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
8
|
+
|
|
9
|
+
// db.ts
|
|
10
|
+
import * as fsSync from "node:fs";
|
|
11
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
var nodeRequire = createRequire2(import.meta.url);
|
|
14
|
+
var sqliteModule = null;
|
|
15
|
+
function requireSqlite() {
|
|
16
|
+
if (sqliteModule)
|
|
17
|
+
return sqliteModule;
|
|
18
|
+
sqliteModule = nodeRequire("node:sqlite");
|
|
19
|
+
return sqliteModule;
|
|
20
|
+
}
|
|
21
|
+
function getOrCreateAgentDb(state, agentId) {
|
|
22
|
+
let db = state.agentDbs.get(agentId);
|
|
23
|
+
if (db)
|
|
24
|
+
return db;
|
|
25
|
+
if (!state.dbBaseDir)
|
|
26
|
+
throw new Error("DB base dir not initialized");
|
|
27
|
+
fsSync.mkdirSync(state.dbBaseDir, { recursive: true });
|
|
28
|
+
const { DatabaseSync } = requireSqlite();
|
|
29
|
+
db = new DatabaseSync(path.join(state.dbBaseDir, `${agentId}.db`));
|
|
30
|
+
state.agentDbs.set(agentId, db);
|
|
31
|
+
return db;
|
|
32
|
+
}
|
|
33
|
+
function getOrCreateHistoryDb(state) {
|
|
34
|
+
if (state.historyDb)
|
|
35
|
+
return state.historyDb;
|
|
36
|
+
if (!state.historyDbPath)
|
|
37
|
+
throw new Error("History DB path not initialized");
|
|
38
|
+
fsSync.mkdirSync(path.dirname(state.historyDbPath), { recursive: true });
|
|
39
|
+
const { DatabaseSync } = requireSqlite();
|
|
40
|
+
state.historyDb = new DatabaseSync(state.historyDbPath);
|
|
41
|
+
state.historyDb.exec(`PRAGMA journal_mode = WAL`);
|
|
42
|
+
state.historyDb.exec(`PRAGMA busy_timeout = 5000`);
|
|
43
|
+
state.historyDb.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS surface_events (
|
|
45
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
46
|
+
surfaceId TEXT NOT NULL,
|
|
47
|
+
agentId TEXT,
|
|
48
|
+
event TEXT NOT NULL,
|
|
49
|
+
dsl TEXT,
|
|
50
|
+
metadata TEXT,
|
|
51
|
+
createdAt INTEGER NOT NULL
|
|
52
|
+
);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_se_surface ON surface_events(surfaceId);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_se_agent ON surface_events(agentId);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_se_time ON surface_events(createdAt);
|
|
56
|
+
|
|
57
|
+
CREATE TABLE IF NOT EXISTS usage_ledger (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
sessionKey TEXT NOT NULL,
|
|
60
|
+
agentId TEXT,
|
|
61
|
+
provider TEXT,
|
|
62
|
+
model TEXT,
|
|
63
|
+
inputTokens INTEGER NOT NULL DEFAULT 0,
|
|
64
|
+
outputTokens INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
cacheReadTokens INTEGER NOT NULL DEFAULT 0,
|
|
66
|
+
cacheWriteTokens INTEGER NOT NULL DEFAULT 0,
|
|
67
|
+
totalTokens INTEGER NOT NULL DEFAULT 0,
|
|
68
|
+
estimatedCost REAL NOT NULL DEFAULT 0,
|
|
69
|
+
createdAt INTEGER NOT NULL
|
|
70
|
+
);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_ul_session ON usage_ledger(sessionKey);
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_ul_agent ON usage_ledger(agentId);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_ul_time ON usage_ledger(createdAt);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_ul_model ON usage_ledger(model);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS surface_usage (
|
|
77
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
78
|
+
surfaceId TEXT NOT NULL,
|
|
79
|
+
agentId TEXT,
|
|
80
|
+
sessionKey TEXT NOT NULL,
|
|
81
|
+
inputTokens INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
outputTokens INTEGER NOT NULL DEFAULT 0,
|
|
83
|
+
cacheReadTokens INTEGER NOT NULL DEFAULT 0,
|
|
84
|
+
cacheWriteTokens INTEGER NOT NULL DEFAULT 0,
|
|
85
|
+
totalTokens INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
estimatedCost REAL NOT NULL DEFAULT 0,
|
|
87
|
+
llmCalls INTEGER NOT NULL DEFAULT 1,
|
|
88
|
+
createdAt INTEGER NOT NULL
|
|
89
|
+
);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_su_surface ON surface_usage(surfaceId);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_su_agent ON surface_usage(agentId);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_su_time ON surface_usage(createdAt);
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS activity_log (
|
|
95
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
+
ts INTEGER NOT NULL,
|
|
97
|
+
sessionKey TEXT,
|
|
98
|
+
agentId TEXT,
|
|
99
|
+
event TEXT NOT NULL,
|
|
100
|
+
toolName TEXT,
|
|
101
|
+
summary TEXT,
|
|
102
|
+
data TEXT,
|
|
103
|
+
runId TEXT
|
|
104
|
+
);
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_al_ts ON activity_log(ts);
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_al_session ON activity_log(sessionKey);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_al_agent ON activity_log(agentId);
|
|
108
|
+
CREATE INDEX IF NOT EXISTS idx_al_event ON activity_log(event);
|
|
109
|
+
|
|
110
|
+
CREATE TABLE IF NOT EXISTS automations (
|
|
111
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
112
|
+
surfaceId TEXT,
|
|
113
|
+
agentId TEXT,
|
|
114
|
+
type TEXT NOT NULL,
|
|
115
|
+
name TEXT NOT NULL,
|
|
116
|
+
path TEXT NOT NULL,
|
|
117
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
118
|
+
createdAt INTEGER NOT NULL,
|
|
119
|
+
updatedAt INTEGER NOT NULL
|
|
120
|
+
);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS idx_auto_surface ON automations(surfaceId);
|
|
122
|
+
CREATE INDEX IF NOT EXISTS idx_auto_agent ON automations(agentId);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_auto_status ON automations(status);
|
|
124
|
+
|
|
125
|
+
CREATE TABLE IF NOT EXISTS bootstrap_snapshots (
|
|
126
|
+
sessionKey TEXT PRIMARY KEY,
|
|
127
|
+
agentId TEXT,
|
|
128
|
+
files TEXT NOT NULL,
|
|
129
|
+
createdAt INTEGER NOT NULL
|
|
130
|
+
);
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_bs_agent ON bootstrap_snapshots(agentId);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_bs_time ON bootstrap_snapshots(createdAt);
|
|
133
|
+
|
|
134
|
+
CREATE TABLE IF NOT EXISTS surfaces (
|
|
135
|
+
surfaceId TEXT PRIMARY KEY,
|
|
136
|
+
dsl TEXT NOT NULL,
|
|
137
|
+
agentId TEXT,
|
|
138
|
+
followup TEXT,
|
|
139
|
+
goal TEXT,
|
|
140
|
+
context TEXT,
|
|
141
|
+
isTransient INTEGER NOT NULL DEFAULT 0,
|
|
142
|
+
isOverlay INTEGER NOT NULL DEFAULT 0,
|
|
143
|
+
createdAt INTEGER NOT NULL,
|
|
144
|
+
updatedAt INTEGER NOT NULL,
|
|
145
|
+
expiredSince INTEGER
|
|
146
|
+
);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_surfaces_agent ON surfaces(agentId);
|
|
148
|
+
|
|
149
|
+
CREATE TABLE IF NOT EXISTS cleanup_tasks (
|
|
150
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
151
|
+
surfaceId TEXT NOT NULL,
|
|
152
|
+
agentId TEXT,
|
|
153
|
+
step TEXT NOT NULL,
|
|
154
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
155
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
156
|
+
maxAttempts INTEGER NOT NULL DEFAULT 3,
|
|
157
|
+
lastError TEXT,
|
|
158
|
+
payload TEXT,
|
|
159
|
+
createdAt INTEGER NOT NULL,
|
|
160
|
+
updatedAt INTEGER NOT NULL
|
|
161
|
+
);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_ct_surface ON cleanup_tasks(surfaceId);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_ct_status ON cleanup_tasks(status);
|
|
164
|
+
|
|
165
|
+
`);
|
|
166
|
+
try {
|
|
167
|
+
state.historyDb.exec("ALTER TABLE surfaces ADD COLUMN cronId TEXT");
|
|
168
|
+
} catch {}
|
|
169
|
+
try {
|
|
170
|
+
state.historyDb.exec("ALTER TABLE surfaces ADD COLUMN state TEXT NOT NULL DEFAULT 'active'");
|
|
171
|
+
} catch {}
|
|
172
|
+
const retentionMs = 90 * 24 * 60 * 60 * 1000;
|
|
173
|
+
const cutoff = Date.now() - retentionMs;
|
|
174
|
+
try {
|
|
175
|
+
state.historyDb.exec(`DELETE FROM surface_events WHERE createdAt < ${cutoff}`);
|
|
176
|
+
state.historyDb.exec(`DELETE FROM usage_ledger WHERE createdAt < ${cutoff}`);
|
|
177
|
+
state.historyDb.exec(`DELETE FROM surface_usage WHERE createdAt < ${cutoff}`);
|
|
178
|
+
state.historyDb.exec(`DELETE FROM activity_log WHERE ts < ${cutoff}`);
|
|
179
|
+
state.historyDb.exec(`DELETE FROM bootstrap_snapshots WHERE createdAt < ${cutoff}`);
|
|
180
|
+
const doneCutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
181
|
+
const failedCutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
182
|
+
state.historyDb.exec(`DELETE FROM cleanup_tasks WHERE status = 'done' AND updatedAt < ${doneCutoff}`);
|
|
183
|
+
state.historyDb.exec(`DELETE FROM cleanup_tasks WHERE status = 'failed' AND updatedAt < ${failedCutoff}`);
|
|
184
|
+
} catch {}
|
|
185
|
+
return state.historyDb;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
class SurfaceDb {
|
|
189
|
+
db;
|
|
190
|
+
constructor(db) {
|
|
191
|
+
this.db = db;
|
|
192
|
+
}
|
|
193
|
+
get(id) {
|
|
194
|
+
const row = this.db.prepare("SELECT * FROM surfaces WHERE surfaceId = ?").get(id);
|
|
195
|
+
if (!row)
|
|
196
|
+
return;
|
|
197
|
+
return SurfaceDb.rowToMeta(row);
|
|
198
|
+
}
|
|
199
|
+
has(id) {
|
|
200
|
+
const row = this.db.prepare("SELECT 1 FROM surfaces WHERE surfaceId = ?").get(id);
|
|
201
|
+
return !!row;
|
|
202
|
+
}
|
|
203
|
+
set(id, meta) {
|
|
204
|
+
const dsl = meta.lines.join(`
|
|
205
|
+
`);
|
|
206
|
+
const contextStr = meta.context ? JSON.stringify(meta.context) : null;
|
|
207
|
+
const headerLine = meta.lines[0] ?? "";
|
|
208
|
+
const isOverlay = /\boverlay\b/.test(headerLine) ? 1 : 0;
|
|
209
|
+
const isInput = /\binput\b/.test(headerLine) ? 1 : 0;
|
|
210
|
+
const isTransient = isOverlay || isInput ? 1 : 0;
|
|
211
|
+
this.db.prepare(`
|
|
212
|
+
INSERT OR REPLACE INTO surfaces
|
|
213
|
+
(surfaceId, dsl, agentId, followup, goal, context, isTransient, isOverlay, createdAt, updatedAt, expiredSince, cronId, state)
|
|
214
|
+
VALUES (?, ?, (SELECT agentId FROM surfaces WHERE surfaceId = ?), ?, ?, ?, ?, ?, ?, ?, ?,
|
|
215
|
+
(SELECT cronId FROM surfaces WHERE surfaceId = ?),
|
|
216
|
+
COALESCE((SELECT state FROM surfaces WHERE surfaceId = ?), 'active'))
|
|
217
|
+
`).run(id, dsl, id, meta.followup ?? null, meta.goal ?? null, contextStr, isTransient, isOverlay, meta.createdAt, meta.updatedAt, meta.expiredSince ?? null, id, id);
|
|
218
|
+
}
|
|
219
|
+
delete(id) {
|
|
220
|
+
const result = this.db.prepare("DELETE FROM surfaces WHERE surfaceId = ?").run(id);
|
|
221
|
+
return result.changes > 0;
|
|
222
|
+
}
|
|
223
|
+
clear() {
|
|
224
|
+
this.db.prepare("DELETE FROM surfaces").run();
|
|
225
|
+
}
|
|
226
|
+
*entries() {
|
|
227
|
+
const rows = this.db.prepare("SELECT * FROM surfaces ORDER BY updatedAt DESC").all();
|
|
228
|
+
for (const row of rows) {
|
|
229
|
+
yield [row.surfaceId, SurfaceDb.rowToMeta(row)];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
keys() {
|
|
233
|
+
const rows = this.db.prepare("SELECT surfaceId FROM surfaces").all();
|
|
234
|
+
return rows.map((r) => r.surfaceId);
|
|
235
|
+
}
|
|
236
|
+
get size() {
|
|
237
|
+
const row = this.db.prepare("SELECT COUNT(*) as c FROM surfaces").get();
|
|
238
|
+
return row?.c ?? 0;
|
|
239
|
+
}
|
|
240
|
+
getAgentId(id) {
|
|
241
|
+
const row = this.db.prepare("SELECT agentId FROM surfaces WHERE surfaceId = ?").get(id);
|
|
242
|
+
return row?.agentId ?? undefined;
|
|
243
|
+
}
|
|
244
|
+
setAgentId(id, agentId) {
|
|
245
|
+
this.db.prepare("UPDATE surfaces SET agentId = ? WHERE surfaceId = ?").run(agentId, id);
|
|
246
|
+
}
|
|
247
|
+
isTransient(id) {
|
|
248
|
+
const row = this.db.prepare("SELECT isTransient FROM surfaces WHERE surfaceId = ?").get(id);
|
|
249
|
+
return row?.isTransient === 1;
|
|
250
|
+
}
|
|
251
|
+
setTransient(id, val) {
|
|
252
|
+
this.db.prepare("UPDATE surfaces SET isTransient = ? WHERE surfaceId = ?").run(val ? 1 : 0, id);
|
|
253
|
+
}
|
|
254
|
+
getCronId(id) {
|
|
255
|
+
const row = this.db.prepare("SELECT cronId FROM surfaces WHERE surfaceId = ?").get(id);
|
|
256
|
+
return row?.cronId ?? null;
|
|
257
|
+
}
|
|
258
|
+
setCronId(id, cronId) {
|
|
259
|
+
this.db.prepare("UPDATE surfaces SET cronId = ? WHERE surfaceId = ?").run(cronId, id);
|
|
260
|
+
}
|
|
261
|
+
getState(id) {
|
|
262
|
+
const row = this.db.prepare("SELECT state FROM surfaces WHERE surfaceId = ?").get(id);
|
|
263
|
+
return row?.state ?? null;
|
|
264
|
+
}
|
|
265
|
+
setState(id, state) {
|
|
266
|
+
this.db.prepare("UPDATE surfaces SET state = ? WHERE surfaceId = ?").run(state, id);
|
|
267
|
+
}
|
|
268
|
+
getActiveSurfacesWithCrons() {
|
|
269
|
+
const rows = this.db.prepare("SELECT surfaceId, cronId FROM surfaces WHERE cronId IS NOT NULL AND state != 'dismissed'").all();
|
|
270
|
+
return rows.map((r) => ({ surfaceId: r.surfaceId, cronId: r.cronId }));
|
|
271
|
+
}
|
|
272
|
+
static rowToMeta(row) {
|
|
273
|
+
let context;
|
|
274
|
+
if (row.context) {
|
|
275
|
+
try {
|
|
276
|
+
context = JSON.parse(row.context);
|
|
277
|
+
} catch {}
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
lines: row.dsl.split(`
|
|
281
|
+
`),
|
|
282
|
+
createdAt: row.createdAt,
|
|
283
|
+
updatedAt: row.updatedAt,
|
|
284
|
+
followup: row.followup ?? undefined,
|
|
285
|
+
goal: row.goal ?? undefined,
|
|
286
|
+
context,
|
|
287
|
+
expiredSince: row.expiredSince ?? undefined,
|
|
288
|
+
cronId: row.cronId ?? undefined,
|
|
289
|
+
state: row.state ?? "active"
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function persistBootstrapSnapshot(state, sessionKey, agentId, files) {
|
|
294
|
+
try {
|
|
295
|
+
const db = getOrCreateHistoryDb(state);
|
|
296
|
+
db.prepare(`
|
|
297
|
+
INSERT OR REPLACE INTO bootstrap_snapshots (sessionKey, agentId, files, createdAt)
|
|
298
|
+
VALUES (?, ?, ?, ?)
|
|
299
|
+
`).run(sessionKey, agentId ?? null, JSON.stringify(files), Date.now());
|
|
300
|
+
} catch (e) {
|
|
301
|
+
console.warn("[agentlife:db] failed to persist bootstrap snapshot:", e?.message);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function loadBootstrapSnapshot(state, sessionKey, agentId) {
|
|
305
|
+
try {
|
|
306
|
+
const db = getOrCreateHistoryDb(state);
|
|
307
|
+
let row = null;
|
|
308
|
+
if (sessionKey) {
|
|
309
|
+
row = db.prepare("SELECT files FROM bootstrap_snapshots WHERE sessionKey = ?").get(sessionKey);
|
|
310
|
+
}
|
|
311
|
+
if (!row && agentId) {
|
|
312
|
+
row = db.prepare("SELECT files FROM bootstrap_snapshots WHERE agentId = ? ORDER BY createdAt DESC LIMIT 1").get(agentId);
|
|
313
|
+
}
|
|
314
|
+
if (row?.files) {
|
|
315
|
+
return JSON.parse(row.files);
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
console.warn("[agentlife:db] failed to load bootstrap snapshot:", e?.message);
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
function closeAllDbs(state) {
|
|
323
|
+
for (const db of state.agentDbs.values()) {
|
|
324
|
+
try {
|
|
325
|
+
db.close();
|
|
326
|
+
} catch {}
|
|
327
|
+
}
|
|
328
|
+
state.agentDbs.clear();
|
|
329
|
+
if (state.historyDb) {
|
|
330
|
+
try {
|
|
331
|
+
state.historyDb.close();
|
|
332
|
+
} catch {}
|
|
333
|
+
state.historyDb = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// registry.ts
|
|
338
|
+
import * as fs from "node:fs/promises";
|
|
339
|
+
import * as path2 from "node:path";
|
|
340
|
+
async function loadRegistryFromDisk(state) {
|
|
341
|
+
if (!state.registryFilePath)
|
|
342
|
+
return;
|
|
343
|
+
try {
|
|
344
|
+
const raw = await fs.readFile(state.registryFilePath, "utf-8");
|
|
345
|
+
const data = JSON.parse(raw);
|
|
346
|
+
state.agentRegistry.clear();
|
|
347
|
+
for (const [id, entry] of Object.entries(data.agents ?? {})) {
|
|
348
|
+
const e = entry;
|
|
349
|
+
if (e.description) {
|
|
350
|
+
state.agentRegistry.set(id, {
|
|
351
|
+
name: e.name ?? id,
|
|
352
|
+
description: e.description,
|
|
353
|
+
model: e.model,
|
|
354
|
+
createdAt: e.createdAt ?? Date.now(),
|
|
355
|
+
needsEnrichment: e.needsEnrichment
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
console.log("[agentlife] loaded %d agents from registry", state.agentRegistry.size);
|
|
360
|
+
} catch (e) {
|
|
361
|
+
if (e?.code === "ENOENT") {
|
|
362
|
+
console.log("[agentlife] no agent registry file, starting empty");
|
|
363
|
+
} else {
|
|
364
|
+
console.warn("[agentlife] failed to load agent registry:", e?.message);
|
|
365
|
+
}
|
|
366
|
+
state.agentRegistry.clear();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function saveRegistryToDisk(state) {
|
|
370
|
+
if (!state.registryFilePath)
|
|
371
|
+
return;
|
|
372
|
+
try {
|
|
373
|
+
const data = {
|
|
374
|
+
_version: 1,
|
|
375
|
+
agents: {}
|
|
376
|
+
};
|
|
377
|
+
for (const [id, entry] of state.agentRegistry.entries()) {
|
|
378
|
+
data.agents[id] = entry;
|
|
379
|
+
}
|
|
380
|
+
const dir = path2.dirname(state.registryFilePath);
|
|
381
|
+
await fs.mkdir(dir, { recursive: true });
|
|
382
|
+
const tmp = state.registryFilePath + ".tmp";
|
|
383
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
384
|
+
await fs.rename(tmp, state.registryFilePath);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
console.warn("[agentlife] failed to save agent registry:", e?.message);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function buildAgentRegistryContext(state) {
|
|
390
|
+
if (state.agentRegistry.size === 0)
|
|
391
|
+
return null;
|
|
392
|
+
const lines = ["## Available Agents", ""];
|
|
393
|
+
for (const [id, entry] of state.agentRegistry.entries()) {
|
|
394
|
+
lines.push(`- **${entry.name}** (id: ${id}) — ${entry.description}`);
|
|
395
|
+
}
|
|
396
|
+
lines.push("");
|
|
397
|
+
lines.push("Route to agents by matching user intent to these descriptions.");
|
|
398
|
+
lines.push("If no registered agent matches, fall back to agents_list to discover unregistered agents.");
|
|
399
|
+
return lines.join(`
|
|
400
|
+
`);
|
|
401
|
+
}
|
|
402
|
+
function isUsableDescription(desc, id, name) {
|
|
403
|
+
const d = desc.toLowerCase().replace(/[^a-z0-9\s]/g, "").trim();
|
|
404
|
+
if (d.length < 20)
|
|
405
|
+
return false;
|
|
406
|
+
if (d === id.toLowerCase() || d === name.toLowerCase())
|
|
407
|
+
return false;
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// provisioning.ts
|
|
412
|
+
import * as fs2 from "node:fs/promises";
|
|
413
|
+
import * as os from "node:os";
|
|
414
|
+
import * as path3 from "node:path";
|
|
415
|
+
|
|
416
|
+
// guidance.ts
|
|
417
|
+
var PLATFORM_AGENTS_MD = `
|
|
418
|
+
## Agent Life — Platform
|
|
419
|
+
|
|
420
|
+
You are a specialist agent on a dashboard app. The user sees ONLY widgets — never chat text.
|
|
421
|
+
|
|
422
|
+
### Output
|
|
423
|
+
|
|
424
|
+
- **User request** → push a widget. Chat text is invisible to the user.
|
|
425
|
+
- **Agent-to-agent message** (sourceSession=agent:*) → reply with text. Push a widget too if there's a user-visible result.
|
|
426
|
+
- **Nothing to surface** → output NO_REPLY.
|
|
427
|
+
|
|
428
|
+
### Widgets
|
|
429
|
+
|
|
430
|
+
A widget is a living surface with a purpose. It exists until the purpose is met, then transitions to terminal. Some live for minutes (a quick answer), some for months (a business goal). The agent decides what each widget needs — goal, followup, detail, context — based on its purpose and lifespan.
|
|
431
|
+
|
|
432
|
+
**Face vs Detail.** The face is a quick glance: metrics, badges, gauges, one-line headers. Prose, analysis, lists, explanations go in \`detail:\`.
|
|
433
|
+
|
|
434
|
+
### surfaceId = Unique Deliverable
|
|
435
|
+
|
|
436
|
+
Each distinct piece of work gets its own surfaceId (e.g. \`yt-{videoId}\`, \`bank-{date}\`, \`task-{slug}\`). Never reuse a generic surfaceId for different tasks — that overwrites previous results. Check the "Current Dashboard State" section in your context: if a widget for the exact same item exists, update it. If it's a different item, new surfaceId.
|
|
437
|
+
|
|
438
|
+
### Loading State
|
|
439
|
+
|
|
440
|
+
Your FIRST tool call on every turn MUST be a widget push with \`state=loading\`. Before any search, fetch, read, or database call. See TOOLS.md for the exact loading structure.
|
|
441
|
+
|
|
442
|
+
Your LAST tool call MUST be the final widget push (without \`state=loading\`). Never end a turn with a loading widget still active.
|
|
443
|
+
|
|
444
|
+
### followup — The Core Engine
|
|
445
|
+
|
|
446
|
+
followup is what makes this system proactive. It's the agent's heartbeat.
|
|
447
|
+
|
|
448
|
+
- Platform schedules a cron automatically from \`followup:\`. Push → cron created. Update with new followup → old cron replaced. Followup removed or \`state=terminal\` → cron killed. Dismiss → platform sends you \`[event:dismissed]\` to clean up.
|
|
449
|
+
- **Always one-shot.** Each followup fires once. You decide the next delay and prompt each cycle with fresh context.
|
|
450
|
+
- **Intentional, not random.** The followup prompt must say exactly what to check and what data to act on. Your session has full continuity — you remember previous turns. context: is your safety net if the session gets compacted.
|
|
451
|
+
- **Cadence matches the domain.** Set the delay based on when the NEXT meaningful event can happen. Food logged → next meal window. Waiting on user approval → check in 2h. Monitoring a metric → check when data updates. Never use a default interval when a domain-specific one exists.
|
|
452
|
+
- **On followup fire:** check the dashboard state engagement stats in your context. User engaged → advance the work. Not viewed and stale → push to terminal. Still relevant → update widget, set new followup.
|
|
453
|
+
- Never create crons directly — \`followup:\` IS your cron mechanism.
|
|
454
|
+
- When user input arrives and updates a widget, the platform replaces the old cron with the new followup. The timer resets automatically.
|
|
455
|
+
|
|
456
|
+
### Widget Interactions
|
|
457
|
+
|
|
458
|
+
Actions live ON the widget. Buttons on widgets send action messages to you. You decide what to do:
|
|
459
|
+
|
|
460
|
+
- **Handle directly** — user clicks [Approve], you execute the action and update the widget.
|
|
461
|
+
- **Need more input** — push an \`input\` surface to expand the input bar with your question and options. The user responds directly to you.
|
|
462
|
+
|
|
463
|
+
The app never decides how to handle an action — you do.
|
|
464
|
+
|
|
465
|
+
\`[action:*]\` messages are always work orders. Process them. Never treat them as terminal signals.
|
|
466
|
+
|
|
467
|
+
### Guided Mode — Input Surfaces
|
|
468
|
+
|
|
469
|
+
When you need structured user input, push a surface with the \`input\` keyword in the header. This expands the user's input bar with your A2UI components (buttons, text fields, anything). The dashboard stays visible — no modals, no overlays.
|
|
470
|
+
|
|
471
|
+
**Flow:**
|
|
472
|
+
1. Push a widget with the question on the face + an action button
|
|
473
|
+
2. User clicks the button → you receive \`[action:*]\`
|
|
474
|
+
3. Push an \`input\` surface with your options:
|
|
475
|
+
\`\`\`
|
|
476
|
+
Single-select (pick one):
|
|
477
|
+
\`\`\`
|
|
478
|
+
surface ask-goal input
|
|
479
|
+
card
|
|
480
|
+
column
|
|
481
|
+
text "What's your goal?" h4
|
|
482
|
+
button "Lose weight" action=choice
|
|
483
|
+
button "Build muscle" action=choice
|
|
484
|
+
button "Maintain" action=choice
|
|
485
|
+
textfield placeholder="Something else"
|
|
486
|
+
\`\`\`
|
|
487
|
+
|
|
488
|
+
Multi-select (toggle several):
|
|
489
|
+
\`\`\`
|
|
490
|
+
surface ask-requirements input
|
|
491
|
+
card
|
|
492
|
+
column
|
|
493
|
+
text "Which features matter?" h4
|
|
494
|
+
button "Offline support" action=toggle
|
|
495
|
+
button "Real-time updates" action=toggle
|
|
496
|
+
button "Low latency" action=toggle
|
|
497
|
+
textfield placeholder="Something else"
|
|
498
|
+
\`\`\`
|
|
499
|
+
4. User responds → you receive \`[action:goal]\` or text input
|
|
500
|
+
5. Delete the input surface when done (push \`delete ask-goal\`)
|
|
501
|
+
|
|
502
|
+
**User controls:**
|
|
503
|
+
- Close [×] = "not now." Input bar returns to free mode. Widget stays. You'll see \`input_closed\` event in engagement stats — push again on next followup if still relevant.
|
|
504
|
+
- Skip = "I don't want this." Widget is dismissed. You'll see \`input_skipped\` — don't ask again.
|
|
505
|
+
|
|
506
|
+
**Rules:**
|
|
507
|
+
- NEVER push \`input\` surfaces unsolicited. Always push a widget first. The user decides when to engage.
|
|
508
|
+
- Input surfaces don't appear on the dashboard — they render in the input bar area.
|
|
509
|
+
- No TTL — input surfaces live until you delete them or the user dismisses.
|
|
510
|
+
- For multi-step flows: push a new input surface (same or different surfaceId) after each step.
|
|
511
|
+
- ALWAYS offer common options as buttons AND a \`textfield\` for custom input. Never make the user type when buttons would work. Never force buttons when the answer is open-ended.
|
|
512
|
+
- The action name controls how the button renders:
|
|
513
|
+
- \`action=choice\` → numbered row (1, 2, 3...). Pick one, fires immediately. Use for single-select.
|
|
514
|
+
- \`action=toggle\` → checkbox row. Click toggles on/off. Use for multi-select.
|
|
515
|
+
- Any other action → regular button.
|
|
516
|
+
- ALWAYS use \`action=choice\` for option lists. Do NOT invent custom action names for options — use \`action=choice\` and differentiate via the button label.
|
|
517
|
+
- Do NOT manually add numbers to labels — the platform auto-numbers choice buttons.
|
|
518
|
+
- Use \`textfield placeholder="..."\` for free text — the placeholder shows in the native input bar. Do NOT use a button that describes typing.
|
|
519
|
+
|
|
520
|
+
### context: — The Agent's Private State
|
|
521
|
+
|
|
522
|
+
context: is your private interpretation of your data. It stores decision state, references, and progress — not raw data. Raw data goes in agentlife.db.
|
|
523
|
+
|
|
524
|
+
context: is private to you. Other agents don't see it. It survives session compaction — it's your durable memory.
|
|
525
|
+
|
|
526
|
+
Rules:
|
|
527
|
+
- \`context:\` fully replaces the previous context. Always re-emit ALL fields.
|
|
528
|
+
- If you omit \`context:\`, the existing context is preserved automatically.
|
|
529
|
+
|
|
530
|
+
### Terminal & Dismiss
|
|
531
|
+
|
|
532
|
+
When the goal is met, push the widget with \`state=terminal\` in the surface header. The platform kills the cron automatically. The widget stays visible with its final result until the user dismisses it. Do NOT include \`followup:\` on a terminal widget.
|
|
533
|
+
|
|
534
|
+
When the user dismisses a widget, the platform deletes the surface and removes the cron, then sends you \`[event:dismissed] surfaceId=<id>\`. The widget is already gone — do NOT push \`delete <id>\`. Clean up any custom infra (scripts, webhooks, background processes). A dismiss is a learning signal — reflect on why the user dismissed it. Say "done" when finished.
|
|
535
|
+
|
|
536
|
+
Never delete a widget silently on your own.
|
|
537
|
+
|
|
538
|
+
### Autonomy & Approval
|
|
539
|
+
|
|
540
|
+
Don't give the user homework. Plan the work yourself, present it for approval (widget with Approve/Reject buttons, full plan in detail:). Once approved, execute fully.
|
|
541
|
+
|
|
542
|
+
Everything persistent requires approval first.
|
|
543
|
+
|
|
544
|
+
### Custom Solutions
|
|
545
|
+
|
|
546
|
+
You have exec access — you're a developer. When a task is better served by a script than repeated LLM sessions, build it. Get approval first, track in \`context.infra\`, show results in a widget, set a followup for health checks, clean up on dismiss.
|
|
547
|
+
|
|
548
|
+
### Cross-Domain
|
|
549
|
+
|
|
550
|
+
When you discover something outside your domain, delegate via \`sessions_send\` with \`sessionKey="agent:{agentId}:main"\` and \`timeoutSeconds=0\`. Include everything — the target agent may not have context on your request.
|
|
551
|
+
|
|
552
|
+
Don't read other agents' data or widget state. Each agent is the single source of truth for its domain.
|
|
553
|
+
|
|
554
|
+
When receiving via sessions_send: push widgets with results. If nothing to push, reply "done". Never re-delegate via sessions_spawn.
|
|
555
|
+
|
|
556
|
+
### Knowledge
|
|
557
|
+
|
|
558
|
+
On every followup: review what you know, update stale entries. After every user interaction: did you learn something new? Store it in context: or agentlife.db.
|
|
559
|
+
`;
|
|
560
|
+
var TENAZITAS_GUIDANCE = `## WidgetDSL — How to Build Widgets
|
|
561
|
+
|
|
562
|
+
### Widget Push Tool
|
|
563
|
+
|
|
564
|
+
Call \`agentlife_push\` with the \`dsl\` parameter containing WidgetDSL (indented plaintext). Never JSON.
|
|
565
|
+
|
|
566
|
+
### Widget Composition — Required Structure
|
|
567
|
+
|
|
568
|
+
Every widget follows this exact nesting. Content directly under card is INVALID and renders broken.
|
|
569
|
+
|
|
570
|
+
\`\`\`
|
|
571
|
+
surface <id> size=s|m|l
|
|
572
|
+
card
|
|
573
|
+
column
|
|
574
|
+
<children here>
|
|
575
|
+
\`\`\`
|
|
576
|
+
|
|
577
|
+
For input surfaces (guided mode — renders in the input bar, not the dashboard):
|
|
578
|
+
\`\`\`
|
|
579
|
+
surface <id> input
|
|
580
|
+
card
|
|
581
|
+
column
|
|
582
|
+
<children here>
|
|
583
|
+
\`\`\`
|
|
584
|
+
|
|
585
|
+
All content goes INSIDE column: text, metric, row, divider, badge, button, progress, gauge, sparkline, pie, barchart. Never place children directly under card — always wrap in column.
|
|
586
|
+
|
|
587
|
+
Rows go inside column. Row children are siblings within the row:
|
|
588
|
+
|
|
589
|
+
\`\`\`
|
|
590
|
+
surface <id> size=m
|
|
591
|
+
card
|
|
592
|
+
column
|
|
593
|
+
text "<title>" h3
|
|
594
|
+
row distribute=spaceBetween
|
|
595
|
+
metric "<label>" "<value>" color=#hex
|
|
596
|
+
metric "<label>" "<value>" color=#hex
|
|
597
|
+
divider
|
|
598
|
+
text "<body text>" body
|
|
599
|
+
\`\`\`
|
|
600
|
+
|
|
601
|
+
Metadata lines go AFTER the component tree, at root level (no indent):
|
|
602
|
+
|
|
603
|
+
\`\`\`
|
|
604
|
+
surface <id> size=m
|
|
605
|
+
card
|
|
606
|
+
column
|
|
607
|
+
<face content>
|
|
608
|
+
goal: <driving completion condition>
|
|
609
|
+
detail:
|
|
610
|
+
<full markdown content>
|
|
611
|
+
followup: +<duration> "<what to check next>"
|
|
612
|
+
context: <JSON state for next session>
|
|
613
|
+
\`\`\`
|
|
614
|
+
|
|
615
|
+
### Two-Phase Push — Loading Then Final
|
|
616
|
+
|
|
617
|
+
Phase 1 — your FIRST tool call on every turn. Before any search, fetch, exec, or db call:
|
|
618
|
+
|
|
619
|
+
\`\`\`
|
|
620
|
+
surface <id> size=s state=loading
|
|
621
|
+
card
|
|
622
|
+
column
|
|
623
|
+
text "<short description of what you are doing>" body
|
|
624
|
+
\`\`\`
|
|
625
|
+
|
|
626
|
+
Phase 2 — your LAST tool call. Same surfaceId, without state=loading:
|
|
627
|
+
|
|
628
|
+
\`\`\`
|
|
629
|
+
surface <id> size=m
|
|
630
|
+
card
|
|
631
|
+
column
|
|
632
|
+
<face: metrics, badges, gauges — 2-second glance only>
|
|
633
|
+
goal: <actionable, measurable condition>
|
|
634
|
+
detail:
|
|
635
|
+
<full markdown: paragraphs, tables, lists, analysis>
|
|
636
|
+
followup: +<duration> "<what to check and why>"
|
|
637
|
+
context: {"key": "value"}
|
|
638
|
+
\`\`\`
|
|
639
|
+
|
|
640
|
+
A loading push without a final push = perpetual spinner.
|
|
641
|
+
|
|
642
|
+
### Terminal State
|
|
643
|
+
|
|
644
|
+
When the goal is met, push with \`state=terminal\` in the surface header. No followup. The widget stays visible until dismissed.
|
|
645
|
+
|
|
646
|
+
### Multiple Surfaces
|
|
647
|
+
|
|
648
|
+
Separate with \`---\` on its own line.
|
|
649
|
+
|
|
650
|
+
### Delete
|
|
651
|
+
|
|
652
|
+
\`delete <id>\` removes a surface.
|
|
653
|
+
|
|
654
|
+
### Components
|
|
655
|
+
|
|
656
|
+
Layout: \`card\`, \`column\`, \`row\`, \`divider\`
|
|
657
|
+
row/column: \`distribute=spaceBetween|spaceAround|spaceEvenly|center\` \`align=center|start|end\`
|
|
658
|
+
|
|
659
|
+
Text: \`text "Content" h1|h2|h3|h4|h5|body|caption color=#hex\`
|
|
660
|
+
|
|
661
|
+
Data:
|
|
662
|
+
\`metric "Label" "Value" trend=up|down|flat color=#hex\`
|
|
663
|
+
\`progress value=0.7 "Label" color=#hex\`
|
|
664
|
+
\`sparkline [1,2,3] height=60 color=#hex filled\`
|
|
665
|
+
\`gauge value=0.85 "Label" color=#hex size=60\`
|
|
666
|
+
|
|
667
|
+
Charts:
|
|
668
|
+
\`pie 45,25,15 labels=["A","B","C"] colors=[#hex,#hex] height=120\`
|
|
669
|
+
\`barchart [42000,28000] labels=["Direct","Organic"] colors=[#hex,#hex]\`
|
|
670
|
+
|
|
671
|
+
Tags: \`badge "Status" color=#hex outlined\`
|
|
672
|
+
Input: \`button "Label" action=<name> primary\`
|
|
673
|
+
|
|
674
|
+
### Size
|
|
675
|
+
|
|
676
|
+
size=s — compact glance. size=m — standard. size=l — rich visualization. Pick the smallest that fits.
|
|
677
|
+
\`priority=high\` — only for genuinely urgent items.
|
|
678
|
+
|
|
679
|
+
### Component Selection
|
|
680
|
+
|
|
681
|
+
Numbers → metric. Completion/budget → progress. Health/score → gauge. Distribution → pie. Comparison → barchart. Status → badge. Time series → sparkline.
|
|
682
|
+
|
|
683
|
+
### context: — The Widget's Brain
|
|
684
|
+
|
|
685
|
+
Sessions may be compacted. context: is your durable state that survives between sessions.
|
|
686
|
+
|
|
687
|
+
Rules:
|
|
688
|
+
- \`context:\` fully replaces the previous context. Always re-emit ALL fields.
|
|
689
|
+
- If you omit \`context:\`, the existing context is preserved automatically — safety net for followup pushes that only update the face.
|
|
690
|
+
|
|
691
|
+
What belongs in context::
|
|
692
|
+
- Decision state — why you chose this approach, what phase you're in, what's next.
|
|
693
|
+
- References — IDs, paths, keys to your data in agentlife.db.
|
|
694
|
+
- Configuration — thresholds, preferences, targets.
|
|
695
|
+
- Infrastructure — \`"infra": [{"type": "script", "name": "...", "path": "..."}]\` for deployed automations.
|
|
696
|
+
|
|
697
|
+
Raw data (meals, metrics, events) goes in agentlife.db, not context:.
|
|
698
|
+
|
|
699
|
+
### Knowledge Strategy — Where to Store What
|
|
700
|
+
|
|
701
|
+
The platform provides two storage systems. Do NOT use OpenClaw memory files (memory_search/memory_get).
|
|
702
|
+
|
|
703
|
+
**Widget context:** — widget-scoped state that drives the next session.
|
|
704
|
+
- \`context:\` fully replaces the previous context — always re-emit all fields.
|
|
705
|
+
- Omitting \`context:\` preserves existing context automatically.
|
|
706
|
+
- Injected into your context every turn (visible in the "Current Dashboard State" section).
|
|
707
|
+
|
|
708
|
+
**agentlife.db (SQLite)** — for data that accumulates over time and outlives any single widget.
|
|
709
|
+
- \`agentlife.db.exec\` / \`agentlife.db.query\` — \`{ agentId, sql, params }\`
|
|
710
|
+
- Food logs, exercise records, expenses, measurements, habits — anything with rows that grow
|
|
711
|
+
- CREATE TABLE IF NOT EXISTS, params array, max 1000 rows per table
|
|
712
|
+
- Query this data in followups to build updated widgets — the DB is the source of truth, context: is the snapshot
|
|
713
|
+
|
|
714
|
+
**How they work together:** agentlife.db stores the raw data (meals, metrics, events). context: stores the widget's current interpretation (totals, trends, next action). On followup, query the DB for fresh data, update the widget, write a new context: snapshot.
|
|
715
|
+
|
|
716
|
+
**Do NOT use:**
|
|
717
|
+
- OpenClaw memory files (memory_search, memory_get, memory/*.md)
|
|
718
|
+
- Workspace files for state
|
|
719
|
+
- Session history as knowledge
|
|
720
|
+
|
|
721
|
+
### Platform Signals
|
|
722
|
+
|
|
723
|
+
ANNOUNCE_SKIP, NO_REPLY, REPLY_SKIP, HEARTBEAT_OK, done — output NO_REPLY and stop.
|
|
724
|
+
\`[action:*]\` messages are work orders — always process them.
|
|
725
|
+
`;
|
|
726
|
+
var ORCHESTRATOR_AGENTS_MD = `# AgentLife Orchestrator
|
|
727
|
+
|
|
728
|
+
You are a message router. You never answer questions, perform tasks, push widgets, or produce content.
|
|
729
|
+
Your job: understand what the user wants → route the message to the right agent.
|
|
730
|
+
|
|
731
|
+
## Rules
|
|
732
|
+
|
|
733
|
+
1. Your tools are sessions_send, sessions_list, Read, memory_search, memory_get, and agents_list. That is ALL. You have no other tools — no exec, no agentlife_push, no web, no cron, no message, no write. Use Read ONLY to extract content from file paths the user sends — then route that content to the right agent.
|
|
734
|
+
2. Every user message gets routed to ONE agent. No exceptions. No fan-out.
|
|
735
|
+
3. NEVER respond NO_REPLY. You always have work to do — route the message.
|
|
736
|
+
4. After calling sessions_send, output "done" and stop. No explanations, no follow-up commentary.
|
|
737
|
+
5. The user sees ONLY the dashboard. Any text you write is invisible to them.
|
|
738
|
+
|
|
739
|
+
## Routing Order
|
|
740
|
+
|
|
741
|
+
Check in this order:
|
|
742
|
+
|
|
743
|
+
### 1. Dashboard — Does a widget exist for this topic?
|
|
744
|
+
Check the "Current Dashboard State" in your context. If a widget matches the user's message, route to the owning agent. That agent updates its existing widget.
|
|
745
|
+
|
|
746
|
+
### 2. Agent descriptions — Which agent handles this domain?
|
|
747
|
+
Check AGENT_REGISTRY.md and agent descriptions. Match the user's intent to the right specialist.
|
|
748
|
+
|
|
749
|
+
### 3. Fallback — No matching agent
|
|
750
|
+
- **Throwaway** ("32 x 44", "what time is it in Tokyo", "convert 5 miles to km") → route to "quick" agent
|
|
751
|
+
- **User data** ("I eat 1 egg", "I'm feeling bad", "track my expenses") → route to "agentlife-builder" to create a domain agent
|
|
752
|
+
|
|
753
|
+
## Button Clicks ([action:*] messages)
|
|
754
|
+
|
|
755
|
+
If the message starts with \`[action:\`, it is a button click — NOT a new user request.
|
|
756
|
+
Extract the surfaceId from the message. Route the ENTIRE original message to the agent that owns that surface. Do not rephrase, interpret, or strip any part of it.
|
|
757
|
+
|
|
758
|
+
## ONE Agent Per Request
|
|
759
|
+
|
|
760
|
+
**Never fan out.** If the message touches multiple domains, pick the primary agent. That agent delegates to others via \`sessions_send\` if it discovers cross-domain needs.
|
|
761
|
+
|
|
762
|
+
Parallel user requests are fine — each message is routed independently.
|
|
763
|
+
|
|
764
|
+
## Delivery
|
|
765
|
+
|
|
766
|
+
Deliver via \`sessions_send\` with parameter **sessionKey**="agent:{agentId}:main", timeoutSeconds=0 (fire-and-forget).
|
|
767
|
+
IMPORTANT: always use the \`sessionKey\` parameter, NEVER the \`label\` parameter. \`label\` does a metadata lookup and fails. \`sessionKey\` routes directly and auto-creates the session if needed.
|
|
768
|
+
|
|
769
|
+
## File Paths
|
|
770
|
+
|
|
771
|
+
When the user sends a file path (starts with \`/\` or \`~\`), use Read to get the contents. Include the content in the message you route — the specialist needs the actual content, not the path.
|
|
772
|
+
|
|
773
|
+
## Image/Photo Attachments
|
|
774
|
+
|
|
775
|
+
You can SEE images the user sends (you have vision). Specialist agents CANNOT — sessions_send only passes text.
|
|
776
|
+
When the user sends a photo, describe what you see in detail and include that description in the message. Be specific and quantitative.
|
|
777
|
+
|
|
778
|
+
## Agent Discovery
|
|
779
|
+
|
|
780
|
+
Your bootstrap context includes AGENT_REGISTRY.md with all registered agents and their descriptions. Use these descriptions to match user intent.
|
|
781
|
+
|
|
782
|
+
If a request doesn't match any registered agent, fall back to \`agents_list\` to discover agents created outside the builder.
|
|
783
|
+
|
|
784
|
+
## Agent Creation
|
|
785
|
+
|
|
786
|
+
If the user asks to create, modify, or improve an agent → route to "agentlife-builder" with sessionKey="agent:agentlife-builder:main".
|
|
787
|
+
|
|
788
|
+
## No Retries
|
|
789
|
+
|
|
790
|
+
- timeoutSeconds: 0 means you always get status "accepted". There is no timeout.
|
|
791
|
+
- Do not retry with a second agent. One intent = one agent.
|
|
792
|
+
- If the USER corrects you ("no, I meant..."), re-route to the correct agent.
|
|
793
|
+
- If sessions_send fails: you probably used \`label\` instead of \`sessionKey\`. Retry with \`sessionKey\`.
|
|
794
|
+
|
|
795
|
+
## Cancelled Requests
|
|
796
|
+
|
|
797
|
+
\`[system:cancelled]\` messages mean the user cancelled. Do NOT re-process. Output NO_REPLY and stop.
|
|
798
|
+
|
|
799
|
+
## Terminal Signals
|
|
800
|
+
|
|
801
|
+
These EXACT standalone tokens are conversation-ending signals. Output NO_REPLY and stop:
|
|
802
|
+
|
|
803
|
+
ANNOUNCE_SKIP, NO_REPLY, REPLY_SKIP, HEARTBEAT_OK, done, \`[system:cancelled]\`
|
|
804
|
+
|
|
805
|
+
**Exception:** \`[action:*]\` messages are WORK ORDERS — always route them.
|
|
806
|
+
|
|
807
|
+
You also output ANNOUNCE_SKIP when asked for an agent-to-agent announce step.
|
|
808
|
+
|
|
809
|
+
## After Routing
|
|
810
|
+
|
|
811
|
+
After calling sessions_send, output "done" and stop. Do NOT:
|
|
812
|
+
- Wait for the agent's response or relay it
|
|
813
|
+
- Respond to the specialist's reply (it is always a terminal signal)
|
|
814
|
+
- Call sessions_send again to the same or different agent
|
|
815
|
+
- Explain what the agent will do
|
|
816
|
+
- Comment on potential issues
|
|
817
|
+
|
|
818
|
+
One user message = one routing decision = one sessions_send = done. The specialist handles everything from here.
|
|
819
|
+
|
|
820
|
+
## What You Are Not
|
|
821
|
+
|
|
822
|
+
- Not a chatbot — no greetings, no small talk, no explanations
|
|
823
|
+
- Not a widget pusher — you NEVER push widgets. You only route.
|
|
824
|
+
- Not a gatekeeper — do not refuse or moderate requests
|
|
825
|
+
- Not a fallback — do not answer if the agent fails; the agent pushes an error widget
|
|
826
|
+
`;
|
|
827
|
+
var QUICK_AGENTS_MD = `# Quick Response Agent
|
|
828
|
+
|
|
829
|
+
You handle throwaway questions — quick answers, calculations, one-off lookups that don't belong to any domain.
|
|
830
|
+
|
|
831
|
+
## What You Do
|
|
832
|
+
|
|
833
|
+
The user asked something quick — math, time zones, definitions, unit conversions, simple facts. Answer via an ephemeral widget and stop.
|
|
834
|
+
|
|
835
|
+
## How to Respond
|
|
836
|
+
|
|
837
|
+
Push a small widget with the answer. Set a short followup to auto-clean:
|
|
838
|
+
|
|
839
|
+
\`\`\`
|
|
840
|
+
surface quick-{slug} size=s
|
|
841
|
+
card
|
|
842
|
+
column
|
|
843
|
+
text "<answer>" body
|
|
844
|
+
followup: +15m "Push this widget to terminal state"
|
|
845
|
+
\`\`\`
|
|
846
|
+
|
|
847
|
+
Then output "done".
|
|
848
|
+
|
|
849
|
+
## Rules
|
|
850
|
+
|
|
851
|
+
- ONE widget push per question.
|
|
852
|
+
- Always restate the question in your answer so the user has context.
|
|
853
|
+
- Match the user's language.
|
|
854
|
+
- Be precise and concise — this is a calculator, not a conversation.
|
|
855
|
+
- If the answer needs markdown formatting, headers, lists, or detailed explanation → output NO_REPLY. The orchestrator will re-route to a specialist.
|
|
856
|
+
- If the question is personal, needs cross-domain data, or would benefit from a long-lived widget → output NO_REPLY.
|
|
857
|
+
- NEVER store data in agentlife.db.
|
|
858
|
+
- NEVER create long-lived widgets with complex goals.
|
|
859
|
+
|
|
860
|
+
## Suggest Domain Agent
|
|
861
|
+
|
|
862
|
+
If a question looks like it belongs to a life domain (health, finance, business), include a suggestion in the widget: "This could benefit from a dedicated agent — just ask me to create one."
|
|
863
|
+
`;
|
|
864
|
+
var BUILDER_AGENTS_MD = `# AgentLife Builder
|
|
865
|
+
|
|
866
|
+
You create and improve agents for the AgentLife dashboard platform.
|
|
867
|
+
|
|
868
|
+
## What an Agent Is
|
|
869
|
+
|
|
870
|
+
An agent = workspace directory + workspace files + a config entry + initialized data.
|
|
871
|
+
|
|
872
|
+
### Workspace Files
|
|
873
|
+
|
|
874
|
+
The gateway loads ALL workspace files into the agent's system prompt at session start. Every file you write reaches the agent. TOOLS.md is the exception — the platform replaces it with the widget/canvas reference at runtime. You still write it as a placeholder.
|
|
875
|
+
|
|
876
|
+
| File | Purpose | When to create |
|
|
877
|
+
|------|---------|----------------|
|
|
878
|
+
| **AGENTS.md** | Domain logic, knowledge strategy, data schemas, environment, operational rules. The core behavioral file. | ALWAYS |
|
|
879
|
+
| **SOUL.md** | Personality and communication style ONLY. Gets wrapped under a "Personality" subsection — keep it to tone, formality, language. One short paragraph. | ALWAYS |
|
|
880
|
+
| **IDENTITY.md** | Agent's self-identity — name, emoji, avatar, vibe. The agent reads this to know who it is. | ALWAYS — fill in agent name, emoji, and personality vibe |
|
|
881
|
+
| **USER.md** | Domain-specific facts about the user that this agent needs. Health gets diet/metrics, home-automation gets network/devices, etc. | When the domain needs user context |
|
|
882
|
+
| **HEARTBEAT.md** | Periodic check instructions. Empty = no heartbeat. Add tasks when the agent should poll. | When the agent needs periodic autonomous checks |
|
|
883
|
+
|
|
884
|
+
**Config entry** — registered via \`agentlife.createAgent\` with tool access and identity.
|
|
885
|
+
**Data schema in AGENTS.md** — CREATE TABLE IF NOT EXISTS statements so the agent self-initializes on first use.
|
|
886
|
+
|
|
887
|
+
## One Agent Per Life Domain
|
|
888
|
+
|
|
889
|
+
Agents map to life domains — not business functions. One business agent handles marketing, sales, operations, analytics — everything. Splitting creates context fragmentation.
|
|
890
|
+
|
|
891
|
+
**Before creating:** ALWAYS check agents_list. If an agent covers the domain, expand its AGENTS.md instead.
|
|
892
|
+
|
|
893
|
+
## Creating a New Agent
|
|
894
|
+
|
|
895
|
+
### Step 1: Understand — Interactive Questions
|
|
896
|
+
|
|
897
|
+
NEVER ask multiple questions in text. First push a widget, then use \`input\` surfaces for ONE question at a time with buttons.
|
|
898
|
+
|
|
899
|
+
1. Push a widget with loading state, then push an \`input\` surface with the first question:
|
|
900
|
+
|
|
901
|
+
\`\`\`
|
|
902
|
+
surface builder-q input
|
|
903
|
+
card
|
|
904
|
+
column
|
|
905
|
+
text "What should this agent focus on?" h4
|
|
906
|
+
button "Research" action=focus-research
|
|
907
|
+
button "Automation" action=focus-automation
|
|
908
|
+
button "Analysis" action=focus-analysis
|
|
909
|
+
\`\`\`
|
|
910
|
+
|
|
911
|
+
2. On button tap → push NEXT question (same surfaceId replaces the input surface).
|
|
912
|
+
3. 2-4 questions total. Then delete the input surface and build.
|
|
913
|
+
|
|
914
|
+
### Step 2: Discover Environment
|
|
915
|
+
|
|
916
|
+
When the domain requires system facts (network, devices, APIs, installed tools), use exec to discover real values. NEVER guess or fabricate system details — IPs, network ranges, service availability, installed software. Always verify.
|
|
917
|
+
|
|
918
|
+
# Network range
|
|
919
|
+
ifconfig | grep "inet "
|
|
920
|
+
# Installed tools
|
|
921
|
+
which nmap python3 mosquitto curl
|
|
922
|
+
# Reachable services
|
|
923
|
+
curl -s http://homeassistant.local:8123/api/ 2>/dev/null && echo "reachable"
|
|
924
|
+
|
|
925
|
+
Include discovered facts in AGENTS.md so the agent has real environment data.
|
|
926
|
+
|
|
927
|
+
### Step 3: Create Workspace + All Files
|
|
928
|
+
|
|
929
|
+
mkdir -p ~/.openclaw/workspace-{agentId}
|
|
930
|
+
|
|
931
|
+
Write ALL workspace files:
|
|
932
|
+
|
|
933
|
+
1. **AGENTS.md** — domain logic, schemas, environment. See "Writing AGENTS.md" below.
|
|
934
|
+
2. **SOUL.md** — one short paragraph: how the agent speaks. Not what it does.
|
|
935
|
+
Good: "Precise and minimal. Confirm actions in one line. Match the user's language."
|
|
936
|
+
Bad: "Device discovery: 1. Check devices.json 2. Run discover.py" — operational, belongs in AGENTS.md.
|
|
937
|
+
3. **IDENTITY.md** — fill in the agent's identity:
|
|
938
|
+
\`\`\`
|
|
939
|
+
# IDENTITY.md — {Agent Name}
|
|
940
|
+
- **Agent ID:** {agentId}
|
|
941
|
+
- **Name:** {Display Name}
|
|
942
|
+
- **Emoji:** {emoji}
|
|
943
|
+
- **Vibe:** {how the agent comes across — sharp? warm? philosophical? precise?}
|
|
944
|
+
- **Avatar:** {path if provided, otherwise omit}
|
|
945
|
+
\`\`\`
|
|
946
|
+
4. **USER.md** — domain-specific user info. Only include facts relevant to THIS agent's domain:
|
|
947
|
+
\`\`\`
|
|
948
|
+
# USER.md — About ACV
|
|
949
|
+
- **Name:** Alberto (ACV)
|
|
950
|
+
- **Timezone:** Europe/Madrid
|
|
951
|
+
- **Language:** Spanish or English — match their language
|
|
952
|
+
## {Domain}-Specific Context
|
|
953
|
+
{Relevant user facts for this domain}
|
|
954
|
+
\`\`\`
|
|
955
|
+
If the domain doesn't need user-specific facts, write a minimal USER.md with just name/timezone/language.
|
|
956
|
+
5. **HEARTBEAT.md** — if the agent needs periodic checks, list them. Otherwise write an empty placeholder:
|
|
957
|
+
\`\`\`
|
|
958
|
+
# HEARTBEAT.md
|
|
959
|
+
# Keep empty to skip heartbeat checks.
|
|
960
|
+
\`\`\`
|
|
961
|
+
|
|
962
|
+
### Step 4: Create Scripts (if needed)
|
|
963
|
+
|
|
964
|
+
If the domain needs automation scripts:
|
|
965
|
+
1. Write the script, then **test it with exec** — run it and verify output before moving on.
|
|
966
|
+
2. Config data (scenes, endpoints, device lists) lives in data files (JSON), not hardcoded in scripts AND AGENTS.md. Single source of truth.
|
|
967
|
+
3. Verify dependencies: \`which nmap\`, \`python3 -c "import json"\`.
|
|
968
|
+
|
|
969
|
+
### Step 5: Register the Agent
|
|
970
|
+
|
|
971
|
+
Call gateway tool with method "agentlife.createAgent":
|
|
972
|
+
\`\`\`json
|
|
973
|
+
{"id": "{agentId}", "name": "Display Name", "model": "{model}", "workspace": "/full/path", "description": "One sentence — specific domains, actions, data types.", "tools": {"profile": "full"}, "identity": {"name": "Display Name", "emoji": "{emoji}"}}
|
|
974
|
+
\`\`\`
|
|
975
|
+
|
|
976
|
+
Use $HOME, not ~. Description is critical — the orchestrator routes by it.
|
|
977
|
+
|
|
978
|
+
**tools config:**
|
|
979
|
+
- \`{"profile": "full"}\` — agent gets all tools (web_search, exec, read, write, agentlife_push, etc.). Use for agents that need web research, script execution, or file operations.
|
|
980
|
+
- \`{"allow": ["agentlife_push"]}\` — restricted to specific tools only. Use for lightweight agents that only push widgets.
|
|
981
|
+
- If the agent needs exec (scripts, image generation), web_search (research), or write (file operations), it MUST have \`profile: "full"\`.
|
|
982
|
+
|
|
983
|
+
**identity config:** Sets the agent's display name and emoji in the platform UI. Match what you wrote in IDENTITY.md.
|
|
984
|
+
|
|
985
|
+
### Step 6: Confirm via Widget
|
|
986
|
+
|
|
987
|
+
Push a confirmation widget that seeds the agent's first context:
|
|
988
|
+
|
|
989
|
+
\`\`\`
|
|
990
|
+
surface agent-created size=m
|
|
991
|
+
card
|
|
992
|
+
column
|
|
993
|
+
text "{Agent Name} is ready" h3
|
|
994
|
+
text "Model: {model} · ID: {agentId}" caption
|
|
995
|
+
button "Try it now" action=try-agent primary
|
|
996
|
+
goal: User sends first request to this agent
|
|
997
|
+
detail: Created agent "{agentId}" for {domain}. The orchestrator will route matching requests here.
|
|
998
|
+
followup: +1h "Check if user has sent a request. If not, push a proactive suggestion based on the domain."
|
|
999
|
+
context: {"agentId":"{agentId}","domain":"{domain}","tables":[list of created tables],"environment":{discovered facts},"status":"ready"}
|
|
1000
|
+
\`\`\`
|
|
1001
|
+
|
|
1002
|
+
## Writing AGENTS.md
|
|
1003
|
+
|
|
1004
|
+
Required sections:
|
|
1005
|
+
- **Role** — what the agent does and doesn't do
|
|
1006
|
+
- **Boundaries** — explicit scope limits
|
|
1007
|
+
- **Environment** — real system facts discovered in Step 2 (IPs, paths, endpoints, available tools)
|
|
1008
|
+
- **Knowledge Strategy** — what goes in context: vs agentlife.db for this domain (see below)
|
|
1009
|
+
- **Data Schema** — CREATE TABLE IF NOT EXISTS statements (agent self-initializes on first use)
|
|
1010
|
+
- **Followup Strategy** — domain-specific check patterns, intervals, deletion triggers
|
|
1011
|
+
- **Goal Patterns** — typical goal: values (concrete, verifiable outcomes)
|
|
1012
|
+
- **Proactive Triggers** — what to discover autonomously, when to delegate
|
|
1013
|
+
- **Approval Patterns** — which actions need user approval
|
|
1014
|
+
- **Widget Structure** — what the face shows (metrics, badges, gauges for this domain)
|
|
1015
|
+
|
|
1016
|
+
### Knowledge Strategy Section
|
|
1017
|
+
|
|
1018
|
+
The platform already injects the general knowledge rules (context: vs agentlife.db vs what to avoid) via TOOLS.md. Do NOT repeat those rules in AGENTS.md.
|
|
1019
|
+
|
|
1020
|
+
AGENTS.md should define the **domain-specific** storage mapping — what concrete data goes where for THIS agent:
|
|
1021
|
+
|
|
1022
|
+
**context: fields for this domain** — list the specific keys:
|
|
1023
|
+
- Health: \`{"date","totals":{"kcal","protein","carbs","fat"},"lastEntry","activeGoals"}\`
|
|
1024
|
+
- Business: \`{"pipelineStage","kpis":{"mrr","churn","leads"},"pendingActions"}\`
|
|
1025
|
+
- Monitoring: \`{"knownDevices":[...],"lastScan","thresholds","infraPaths"}\`
|
|
1026
|
+
|
|
1027
|
+
**agentlife.db tables for this domain** — reference the schemas from Step 5:
|
|
1028
|
+
- Health: meals, exercise_log, measurements — "query meals for today's totals, store in context:"
|
|
1029
|
+
- Business: contacts, deals, activities — "query deals by stage for pipeline widget"
|
|
1030
|
+
|
|
1031
|
+
This tells the agent exactly which keys to write in context: and which tables to query — not the general rules about how context: works (the platform handles that).
|
|
1032
|
+
|
|
1033
|
+
### Guidelines
|
|
1034
|
+
|
|
1035
|
+
1. **Specific, not generic.** "Search flights on Google Flights" > "Help with travel."
|
|
1036
|
+
2. **Tool-first.** Write instructions around available tools, not abstract behaviors.
|
|
1037
|
+
3. **Real data.** Include IPs, paths, endpoints from Step 2 — never placeholders or guesses.
|
|
1038
|
+
4. **Error paths.** What happens when tools fail?
|
|
1039
|
+
5. **Concise but complete.** Cover the full domain — don't sacrifice completeness for brevity.
|
|
1040
|
+
|
|
1041
|
+
## Registry Enrichment
|
|
1042
|
+
|
|
1043
|
+
When you receive "Enrich agent registry descriptions" — system task, no widgets.
|
|
1044
|
+
|
|
1045
|
+
For each agent: read workspace AGENTS.md + SOUL.md → write one-sentence description via agentlife.createAgent. Specific domains, actions, data types. Process all, respond "Done".
|
|
1046
|
+
|
|
1047
|
+
## Deleting an Agent
|
|
1048
|
+
|
|
1049
|
+
1. agentlife.deleteAgent: {"id": "{agentId}"}
|
|
1050
|
+
2. Optionally: exec rm -rf ~/.openclaw/workspace-{agentId}
|
|
1051
|
+
3. Push confirmation widget.
|
|
1052
|
+
|
|
1053
|
+
## Improving Existing Agents
|
|
1054
|
+
|
|
1055
|
+
1. Read ALL workspace files (AGENTS.md, SOUL.md, IDENTITY.md, USER.md, HEARTBEAT.md)
|
|
1056
|
+
2. Query agentlife.quality + agentlife.trace for actual quality issues
|
|
1057
|
+
3. Audit:
|
|
1058
|
+
- **Workspace files complete?** All files present? IDENTITY.md filled in?
|
|
1059
|
+
- **Config complete?** Agent has \`tools\` config? (\`profile: "full"\` or appropriate \`allow\` list)
|
|
1060
|
+
- Knowledge strategy defined? (context: vs agentlife.db split)
|
|
1061
|
+
- DB schema in AGENTS.md matches what the agent actually creates?
|
|
1062
|
+
- Followup strategy domain-appropriate?
|
|
1063
|
+
- Widget context quality — are agents writing complete snapshots?
|
|
1064
|
+
- Environment facts still accurate? (re-discover if stale)
|
|
1065
|
+
- Proactive behaviors producing value?
|
|
1066
|
+
4. Targeted edits — don't rewrite unless fundamentally broken
|
|
1067
|
+
5. Write updated files back. If missing workspace files, create them.
|
|
1068
|
+
6. If config is missing \`tools\`, call \`agentlife.createAgent\` with the existing id + the missing config fields (it patches existing agents).
|
|
1069
|
+
7. Show diff summary via widget
|
|
1070
|
+
|
|
1071
|
+
## What You Are Not
|
|
1072
|
+
|
|
1073
|
+
- Not an orchestrator — you don't route messages
|
|
1074
|
+
- Not a general assistant — only agent creation and improvement
|
|
1075
|
+
`;
|
|
1076
|
+
function buildPlatformSoulMd(userContent) {
|
|
1077
|
+
const userSection = userContent.trim() ? `
|
|
1078
|
+
---
|
|
1079
|
+
|
|
1080
|
+
## Personality
|
|
1081
|
+
|
|
1082
|
+
${userContent.trim()}
|
|
1083
|
+
` : "";
|
|
1084
|
+
return `# Agent Life Voice
|
|
1085
|
+
|
|
1086
|
+
Your personality shows in widget detail: sections — that is where your voice lives. The widget face is data-driven and impersonal.
|
|
1087
|
+
${userSection}`;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// provisioning.ts
|
|
1091
|
+
var PROVISIONED_AGENTS = [
|
|
1092
|
+
{
|
|
1093
|
+
id: "agentlife",
|
|
1094
|
+
name: "AgentLife",
|
|
1095
|
+
isDefault: true,
|
|
1096
|
+
agentsMd: ORCHESTRATOR_AGENTS_MD,
|
|
1097
|
+
subagents: { allowAgents: ["*"] },
|
|
1098
|
+
tools: {
|
|
1099
|
+
allow: ["sessions_send", "sessions_list", "read", "memory_search", "memory_get", "agents_list"]
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
id: "quick",
|
|
1104
|
+
name: "Quick",
|
|
1105
|
+
agentsMd: QUICK_AGENTS_MD
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
id: "agentlife-builder",
|
|
1109
|
+
name: "AgentLife Builder",
|
|
1110
|
+
agentsMd: BUILDER_AGENTS_MD
|
|
1111
|
+
}
|
|
1112
|
+
];
|
|
1113
|
+
function scheduleEnrichment(state, runtime, targets, log) {
|
|
1114
|
+
if (!runtime.system?.runCommandWithTimeout) {
|
|
1115
|
+
log("[agentlife] enrichment skipped — runtime.system not available");
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const run = runtime.system.runCommandWithTimeout;
|
|
1119
|
+
setTimeout(async () => {
|
|
1120
|
+
log(`[agentlife] starting registry enrichment for ${targets.length} agents`);
|
|
1121
|
+
const promises = targets.map(async (t) => {
|
|
1122
|
+
const msg = [
|
|
1123
|
+
`Write a one-sentence description for agent "${t.name}" (id: ${t.id}).`,
|
|
1124
|
+
`The description must capture what domains/topics the agent handles and what actions it performs.`,
|
|
1125
|
+
`Reply with ONLY the description sentence — no preamble, no quotes, no explanation.`,
|
|
1126
|
+
``,
|
|
1127
|
+
`SOUL.md:`,
|
|
1128
|
+
"```",
|
|
1129
|
+
t.soulMd || "(empty)",
|
|
1130
|
+
"```",
|
|
1131
|
+
``,
|
|
1132
|
+
`AGENTS.md:`,
|
|
1133
|
+
"```",
|
|
1134
|
+
t.agentsMd || "(empty)",
|
|
1135
|
+
"```"
|
|
1136
|
+
].join(`
|
|
1137
|
+
`);
|
|
1138
|
+
try {
|
|
1139
|
+
const result = await run(["openclaw", "agent", "--agent", "agentlife-builder", "--message", msg, "--json"], { timeoutMs: 60000 });
|
|
1140
|
+
const stdout = result?.stdout ?? "";
|
|
1141
|
+
let description = "";
|
|
1142
|
+
try {
|
|
1143
|
+
const json = JSON.parse(stdout);
|
|
1144
|
+
const payloads = json?.result?.payloads;
|
|
1145
|
+
if (Array.isArray(payloads)) {
|
|
1146
|
+
description = payloads.map((p) => p.text ?? "").join(" ").trim();
|
|
1147
|
+
}
|
|
1148
|
+
} catch {}
|
|
1149
|
+
if (description && isUsableDescription(description, t.id, t.name)) {
|
|
1150
|
+
state.agentRegistry.set(t.id, {
|
|
1151
|
+
name: t.name,
|
|
1152
|
+
description,
|
|
1153
|
+
model: t.model,
|
|
1154
|
+
createdAt: state.agentRegistry.get(t.id)?.createdAt ?? Date.now()
|
|
1155
|
+
});
|
|
1156
|
+
await saveRegistryToDisk(state);
|
|
1157
|
+
log(`[agentlife] enriched ${t.id}: ${description}`);
|
|
1158
|
+
} else {
|
|
1159
|
+
log(`[agentlife] enrichment for ${t.id} produced unusable description: ${description || "(empty)"}`);
|
|
1160
|
+
}
|
|
1161
|
+
} catch (err) {
|
|
1162
|
+
log(`[agentlife] enrichment failed for ${t.id} (non-critical): ${err?.message}`);
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
await Promise.all(promises);
|
|
1166
|
+
log("[agentlife] enrichment complete");
|
|
1167
|
+
}, 5000);
|
|
1168
|
+
}
|
|
1169
|
+
async function provisionAgents(state, cfg, runtime, log) {
|
|
1170
|
+
const home = os.homedir();
|
|
1171
|
+
const currentList = [...cfg.agents?.list ?? []];
|
|
1172
|
+
let configChanged = false;
|
|
1173
|
+
for (const agent of PROVISIONED_AGENTS) {
|
|
1174
|
+
const workspaceDir = agent.workspaceDir ?? path3.join(home, ".openclaw", `workspace-${agent.id}`);
|
|
1175
|
+
await fs2.mkdir(workspaceDir, { recursive: true });
|
|
1176
|
+
await fs2.writeFile(path3.join(workspaceDir, "AGENTS.md"), agent.agentsMd, "utf-8");
|
|
1177
|
+
if (!agent.existingAgent) {
|
|
1178
|
+
const stubPath = path3.join(workspaceDir, "SOUL.md");
|
|
1179
|
+
await fs2.writeFile(stubPath, "", { encoding: "utf-8", flag: "wx" }).catch(() => {});
|
|
1180
|
+
}
|
|
1181
|
+
if (agent.existingAgent)
|
|
1182
|
+
continue;
|
|
1183
|
+
const exists = currentList.some((a) => a.id === agent.id);
|
|
1184
|
+
if (!exists) {
|
|
1185
|
+
const entry = {
|
|
1186
|
+
id: agent.id,
|
|
1187
|
+
name: agent.name,
|
|
1188
|
+
workspace: workspaceDir
|
|
1189
|
+
};
|
|
1190
|
+
if (agent.isDefault)
|
|
1191
|
+
entry.default = true;
|
|
1192
|
+
if (agent.subagents)
|
|
1193
|
+
entry.subagents = agent.subagents;
|
|
1194
|
+
if (agent.tools)
|
|
1195
|
+
entry.tools = agent.tools;
|
|
1196
|
+
if (agent.isDefault) {
|
|
1197
|
+
currentList.unshift(entry);
|
|
1198
|
+
} else {
|
|
1199
|
+
currentList.push(entry);
|
|
1200
|
+
}
|
|
1201
|
+
configChanged = true;
|
|
1202
|
+
log(`[agentlife] provisioned agent: ${agent.id}`);
|
|
1203
|
+
} else if (agent.tools) {
|
|
1204
|
+
const existing = currentList.find((a) => a.id === agent.id);
|
|
1205
|
+
if (existing) {
|
|
1206
|
+
const currentAllow = existing.tools?.allow;
|
|
1207
|
+
const wantAllow = agent.tools.allow;
|
|
1208
|
+
const needsUpdate = !currentAllow || currentAllow.length !== wantAllow?.length || !wantAllow?.every((t) => currentAllow.includes(t));
|
|
1209
|
+
if (needsUpdate) {
|
|
1210
|
+
existing.tools = agent.tools;
|
|
1211
|
+
configChanged = true;
|
|
1212
|
+
log(`[agentlife] updated tool restrictions for ${agent.id}`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (configChanged) {
|
|
1218
|
+
const updatedCfg = {
|
|
1219
|
+
...cfg,
|
|
1220
|
+
agents: {
|
|
1221
|
+
...cfg.agents,
|
|
1222
|
+
list: currentList
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
await runtime.config.writeConfigFile(updatedCfg);
|
|
1226
|
+
log("[agentlife] config updated with provisioned agents");
|
|
1227
|
+
}
|
|
1228
|
+
const SKIP_IDS = new Set(PROVISIONED_AGENTS.filter((a) => !a.existingAgent).map((a) => a.id));
|
|
1229
|
+
const finalList = runtime.config.loadConfig().agents?.list ?? currentList;
|
|
1230
|
+
const configIds = new Set(finalList.map((a) => a?.id).filter(Boolean));
|
|
1231
|
+
let seeded = 0;
|
|
1232
|
+
let pruned = 0;
|
|
1233
|
+
for (const id of state.agentRegistry.keys()) {
|
|
1234
|
+
if (SKIP_IDS.has(id))
|
|
1235
|
+
continue;
|
|
1236
|
+
if (!configIds.has(id)) {
|
|
1237
|
+
state.agentRegistry.delete(id);
|
|
1238
|
+
pruned++;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
for (const agent of finalList) {
|
|
1242
|
+
const id = agent?.id;
|
|
1243
|
+
if (!id || SKIP_IDS.has(id) || state.agentRegistry.has(id))
|
|
1244
|
+
continue;
|
|
1245
|
+
const name = agent.name || id;
|
|
1246
|
+
state.agentRegistry.set(id, {
|
|
1247
|
+
name,
|
|
1248
|
+
description: name,
|
|
1249
|
+
model: agent.model || undefined,
|
|
1250
|
+
createdAt: Date.now(),
|
|
1251
|
+
needsEnrichment: true
|
|
1252
|
+
});
|
|
1253
|
+
seeded++;
|
|
1254
|
+
}
|
|
1255
|
+
if (seeded > 0 || pruned > 0) {
|
|
1256
|
+
await saveRegistryToDisk(state);
|
|
1257
|
+
if (seeded > 0)
|
|
1258
|
+
log(`[agentlife] seeded registry with ${seeded} existing agents`);
|
|
1259
|
+
if (pruned > 0)
|
|
1260
|
+
log(`[agentlife] pruned ${pruned} stale agents from registry`);
|
|
1261
|
+
}
|
|
1262
|
+
const enrichTargets = [];
|
|
1263
|
+
for (const [id, entry] of state.agentRegistry.entries()) {
|
|
1264
|
+
if (!entry.needsEnrichment)
|
|
1265
|
+
continue;
|
|
1266
|
+
const agent = finalList.find((a) => a?.id === id);
|
|
1267
|
+
const workspace = agent?.workspace || "";
|
|
1268
|
+
let soulMd = "";
|
|
1269
|
+
let agentsMd = "";
|
|
1270
|
+
if (workspace) {
|
|
1271
|
+
try {
|
|
1272
|
+
soulMd = await fs2.readFile(path3.join(workspace, "SOUL.md"), "utf-8");
|
|
1273
|
+
} catch {}
|
|
1274
|
+
try {
|
|
1275
|
+
agentsMd = await fs2.readFile(path3.join(workspace, "AGENTS.md"), "utf-8");
|
|
1276
|
+
} catch {}
|
|
1277
|
+
}
|
|
1278
|
+
enrichTargets.push({ id, name: entry.name, workspace, model: entry.model, soulMd, agentsMd });
|
|
1279
|
+
}
|
|
1280
|
+
if (enrichTargets.length > 0) {
|
|
1281
|
+
scheduleEnrichment(state, runtime, enrichTargets, log);
|
|
1282
|
+
}
|
|
1283
|
+
log("[agentlife] agent provisioning complete");
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// services/surfaces-init.ts
|
|
1287
|
+
import * as path4 from "node:path";
|
|
1288
|
+
|
|
1289
|
+
// activity.ts
|
|
1290
|
+
function recordSurfaceEvent(state, surfaceId, event, dsl, agentId, metadata) {
|
|
1291
|
+
try {
|
|
1292
|
+
getOrCreateHistoryDb(state).prepare("INSERT INTO surface_events (surfaceId,agentId,event,dsl,metadata,createdAt) VALUES (?,?,?,?,?,?)").run(surfaceId, agentId ?? null, event, dsl ?? null, metadata ?? null, Date.now());
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
console.warn("[agentlife] failed to record surface event:", err?.message);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
function recordActivity(state, event, sessionKey, agentId, opts) {
|
|
1298
|
+
try {
|
|
1299
|
+
getOrCreateHistoryDb(state).prepare("INSERT INTO activity_log (ts,sessionKey,agentId,event,toolName,summary,data,runId) VALUES (?,?,?,?,?,?,?,?)").run(Date.now(), sessionKey ?? null, agentId ?? null, event, opts?.toolName ?? null, opts?.summary ?? null, opts?.data ?? null, opts?.runId ?? null);
|
|
1300
|
+
} catch (err) {
|
|
1301
|
+
console.warn("[agentlife] failed to record activity:", err?.message);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
function summarizeParams(params) {
|
|
1305
|
+
if (!params)
|
|
1306
|
+
return null;
|
|
1307
|
+
const filePath = params.file_path ?? params.path;
|
|
1308
|
+
if (typeof filePath === "string")
|
|
1309
|
+
return filePath;
|
|
1310
|
+
const command = params.command;
|
|
1311
|
+
if (typeof command === "string")
|
|
1312
|
+
return command.slice(0, 200);
|
|
1313
|
+
const pattern = params.pattern;
|
|
1314
|
+
if (typeof pattern === "string")
|
|
1315
|
+
return pattern;
|
|
1316
|
+
const url = params.url;
|
|
1317
|
+
if (typeof url === "string")
|
|
1318
|
+
return url;
|
|
1319
|
+
const query = params.query;
|
|
1320
|
+
if (typeof query === "string")
|
|
1321
|
+
return query;
|
|
1322
|
+
const action = params.action;
|
|
1323
|
+
if (typeof action === "string")
|
|
1324
|
+
return action;
|
|
1325
|
+
return null;
|
|
1326
|
+
}
|
|
1327
|
+
function registerAutomation(state, surfaceId, agentId, type, name, automationPath) {
|
|
1328
|
+
const db = getOrCreateHistoryDb(state);
|
|
1329
|
+
const now = Date.now();
|
|
1330
|
+
let existing = null;
|
|
1331
|
+
if (surfaceId) {
|
|
1332
|
+
existing = db.prepare("SELECT id FROM automations WHERE surfaceId = ? AND path = ? AND status != 'removed'").get(surfaceId, automationPath);
|
|
1333
|
+
}
|
|
1334
|
+
if (!existing && agentId) {
|
|
1335
|
+
existing = db.prepare("SELECT id FROM automations WHERE agentId = ? AND path = ? AND status != 'removed'").get(agentId, automationPath);
|
|
1336
|
+
}
|
|
1337
|
+
if (existing) {
|
|
1338
|
+
db.prepare("UPDATE automations SET surfaceId = ?, agentId = ?, type = ?, name = ?, status = 'running', updatedAt = ? WHERE id = ?").run(surfaceId, agentId, type, name, now, existing.id);
|
|
1339
|
+
return existing.id;
|
|
1340
|
+
}
|
|
1341
|
+
const result = db.prepare("INSERT INTO automations (surfaceId, agentId, type, name, path, status, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, 'running', ?, ?)").run(surfaceId, agentId, type, name, automationPath, now, now);
|
|
1342
|
+
return Number(result.lastInsertRowid);
|
|
1343
|
+
}
|
|
1344
|
+
function autoRegisterInfraFromContext(state, surfaceId, agentId, context) {
|
|
1345
|
+
const infra = context.infra;
|
|
1346
|
+
if (!Array.isArray(infra))
|
|
1347
|
+
return;
|
|
1348
|
+
for (const item of infra) {
|
|
1349
|
+
if (typeof item !== "object" || !item)
|
|
1350
|
+
continue;
|
|
1351
|
+
const type = typeof item.type === "string" ? item.type : "";
|
|
1352
|
+
const itemPath = typeof item.path === "string" ? item.path : "";
|
|
1353
|
+
if (!type || !itemPath)
|
|
1354
|
+
continue;
|
|
1355
|
+
const name = typeof item.name === "string" ? item.name : `${type} for ${surfaceId}`;
|
|
1356
|
+
registerAutomation(state, surfaceId, agentId, type, name, itemPath);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// dashboard-state.ts
|
|
1361
|
+
function extractTitleAndDetail(meta) {
|
|
1362
|
+
let title = null;
|
|
1363
|
+
let detail = null;
|
|
1364
|
+
const fullText = meta.lines.join(`
|
|
1365
|
+
`);
|
|
1366
|
+
const titleMatch = fullText.match(/text\s+"([^"]+)"\s+h[34]/);
|
|
1367
|
+
if (titleMatch)
|
|
1368
|
+
title = titleMatch[1];
|
|
1369
|
+
if (!title) {
|
|
1370
|
+
const bodyMatch = fullText.match(/text\s+"([^"]+)"/);
|
|
1371
|
+
if (bodyMatch)
|
|
1372
|
+
title = bodyMatch[1];
|
|
1373
|
+
}
|
|
1374
|
+
const inlineDetail = fullText.match(/^\s*detail:\s*(.+)/m);
|
|
1375
|
+
if (inlineDetail && inlineDetail[1].trim().length > 0) {
|
|
1376
|
+
detail = inlineDetail[1].trim().replace(/^"|"$/g, "");
|
|
1377
|
+
} else {
|
|
1378
|
+
const detailBlockMatch = fullText.match(/^\s*detail:\s*\n((?:\s+.+\n?)+)/m);
|
|
1379
|
+
if (detailBlockMatch) {
|
|
1380
|
+
detail = detailBlockMatch[1].replace(/^ {2,4}/gm, "").trim();
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return { title, detail };
|
|
1384
|
+
}
|
|
1385
|
+
function buildDashboardStateContext(state, agentId) {
|
|
1386
|
+
if (!state.surfaceDb)
|
|
1387
|
+
return null;
|
|
1388
|
+
const now = Date.now();
|
|
1389
|
+
let engagementMap = new Map;
|
|
1390
|
+
try {
|
|
1391
|
+
const db = getOrCreateHistoryDb(state);
|
|
1392
|
+
const rows = db.prepare(`
|
|
1393
|
+
SELECT surfaceId,
|
|
1394
|
+
SUM(CASE WHEN event = 'detail_viewed' THEN 1 ELSE 0 END) as viewCount,
|
|
1395
|
+
MAX(CASE WHEN event = 'detail_viewed' THEN createdAt ELSE 0 END) as lastViewedAt
|
|
1396
|
+
FROM surface_events
|
|
1397
|
+
WHERE event IN ('detail_viewed', 'action_clicked')
|
|
1398
|
+
GROUP BY surfaceId
|
|
1399
|
+
`).all();
|
|
1400
|
+
for (const row of rows) {
|
|
1401
|
+
engagementMap.set(row.surfaceId, { viewed: row.viewCount, lastViewedAt: row.lastViewedAt });
|
|
1402
|
+
}
|
|
1403
|
+
} catch {}
|
|
1404
|
+
const cronInfo = new Map;
|
|
1405
|
+
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
1406
|
+
if (meta.cronId && meta.followup) {
|
|
1407
|
+
cronInfo.set(surfaceId, { cronId: meta.cronId, followup: meta.followup });
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
let hasActiveSurfaces = false;
|
|
1411
|
+
for (const [, meta] of state.surfaceDb.entries()) {
|
|
1412
|
+
if (!isExpired(meta, now)) {
|
|
1413
|
+
hasActiveSurfaces = true;
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (state.surfaceDb.size === 0 || !hasActiveSurfaces) {
|
|
1418
|
+
let result2 = `## Dashboard is Empty
|
|
1419
|
+
|
|
1420
|
+
No active widgets. Wait for user input.
|
|
1421
|
+
`;
|
|
1422
|
+
if (state.surfaceDb.size > 0) {
|
|
1423
|
+
const expiredEntries2 = [];
|
|
1424
|
+
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
1425
|
+
const { title } = extractTitleAndDetail(meta);
|
|
1426
|
+
if (isExpired(meta, now) && !meta.expiredSince) {
|
|
1427
|
+
meta.expiredSince = now;
|
|
1428
|
+
}
|
|
1429
|
+
expiredEntries2.push(`[${surfaceId}] ${title ?? surfaceId} (updated ${formatAge(now - meta.updatedAt)}, EXPIRED)`);
|
|
1430
|
+
}
|
|
1431
|
+
if (expiredEntries2.length > 0) {
|
|
1432
|
+
result2 += `
|
|
1433
|
+
### Recently Expired
|
|
1434
|
+
|
|
1435
|
+
${expiredEntries2.join(`
|
|
1436
|
+
`)}`;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return result2;
|
|
1440
|
+
}
|
|
1441
|
+
const priorityOrder = { high: 0, normal: 1, low: 2 };
|
|
1442
|
+
const activeEntries = [];
|
|
1443
|
+
const expiredEntries = [];
|
|
1444
|
+
const isOrchestrator = agentId === "agentlife";
|
|
1445
|
+
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
1446
|
+
const expired = isExpired(meta, now);
|
|
1447
|
+
if (expired && !meta.expiredSince) {
|
|
1448
|
+
meta.expiredSince = now;
|
|
1449
|
+
}
|
|
1450
|
+
const owner = state.surfaceDb.getAgentId(surfaceId);
|
|
1451
|
+
const isOwnWidget = !agentId || owner === agentId;
|
|
1452
|
+
const { title } = extractTitleAndDetail(meta);
|
|
1453
|
+
const label = title ?? surfaceId;
|
|
1454
|
+
const age = formatAge(now - meta.updatedAt);
|
|
1455
|
+
const headerLine = meta.lines[0] ?? "";
|
|
1456
|
+
const isOverlay = /\boverlay\b/.test(headerLine);
|
|
1457
|
+
const sizeMatch = headerLine.match(/\bsize=(\w+)/);
|
|
1458
|
+
const priorityMatch = headerLine.match(/\bpriority=(\w+)/);
|
|
1459
|
+
const priority = priorityOrder[priorityMatch?.[1] ?? "normal"] ?? 1;
|
|
1460
|
+
const parts = [];
|
|
1461
|
+
if (isOverlay)
|
|
1462
|
+
parts.push("OVERLAY — user is seeing this right now");
|
|
1463
|
+
if (owner)
|
|
1464
|
+
parts.push(`by ${owner}`);
|
|
1465
|
+
if (sizeMatch)
|
|
1466
|
+
parts.push(`size=${sizeMatch[1]}`);
|
|
1467
|
+
if (meta.goal)
|
|
1468
|
+
parts.push(`goal: "${meta.goal}"`);
|
|
1469
|
+
parts.push(`updated ${age}`);
|
|
1470
|
+
const cron = cronInfo.get(surfaceId);
|
|
1471
|
+
if (cron) {
|
|
1472
|
+
parts.push(`followup: ${meta.followup} (cronId: ${cron.cronId})`);
|
|
1473
|
+
} else if (meta.followup) {
|
|
1474
|
+
parts.push(`followup: ${meta.followup} (pending)`);
|
|
1475
|
+
} else {
|
|
1476
|
+
parts.push("no followup");
|
|
1477
|
+
}
|
|
1478
|
+
if (meta.state && meta.state !== "active") {
|
|
1479
|
+
parts.push(`state=${meta.state}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (expired) {
|
|
1482
|
+
parts.push("EXPIRED");
|
|
1483
|
+
}
|
|
1484
|
+
const eng = engagementMap.get(surfaceId);
|
|
1485
|
+
if (eng && eng.viewed > 0) {
|
|
1486
|
+
parts.push(`viewed ${eng.viewed}x`);
|
|
1487
|
+
} else {
|
|
1488
|
+
parts.push("not viewed");
|
|
1489
|
+
}
|
|
1490
|
+
let contextLine = "";
|
|
1491
|
+
if (meta.context && isOwnWidget && !isOrchestrator) {
|
|
1492
|
+
contextLine = `
|
|
1493
|
+
context: ${JSON.stringify(meta.context)}`;
|
|
1494
|
+
}
|
|
1495
|
+
const entry = `[${surfaceId}] ${label} (${parts.join(", ")})${contextLine}`;
|
|
1496
|
+
if (expired) {
|
|
1497
|
+
expiredEntries.push(entry);
|
|
1498
|
+
} else {
|
|
1499
|
+
activeEntries.push({ entry, priority, updatedAt: meta.updatedAt });
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
activeEntries.sort((a, b) => a.priority - b.priority || b.updatedAt - a.updatedAt);
|
|
1503
|
+
if (activeEntries.length === 0 && expiredEntries.length === 0)
|
|
1504
|
+
return null;
|
|
1505
|
+
let result = `## Current Dashboard State
|
|
1506
|
+
|
|
1507
|
+
Every widget on the user's dashboard, sorted by recency (most recently updated first). Do NOT create widgets that overlap with existing ones — update or skip instead.
|
|
1508
|
+
`;
|
|
1509
|
+
if (activeEntries.length > 0) {
|
|
1510
|
+
result += `
|
|
1511
|
+
### Active Widgets
|
|
1512
|
+
|
|
1513
|
+
${activeEntries.map((e) => e.entry).join(`
|
|
1514
|
+
|
|
1515
|
+
`)}`;
|
|
1516
|
+
}
|
|
1517
|
+
if (expiredEntries.length > 0) {
|
|
1518
|
+
result += `
|
|
1519
|
+
|
|
1520
|
+
### Expired (delete or refresh)
|
|
1521
|
+
|
|
1522
|
+
${expiredEntries.join(`
|
|
1523
|
+
|
|
1524
|
+
`)}`;
|
|
1525
|
+
}
|
|
1526
|
+
if (agentId) {
|
|
1527
|
+
try {
|
|
1528
|
+
const db = getOrCreateHistoryDb(state);
|
|
1529
|
+
const sevenDaysAgo = now - 7 * 24 * 60 * 60 * 1000;
|
|
1530
|
+
const stats = db.prepare(`
|
|
1531
|
+
SELECT
|
|
1532
|
+
SUM(CASE WHEN event = 'created' THEN 1 ELSE 0 END) as pushed,
|
|
1533
|
+
SUM(CASE WHEN event = 'detail_viewed' THEN 1 ELSE 0 END) as viewed,
|
|
1534
|
+
SUM(CASE WHEN event = 'dismissed' THEN 1 ELSE 0 END) as dismissed,
|
|
1535
|
+
SUM(CASE WHEN event = 'expired' THEN 1 ELSE 0 END) as expired,
|
|
1536
|
+
SUM(CASE WHEN event = 'quality_warning' THEN 1 ELSE 0 END) as warnings
|
|
1537
|
+
FROM surface_events
|
|
1538
|
+
WHERE agentId = ? AND createdAt > ?
|
|
1539
|
+
`).get(agentId, sevenDaysAgo);
|
|
1540
|
+
if (stats && (stats.pushed > 0 || stats.warnings > 0)) {
|
|
1541
|
+
let perfLine = `Pushed: ${stats.pushed ?? 0} | Viewed: ${stats.viewed ?? 0} | Dismissed: ${stats.dismissed ?? 0} | Expired: ${stats.expired ?? 0} | Quality warnings: ${stats.warnings ?? 0}`;
|
|
1542
|
+
if ((stats.dismissed ?? 0) > 0) {
|
|
1543
|
+
try {
|
|
1544
|
+
const reasons = db.prepare(`
|
|
1545
|
+
SELECT
|
|
1546
|
+
json_extract(metadata, '$.reason') as reason,
|
|
1547
|
+
COUNT(*) as cnt
|
|
1548
|
+
FROM surface_events
|
|
1549
|
+
WHERE agentId = ? AND event = 'dismissed' AND metadata IS NOT NULL AND createdAt > ?
|
|
1550
|
+
GROUP BY reason ORDER BY cnt DESC
|
|
1551
|
+
`).all(agentId, sevenDaysAgo);
|
|
1552
|
+
if (reasons?.length > 0) {
|
|
1553
|
+
const reasonParts = reasons.map((r) => `${r.reason ?? "unknown"}: ${r.cnt}`);
|
|
1554
|
+
perfLine += `
|
|
1555
|
+
Dismiss reasons: ${reasonParts.join(", ")}`;
|
|
1556
|
+
}
|
|
1557
|
+
} catch {}
|
|
1558
|
+
}
|
|
1559
|
+
result += `
|
|
1560
|
+
|
|
1561
|
+
## Your Performance (7 days)
|
|
1562
|
+
${perfLine}`;
|
|
1563
|
+
}
|
|
1564
|
+
} catch {}
|
|
1565
|
+
}
|
|
1566
|
+
if (agentId) {
|
|
1567
|
+
try {
|
|
1568
|
+
const db = getOrCreateHistoryDb(state);
|
|
1569
|
+
const autos = db.prepare("SELECT * FROM automations WHERE agentId = ? AND status != 'removed' ORDER BY updatedAt DESC").all(agentId);
|
|
1570
|
+
if (autos.length > 0) {
|
|
1571
|
+
const autoLines = autos.map((a) => {
|
|
1572
|
+
const widgetRef = a.surfaceId ? `, widget=${a.surfaceId}` : "";
|
|
1573
|
+
const statusTag = a.status !== "running" ? ` STATUS: ${a.status.toUpperCase()}` : "";
|
|
1574
|
+
return `[auto:${a.id}] ${a.name} (type=${a.type}, path=${a.path}${widgetRef}, ${a.status}${statusTag})`;
|
|
1575
|
+
});
|
|
1576
|
+
result += `
|
|
1577
|
+
|
|
1578
|
+
### Your Automations
|
|
1579
|
+
|
|
1580
|
+
${autoLines.join(`
|
|
1581
|
+
`)}`;
|
|
1582
|
+
}
|
|
1583
|
+
const orphaned = autos.filter((a) => a.surfaceId && !state.surfaceDb.has(a.surfaceId) && a.status === "running");
|
|
1584
|
+
if (orphaned.length > 0) {
|
|
1585
|
+
const orphanLines = orphaned.map((a) => `[auto:${a.id}] ${a.name} (type=${a.type}, path=${a.path}, was widget=${a.surfaceId})`);
|
|
1586
|
+
result += `
|
|
1587
|
+
|
|
1588
|
+
### Orphaned Automations (widget dismissed, still running)
|
|
1589
|
+
|
|
1590
|
+
${orphanLines.join(`
|
|
1591
|
+
`)}`;
|
|
1592
|
+
}
|
|
1593
|
+
} catch {}
|
|
1594
|
+
}
|
|
1595
|
+
if (state.agentRegistry.size > 0) {
|
|
1596
|
+
const registryLines = [];
|
|
1597
|
+
for (const [id, entry] of state.agentRegistry.entries()) {
|
|
1598
|
+
registryLines.push(`- ${entry.name} (id: ${id}) — ${entry.description}`);
|
|
1599
|
+
}
|
|
1600
|
+
result += `
|
|
1601
|
+
|
|
1602
|
+
## Available Agents
|
|
1603
|
+
|
|
1604
|
+
${registryLines.join(`
|
|
1605
|
+
`)}`;
|
|
1606
|
+
}
|
|
1607
|
+
return result;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// followup.ts
|
|
1611
|
+
var pollerInterval = null;
|
|
1612
|
+
function initFollowupSystem(api, state) {
|
|
1613
|
+
if (!state.runCommand) {
|
|
1614
|
+
console.warn("[agentlife:followup] runCommand not available — followups will not fire");
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const sys = api.runtime?.system;
|
|
1618
|
+
if (sys?.enqueueSystemEvent)
|
|
1619
|
+
state.enqueueSystemEvent = sys.enqueueSystemEvent;
|
|
1620
|
+
if (sys?.requestHeartbeatNow)
|
|
1621
|
+
state.requestHeartbeatNow = sys.requestHeartbeatNow;
|
|
1622
|
+
const db = getOrCreateHistoryDb(state);
|
|
1623
|
+
db.exec(`
|
|
1624
|
+
CREATE TABLE IF NOT EXISTS followups (
|
|
1625
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1626
|
+
surfaceId TEXT NOT NULL,
|
|
1627
|
+
agentId TEXT,
|
|
1628
|
+
fireAt INTEGER NOT NULL,
|
|
1629
|
+
instruction TEXT NOT NULL,
|
|
1630
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
1631
|
+
createdAt INTEGER NOT NULL
|
|
1632
|
+
);
|
|
1633
|
+
CREATE INDEX IF NOT EXISTS idx_followups_fire ON followups(fireAt);
|
|
1634
|
+
CREATE INDEX IF NOT EXISTS idx_followups_surface ON followups(surfaceId);
|
|
1635
|
+
CREATE INDEX IF NOT EXISTS idx_followups_status ON followups(status);
|
|
1636
|
+
`);
|
|
1637
|
+
pollerInterval = setInterval(() => pollFollowups(state), 60000);
|
|
1638
|
+
setTimeout(() => pollFollowups(state), 5000);
|
|
1639
|
+
console.log("[agentlife:followup] system initialized — polling every 60s");
|
|
1640
|
+
}
|
|
1641
|
+
function stopFollowupSystem() {
|
|
1642
|
+
if (pollerInterval) {
|
|
1643
|
+
clearInterval(pollerInterval);
|
|
1644
|
+
pollerInterval = null;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
function parseFollowup(raw) {
|
|
1648
|
+
const match = raw.match(/^(\+\d+(?:ms|s|m|h|d))\s+(.+)/);
|
|
1649
|
+
if (!match)
|
|
1650
|
+
return null;
|
|
1651
|
+
return {
|
|
1652
|
+
timeOffset: match[1],
|
|
1653
|
+
message: match[2].replace(/^"|"$/g, "").trim()
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
function scheduleFollowup(state, surfaceId, followupRaw, agentId) {
|
|
1657
|
+
const parsed = parseFollowup(followupRaw);
|
|
1658
|
+
if (!parsed) {
|
|
1659
|
+
console.warn("[agentlife:followup] parse failed for %s: %s", surfaceId, followupRaw);
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
const existing = state.pendingFollowups.get(surfaceId);
|
|
1663
|
+
if (existing)
|
|
1664
|
+
clearTimeout(existing);
|
|
1665
|
+
state.pendingFollowups.set(surfaceId, setTimeout(() => {
|
|
1666
|
+
state.pendingFollowups.delete(surfaceId);
|
|
1667
|
+
executeSchedule(state, surfaceId, parsed, agentId);
|
|
1668
|
+
}, 2000));
|
|
1669
|
+
}
|
|
1670
|
+
function removeFollowup(state, surfaceId) {
|
|
1671
|
+
const pending = state.pendingFollowups.get(surfaceId);
|
|
1672
|
+
if (pending) {
|
|
1673
|
+
clearTimeout(pending);
|
|
1674
|
+
state.pendingFollowups.delete(surfaceId);
|
|
1675
|
+
}
|
|
1676
|
+
try {
|
|
1677
|
+
const db = getOrCreateHistoryDb(state);
|
|
1678
|
+
const result = db.prepare("UPDATE followups SET status = 'cancelled' WHERE surfaceId = ? AND status = 'pending'").run(surfaceId);
|
|
1679
|
+
if (result.changes > 0) {
|
|
1680
|
+
console.log("[agentlife:followup] cancelled %d pending followup(s) for %s", result.changes, surfaceId);
|
|
1681
|
+
}
|
|
1682
|
+
} catch (e) {
|
|
1683
|
+
console.warn("[agentlife:followup] cancel failed for %s: %s", surfaceId, e?.message);
|
|
1684
|
+
}
|
|
1685
|
+
state.surfaceDb?.setCronId(surfaceId, null);
|
|
1686
|
+
}
|
|
1687
|
+
function formatAge(ms) {
|
|
1688
|
+
if (ms < 60000)
|
|
1689
|
+
return "just now";
|
|
1690
|
+
const minutes = Math.floor(ms / 60000);
|
|
1691
|
+
if (minutes < 60)
|
|
1692
|
+
return `${minutes}min ago`;
|
|
1693
|
+
const hours = Math.floor(minutes / 60);
|
|
1694
|
+
if (hours < 24)
|
|
1695
|
+
return `${hours}h ago`;
|
|
1696
|
+
const days = Math.floor(hours / 24);
|
|
1697
|
+
return `${days}d ago`;
|
|
1698
|
+
}
|
|
1699
|
+
function parseOffset(offset) {
|
|
1700
|
+
const match = offset.match(/^\+?(\d+)(ms|s|m|h|d)$/);
|
|
1701
|
+
if (!match)
|
|
1702
|
+
return 0;
|
|
1703
|
+
const val = parseInt(match[1], 10);
|
|
1704
|
+
switch (match[2]) {
|
|
1705
|
+
case "ms":
|
|
1706
|
+
return val;
|
|
1707
|
+
case "s":
|
|
1708
|
+
return val * 1000;
|
|
1709
|
+
case "m":
|
|
1710
|
+
return val * 60000;
|
|
1711
|
+
case "h":
|
|
1712
|
+
return val * 3600000;
|
|
1713
|
+
case "d":
|
|
1714
|
+
return val * 86400000;
|
|
1715
|
+
default:
|
|
1716
|
+
return 0;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
function executeSchedule(state, surfaceId, parsed, agentId) {
|
|
1720
|
+
const meta = state.surfaceDb?.get(surfaceId);
|
|
1721
|
+
const goalText = meta?.goal ?? (meta ? extractTitleAndDetail(meta).title : null);
|
|
1722
|
+
const goalLabel = goalText ? ` (goal: "${goalText}")` : "";
|
|
1723
|
+
const instruction = [
|
|
1724
|
+
`Widget followup for ${surfaceId}${goalLabel}: ${parsed.message}`,
|
|
1725
|
+
`Check your dashboard state for engagement. Update widget state. Advance the work or delete the widget.`
|
|
1726
|
+
].join(`
|
|
1727
|
+
`);
|
|
1728
|
+
const fireAt = Date.now() + parseOffset(parsed.timeOffset);
|
|
1729
|
+
try {
|
|
1730
|
+
const db = getOrCreateHistoryDb(state);
|
|
1731
|
+
db.prepare("UPDATE followups SET status = 'cancelled' WHERE surfaceId = ? AND status = 'pending'").run(surfaceId);
|
|
1732
|
+
const result = db.prepare("INSERT INTO followups (surfaceId, agentId, fireAt, instruction, status, createdAt) VALUES (?, ?, ?, ?, 'pending', ?)").run(surfaceId, agentId, fireAt, instruction, Date.now());
|
|
1733
|
+
const followupId = result.lastInsertRowid;
|
|
1734
|
+
state.surfaceDb?.setCronId(surfaceId, String(followupId));
|
|
1735
|
+
recordSurfaceEvent(state, surfaceId, "cron_scheduled", undefined, agentId ?? undefined, JSON.stringify({ followupId, timeOffset: parsed.timeOffset, message: parsed.message }));
|
|
1736
|
+
console.log("[agentlife:followup] scheduled %s: fires in %s (%s)", surfaceId, parsed.timeOffset, parsed.message);
|
|
1737
|
+
} catch (e) {
|
|
1738
|
+
console.error("[agentlife:followup] schedule failed for %s: %s", surfaceId, e?.message);
|
|
1739
|
+
recordSurfaceEvent(state, surfaceId, "cron_failed", undefined, agentId ?? undefined, JSON.stringify({ error: e?.message, timeOffset: parsed.timeOffset }));
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
function pollFollowups(state) {
|
|
1743
|
+
if (state.disabled || !state.runCommand)
|
|
1744
|
+
return;
|
|
1745
|
+
try {
|
|
1746
|
+
const db = getOrCreateHistoryDb(state);
|
|
1747
|
+
const now = Date.now();
|
|
1748
|
+
const due = db.prepare("SELECT id, surfaceId, agentId, instruction FROM followups WHERE status = 'pending' AND fireAt <= ? ORDER BY fireAt ASC").all(now);
|
|
1749
|
+
for (const row of due) {
|
|
1750
|
+
db.prepare("UPDATE followups SET status = 'fired' WHERE id = ?").run(row.id);
|
|
1751
|
+
const meta = state.surfaceDb?.get(row.surfaceId);
|
|
1752
|
+
if (!meta || meta.state === "terminal" || meta.state === "dismissed") {
|
|
1753
|
+
console.log("[agentlife:followup] skipped %s — surface gone or terminal", row.surfaceId);
|
|
1754
|
+
continue;
|
|
1755
|
+
}
|
|
1756
|
+
const agentId = row.agentId ?? state.surfaceDb?.getAgentId(row.surfaceId) ?? null;
|
|
1757
|
+
if (!agentId) {
|
|
1758
|
+
console.warn("[agentlife:followup] skipped %s — no agentId", row.surfaceId);
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
const sessionKey = `agent:${agentId}:main`;
|
|
1762
|
+
const idempotencyKey = `followup-${row.surfaceId}-${Date.now()}`;
|
|
1763
|
+
const chatParams = JSON.stringify({ sessionKey, message: `[system] ${row.instruction}`, idempotencyKey });
|
|
1764
|
+
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", chatParams], { timeoutMs: 60000 }).then((result) => {
|
|
1765
|
+
console.log("[agentlife:followup] chat.send for %s: code=%s", row.surfaceId, result?.code ?? "?");
|
|
1766
|
+
}).catch((e) => {
|
|
1767
|
+
console.error("[agentlife:followup] chat.send failed for %s: %s", row.surfaceId, e?.message);
|
|
1768
|
+
});
|
|
1769
|
+
recordSurfaceEvent(state, row.surfaceId, "cron_fired", undefined, agentId);
|
|
1770
|
+
console.log("[agentlife:followup] fired %s → %s", row.surfaceId, sessionKey);
|
|
1771
|
+
}
|
|
1772
|
+
} catch (e) {
|
|
1773
|
+
console.warn("[agentlife:followup] poll error: %s", e?.message);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// surfaces.ts
|
|
1778
|
+
var DEFAULT_CEILING_MS = 7 * 24 * 60 * 60 * 1000;
|
|
1779
|
+
var NO_FOLLOWUP_CEILING_MS = 48 * 60 * 60 * 1000;
|
|
1780
|
+
var OVERLAY_TTL_MS = 30 * 60 * 1000;
|
|
1781
|
+
var GRACE_PERIOD_MS = 24 * 60 * 60 * 1000;
|
|
1782
|
+
function isExpired(meta, now = Date.now()) {
|
|
1783
|
+
if (meta.state === "terminal")
|
|
1784
|
+
return false;
|
|
1785
|
+
const isInput = meta.lines.some((l) => /^surface\s+\S+.*\binput\b/.test(l.trim()));
|
|
1786
|
+
if (isInput)
|
|
1787
|
+
return false;
|
|
1788
|
+
const isOverlay = meta.lines.some((l) => /^surface\s+\S+.*\boverlay\b/.test(l.trim()));
|
|
1789
|
+
if (isOverlay && now - meta.updatedAt > OVERLAY_TTL_MS)
|
|
1790
|
+
return true;
|
|
1791
|
+
if (!meta.followup && now - meta.updatedAt > NO_FOLLOWUP_CEILING_MS)
|
|
1792
|
+
return true;
|
|
1793
|
+
if (now - meta.updatedAt > DEFAULT_CEILING_MS)
|
|
1794
|
+
return true;
|
|
1795
|
+
return false;
|
|
1796
|
+
}
|
|
1797
|
+
function isPastGrace(meta, now = Date.now()) {
|
|
1798
|
+
return !!meta.expiredSince && now - meta.expiredSince > GRACE_PERIOD_MS;
|
|
1799
|
+
}
|
|
1800
|
+
function processDslBlock(state, dsl) {
|
|
1801
|
+
if (!state.surfaceDb)
|
|
1802
|
+
return [];
|
|
1803
|
+
const now = Date.now();
|
|
1804
|
+
const results = [];
|
|
1805
|
+
const blocks = dsl.split(/\n---(?:\n|$)/).filter((b) => b.trim().length > 0);
|
|
1806
|
+
for (const block of blocks) {
|
|
1807
|
+
const firstLine = block.trim().split(`
|
|
1808
|
+
`)[0]?.trim() ?? "";
|
|
1809
|
+
if (firstLine.startsWith("delete ")) {
|
|
1810
|
+
const sid2 = firstLine.slice(7).trim();
|
|
1811
|
+
if (sid2) {
|
|
1812
|
+
state.surfaceDb.delete(sid2);
|
|
1813
|
+
recordSurfaceEvent(state, sid2, "deleted");
|
|
1814
|
+
results.push({ surfaceId: sid2, isNew: false, followupChanged: false, followupRemoved: false, stateTransition: null });
|
|
1815
|
+
}
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
const surfaceMatch = block.match(/^surface\s+(\S+)/m);
|
|
1819
|
+
if (!surfaceMatch)
|
|
1820
|
+
continue;
|
|
1821
|
+
const sid = surfaceMatch[1];
|
|
1822
|
+
const headerLine = block.match(/^surface\s+.*/m)?.[0] ?? "";
|
|
1823
|
+
const isOverlay = /\boverlay\b/.test(headerLine);
|
|
1824
|
+
const isInput = /\binput\b/.test(headerLine);
|
|
1825
|
+
const stateMatch = headerLine.match(/\bstate=(\w+)\b/);
|
|
1826
|
+
const declaredState = stateMatch?.[1] ?? null;
|
|
1827
|
+
const followupMatch = block.match(/^\s*followup:\s*(.+)/m);
|
|
1828
|
+
const goalMatch = block.match(/^\s*goal:\s*(.+)/m);
|
|
1829
|
+
const contextMatch = block.match(/^\s*context:\s*(.+)/m);
|
|
1830
|
+
let parsedContext;
|
|
1831
|
+
if (contextMatch) {
|
|
1832
|
+
try {
|
|
1833
|
+
const parsed = JSON.parse(contextMatch[1].trim());
|
|
1834
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1835
|
+
parsedContext = parsed;
|
|
1836
|
+
} else {
|
|
1837
|
+
console.warn("[agentlife] context: must be a JSON object — skipping");
|
|
1838
|
+
}
|
|
1839
|
+
} catch (err) {
|
|
1840
|
+
console.warn("[agentlife] malformed context: JSON — %s", err?.message);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const hasTransientState = /\bstate=(loading|dismissing)\b/.test(headerLine);
|
|
1844
|
+
const cleanedBlock = hasTransientState ? block.replace(/\bstate=(loading|dismissing)\b/, "") : block;
|
|
1845
|
+
const cleanedLines = cleanedBlock.split(`
|
|
1846
|
+
`).filter((line) => !/^\s*state\s*=\s*\w+\s*$/.test(line));
|
|
1847
|
+
const existing = state.surfaceDb.get(sid);
|
|
1848
|
+
const oldFollowup = existing?.followup;
|
|
1849
|
+
const oldState = existing?.state ?? "active";
|
|
1850
|
+
let newState;
|
|
1851
|
+
if (declaredState === "terminal") {
|
|
1852
|
+
newState = "terminal";
|
|
1853
|
+
} else if (declaredState === "loading") {
|
|
1854
|
+
newState = "loading";
|
|
1855
|
+
} else if (!existing) {
|
|
1856
|
+
newState = "active";
|
|
1857
|
+
} else {
|
|
1858
|
+
newState = oldState === "terminal" ? "terminal" : "active";
|
|
1859
|
+
}
|
|
1860
|
+
const newFollowup = followupMatch?.[1]?.trim();
|
|
1861
|
+
let resolvedContext;
|
|
1862
|
+
let contextDroppedFields;
|
|
1863
|
+
if (parsedContext) {
|
|
1864
|
+
resolvedContext = parsedContext;
|
|
1865
|
+
if (existing?.context) {
|
|
1866
|
+
const oldKeys = Object.keys(existing.context);
|
|
1867
|
+
const newKeys = new Set(Object.keys(parsedContext));
|
|
1868
|
+
contextDroppedFields = oldKeys.filter((k) => !newKeys.has(k));
|
|
1869
|
+
if (contextDroppedFields.length === 0)
|
|
1870
|
+
contextDroppedFields = undefined;
|
|
1871
|
+
}
|
|
1872
|
+
} else {
|
|
1873
|
+
resolvedContext = existing?.context;
|
|
1874
|
+
}
|
|
1875
|
+
const newGoal = goalMatch?.[1]?.trim() ?? existing?.goal;
|
|
1876
|
+
const goalChanged = existing?.goal && newGoal && goalMatch && existing.goal !== newGoal ? { from: existing.goal, to: newGoal } : undefined;
|
|
1877
|
+
if (goalChanged) {
|
|
1878
|
+
console.warn('[agentlife] %s: goal changed from "%s" to "%s" — consider using a new surfaceId for a new goal', sid, goalChanged.from, goalChanged.to);
|
|
1879
|
+
}
|
|
1880
|
+
const meta = {
|
|
1881
|
+
lines: cleanedLines,
|
|
1882
|
+
createdAt: existing?.createdAt ?? now,
|
|
1883
|
+
updatedAt: now,
|
|
1884
|
+
followup: newFollowup,
|
|
1885
|
+
goal: newGoal,
|
|
1886
|
+
context: resolvedContext,
|
|
1887
|
+
expiredSince: undefined
|
|
1888
|
+
};
|
|
1889
|
+
state.surfaceDb.set(sid, meta);
|
|
1890
|
+
state.surfaceDb.setState(sid, newState);
|
|
1891
|
+
if (isOverlay || isInput) {
|
|
1892
|
+
state.surfaceDb.setTransient(sid, true);
|
|
1893
|
+
} else {
|
|
1894
|
+
state.surfaceDb.setTransient(sid, false);
|
|
1895
|
+
}
|
|
1896
|
+
const followupChanged = oldFollowup !== newFollowup;
|
|
1897
|
+
const followupRemoved = !!oldFollowup && !newFollowup;
|
|
1898
|
+
const stateTransition = oldState !== newState ? `${oldState}->${newState}` : null;
|
|
1899
|
+
results.push({ surfaceId: sid, isNew: !existing, followupChanged, followupRemoved, stateTransition, contextDroppedFields, goalChanged });
|
|
1900
|
+
recordSurfaceEvent(state, sid, existing ? "updated" : "created", block, state.surfaceDb.getAgentId(sid));
|
|
1901
|
+
}
|
|
1902
|
+
return results;
|
|
1903
|
+
}
|
|
1904
|
+
function runStartupPurge(state) {
|
|
1905
|
+
if (!state.surfaceDb)
|
|
1906
|
+
return;
|
|
1907
|
+
const now = Date.now();
|
|
1908
|
+
let purged = 0;
|
|
1909
|
+
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
1910
|
+
if (isPastGrace(meta, now)) {
|
|
1911
|
+
const cronId = state.surfaceDb.getCronId(surfaceId);
|
|
1912
|
+
state.surfaceDb.delete(surfaceId);
|
|
1913
|
+
recordSurfaceEvent(state, surfaceId, "expired");
|
|
1914
|
+
if (cronId) {
|
|
1915
|
+
removeFollowup(state, surfaceId);
|
|
1916
|
+
}
|
|
1917
|
+
purged++;
|
|
1918
|
+
} else if (isExpired(meta, now) && !meta.expiredSince) {
|
|
1919
|
+
state.surfaceDb.set(surfaceId, { ...meta, expiredSince: now });
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
let backfilled = 0;
|
|
1923
|
+
const orphanedIds = state.surfaceDb.keys().filter((id) => !state.surfaceDb.getAgentId(id));
|
|
1924
|
+
if (orphanedIds.length > 0) {
|
|
1925
|
+
try {
|
|
1926
|
+
const db = getOrCreateHistoryDb(state);
|
|
1927
|
+
for (const sid of orphanedIds) {
|
|
1928
|
+
const row = db.prepare("SELECT agentId FROM surface_events WHERE surfaceId = ? AND agentId IS NOT NULL ORDER BY createdAt DESC LIMIT 1").get(sid);
|
|
1929
|
+
if (row?.agentId) {
|
|
1930
|
+
state.surfaceDb.setAgentId(sid, row.agentId);
|
|
1931
|
+
backfilled++;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
} catch {}
|
|
1935
|
+
}
|
|
1936
|
+
console.log("[agentlife] surfaces DB: %d persisted%s%s", state.surfaceDb.size, purged > 0 ? ` (purged ${purged} past grace period)` : "", backfilled > 0 ? ` (backfilled ${backfilled} agent mappings from history)` : "");
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// cleanup.ts
|
|
1940
|
+
function enqueueCleanupTasks(state, surfaceId, agentId, cronId, automations) {
|
|
1941
|
+
const db = getOrCreateHistoryDb(state);
|
|
1942
|
+
const now = Date.now();
|
|
1943
|
+
const insert = db.prepare(`INSERT INTO cleanup_tasks (surfaceId, agentId, step, status, attempts, maxAttempts, lastError, payload, createdAt, updatedAt)
|
|
1944
|
+
VALUES (?, ?, ?, 'pending', 0, ?, NULL, ?, ?, ?)`);
|
|
1945
|
+
if (cronId) {
|
|
1946
|
+
insert.run(surfaceId, agentId, "cron_remove", 3, JSON.stringify({ cronId }), now, now);
|
|
1947
|
+
}
|
|
1948
|
+
if (agentId) {
|
|
1949
|
+
insert.run(surfaceId, agentId, "agent_notify", 5, JSON.stringify({ agentId, automations }), now, now);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
function resetInterruptedTasks(state) {
|
|
1953
|
+
try {
|
|
1954
|
+
const db = getOrCreateHistoryDb(state);
|
|
1955
|
+
const result = db.prepare("UPDATE cleanup_tasks SET status = 'pending', updatedAt = ? WHERE status = 'running'").run(Date.now());
|
|
1956
|
+
return result.changes;
|
|
1957
|
+
} catch {
|
|
1958
|
+
return 0;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
async function processCleanupTasks(state, surfaceId) {
|
|
1962
|
+
const db = getOrCreateHistoryDb(state);
|
|
1963
|
+
const query = surfaceId ? db.prepare("SELECT * FROM cleanup_tasks WHERE status IN ('pending', 'failed') AND attempts < maxAttempts AND surfaceId = ? ORDER BY createdAt ASC") : db.prepare("SELECT * FROM cleanup_tasks WHERE status IN ('pending', 'failed') AND attempts < maxAttempts ORDER BY createdAt ASC");
|
|
1964
|
+
const tasks = surfaceId ? query.all(surfaceId) : query.all();
|
|
1965
|
+
if (tasks.length === 0)
|
|
1966
|
+
return;
|
|
1967
|
+
let hadFailures = false;
|
|
1968
|
+
for (const task of tasks) {
|
|
1969
|
+
const ok = await executeTask(state, db, task);
|
|
1970
|
+
if (!ok)
|
|
1971
|
+
hadFailures = true;
|
|
1972
|
+
}
|
|
1973
|
+
if (hadFailures) {
|
|
1974
|
+
setTimeout(() => processCleanupTasks(state).catch(() => {}), 30000);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
async function executeTask(state, db, task) {
|
|
1978
|
+
const now = Date.now();
|
|
1979
|
+
db.prepare("UPDATE cleanup_tasks SET status = 'running', attempts = attempts + 1, updatedAt = ? WHERE id = ?").run(now, task.id);
|
|
1980
|
+
try {
|
|
1981
|
+
const payload = task.payload ? JSON.parse(task.payload) : {};
|
|
1982
|
+
switch (task.step) {
|
|
1983
|
+
case "cron_remove":
|
|
1984
|
+
await executeCronRemove(state, task.surfaceId, payload);
|
|
1985
|
+
break;
|
|
1986
|
+
case "agent_notify":
|
|
1987
|
+
await executeAgentNotify(state, db, task.surfaceId, payload);
|
|
1988
|
+
break;
|
|
1989
|
+
default:
|
|
1990
|
+
throw new Error(`unknown cleanup step: ${task.step}`);
|
|
1991
|
+
}
|
|
1992
|
+
db.prepare("UPDATE cleanup_tasks SET status = 'done', updatedAt = ? WHERE id = ?").run(Date.now(), task.id);
|
|
1993
|
+
return true;
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
const msg = err?.message ?? String(err);
|
|
1996
|
+
db.prepare("UPDATE cleanup_tasks SET status = 'failed', lastError = ?, updatedAt = ? WHERE id = ?").run(msg, Date.now(), task.id);
|
|
1997
|
+
console.warn("[agentlife:cleanup] task %d (%s/%s) failed (attempt %d/%d): %s", task.id, task.surfaceId, task.step, task.attempts + 1, task.maxAttempts, msg);
|
|
1998
|
+
return false;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
async function executeCronRemove(state, surfaceId, _payload) {
|
|
2002
|
+
removeFollowup(state, surfaceId);
|
|
2003
|
+
}
|
|
2004
|
+
async function executeAgentNotify(state, db, surfaceId, payload) {
|
|
2005
|
+
if (!state.runCommand)
|
|
2006
|
+
throw new Error("runCommand not available");
|
|
2007
|
+
if (!payload.agentId)
|
|
2008
|
+
return;
|
|
2009
|
+
await state.runCommand([
|
|
2010
|
+
"openclaw",
|
|
2011
|
+
"agent",
|
|
2012
|
+
"--agent",
|
|
2013
|
+
payload.agentId,
|
|
2014
|
+
"--message",
|
|
2015
|
+
`[event:dismissed] surfaceId=${surfaceId}`,
|
|
2016
|
+
"--json"
|
|
2017
|
+
], { timeoutMs: 60000 });
|
|
2018
|
+
const automations = Array.isArray(payload.automations) ? payload.automations : [];
|
|
2019
|
+
const ids = automations.map((a) => a.id).filter((id) => typeof id === "number");
|
|
2020
|
+
if (ids.length > 0) {
|
|
2021
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
2022
|
+
db.prepare(`UPDATE automations SET status = 'removed', updatedAt = ? WHERE id IN (${placeholders}) AND status != 'removed'`).run(Date.now(), ...ids);
|
|
2023
|
+
console.log("[agentlife:cleanup] marked %d automations as removed for %s (agent notified)", ids.length, surfaceId);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// services/surfaces-init.ts
|
|
2028
|
+
function registerSurfacesService(api, state) {
|
|
2029
|
+
api.registerService({
|
|
2030
|
+
id: "agentlife-surfaces",
|
|
2031
|
+
start: async (ctx) => {
|
|
2032
|
+
const agentlifeDir = path4.join(ctx.stateDir, "agentlife");
|
|
2033
|
+
state.agentlifeStateDir = agentlifeDir;
|
|
2034
|
+
state.registryFilePath = path4.join(agentlifeDir, "agent-registry.json");
|
|
2035
|
+
state.dbBaseDir = path4.join(agentlifeDir, "db");
|
|
2036
|
+
state.historyDbPath = path4.join(agentlifeDir, "agentlife.db");
|
|
2037
|
+
if (!state.surfaceDb) {
|
|
2038
|
+
const db = getOrCreateHistoryDb(state);
|
|
2039
|
+
state.surfaceDb = new SurfaceDb(db);
|
|
2040
|
+
}
|
|
2041
|
+
runStartupPurge(state);
|
|
2042
|
+
let inputPurged = 0;
|
|
2043
|
+
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
2044
|
+
const headerLine = meta.lines[0] ?? "";
|
|
2045
|
+
if (/\binput\b/.test(headerLine)) {
|
|
2046
|
+
state.surfaceDb.delete(surfaceId);
|
|
2047
|
+
inputPurged++;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
if (inputPurged > 0) {
|
|
2051
|
+
console.log("[agentlife] purged %d stale input surfaces on startup", inputPurged);
|
|
2052
|
+
}
|
|
2053
|
+
await loadRegistryFromDisk(state);
|
|
2054
|
+
console.log("[agentlife] surface persistence service started (SQLite: %s)", state.historyDbPath);
|
|
2055
|
+
setTimeout(async () => {
|
|
2056
|
+
try {
|
|
2057
|
+
let rescued = 0;
|
|
2058
|
+
const db = getOrCreateHistoryDb(state);
|
|
2059
|
+
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
2060
|
+
if (meta.followup && meta.state === "active" && !isExpired(meta)) {
|
|
2061
|
+
const pending = db.prepare("SELECT id FROM followups WHERE surfaceId = ? AND status = 'pending' LIMIT 1").get(surfaceId);
|
|
2062
|
+
if (!pending) {
|
|
2063
|
+
const agentId = state.surfaceDb.getAgentId(surfaceId) ?? null;
|
|
2064
|
+
scheduleFollowup(state, surfaceId, meta.followup, agentId);
|
|
2065
|
+
rescued++;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
if (rescued > 0) {
|
|
2070
|
+
console.log("[agentlife] followup reconciliation: %d orphaned followups rescued", rescued);
|
|
2071
|
+
}
|
|
2072
|
+
const runningAutos = db.prepare("SELECT id, surfaceId FROM automations WHERE status = 'running' AND surfaceId IS NOT NULL").all();
|
|
2073
|
+
let autosOrphaned = 0;
|
|
2074
|
+
for (const a of runningAutos) {
|
|
2075
|
+
if (!state.surfaceDb.has(a.surfaceId)) {
|
|
2076
|
+
db.prepare("UPDATE automations SET status = 'removed', updatedAt = ? WHERE id = ?").run(Date.now(), a.id);
|
|
2077
|
+
autosOrphaned++;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
if (autosOrphaned > 0) {
|
|
2081
|
+
console.log("[agentlife] automation reconciliation: marked %d orphaned automations as removed", autosOrphaned);
|
|
2082
|
+
}
|
|
2083
|
+
} catch (err) {
|
|
2084
|
+
console.warn("[agentlife] reconciliation failed (non-critical): %s", err?.message);
|
|
2085
|
+
}
|
|
2086
|
+
try {
|
|
2087
|
+
const reset = resetInterruptedTasks(state);
|
|
2088
|
+
if (reset > 0) {
|
|
2089
|
+
console.log("[agentlife] cleanup reconciliation: reset %d interrupted tasks", reset);
|
|
2090
|
+
}
|
|
2091
|
+
await processCleanupTasks(state);
|
|
2092
|
+
} catch (err) {
|
|
2093
|
+
console.warn("[agentlife] cleanup reconciliation failed (non-critical): %s", err?.message);
|
|
2094
|
+
}
|
|
2095
|
+
}, 3000);
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// services/config-optimizer.ts
|
|
2101
|
+
import * as fs3 from "node:fs/promises";
|
|
2102
|
+
import * as path5 from "node:path";
|
|
2103
|
+
async function restoreConfigFromBackup(api, backupPath) {
|
|
2104
|
+
const raw = await fs3.readFile(backupPath, "utf-8");
|
|
2105
|
+
const backup = JSON.parse(raw);
|
|
2106
|
+
const liveCfg = api.runtime.config.loadConfig();
|
|
2107
|
+
const restored = {
|
|
2108
|
+
...liveCfg,
|
|
2109
|
+
session: {
|
|
2110
|
+
...liveCfg.session,
|
|
2111
|
+
agentToAgent: {
|
|
2112
|
+
...liveCfg.session?.agentToAgent,
|
|
2113
|
+
maxPingPongTurns: backup.maxPingPongTurns ?? undefined
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
await api.runtime.config.writeConfigFile(restored);
|
|
2118
|
+
await fs3.unlink(backupPath);
|
|
2119
|
+
const label = backup.maxPingPongTurns ?? "default";
|
|
2120
|
+
console.log("[agentlife] restored config: maxPingPongTurns=%s", label);
|
|
2121
|
+
return `maxPingPongTurns restored to ${label}`;
|
|
2122
|
+
}
|
|
2123
|
+
function registerConfigOptimizer(api, _state) {
|
|
2124
|
+
api.registerService({
|
|
2125
|
+
id: "agentlife-config-optimizer",
|
|
2126
|
+
start: async (ctx) => {
|
|
2127
|
+
const configBackupDir = path5.join(ctx.stateDir, "agentlife");
|
|
2128
|
+
const configBackupPath = path5.join(configBackupDir, "config-backup.json");
|
|
2129
|
+
const cfg = api.runtime.config.loadConfig();
|
|
2130
|
+
const current = cfg.session?.agentToAgent?.maxPingPongTurns;
|
|
2131
|
+
if (current === 1) {
|
|
2132
|
+
console.log("[agentlife] config already optimized (maxPingPongTurns=1), skipping");
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
await fs3.mkdir(configBackupDir, { recursive: true });
|
|
2136
|
+
await fs3.writeFile(configBackupPath, JSON.stringify({ maxPingPongTurns: current ?? null }));
|
|
2137
|
+
const next = {
|
|
2138
|
+
...cfg,
|
|
2139
|
+
session: {
|
|
2140
|
+
...cfg.session,
|
|
2141
|
+
agentToAgent: { ...cfg.session?.agentToAgent, maxPingPongTurns: 1 }
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
await api.runtime.config.writeConfigFile(next);
|
|
2145
|
+
const verify = api.runtime.config.loadConfig();
|
|
2146
|
+
const applied = verify.session?.agentToAgent?.maxPingPongTurns;
|
|
2147
|
+
if (applied === 1) {
|
|
2148
|
+
console.log("[agentlife] config optimized: maxPingPongTurns 1 (was %s)", current ?? "default");
|
|
2149
|
+
} else {
|
|
2150
|
+
console.error("[agentlife] config write FAILED — maxPingPongTurns is %s after write (expected 1)", applied);
|
|
2151
|
+
}
|
|
2152
|
+
},
|
|
2153
|
+
stop: async (ctx) => {
|
|
2154
|
+
try {
|
|
2155
|
+
const backupPath = path5.join(ctx.stateDir, "agentlife", "config-backup.json");
|
|
2156
|
+
await restoreConfigFromBackup(api, backupPath);
|
|
2157
|
+
} catch {}
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// services/pairing.ts
|
|
2163
|
+
import * as fsSync2 from "node:fs";
|
|
2164
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
2165
|
+
import * as os2 from "node:os";
|
|
2166
|
+
import * as path6 from "node:path";
|
|
2167
|
+
function registerPairingServices(api) {
|
|
2168
|
+
api.registerService({
|
|
2169
|
+
id: "agentlife-pairing-qr",
|
|
2170
|
+
start: async (_ctx) => {
|
|
2171
|
+
try {
|
|
2172
|
+
const cfg = api.runtime.config.loadConfig();
|
|
2173
|
+
const bind = cfg.gateway?.bind ?? "loopback";
|
|
2174
|
+
const port = cfg.gateway?.port ?? 18789;
|
|
2175
|
+
const token = cfg.gateway?.auth?.token;
|
|
2176
|
+
if (!token)
|
|
2177
|
+
return;
|
|
2178
|
+
let host;
|
|
2179
|
+
if (bind === "lan" || bind === "auto" || bind === "custom") {
|
|
2180
|
+
const nets = os2.networkInterfaces();
|
|
2181
|
+
const lanIp = Object.values(nets).flat().find((n) => n && !n.internal && n.family === "IPv4")?.address;
|
|
2182
|
+
host = lanIp ?? "127.0.0.1";
|
|
2183
|
+
} else {
|
|
2184
|
+
host = "127.0.0.1";
|
|
2185
|
+
}
|
|
2186
|
+
const payload = JSON.stringify({ url: `ws://${host}:${port}`, token });
|
|
2187
|
+
const setupCode = Buffer.from(payload).toString("base64");
|
|
2188
|
+
let qrTerminal = null;
|
|
2189
|
+
try {
|
|
2190
|
+
const realPath = fsSync2.realpathSync(process.argv[1] || "");
|
|
2191
|
+
const mainRequire = createRequire3(realPath);
|
|
2192
|
+
qrTerminal = mainRequire("qrcode-terminal");
|
|
2193
|
+
} catch {}
|
|
2194
|
+
console.log(`
|
|
2195
|
+
\x1B[36m[agentlife]\x1B[0m \x1B[1mPairing QR\x1B[0m`);
|
|
2196
|
+
console.log(`Scan this with the Agent Life app to connect.
|
|
2197
|
+
`);
|
|
2198
|
+
if (qrTerminal?.generate) {
|
|
2199
|
+
qrTerminal.generate(setupCode, { small: true }, (qr) => {
|
|
2200
|
+
console.log(qr);
|
|
2201
|
+
console.log(`
|
|
2202
|
+
Setup code: ${setupCode}
|
|
2203
|
+
`);
|
|
2204
|
+
});
|
|
2205
|
+
} else {
|
|
2206
|
+
console.log(`Setup code: ${setupCode}`);
|
|
2207
|
+
console.log(`Run 'openclaw qr' for a scannable QR code.
|
|
2208
|
+
`);
|
|
2209
|
+
}
|
|
2210
|
+
console.log(" Download the app:");
|
|
2211
|
+
console.log(" iOS: https://agentlife.app/ios");
|
|
2212
|
+
console.log(" Android: https://agentlife.app/android");
|
|
2213
|
+
console.log("");
|
|
2214
|
+
} catch (err) {
|
|
2215
|
+
console.warn("[agentlife] QR generation skipped:", err?.message);
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2219
|
+
api.registerService({
|
|
2220
|
+
id: "agentlife-auto-pair",
|
|
2221
|
+
start: async (ctx) => {
|
|
2222
|
+
const devicesDir = path6.join(os2.homedir(), ".openclaw", "devices");
|
|
2223
|
+
const pendingPath = path6.join(devicesDir, "pending.json");
|
|
2224
|
+
const pairedPath = path6.join(devicesDir, "paired.json");
|
|
2225
|
+
const pollInterval = 3000;
|
|
2226
|
+
const poll = () => {
|
|
2227
|
+
try {
|
|
2228
|
+
if (!fsSync2.existsSync(pendingPath))
|
|
2229
|
+
return;
|
|
2230
|
+
const pending = JSON.parse(fsSync2.readFileSync(pendingPath, "utf-8"));
|
|
2231
|
+
const reqIds = Object.keys(pending);
|
|
2232
|
+
if (!reqIds.length)
|
|
2233
|
+
return;
|
|
2234
|
+
const paired = fsSync2.existsSync(pairedPath) ? JSON.parse(fsSync2.readFileSync(pairedPath, "utf-8")) : {};
|
|
2235
|
+
for (const reqId of reqIds) {
|
|
2236
|
+
const req = pending[reqId];
|
|
2237
|
+
paired[req.deviceId] = { ...req, approvedAtMs: Date.now() };
|
|
2238
|
+
delete pending[reqId];
|
|
2239
|
+
console.log("[agentlife] Auto-approved device: %s (%s)", req.deviceId?.slice(0, 16), req.platform || "?");
|
|
2240
|
+
}
|
|
2241
|
+
fsSync2.writeFileSync(pendingPath, JSON.stringify(pending, null, 2));
|
|
2242
|
+
fsSync2.writeFileSync(pairedPath, JSON.stringify(paired, null, 2));
|
|
2243
|
+
} catch (err) {
|
|
2244
|
+
console.warn("[agentlife] Auto-pair poll error:", err?.message);
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
const timer = setInterval(poll, pollInterval);
|
|
2248
|
+
setTimeout(poll, 2000);
|
|
2249
|
+
if (typeof ctx.onStop === "function") {
|
|
2250
|
+
ctx.onStop(() => clearInterval(timer));
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// channel-guard.ts
|
|
2257
|
+
var CHANNEL_SEGMENTS = [
|
|
2258
|
+
":telegram:",
|
|
2259
|
+
":discord:",
|
|
2260
|
+
":whatsapp:",
|
|
2261
|
+
":slack:",
|
|
2262
|
+
":signal:",
|
|
2263
|
+
":irc:",
|
|
2264
|
+
":matrix:",
|
|
2265
|
+
":line:",
|
|
2266
|
+
":msteams:",
|
|
2267
|
+
":nostr:",
|
|
2268
|
+
":imessage:",
|
|
2269
|
+
":bluebubbles:",
|
|
2270
|
+
":googlechat:",
|
|
2271
|
+
":feishu:",
|
|
2272
|
+
":zalo:"
|
|
2273
|
+
];
|
|
2274
|
+
function isChannelSession(sessionKey) {
|
|
2275
|
+
if (!sessionKey)
|
|
2276
|
+
return false;
|
|
2277
|
+
return CHANNEL_SEGMENTS.some((seg) => sessionKey.includes(seg));
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
// hooks/bootstrap.ts
|
|
2281
|
+
var SKIP_GUIDANCE_AGENTS = new Set(["agentlife"]);
|
|
2282
|
+
var SKIP_AGENTS_MD_INJECTION = new Set(["agentlife", "agentlife-builder", "quick"]);
|
|
2283
|
+
function registerBootstrapHook(api, state) {
|
|
2284
|
+
api.registerService({
|
|
2285
|
+
id: "agentlife-bootstrap-hook",
|
|
2286
|
+
start: () => {
|
|
2287
|
+
registerBootstrapHookImpl(api, state);
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
function registerBootstrapHookImpl(api, state) {
|
|
2292
|
+
api.registerHook("agent:bootstrap", (event) => {
|
|
2293
|
+
if (state.disabled)
|
|
2294
|
+
return;
|
|
2295
|
+
const ctx = event.context;
|
|
2296
|
+
if (!ctx?.bootstrapFiles) {
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
if (isChannelSession(ctx.sessionKey)) {
|
|
2300
|
+
console.log("[agentlife:bootstrap] SKIP channel session %s — no injection", ctx.sessionKey);
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
console.log("[agentlife:bootstrap] agentId=%s sessionKey=%s files=%s", ctx.agentId ?? "NONE", ctx.sessionKey ?? "NONE", ctx.bootstrapFiles.map((f) => `${f.name}(${f.content?.length ?? 0})`).join(", "));
|
|
2304
|
+
if (ctx.agentId && SKIP_GUIDANCE_AGENTS.has(ctx.agentId)) {
|
|
2305
|
+
const registryContext = buildAgentRegistryContext(state);
|
|
2306
|
+
if (registryContext) {
|
|
2307
|
+
const regIdx = ctx.bootstrapFiles.findIndex((f) => f.name === "AGENT_REGISTRY.md");
|
|
2308
|
+
const regEntry = {
|
|
2309
|
+
name: "AGENT_REGISTRY.md",
|
|
2310
|
+
path: "agentlife://agent-registry",
|
|
2311
|
+
content: registryContext,
|
|
2312
|
+
missing: false
|
|
2313
|
+
};
|
|
2314
|
+
if (regIdx >= 0) {
|
|
2315
|
+
ctx.bootstrapFiles[regIdx] = regEntry;
|
|
2316
|
+
} else {
|
|
2317
|
+
ctx.bootstrapFiles.push(regEntry);
|
|
2318
|
+
}
|
|
2319
|
+
console.log("[agentlife] injected agent registry (%d agents) into %s bootstrap", state.agentRegistry.size, ctx.agentId);
|
|
2320
|
+
} else {
|
|
2321
|
+
console.log("[agentlife] no agents in registry for %s bootstrap", ctx.agentId);
|
|
2322
|
+
}
|
|
2323
|
+
if (ctx.sessionKey) {
|
|
2324
|
+
const snap = ctx.bootstrapFiles.map((f) => ({ ...f }));
|
|
2325
|
+
state.sessionBootstrapSnapshots.set(ctx.sessionKey, snap);
|
|
2326
|
+
persistBootstrapSnapshot(state, ctx.sessionKey, ctx.agentId, snap);
|
|
2327
|
+
}
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
const agentsIdx = ctx.bootstrapFiles.findIndex((f) => f.name === "AGENTS.md");
|
|
2331
|
+
const skipAgentsMd = ctx.agentId && SKIP_AGENTS_MD_INJECTION.has(ctx.agentId);
|
|
2332
|
+
if (agentsIdx >= 0 && !skipAgentsMd && ctx.bootstrapFiles[agentsIdx].path !== "agentlife://agents") {
|
|
2333
|
+
const existing = !ctx.bootstrapFiles[agentsIdx].missing && ctx.bootstrapFiles[agentsIdx].content || "";
|
|
2334
|
+
const isGenericTemplate = /heartbeat|group.?chat|discord|whatsapp|emoji.?react/i.test(existing);
|
|
2335
|
+
const isDelegationTemplate = /sessions_spawn|orchestration\s+space|always\s+delegate/i.test(existing);
|
|
2336
|
+
let roleContent = existing;
|
|
2337
|
+
if ((isGenericTemplate || isDelegationTemplate) && existing.length > 200) {
|
|
2338
|
+
const genericHeaders = /^##\s*(First Run|Every Session|Memory|Safety|External|Group Chat|Tools|Make It Yours|Heartbeat)/mi;
|
|
2339
|
+
const genericStart = existing.search(genericHeaders);
|
|
2340
|
+
roleContent = genericStart > 0 ? existing.slice(0, genericStart).trim() : "";
|
|
2341
|
+
roleContent = roleContent.replace(/^#\s*AGENTS\.md.*$/m, "").trim();
|
|
2342
|
+
console.log("[agentlife] stripped generic template from AGENTS.md for %s (%d → %d chars)", ctx.agentId ?? "unknown", existing.length, roleContent.length);
|
|
2343
|
+
}
|
|
2344
|
+
ctx.bootstrapFiles[agentsIdx] = {
|
|
2345
|
+
name: "AGENTS.md",
|
|
2346
|
+
path: "agentlife://agents",
|
|
2347
|
+
content: (roleContent ? roleContent + `
|
|
2348
|
+
|
|
2349
|
+
` : "") + PLATFORM_AGENTS_MD,
|
|
2350
|
+
missing: false
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
const STRIP_TEMPLATES = ["IDENTITY.md", "USER.md", "HEARTBEAT.md", "BOOTSTRAP.md"];
|
|
2354
|
+
for (const fileName of STRIP_TEMPLATES) {
|
|
2355
|
+
const fi = ctx.bootstrapFiles.findIndex((f) => f.name === fileName);
|
|
2356
|
+
if (fi >= 0 && !ctx.bootstrapFiles[fi].missing) {
|
|
2357
|
+
ctx.bootstrapFiles[fi] = { name: fileName, path: ctx.bootstrapFiles[fi].path, content: "", missing: true };
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
const SOUL_MARKER = "# Agent Life Voice";
|
|
2361
|
+
const soulIdx = ctx.bootstrapFiles.findIndex((f) => f.name === "SOUL.md");
|
|
2362
|
+
if (soulIdx >= 0 && ctx.bootstrapFiles[soulIdx].path !== "agentlife://soul") {
|
|
2363
|
+
const userSoulMd = !ctx.bootstrapFiles[soulIdx].missing && ctx.bootstrapFiles[soulIdx].content || "";
|
|
2364
|
+
const wrapped = buildPlatformSoulMd(userSoulMd);
|
|
2365
|
+
ctx.bootstrapFiles[soulIdx] = {
|
|
2366
|
+
name: "SOUL.md",
|
|
2367
|
+
path: "agentlife://soul",
|
|
2368
|
+
content: wrapped,
|
|
2369
|
+
missing: false
|
|
2370
|
+
};
|
|
2371
|
+
const agentLabel = ctx.agentId ?? "unknown";
|
|
2372
|
+
console.log("[agentlife:diff] %s", JSON.stringify({
|
|
2373
|
+
file: "SOUL.md",
|
|
2374
|
+
agent: agentLabel,
|
|
2375
|
+
before: userSoulMd.slice(0, 3000),
|
|
2376
|
+
after: wrapped.slice(0, 3000),
|
|
2377
|
+
beforeLen: userSoulMd.length,
|
|
2378
|
+
afterLen: wrapped.length
|
|
2379
|
+
}));
|
|
2380
|
+
}
|
|
2381
|
+
const idx = ctx.bootstrapFiles.findIndex((f) => f.name === "TOOLS.md");
|
|
2382
|
+
const toolsAlreadyInjected = idx >= 0 && ctx.bootstrapFiles[idx].path === "agentlife://a2ui-guidance";
|
|
2383
|
+
if (!toolsAlreadyInjected) {
|
|
2384
|
+
const originalTools = idx >= 0 && !ctx.bootstrapFiles[idx].missing && ctx.bootstrapFiles[idx].content || "";
|
|
2385
|
+
const entry = {
|
|
2386
|
+
name: "TOOLS.md",
|
|
2387
|
+
path: "agentlife://a2ui-guidance",
|
|
2388
|
+
content: TENAZITAS_GUIDANCE,
|
|
2389
|
+
missing: false
|
|
2390
|
+
};
|
|
2391
|
+
if (idx >= 0) {
|
|
2392
|
+
ctx.bootstrapFiles[idx] = entry;
|
|
2393
|
+
} else {
|
|
2394
|
+
ctx.bootstrapFiles.push(entry);
|
|
2395
|
+
}
|
|
2396
|
+
const agentLabel = ctx.agentId ?? "unknown";
|
|
2397
|
+
console.log("[agentlife:diff] %s", JSON.stringify({
|
|
2398
|
+
file: "TOOLS.md",
|
|
2399
|
+
agent: agentLabel,
|
|
2400
|
+
before: originalTools.slice(0, 3000),
|
|
2401
|
+
after: TENAZITAS_GUIDANCE.slice(0, 3000),
|
|
2402
|
+
beforeLen: originalTools.length,
|
|
2403
|
+
afterLen: TENAZITAS_GUIDANCE.length
|
|
2404
|
+
}));
|
|
2405
|
+
}
|
|
2406
|
+
if (ctx.sessionKey) {
|
|
2407
|
+
const snap = ctx.bootstrapFiles.map((f) => ({ ...f }));
|
|
2408
|
+
state.sessionBootstrapSnapshots.set(ctx.sessionKey, snap);
|
|
2409
|
+
persistBootstrapSnapshot(state, ctx.sessionKey, ctx.agentId, snap);
|
|
2410
|
+
}
|
|
2411
|
+
}, { name: "agentlife-a2ui-guidance", description: "Inject A2UI component catalog and dashboard rules" });
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// usage.ts
|
|
2415
|
+
var MODEL_COSTS = {
|
|
2416
|
+
"claude-opus": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
2417
|
+
"claude-sonnet": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
2418
|
+
"claude-haiku": { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
|
|
2419
|
+
"gpt-4o": { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 2.5 },
|
|
2420
|
+
"gpt-4.1": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 2 },
|
|
2421
|
+
o3: { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 2 },
|
|
2422
|
+
"o4-mini": { input: 1.1, output: 4.4, cacheRead: 0.275, cacheWrite: 1.1 },
|
|
2423
|
+
"gemini-2.5-pro": { input: 1.25, output: 10, cacheRead: 0.315, cacheWrite: 1.25 },
|
|
2424
|
+
"gemini-2.5-flash": { input: 0.15, output: 0.6, cacheRead: 0.0375, cacheWrite: 0.15 }
|
|
2425
|
+
};
|
|
2426
|
+
function estimateCost(model, usage) {
|
|
2427
|
+
if (!model)
|
|
2428
|
+
return 0;
|
|
2429
|
+
const m = model.toLowerCase();
|
|
2430
|
+
let costs;
|
|
2431
|
+
for (const [prefix, c] of Object.entries(MODEL_COSTS)) {
|
|
2432
|
+
if (m.includes(prefix)) {
|
|
2433
|
+
costs = c;
|
|
2434
|
+
break;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
if (!costs)
|
|
2438
|
+
return 0;
|
|
2439
|
+
return ((usage.input ?? 0) * costs.input + (usage.output ?? 0) * costs.output + (usage.cacheRead ?? 0) * costs.cacheRead + (usage.cacheWrite ?? 0) * costs.cacheWrite) / 1e6;
|
|
2440
|
+
}
|
|
2441
|
+
function recordUsageLedger(state, sessionKey, agentId, provider, model, usage) {
|
|
2442
|
+
if (!usage)
|
|
2443
|
+
return;
|
|
2444
|
+
const input = usage.input ?? 0;
|
|
2445
|
+
const output = usage.output ?? 0;
|
|
2446
|
+
const cacheRead = usage.cacheRead ?? 0;
|
|
2447
|
+
const cacheWrite = usage.cacheWrite ?? 0;
|
|
2448
|
+
const total = usage.total ?? input + output + cacheRead + cacheWrite;
|
|
2449
|
+
if (total === 0)
|
|
2450
|
+
return;
|
|
2451
|
+
const cost = estimateCost(model, usage);
|
|
2452
|
+
try {
|
|
2453
|
+
getOrCreateHistoryDb(state).prepare(`INSERT INTO usage_ledger (sessionKey,agentId,provider,model,inputTokens,outputTokens,cacheReadTokens,cacheWriteTokens,totalTokens,estimatedCost,createdAt)
|
|
2454
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run(sessionKey, agentId, provider ?? null, model ?? null, input, output, cacheRead, cacheWrite, total, cost, Date.now());
|
|
2455
|
+
} catch (err) {
|
|
2456
|
+
console.warn("[agentlife] failed to record usage:", err?.message);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
function accumulateUsage(state, sessionKey, agentId, model, usage) {
|
|
2460
|
+
if (!usage)
|
|
2461
|
+
return;
|
|
2462
|
+
const input = usage.input ?? 0;
|
|
2463
|
+
const output = usage.output ?? 0;
|
|
2464
|
+
const cacheRead = usage.cacheRead ?? 0;
|
|
2465
|
+
const cacheWrite = usage.cacheWrite ?? 0;
|
|
2466
|
+
const total = usage.total ?? input + output + cacheRead + cacheWrite;
|
|
2467
|
+
if (total === 0)
|
|
2468
|
+
return;
|
|
2469
|
+
const pending = state.usageAccumulator.get(sessionKey) ?? {
|
|
2470
|
+
inputTokens: 0,
|
|
2471
|
+
outputTokens: 0,
|
|
2472
|
+
cacheReadTokens: 0,
|
|
2473
|
+
cacheWriteTokens: 0,
|
|
2474
|
+
totalTokens: 0,
|
|
2475
|
+
estimatedCost: 0,
|
|
2476
|
+
llmCalls: 0,
|
|
2477
|
+
agentId: null
|
|
2478
|
+
};
|
|
2479
|
+
pending.inputTokens += input;
|
|
2480
|
+
pending.outputTokens += output;
|
|
2481
|
+
pending.cacheReadTokens += cacheRead;
|
|
2482
|
+
pending.cacheWriteTokens += cacheWrite;
|
|
2483
|
+
pending.totalTokens += total;
|
|
2484
|
+
pending.estimatedCost += estimateCost(model, usage);
|
|
2485
|
+
pending.llmCalls += 1;
|
|
2486
|
+
pending.agentId = agentId ?? pending.agentId;
|
|
2487
|
+
state.usageAccumulator.set(sessionKey, pending);
|
|
2488
|
+
}
|
|
2489
|
+
function drainAccumulatorToSurfaces(state, sessionKey, surfaceIds) {
|
|
2490
|
+
const pending = state.usageAccumulator.get(sessionKey);
|
|
2491
|
+
if (!pending || pending.llmCalls === 0 || surfaceIds.length === 0)
|
|
2492
|
+
return;
|
|
2493
|
+
const share = surfaceIds.length;
|
|
2494
|
+
const now = Date.now();
|
|
2495
|
+
try {
|
|
2496
|
+
const db = getOrCreateHistoryDb(state);
|
|
2497
|
+
const stmt = db.prepare(`INSERT INTO surface_usage (surfaceId,agentId,sessionKey,inputTokens,outputTokens,cacheReadTokens,cacheWriteTokens,totalTokens,estimatedCost,llmCalls,createdAt)
|
|
2498
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)`);
|
|
2499
|
+
for (const sid of surfaceIds) {
|
|
2500
|
+
stmt.run(sid, pending.agentId, sessionKey, Math.round(pending.inputTokens / share), Math.round(pending.outputTokens / share), Math.round(pending.cacheReadTokens / share), Math.round(pending.cacheWriteTokens / share), Math.round(pending.totalTokens / share), pending.estimatedCost / share, pending.llmCalls, now);
|
|
2501
|
+
}
|
|
2502
|
+
} catch (err) {
|
|
2503
|
+
console.warn("[agentlife] failed to record surface usage:", err?.message);
|
|
2504
|
+
}
|
|
2505
|
+
state.usageAccumulator.delete(sessionKey);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// distribution.ts
|
|
2509
|
+
var broadcastRef = null;
|
|
2510
|
+
function captureBridge(context) {
|
|
2511
|
+
if (!broadcastRef && context?.broadcast) {
|
|
2512
|
+
broadcastRef = context.broadcast;
|
|
2513
|
+
console.log("[agentlife:distribution] broadcast bridge captured");
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
function broadcastSurface(dsl) {
|
|
2517
|
+
if (!broadcastRef)
|
|
2518
|
+
return;
|
|
2519
|
+
broadcastRef("agentlife.surface.push", { dsl, timestamp: Date.now() });
|
|
2520
|
+
}
|
|
2521
|
+
function broadcastDelete(surfaceId) {
|
|
2522
|
+
if (!broadcastRef)
|
|
2523
|
+
return;
|
|
2524
|
+
broadcastRef("agentlife.surface.delete", { surfaceId, timestamp: Date.now() });
|
|
2525
|
+
}
|
|
2526
|
+
function broadcastInput(message, sessionKey) {
|
|
2527
|
+
if (!broadcastRef)
|
|
2528
|
+
return;
|
|
2529
|
+
broadcastRef("agentlife.input", { message, sessionKey, timestamp: Date.now() });
|
|
2530
|
+
}
|
|
2531
|
+
function broadcastActivity(event, sessionKey, agentId, payload) {
|
|
2532
|
+
if (!broadcastRef)
|
|
2533
|
+
return;
|
|
2534
|
+
broadcastRef("agentlife.activity", { event, sessionKey, agentId, ...payload, timestamp: Date.now() });
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// notifications.ts
|
|
2538
|
+
import * as fs4 from "node:fs";
|
|
2539
|
+
import * as path7 from "node:path";
|
|
2540
|
+
var config;
|
|
2541
|
+
var recentNotifications = new Map;
|
|
2542
|
+
var DEBOUNCE_MS = 60000;
|
|
2543
|
+
function loadConfig() {
|
|
2544
|
+
if (config !== undefined)
|
|
2545
|
+
return config;
|
|
2546
|
+
try {
|
|
2547
|
+
const configPath = path7.join(process.env.HOME ?? "~", ".openclaw", "agentlife", "notification-config.json");
|
|
2548
|
+
const raw = fs4.readFileSync(configPath, "utf-8");
|
|
2549
|
+
const parsed = JSON.parse(raw);
|
|
2550
|
+
if (parsed.serverUrl && parsed.apiKey) {
|
|
2551
|
+
config = { serverUrl: parsed.serverUrl, apiKey: parsed.apiKey };
|
|
2552
|
+
console.log("[agentlife:notify] Notification config loaded from %s", configPath);
|
|
2553
|
+
return config;
|
|
2554
|
+
}
|
|
2555
|
+
console.warn("[agentlife:notify] Config missing serverUrl or apiKey");
|
|
2556
|
+
config = null;
|
|
2557
|
+
return null;
|
|
2558
|
+
} catch (e) {
|
|
2559
|
+
if (e?.code !== "ENOENT") {
|
|
2560
|
+
console.warn("[agentlife:notify] Failed to load notification config: %s", e?.message);
|
|
2561
|
+
}
|
|
2562
|
+
config = null;
|
|
2563
|
+
return null;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function notifyWidgetEvent(_state, event, surfaceId, title, body) {
|
|
2567
|
+
const cfg = loadConfig();
|
|
2568
|
+
if (!cfg)
|
|
2569
|
+
return;
|
|
2570
|
+
const now = Date.now();
|
|
2571
|
+
const lastNotified = recentNotifications.get(surfaceId);
|
|
2572
|
+
if (lastNotified && now - lastNotified < DEBOUNCE_MS)
|
|
2573
|
+
return;
|
|
2574
|
+
recentNotifications.set(surfaceId, now);
|
|
2575
|
+
if (recentNotifications.size > 100) {
|
|
2576
|
+
for (const [sid, ts] of recentNotifications) {
|
|
2577
|
+
if (now - ts > DEBOUNCE_MS)
|
|
2578
|
+
recentNotifications.delete(sid);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
const url = `${cfg.serverUrl.replace(/\/+$/, "")}/api/v1/notifications/send`;
|
|
2582
|
+
fetch(url, {
|
|
2583
|
+
method: "POST",
|
|
2584
|
+
headers: {
|
|
2585
|
+
"Content-Type": "application/json",
|
|
2586
|
+
Authorization: `Bearer ${cfg.apiKey}`
|
|
2587
|
+
},
|
|
2588
|
+
body: JSON.stringify({ title, body, data: { event, surfaceId } }),
|
|
2589
|
+
signal: AbortSignal.timeout(1e4)
|
|
2590
|
+
}).then((res) => {
|
|
2591
|
+
if (!res.ok) {
|
|
2592
|
+
console.warn("[agentlife:notify] Server responded %d for %s", res.status, surfaceId);
|
|
2593
|
+
}
|
|
2594
|
+
}).catch((err) => {
|
|
2595
|
+
console.warn("[agentlife:notify] Server failed for %s: %s", surfaceId, err?.message);
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
// tools/widget-push.ts
|
|
2600
|
+
function registerWidgetPushTool(api, state) {
|
|
2601
|
+
api.registerTool((ctx) => {
|
|
2602
|
+
const sessionKey = ctx?.sessionKey ?? null;
|
|
2603
|
+
const agentId = ctx?.agentId ?? null;
|
|
2604
|
+
return {
|
|
2605
|
+
name: "agentlife_push",
|
|
2606
|
+
label: "Widget Push",
|
|
2607
|
+
description: "Push or update widgets on the AgentLife dashboard using Widget DSL. " + "Each DSL block defines a surface (widget) with content, goal, followup schedule, and context. " + "Separate multiple blocks with ---.",
|
|
2608
|
+
parameters: {
|
|
2609
|
+
type: "object",
|
|
2610
|
+
properties: {
|
|
2611
|
+
dsl: {
|
|
2612
|
+
type: "string",
|
|
2613
|
+
description: "Widget DSL — one or more surface blocks separated by ---"
|
|
2614
|
+
}
|
|
2615
|
+
},
|
|
2616
|
+
required: ["dsl"]
|
|
2617
|
+
},
|
|
2618
|
+
async execute(_toolCallId, params) {
|
|
2619
|
+
const dsl = params.dsl;
|
|
2620
|
+
if (!dsl || typeof dsl !== "string") {
|
|
2621
|
+
return { content: [{ type: "text", text: "Error: dsl parameter required" }] };
|
|
2622
|
+
}
|
|
2623
|
+
if (!agentId) {
|
|
2624
|
+
console.log("[agentlife] agentlife_push tool has no agentId from context");
|
|
2625
|
+
}
|
|
2626
|
+
const blocks = dsl.split(/\n---(?:\n|$)/).filter((b) => b.trim().length > 0);
|
|
2627
|
+
const pushSurfaceIds = [];
|
|
2628
|
+
const newSurfaceIds = [];
|
|
2629
|
+
for (const block of blocks) {
|
|
2630
|
+
const match = block.match(/^surface\s+(\S+)/m);
|
|
2631
|
+
if (match) {
|
|
2632
|
+
pushSurfaceIds.push(match[1]);
|
|
2633
|
+
if (!state.surfaceDb?.has(match[1]))
|
|
2634
|
+
newSurfaceIds.push(match[1]);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
const dslResults = processDslBlock(state, dsl);
|
|
2638
|
+
if (agentId && state.surfaceDb) {
|
|
2639
|
+
for (const sid of pushSurfaceIds) {
|
|
2640
|
+
state.surfaceDb.setAgentId(sid, agentId);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
broadcastSurface(dsl);
|
|
2644
|
+
for (const block of blocks) {
|
|
2645
|
+
if (block.trim().startsWith("delete ")) {
|
|
2646
|
+
const deleteMatch = block.trim().match(/^delete\s+(\S+)/);
|
|
2647
|
+
if (deleteMatch)
|
|
2648
|
+
broadcastDelete(deleteMatch[1]);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
for (const sid of newSurfaceIds) {
|
|
2652
|
+
const meta = state.surfaceDb?.get(sid);
|
|
2653
|
+
if (!meta)
|
|
2654
|
+
continue;
|
|
2655
|
+
const headerLine = meta.lines[0] ?? "";
|
|
2656
|
+
if (/\boverlay\b/.test(headerLine) || /\binput\b/.test(headerLine) || /\bstate=loading\b/.test(headerLine))
|
|
2657
|
+
continue;
|
|
2658
|
+
const notifTitle = capitalize(agentId ?? "agent");
|
|
2659
|
+
const notifBody = extractWidgetText(meta) ?? "New update";
|
|
2660
|
+
notifyWidgetEvent(state, "surface_created", sid, notifTitle, notifBody);
|
|
2661
|
+
}
|
|
2662
|
+
for (const result of dslResults) {
|
|
2663
|
+
const meta = state.surfaceDb?.get(result.surfaceId);
|
|
2664
|
+
if (!meta)
|
|
2665
|
+
continue;
|
|
2666
|
+
if (result.stateTransition?.endsWith("->terminal")) {
|
|
2667
|
+
removeFollowup(state, result.surfaceId);
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
if (result.followupRemoved) {
|
|
2671
|
+
removeFollowup(state, result.surfaceId);
|
|
2672
|
+
} else if (meta.followup && (result.followupChanged || result.isNew)) {
|
|
2673
|
+
scheduleFollowup(state, result.surfaceId, meta.followup, agentId);
|
|
2674
|
+
}
|
|
2675
|
+
if (meta.context) {
|
|
2676
|
+
autoRegisterInfraFromContext(state, result.surfaceId, agentId, meta.context);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
if (sessionKey && pushSurfaceIds.length > 0) {
|
|
2680
|
+
drainAccumulatorToSurfaces(state, sessionKey, pushSurfaceIds);
|
|
2681
|
+
}
|
|
2682
|
+
const summary = pushSurfaceIds.length === 1 ? `Widget ${pushSurfaceIds[0]} updated` : `${pushSurfaceIds.length} widgets updated`;
|
|
2683
|
+
return { content: [{ type: "text", text: summary }] };
|
|
2684
|
+
}
|
|
2685
|
+
};
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
function capitalize(s) {
|
|
2689
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
2690
|
+
}
|
|
2691
|
+
function extractWidgetText(meta) {
|
|
2692
|
+
const headingLine = meta.lines.find((l) => /^\s*text\s+"[^"]+"\s+h[1-4]/.test(l));
|
|
2693
|
+
if (headingLine)
|
|
2694
|
+
return headingLine.match(/text\s+"([^"]+)"/)?.[1] ?? null;
|
|
2695
|
+
const goalLine = meta.lines.find((l) => /^\s*goal\s+"[^"]+"/.test(l));
|
|
2696
|
+
if (goalLine)
|
|
2697
|
+
return goalLine.match(/goal\s+"([^"]+)"/)?.[1] ?? null;
|
|
2698
|
+
const textLine = meta.lines.find((l) => /^\s*text\s+"[^"]+"/.test(l));
|
|
2699
|
+
if (textLine)
|
|
2700
|
+
return textLine.match(/text\s+"([^"]+)"/)?.[1] ?? null;
|
|
2701
|
+
return null;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
// hooks/activity-hooks.ts
|
|
2705
|
+
var sessionContext = new Map;
|
|
2706
|
+
function registerActivityHooks(api, state) {
|
|
2707
|
+
api.on("llm_output", (event, ctx) => {
|
|
2708
|
+
if (state.disabled)
|
|
2709
|
+
return;
|
|
2710
|
+
const sessionKey = ctx?.sessionKey ?? event.sessionId ?? "unknown";
|
|
2711
|
+
if (isChannelSession(sessionKey))
|
|
2712
|
+
return;
|
|
2713
|
+
const agentId = ctx?.agentId ?? null;
|
|
2714
|
+
recordUsageLedger(state, sessionKey, agentId, event.provider, event.model, event.usage);
|
|
2715
|
+
accumulateUsage(state, sessionKey, agentId, event.model, event.usage);
|
|
2716
|
+
});
|
|
2717
|
+
api.on("agent_end", (_event, ctx) => {
|
|
2718
|
+
if (state.disabled)
|
|
2719
|
+
return;
|
|
2720
|
+
const sessionKey = ctx?.sessionKey ?? null;
|
|
2721
|
+
if (sessionKey) {
|
|
2722
|
+
state.usageAccumulator.delete(sessionKey);
|
|
2723
|
+
state.sessionBootstrapSnapshots.delete(sessionKey);
|
|
2724
|
+
}
|
|
2725
|
+
});
|
|
2726
|
+
api.on("session_start", (event, ctx) => {
|
|
2727
|
+
if (state.disabled)
|
|
2728
|
+
return;
|
|
2729
|
+
const sessionKey = event.sessionKey ?? ctx?.sessionKey ?? null;
|
|
2730
|
+
if (isChannelSession(sessionKey))
|
|
2731
|
+
return;
|
|
2732
|
+
const agentId = ctx?.agentId ?? null;
|
|
2733
|
+
recordActivity(state, "session_start", sessionKey, agentId, {
|
|
2734
|
+
runId: ctx?.runId ?? null,
|
|
2735
|
+
data: JSON.stringify({ sessionId: event.sessionId, resumedFrom: event.resumedFrom })
|
|
2736
|
+
});
|
|
2737
|
+
const message = event.message ?? ctx?.message ?? "";
|
|
2738
|
+
if (sessionKey && message) {
|
|
2739
|
+
sessionContext.set(sessionKey, { message, startedAt: Date.now() });
|
|
2740
|
+
}
|
|
2741
|
+
const cronMatch = message.match(/^Widget followup for (\S+):/);
|
|
2742
|
+
if (cronMatch) {
|
|
2743
|
+
const cronSurfaceId = cronMatch[1];
|
|
2744
|
+
recordSurfaceEvent(state, cronSurfaceId, "cron_fired", undefined, agentId ?? undefined);
|
|
2745
|
+
const meta = state.surfaceDb?.get(cronSurfaceId);
|
|
2746
|
+
const agentLabel = agentId ? agentId.charAt(0).toUpperCase() + agentId.slice(1) : "Agent";
|
|
2747
|
+
const surfaceTitle = extractCronTitle(meta) ?? "Updated";
|
|
2748
|
+
notifyWidgetEvent(state, "cron_followup", cronSurfaceId, agentLabel, surfaceTitle);
|
|
2749
|
+
if (agentId && state.runCommand) {
|
|
2750
|
+
state.runCommand(["openclaw", "agent", "--agent", agentId, "--message", message, "--json"], { timeoutMs: 120000 }).catch((e) => console.warn("[agentlife] followup redirect failed for %s: %s", cronSurfaceId, e?.message));
|
|
2751
|
+
console.log("[agentlife] followup redirected to agent:%s:main for %s", agentId, cronSurfaceId);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
});
|
|
2755
|
+
api.on("llm_output", (event, ctx) => {
|
|
2756
|
+
if (state.disabled)
|
|
2757
|
+
return;
|
|
2758
|
+
const sessionKey = ctx?.sessionKey ?? event.sessionId ?? "unknown";
|
|
2759
|
+
if (isChannelSession(sessionKey))
|
|
2760
|
+
return;
|
|
2761
|
+
const agentId = ctx?.agentId ?? null;
|
|
2762
|
+
const texts = event.assistantTexts ?? [];
|
|
2763
|
+
const lastAssistant = event.lastAssistant;
|
|
2764
|
+
let thinking = null;
|
|
2765
|
+
if (lastAssistant && typeof lastAssistant === "object") {
|
|
2766
|
+
const content = Array.isArray(lastAssistant.content) ? lastAssistant.content : [];
|
|
2767
|
+
const thinkingBlocks = content.filter((b) => b.type === "thinking" && b.thinking).map((b) => b.thinking);
|
|
2768
|
+
if (thinkingBlocks.length > 0)
|
|
2769
|
+
thinking = thinkingBlocks.join(`
|
|
2770
|
+
`);
|
|
2771
|
+
}
|
|
2772
|
+
if (texts.length > 0 || thinking) {
|
|
2773
|
+
recordActivity(state, "llm_response", sessionKey, agentId, {
|
|
2774
|
+
runId: event.runId ?? ctx?.runId ?? null,
|
|
2775
|
+
summary: texts.join(`
|
|
2776
|
+
`).slice(0, 500) || null,
|
|
2777
|
+
data: JSON.stringify({
|
|
2778
|
+
texts,
|
|
2779
|
+
thinking,
|
|
2780
|
+
model: event.model,
|
|
2781
|
+
provider: event.provider,
|
|
2782
|
+
usage: event.usage
|
|
2783
|
+
})
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
}, { priority: 100 });
|
|
2787
|
+
api.on("before_tool_call", (event, ctx) => {
|
|
2788
|
+
if (state.disabled)
|
|
2789
|
+
return;
|
|
2790
|
+
const sessionKey = ctx?.sessionKey ?? null;
|
|
2791
|
+
if (isChannelSession(sessionKey))
|
|
2792
|
+
return;
|
|
2793
|
+
const agentId = ctx?.agentId ?? null;
|
|
2794
|
+
const toolName = event.toolName ?? ctx?.toolName;
|
|
2795
|
+
const toolCallId = event.toolCallId ?? ctx?.toolCallId ?? null;
|
|
2796
|
+
const runId = event.runId ?? ctx?.runId ?? null;
|
|
2797
|
+
recordActivity(state, "tool_start", sessionKey, agentId, {
|
|
2798
|
+
toolName,
|
|
2799
|
+
runId,
|
|
2800
|
+
summary: summarizeParams(event.params),
|
|
2801
|
+
data: JSON.stringify({ params: event.params, toolCallId })
|
|
2802
|
+
});
|
|
2803
|
+
const broadcastExtra = {};
|
|
2804
|
+
if (toolName === "sessions_send" && typeof event.params?.sessionKey === "string") {
|
|
2805
|
+
const match = event.params.sessionKey.match(/^agent:([^:]+):/);
|
|
2806
|
+
if (match)
|
|
2807
|
+
broadcastExtra.targetAgentId = match[1];
|
|
2808
|
+
}
|
|
2809
|
+
broadcastActivity("tool_start", sessionKey, agentId, {
|
|
2810
|
+
toolName,
|
|
2811
|
+
toolCallId,
|
|
2812
|
+
runId,
|
|
2813
|
+
inputSummary: summarizeParams(event.params),
|
|
2814
|
+
...broadcastExtra
|
|
2815
|
+
});
|
|
2816
|
+
});
|
|
2817
|
+
api.on("after_tool_call", (event, ctx) => {
|
|
2818
|
+
if (state.disabled)
|
|
2819
|
+
return;
|
|
2820
|
+
const sessionKey = ctx?.sessionKey ?? null;
|
|
2821
|
+
if (isChannelSession(sessionKey))
|
|
2822
|
+
return;
|
|
2823
|
+
const agentId = ctx?.agentId ?? null;
|
|
2824
|
+
const isError = !!event.error;
|
|
2825
|
+
const toolName = event.toolName ?? ctx?.toolName;
|
|
2826
|
+
const toolCallId = event.toolCallId ?? ctx?.toolCallId ?? null;
|
|
2827
|
+
const runId = event.runId ?? ctx?.runId ?? null;
|
|
2828
|
+
let canvasDsl = null;
|
|
2829
|
+
if (event.toolName === "agentlife_push" && typeof event.params?.dsl === "string") {
|
|
2830
|
+
canvasDsl = event.params.dsl;
|
|
2831
|
+
}
|
|
2832
|
+
const resultStr = typeof event.result === "string" ? event.result : event.result != null ? JSON.stringify(event.result) : null;
|
|
2833
|
+
recordActivity(state, "tool_end", sessionKey, agentId, {
|
|
2834
|
+
toolName,
|
|
2835
|
+
runId,
|
|
2836
|
+
summary: isError ? event.error : resultStr?.slice(0, 300) ?? null,
|
|
2837
|
+
data: JSON.stringify({
|
|
2838
|
+
params: event.params,
|
|
2839
|
+
result: resultStr?.slice(0, 1e4),
|
|
2840
|
+
error: event.error,
|
|
2841
|
+
isError,
|
|
2842
|
+
durationMs: event.durationMs,
|
|
2843
|
+
toolCallId,
|
|
2844
|
+
canvasDsl
|
|
2845
|
+
})
|
|
2846
|
+
});
|
|
2847
|
+
broadcastActivity("tool_end", sessionKey, agentId, {
|
|
2848
|
+
toolName,
|
|
2849
|
+
toolCallId,
|
|
2850
|
+
runId,
|
|
2851
|
+
isError,
|
|
2852
|
+
resultSummary: isError ? event.error : resultStr?.slice(0, 300) ?? null
|
|
2853
|
+
});
|
|
2854
|
+
if (event.toolName === "write" || event.toolName === "exec") {
|
|
2855
|
+
const filePath = event.params?.path ?? event.params?.command ?? "";
|
|
2856
|
+
if (typeof filePath === "string" && filePath.includes("workspace-") && filePath.includes("AGENTS.md")) {
|
|
2857
|
+
const content = event.params?.content ?? "";
|
|
2858
|
+
const issues = [];
|
|
2859
|
+
if (typeof content === "string" && content.length > 0) {
|
|
2860
|
+
if (!/followup/i.test(content))
|
|
2861
|
+
issues.push("no followup strategy");
|
|
2862
|
+
if (!/goal/i.test(content))
|
|
2863
|
+
issues.push("no goal patterns");
|
|
2864
|
+
if (!/approv/i.test(content))
|
|
2865
|
+
issues.push("no approval patterns");
|
|
2866
|
+
if (issues.length > 0) {
|
|
2867
|
+
console.warn("[agentlife:builder] AGENTS.md write missing: %s", issues.join(", "));
|
|
2868
|
+
recordActivity(state, "builder_validation", sessionKey, agentId, {
|
|
2869
|
+
summary: `AGENTS.md missing: ${issues.join(", ")}`,
|
|
2870
|
+
data: JSON.stringify({ file: filePath, issues })
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}, { priority: 100 });
|
|
2877
|
+
api.on("agent_end", (event, ctx) => {
|
|
2878
|
+
if (state.disabled)
|
|
2879
|
+
return;
|
|
2880
|
+
const sessionKey = ctx?.sessionKey ?? null;
|
|
2881
|
+
const agentId = ctx?.agentId ?? null;
|
|
2882
|
+
recordActivity(state, "agent_end", sessionKey, agentId, {
|
|
2883
|
+
runId: ctx?.runId ?? null,
|
|
2884
|
+
data: JSON.stringify({
|
|
2885
|
+
success: event.success,
|
|
2886
|
+
error: event.error,
|
|
2887
|
+
durationMs: event.durationMs
|
|
2888
|
+
})
|
|
2889
|
+
});
|
|
2890
|
+
if (agentId && state.surfaceDb) {
|
|
2891
|
+
for (const [sid, meta] of state.surfaceDb.entries()) {
|
|
2892
|
+
if (state.surfaceDb.getAgentId(sid) !== agentId)
|
|
2893
|
+
continue;
|
|
2894
|
+
const headerLine = meta.lines[0] ?? "";
|
|
2895
|
+
if (!/\bstate=loading\b/.test(headerLine))
|
|
2896
|
+
continue;
|
|
2897
|
+
meta.lines[0] = headerLine.replace(/\bstate=loading\b/, "state=error");
|
|
2898
|
+
state.surfaceDb.set(sid, { ...meta, updatedAt: Date.now() });
|
|
2899
|
+
recordSurfaceEvent(state, sid, "loading_resolved_error", undefined, agentId, JSON.stringify({ reason: event.error ?? "agent ended with orphaned loading", success: event.success, runId: ctx?.runId }));
|
|
2900
|
+
console.log("[agentlife] resolved orphaned loading surface %s → error (agent %s, success=%s)", sid, agentId, event.success);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
if (agentId && sessionKey && state.surfaceDb) {
|
|
2904
|
+
persistSessionTrace(state, sessionKey, agentId, event.durationMs);
|
|
2905
|
+
}
|
|
2906
|
+
if (sessionKey)
|
|
2907
|
+
sessionContext.delete(sessionKey);
|
|
2908
|
+
}, { priority: 100 });
|
|
2909
|
+
api.on("message_received", (event, ctx) => {
|
|
2910
|
+
if (state.disabled)
|
|
2911
|
+
return;
|
|
2912
|
+
recordActivity(state, "message_received", null, null, {
|
|
2913
|
+
summary: (event.content ?? "").slice(0, 500),
|
|
2914
|
+
data: JSON.stringify({
|
|
2915
|
+
from: event.from,
|
|
2916
|
+
channelId: ctx?.channelId,
|
|
2917
|
+
accountId: ctx?.accountId,
|
|
2918
|
+
timestamp: event.timestamp
|
|
2919
|
+
})
|
|
2920
|
+
});
|
|
2921
|
+
});
|
|
2922
|
+
api.on("subagent_spawned", (event, ctx) => {
|
|
2923
|
+
if (state.disabled)
|
|
2924
|
+
return;
|
|
2925
|
+
const sessionKey = ctx?.requesterSessionKey ?? null;
|
|
2926
|
+
recordActivity(state, "subagent_spawned", sessionKey, null, {
|
|
2927
|
+
runId: event.runId ?? ctx?.runId ?? null,
|
|
2928
|
+
summary: event.agentId,
|
|
2929
|
+
data: JSON.stringify({
|
|
2930
|
+
childSessionKey: event.childSessionKey,
|
|
2931
|
+
agentId: event.agentId,
|
|
2932
|
+
label: event.label,
|
|
2933
|
+
mode: event.mode
|
|
2934
|
+
})
|
|
2935
|
+
});
|
|
2936
|
+
});
|
|
2937
|
+
api.on("subagent_ended", (event, ctx) => {
|
|
2938
|
+
if (state.disabled)
|
|
2939
|
+
return;
|
|
2940
|
+
const sessionKey = event.targetSessionKey ?? ctx?.childSessionKey ?? null;
|
|
2941
|
+
recordActivity(state, "subagent_ended", sessionKey, null, {
|
|
2942
|
+
runId: event.runId ?? ctx?.runId ?? null,
|
|
2943
|
+
summary: event.outcome ?? event.reason,
|
|
2944
|
+
data: JSON.stringify({
|
|
2945
|
+
targetSessionKey: event.targetSessionKey,
|
|
2946
|
+
reason: event.reason,
|
|
2947
|
+
outcome: event.outcome,
|
|
2948
|
+
error: event.error
|
|
2949
|
+
})
|
|
2950
|
+
});
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
function extractCronTitle(meta) {
|
|
2954
|
+
if (!meta)
|
|
2955
|
+
return null;
|
|
2956
|
+
const headingLine = meta.lines.find((l) => /^\s*text\s+"[^"]+"\s+h[1-4]/.test(l));
|
|
2957
|
+
if (headingLine)
|
|
2958
|
+
return headingLine.match(/text\s+"([^"]+)"/)?.[1] ?? null;
|
|
2959
|
+
const goalLine = meta.lines.find((l) => /^\s*goal\s+"[^"]+"/.test(l));
|
|
2960
|
+
if (goalLine)
|
|
2961
|
+
return goalLine.match(/goal\s+"([^"]+)"/)?.[1] ?? null;
|
|
2962
|
+
const textLine = meta.lines.find((l) => /^\s*text\s+"[^"]+"/.test(l));
|
|
2963
|
+
if (textLine)
|
|
2964
|
+
return textLine.match(/text\s+"([^"]+)"/)?.[1] ?? null;
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
function persistSessionTrace(state, sessionKey, agentId, durationMs) {
|
|
2968
|
+
try {
|
|
2969
|
+
const now = Date.now();
|
|
2970
|
+
const sessionStart = sessionContext.get(sessionKey)?.startedAt ?? (durationMs ? now - durationMs - 5000 : now - 120000);
|
|
2971
|
+
const touchedSurfaces = [];
|
|
2972
|
+
for (const [sid, meta] of state.surfaceDb.entries()) {
|
|
2973
|
+
if (state.surfaceDb.getAgentId(sid) !== agentId)
|
|
2974
|
+
continue;
|
|
2975
|
+
if (meta.updatedAt >= sessionStart) {
|
|
2976
|
+
touchedSurfaces.push(sid);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
if (touchedSurfaces.length === 0)
|
|
2980
|
+
return;
|
|
2981
|
+
const db = getOrCreateHistoryDb(state);
|
|
2982
|
+
const entries = db.prepare(`SELECT ts, sessionKey, agentId, event, toolName, summary, data, runId
|
|
2983
|
+
FROM activity_log
|
|
2984
|
+
WHERE sessionKey = ? AND ts >= ?
|
|
2985
|
+
ORDER BY ts ASC
|
|
2986
|
+
LIMIT 500`).all(sessionKey, sessionStart);
|
|
2987
|
+
if (entries.length === 0)
|
|
2988
|
+
return;
|
|
2989
|
+
const rawMessage = sessionContext.get(sessionKey)?.message ?? "";
|
|
2990
|
+
const cronLabelMatch = rawMessage.match(/\]\s*Widget followup for \S+.*?:\s*(.*)/s);
|
|
2991
|
+
const queryText = cronLabelMatch?.[1]?.trim() || rawMessage.replace(/^\[(?:system|action)[^\]]*\]\s*/i, "").trim();
|
|
2992
|
+
for (const surfaceId of touchedSurfaces) {
|
|
2993
|
+
const traceMetadata = JSON.stringify({
|
|
2994
|
+
query: queryText,
|
|
2995
|
+
agent: agentId,
|
|
2996
|
+
sessionKey,
|
|
2997
|
+
entries: entries.map((e) => ({
|
|
2998
|
+
ts: e.ts,
|
|
2999
|
+
event: e.event,
|
|
3000
|
+
toolName: e.toolName ?? undefined,
|
|
3001
|
+
summary: e.summary ?? undefined,
|
|
3002
|
+
data: e.data ?? undefined,
|
|
3003
|
+
runId: e.runId ?? undefined
|
|
3004
|
+
}))
|
|
3005
|
+
});
|
|
3006
|
+
recordSurfaceEvent(state, surfaceId, "trace", undefined, agentId, traceMetadata);
|
|
3007
|
+
}
|
|
3008
|
+
console.log("[agentlife] persisted trace for %d surfaces (agent=%s, entries=%d)", touchedSurfaces.length, agentId, entries.length);
|
|
3009
|
+
} catch (err) {
|
|
3010
|
+
console.warn("[agentlife] failed to persist session trace:", err?.message);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// hooks/prompt-state.ts
|
|
3015
|
+
function registerPromptStateHook(api, state) {
|
|
3016
|
+
api.on("before_prompt_build", (_event, ctx) => {
|
|
3017
|
+
if (state.disabled)
|
|
3018
|
+
return;
|
|
3019
|
+
if (isChannelSession(ctx?.sessionKey))
|
|
3020
|
+
return;
|
|
3021
|
+
const agentId = ctx?.agentId;
|
|
3022
|
+
const isOrchestrator = agentId === "agentlife";
|
|
3023
|
+
const dashboardState = buildDashboardStateContext(state, agentId);
|
|
3024
|
+
if (!dashboardState) {
|
|
3025
|
+
console.log("[agentlife:prompt-state] agentId=%s — no dashboard state (surfaceDb size=%d)", agentId ?? "NONE", state.surfaceDb?.size ?? 0);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
let registryContext = "";
|
|
3029
|
+
if (isOrchestrator) {
|
|
3030
|
+
registryContext = buildAgentRegistryContext(state) ?? "";
|
|
3031
|
+
}
|
|
3032
|
+
const parts = [];
|
|
3033
|
+
if (dashboardState)
|
|
3034
|
+
parts.push(dashboardState);
|
|
3035
|
+
if (registryContext)
|
|
3036
|
+
parts.push(registryContext);
|
|
3037
|
+
if (parts.length === 0)
|
|
3038
|
+
return;
|
|
3039
|
+
const joined = parts.join(`
|
|
3040
|
+
|
|
3041
|
+
`);
|
|
3042
|
+
console.log(`[agentlife:prompt-state] agentId=%s injecting %d chars (dashboard=%d, registry=%d)
|
|
3043
|
+
%s`, agentId ?? "NONE", joined.length, dashboardState?.length ?? 0, registryContext.length, joined);
|
|
3044
|
+
return {
|
|
3045
|
+
appendSystemContext: joined
|
|
3046
|
+
};
|
|
3047
|
+
});
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
// gateway/agents.ts
|
|
3051
|
+
import * as fs5 from "node:fs";
|
|
3052
|
+
import * as path8 from "node:path";
|
|
3053
|
+
function registerAgentGateway(api, state) {
|
|
3054
|
+
api.registerGatewayMethod("agentlife.createAgent", async ({ params, respond }) => {
|
|
3055
|
+
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
3056
|
+
const name = typeof params?.name === "string" ? params.name.trim() : "";
|
|
3057
|
+
const model = typeof params?.model === "string" ? params.model.trim() : "";
|
|
3058
|
+
const workspace = typeof params?.workspace === "string" ? params.workspace.trim() : "";
|
|
3059
|
+
const description = typeof params?.description === "string" ? params.description.trim() : "";
|
|
3060
|
+
const tools = params?.tools && typeof params.tools === "object" ? params.tools : undefined;
|
|
3061
|
+
const subagents = params?.subagents && typeof params.subagents === "object" ? params.subagents : undefined;
|
|
3062
|
+
const identity = params?.identity && typeof params.identity === "object" ? params.identity : undefined;
|
|
3063
|
+
if (!id) {
|
|
3064
|
+
respond(false, { error: "missing agent id" });
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
3067
|
+
if (!workspace) {
|
|
3068
|
+
respond(false, { error: "missing workspace path" });
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
const cfg = api.runtime.config.loadConfig();
|
|
3072
|
+
const currentList = cfg.agents?.list ?? [];
|
|
3073
|
+
const existing = currentList.find((a) => a.id === id);
|
|
3074
|
+
if (!existing) {
|
|
3075
|
+
const entry = { id, workspace };
|
|
3076
|
+
if (name)
|
|
3077
|
+
entry.name = name;
|
|
3078
|
+
if (model)
|
|
3079
|
+
entry.model = model;
|
|
3080
|
+
if (tools)
|
|
3081
|
+
entry.tools = tools;
|
|
3082
|
+
if (subagents)
|
|
3083
|
+
entry.subagents = subagents;
|
|
3084
|
+
if (identity)
|
|
3085
|
+
entry.identity = identity;
|
|
3086
|
+
const updatedCfg = {
|
|
3087
|
+
...cfg,
|
|
3088
|
+
agents: {
|
|
3089
|
+
...cfg.agents,
|
|
3090
|
+
list: [...currentList, entry]
|
|
3091
|
+
}
|
|
3092
|
+
};
|
|
3093
|
+
await api.runtime.config.writeConfigFile(updatedCfg);
|
|
3094
|
+
console.log("[agentlife] builder created agent: %s (workspace: %s)", id, workspace);
|
|
3095
|
+
} else {
|
|
3096
|
+
let patched = false;
|
|
3097
|
+
if (tools && !existing.tools) {
|
|
3098
|
+
existing.tools = tools;
|
|
3099
|
+
patched = true;
|
|
3100
|
+
}
|
|
3101
|
+
if (subagents && !existing.subagents) {
|
|
3102
|
+
existing.subagents = subagents;
|
|
3103
|
+
patched = true;
|
|
3104
|
+
}
|
|
3105
|
+
if (identity && !existing.identity) {
|
|
3106
|
+
existing.identity = identity;
|
|
3107
|
+
patched = true;
|
|
3108
|
+
}
|
|
3109
|
+
if (model && !existing.model) {
|
|
3110
|
+
existing.model = model;
|
|
3111
|
+
patched = true;
|
|
3112
|
+
}
|
|
3113
|
+
if (patched) {
|
|
3114
|
+
const updatedCfg = { ...cfg, agents: { ...cfg.agents, list: currentList } };
|
|
3115
|
+
await api.runtime.config.writeConfigFile(updatedCfg);
|
|
3116
|
+
console.log("[agentlife] builder patched agent config: %s", id);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
if (description && isUsableDescription(description, id, name)) {
|
|
3120
|
+
state.agentRegistry.set(id, {
|
|
3121
|
+
name: name || id,
|
|
3122
|
+
description,
|
|
3123
|
+
model: model || undefined,
|
|
3124
|
+
createdAt: state.agentRegistry.get(id)?.createdAt ?? Date.now()
|
|
3125
|
+
});
|
|
3126
|
+
await saveRegistryToDisk(state);
|
|
3127
|
+
console.log("[agentlife] registered agent in registry: %s — %s", id, description);
|
|
3128
|
+
} else if (description) {
|
|
3129
|
+
console.log("[agentlife] rejected low-quality description for %s: %s", id, description);
|
|
3130
|
+
}
|
|
3131
|
+
const status = existing ? "exists" : "created";
|
|
3132
|
+
const configFields = [];
|
|
3133
|
+
if (tools)
|
|
3134
|
+
configFields.push("tools");
|
|
3135
|
+
if (subagents)
|
|
3136
|
+
configFields.push("subagents");
|
|
3137
|
+
if (identity)
|
|
3138
|
+
configFields.push("identity");
|
|
3139
|
+
respond(true, { status, id, name, model, workspace, description, ...configFields.length ? { configFields } : {} });
|
|
3140
|
+
});
|
|
3141
|
+
api.registerGatewayMethod("agentlife.deleteAgent", async ({ params, respond }) => {
|
|
3142
|
+
const id = typeof params?.id === "string" ? params.id.trim() : "";
|
|
3143
|
+
if (!id) {
|
|
3144
|
+
respond(false, { error: "missing agent id" });
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
const provisionedIds = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
3148
|
+
if (provisionedIds.has(id)) {
|
|
3149
|
+
respond(false, { error: "cannot delete provisioned agent" });
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
const cleanup = { sessions: 0, cronJobs: 0, historyRows: 0, agentDbDeleted: false };
|
|
3153
|
+
if (state.runCommand) {
|
|
3154
|
+
try {
|
|
3155
|
+
const sessResult = await state.runCommand(["openclaw", "gateway", "call", "sessions.list"], { timeoutMs: 1e4 });
|
|
3156
|
+
const sessions = sessResult?.result?.sessions ?? sessResult?.sessions ?? [];
|
|
3157
|
+
for (const sess of sessions) {
|
|
3158
|
+
if (sess.agentId === id) {
|
|
3159
|
+
try {
|
|
3160
|
+
await state.runCommand(["openclaw", "gateway", "call", "sessions.delete", JSON.stringify({ key: sess.key, deleteTranscript: true })], { timeoutMs: 1e4 });
|
|
3161
|
+
cleanup.sessions++;
|
|
3162
|
+
} catch {}
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
} catch (e) {
|
|
3166
|
+
console.warn("[agentlife] deleteAgent: failed to clean sessions:", e?.message);
|
|
3167
|
+
}
|
|
3168
|
+
try {
|
|
3169
|
+
const cronResult = await state.runCommand(["openclaw", "cron", "list", "--json"], { timeoutMs: 1e4 });
|
|
3170
|
+
const jobs = Array.isArray(cronResult) ? cronResult : cronResult?.jobs ?? [];
|
|
3171
|
+
for (const job of jobs) {
|
|
3172
|
+
if (job.agentId === id) {
|
|
3173
|
+
try {
|
|
3174
|
+
await state.runCommand(["openclaw", "cron", "rm", String(job.id)], { timeoutMs: 1e4 });
|
|
3175
|
+
cleanup.cronJobs++;
|
|
3176
|
+
} catch {}
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
} catch (e) {
|
|
3180
|
+
console.warn("[agentlife] deleteAgent: failed to clean cron jobs:", e?.message);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
try {
|
|
3184
|
+
const histDb = getOrCreateHistoryDb(state);
|
|
3185
|
+
const tables = ["surfaces", "surface_events", "surface_usage", "usage_ledger", "activity_log", "automations"];
|
|
3186
|
+
for (const table of tables) {
|
|
3187
|
+
const result = histDb.prepare(`DELETE FROM ${table} WHERE agentId = ?`).run(id);
|
|
3188
|
+
cleanup.historyRows += result.changes;
|
|
3189
|
+
}
|
|
3190
|
+
} catch (e) {
|
|
3191
|
+
console.warn("[agentlife] deleteAgent: failed to clean history DB:", e?.message);
|
|
3192
|
+
}
|
|
3193
|
+
const agentDb = state.agentDbs.get(id);
|
|
3194
|
+
if (agentDb) {
|
|
3195
|
+
try {
|
|
3196
|
+
agentDb.close();
|
|
3197
|
+
} catch {}
|
|
3198
|
+
state.agentDbs.delete(id);
|
|
3199
|
+
}
|
|
3200
|
+
if (state.dbBaseDir) {
|
|
3201
|
+
const dbPath = path8.join(state.dbBaseDir, `${id}.db`);
|
|
3202
|
+
try {
|
|
3203
|
+
if (fs5.existsSync(dbPath)) {
|
|
3204
|
+
fs5.unlinkSync(dbPath);
|
|
3205
|
+
cleanup.agentDbDeleted = true;
|
|
3206
|
+
}
|
|
3207
|
+
} catch (e) {
|
|
3208
|
+
console.warn("[agentlife] deleteAgent: failed to delete agent DB:", e?.message);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
const cfg = api.runtime.config.loadConfig();
|
|
3212
|
+
const currentList = cfg.agents?.list ?? [];
|
|
3213
|
+
const filtered = currentList.filter((a) => a.id !== id);
|
|
3214
|
+
const removedFromConfig = filtered.length < currentList.length;
|
|
3215
|
+
if (removedFromConfig) {
|
|
3216
|
+
const updatedCfg = {
|
|
3217
|
+
...cfg,
|
|
3218
|
+
agents: {
|
|
3219
|
+
...cfg.agents,
|
|
3220
|
+
list: filtered
|
|
3221
|
+
}
|
|
3222
|
+
};
|
|
3223
|
+
await api.runtime.config.writeConfigFile(updatedCfg);
|
|
3224
|
+
}
|
|
3225
|
+
const removedFromRegistry = state.agentRegistry.delete(id);
|
|
3226
|
+
if (removedFromRegistry) {
|
|
3227
|
+
await saveRegistryToDisk(state);
|
|
3228
|
+
}
|
|
3229
|
+
console.log("[agentlife] deleted agent: %s (config=%s, registry=%s, sessions=%d, cron=%d, historyRows=%d, agentDb=%s)", id, removedFromConfig, removedFromRegistry, cleanup.sessions, cleanup.cronJobs, cleanup.historyRows, cleanup.agentDbDeleted);
|
|
3230
|
+
respond(true, { status: "deleted", id, removedFromConfig, removedFromRegistry, cleanup });
|
|
3231
|
+
});
|
|
3232
|
+
api.registerGatewayMethod("agentlife.agents", ({ respond }) => {
|
|
3233
|
+
const agents = {};
|
|
3234
|
+
for (const [id, entry] of state.agentRegistry.entries()) {
|
|
3235
|
+
agents[id] = { name: entry.name, description: entry.description, model: entry.model };
|
|
3236
|
+
}
|
|
3237
|
+
respond(true, { agents, count: state.agentRegistry.size });
|
|
3238
|
+
}, { scope: "operator.read" });
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
// gateway/db-access.ts
|
|
3242
|
+
function registerDbGateway(api, state) {
|
|
3243
|
+
api.registerGatewayMethod("agentlife.db.exec", ({ params, respond }) => {
|
|
3244
|
+
const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : "";
|
|
3245
|
+
const sql = typeof params?.sql === "string" ? params.sql.trim() : "";
|
|
3246
|
+
const sqlParams = Array.isArray(params?.params) ? params.params : [];
|
|
3247
|
+
if (!agentId)
|
|
3248
|
+
return respond(false, { error: "missing agentId" });
|
|
3249
|
+
if (!sql)
|
|
3250
|
+
return respond(false, { error: "missing sql" });
|
|
3251
|
+
if (/^\s*SELECT\b/i.test(sql))
|
|
3252
|
+
return respond(false, { error: "use agentlife.db.query for SELECT" });
|
|
3253
|
+
try {
|
|
3254
|
+
const db = getOrCreateAgentDb(state, agentId);
|
|
3255
|
+
if (sqlParams.length > 0) {
|
|
3256
|
+
const result = db.prepare(sql).run(...sqlParams);
|
|
3257
|
+
respond(true, { changes: result.changes ?? 0, lastInsertRowid: Number(result.lastInsertRowid ?? 0) });
|
|
3258
|
+
} else {
|
|
3259
|
+
db.exec(sql);
|
|
3260
|
+
respond(true, { changes: 0, lastInsertRowid: 0 });
|
|
3261
|
+
}
|
|
3262
|
+
} catch (err) {
|
|
3263
|
+
respond(false, { error: err?.message ?? "db error" });
|
|
3264
|
+
}
|
|
3265
|
+
});
|
|
3266
|
+
api.registerGatewayMethod("agentlife.db.query", ({ params, respond }) => {
|
|
3267
|
+
const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : "";
|
|
3268
|
+
const sql = typeof params?.sql === "string" ? params.sql.trim() : "";
|
|
3269
|
+
const sqlParams = Array.isArray(params?.params) ? params.params : [];
|
|
3270
|
+
if (!agentId)
|
|
3271
|
+
return respond(false, { error: "missing agentId" });
|
|
3272
|
+
if (!sql)
|
|
3273
|
+
return respond(false, { error: "missing sql" });
|
|
3274
|
+
if (!/^\s*SELECT\b/i.test(sql))
|
|
3275
|
+
return respond(false, { error: "use agentlife.db.exec for non-SELECT" });
|
|
3276
|
+
try {
|
|
3277
|
+
const db = getOrCreateAgentDb(state, agentId);
|
|
3278
|
+
const rows = db.prepare(sql).all(...sqlParams);
|
|
3279
|
+
const MAX = 1000;
|
|
3280
|
+
const truncated = rows.length > MAX;
|
|
3281
|
+
const result = truncated ? rows.slice(0, MAX) : rows;
|
|
3282
|
+
const columns = result.length > 0 ? Object.keys(result[0]) : [];
|
|
3283
|
+
respond(true, { rows: result, columns, count: result.length, truncated });
|
|
3284
|
+
} catch (err) {
|
|
3285
|
+
respond(false, { error: err?.message ?? "db error" });
|
|
3286
|
+
}
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
// gateway/history.ts
|
|
3291
|
+
function registerHistoryGateway(api, state) {
|
|
3292
|
+
api.registerGatewayMethod("agentlife.history", ({ params, respond }) => {
|
|
3293
|
+
try {
|
|
3294
|
+
const db = getOrCreateHistoryDb(state);
|
|
3295
|
+
const conds = [];
|
|
3296
|
+
const bind = [];
|
|
3297
|
+
if (typeof params?.surfaceId === "string" && params.surfaceId) {
|
|
3298
|
+
conds.push("surfaceId = ?");
|
|
3299
|
+
bind.push(params.surfaceId);
|
|
3300
|
+
}
|
|
3301
|
+
if (typeof params?.agentId === "string" && params.agentId) {
|
|
3302
|
+
conds.push("agentId = ?");
|
|
3303
|
+
bind.push(params.agentId);
|
|
3304
|
+
}
|
|
3305
|
+
if (typeof params?.event === "string" && params.event) {
|
|
3306
|
+
conds.push("event = ?");
|
|
3307
|
+
bind.push(params.event);
|
|
3308
|
+
}
|
|
3309
|
+
if (typeof params?.search === "string" && params.search) {
|
|
3310
|
+
conds.push("dsl LIKE ?");
|
|
3311
|
+
bind.push(`%${params.search}%`);
|
|
3312
|
+
}
|
|
3313
|
+
if (typeof params?.since === "number") {
|
|
3314
|
+
conds.push("createdAt >= ?");
|
|
3315
|
+
bind.push(params.since);
|
|
3316
|
+
}
|
|
3317
|
+
if (typeof params?.until === "number") {
|
|
3318
|
+
conds.push("createdAt <= ?");
|
|
3319
|
+
bind.push(params.until);
|
|
3320
|
+
}
|
|
3321
|
+
const where = conds.length > 0 ? `WHERE ${conds.join(" AND ")}` : "";
|
|
3322
|
+
const limit = Math.min(typeof params?.limit === "number" ? params.limit : 100, 500);
|
|
3323
|
+
const offset = typeof params?.offset === "number" ? params.offset : 0;
|
|
3324
|
+
bind.push(limit, offset);
|
|
3325
|
+
const rows = db.prepare(`SELECT * FROM surface_events ${where} ORDER BY createdAt DESC LIMIT ? OFFSET ?`).all(...bind);
|
|
3326
|
+
respond(true, { events: rows, count: rows.length });
|
|
3327
|
+
} catch (err) {
|
|
3328
|
+
respond(false, { error: err?.message ?? "history error" });
|
|
3329
|
+
}
|
|
3330
|
+
}, { scope: "operator.read" });
|
|
3331
|
+
api.registerGatewayMethod("agentlife.history.widgets", ({ params, respond }) => {
|
|
3332
|
+
try {
|
|
3333
|
+
const db = getOrCreateHistoryDb(state);
|
|
3334
|
+
const conds = ["dsl IS NOT NULL"];
|
|
3335
|
+
const bind = [];
|
|
3336
|
+
if (typeof params?.search === "string" && params.search) {
|
|
3337
|
+
conds.push("dsl LIKE ?");
|
|
3338
|
+
bind.push(`%${params.search}%`);
|
|
3339
|
+
}
|
|
3340
|
+
if (typeof params?.agentId === "string" && params.agentId) {
|
|
3341
|
+
conds.push("agentId = ?");
|
|
3342
|
+
bind.push(params.agentId);
|
|
3343
|
+
}
|
|
3344
|
+
if (typeof params?.since === "number") {
|
|
3345
|
+
conds.push("createdAt >= ?");
|
|
3346
|
+
bind.push(params.since);
|
|
3347
|
+
}
|
|
3348
|
+
if (typeof params?.until === "number") {
|
|
3349
|
+
conds.push("createdAt <= ?");
|
|
3350
|
+
bind.push(params.until);
|
|
3351
|
+
}
|
|
3352
|
+
const where = conds.join(" AND ");
|
|
3353
|
+
const limit = Math.min(typeof params?.limit === "number" ? params.limit : 50, 200);
|
|
3354
|
+
const offset = typeof params?.offset === "number" ? params.offset : 0;
|
|
3355
|
+
const rows = db.prepare(`
|
|
3356
|
+
SELECT se.surfaceId, se.agentId, se.dsl, se.createdAt
|
|
3357
|
+
FROM surface_events se
|
|
3358
|
+
INNER JOIN (
|
|
3359
|
+
SELECT surfaceId, MAX(id) as maxId
|
|
3360
|
+
FROM surface_events
|
|
3361
|
+
WHERE ${where}
|
|
3362
|
+
GROUP BY surfaceId
|
|
3363
|
+
) latest ON se.id = latest.maxId
|
|
3364
|
+
ORDER BY se.createdAt DESC
|
|
3365
|
+
LIMIT ? OFFSET ?
|
|
3366
|
+
`).all(...bind, limit, offset);
|
|
3367
|
+
respond(true, { widgets: rows, count: rows.length, hasMore: rows.length >= limit });
|
|
3368
|
+
} catch (err) {
|
|
3369
|
+
respond(false, { error: err?.message ?? "history.widgets error" });
|
|
3370
|
+
}
|
|
3371
|
+
}, { scope: "operator.read" });
|
|
3372
|
+
api.registerGatewayMethod("agentlife.trace", ({ params, respond }) => {
|
|
3373
|
+
try {
|
|
3374
|
+
const db = getOrCreateHistoryDb(state);
|
|
3375
|
+
const conds = [];
|
|
3376
|
+
const args = [];
|
|
3377
|
+
const since = typeof params?.since === "number" ? params.since : null;
|
|
3378
|
+
const until = typeof params?.until === "number" ? params.until : null;
|
|
3379
|
+
if (since != null) {
|
|
3380
|
+
conds.push("ts >= ?");
|
|
3381
|
+
args.push(since);
|
|
3382
|
+
}
|
|
3383
|
+
if (until != null) {
|
|
3384
|
+
conds.push("ts <= ?");
|
|
3385
|
+
args.push(until);
|
|
3386
|
+
}
|
|
3387
|
+
const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : null;
|
|
3388
|
+
if (agentId) {
|
|
3389
|
+
conds.push("agentId = ?");
|
|
3390
|
+
args.push(agentId);
|
|
3391
|
+
}
|
|
3392
|
+
const sessionKey = typeof params?.sessionKey === "string" ? params.sessionKey.trim() : null;
|
|
3393
|
+
if (sessionKey) {
|
|
3394
|
+
conds.push("sessionKey = ?");
|
|
3395
|
+
args.push(sessionKey);
|
|
3396
|
+
}
|
|
3397
|
+
const eventFilter = typeof params?.event === "string" ? params.event.trim() : null;
|
|
3398
|
+
if (eventFilter) {
|
|
3399
|
+
const events = eventFilter.split(",").map((e) => e.trim()).filter(Boolean);
|
|
3400
|
+
if (events.length === 1) {
|
|
3401
|
+
conds.push("event = ?");
|
|
3402
|
+
args.push(events[0]);
|
|
3403
|
+
} else if (events.length > 1) {
|
|
3404
|
+
conds.push(`event IN (${events.map(() => "?").join(",")})`);
|
|
3405
|
+
args.push(...events);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
const limit = Math.min(typeof params?.limit === "number" ? params.limit : 200, 1000);
|
|
3409
|
+
const offset = typeof params?.offset === "number" ? params.offset : 0;
|
|
3410
|
+
const where = conds.length > 0 ? `WHERE ${conds.join(" AND ")}` : "";
|
|
3411
|
+
const rows = db.prepare(`SELECT id,ts,sessionKey,agentId,event,toolName,summary,data,runId FROM activity_log ${where} ORDER BY ts DESC LIMIT ? OFFSET ?`).all(...args, limit + 1, offset);
|
|
3412
|
+
const hasMore = rows.length > limit;
|
|
3413
|
+
const entries = rows.slice(0, limit).map((r) => ({
|
|
3414
|
+
id: r.id,
|
|
3415
|
+
ts: r.ts,
|
|
3416
|
+
sessionKey: r.sessionKey,
|
|
3417
|
+
agentId: r.agentId,
|
|
3418
|
+
event: r.event,
|
|
3419
|
+
toolName: r.toolName,
|
|
3420
|
+
summary: r.summary,
|
|
3421
|
+
data: r.data,
|
|
3422
|
+
runId: r.runId
|
|
3423
|
+
}));
|
|
3424
|
+
respond(true, { entries, hasMore });
|
|
3425
|
+
} catch (err) {
|
|
3426
|
+
respond(false, { error: err?.message ?? "trace query failed" });
|
|
3427
|
+
}
|
|
3428
|
+
}, { scope: "operator.read" });
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
// gateway/usage-gateway.ts
|
|
3432
|
+
function registerUsageGateway(api, state) {
|
|
3433
|
+
api.registerGatewayMethod("agentlife.usage", ({ params, respond }) => {
|
|
3434
|
+
try {
|
|
3435
|
+
const db = getOrCreateHistoryDb(state);
|
|
3436
|
+
const conds = [];
|
|
3437
|
+
const bind = [];
|
|
3438
|
+
if (typeof params?.startDate === "string" && params.startDate) {
|
|
3439
|
+
const startMs = new Date(params.startDate + "T00:00:00").getTime();
|
|
3440
|
+
if (!isNaN(startMs)) {
|
|
3441
|
+
conds.push("createdAt >= ?");
|
|
3442
|
+
bind.push(startMs);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
if (typeof params?.endDate === "string" && params.endDate) {
|
|
3446
|
+
const endMs = new Date(params.endDate + "T23:59:59.999").getTime();
|
|
3447
|
+
if (!isNaN(endMs)) {
|
|
3448
|
+
conds.push("createdAt <= ?");
|
|
3449
|
+
bind.push(endMs);
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
if (typeof params?.agentId === "string" && params.agentId) {
|
|
3453
|
+
conds.push("agentId = ?");
|
|
3454
|
+
bind.push(params.agentId);
|
|
3455
|
+
}
|
|
3456
|
+
const where = conds.length > 0 ? `WHERE ${conds.join(" AND ")}` : "";
|
|
3457
|
+
const totalsRow = db.prepare(`SELECT COUNT(*) as llmCalls,
|
|
3458
|
+
COALESCE(SUM(inputTokens),0) as input,
|
|
3459
|
+
COALESCE(SUM(outputTokens),0) as output,
|
|
3460
|
+
COALESCE(SUM(cacheReadTokens),0) as cacheRead,
|
|
3461
|
+
COALESCE(SUM(cacheWriteTokens),0) as cacheWrite,
|
|
3462
|
+
COALESCE(SUM(totalTokens),0) as totalTokens,
|
|
3463
|
+
COALESCE(SUM(estimatedCost),0) as totalCost
|
|
3464
|
+
FROM usage_ledger ${where}`).get(...bind);
|
|
3465
|
+
const dailyRows = db.prepare(`SELECT date(createdAt / 1000, 'unixepoch', 'localtime') as date,
|
|
3466
|
+
COUNT(*) as llmCalls,
|
|
3467
|
+
COALESCE(SUM(inputTokens),0) as input,
|
|
3468
|
+
COALESCE(SUM(outputTokens),0) as output,
|
|
3469
|
+
COALESCE(SUM(cacheReadTokens),0) as cacheRead,
|
|
3470
|
+
COALESCE(SUM(cacheWriteTokens),0) as cacheWrite,
|
|
3471
|
+
COALESCE(SUM(totalTokens),0) as totalTokens,
|
|
3472
|
+
COALESCE(SUM(estimatedCost),0) as totalCost
|
|
3473
|
+
FROM usage_ledger ${where}
|
|
3474
|
+
GROUP BY date ORDER BY date`).all(...bind);
|
|
3475
|
+
const byModelRows = db.prepare(`SELECT provider, model, COUNT(*) as llmCalls,
|
|
3476
|
+
COALESCE(SUM(totalTokens),0) as totalTokens,
|
|
3477
|
+
COALESCE(SUM(estimatedCost),0) as totalCost
|
|
3478
|
+
FROM usage_ledger ${where}
|
|
3479
|
+
GROUP BY provider, model ORDER BY totalCost DESC`).all(...bind);
|
|
3480
|
+
const byAgentRows = db.prepare(`SELECT agentId, COUNT(*) as llmCalls,
|
|
3481
|
+
COALESCE(SUM(totalTokens),0) as totalTokens,
|
|
3482
|
+
COALESCE(SUM(estimatedCost),0) as totalCost
|
|
3483
|
+
FROM usage_ledger ${where}
|
|
3484
|
+
GROUP BY agentId ORDER BY totalCost DESC`).all(...bind);
|
|
3485
|
+
const bySessionRows = db.prepare(`SELECT sessionKey, agentId, model, COUNT(*) as llmCalls,
|
|
3486
|
+
COALESCE(SUM(totalTokens),0) as totalTokens,
|
|
3487
|
+
COALESCE(SUM(estimatedCost),0) as totalCost,
|
|
3488
|
+
MIN(createdAt) as startedAt
|
|
3489
|
+
FROM usage_ledger ${where}
|
|
3490
|
+
GROUP BY sessionKey ORDER BY startedAt DESC`).all(...bind);
|
|
3491
|
+
respond(true, {
|
|
3492
|
+
totals: totalsRow,
|
|
3493
|
+
daily: dailyRows,
|
|
3494
|
+
byModel: byModelRows,
|
|
3495
|
+
byAgent: byAgentRows,
|
|
3496
|
+
bySession: bySessionRows
|
|
3497
|
+
});
|
|
3498
|
+
} catch (err) {
|
|
3499
|
+
respond(false, { error: err?.message ?? "usage query error" });
|
|
3500
|
+
}
|
|
3501
|
+
}, { scope: "operator.read" });
|
|
3502
|
+
api.registerGatewayMethod("agentlife.surfaceUsage", ({ params, respond }) => {
|
|
3503
|
+
const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
|
|
3504
|
+
if (!surfaceId)
|
|
3505
|
+
return respond(false, { error: "missing surfaceId" });
|
|
3506
|
+
try {
|
|
3507
|
+
const db = getOrCreateHistoryDb(state);
|
|
3508
|
+
const rows = db.prepare(`SELECT * FROM surface_usage WHERE surfaceId = ? ORDER BY createdAt DESC`).all(surfaceId);
|
|
3509
|
+
const totals = rows.reduce((acc, r) => {
|
|
3510
|
+
acc.inputTokens += r.inputTokens;
|
|
3511
|
+
acc.outputTokens += r.outputTokens;
|
|
3512
|
+
acc.cacheReadTokens += r.cacheReadTokens;
|
|
3513
|
+
acc.cacheWriteTokens += r.cacheWriteTokens;
|
|
3514
|
+
acc.totalTokens += r.totalTokens;
|
|
3515
|
+
acc.estimatedCost += r.estimatedCost;
|
|
3516
|
+
acc.llmCalls += r.llmCalls;
|
|
3517
|
+
return acc;
|
|
3518
|
+
}, { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, estimatedCost: 0, llmCalls: 0 });
|
|
3519
|
+
respond(true, { surfaceId, entries: rows, totals });
|
|
3520
|
+
} catch (err) {
|
|
3521
|
+
respond(false, { error: err?.message ?? "surface usage error" });
|
|
3522
|
+
}
|
|
3523
|
+
}, { scope: "operator.read" });
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
// gateway/surfaces-gateway.ts
|
|
3527
|
+
function registerSurfacesGateway(api, state) {
|
|
3528
|
+
api.registerGatewayMethod("agentlife.surfaces", ({ respond, context }) => {
|
|
3529
|
+
captureBridge(context);
|
|
3530
|
+
const now = Date.now();
|
|
3531
|
+
const activeSurfaceIds = [];
|
|
3532
|
+
const surfaceEntries = [];
|
|
3533
|
+
if (!state.surfaceDb) {
|
|
3534
|
+
respond(true, { surfaces: [] });
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
3537
|
+
const LOADING_TTL_MS = 2 * 60 * 1000;
|
|
3538
|
+
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
3539
|
+
const headerLine = meta.lines[0] ?? "";
|
|
3540
|
+
if (/\bstate=loading\b/.test(headerLine) && now - meta.updatedAt > LOADING_TTL_MS) {
|
|
3541
|
+
const agentId = state.surfaceDb.getAgentId(surfaceId);
|
|
3542
|
+
meta.lines[0] = headerLine.replace(/\bstate=loading\b/, "state=error");
|
|
3543
|
+
state.surfaceDb.set(surfaceId, { ...meta, updatedAt: now });
|
|
3544
|
+
recordSurfaceEvent(state, surfaceId, "loading_resolved_error", undefined, agentId ?? undefined, JSON.stringify({ reason: "loading surface orphaned (>2min)", resolvedBy: "surfaces_read" }));
|
|
3545
|
+
console.log("[agentlife] resolved stale loading surface %s → error (stuck >2min)", surfaceId);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
for (const [surfaceId, meta] of state.surfaceDb.entries()) {
|
|
3549
|
+
if (isExpired(meta, now))
|
|
3550
|
+
continue;
|
|
3551
|
+
const headerLine = meta.lines[0] ?? "";
|
|
3552
|
+
if (/\binput\b/.test(headerLine))
|
|
3553
|
+
continue;
|
|
3554
|
+
if (meta.lines.length > 0) {
|
|
3555
|
+
surfaceEntries.push({ surfaceId, dsl: meta.lines.join(`
|
|
3556
|
+
`) });
|
|
3557
|
+
activeSurfaceIds.push(surfaceId);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
let usageMap;
|
|
3561
|
+
if (activeSurfaceIds.length > 0) {
|
|
3562
|
+
try {
|
|
3563
|
+
const db = getOrCreateHistoryDb(state);
|
|
3564
|
+
const placeholders = activeSurfaceIds.map(() => "?").join(",");
|
|
3565
|
+
const rows = db.prepare(`SELECT surfaceId, SUM(totalTokens) as totalTokens, SUM(estimatedCost) as estimatedCost, SUM(llmCalls) as llmCalls
|
|
3566
|
+
FROM surface_usage WHERE surfaceId IN (${placeholders}) GROUP BY surfaceId`).all(...activeSurfaceIds);
|
|
3567
|
+
usageMap = new Map(rows.map((r) => [r.surfaceId, { totalTokens: r.totalTokens, estimatedCost: r.estimatedCost, llmCalls: r.llmCalls }]));
|
|
3568
|
+
} catch {}
|
|
3569
|
+
}
|
|
3570
|
+
let viewedSet;
|
|
3571
|
+
if (activeSurfaceIds.length > 0) {
|
|
3572
|
+
try {
|
|
3573
|
+
const db = getOrCreateHistoryDb(state);
|
|
3574
|
+
const placeholders = activeSurfaceIds.map(() => "?").join(",");
|
|
3575
|
+
const viewedRows = db.prepare(`SELECT DISTINCT surfaceId FROM surface_events WHERE event = 'detail_viewed' AND surfaceId IN (${placeholders})`).all(...activeSurfaceIds);
|
|
3576
|
+
viewedSet = new Set(viewedRows.map((r) => r.surfaceId));
|
|
3577
|
+
} catch {}
|
|
3578
|
+
}
|
|
3579
|
+
let followupCountMap;
|
|
3580
|
+
let nextFollowupMap;
|
|
3581
|
+
let automationsMap;
|
|
3582
|
+
if (activeSurfaceIds.length > 0) {
|
|
3583
|
+
try {
|
|
3584
|
+
const db = getOrCreateHistoryDb(state);
|
|
3585
|
+
const placeholders = activeSurfaceIds.map(() => "?").join(",");
|
|
3586
|
+
const countRows = db.prepare(`SELECT surfaceId, COUNT(*) as c FROM surface_events WHERE event = 'cron_scheduled' AND surfaceId IN (${placeholders}) GROUP BY surfaceId`).all(...activeSurfaceIds);
|
|
3587
|
+
followupCountMap = new Map(countRows.map((r) => [r.surfaceId, r.c]));
|
|
3588
|
+
const followupRows = db.prepare(`SELECT id, surfaceId, fireAt, instruction, status FROM followups
|
|
3589
|
+
WHERE surfaceId IN (${placeholders}) AND status = 'pending'
|
|
3590
|
+
ORDER BY fireAt ASC`).all(...activeSurfaceIds);
|
|
3591
|
+
nextFollowupMap = new Map;
|
|
3592
|
+
for (const r of followupRows) {
|
|
3593
|
+
if (!nextFollowupMap.has(r.surfaceId)) {
|
|
3594
|
+
nextFollowupMap.set(r.surfaceId, { id: r.id, fireAt: r.fireAt, instruction: r.instruction, status: r.status });
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
const autoRows = db.prepare(`SELECT id, surfaceId, type, name, path, status FROM automations WHERE surfaceId IN (${placeholders}) AND status != 'removed'`).all(...activeSurfaceIds);
|
|
3598
|
+
automationsMap = new Map;
|
|
3599
|
+
for (const r of autoRows) {
|
|
3600
|
+
const list = automationsMap.get(r.surfaceId) ?? [];
|
|
3601
|
+
list.push({ id: r.id, type: r.type, name: r.name, path: r.path, status: r.status });
|
|
3602
|
+
automationsMap.set(r.surfaceId, list);
|
|
3603
|
+
}
|
|
3604
|
+
} catch {}
|
|
3605
|
+
}
|
|
3606
|
+
let traceMap;
|
|
3607
|
+
if (activeSurfaceIds.length > 0) {
|
|
3608
|
+
try {
|
|
3609
|
+
const db = getOrCreateHistoryDb(state);
|
|
3610
|
+
const placeholders = activeSurfaceIds.map(() => "?").join(",");
|
|
3611
|
+
const traceRows = db.prepare(`SELECT surfaceId, metadata FROM surface_events WHERE event = 'trace' AND surfaceId IN (${placeholders}) ORDER BY createdAt DESC`).all(...activeSurfaceIds);
|
|
3612
|
+
traceMap = new Map;
|
|
3613
|
+
for (const r of traceRows) {
|
|
3614
|
+
if (!traceMap.has(r.surfaceId))
|
|
3615
|
+
traceMap.set(r.surfaceId, r.metadata);
|
|
3616
|
+
}
|
|
3617
|
+
} catch {}
|
|
3618
|
+
}
|
|
3619
|
+
const surfaces = surfaceEntries.map((s) => {
|
|
3620
|
+
const u = usageMap?.get(s.surfaceId);
|
|
3621
|
+
const viewed = viewedSet?.has(s.surfaceId) ?? false;
|
|
3622
|
+
const meta = state.surfaceDb.get(s.surfaceId);
|
|
3623
|
+
const updatedAt = meta?.updatedAt ?? 0;
|
|
3624
|
+
return {
|
|
3625
|
+
...s,
|
|
3626
|
+
viewed,
|
|
3627
|
+
updatedAt,
|
|
3628
|
+
state: meta?.state ?? "active",
|
|
3629
|
+
cronId: meta?.cronId ?? null,
|
|
3630
|
+
followup: meta?.followup ?? null,
|
|
3631
|
+
goal: meta?.goal ?? null,
|
|
3632
|
+
context: meta?.context ?? null,
|
|
3633
|
+
followupCount: followupCountMap?.get(s.surfaceId) ?? 0,
|
|
3634
|
+
nextFollowup: nextFollowupMap?.get(s.surfaceId) ?? null,
|
|
3635
|
+
trace: traceMap?.get(s.surfaceId) ?? null,
|
|
3636
|
+
automations: automationsMap?.get(s.surfaceId) ?? [],
|
|
3637
|
+
...u ? { usage: u } : {}
|
|
3638
|
+
};
|
|
3639
|
+
});
|
|
3640
|
+
respond(true, { surfaces });
|
|
3641
|
+
}, { scope: "operator.read" });
|
|
3642
|
+
api.registerGatewayMethod("agentlife.dismiss", ({ params, respond }) => {
|
|
3643
|
+
const surfaceId = params?.surfaceId;
|
|
3644
|
+
if (!surfaceId || typeof surfaceId !== "string") {
|
|
3645
|
+
respond(false, { error: "missing surfaceId" });
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
const meta = state.surfaceDb?.get(surfaceId);
|
|
3649
|
+
if (!meta) {
|
|
3650
|
+
respond(true, { surfaceId, dismissed: false });
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
const agentId = state.surfaceDb.getAgentId(surfaceId) ?? null;
|
|
3654
|
+
const cronId = state.surfaceDb.getCronId(surfaceId);
|
|
3655
|
+
recordSurfaceEvent(state, surfaceId, "dismissed", undefined, agentId ?? undefined);
|
|
3656
|
+
let automations = [];
|
|
3657
|
+
try {
|
|
3658
|
+
const db = getOrCreateHistoryDb(state);
|
|
3659
|
+
automations = db.prepare("SELECT id, type, name, path FROM automations WHERE surfaceId = ? AND status != 'removed'").all(surfaceId);
|
|
3660
|
+
} catch {}
|
|
3661
|
+
state.surfaceDb.delete(surfaceId);
|
|
3662
|
+
broadcastDelete(surfaceId);
|
|
3663
|
+
try {
|
|
3664
|
+
enqueueCleanupTasks(state, surfaceId, agentId, cronId, automations);
|
|
3665
|
+
} catch (err) {
|
|
3666
|
+
console.error("[agentlife] dismiss: failed to enqueue cleanup tasks for %s: %s", surfaceId, err?.message);
|
|
3667
|
+
}
|
|
3668
|
+
const taskCount = (cronId ? 1 : 0) + (agentId ? 1 : 0);
|
|
3669
|
+
console.log("[agentlife] dismiss: %s deleted, %d cleanup tasks enqueued", surfaceId, taskCount);
|
|
3670
|
+
respond(true, { surfaceId, dismissed: true });
|
|
3671
|
+
processCleanupTasks(state, surfaceId).catch((e) => console.warn("[agentlife] dismiss cleanup processor error:", e?.message));
|
|
3672
|
+
}, { scope: "operator.write" });
|
|
3673
|
+
api.registerGatewayMethod("agentlife.engage", ({ params, respond }) => {
|
|
3674
|
+
const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
|
|
3675
|
+
const event = typeof params?.event === "string" ? params.event.trim() : "";
|
|
3676
|
+
if (!surfaceId || !event) {
|
|
3677
|
+
respond(false, { error: "missing surfaceId or event" });
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
const agentId = state.surfaceDb?.getAgentId(surfaceId);
|
|
3681
|
+
const metadata = typeof params?.metadata === "string" ? params.metadata.trim() : undefined;
|
|
3682
|
+
recordSurfaceEvent(state, surfaceId, event, undefined, agentId, metadata);
|
|
3683
|
+
respond(true, { surfaceId, event });
|
|
3684
|
+
}, { scope: "operator.read" });
|
|
3685
|
+
api.registerGatewayMethod("agentlife.input", ({ params, respond }) => {
|
|
3686
|
+
const message = typeof params?.message === "string" ? params.message : "";
|
|
3687
|
+
const sessionKey = typeof params?.sessionKey === "string" ? params.sessionKey : "";
|
|
3688
|
+
if (!message) {
|
|
3689
|
+
respond(false, { error: "missing message" });
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
broadcastInput(message, sessionKey);
|
|
3693
|
+
respond(true, { broadcast: true });
|
|
3694
|
+
}, { scope: "operator.write" });
|
|
3695
|
+
api.registerGatewayMethod("agentlife.action", ({ params, respond }) => {
|
|
3696
|
+
const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
|
|
3697
|
+
if (!surfaceId) {
|
|
3698
|
+
respond(false, { error: "missing surfaceId" });
|
|
3699
|
+
return;
|
|
3700
|
+
}
|
|
3701
|
+
const agentId = state.surfaceDb?.getAgentId(surfaceId) ?? null;
|
|
3702
|
+
const sessionKey = agentId ? `agent:${agentId}:main` : null;
|
|
3703
|
+
console.log("[agentlife] action target: surface=%s agent=%s session=%s", surfaceId, agentId ?? "unknown", sessionKey ?? "orchestrator");
|
|
3704
|
+
respond(true, { surfaceId, agentId, sessionKey });
|
|
3705
|
+
}, { scope: "operator.read" });
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
// gateway/automations.ts
|
|
3709
|
+
function registerAutomationsGateway(api, state) {
|
|
3710
|
+
api.registerGatewayMethod("agentlife.automations.register", ({ params, respond }) => {
|
|
3711
|
+
try {
|
|
3712
|
+
const type = typeof params?.type === "string" ? params.type.trim() : "";
|
|
3713
|
+
const name = typeof params?.name === "string" ? params.name.trim() : "";
|
|
3714
|
+
const automationPath = typeof params?.path === "string" ? params.path.trim() : "";
|
|
3715
|
+
if (!type || !name || !automationPath) {
|
|
3716
|
+
respond(false, { error: "missing required fields: type, name, path" });
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3719
|
+
const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
|
|
3720
|
+
const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : state.surfaceDb?.getAgentId(surfaceId) ?? "";
|
|
3721
|
+
const id = registerAutomation(state, { surfaceId, agentId, type, name, path: automationPath });
|
|
3722
|
+
respond(true, { id, surfaceId, agentId, type, name, path: automationPath, status: "running" });
|
|
3723
|
+
} catch (err) {
|
|
3724
|
+
respond(false, { error: err?.message ?? "register failed" });
|
|
3725
|
+
}
|
|
3726
|
+
}, { scope: "operator.write" });
|
|
3727
|
+
api.registerGatewayMethod("agentlife.automations.list", ({ params, respond }) => {
|
|
3728
|
+
try {
|
|
3729
|
+
const db = getOrCreateHistoryDb(state);
|
|
3730
|
+
let sql = "SELECT * FROM automations WHERE 1=1";
|
|
3731
|
+
const sqlParams = [];
|
|
3732
|
+
const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId.trim() : "";
|
|
3733
|
+
if (surfaceId) {
|
|
3734
|
+
sql += " AND surfaceId = ?";
|
|
3735
|
+
sqlParams.push(surfaceId);
|
|
3736
|
+
}
|
|
3737
|
+
const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : "";
|
|
3738
|
+
if (agentId) {
|
|
3739
|
+
sql += " AND agentId = ?";
|
|
3740
|
+
sqlParams.push(agentId);
|
|
3741
|
+
}
|
|
3742
|
+
const status = typeof params?.status === "string" ? params.status.trim() : "";
|
|
3743
|
+
if (status) {
|
|
3744
|
+
sql += " AND status = ?";
|
|
3745
|
+
sqlParams.push(status);
|
|
3746
|
+
} else {
|
|
3747
|
+
sql += " AND status != 'removed'";
|
|
3748
|
+
}
|
|
3749
|
+
sql += " ORDER BY updatedAt DESC";
|
|
3750
|
+
const rows = db.prepare(sql).all(...sqlParams);
|
|
3751
|
+
respond(true, {
|
|
3752
|
+
automations: rows.map((r) => ({
|
|
3753
|
+
id: r.id,
|
|
3754
|
+
surfaceId: r.surfaceId,
|
|
3755
|
+
agentId: r.agentId,
|
|
3756
|
+
type: r.type,
|
|
3757
|
+
name: r.name,
|
|
3758
|
+
path: r.path,
|
|
3759
|
+
status: r.status,
|
|
3760
|
+
createdAt: r.createdAt,
|
|
3761
|
+
updatedAt: r.updatedAt
|
|
3762
|
+
}))
|
|
3763
|
+
});
|
|
3764
|
+
} catch (err) {
|
|
3765
|
+
respond(false, { error: err?.message ?? "list failed" });
|
|
3766
|
+
}
|
|
3767
|
+
}, { scope: "operator.read" });
|
|
3768
|
+
api.registerGatewayMethod("agentlife.automations.update", ({ params, respond }) => {
|
|
3769
|
+
try {
|
|
3770
|
+
const id = typeof params?.id === "number" ? params.id : parseInt(params?.id, 10);
|
|
3771
|
+
const status = typeof params?.status === "string" ? params.status.trim() : "";
|
|
3772
|
+
if (!id || !status || !["running", "stopped", "error"].includes(status)) {
|
|
3773
|
+
respond(false, { error: "missing id or invalid status (running|stopped|error)" });
|
|
3774
|
+
return;
|
|
3775
|
+
}
|
|
3776
|
+
const db = getOrCreateHistoryDb(state);
|
|
3777
|
+
db.prepare("UPDATE automations SET status = ?, updatedAt = ? WHERE id = ?").run(status, Date.now(), id);
|
|
3778
|
+
respond(true, { id, status });
|
|
3779
|
+
} catch (err) {
|
|
3780
|
+
respond(false, { error: err?.message ?? "update failed" });
|
|
3781
|
+
}
|
|
3782
|
+
}, { scope: "operator.write" });
|
|
3783
|
+
api.registerGatewayMethod("agentlife.automations.remove", ({ params, respond }) => {
|
|
3784
|
+
try {
|
|
3785
|
+
const id = typeof params?.id === "number" ? params.id : parseInt(params?.id, 10);
|
|
3786
|
+
if (!id) {
|
|
3787
|
+
respond(false, { error: "missing id" });
|
|
3788
|
+
return;
|
|
3789
|
+
}
|
|
3790
|
+
const db = getOrCreateHistoryDb(state);
|
|
3791
|
+
db.prepare("UPDATE automations SET status = 'removed', updatedAt = ? WHERE id = ?").run(Date.now(), id);
|
|
3792
|
+
respond(true, { id, status: "removed" });
|
|
3793
|
+
} catch (err) {
|
|
3794
|
+
respond(false, { error: err?.message ?? "remove failed" });
|
|
3795
|
+
}
|
|
3796
|
+
}, { scope: "operator.write" });
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
// gateway/admin.ts
|
|
3800
|
+
import * as fs6 from "node:fs/promises";
|
|
3801
|
+
import * as os3 from "node:os";
|
|
3802
|
+
import * as path9 from "node:path";
|
|
3803
|
+
function registerAdminGateway(api, state) {
|
|
3804
|
+
api.registerGatewayMethod("agentlife.quality", ({ respond }) => {
|
|
3805
|
+
try {
|
|
3806
|
+
const db = getOrCreateHistoryDb(state);
|
|
3807
|
+
const totalWarnings = db.prepare("SELECT COUNT(*) as c FROM surface_events WHERE event='quality_warning'").get()?.c ?? 0;
|
|
3808
|
+
const warningsByType = {};
|
|
3809
|
+
const rows = db.prepare("SELECT surfaceId, agentId, metadata, createdAt FROM surface_events WHERE event='quality_warning' ORDER BY createdAt DESC").all();
|
|
3810
|
+
const warnings = [];
|
|
3811
|
+
for (const row of rows) {
|
|
3812
|
+
try {
|
|
3813
|
+
const meta = JSON.parse(row.metadata);
|
|
3814
|
+
const issues = meta.issues ?? [];
|
|
3815
|
+
for (const issue of issues) {
|
|
3816
|
+
warningsByType[issue] = (warningsByType[issue] ?? 0) + 1;
|
|
3817
|
+
}
|
|
3818
|
+
warnings.push({ surfaceId: row.surfaceId, agentId: row.agentId ?? null, issues, sessionKey: meta.sessionKey ?? null, createdAt: row.createdAt });
|
|
3819
|
+
} catch {}
|
|
3820
|
+
}
|
|
3821
|
+
const warningsByAgent = {};
|
|
3822
|
+
const agentRows = db.prepare("SELECT agentId, COUNT(*) as c FROM surface_events WHERE event='quality_warning' AND agentId IS NOT NULL GROUP BY agentId").all();
|
|
3823
|
+
for (const r of agentRows) {
|
|
3824
|
+
warningsByAgent[r.agentId] = r.c;
|
|
3825
|
+
}
|
|
3826
|
+
const cronScheduled = db.prepare("SELECT COUNT(*) as c FROM surface_events WHERE event='cron_scheduled'").get()?.c ?? 0;
|
|
3827
|
+
const cronFired = db.prepare("SELECT COUNT(*) as c FROM surface_events WHERE event='cron_fired'").get()?.c ?? 0;
|
|
3828
|
+
const cronRemoved = db.prepare("SELECT COUNT(*) as c FROM surface_events WHERE event='cron_removed'").get()?.c ?? 0;
|
|
3829
|
+
const cleanupPending = db.prepare("SELECT COUNT(*) as c FROM cleanup_tasks WHERE status IN ('pending', 'running')").get()?.c ?? 0;
|
|
3830
|
+
const cleanupFailed = db.prepare("SELECT COUNT(*) as c FROM cleanup_tasks WHERE status = 'failed' AND attempts < maxAttempts").get()?.c ?? 0;
|
|
3831
|
+
const cleanupExhausted = db.prepare("SELECT COUNT(*) as c FROM cleanup_tasks WHERE status = 'failed' AND attempts >= maxAttempts").get()?.c ?? 0;
|
|
3832
|
+
respond(true, {
|
|
3833
|
+
totalWarnings,
|
|
3834
|
+
warningsByType,
|
|
3835
|
+
warningsByAgent,
|
|
3836
|
+
warnings,
|
|
3837
|
+
cron: { scheduled: cronScheduled, fired: cronFired, removed: cronRemoved },
|
|
3838
|
+
cleanup: { pending: cleanupPending, failed: cleanupFailed, exhausted: cleanupExhausted }
|
|
3839
|
+
});
|
|
3840
|
+
} catch (err) {
|
|
3841
|
+
respond(false, { error: err?.message ?? "quality metrics unavailable" });
|
|
3842
|
+
}
|
|
3843
|
+
}, { scope: "operator.read" });
|
|
3844
|
+
api.registerGatewayMethod("agentlife.bootstrap", ({ params, respond }) => {
|
|
3845
|
+
const sessionKey = typeof params?.sessionKey === "string" ? params.sessionKey.trim() : null;
|
|
3846
|
+
const agentId = typeof params?.agentId === "string" ? params.agentId.trim() : null;
|
|
3847
|
+
let snapshot = [];
|
|
3848
|
+
if (sessionKey) {
|
|
3849
|
+
snapshot = state.sessionBootstrapSnapshots.get(sessionKey) ?? [];
|
|
3850
|
+
}
|
|
3851
|
+
if (snapshot.length === 0 && agentId) {
|
|
3852
|
+
const prefix = `agent:${agentId}:`;
|
|
3853
|
+
for (const [key, files] of state.sessionBootstrapSnapshots.entries()) {
|
|
3854
|
+
if (key.startsWith(prefix)) {
|
|
3855
|
+
snapshot = files;
|
|
3856
|
+
break;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
if (snapshot.length === 0) {
|
|
3861
|
+
const fromDb = loadBootstrapSnapshot(state, sessionKey ?? "", agentId);
|
|
3862
|
+
if (fromDb && fromDb.length > 0)
|
|
3863
|
+
snapshot = fromDb;
|
|
3864
|
+
}
|
|
3865
|
+
if (snapshot.length === 0) {
|
|
3866
|
+
respond(true, { status: "no bootstrap snapshot for this session", files: [], availableSessions: [...state.sessionBootstrapSnapshots.keys()] });
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
respond(true, {
|
|
3870
|
+
files: snapshot.map((f) => ({
|
|
3871
|
+
name: f.name,
|
|
3872
|
+
path: f.path,
|
|
3873
|
+
missing: f.missing,
|
|
3874
|
+
bytes: (f.content ?? "").length,
|
|
3875
|
+
content: f.content ?? ""
|
|
3876
|
+
}))
|
|
3877
|
+
});
|
|
3878
|
+
}, { scope: "operator.read" });
|
|
3879
|
+
api.registerGatewayMethod("agentlife.uninstall", async ({ respond }) => {
|
|
3880
|
+
try {
|
|
3881
|
+
const cleaned = [];
|
|
3882
|
+
const baseDir = state.agentlifeStateDir ?? path9.join(os3.homedir(), ".openclaw", "agentlife");
|
|
3883
|
+
if (state.runCommand) {
|
|
3884
|
+
try {
|
|
3885
|
+
const result = await state.runCommand(["openclaw", "cron", "list", "--json"], { timeoutMs: 1e4 });
|
|
3886
|
+
if (result?.code === 0) {
|
|
3887
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
3888
|
+
const jsonStart = stdout.indexOf("{");
|
|
3889
|
+
const data = jsonStart >= 0 ? JSON.parse(stdout.slice(jsonStart)) : null;
|
|
3890
|
+
const jobs = [...data?.jobs ?? []];
|
|
3891
|
+
const provisionedIds = new Set(PROVISIONED_AGENTS.map((a) => a.id));
|
|
3892
|
+
for (const job of jobs) {
|
|
3893
|
+
if (job.agentId && provisionedIds.has(job.agentId)) {
|
|
3894
|
+
try {
|
|
3895
|
+
await state.runCommand(["openclaw", "cron", "rm", String(job.id)], { timeoutMs: 5000 });
|
|
3896
|
+
cleaned.push(`deleted cron job ${job.id} (${job.name ?? job.agentId})`);
|
|
3897
|
+
} catch {}
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
} catch {
|
|
3902
|
+
cleaned.push("cron cleanup skipped (CLI unavailable)");
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
try {
|
|
3906
|
+
const cfg = api.runtime.config.loadConfig();
|
|
3907
|
+
const agentList = cfg?.agents?.list ?? [];
|
|
3908
|
+
if (Array.isArray(agentList)) {
|
|
3909
|
+
const provisionedIds = new Set(PROVISIONED_AGENTS.filter((a) => !a.existingAgent).map((a) => a.id));
|
|
3910
|
+
const filtered = agentList.filter((a) => !provisionedIds.has(a.id));
|
|
3911
|
+
if (filtered.length !== agentList.length) {
|
|
3912
|
+
cfg.agents.list = filtered;
|
|
3913
|
+
await api.runtime.config.writeConfigFile(cfg);
|
|
3914
|
+
cleaned.push(`removed ${agentList.length - filtered.length} provisioned agents from config`);
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
} catch {
|
|
3918
|
+
cleaned.push("agent config cleanup skipped");
|
|
3919
|
+
}
|
|
3920
|
+
const backupPath = path9.join(baseDir, "config-backup.json");
|
|
3921
|
+
try {
|
|
3922
|
+
cleaned.push(await restoreConfigFromBackup(api, backupPath));
|
|
3923
|
+
} catch {
|
|
3924
|
+
cleaned.push("maxPingPongTurns already at default (no backup found)");
|
|
3925
|
+
}
|
|
3926
|
+
stopFollowupSystem();
|
|
3927
|
+
if (state.surfaceDb)
|
|
3928
|
+
state.surfaceDb.clear();
|
|
3929
|
+
closeAllDbs(state);
|
|
3930
|
+
state.agentRegistry.clear();
|
|
3931
|
+
state.usageAccumulator.clear();
|
|
3932
|
+
state.sessionBootstrapSnapshots.clear();
|
|
3933
|
+
state.pendingFollowups.clear();
|
|
3934
|
+
state.surfaceDb = null;
|
|
3935
|
+
state.disabled = true;
|
|
3936
|
+
cleaned.push("stopped runtime (poller, DBs, in-memory state)");
|
|
3937
|
+
const dbPath = state.historyDbPath;
|
|
3938
|
+
const stateFiles = [
|
|
3939
|
+
state.registryFilePath,
|
|
3940
|
+
dbPath,
|
|
3941
|
+
dbPath ? `${dbPath}-wal` : null,
|
|
3942
|
+
dbPath ? `${dbPath}-shm` : null,
|
|
3943
|
+
path9.join(baseDir, "config-backup.json"),
|
|
3944
|
+
path9.join(baseDir, "notification-config.json"),
|
|
3945
|
+
path9.join(baseDir, "canvas-node-identity.json")
|
|
3946
|
+
].filter(Boolean);
|
|
3947
|
+
for (const fp of stateFiles) {
|
|
3948
|
+
try {
|
|
3949
|
+
await fs6.unlink(fp);
|
|
3950
|
+
cleaned.push(`deleted ${path9.basename(fp)}`);
|
|
3951
|
+
} catch {}
|
|
3952
|
+
}
|
|
3953
|
+
if (state.dbBaseDir) {
|
|
3954
|
+
try {
|
|
3955
|
+
await fs6.rm(state.dbBaseDir, { recursive: true, force: true });
|
|
3956
|
+
cleaned.push("deleted agent databases");
|
|
3957
|
+
} catch {}
|
|
3958
|
+
}
|
|
3959
|
+
for (const agent of PROVISIONED_AGENTS) {
|
|
3960
|
+
if (agent.existingAgent)
|
|
3961
|
+
continue;
|
|
3962
|
+
const wsDir = agent.workspaceDir ?? path9.join(os3.homedir(), ".openclaw", `workspace-${agent.id}`);
|
|
3963
|
+
try {
|
|
3964
|
+
await fs6.rm(wsDir, { recursive: true, force: true });
|
|
3965
|
+
cleaned.push(`deleted workspace ${agent.id}`);
|
|
3966
|
+
} catch {}
|
|
3967
|
+
}
|
|
3968
|
+
try {
|
|
3969
|
+
const remaining = await fs6.readdir(baseDir);
|
|
3970
|
+
if (remaining.length === 0) {
|
|
3971
|
+
await fs6.rmdir(baseDir);
|
|
3972
|
+
cleaned.push("deleted agentlife state directory");
|
|
3973
|
+
}
|
|
3974
|
+
} catch {}
|
|
3975
|
+
console.log("[agentlife] uninstall: cleanup complete — %s", cleaned.join(", "));
|
|
3976
|
+
respond(true, {
|
|
3977
|
+
cleaned,
|
|
3978
|
+
nextStep: "Run `openclaw plugins uninstall agentlife` to remove the plugin code, then restart the gateway."
|
|
3979
|
+
});
|
|
3980
|
+
} catch (err) {
|
|
3981
|
+
console.error("[agentlife] uninstall failed:", err);
|
|
3982
|
+
respond(false, { error: err?.message ?? "unknown error during uninstall" });
|
|
3983
|
+
}
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
// gateway/followups-gateway.ts
|
|
3988
|
+
function registerFollowupsGateway(api, state) {
|
|
3989
|
+
api.registerGatewayMethod("agentlife.followups", ({ params, respond }) => {
|
|
3990
|
+
const action = typeof params?.action === "string" ? params.action : "list";
|
|
3991
|
+
switch (action) {
|
|
3992
|
+
case "list":
|
|
3993
|
+
return handleList(state, params, respond);
|
|
3994
|
+
case "cancel":
|
|
3995
|
+
return handleCancel(state, params, respond);
|
|
3996
|
+
case "run":
|
|
3997
|
+
return handleRun(state, params, respond);
|
|
3998
|
+
case "edit":
|
|
3999
|
+
return handleEdit(state, params, respond);
|
|
4000
|
+
default:
|
|
4001
|
+
respond(false, undefined, { code: "INVALID_ACTION", message: `Unknown action: ${action}` });
|
|
4002
|
+
}
|
|
4003
|
+
}, { scope: "operator.write" });
|
|
4004
|
+
}
|
|
4005
|
+
function handleList(state, params, respond) {
|
|
4006
|
+
try {
|
|
4007
|
+
const db = getOrCreateHistoryDb(state);
|
|
4008
|
+
const surfaceId = typeof params?.surfaceId === "string" ? params.surfaceId : null;
|
|
4009
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
4010
|
+
let rows;
|
|
4011
|
+
if (surfaceId) {
|
|
4012
|
+
rows = db.prepare(`SELECT id, surfaceId, agentId, fireAt, instruction, status, createdAt
|
|
4013
|
+
FROM followups
|
|
4014
|
+
WHERE surfaceId = ? AND (status = 'pending' OR createdAt > ?)
|
|
4015
|
+
ORDER BY fireAt DESC`).all(surfaceId, cutoff);
|
|
4016
|
+
} else {
|
|
4017
|
+
rows = db.prepare(`SELECT id, surfaceId, agentId, fireAt, instruction, status, createdAt
|
|
4018
|
+
FROM followups
|
|
4019
|
+
WHERE status = 'pending' OR createdAt > ?
|
|
4020
|
+
ORDER BY fireAt DESC`).all(cutoff);
|
|
4021
|
+
}
|
|
4022
|
+
respond(true, {
|
|
4023
|
+
followups: rows.map((r) => {
|
|
4024
|
+
const meta = state.surfaceDb?.get(r.surfaceId);
|
|
4025
|
+
const title = meta ? extractTitleAndDetail(meta).title : null;
|
|
4026
|
+
const goal = meta?.goal ?? null;
|
|
4027
|
+
return {
|
|
4028
|
+
id: r.id,
|
|
4029
|
+
surfaceId: r.surfaceId,
|
|
4030
|
+
agentId: r.agentId,
|
|
4031
|
+
fireAt: r.fireAt,
|
|
4032
|
+
instruction: r.instruction,
|
|
4033
|
+
status: r.status,
|
|
4034
|
+
createdAt: r.createdAt,
|
|
4035
|
+
widgetTitle: title,
|
|
4036
|
+
goal
|
|
4037
|
+
};
|
|
4038
|
+
})
|
|
4039
|
+
});
|
|
4040
|
+
} catch (e) {
|
|
4041
|
+
console.error("[agentlife:followups-gw] list error:", e?.message);
|
|
4042
|
+
respond(false, undefined, { code: "LIST_ERROR", message: e?.message });
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
function handleCancel(state, params, respond) {
|
|
4046
|
+
const id = typeof params?.id === "number" ? params.id : null;
|
|
4047
|
+
if (id == null) {
|
|
4048
|
+
respond(false, undefined, { code: "MISSING_ID", message: "id is required" });
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
try {
|
|
4052
|
+
const db = getOrCreateHistoryDb(state);
|
|
4053
|
+
const row = db.prepare("SELECT surfaceId, status FROM followups WHERE id = ?").get(id);
|
|
4054
|
+
if (!row) {
|
|
4055
|
+
respond(false, undefined, { code: "NOT_FOUND", message: `Followup ${id} not found` });
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
if (row.status !== "pending") {
|
|
4059
|
+
respond(false, undefined, { code: "NOT_PENDING", message: `Followup ${id} is ${row.status}, not pending` });
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
db.prepare("UPDATE followups SET status = 'cancelled' WHERE id = ?").run(id);
|
|
4063
|
+
state.surfaceDb?.setCronId(row.surfaceId, null);
|
|
4064
|
+
recordSurfaceEvent(state, row.surfaceId, "followup_cancelled", undefined, undefined, JSON.stringify({ followupId: id }));
|
|
4065
|
+
console.log("[agentlife:followups-gw] cancelled followup %d for %s", id, row.surfaceId);
|
|
4066
|
+
respond(true, { id, cancelled: true });
|
|
4067
|
+
} catch (e) {
|
|
4068
|
+
console.error("[agentlife:followups-gw] cancel error:", e?.message);
|
|
4069
|
+
respond(false, undefined, { code: "CANCEL_ERROR", message: e?.message });
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
function handleRun(state, params, respond) {
|
|
4073
|
+
const id = typeof params?.id === "number" ? params.id : null;
|
|
4074
|
+
if (id == null) {
|
|
4075
|
+
respond(false, undefined, { code: "MISSING_ID", message: "id is required" });
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
try {
|
|
4079
|
+
const db = getOrCreateHistoryDb(state);
|
|
4080
|
+
const row = db.prepare("SELECT surfaceId, agentId, instruction, status FROM followups WHERE id = ?").get(id);
|
|
4081
|
+
if (!row) {
|
|
4082
|
+
respond(false, undefined, { code: "NOT_FOUND", message: `Followup ${id} not found` });
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
if (row.status !== "pending") {
|
|
4086
|
+
respond(false, undefined, { code: "NOT_PENDING", message: `Followup ${id} is ${row.status}, not pending` });
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
db.prepare("UPDATE followups SET status = 'fired' WHERE id = ?").run(id);
|
|
4090
|
+
const agentId = row.agentId ?? state.surfaceDb?.getAgentId(row.surfaceId) ?? null;
|
|
4091
|
+
if (!agentId) {
|
|
4092
|
+
respond(false, undefined, { code: "NO_AGENT", message: "Cannot resolve agent for surface" });
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
const sessionKey = `agent:${agentId}:main`;
|
|
4096
|
+
fireFollowupViaChat(state, sessionKey, row.instruction, row.surfaceId);
|
|
4097
|
+
recordSurfaceEvent(state, row.surfaceId, "cron_fired", undefined, agentId, JSON.stringify({ followupId: id, manual: true }));
|
|
4098
|
+
console.log("[agentlife:followups-gw] manually fired followup %d → %s", id, sessionKey);
|
|
4099
|
+
respond(true, { id, fired: true });
|
|
4100
|
+
} catch (e) {
|
|
4101
|
+
console.error("[agentlife:followups-gw] run error:", e?.message);
|
|
4102
|
+
respond(false, undefined, { code: "RUN_ERROR", message: e?.message });
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
function fireFollowupViaChat(state, sessionKey, instruction, surfaceId) {
|
|
4106
|
+
if (!state.runCommand) {
|
|
4107
|
+
console.warn("[agentlife:followups-gw] runCommand not available — cannot fire followup");
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
4110
|
+
const idempotencyKey = `followup-${surfaceId}-${Date.now()}`;
|
|
4111
|
+
const params = JSON.stringify({ sessionKey, message: `[system] ${instruction}`, idempotencyKey });
|
|
4112
|
+
state.runCommand(["openclaw", "gateway", "call", "chat.send", "--params", params], { timeoutMs: 60000 }).then((result) => {
|
|
4113
|
+
console.log("[agentlife:followups-gw] chat.send result: code=%s", result?.code ?? "?");
|
|
4114
|
+
}).catch((e) => {
|
|
4115
|
+
console.error("[agentlife:followups-gw] chat.send failed: %s", e?.message);
|
|
4116
|
+
});
|
|
4117
|
+
}
|
|
4118
|
+
function handleEdit(state, params, respond) {
|
|
4119
|
+
const id = typeof params?.id === "number" ? params.id : null;
|
|
4120
|
+
if (id == null) {
|
|
4121
|
+
respond(false, undefined, { code: "MISSING_ID", message: "id is required" });
|
|
4122
|
+
return;
|
|
4123
|
+
}
|
|
4124
|
+
const delay = typeof params?.delay === "string" ? params.delay.trim() : null;
|
|
4125
|
+
const instruction = typeof params?.instruction === "string" ? params.instruction.trim() : null;
|
|
4126
|
+
if (!delay && !instruction) {
|
|
4127
|
+
respond(false, undefined, { code: "NO_CHANGES", message: "Provide delay and/or instruction" });
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
try {
|
|
4131
|
+
const db = getOrCreateHistoryDb(state);
|
|
4132
|
+
const row = db.prepare("SELECT surfaceId, agentId, fireAt, instruction, status FROM followups WHERE id = ?").get(id);
|
|
4133
|
+
if (!row) {
|
|
4134
|
+
respond(false, undefined, { code: "NOT_FOUND", message: `Followup ${id} not found` });
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
if (row.status !== "pending") {
|
|
4138
|
+
respond(false, undefined, { code: "NOT_PENDING", message: `Followup ${id} is ${row.status}, not pending` });
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4141
|
+
let newFireAt = row.fireAt;
|
|
4142
|
+
if (delay) {
|
|
4143
|
+
const offsetMs = parseOffset2(delay);
|
|
4144
|
+
if (offsetMs <= 0) {
|
|
4145
|
+
respond(false, undefined, { code: "INVALID_DELAY", message: `Cannot parse delay: ${delay}` });
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
newFireAt = Date.now() + offsetMs;
|
|
4149
|
+
}
|
|
4150
|
+
const newInstruction = instruction ?? row.instruction;
|
|
4151
|
+
db.prepare("UPDATE followups SET fireAt = ?, instruction = ? WHERE id = ?").run(newFireAt, newInstruction, id);
|
|
4152
|
+
recordSurfaceEvent(state, row.surfaceId, "followup_edited", undefined, row.agentId ?? undefined, JSON.stringify({ followupId: id, delay, instruction: instruction ? "(updated)" : null }));
|
|
4153
|
+
console.log("[agentlife:followups-gw] edited followup %d: fireAt=%d", id, newFireAt);
|
|
4154
|
+
respond(true, {
|
|
4155
|
+
followup: {
|
|
4156
|
+
id,
|
|
4157
|
+
surfaceId: row.surfaceId,
|
|
4158
|
+
agentId: row.agentId,
|
|
4159
|
+
fireAt: newFireAt,
|
|
4160
|
+
instruction: newInstruction,
|
|
4161
|
+
status: "pending",
|
|
4162
|
+
createdAt: row.createdAt
|
|
4163
|
+
}
|
|
4164
|
+
});
|
|
4165
|
+
} catch (e) {
|
|
4166
|
+
console.error("[agentlife:followups-gw] edit error:", e?.message);
|
|
4167
|
+
respond(false, undefined, { code: "EDIT_ERROR", message: e?.message });
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
function parseOffset2(offset) {
|
|
4171
|
+
const match = offset.match(/^\+?(\d+)(ms|s|m|h|d)$/);
|
|
4172
|
+
if (!match)
|
|
4173
|
+
return 0;
|
|
4174
|
+
const val = parseInt(match[1], 10);
|
|
4175
|
+
switch (match[2]) {
|
|
4176
|
+
case "ms":
|
|
4177
|
+
return val;
|
|
4178
|
+
case "s":
|
|
4179
|
+
return val * 1000;
|
|
4180
|
+
case "m":
|
|
4181
|
+
return val * 60000;
|
|
4182
|
+
case "h":
|
|
4183
|
+
return val * 3600000;
|
|
4184
|
+
case "d":
|
|
4185
|
+
return val * 86400000;
|
|
4186
|
+
default:
|
|
4187
|
+
return 0;
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
// gateway/web-app.ts
|
|
4192
|
+
import * as path10 from "node:path";
|
|
4193
|
+
import * as fs7 from "node:fs";
|
|
4194
|
+
var MIME_TYPES = {
|
|
4195
|
+
".html": "text/html; charset=utf-8",
|
|
4196
|
+
".js": "application/javascript",
|
|
4197
|
+
".mjs": "application/javascript",
|
|
4198
|
+
".wasm": "application/wasm",
|
|
4199
|
+
".css": "text/css",
|
|
4200
|
+
".json": "application/json",
|
|
4201
|
+
".png": "image/png",
|
|
4202
|
+
".svg": "image/svg+xml",
|
|
4203
|
+
".ico": "image/x-icon",
|
|
4204
|
+
".txt": "text/plain",
|
|
4205
|
+
".map": "application/json"
|
|
4206
|
+
};
|
|
4207
|
+
function registerWebApp(api) {
|
|
4208
|
+
const pluginRoot = path10.resolve(path10.dirname(api.source), "..");
|
|
4209
|
+
const appRoot = path10.join(pluginRoot, "web-build");
|
|
4210
|
+
if (!fs7.existsSync(appRoot)) {
|
|
4211
|
+
api.logger.info("[agentlife] web-build/ not found — /agentlife/ route not registered");
|
|
4212
|
+
return;
|
|
4213
|
+
}
|
|
4214
|
+
const indexPath = path10.join(appRoot, "index.html");
|
|
4215
|
+
if (!fs7.existsSync(indexPath)) {
|
|
4216
|
+
api.logger.warn("[agentlife] web-build/index.html missing — /agentlife/ route not registered");
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4219
|
+
let gatewayToken = "";
|
|
4220
|
+
try {
|
|
4221
|
+
const configPath = path10.join(__require("node:os").homedir(), ".openclaw", "openclaw.json");
|
|
4222
|
+
const raw = JSON.parse(fs7.readFileSync(configPath, "utf-8"));
|
|
4223
|
+
gatewayToken = raw?.gateway?.auth?.token || "";
|
|
4224
|
+
} catch {}
|
|
4225
|
+
const approveLatest = () => {
|
|
4226
|
+
try {
|
|
4227
|
+
const { execSync } = __require("node:child_process");
|
|
4228
|
+
execSync("openclaw devices approve --latest", { timeout: 5000, stdio: "ignore" });
|
|
4229
|
+
} catch {}
|
|
4230
|
+
};
|
|
4231
|
+
api.registerHttpRoute({
|
|
4232
|
+
path: "/agentlife",
|
|
4233
|
+
match: "prefix",
|
|
4234
|
+
auth: "plugin",
|
|
4235
|
+
handler: (req, res) => {
|
|
4236
|
+
const urlPath = new URL(req.url || "/", "http://localhost").pathname;
|
|
4237
|
+
const relative = urlPath.replace(/^\/agentlife\/?/, "") || "index.html";
|
|
4238
|
+
const filePath = path10.resolve(appRoot, relative);
|
|
4239
|
+
if (!filePath.startsWith(appRoot)) {
|
|
4240
|
+
res.writeHead(403);
|
|
4241
|
+
res.end();
|
|
4242
|
+
return true;
|
|
4243
|
+
}
|
|
4244
|
+
const target = fs7.existsSync(filePath) && fs7.statSync(filePath).isFile() ? filePath : indexPath;
|
|
4245
|
+
const ext = path10.extname(target).toLowerCase();
|
|
4246
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
4247
|
+
let content = fs7.readFileSync(target);
|
|
4248
|
+
if (target === indexPath) {
|
|
4249
|
+
setTimeout(approveLatest, 2000);
|
|
4250
|
+
setTimeout(approveLatest, 5000);
|
|
4251
|
+
if (gatewayToken) {
|
|
4252
|
+
const injection = `<script>window.__GATEWAY_TOKEN__="${gatewayToken}";</script>`;
|
|
4253
|
+
const html = content.toString().replace("</head>", injection + "</head>");
|
|
4254
|
+
content = Buffer.from(html);
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
res.writeHead(200, {
|
|
4258
|
+
"Content-Type": contentType,
|
|
4259
|
+
"Content-Length": content.length,
|
|
4260
|
+
"Cache-Control": ext === ".html" ? "no-cache" : "public, max-age=86400"
|
|
4261
|
+
});
|
|
4262
|
+
res.end(content);
|
|
4263
|
+
return true;
|
|
4264
|
+
}
|
|
4265
|
+
});
|
|
4266
|
+
api.logger.info("[agentlife] /agentlife/ route registered — serving WASM web app");
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
// index.ts
|
|
4270
|
+
var currentState = null;
|
|
4271
|
+
function _closeAllDbs() {
|
|
4272
|
+
if (currentState)
|
|
4273
|
+
closeAllDbs(currentState);
|
|
4274
|
+
}
|
|
4275
|
+
function _getState() {
|
|
4276
|
+
if (!currentState)
|
|
4277
|
+
throw new Error("plugin not registered");
|
|
4278
|
+
return currentState;
|
|
4279
|
+
}
|
|
4280
|
+
function register(api) {
|
|
4281
|
+
const fallbackDir = path11.join(homedir4(), ".openclaw", "agentlife");
|
|
4282
|
+
const state = {
|
|
4283
|
+
surfaceDb: null,
|
|
4284
|
+
agentRegistry: new Map,
|
|
4285
|
+
usageAccumulator: new Map,
|
|
4286
|
+
sessionBootstrapSnapshots: new Map,
|
|
4287
|
+
pendingFollowups: new Map,
|
|
4288
|
+
agentDbs: new Map,
|
|
4289
|
+
historyDb: null,
|
|
4290
|
+
agentlifeStateDir: fallbackDir,
|
|
4291
|
+
registryFilePath: path11.join(fallbackDir, "agent-registry.json"),
|
|
4292
|
+
dbBaseDir: path11.join(fallbackDir, "db"),
|
|
4293
|
+
historyDbPath: path11.join(fallbackDir, "agentlife.db"),
|
|
4294
|
+
runCommand: api.runtime.system?.runCommandWithTimeout ?? null,
|
|
4295
|
+
enqueueSystemEvent: null,
|
|
4296
|
+
requestHeartbeatNow: null,
|
|
4297
|
+
disabled: false
|
|
4298
|
+
};
|
|
4299
|
+
currentState = state;
|
|
4300
|
+
if (existsSync4(fallbackDir)) {
|
|
4301
|
+
try {
|
|
4302
|
+
const db = getOrCreateHistoryDb(state);
|
|
4303
|
+
state.surfaceDb = new SurfaceDb(db);
|
|
4304
|
+
} catch (e) {
|
|
4305
|
+
console.warn("[agentlife] eager surface DB init failed:", e?.message);
|
|
4306
|
+
}
|
|
4307
|
+
loadRegistryFromDisk(state).catch((e) => console.warn("[agentlife] eager registry load failed:", e?.message));
|
|
4308
|
+
}
|
|
4309
|
+
registerSurfacesService(api, state);
|
|
4310
|
+
api.registerService({
|
|
4311
|
+
id: "agentlife-provisioning",
|
|
4312
|
+
start: async () => {
|
|
4313
|
+
const cfg = api.runtime.config.loadConfig();
|
|
4314
|
+
await provisionAgents(state, cfg, api.runtime, console.log);
|
|
4315
|
+
}
|
|
4316
|
+
});
|
|
4317
|
+
registerConfigOptimizer(api, state);
|
|
4318
|
+
registerPairingServices(api);
|
|
4319
|
+
api.registerService({
|
|
4320
|
+
id: "agentlife-shutdown",
|
|
4321
|
+
start: () => {},
|
|
4322
|
+
stop: () => {
|
|
4323
|
+
stopFollowupSystem();
|
|
4324
|
+
closeAllDbs(state);
|
|
4325
|
+
}
|
|
4326
|
+
});
|
|
4327
|
+
initFollowupSystem(api, state);
|
|
4328
|
+
registerBootstrapHook(api, state);
|
|
4329
|
+
registerPromptStateHook(api, state);
|
|
4330
|
+
registerWidgetPushTool(api, state);
|
|
4331
|
+
registerActivityHooks(api, state);
|
|
4332
|
+
registerAgentGateway(api, state);
|
|
4333
|
+
registerDbGateway(api, state);
|
|
4334
|
+
registerHistoryGateway(api, state);
|
|
4335
|
+
registerUsageGateway(api, state);
|
|
4336
|
+
registerSurfacesGateway(api, state);
|
|
4337
|
+
registerAutomationsGateway(api, state);
|
|
4338
|
+
registerFollowupsGateway(api, state);
|
|
4339
|
+
registerAdminGateway(api, state);
|
|
4340
|
+
registerWebApp(api);
|
|
4341
|
+
const notifyConfigPath = path11.join(fallbackDir, "notification-config.json");
|
|
4342
|
+
api.registerGatewayMethod("agentlife.notifications.register", ({ params, respond }) => {
|
|
4343
|
+
const serverUrl = typeof params?.serverUrl === "string" ? params.serverUrl.trim() : "";
|
|
4344
|
+
const apiKey = typeof params?.apiKey === "string" ? params.apiKey.trim() : "";
|
|
4345
|
+
if (!serverUrl || !apiKey) {
|
|
4346
|
+
return respond(false, { error: "missing serverUrl or apiKey" });
|
|
4347
|
+
}
|
|
4348
|
+
try {
|
|
4349
|
+
const { writeFileSync: writeFileSync2, mkdirSync: mkdirSync2 } = __require("node:fs");
|
|
4350
|
+
mkdirSync2(path11.dirname(notifyConfigPath), { recursive: true });
|
|
4351
|
+
writeFileSync2(notifyConfigPath, JSON.stringify({ serverUrl, apiKey }));
|
|
4352
|
+
} catch {}
|
|
4353
|
+
respond(true, { registered: true });
|
|
4354
|
+
}, { scope: "operator.write" });
|
|
4355
|
+
}
|
|
4356
|
+
export {
|
|
4357
|
+
register as default,
|
|
4358
|
+
_getState,
|
|
4359
|
+
_closeAllDbs
|
|
4360
|
+
};
|