agentflow-dashboard 0.6.0 → 0.7.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/README.md +5 -1
- package/dist/{chunk-N6IN5SHX.js → chunk-3S4AAIPA.js} +883 -69
- package/dist/cli.cjs +875 -68
- package/dist/cli.js +1 -1
- package/dist/client/assets/index-DSuI0NgP.js +50 -0
- package/dist/client/assets/index-Ds_npIxI.css +1 -0
- package/dist/client/dashboard.js +3113 -0
- package/dist/client/index.html +13 -0
- package/dist/index.cjs +875 -68
- package/dist/index.js +1 -1
- package/dist/public/dashboard.js +74 -42
- package/dist/public/index.html +13 -0
- package/dist/server.cjs +875 -68
- package/dist/server.js +1 -1
- package/package.json +10 -2
- package/public/dashboard.js +74 -42
- package/public/index.html +13 -0
- package/dist/public/debug.html +0 -43
- package/public/debug.html +0 -43
package/dist/index.cjs
CHANGED
|
@@ -45,6 +45,422 @@ var import_agentflow_core3 = require("agentflow-core");
|
|
|
45
45
|
var import_express = __toESM(require("express"), 1);
|
|
46
46
|
var import_ws = require("ws");
|
|
47
47
|
|
|
48
|
+
// src/adapters/agentflow.ts
|
|
49
|
+
var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
50
|
+
"workers.json",
|
|
51
|
+
"package.json",
|
|
52
|
+
"package-lock.json",
|
|
53
|
+
"tsconfig.json",
|
|
54
|
+
"biome.json",
|
|
55
|
+
"auth.json",
|
|
56
|
+
"models.json",
|
|
57
|
+
"config.json"
|
|
58
|
+
]);
|
|
59
|
+
var SKIP_SUFFIXES = ["-state.json", "-config.json", "-watch-state.json", ".tmp", ".bak", ".backup"];
|
|
60
|
+
var AgentFlowAdapter = class {
|
|
61
|
+
name = "agentflow";
|
|
62
|
+
detect(_dirPath) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
canHandle(filePath) {
|
|
66
|
+
const filename = filePath.split("/").pop() ?? "";
|
|
67
|
+
if (SKIP_FILES.has(filename)) return false;
|
|
68
|
+
if (SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
69
|
+
return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
|
|
70
|
+
}
|
|
71
|
+
parse(_filePath) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/adapters/openclaw.ts
|
|
77
|
+
var import_node_fs = require("fs");
|
|
78
|
+
var import_node_path = require("path");
|
|
79
|
+
var jobCache = /* @__PURE__ */ new Map();
|
|
80
|
+
function loadJobs(openclawDir) {
|
|
81
|
+
const cached = jobCache.get(openclawDir);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
const jobsPath = (0, import_node_path.join)(openclawDir, "cron", "jobs.json");
|
|
84
|
+
const map = /* @__PURE__ */ new Map();
|
|
85
|
+
try {
|
|
86
|
+
if ((0, import_node_fs.existsSync)(jobsPath)) {
|
|
87
|
+
const data = JSON.parse((0, import_node_fs.readFileSync)(jobsPath, "utf-8"));
|
|
88
|
+
const jobs = Array.isArray(data) ? data : data.jobs ?? [];
|
|
89
|
+
for (const job of jobs) {
|
|
90
|
+
if (job.id) map.set(job.id, job);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
jobCache.set(openclawDir, map);
|
|
96
|
+
return map;
|
|
97
|
+
}
|
|
98
|
+
function findOpenClawRoot(filePath) {
|
|
99
|
+
let dir = (0, import_node_path.dirname)(filePath);
|
|
100
|
+
for (let i = 0; i < 5; i++) {
|
|
101
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "cron", "jobs.json")) || (0, import_node_path.basename)(dir) === ".openclaw") {
|
|
102
|
+
return dir;
|
|
103
|
+
}
|
|
104
|
+
dir = (0, import_node_path.dirname)(dir);
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
var OpenClawAdapter = class {
|
|
109
|
+
name = "openclaw";
|
|
110
|
+
detect(dirPath) {
|
|
111
|
+
return (0, import_node_fs.existsSync)((0, import_node_path.join)(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || (0, import_node_fs.existsSync)((0, import_node_path.join)(dirPath, "cron", "runs"));
|
|
112
|
+
}
|
|
113
|
+
canHandle(filePath) {
|
|
114
|
+
if (!filePath.endsWith(".jsonl")) return false;
|
|
115
|
+
return filePath.includes("/cron/runs/") || filePath.includes("\\cron\\runs\\");
|
|
116
|
+
}
|
|
117
|
+
parse(filePath) {
|
|
118
|
+
const traces = [];
|
|
119
|
+
try {
|
|
120
|
+
const content = (0, import_node_fs.readFileSync)(filePath, "utf-8");
|
|
121
|
+
const root = findOpenClawRoot(filePath);
|
|
122
|
+
const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
|
|
123
|
+
for (const line of content.split("\n")) {
|
|
124
|
+
if (!line.trim()) continue;
|
|
125
|
+
let entry;
|
|
126
|
+
try {
|
|
127
|
+
entry = JSON.parse(line);
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (entry.action !== "finished") continue;
|
|
132
|
+
const jobId = entry.jobId ?? (0, import_node_path.basename)(filePath, ".jsonl");
|
|
133
|
+
const job = jobs.get(jobId);
|
|
134
|
+
const jobName = (job == null ? void 0 : job.name) ?? jobId;
|
|
135
|
+
const startTime = entry.runAtMs ?? entry.ts;
|
|
136
|
+
const duration = entry.durationMs ?? 0;
|
|
137
|
+
const trace = {
|
|
138
|
+
id: entry.sessionId ?? `${jobId}-${entry.ts}`,
|
|
139
|
+
agentId: `openclaw:${jobId}`,
|
|
140
|
+
name: jobName,
|
|
141
|
+
status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
|
|
142
|
+
startTime,
|
|
143
|
+
endTime: startTime + duration,
|
|
144
|
+
trigger: "cron",
|
|
145
|
+
source: "openclaw",
|
|
146
|
+
nodes: {
|
|
147
|
+
root: {
|
|
148
|
+
id: "root",
|
|
149
|
+
type: "cron-job",
|
|
150
|
+
name: jobName,
|
|
151
|
+
status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
|
|
152
|
+
startTime,
|
|
153
|
+
endTime: startTime + duration,
|
|
154
|
+
parentId: null,
|
|
155
|
+
children: [],
|
|
156
|
+
metadata: {
|
|
157
|
+
jobId,
|
|
158
|
+
summary: entry.summary,
|
|
159
|
+
error: entry.error,
|
|
160
|
+
delivered: entry.delivered,
|
|
161
|
+
deliveryStatus: entry.deliveryStatus
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
metadata: {
|
|
166
|
+
model: entry.model,
|
|
167
|
+
provider: entry.provider,
|
|
168
|
+
usage: entry.usage,
|
|
169
|
+
sessionId: entry.sessionId,
|
|
170
|
+
sessionKey: entry.sessionKey,
|
|
171
|
+
nextRunAtMs: entry.nextRunAtMs
|
|
172
|
+
},
|
|
173
|
+
filePath
|
|
174
|
+
};
|
|
175
|
+
traces.push(trace);
|
|
176
|
+
}
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
return traces;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/adapters/otel.ts
|
|
184
|
+
var import_node_fs2 = require("fs");
|
|
185
|
+
var import_node_path2 = require("path");
|
|
186
|
+
var SPAN_TYPE_MAP = {
|
|
187
|
+
"gen_ai.chat": "llm",
|
|
188
|
+
"gen_ai.completion": "llm",
|
|
189
|
+
"gen_ai.embeddings": "embedding",
|
|
190
|
+
"gen_ai.content.prompt": "llm",
|
|
191
|
+
"gen_ai.content.completion": "llm"
|
|
192
|
+
};
|
|
193
|
+
function mapSpanType(spanName, attributes) {
|
|
194
|
+
for (const [prefix, type] of Object.entries(SPAN_TYPE_MAP)) {
|
|
195
|
+
if (spanName.startsWith(prefix)) return type;
|
|
196
|
+
}
|
|
197
|
+
if (attributes["tool.name"] || attributes["code.function"]) return "tool";
|
|
198
|
+
if (attributes["gen_ai.system"] || attributes["llm.vendor"]) return "llm";
|
|
199
|
+
if (attributes["db.system"]) return "database";
|
|
200
|
+
if (attributes["http.method"] || attributes["http.request.method"]) return "http";
|
|
201
|
+
return "span";
|
|
202
|
+
}
|
|
203
|
+
function extractAttributes(attrs) {
|
|
204
|
+
const result = {};
|
|
205
|
+
if (!Array.isArray(attrs)) return result;
|
|
206
|
+
for (const attr of attrs) {
|
|
207
|
+
const a = attr;
|
|
208
|
+
if (!a.key || !a.value) continue;
|
|
209
|
+
result[a.key] = a.value.stringValue ?? a.value.intValue ?? a.value.doubleValue ?? a.value.boolValue;
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
function parseOtlpPayload(payload) {
|
|
214
|
+
var _a, _b, _c;
|
|
215
|
+
const traceMap = /* @__PURE__ */ new Map();
|
|
216
|
+
for (const rs of payload.resourceSpans ?? []) {
|
|
217
|
+
const resourceAttrs = extractAttributes(((_a = rs.resource) == null ? void 0 : _a.attributes) ?? []);
|
|
218
|
+
for (const ss of rs.scopeSpans ?? []) {
|
|
219
|
+
for (const span of ss.spans ?? []) {
|
|
220
|
+
if (!span.traceId) continue;
|
|
221
|
+
let entry = traceMap.get(span.traceId);
|
|
222
|
+
if (!entry) {
|
|
223
|
+
entry = { spans: [], resource: resourceAttrs };
|
|
224
|
+
traceMap.set(span.traceId, entry);
|
|
225
|
+
}
|
|
226
|
+
entry.spans.push(span);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const traces = [];
|
|
231
|
+
for (const [traceId, { spans, resource }] of traceMap) {
|
|
232
|
+
const nodes = {};
|
|
233
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
234
|
+
let traceStart = Number.MAX_SAFE_INTEGER;
|
|
235
|
+
let traceEnd = 0;
|
|
236
|
+
let hasFailed = false;
|
|
237
|
+
for (const span of spans) {
|
|
238
|
+
const attrs = extractAttributes(span.attributes ?? []);
|
|
239
|
+
const startNs = span.startTimeUnixNano ? Number(BigInt(span.startTimeUnixNano) / 1000000n) : 0;
|
|
240
|
+
const endNs = span.endTimeUnixNano ? Number(BigInt(span.endTimeUnixNano) / 1000000n) : null;
|
|
241
|
+
const failed = ((_b = span.status) == null ? void 0 : _b.code) === 2;
|
|
242
|
+
if (failed) hasFailed = true;
|
|
243
|
+
if (startNs < traceStart) traceStart = startNs;
|
|
244
|
+
if (endNs && endNs > traceEnd) traceEnd = endNs;
|
|
245
|
+
nodes[span.spanId] = {
|
|
246
|
+
id: span.spanId,
|
|
247
|
+
type: mapSpanType(span.name, attrs),
|
|
248
|
+
name: span.name,
|
|
249
|
+
status: failed ? "failed" : endNs ? "completed" : "running",
|
|
250
|
+
startTime: startNs,
|
|
251
|
+
endTime: endNs,
|
|
252
|
+
parentId: span.parentSpanId ?? null,
|
|
253
|
+
children: [],
|
|
254
|
+
metadata: {
|
|
255
|
+
...attrs,
|
|
256
|
+
model: attrs["gen_ai.request.model"] ?? attrs["llm.request.model"],
|
|
257
|
+
inputTokens: attrs["gen_ai.usage.input_tokens"] ?? attrs["llm.usage.input_tokens"],
|
|
258
|
+
outputTokens: attrs["gen_ai.usage.output_tokens"] ?? attrs["llm.usage.output_tokens"]
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
if (span.parentSpanId) {
|
|
262
|
+
const siblings = childMap.get(span.parentSpanId) ?? [];
|
|
263
|
+
siblings.push(span.spanId);
|
|
264
|
+
childMap.set(span.parentSpanId, siblings);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const [parentId, children] of childMap) {
|
|
268
|
+
if (nodes[parentId]) {
|
|
269
|
+
nodes[parentId].children = children;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const serviceName = resource["service.name"] ?? "unknown-service";
|
|
273
|
+
traces.push({
|
|
274
|
+
id: traceId,
|
|
275
|
+
agentId: `otel:${serviceName}`,
|
|
276
|
+
name: ((_c = spans.find((s) => !s.parentSpanId)) == null ? void 0 : _c.name) ?? traceId,
|
|
277
|
+
status: hasFailed ? "failed" : "completed",
|
|
278
|
+
startTime: traceStart === Number.MAX_SAFE_INTEGER ? 0 : traceStart,
|
|
279
|
+
endTime: traceEnd,
|
|
280
|
+
trigger: "otel",
|
|
281
|
+
source: "otel",
|
|
282
|
+
nodes,
|
|
283
|
+
metadata: { ...resource }
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return traces;
|
|
287
|
+
}
|
|
288
|
+
var OTelAdapter = class {
|
|
289
|
+
name = "otel";
|
|
290
|
+
detect(dirPath) {
|
|
291
|
+
try {
|
|
292
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(dirPath, "otel-traces"))) return true;
|
|
293
|
+
const files = (0, import_node_fs2.readdirSync)(dirPath);
|
|
294
|
+
return files.some((f) => f.endsWith(".otlp.json"));
|
|
295
|
+
} catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
canHandle(filePath) {
|
|
300
|
+
return filePath.endsWith(".otlp.json");
|
|
301
|
+
}
|
|
302
|
+
parse(filePath) {
|
|
303
|
+
try {
|
|
304
|
+
const content = (0, import_node_fs2.readFileSync)(filePath, "utf-8");
|
|
305
|
+
const payload = JSON.parse(content);
|
|
306
|
+
const traces = parseOtlpPayload(payload);
|
|
307
|
+
for (const t of traces) t.filePath = filePath;
|
|
308
|
+
return traces;
|
|
309
|
+
} catch {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/adapters/registry.ts
|
|
316
|
+
var adapters = [];
|
|
317
|
+
function registerAdapter(adapter) {
|
|
318
|
+
adapters.push(adapter);
|
|
319
|
+
}
|
|
320
|
+
function findAdapter(filePath) {
|
|
321
|
+
for (const adapter of adapters) {
|
|
322
|
+
if (adapter.canHandle(filePath)) return adapter;
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/adapters/index.ts
|
|
328
|
+
registerAdapter(new OpenClawAdapter());
|
|
329
|
+
registerAdapter(new OTelAdapter());
|
|
330
|
+
registerAdapter(new AgentFlowAdapter());
|
|
331
|
+
|
|
332
|
+
// src/agent-clustering.ts
|
|
333
|
+
var PURPOSE_KEYWORDS = [
|
|
334
|
+
{ keywords: ["email", "mail", "inbox", "smtp"], group: "Email Processors" },
|
|
335
|
+
{ keywords: ["monitor", "watch", "alert", "surveillance"], group: "Monitors" },
|
|
336
|
+
{ keywords: ["digest", "newsletter", "summary", "report", "briefing"], group: "Digests & Reports" },
|
|
337
|
+
{ keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"], group: "Workers" },
|
|
338
|
+
{ keywords: ["cron", "schedule", "timer", "periodic"], group: "Scheduled Jobs" },
|
|
339
|
+
{ keywords: ["search", "scrape", "crawl", "fetch"], group: "Data Collection" },
|
|
340
|
+
{ keywords: ["embed", "vector", "index"], group: "Embeddings" }
|
|
341
|
+
];
|
|
342
|
+
function extractSource(agentId) {
|
|
343
|
+
const colonIdx = agentId.indexOf(":");
|
|
344
|
+
if (colonIdx > 0 && colonIdx < 20) {
|
|
345
|
+
const prefix = agentId.slice(0, colonIdx);
|
|
346
|
+
if (["openclaw", "otel", "langchain", "crewai", "mastra"].includes(prefix)) {
|
|
347
|
+
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { source: "agentflow", localId: agentId };
|
|
351
|
+
}
|
|
352
|
+
function extractSuffix(localId) {
|
|
353
|
+
const dashIdx = localId.indexOf("-");
|
|
354
|
+
if (dashIdx > 0 && dashIdx < localId.length - 1) {
|
|
355
|
+
const suffix = localId.slice(dashIdx + 1);
|
|
356
|
+
if (suffix.length >= 4) return suffix;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
function findPurpose(name) {
|
|
361
|
+
const lower = name.toLowerCase();
|
|
362
|
+
for (const { keywords, group } of PURPOSE_KEYWORDS) {
|
|
363
|
+
if (keywords.some((kw) => lower.includes(kw))) return group;
|
|
364
|
+
}
|
|
365
|
+
return "General";
|
|
366
|
+
}
|
|
367
|
+
function capitalize(s) {
|
|
368
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
369
|
+
}
|
|
370
|
+
function deduplicateAgents(agents) {
|
|
371
|
+
const tagged = agents.map((a) => ({
|
|
372
|
+
...a,
|
|
373
|
+
...extractSource(a.agentId)
|
|
374
|
+
}));
|
|
375
|
+
const suffixGroups = /* @__PURE__ */ new Map();
|
|
376
|
+
for (const a of tagged) {
|
|
377
|
+
const suffix = extractSuffix(a.localId);
|
|
378
|
+
if (!suffix) continue;
|
|
379
|
+
const group = suffixGroups.get(suffix) ?? [];
|
|
380
|
+
group.push(a);
|
|
381
|
+
suffixGroups.set(suffix, group);
|
|
382
|
+
}
|
|
383
|
+
const mergedIds = /* @__PURE__ */ new Set();
|
|
384
|
+
const mergedAgents = [];
|
|
385
|
+
for (const [suffix, group] of suffixGroups) {
|
|
386
|
+
if (group.length < 2) continue;
|
|
387
|
+
const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
|
|
388
|
+
if (prefixes.size < 2) continue;
|
|
389
|
+
const merged = {
|
|
390
|
+
agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
|
|
391
|
+
displayName: suffix,
|
|
392
|
+
totalExecutions: group.reduce((s, a) => s + a.totalExecutions, 0),
|
|
393
|
+
successfulExecutions: group.reduce((s, a) => s + a.successfulExecutions, 0),
|
|
394
|
+
failedExecutions: group.reduce((s, a) => s + a.failedExecutions, 0),
|
|
395
|
+
successRate: 0,
|
|
396
|
+
avgExecutionTime: 0,
|
|
397
|
+
lastExecution: Math.max(...group.map((a) => a.lastExecution)),
|
|
398
|
+
triggers: {},
|
|
399
|
+
recentActivity: group.flatMap((a) => a.recentActivity).sort((a, b) => b.timestamp - a.timestamp).slice(0, 50),
|
|
400
|
+
sources: group.map((a) => a.agentId),
|
|
401
|
+
adapterSource: group[0].source
|
|
402
|
+
};
|
|
403
|
+
merged.successRate = merged.totalExecutions > 0 ? merged.successfulExecutions / merged.totalExecutions * 100 : 0;
|
|
404
|
+
const totalExecTime = group.reduce((s, a) => s + a.avgExecutionTime * a.totalExecutions, 0);
|
|
405
|
+
merged.avgExecutionTime = merged.totalExecutions > 0 ? totalExecTime / merged.totalExecutions : 0;
|
|
406
|
+
for (const a of group) {
|
|
407
|
+
for (const [k, v] of Object.entries(a.triggers)) {
|
|
408
|
+
merged.triggers[k] = (merged.triggers[k] ?? 0) + v;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
mergedAgents.push(merged);
|
|
412
|
+
for (const a of group) mergedIds.add(a.agentId);
|
|
413
|
+
}
|
|
414
|
+
const result = [];
|
|
415
|
+
for (const a of agents) {
|
|
416
|
+
if (mergedIds.has(a.agentId)) continue;
|
|
417
|
+
const { source, localId } = extractSource(a.agentId);
|
|
418
|
+
result.push({
|
|
419
|
+
...a,
|
|
420
|
+
displayName: a.displayName ?? localId,
|
|
421
|
+
adapterSource: source
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return [...result, ...mergedAgents];
|
|
425
|
+
}
|
|
426
|
+
function groupAgents(agents) {
|
|
427
|
+
const sourceMap = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const a of agents) {
|
|
429
|
+
const source = a.adapterSource ?? extractSource(a.agentId).source;
|
|
430
|
+
const list = sourceMap.get(source) ?? [];
|
|
431
|
+
list.push(a);
|
|
432
|
+
sourceMap.set(source, list);
|
|
433
|
+
}
|
|
434
|
+
const SOURCE_DISPLAY = {
|
|
435
|
+
agentflow: "AgentFlow",
|
|
436
|
+
openclaw: "OpenClaw",
|
|
437
|
+
otel: "OpenTelemetry",
|
|
438
|
+
langchain: "LangChain",
|
|
439
|
+
crewai: "CrewAI"
|
|
440
|
+
};
|
|
441
|
+
const groups = [];
|
|
442
|
+
for (const [source, sourceAgents] of sourceMap) {
|
|
443
|
+
const subMap = /* @__PURE__ */ new Map();
|
|
444
|
+
for (const a of sourceAgents) {
|
|
445
|
+
const purpose = findPurpose(a.displayName ?? a.agentId);
|
|
446
|
+
const list = subMap.get(purpose) ?? [];
|
|
447
|
+
list.push(a.agentId);
|
|
448
|
+
subMap.set(purpose, list);
|
|
449
|
+
}
|
|
450
|
+
const subGroups = [...subMap.entries()].map(([name, agentIds]) => ({ name, agentIds })).sort((a, b) => b.agentIds.length - a.agentIds.length);
|
|
451
|
+
groups.push({
|
|
452
|
+
name: source,
|
|
453
|
+
displayName: SOURCE_DISPLAY[source] ?? capitalize(source),
|
|
454
|
+
totalExecutions: sourceAgents.reduce((s, a) => s + a.totalExecutions, 0),
|
|
455
|
+
failedExecutions: sourceAgents.reduce((s, a) => s + a.failedExecutions, 0),
|
|
456
|
+
agents: sourceAgents.sort((a, b) => b.totalExecutions - a.totalExecutions),
|
|
457
|
+
subGroups
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
groups.sort((a, b) => b.totalExecutions - a.totalExecutions);
|
|
461
|
+
return { groups };
|
|
462
|
+
}
|
|
463
|
+
|
|
48
464
|
// src/stats.ts
|
|
49
465
|
var import_agentflow_core = require("agentflow-core");
|
|
50
466
|
var AgentStats = class {
|
|
@@ -369,7 +785,7 @@ function openClawSessionIdToAgent(sessionId) {
|
|
|
369
785
|
if (sessionId.startsWith("janitor-")) return "vault-janitor";
|
|
370
786
|
if (sessionId.startsWith("curator-")) return "vault-curator";
|
|
371
787
|
if (sessionId.startsWith("distiller-")) return "vault-distiller";
|
|
372
|
-
if (sessionId.startsWith("main-")) return "main";
|
|
788
|
+
if (sessionId.startsWith("main-")) return "alfred-main";
|
|
373
789
|
const firstSegment = sessionId.split("-")[0];
|
|
374
790
|
if (firstSegment) return firstSegment;
|
|
375
791
|
return "openclaw";
|
|
@@ -454,10 +870,14 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
454
870
|
"package-lock.json",
|
|
455
871
|
"tsconfig.json",
|
|
456
872
|
"biome.json",
|
|
457
|
-
"jobs.json",
|
|
458
873
|
"auth.json",
|
|
459
874
|
"models.json",
|
|
460
|
-
"config.json"
|
|
875
|
+
"config.json",
|
|
876
|
+
"runs.json",
|
|
877
|
+
"sessions.json",
|
|
878
|
+
"containers.json",
|
|
879
|
+
"update-check.json",
|
|
880
|
+
"exec-approvals.json"
|
|
461
881
|
]);
|
|
462
882
|
static SKIP_SUFFIXES = [
|
|
463
883
|
"-state.json",
|
|
@@ -467,11 +887,15 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
467
887
|
".bak",
|
|
468
888
|
".backup"
|
|
469
889
|
];
|
|
470
|
-
/** Load a
|
|
890
|
+
/** Load a file using the adapter registry, falling back to built-in parsing. */
|
|
471
891
|
loadFile(filePath) {
|
|
472
892
|
const filename = path.basename(filePath);
|
|
473
893
|
if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
|
|
474
894
|
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
895
|
+
const adapter = findAdapter(filePath);
|
|
896
|
+
if (adapter && adapter.name !== "agentflow") {
|
|
897
|
+
return this.loadViaAdapter(filePath, adapter.name);
|
|
898
|
+
}
|
|
475
899
|
if (filePath.endsWith(".jsonl")) {
|
|
476
900
|
return this.loadSessionFile(filePath);
|
|
477
901
|
}
|
|
@@ -480,6 +904,57 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
480
904
|
}
|
|
481
905
|
return this.loadTraceFile(filePath);
|
|
482
906
|
}
|
|
907
|
+
/** Load a file using a specific adapter and store normalized traces. */
|
|
908
|
+
loadViaAdapter(filePath, adapterName) {
|
|
909
|
+
try {
|
|
910
|
+
const adapter = findAdapter(filePath);
|
|
911
|
+
if (!adapter) return false;
|
|
912
|
+
const normalized = adapter.parse(filePath);
|
|
913
|
+
if (normalized.length === 0) return false;
|
|
914
|
+
for (const trace of normalized) {
|
|
915
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
916
|
+
for (const [id, node] of Object.entries(trace.nodes)) {
|
|
917
|
+
nodes.set(id, {
|
|
918
|
+
id: node.id,
|
|
919
|
+
type: node.type,
|
|
920
|
+
name: node.name,
|
|
921
|
+
status: node.status,
|
|
922
|
+
startTime: node.startTime,
|
|
923
|
+
endTime: node.endTime,
|
|
924
|
+
parentId: node.parentId,
|
|
925
|
+
children: node.children,
|
|
926
|
+
metadata: node.metadata,
|
|
927
|
+
state: {}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
const watched = {
|
|
931
|
+
id: trace.id,
|
|
932
|
+
rootNodeId: Object.keys(trace.nodes)[0] ?? "",
|
|
933
|
+
agentId: trace.agentId,
|
|
934
|
+
name: trace.name,
|
|
935
|
+
trigger: trace.trigger,
|
|
936
|
+
startTime: trace.startTime,
|
|
937
|
+
endTime: trace.endTime,
|
|
938
|
+
status: trace.status,
|
|
939
|
+
nodes,
|
|
940
|
+
edges: [],
|
|
941
|
+
events: [],
|
|
942
|
+
metadata: { ...trace.metadata, adapterSource: adapterName },
|
|
943
|
+
sessionEvents: trace.sessionEvents ?? [],
|
|
944
|
+
sourceType: "session",
|
|
945
|
+
filename: path.basename(filePath),
|
|
946
|
+
lastModified: Date.now(),
|
|
947
|
+
sourceDir: path.dirname(filePath)
|
|
948
|
+
};
|
|
949
|
+
const key = `${adapterName}:${trace.id}`;
|
|
950
|
+
this.traces.set(key, watched);
|
|
951
|
+
}
|
|
952
|
+
return true;
|
|
953
|
+
} catch (error) {
|
|
954
|
+
console.error(`Adapter ${adapterName} failed for ${filePath}:`, error);
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
483
958
|
loadLogFile(filePath) {
|
|
484
959
|
try {
|
|
485
960
|
const content = fs.readFileSync(filePath, "utf8");
|
|
@@ -592,21 +1067,46 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
592
1067
|
}
|
|
593
1068
|
return traces;
|
|
594
1069
|
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Normalise agent identifiers so that the same worker is never shown
|
|
1072
|
+
* under two different names (e.g. "vault-curator" vs "openclaw-vault-curator").
|
|
1073
|
+
*
|
|
1074
|
+
* Canonical names: alfred-main, vault-curator, vault-janitor,
|
|
1075
|
+
* vault-distiller, vault-surveyor
|
|
1076
|
+
*/
|
|
1077
|
+
static AGENT_ALIASES = {
|
|
1078
|
+
"openclaw-main": "alfred-main",
|
|
1079
|
+
"openclaw-vault-curator": "vault-curator",
|
|
1080
|
+
"openclaw-vault-janitor": "vault-janitor",
|
|
1081
|
+
"openclaw-vault-distiller": "vault-distiller",
|
|
1082
|
+
"openclaw-vault-surveyor": "vault-surveyor",
|
|
1083
|
+
"alfred-curator": "vault-curator",
|
|
1084
|
+
"alfred-janitor": "vault-janitor",
|
|
1085
|
+
"alfred-distiller": "vault-distiller",
|
|
1086
|
+
"alfred-surveyor": "vault-surveyor",
|
|
1087
|
+
curator: "vault-curator",
|
|
1088
|
+
janitor: "vault-janitor",
|
|
1089
|
+
distiller: "vault-distiller",
|
|
1090
|
+
surveyor: "vault-surveyor"
|
|
1091
|
+
};
|
|
1092
|
+
normaliseAgentId(raw) {
|
|
1093
|
+
return _TraceWatcher.AGENT_ALIASES[raw] ?? raw;
|
|
1094
|
+
}
|
|
595
1095
|
detectAgentIdentifier(activity, _filename, filePath) {
|
|
596
1096
|
if (activity.agent_id) {
|
|
597
1097
|
const agentId = activity.agent_id;
|
|
598
|
-
if (agentId.
|
|
599
|
-
|
|
600
|
-
return agentId;
|
|
1098
|
+
if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
|
|
1099
|
+
return this.normaliseAgentId(agentId);
|
|
601
1100
|
}
|
|
602
1101
|
const pathAgent = this.extractAgentFromPath(filePath);
|
|
603
1102
|
if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
|
|
604
|
-
const
|
|
605
|
-
if (
|
|
606
|
-
|
|
1103
|
+
const basename3 = path.basename(filePath, path.extname(filePath));
|
|
1104
|
+
if (basename3.match(/^(janitor|curator|distiller|surveyor|alfred)$/)) {
|
|
1105
|
+
const raw = basename3 === "alfred" ? "alfred" : `alfred-${basename3}`;
|
|
1106
|
+
return this.normaliseAgentId(raw);
|
|
607
1107
|
}
|
|
608
1108
|
}
|
|
609
|
-
return pathAgent;
|
|
1109
|
+
return this.normaliseAgentId(pathAgent);
|
|
610
1110
|
}
|
|
611
1111
|
extractAgentFromPath(filePath) {
|
|
612
1112
|
const filename = path.basename(filePath, path.extname(filePath));
|
|
@@ -1437,8 +1937,23 @@ var TraceWatcher = class _TraceWatcher extends import_node_events.EventEmitter {
|
|
|
1437
1937
|
getTrace(filename) {
|
|
1438
1938
|
const exact = this.traces.get(filename);
|
|
1439
1939
|
if (exact) return exact;
|
|
1940
|
+
if (filename.includes("::")) {
|
|
1941
|
+
const [fname, startTimeStr] = filename.split("::");
|
|
1942
|
+
const startTime = Number(startTimeStr);
|
|
1943
|
+
if (fname && !Number.isNaN(startTime)) {
|
|
1944
|
+
for (const trace of this.traces.values()) {
|
|
1945
|
+
if (trace.filename === fname && trace.startTime === startTime) {
|
|
1946
|
+
return trace;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
for (const prefix of ["openclaw:", "otel:", ""]) {
|
|
1952
|
+
const prefixed = this.traces.get(prefix + filename);
|
|
1953
|
+
if (prefixed) return prefixed;
|
|
1954
|
+
}
|
|
1440
1955
|
for (const [key, trace] of this.traces) {
|
|
1441
|
-
if (trace.filename === filename || key.endsWith(filename)) {
|
|
1956
|
+
if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
|
|
1442
1957
|
return trace;
|
|
1443
1958
|
}
|
|
1444
1959
|
}
|
|
@@ -1568,11 +2083,23 @@ async function startDashboard() {
|
|
|
1568
2083
|
case "--cors":
|
|
1569
2084
|
config.enableCors = true;
|
|
1570
2085
|
break;
|
|
2086
|
+
case "--no-collector":
|
|
2087
|
+
config.enableCollector = false;
|
|
2088
|
+
break;
|
|
2089
|
+
case "--collector-token":
|
|
2090
|
+
config.collectorAuthToken = args[++i];
|
|
2091
|
+
break;
|
|
1571
2092
|
case "--help":
|
|
1572
2093
|
printHelp();
|
|
1573
2094
|
process.exit(0);
|
|
1574
2095
|
}
|
|
1575
2096
|
}
|
|
2097
|
+
if (!config.collectorAuthToken && process.env.AGENTFLOW_COLLECTOR_TOKEN) {
|
|
2098
|
+
config.collectorAuthToken = process.env.AGENTFLOW_COLLECTOR_TOKEN;
|
|
2099
|
+
}
|
|
2100
|
+
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
2101
|
+
config.enableCollector = false;
|
|
2102
|
+
}
|
|
1576
2103
|
const tracesPath = path2.resolve(config.tracesDir);
|
|
1577
2104
|
if (!fs2.existsSync(tracesPath)) {
|
|
1578
2105
|
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -1615,6 +2142,8 @@ Options:
|
|
|
1615
2142
|
-h, --host <address> Host address (default: localhost)
|
|
1616
2143
|
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
1617
2144
|
--cors Enable CORS headers
|
|
2145
|
+
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
2146
|
+
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
1618
2147
|
--help Show this help message
|
|
1619
2148
|
|
|
1620
2149
|
Examples:
|
|
@@ -1650,6 +2179,31 @@ function serializeTrace(trace) {
|
|
|
1650
2179
|
var DashboardServer = class {
|
|
1651
2180
|
constructor(config) {
|
|
1652
2181
|
this.config = config;
|
|
2182
|
+
const home = process.env.HOME ?? "/home/trader";
|
|
2183
|
+
const configPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2184
|
+
if (!config.dataDirs) config.dataDirs = [];
|
|
2185
|
+
try {
|
|
2186
|
+
if (fs3.existsSync(configPath)) {
|
|
2187
|
+
const saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
2188
|
+
const extraDirs = saved.extraDirs ?? [];
|
|
2189
|
+
for (const d of extraDirs) {
|
|
2190
|
+
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
} catch {
|
|
2194
|
+
}
|
|
2195
|
+
const autoDiscoverPaths = [
|
|
2196
|
+
path3.join(home, ".openclaw/cron/runs"),
|
|
2197
|
+
path3.join(home, ".openclaw/workspace/traces"),
|
|
2198
|
+
path3.join(home, ".openclaw/subagents"),
|
|
2199
|
+
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2200
|
+
path3.join(home, ".agentflow/traces")
|
|
2201
|
+
];
|
|
2202
|
+
for (const p of autoDiscoverPaths) {
|
|
2203
|
+
if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2204
|
+
config.dataDirs.push(p);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
1653
2207
|
this.watcher = new TraceWatcher({
|
|
1654
2208
|
tracesDir: config.tracesDir,
|
|
1655
2209
|
dataDirs: config.dataDirs
|
|
@@ -1698,9 +2252,13 @@ var DashboardServer = class {
|
|
|
1698
2252
|
next();
|
|
1699
2253
|
});
|
|
1700
2254
|
}
|
|
2255
|
+
const clientDir = path3.join(__dirname, "../dist/client");
|
|
2256
|
+
if (fs3.existsSync(clientDir)) {
|
|
2257
|
+
this.app.use(import_express.default.static(clientDir));
|
|
2258
|
+
}
|
|
1701
2259
|
const publicDir = path3.join(__dirname, "../public");
|
|
1702
2260
|
if (fs3.existsSync(publicDir)) {
|
|
1703
|
-
this.app.use(import_express.default.static(publicDir));
|
|
2261
|
+
this.app.use("/v1", import_express.default.static(publicDir));
|
|
1704
2262
|
}
|
|
1705
2263
|
this.app.get("/api/traces", (_req, res) => {
|
|
1706
2264
|
try {
|
|
@@ -1736,10 +2294,28 @@ var DashboardServer = class {
|
|
|
1736
2294
|
res.status(500).json({ error: "Failed to load trace events" });
|
|
1737
2295
|
}
|
|
1738
2296
|
});
|
|
1739
|
-
this.app.get("/api/agents", (
|
|
2297
|
+
this.app.get("/api/agents", (req, res) => {
|
|
1740
2298
|
try {
|
|
1741
|
-
const
|
|
1742
|
-
|
|
2299
|
+
const raw = this.stats.getAgentsList();
|
|
2300
|
+
for (const agent of raw) {
|
|
2301
|
+
if (!agent.displayName) {
|
|
2302
|
+
const traces = this.watcher.getTracesByAgent(agent.agentId);
|
|
2303
|
+
if (traces.length > 0) {
|
|
2304
|
+
const latest = traces[traces.length - 1];
|
|
2305
|
+
const name = latest == null ? void 0 : latest.name;
|
|
2306
|
+
if (name && name !== "default" && name !== agent.agentId && !name.startsWith("pipeline:") && name.length < 40) {
|
|
2307
|
+
agent.displayName = name;
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
if (!agent.displayName) agent.displayName = agent.agentId;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (req.query.flat === "true") {
|
|
2314
|
+
return res.json(raw);
|
|
2315
|
+
}
|
|
2316
|
+
const deduped = deduplicateAgents(raw);
|
|
2317
|
+
const grouped = groupAgents(deduped);
|
|
2318
|
+
res.json(grouped);
|
|
1743
2319
|
} catch (_error) {
|
|
1744
2320
|
res.status(500).json({ error: "Failed to load agents" });
|
|
1745
2321
|
}
|
|
@@ -1889,6 +2465,84 @@ var DashboardServer = class {
|
|
|
1889
2465
|
res.status(500).json({ error: "Failed to load agent profile" });
|
|
1890
2466
|
}
|
|
1891
2467
|
});
|
|
2468
|
+
this.app.get("/api/process-model/:agentId", (req, res) => {
|
|
2469
|
+
try {
|
|
2470
|
+
const agentId = req.params.agentId;
|
|
2471
|
+
const allTraces = this.watcher.getTracesByAgent(agentId);
|
|
2472
|
+
if (allTraces.length === 0) {
|
|
2473
|
+
return res.status(404).json({ error: "No traces for agent" });
|
|
2474
|
+
}
|
|
2475
|
+
const transMap = /* @__PURE__ */ new Map();
|
|
2476
|
+
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
2477
|
+
const variantMap = /* @__PURE__ */ new Map();
|
|
2478
|
+
const durationMap = /* @__PURE__ */ new Map();
|
|
2479
|
+
for (const trace of allTraces) {
|
|
2480
|
+
const serialized = serializeTrace(trace);
|
|
2481
|
+
const nodes = serialized.nodes;
|
|
2482
|
+
if (!nodes || typeof nodes !== "object") continue;
|
|
2483
|
+
const nodeArr = Object.values(nodes);
|
|
2484
|
+
const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
|
|
2485
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
2486
|
+
const from = sorted[i].name;
|
|
2487
|
+
const to = sorted[i + 1].name;
|
|
2488
|
+
const key = `${from}|||${to}`;
|
|
2489
|
+
transMap.set(key, (transMap.get(key) ?? 0) + 1);
|
|
2490
|
+
}
|
|
2491
|
+
for (const n of sorted) {
|
|
2492
|
+
if (n.name && n.type) nodeTypeMap.set(n.name, n.type);
|
|
2493
|
+
}
|
|
2494
|
+
const sig = sorted.map((n) => n.name).join("\u2192");
|
|
2495
|
+
if (sig) variantMap.set(sig, (variantMap.get(sig) ?? 0) + 1);
|
|
2496
|
+
for (const n of sorted) {
|
|
2497
|
+
if (n.name && n.endTime && n.startTime) {
|
|
2498
|
+
const dur = n.endTime - n.startTime;
|
|
2499
|
+
if (dur > 0) {
|
|
2500
|
+
const arr = durationMap.get(n.name) ?? [];
|
|
2501
|
+
arr.push(dur);
|
|
2502
|
+
durationMap.set(n.name, arr);
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
const model = {
|
|
2508
|
+
transitions: [...transMap.entries()].map(([key, count]) => {
|
|
2509
|
+
const [from, to] = key.split("|||");
|
|
2510
|
+
return { from, to, count };
|
|
2511
|
+
}),
|
|
2512
|
+
nodeTypes: Object.fromEntries(nodeTypeMap)
|
|
2513
|
+
};
|
|
2514
|
+
const totalTraces = allTraces.length;
|
|
2515
|
+
const variants = [...variantMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([sig, count]) => ({
|
|
2516
|
+
pathSignature: sig,
|
|
2517
|
+
count,
|
|
2518
|
+
percentage: totalTraces > 0 ? count / totalTraces * 100 : 0
|
|
2519
|
+
}));
|
|
2520
|
+
const bottlenecks = [...durationMap.entries()].map(([name, durations]) => {
|
|
2521
|
+
const sorted = durations.sort((a, b) => a - b);
|
|
2522
|
+
const p95 = sorted[Math.floor(sorted.length * 0.95)] ?? 0;
|
|
2523
|
+
return { nodeName: name, nodeType: nodeTypeMap.get(name) ?? "unknown", p95 };
|
|
2524
|
+
}).sort((a, b) => b.p95 - a.p95).slice(0, 15);
|
|
2525
|
+
const graphs = this.getGraphTraces(agentId);
|
|
2526
|
+
if (graphs.length > 0) {
|
|
2527
|
+
try {
|
|
2528
|
+
const coreBottlenecks = (0, import_agentflow_core3.getBottlenecks)(graphs).map((b) => ({
|
|
2529
|
+
nodeName: b.nodeName,
|
|
2530
|
+
nodeType: b.nodeType,
|
|
2531
|
+
p95: b.durations.sort((a, b2) => a - b2)[Math.floor(b.durations.length * 0.95)] ?? 0
|
|
2532
|
+
}));
|
|
2533
|
+
if (coreBottlenecks.length > bottlenecks.length) {
|
|
2534
|
+
bottlenecks.length = 0;
|
|
2535
|
+
bottlenecks.push(...coreBottlenecks);
|
|
2536
|
+
}
|
|
2537
|
+
} catch {
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
res.json({ model, variants, bottlenecks });
|
|
2541
|
+
} catch (error) {
|
|
2542
|
+
console.error("Process model error:", error);
|
|
2543
|
+
res.status(500).json({ error: "Failed to compute process model" });
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
1892
2546
|
this.app.get("/api/stats/:agentId", (req, res) => {
|
|
1893
2547
|
try {
|
|
1894
2548
|
const agentStats = this.stats.getAgentStats(req.params.agentId);
|
|
@@ -1901,6 +2555,7 @@ var DashboardServer = class {
|
|
|
1901
2555
|
}
|
|
1902
2556
|
});
|
|
1903
2557
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2558
|
+
var _a, _b;
|
|
1904
2559
|
try {
|
|
1905
2560
|
const now = Date.now();
|
|
1906
2561
|
if (this.processHealthCache.result && now - this.processHealthCache.ts < 1e4) {
|
|
@@ -1911,67 +2566,211 @@ var DashboardServer = class {
|
|
|
1911
2566
|
path3.dirname(this.config.tracesDir),
|
|
1912
2567
|
...this.config.dataDirs || []
|
|
1913
2568
|
];
|
|
1914
|
-
const
|
|
1915
|
-
if (
|
|
2569
|
+
const configs = (0, import_agentflow_core3.discoverAllProcessConfigs)(discoveryDirs);
|
|
2570
|
+
if (configs.length === 0) {
|
|
1916
2571
|
return res.json(null);
|
|
1917
2572
|
}
|
|
1918
|
-
const
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
const
|
|
1934
|
-
|
|
1935
|
-
...openclawResult.osProcesses,
|
|
1936
|
-
...clawmetryResult.osProcesses
|
|
1937
|
-
];
|
|
2573
|
+
const services = [];
|
|
2574
|
+
const allKnownPids = /* @__PURE__ */ new Set();
|
|
2575
|
+
for (const config of configs) {
|
|
2576
|
+
const audit = (0, import_agentflow_core3.auditProcesses)(config);
|
|
2577
|
+
services.push({ name: config.processName, audit });
|
|
2578
|
+
if (((_a = audit.pidFile) == null ? void 0 : _a.pid) && !audit.pidFile.stale) allKnownPids.add(audit.pidFile.pid);
|
|
2579
|
+
if ((_b = audit.systemd) == null ? void 0 : _b.mainPid) allKnownPids.add(audit.systemd.mainPid);
|
|
2580
|
+
if (audit.workers) {
|
|
2581
|
+
if (audit.workers.orchestratorPid) allKnownPids.add(audit.workers.orchestratorPid);
|
|
2582
|
+
for (const w of audit.workers.workers) {
|
|
2583
|
+
if (w.pid) allKnownPids.add(w.pid);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
for (const p of audit.osProcesses) allKnownPids.add(p.pid);
|
|
2587
|
+
}
|
|
2588
|
+
const primary = services.find((s) => s.audit.pidFile) ?? services[0];
|
|
2589
|
+
const allOsProcesses = services.flatMap((s) => s.audit.osProcesses);
|
|
1938
2590
|
const uniqueProcesses = allOsProcesses.filter(
|
|
1939
2591
|
(proc, index, arr) => arr.findIndex((p) => p.pid === proc.pid) === index
|
|
1940
2592
|
);
|
|
2593
|
+
const orphans = uniqueProcesses.filter(
|
|
2594
|
+
(p) => !allKnownPids.has(p.pid) && p.pid !== process.pid && p.pid !== process.ppid
|
|
2595
|
+
);
|
|
2596
|
+
const problems = services.flatMap(
|
|
2597
|
+
(s) => s.audit.problems.map((p) => `[${s.name}] ${p}`)
|
|
2598
|
+
);
|
|
1941
2599
|
const result = {
|
|
1942
|
-
|
|
2600
|
+
// Backward-compatible fields from primary service
|
|
2601
|
+
pidFile: (primary == null ? void 0 : primary.audit.pidFile) ?? null,
|
|
2602
|
+
systemd: (primary == null ? void 0 : primary.audit.systemd) ?? null,
|
|
2603
|
+
workers: (primary == null ? void 0 : primary.audit.workers) ?? null,
|
|
1943
2604
|
osProcesses: uniqueProcesses,
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2605
|
+
orphans,
|
|
2606
|
+
problems,
|
|
2607
|
+
// All discovered services with their individual audit results + metrics
|
|
2608
|
+
services: services.map((s) => {
|
|
2609
|
+
var _a2, _b2;
|
|
2610
|
+
const mainPid = ((_a2 = s.audit.pidFile) == null ? void 0 : _a2.pid) ?? ((_b2 = s.audit.systemd) == null ? void 0 : _b2.mainPid);
|
|
2611
|
+
const osProc = mainPid ? uniqueProcesses.find((p) => p.pid === mainPid) : void 0;
|
|
2612
|
+
return {
|
|
2613
|
+
name: s.name,
|
|
2614
|
+
pidFile: s.audit.pidFile,
|
|
2615
|
+
systemd: s.audit.systemd,
|
|
2616
|
+
workers: s.audit.workers,
|
|
2617
|
+
problems: s.audit.problems,
|
|
2618
|
+
metrics: osProc ? { cpu: osProc.cpu, mem: osProc.mem, elapsed: osProc.elapsed } : void 0
|
|
2619
|
+
};
|
|
2620
|
+
}),
|
|
2621
|
+
// Topology edges: parent-child relationships from process ppid
|
|
2622
|
+
topology: uniqueProcesses.map((p) => {
|
|
2623
|
+
try {
|
|
2624
|
+
const statusContent = fs3.readFileSync(`/proc/${p.pid}/status`, "utf8");
|
|
2625
|
+
const ppidMatch = statusContent.match(/^PPid:\s+(\d+)/m);
|
|
2626
|
+
const ppid = ppidMatch ? parseInt(ppidMatch[1] ?? "0", 10) : 0;
|
|
2627
|
+
if (ppid > 1 && allKnownPids.has(ppid)) {
|
|
2628
|
+
return { source: ppid, target: p.pid };
|
|
1955
2629
|
}
|
|
2630
|
+
} catch {
|
|
1956
2631
|
}
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
})
|
|
2632
|
+
return null;
|
|
2633
|
+
}).filter(Boolean)
|
|
1960
2634
|
};
|
|
1961
|
-
const openclawProblems = [];
|
|
1962
|
-
if (openclawResult.osProcesses.length === 0) {
|
|
1963
|
-
openclawProblems.push("No OpenClaw gateway processes detected");
|
|
1964
|
-
}
|
|
1965
|
-
if (clawmetryResult.osProcesses.length === 0) {
|
|
1966
|
-
openclawProblems.push("No clawmetry processes detected");
|
|
1967
|
-
}
|
|
1968
|
-
result.problems = [...alfredResult.problems || [], ...openclawProblems];
|
|
1969
2635
|
this.processHealthCache = { result, ts: now };
|
|
1970
2636
|
res.json(result);
|
|
1971
2637
|
} catch (_error) {
|
|
1972
2638
|
res.status(500).json({ error: "Failed to audit processes" });
|
|
1973
2639
|
}
|
|
1974
2640
|
});
|
|
2641
|
+
this.app.get("/api/directories", (_req, res) => {
|
|
2642
|
+
try {
|
|
2643
|
+
const home = process.env.HOME ?? "/home/trader";
|
|
2644
|
+
const configPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2645
|
+
let extraDirs = [];
|
|
2646
|
+
try {
|
|
2647
|
+
if (fs3.existsSync(configPath)) {
|
|
2648
|
+
const cfg = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
2649
|
+
extraDirs = cfg.extraDirs ?? [];
|
|
2650
|
+
}
|
|
2651
|
+
} catch {
|
|
2652
|
+
}
|
|
2653
|
+
const watched = [
|
|
2654
|
+
this.config.tracesDir,
|
|
2655
|
+
...this.config.dataDirs || [],
|
|
2656
|
+
...extraDirs
|
|
2657
|
+
];
|
|
2658
|
+
const discovered = [];
|
|
2659
|
+
try {
|
|
2660
|
+
const { execSync } = require("child_process");
|
|
2661
|
+
const raw = execSync(
|
|
2662
|
+
"systemctl --user show --property=ExecStart --no-pager alfred.service openclaw-gateway.service 2>/dev/null",
|
|
2663
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
2664
|
+
);
|
|
2665
|
+
for (const line of raw.split("\n")) {
|
|
2666
|
+
const match = line.match(/path=([^\s;]+)/);
|
|
2667
|
+
if (match == null ? void 0 : match[1]) {
|
|
2668
|
+
const dir = path3.dirname(match[1]);
|
|
2669
|
+
if (fs3.existsSync(dir)) discovered.push(dir);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
} catch {
|
|
2673
|
+
}
|
|
2674
|
+
const commonPaths = [
|
|
2675
|
+
path3.join(home, ".alfred/traces"),
|
|
2676
|
+
path3.join(home, ".alfred/data"),
|
|
2677
|
+
path3.join(home, ".openclaw/workspace/traces"),
|
|
2678
|
+
path3.join(home, ".openclaw/subagents"),
|
|
2679
|
+
path3.join(home, ".openclaw/cron/runs"),
|
|
2680
|
+
path3.join(home, ".openclaw/cron"),
|
|
2681
|
+
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2682
|
+
path3.join(home, ".agentflow/traces")
|
|
2683
|
+
];
|
|
2684
|
+
for (const p of commonPaths) {
|
|
2685
|
+
if (fs3.existsSync(p) && !discovered.includes(p)) {
|
|
2686
|
+
discovered.push(p);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
const watchedSet = new Set(watched.map((w) => path3.resolve(w)));
|
|
2690
|
+
const suggested = discovered.filter((d) => !watchedSet.has(path3.resolve(d)));
|
|
2691
|
+
res.json({ watched, discovered, suggested });
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
console.error("Directory discovery error:", error);
|
|
2694
|
+
res.status(500).json({ error: "Failed to discover directories" });
|
|
2695
|
+
}
|
|
2696
|
+
});
|
|
2697
|
+
this.app.post("/api/directories", import_express.default.json(), (req, res) => {
|
|
2698
|
+
try {
|
|
2699
|
+
const { add, remove } = req.body;
|
|
2700
|
+
if (add && !fs3.existsSync(add)) {
|
|
2701
|
+
return res.status(400).json({ error: `Directory does not exist: ${add}` });
|
|
2702
|
+
}
|
|
2703
|
+
const configPath = path3.join(process.env.HOME ?? "/home/trader", ".agentflow/dashboard-config.json");
|
|
2704
|
+
let config = {};
|
|
2705
|
+
try {
|
|
2706
|
+
if (fs3.existsSync(configPath)) {
|
|
2707
|
+
config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
2708
|
+
}
|
|
2709
|
+
} catch {
|
|
2710
|
+
}
|
|
2711
|
+
if (!config.extraDirs) config.extraDirs = [];
|
|
2712
|
+
if (add && !config.extraDirs.includes(add)) {
|
|
2713
|
+
config.extraDirs.push(add);
|
|
2714
|
+
}
|
|
2715
|
+
if (remove) {
|
|
2716
|
+
config.extraDirs = config.extraDirs.filter((d) => d !== remove);
|
|
2717
|
+
}
|
|
2718
|
+
fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
|
|
2719
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
2720
|
+
res.json({ ok: true, extraDirs: config.extraDirs });
|
|
2721
|
+
} catch (error) {
|
|
2722
|
+
console.error("Directory config error:", error);
|
|
2723
|
+
res.status(500).json({ error: "Failed to update directory config" });
|
|
2724
|
+
}
|
|
2725
|
+
});
|
|
2726
|
+
if (this.config.enableCollector !== false) {
|
|
2727
|
+
this.app.post("/v1/traces", import_express.default.json({ limit: "10mb" }), (req, res) => {
|
|
2728
|
+
try {
|
|
2729
|
+
if (this.config.collectorAuthToken) {
|
|
2730
|
+
const auth = req.headers.authorization;
|
|
2731
|
+
if (!auth || auth !== `Bearer ${this.config.collectorAuthToken}`) {
|
|
2732
|
+
return res.status(401).json({ error: "Unauthorized \u2014 provide Authorization: Bearer <token>" });
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
const traces = parseOtlpPayload(req.body);
|
|
2736
|
+
let ingested = 0;
|
|
2737
|
+
for (const trace of traces) {
|
|
2738
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
2739
|
+
for (const [id, node] of Object.entries(trace.nodes)) {
|
|
2740
|
+
nodes.set(id, { ...node, state: {} });
|
|
2741
|
+
}
|
|
2742
|
+
const watched = {
|
|
2743
|
+
id: trace.id,
|
|
2744
|
+
rootNodeId: Object.keys(trace.nodes)[0] ?? "",
|
|
2745
|
+
agentId: trace.agentId,
|
|
2746
|
+
name: trace.name,
|
|
2747
|
+
trigger: trace.trigger,
|
|
2748
|
+
startTime: trace.startTime,
|
|
2749
|
+
endTime: trace.endTime,
|
|
2750
|
+
status: trace.status,
|
|
2751
|
+
nodes,
|
|
2752
|
+
edges: [],
|
|
2753
|
+
events: [],
|
|
2754
|
+
metadata: { ...trace.metadata, adapterSource: "otel" },
|
|
2755
|
+
sessionEvents: [],
|
|
2756
|
+
sourceType: "session",
|
|
2757
|
+
filename: `otel-${trace.id}`,
|
|
2758
|
+
lastModified: Date.now(),
|
|
2759
|
+
sourceDir: "http-collector"
|
|
2760
|
+
};
|
|
2761
|
+
this.watcher.traces.set(`otel:${trace.id}`, watched);
|
|
2762
|
+
ingested++;
|
|
2763
|
+
}
|
|
2764
|
+
if (ingested > 0) {
|
|
2765
|
+
this.broadcast({ type: "traces-updated", count: ingested });
|
|
2766
|
+
}
|
|
2767
|
+
res.json({ ok: true, tracesIngested: ingested });
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
console.error("OTLP collector error:", error);
|
|
2770
|
+
res.status(400).json({ error: "Failed to parse OTLP payload" });
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
1975
2774
|
this.app.get("/health", (_req, res) => {
|
|
1976
2775
|
res.json({
|
|
1977
2776
|
status: "ok",
|
|
@@ -1983,10 +2782,18 @@ var DashboardServer = class {
|
|
|
1983
2782
|
this.app.get("/ready", (_req, res) => {
|
|
1984
2783
|
res.json({ status: "ready" });
|
|
1985
2784
|
});
|
|
2785
|
+
this.app.get("/v1/*", (_req, res) => {
|
|
2786
|
+
const legacyIndex = path3.join(__dirname, "../public/index.html");
|
|
2787
|
+
if (fs3.existsSync(legacyIndex)) {
|
|
2788
|
+
res.sendFile(legacyIndex);
|
|
2789
|
+
} else {
|
|
2790
|
+
res.status(404).send("Legacy dashboard not found");
|
|
2791
|
+
}
|
|
2792
|
+
});
|
|
1986
2793
|
this.app.get("*", (_req, res) => {
|
|
1987
|
-
const
|
|
1988
|
-
if (fs3.existsSync(
|
|
1989
|
-
res.sendFile(
|
|
2794
|
+
const clientIndex = path3.join(__dirname, "../dist/client/index.html");
|
|
2795
|
+
if (fs3.existsSync(clientIndex)) {
|
|
2796
|
+
res.sendFile(clientIndex);
|
|
1990
2797
|
} else {
|
|
1991
2798
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
1992
2799
|
}
|
|
@@ -2233,21 +3040,21 @@ var DashboardServer = class {
|
|
|
2233
3040
|
});
|
|
2234
3041
|
}
|
|
2235
3042
|
async start() {
|
|
2236
|
-
return new Promise((
|
|
3043
|
+
return new Promise((resolve4) => {
|
|
2237
3044
|
const host = this.config.host || "localhost";
|
|
2238
3045
|
this.server.listen(this.config.port, host, () => {
|
|
2239
3046
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
2240
3047
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
2241
|
-
|
|
3048
|
+
resolve4();
|
|
2242
3049
|
});
|
|
2243
3050
|
});
|
|
2244
3051
|
}
|
|
2245
3052
|
async stop() {
|
|
2246
|
-
return new Promise((
|
|
3053
|
+
return new Promise((resolve4) => {
|
|
2247
3054
|
this.watcher.stop();
|
|
2248
3055
|
this.server.close(() => {
|
|
2249
3056
|
console.log("Dashboard server stopped");
|
|
2250
|
-
|
|
3057
|
+
resolve4();
|
|
2251
3058
|
});
|
|
2252
3059
|
});
|
|
2253
3060
|
}
|