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