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
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/server.ts
|
|
2
9
|
import * as fs3 from "fs";
|
|
3
10
|
import { createServer } from "http";
|
|
@@ -7,8 +14,8 @@ import {
|
|
|
7
14
|
auditProcesses,
|
|
8
15
|
createExecutionEvent,
|
|
9
16
|
createKnowledgeStore,
|
|
17
|
+
discoverAllProcessConfigs,
|
|
10
18
|
discoverProcess,
|
|
11
|
-
discoverProcessConfig,
|
|
12
19
|
findVariants,
|
|
13
20
|
getBottlenecks,
|
|
14
21
|
loadGraph as loadGraph2
|
|
@@ -16,6 +23,422 @@ import {
|
|
|
16
23
|
import express from "express";
|
|
17
24
|
import { WebSocketServer } from "ws";
|
|
18
25
|
|
|
26
|
+
// src/adapters/agentflow.ts
|
|
27
|
+
var SKIP_FILES = /* @__PURE__ */ new Set([
|
|
28
|
+
"workers.json",
|
|
29
|
+
"package.json",
|
|
30
|
+
"package-lock.json",
|
|
31
|
+
"tsconfig.json",
|
|
32
|
+
"biome.json",
|
|
33
|
+
"auth.json",
|
|
34
|
+
"models.json",
|
|
35
|
+
"config.json"
|
|
36
|
+
]);
|
|
37
|
+
var SKIP_SUFFIXES = ["-state.json", "-config.json", "-watch-state.json", ".tmp", ".bak", ".backup"];
|
|
38
|
+
var AgentFlowAdapter = class {
|
|
39
|
+
name = "agentflow";
|
|
40
|
+
detect(_dirPath) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
canHandle(filePath) {
|
|
44
|
+
const filename = filePath.split("/").pop() ?? "";
|
|
45
|
+
if (SKIP_FILES.has(filename)) return false;
|
|
46
|
+
if (SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
47
|
+
return filename.endsWith(".json") || filename.endsWith(".jsonl") || filename.endsWith(".log") || filename.endsWith(".trace");
|
|
48
|
+
}
|
|
49
|
+
parse(_filePath) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/adapters/openclaw.ts
|
|
55
|
+
import { existsSync, readFileSync } from "fs";
|
|
56
|
+
import { basename, dirname, join } from "path";
|
|
57
|
+
var jobCache = /* @__PURE__ */ new Map();
|
|
58
|
+
function loadJobs(openclawDir) {
|
|
59
|
+
const cached = jobCache.get(openclawDir);
|
|
60
|
+
if (cached) return cached;
|
|
61
|
+
const jobsPath = join(openclawDir, "cron", "jobs.json");
|
|
62
|
+
const map = /* @__PURE__ */ new Map();
|
|
63
|
+
try {
|
|
64
|
+
if (existsSync(jobsPath)) {
|
|
65
|
+
const data = JSON.parse(readFileSync(jobsPath, "utf-8"));
|
|
66
|
+
const jobs = Array.isArray(data) ? data : data.jobs ?? [];
|
|
67
|
+
for (const job of jobs) {
|
|
68
|
+
if (job.id) map.set(job.id, job);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
jobCache.set(openclawDir, map);
|
|
74
|
+
return map;
|
|
75
|
+
}
|
|
76
|
+
function findOpenClawRoot(filePath) {
|
|
77
|
+
let dir = dirname(filePath);
|
|
78
|
+
for (let i = 0; i < 5; i++) {
|
|
79
|
+
if (existsSync(join(dir, "cron", "jobs.json")) || basename(dir) === ".openclaw") {
|
|
80
|
+
return dir;
|
|
81
|
+
}
|
|
82
|
+
dir = dirname(dir);
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
var OpenClawAdapter = class {
|
|
87
|
+
name = "openclaw";
|
|
88
|
+
detect(dirPath) {
|
|
89
|
+
return existsSync(join(dirPath, "cron", "jobs.json")) || dirPath.includes(".openclaw") || existsSync(join(dirPath, "cron", "runs"));
|
|
90
|
+
}
|
|
91
|
+
canHandle(filePath) {
|
|
92
|
+
if (!filePath.endsWith(".jsonl")) return false;
|
|
93
|
+
return filePath.includes("/cron/runs/") || filePath.includes("\\cron\\runs\\");
|
|
94
|
+
}
|
|
95
|
+
parse(filePath) {
|
|
96
|
+
const traces = [];
|
|
97
|
+
try {
|
|
98
|
+
const content = readFileSync(filePath, "utf-8");
|
|
99
|
+
const root = findOpenClawRoot(filePath);
|
|
100
|
+
const jobs = root ? loadJobs(root) : /* @__PURE__ */ new Map();
|
|
101
|
+
for (const line of content.split("\n")) {
|
|
102
|
+
if (!line.trim()) continue;
|
|
103
|
+
let entry;
|
|
104
|
+
try {
|
|
105
|
+
entry = JSON.parse(line);
|
|
106
|
+
} catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (entry.action !== "finished") continue;
|
|
110
|
+
const jobId = entry.jobId ?? basename(filePath, ".jsonl");
|
|
111
|
+
const job = jobs.get(jobId);
|
|
112
|
+
const jobName = (job == null ? void 0 : job.name) ?? jobId;
|
|
113
|
+
const startTime = entry.runAtMs ?? entry.ts;
|
|
114
|
+
const duration = entry.durationMs ?? 0;
|
|
115
|
+
const trace = {
|
|
116
|
+
id: entry.sessionId ?? `${jobId}-${entry.ts}`,
|
|
117
|
+
agentId: `openclaw:${jobId}`,
|
|
118
|
+
name: jobName,
|
|
119
|
+
status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
|
|
120
|
+
startTime,
|
|
121
|
+
endTime: startTime + duration,
|
|
122
|
+
trigger: "cron",
|
|
123
|
+
source: "openclaw",
|
|
124
|
+
nodes: {
|
|
125
|
+
root: {
|
|
126
|
+
id: "root",
|
|
127
|
+
type: "cron-job",
|
|
128
|
+
name: jobName,
|
|
129
|
+
status: entry.status === "ok" ? "completed" : entry.status === "error" ? "failed" : "unknown",
|
|
130
|
+
startTime,
|
|
131
|
+
endTime: startTime + duration,
|
|
132
|
+
parentId: null,
|
|
133
|
+
children: [],
|
|
134
|
+
metadata: {
|
|
135
|
+
jobId,
|
|
136
|
+
summary: entry.summary,
|
|
137
|
+
error: entry.error,
|
|
138
|
+
delivered: entry.delivered,
|
|
139
|
+
deliveryStatus: entry.deliveryStatus
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
metadata: {
|
|
144
|
+
model: entry.model,
|
|
145
|
+
provider: entry.provider,
|
|
146
|
+
usage: entry.usage,
|
|
147
|
+
sessionId: entry.sessionId,
|
|
148
|
+
sessionKey: entry.sessionKey,
|
|
149
|
+
nextRunAtMs: entry.nextRunAtMs
|
|
150
|
+
},
|
|
151
|
+
filePath
|
|
152
|
+
};
|
|
153
|
+
traces.push(trace);
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
return traces;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// src/adapters/otel.ts
|
|
162
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
163
|
+
import { join as join2 } from "path";
|
|
164
|
+
var SPAN_TYPE_MAP = {
|
|
165
|
+
"gen_ai.chat": "llm",
|
|
166
|
+
"gen_ai.completion": "llm",
|
|
167
|
+
"gen_ai.embeddings": "embedding",
|
|
168
|
+
"gen_ai.content.prompt": "llm",
|
|
169
|
+
"gen_ai.content.completion": "llm"
|
|
170
|
+
};
|
|
171
|
+
function mapSpanType(spanName, attributes) {
|
|
172
|
+
for (const [prefix, type] of Object.entries(SPAN_TYPE_MAP)) {
|
|
173
|
+
if (spanName.startsWith(prefix)) return type;
|
|
174
|
+
}
|
|
175
|
+
if (attributes["tool.name"] || attributes["code.function"]) return "tool";
|
|
176
|
+
if (attributes["gen_ai.system"] || attributes["llm.vendor"]) return "llm";
|
|
177
|
+
if (attributes["db.system"]) return "database";
|
|
178
|
+
if (attributes["http.method"] || attributes["http.request.method"]) return "http";
|
|
179
|
+
return "span";
|
|
180
|
+
}
|
|
181
|
+
function extractAttributes(attrs) {
|
|
182
|
+
const result = {};
|
|
183
|
+
if (!Array.isArray(attrs)) return result;
|
|
184
|
+
for (const attr of attrs) {
|
|
185
|
+
const a = attr;
|
|
186
|
+
if (!a.key || !a.value) continue;
|
|
187
|
+
result[a.key] = a.value.stringValue ?? a.value.intValue ?? a.value.doubleValue ?? a.value.boolValue;
|
|
188
|
+
}
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
function parseOtlpPayload(payload) {
|
|
192
|
+
var _a, _b, _c;
|
|
193
|
+
const traceMap = /* @__PURE__ */ new Map();
|
|
194
|
+
for (const rs of payload.resourceSpans ?? []) {
|
|
195
|
+
const resourceAttrs = extractAttributes(((_a = rs.resource) == null ? void 0 : _a.attributes) ?? []);
|
|
196
|
+
for (const ss of rs.scopeSpans ?? []) {
|
|
197
|
+
for (const span of ss.spans ?? []) {
|
|
198
|
+
if (!span.traceId) continue;
|
|
199
|
+
let entry = traceMap.get(span.traceId);
|
|
200
|
+
if (!entry) {
|
|
201
|
+
entry = { spans: [], resource: resourceAttrs };
|
|
202
|
+
traceMap.set(span.traceId, entry);
|
|
203
|
+
}
|
|
204
|
+
entry.spans.push(span);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const traces = [];
|
|
209
|
+
for (const [traceId, { spans, resource }] of traceMap) {
|
|
210
|
+
const nodes = {};
|
|
211
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
212
|
+
let traceStart = Number.MAX_SAFE_INTEGER;
|
|
213
|
+
let traceEnd = 0;
|
|
214
|
+
let hasFailed = false;
|
|
215
|
+
for (const span of spans) {
|
|
216
|
+
const attrs = extractAttributes(span.attributes ?? []);
|
|
217
|
+
const startNs = span.startTimeUnixNano ? Number(BigInt(span.startTimeUnixNano) / 1000000n) : 0;
|
|
218
|
+
const endNs = span.endTimeUnixNano ? Number(BigInt(span.endTimeUnixNano) / 1000000n) : null;
|
|
219
|
+
const failed = ((_b = span.status) == null ? void 0 : _b.code) === 2;
|
|
220
|
+
if (failed) hasFailed = true;
|
|
221
|
+
if (startNs < traceStart) traceStart = startNs;
|
|
222
|
+
if (endNs && endNs > traceEnd) traceEnd = endNs;
|
|
223
|
+
nodes[span.spanId] = {
|
|
224
|
+
id: span.spanId,
|
|
225
|
+
type: mapSpanType(span.name, attrs),
|
|
226
|
+
name: span.name,
|
|
227
|
+
status: failed ? "failed" : endNs ? "completed" : "running",
|
|
228
|
+
startTime: startNs,
|
|
229
|
+
endTime: endNs,
|
|
230
|
+
parentId: span.parentSpanId ?? null,
|
|
231
|
+
children: [],
|
|
232
|
+
metadata: {
|
|
233
|
+
...attrs,
|
|
234
|
+
model: attrs["gen_ai.request.model"] ?? attrs["llm.request.model"],
|
|
235
|
+
inputTokens: attrs["gen_ai.usage.input_tokens"] ?? attrs["llm.usage.input_tokens"],
|
|
236
|
+
outputTokens: attrs["gen_ai.usage.output_tokens"] ?? attrs["llm.usage.output_tokens"]
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
if (span.parentSpanId) {
|
|
240
|
+
const siblings = childMap.get(span.parentSpanId) ?? [];
|
|
241
|
+
siblings.push(span.spanId);
|
|
242
|
+
childMap.set(span.parentSpanId, siblings);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (const [parentId, children] of childMap) {
|
|
246
|
+
if (nodes[parentId]) {
|
|
247
|
+
nodes[parentId].children = children;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const serviceName = resource["service.name"] ?? "unknown-service";
|
|
251
|
+
traces.push({
|
|
252
|
+
id: traceId,
|
|
253
|
+
agentId: `otel:${serviceName}`,
|
|
254
|
+
name: ((_c = spans.find((s) => !s.parentSpanId)) == null ? void 0 : _c.name) ?? traceId,
|
|
255
|
+
status: hasFailed ? "failed" : "completed",
|
|
256
|
+
startTime: traceStart === Number.MAX_SAFE_INTEGER ? 0 : traceStart,
|
|
257
|
+
endTime: traceEnd,
|
|
258
|
+
trigger: "otel",
|
|
259
|
+
source: "otel",
|
|
260
|
+
nodes,
|
|
261
|
+
metadata: { ...resource }
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return traces;
|
|
265
|
+
}
|
|
266
|
+
var OTelAdapter = class {
|
|
267
|
+
name = "otel";
|
|
268
|
+
detect(dirPath) {
|
|
269
|
+
try {
|
|
270
|
+
if (existsSync2(join2(dirPath, "otel-traces"))) return true;
|
|
271
|
+
const files = readdirSync(dirPath);
|
|
272
|
+
return files.some((f) => f.endsWith(".otlp.json"));
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
canHandle(filePath) {
|
|
278
|
+
return filePath.endsWith(".otlp.json");
|
|
279
|
+
}
|
|
280
|
+
parse(filePath) {
|
|
281
|
+
try {
|
|
282
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
283
|
+
const payload = JSON.parse(content);
|
|
284
|
+
const traces = parseOtlpPayload(payload);
|
|
285
|
+
for (const t of traces) t.filePath = filePath;
|
|
286
|
+
return traces;
|
|
287
|
+
} catch {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// src/adapters/registry.ts
|
|
294
|
+
var adapters = [];
|
|
295
|
+
function registerAdapter(adapter) {
|
|
296
|
+
adapters.push(adapter);
|
|
297
|
+
}
|
|
298
|
+
function findAdapter(filePath) {
|
|
299
|
+
for (const adapter of adapters) {
|
|
300
|
+
if (adapter.canHandle(filePath)) return adapter;
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/adapters/index.ts
|
|
306
|
+
registerAdapter(new OpenClawAdapter());
|
|
307
|
+
registerAdapter(new OTelAdapter());
|
|
308
|
+
registerAdapter(new AgentFlowAdapter());
|
|
309
|
+
|
|
310
|
+
// src/agent-clustering.ts
|
|
311
|
+
var PURPOSE_KEYWORDS = [
|
|
312
|
+
{ keywords: ["email", "mail", "inbox", "smtp"], group: "Email Processors" },
|
|
313
|
+
{ keywords: ["monitor", "watch", "alert", "surveillance"], group: "Monitors" },
|
|
314
|
+
{ keywords: ["digest", "newsletter", "summary", "report", "briefing"], group: "Digests & Reports" },
|
|
315
|
+
{ keywords: ["curator", "janitor", "distiller", "surveyor", "worker", "indexer"], group: "Workers" },
|
|
316
|
+
{ keywords: ["cron", "schedule", "timer", "periodic"], group: "Scheduled Jobs" },
|
|
317
|
+
{ keywords: ["search", "scrape", "crawl", "fetch"], group: "Data Collection" },
|
|
318
|
+
{ keywords: ["embed", "vector", "index"], group: "Embeddings" }
|
|
319
|
+
];
|
|
320
|
+
function extractSource(agentId) {
|
|
321
|
+
const colonIdx = agentId.indexOf(":");
|
|
322
|
+
if (colonIdx > 0 && colonIdx < 20) {
|
|
323
|
+
const prefix = agentId.slice(0, colonIdx);
|
|
324
|
+
if (["openclaw", "otel", "langchain", "crewai", "mastra"].includes(prefix)) {
|
|
325
|
+
return { source: prefix, localId: agentId.slice(colonIdx + 1) };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { source: "agentflow", localId: agentId };
|
|
329
|
+
}
|
|
330
|
+
function extractSuffix(localId) {
|
|
331
|
+
const dashIdx = localId.indexOf("-");
|
|
332
|
+
if (dashIdx > 0 && dashIdx < localId.length - 1) {
|
|
333
|
+
const suffix = localId.slice(dashIdx + 1);
|
|
334
|
+
if (suffix.length >= 4) return suffix;
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
function findPurpose(name) {
|
|
339
|
+
const lower = name.toLowerCase();
|
|
340
|
+
for (const { keywords, group } of PURPOSE_KEYWORDS) {
|
|
341
|
+
if (keywords.some((kw) => lower.includes(kw))) return group;
|
|
342
|
+
}
|
|
343
|
+
return "General";
|
|
344
|
+
}
|
|
345
|
+
function capitalize(s) {
|
|
346
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
347
|
+
}
|
|
348
|
+
function deduplicateAgents(agents) {
|
|
349
|
+
const tagged = agents.map((a) => ({
|
|
350
|
+
...a,
|
|
351
|
+
...extractSource(a.agentId)
|
|
352
|
+
}));
|
|
353
|
+
const suffixGroups = /* @__PURE__ */ new Map();
|
|
354
|
+
for (const a of tagged) {
|
|
355
|
+
const suffix = extractSuffix(a.localId);
|
|
356
|
+
if (!suffix) continue;
|
|
357
|
+
const group = suffixGroups.get(suffix) ?? [];
|
|
358
|
+
group.push(a);
|
|
359
|
+
suffixGroups.set(suffix, group);
|
|
360
|
+
}
|
|
361
|
+
const mergedIds = /* @__PURE__ */ new Set();
|
|
362
|
+
const mergedAgents = [];
|
|
363
|
+
for (const [suffix, group] of suffixGroups) {
|
|
364
|
+
if (group.length < 2) continue;
|
|
365
|
+
const prefixes = new Set(group.map((a) => a.localId.split("-")[0]));
|
|
366
|
+
if (prefixes.size < 2) continue;
|
|
367
|
+
const merged = {
|
|
368
|
+
agentId: group[0].source === "agentflow" ? suffix : `${group[0].source}:${suffix}`,
|
|
369
|
+
displayName: suffix,
|
|
370
|
+
totalExecutions: group.reduce((s, a) => s + a.totalExecutions, 0),
|
|
371
|
+
successfulExecutions: group.reduce((s, a) => s + a.successfulExecutions, 0),
|
|
372
|
+
failedExecutions: group.reduce((s, a) => s + a.failedExecutions, 0),
|
|
373
|
+
successRate: 0,
|
|
374
|
+
avgExecutionTime: 0,
|
|
375
|
+
lastExecution: Math.max(...group.map((a) => a.lastExecution)),
|
|
376
|
+
triggers: {},
|
|
377
|
+
recentActivity: group.flatMap((a) => a.recentActivity).sort((a, b) => b.timestamp - a.timestamp).slice(0, 50),
|
|
378
|
+
sources: group.map((a) => a.agentId),
|
|
379
|
+
adapterSource: group[0].source
|
|
380
|
+
};
|
|
381
|
+
merged.successRate = merged.totalExecutions > 0 ? merged.successfulExecutions / merged.totalExecutions * 100 : 0;
|
|
382
|
+
const totalExecTime = group.reduce((s, a) => s + a.avgExecutionTime * a.totalExecutions, 0);
|
|
383
|
+
merged.avgExecutionTime = merged.totalExecutions > 0 ? totalExecTime / merged.totalExecutions : 0;
|
|
384
|
+
for (const a of group) {
|
|
385
|
+
for (const [k, v] of Object.entries(a.triggers)) {
|
|
386
|
+
merged.triggers[k] = (merged.triggers[k] ?? 0) + v;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
mergedAgents.push(merged);
|
|
390
|
+
for (const a of group) mergedIds.add(a.agentId);
|
|
391
|
+
}
|
|
392
|
+
const result = [];
|
|
393
|
+
for (const a of agents) {
|
|
394
|
+
if (mergedIds.has(a.agentId)) continue;
|
|
395
|
+
const { source, localId } = extractSource(a.agentId);
|
|
396
|
+
result.push({
|
|
397
|
+
...a,
|
|
398
|
+
displayName: a.displayName ?? localId,
|
|
399
|
+
adapterSource: source
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return [...result, ...mergedAgents];
|
|
403
|
+
}
|
|
404
|
+
function groupAgents(agents) {
|
|
405
|
+
const sourceMap = /* @__PURE__ */ new Map();
|
|
406
|
+
for (const a of agents) {
|
|
407
|
+
const source = a.adapterSource ?? extractSource(a.agentId).source;
|
|
408
|
+
const list = sourceMap.get(source) ?? [];
|
|
409
|
+
list.push(a);
|
|
410
|
+
sourceMap.set(source, list);
|
|
411
|
+
}
|
|
412
|
+
const SOURCE_DISPLAY = {
|
|
413
|
+
agentflow: "AgentFlow",
|
|
414
|
+
openclaw: "OpenClaw",
|
|
415
|
+
otel: "OpenTelemetry",
|
|
416
|
+
langchain: "LangChain",
|
|
417
|
+
crewai: "CrewAI"
|
|
418
|
+
};
|
|
419
|
+
const groups = [];
|
|
420
|
+
for (const [source, sourceAgents] of sourceMap) {
|
|
421
|
+
const subMap = /* @__PURE__ */ new Map();
|
|
422
|
+
for (const a of sourceAgents) {
|
|
423
|
+
const purpose = findPurpose(a.displayName ?? a.agentId);
|
|
424
|
+
const list = subMap.get(purpose) ?? [];
|
|
425
|
+
list.push(a.agentId);
|
|
426
|
+
subMap.set(purpose, list);
|
|
427
|
+
}
|
|
428
|
+
const subGroups = [...subMap.entries()].map(([name, agentIds]) => ({ name, agentIds })).sort((a, b) => b.agentIds.length - a.agentIds.length);
|
|
429
|
+
groups.push({
|
|
430
|
+
name: source,
|
|
431
|
+
displayName: SOURCE_DISPLAY[source] ?? capitalize(source),
|
|
432
|
+
totalExecutions: sourceAgents.reduce((s, a) => s + a.totalExecutions, 0),
|
|
433
|
+
failedExecutions: sourceAgents.reduce((s, a) => s + a.failedExecutions, 0),
|
|
434
|
+
agents: sourceAgents.sort((a, b) => b.totalExecutions - a.totalExecutions),
|
|
435
|
+
subGroups
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
groups.sort((a, b) => b.totalExecutions - a.totalExecutions);
|
|
439
|
+
return { groups };
|
|
440
|
+
}
|
|
441
|
+
|
|
19
442
|
// src/stats.ts
|
|
20
443
|
import { getFailures, getHungNodes, getStats } from "agentflow-core";
|
|
21
444
|
var AgentStats = class {
|
|
@@ -340,7 +763,7 @@ function openClawSessionIdToAgent(sessionId) {
|
|
|
340
763
|
if (sessionId.startsWith("janitor-")) return "vault-janitor";
|
|
341
764
|
if (sessionId.startsWith("curator-")) return "vault-curator";
|
|
342
765
|
if (sessionId.startsWith("distiller-")) return "vault-distiller";
|
|
343
|
-
if (sessionId.startsWith("main-")) return "main";
|
|
766
|
+
if (sessionId.startsWith("main-")) return "alfred-main";
|
|
344
767
|
const firstSegment = sessionId.split("-")[0];
|
|
345
768
|
if (firstSegment) return firstSegment;
|
|
346
769
|
return "openclaw";
|
|
@@ -425,10 +848,14 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
425
848
|
"package-lock.json",
|
|
426
849
|
"tsconfig.json",
|
|
427
850
|
"biome.json",
|
|
428
|
-
"jobs.json",
|
|
429
851
|
"auth.json",
|
|
430
852
|
"models.json",
|
|
431
|
-
"config.json"
|
|
853
|
+
"config.json",
|
|
854
|
+
"runs.json",
|
|
855
|
+
"sessions.json",
|
|
856
|
+
"containers.json",
|
|
857
|
+
"update-check.json",
|
|
858
|
+
"exec-approvals.json"
|
|
432
859
|
]);
|
|
433
860
|
static SKIP_SUFFIXES = [
|
|
434
861
|
"-state.json",
|
|
@@ -438,11 +865,15 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
438
865
|
".bak",
|
|
439
866
|
".backup"
|
|
440
867
|
];
|
|
441
|
-
/** Load a
|
|
868
|
+
/** Load a file using the adapter registry, falling back to built-in parsing. */
|
|
442
869
|
loadFile(filePath) {
|
|
443
870
|
const filename = path.basename(filePath);
|
|
444
871
|
if (_TraceWatcher.SKIP_FILES.has(filename)) return false;
|
|
445
872
|
if (_TraceWatcher.SKIP_SUFFIXES.some((s) => filename.endsWith(s))) return false;
|
|
873
|
+
const adapter = findAdapter(filePath);
|
|
874
|
+
if (adapter && adapter.name !== "agentflow") {
|
|
875
|
+
return this.loadViaAdapter(filePath, adapter.name);
|
|
876
|
+
}
|
|
446
877
|
if (filePath.endsWith(".jsonl")) {
|
|
447
878
|
return this.loadSessionFile(filePath);
|
|
448
879
|
}
|
|
@@ -451,6 +882,57 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
451
882
|
}
|
|
452
883
|
return this.loadTraceFile(filePath);
|
|
453
884
|
}
|
|
885
|
+
/** Load a file using a specific adapter and store normalized traces. */
|
|
886
|
+
loadViaAdapter(filePath, adapterName) {
|
|
887
|
+
try {
|
|
888
|
+
const adapter = findAdapter(filePath);
|
|
889
|
+
if (!adapter) return false;
|
|
890
|
+
const normalized = adapter.parse(filePath);
|
|
891
|
+
if (normalized.length === 0) return false;
|
|
892
|
+
for (const trace of normalized) {
|
|
893
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
894
|
+
for (const [id, node] of Object.entries(trace.nodes)) {
|
|
895
|
+
nodes.set(id, {
|
|
896
|
+
id: node.id,
|
|
897
|
+
type: node.type,
|
|
898
|
+
name: node.name,
|
|
899
|
+
status: node.status,
|
|
900
|
+
startTime: node.startTime,
|
|
901
|
+
endTime: node.endTime,
|
|
902
|
+
parentId: node.parentId,
|
|
903
|
+
children: node.children,
|
|
904
|
+
metadata: node.metadata,
|
|
905
|
+
state: {}
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
const watched = {
|
|
909
|
+
id: trace.id,
|
|
910
|
+
rootNodeId: Object.keys(trace.nodes)[0] ?? "",
|
|
911
|
+
agentId: trace.agentId,
|
|
912
|
+
name: trace.name,
|
|
913
|
+
trigger: trace.trigger,
|
|
914
|
+
startTime: trace.startTime,
|
|
915
|
+
endTime: trace.endTime,
|
|
916
|
+
status: trace.status,
|
|
917
|
+
nodes,
|
|
918
|
+
edges: [],
|
|
919
|
+
events: [],
|
|
920
|
+
metadata: { ...trace.metadata, adapterSource: adapterName },
|
|
921
|
+
sessionEvents: trace.sessionEvents ?? [],
|
|
922
|
+
sourceType: "session",
|
|
923
|
+
filename: path.basename(filePath),
|
|
924
|
+
lastModified: Date.now(),
|
|
925
|
+
sourceDir: path.dirname(filePath)
|
|
926
|
+
};
|
|
927
|
+
const key = `${adapterName}:${trace.id}`;
|
|
928
|
+
this.traces.set(key, watched);
|
|
929
|
+
}
|
|
930
|
+
return true;
|
|
931
|
+
} catch (error) {
|
|
932
|
+
console.error(`Adapter ${adapterName} failed for ${filePath}:`, error);
|
|
933
|
+
return false;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
454
936
|
loadLogFile(filePath) {
|
|
455
937
|
try {
|
|
456
938
|
const content = fs.readFileSync(filePath, "utf8");
|
|
@@ -563,21 +1045,46 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
563
1045
|
}
|
|
564
1046
|
return traces;
|
|
565
1047
|
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Normalise agent identifiers so that the same worker is never shown
|
|
1050
|
+
* under two different names (e.g. "vault-curator" vs "openclaw-vault-curator").
|
|
1051
|
+
*
|
|
1052
|
+
* Canonical names: alfred-main, vault-curator, vault-janitor,
|
|
1053
|
+
* vault-distiller, vault-surveyor
|
|
1054
|
+
*/
|
|
1055
|
+
static AGENT_ALIASES = {
|
|
1056
|
+
"openclaw-main": "alfred-main",
|
|
1057
|
+
"openclaw-vault-curator": "vault-curator",
|
|
1058
|
+
"openclaw-vault-janitor": "vault-janitor",
|
|
1059
|
+
"openclaw-vault-distiller": "vault-distiller",
|
|
1060
|
+
"openclaw-vault-surveyor": "vault-surveyor",
|
|
1061
|
+
"alfred-curator": "vault-curator",
|
|
1062
|
+
"alfred-janitor": "vault-janitor",
|
|
1063
|
+
"alfred-distiller": "vault-distiller",
|
|
1064
|
+
"alfred-surveyor": "vault-surveyor",
|
|
1065
|
+
curator: "vault-curator",
|
|
1066
|
+
janitor: "vault-janitor",
|
|
1067
|
+
distiller: "vault-distiller",
|
|
1068
|
+
surveyor: "vault-surveyor"
|
|
1069
|
+
};
|
|
1070
|
+
normaliseAgentId(raw) {
|
|
1071
|
+
return _TraceWatcher.AGENT_ALIASES[raw] ?? raw;
|
|
1072
|
+
}
|
|
566
1073
|
detectAgentIdentifier(activity, _filename, filePath) {
|
|
567
1074
|
if (activity.agent_id) {
|
|
568
1075
|
const agentId = activity.agent_id;
|
|
569
|
-
if (agentId.
|
|
570
|
-
|
|
571
|
-
return agentId;
|
|
1076
|
+
if (agentId === "main" && filePath.includes(".alfred/")) return this.normaliseAgentId("alfred-main");
|
|
1077
|
+
return this.normaliseAgentId(agentId);
|
|
572
1078
|
}
|
|
573
1079
|
const pathAgent = this.extractAgentFromPath(filePath);
|
|
574
1080
|
if (filePath.includes(".alfred/") && !pathAgent.startsWith("alfred-")) {
|
|
575
|
-
const
|
|
576
|
-
if (
|
|
577
|
-
|
|
1081
|
+
const basename3 = path.basename(filePath, path.extname(filePath));
|
|
1082
|
+
if (basename3.match(/^(janitor|curator|distiller|surveyor|alfred)$/)) {
|
|
1083
|
+
const raw = basename3 === "alfred" ? "alfred" : `alfred-${basename3}`;
|
|
1084
|
+
return this.normaliseAgentId(raw);
|
|
578
1085
|
}
|
|
579
1086
|
}
|
|
580
|
-
return pathAgent;
|
|
1087
|
+
return this.normaliseAgentId(pathAgent);
|
|
581
1088
|
}
|
|
582
1089
|
extractAgentFromPath(filePath) {
|
|
583
1090
|
const filename = path.basename(filePath, path.extname(filePath));
|
|
@@ -1408,8 +1915,23 @@ var TraceWatcher = class _TraceWatcher extends EventEmitter {
|
|
|
1408
1915
|
getTrace(filename) {
|
|
1409
1916
|
const exact = this.traces.get(filename);
|
|
1410
1917
|
if (exact) return exact;
|
|
1918
|
+
if (filename.includes("::")) {
|
|
1919
|
+
const [fname, startTimeStr] = filename.split("::");
|
|
1920
|
+
const startTime = Number(startTimeStr);
|
|
1921
|
+
if (fname && !Number.isNaN(startTime)) {
|
|
1922
|
+
for (const trace of this.traces.values()) {
|
|
1923
|
+
if (trace.filename === fname && trace.startTime === startTime) {
|
|
1924
|
+
return trace;
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
for (const prefix of ["openclaw:", "otel:", ""]) {
|
|
1930
|
+
const prefixed = this.traces.get(prefix + filename);
|
|
1931
|
+
if (prefixed) return prefixed;
|
|
1932
|
+
}
|
|
1411
1933
|
for (const [key, trace] of this.traces) {
|
|
1412
|
-
if (trace.filename === filename || key.endsWith(filename)) {
|
|
1934
|
+
if (trace.filename === filename || trace.id === filename || key.endsWith(filename)) {
|
|
1413
1935
|
return trace;
|
|
1414
1936
|
}
|
|
1415
1937
|
}
|
|
@@ -1539,11 +2061,23 @@ async function startDashboard() {
|
|
|
1539
2061
|
case "--cors":
|
|
1540
2062
|
config.enableCors = true;
|
|
1541
2063
|
break;
|
|
2064
|
+
case "--no-collector":
|
|
2065
|
+
config.enableCollector = false;
|
|
2066
|
+
break;
|
|
2067
|
+
case "--collector-token":
|
|
2068
|
+
config.collectorAuthToken = args[++i];
|
|
2069
|
+
break;
|
|
1542
2070
|
case "--help":
|
|
1543
2071
|
printHelp();
|
|
1544
2072
|
process.exit(0);
|
|
1545
2073
|
}
|
|
1546
2074
|
}
|
|
2075
|
+
if (!config.collectorAuthToken && process.env.AGENTFLOW_COLLECTOR_TOKEN) {
|
|
2076
|
+
config.collectorAuthToken = process.env.AGENTFLOW_COLLECTOR_TOKEN;
|
|
2077
|
+
}
|
|
2078
|
+
if (process.env.AGENTFLOW_NO_COLLECTOR === "true") {
|
|
2079
|
+
config.enableCollector = false;
|
|
2080
|
+
}
|
|
1547
2081
|
const tracesPath = path2.resolve(config.tracesDir);
|
|
1548
2082
|
if (!fs2.existsSync(tracesPath)) {
|
|
1549
2083
|
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
@@ -1586,6 +2120,8 @@ Options:
|
|
|
1586
2120
|
-h, --host <address> Host address (default: localhost)
|
|
1587
2121
|
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
1588
2122
|
--cors Enable CORS headers
|
|
2123
|
+
--no-collector Disable OTLP trace collector (POST /v1/traces)
|
|
2124
|
+
--collector-token <tok> Require auth token for collector (or set AGENTFLOW_COLLECTOR_TOKEN)
|
|
1589
2125
|
--help Show this help message
|
|
1590
2126
|
|
|
1591
2127
|
Examples:
|
|
@@ -1620,6 +2156,31 @@ function serializeTrace(trace) {
|
|
|
1620
2156
|
var DashboardServer = class {
|
|
1621
2157
|
constructor(config) {
|
|
1622
2158
|
this.config = config;
|
|
2159
|
+
const home = process.env.HOME ?? "/home/trader";
|
|
2160
|
+
const configPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2161
|
+
if (!config.dataDirs) config.dataDirs = [];
|
|
2162
|
+
try {
|
|
2163
|
+
if (fs3.existsSync(configPath)) {
|
|
2164
|
+
const saved = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
2165
|
+
const extraDirs = saved.extraDirs ?? [];
|
|
2166
|
+
for (const d of extraDirs) {
|
|
2167
|
+
if (!config.dataDirs.includes(d)) config.dataDirs.push(d);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
} catch {
|
|
2171
|
+
}
|
|
2172
|
+
const autoDiscoverPaths = [
|
|
2173
|
+
path3.join(home, ".openclaw/cron/runs"),
|
|
2174
|
+
path3.join(home, ".openclaw/workspace/traces"),
|
|
2175
|
+
path3.join(home, ".openclaw/subagents"),
|
|
2176
|
+
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2177
|
+
path3.join(home, ".agentflow/traces")
|
|
2178
|
+
];
|
|
2179
|
+
for (const p of autoDiscoverPaths) {
|
|
2180
|
+
if (fs3.existsSync(p) && !config.dataDirs.includes(p)) {
|
|
2181
|
+
config.dataDirs.push(p);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
1623
2184
|
this.watcher = new TraceWatcher({
|
|
1624
2185
|
tracesDir: config.tracesDir,
|
|
1625
2186
|
dataDirs: config.dataDirs
|
|
@@ -1668,9 +2229,13 @@ var DashboardServer = class {
|
|
|
1668
2229
|
next();
|
|
1669
2230
|
});
|
|
1670
2231
|
}
|
|
2232
|
+
const clientDir = path3.join(__dirname, "../dist/client");
|
|
2233
|
+
if (fs3.existsSync(clientDir)) {
|
|
2234
|
+
this.app.use(express.static(clientDir));
|
|
2235
|
+
}
|
|
1671
2236
|
const publicDir = path3.join(__dirname, "../public");
|
|
1672
2237
|
if (fs3.existsSync(publicDir)) {
|
|
1673
|
-
this.app.use(express.static(publicDir));
|
|
2238
|
+
this.app.use("/v1", express.static(publicDir));
|
|
1674
2239
|
}
|
|
1675
2240
|
this.app.get("/api/traces", (_req, res) => {
|
|
1676
2241
|
try {
|
|
@@ -1706,10 +2271,28 @@ var DashboardServer = class {
|
|
|
1706
2271
|
res.status(500).json({ error: "Failed to load trace events" });
|
|
1707
2272
|
}
|
|
1708
2273
|
});
|
|
1709
|
-
this.app.get("/api/agents", (
|
|
2274
|
+
this.app.get("/api/agents", (req, res) => {
|
|
1710
2275
|
try {
|
|
1711
|
-
const
|
|
1712
|
-
|
|
2276
|
+
const raw = this.stats.getAgentsList();
|
|
2277
|
+
for (const agent of raw) {
|
|
2278
|
+
if (!agent.displayName) {
|
|
2279
|
+
const traces = this.watcher.getTracesByAgent(agent.agentId);
|
|
2280
|
+
if (traces.length > 0) {
|
|
2281
|
+
const latest = traces[traces.length - 1];
|
|
2282
|
+
const name = latest == null ? void 0 : latest.name;
|
|
2283
|
+
if (name && name !== "default" && name !== agent.agentId && !name.startsWith("pipeline:") && name.length < 40) {
|
|
2284
|
+
agent.displayName = name;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
if (!agent.displayName) agent.displayName = agent.agentId;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (req.query.flat === "true") {
|
|
2291
|
+
return res.json(raw);
|
|
2292
|
+
}
|
|
2293
|
+
const deduped = deduplicateAgents(raw);
|
|
2294
|
+
const grouped = groupAgents(deduped);
|
|
2295
|
+
res.json(grouped);
|
|
1713
2296
|
} catch (_error) {
|
|
1714
2297
|
res.status(500).json({ error: "Failed to load agents" });
|
|
1715
2298
|
}
|
|
@@ -1859,6 +2442,84 @@ var DashboardServer = class {
|
|
|
1859
2442
|
res.status(500).json({ error: "Failed to load agent profile" });
|
|
1860
2443
|
}
|
|
1861
2444
|
});
|
|
2445
|
+
this.app.get("/api/process-model/:agentId", (req, res) => {
|
|
2446
|
+
try {
|
|
2447
|
+
const agentId = req.params.agentId;
|
|
2448
|
+
const allTraces = this.watcher.getTracesByAgent(agentId);
|
|
2449
|
+
if (allTraces.length === 0) {
|
|
2450
|
+
return res.status(404).json({ error: "No traces for agent" });
|
|
2451
|
+
}
|
|
2452
|
+
const transMap = /* @__PURE__ */ new Map();
|
|
2453
|
+
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
2454
|
+
const variantMap = /* @__PURE__ */ new Map();
|
|
2455
|
+
const durationMap = /* @__PURE__ */ new Map();
|
|
2456
|
+
for (const trace of allTraces) {
|
|
2457
|
+
const serialized = serializeTrace(trace);
|
|
2458
|
+
const nodes = serialized.nodes;
|
|
2459
|
+
if (!nodes || typeof nodes !== "object") continue;
|
|
2460
|
+
const nodeArr = Object.values(nodes);
|
|
2461
|
+
const sorted = nodeArr.filter((n) => n.name && typeof n.startTime === "number" && n.startTime > 0).sort((a, b) => (a.startTime ?? 0) - (b.startTime ?? 0));
|
|
2462
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
2463
|
+
const from = sorted[i].name;
|
|
2464
|
+
const to = sorted[i + 1].name;
|
|
2465
|
+
const key = `${from}|||${to}`;
|
|
2466
|
+
transMap.set(key, (transMap.get(key) ?? 0) + 1);
|
|
2467
|
+
}
|
|
2468
|
+
for (const n of sorted) {
|
|
2469
|
+
if (n.name && n.type) nodeTypeMap.set(n.name, n.type);
|
|
2470
|
+
}
|
|
2471
|
+
const sig = sorted.map((n) => n.name).join("\u2192");
|
|
2472
|
+
if (sig) variantMap.set(sig, (variantMap.get(sig) ?? 0) + 1);
|
|
2473
|
+
for (const n of sorted) {
|
|
2474
|
+
if (n.name && n.endTime && n.startTime) {
|
|
2475
|
+
const dur = n.endTime - n.startTime;
|
|
2476
|
+
if (dur > 0) {
|
|
2477
|
+
const arr = durationMap.get(n.name) ?? [];
|
|
2478
|
+
arr.push(dur);
|
|
2479
|
+
durationMap.set(n.name, arr);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
const model = {
|
|
2485
|
+
transitions: [...transMap.entries()].map(([key, count]) => {
|
|
2486
|
+
const [from, to] = key.split("|||");
|
|
2487
|
+
return { from, to, count };
|
|
2488
|
+
}),
|
|
2489
|
+
nodeTypes: Object.fromEntries(nodeTypeMap)
|
|
2490
|
+
};
|
|
2491
|
+
const totalTraces = allTraces.length;
|
|
2492
|
+
const variants = [...variantMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([sig, count]) => ({
|
|
2493
|
+
pathSignature: sig,
|
|
2494
|
+
count,
|
|
2495
|
+
percentage: totalTraces > 0 ? count / totalTraces * 100 : 0
|
|
2496
|
+
}));
|
|
2497
|
+
const bottlenecks = [...durationMap.entries()].map(([name, durations]) => {
|
|
2498
|
+
const sorted = durations.sort((a, b) => a - b);
|
|
2499
|
+
const p95 = sorted[Math.floor(sorted.length * 0.95)] ?? 0;
|
|
2500
|
+
return { nodeName: name, nodeType: nodeTypeMap.get(name) ?? "unknown", p95 };
|
|
2501
|
+
}).sort((a, b) => b.p95 - a.p95).slice(0, 15);
|
|
2502
|
+
const graphs = this.getGraphTraces(agentId);
|
|
2503
|
+
if (graphs.length > 0) {
|
|
2504
|
+
try {
|
|
2505
|
+
const coreBottlenecks = getBottlenecks(graphs).map((b) => ({
|
|
2506
|
+
nodeName: b.nodeName,
|
|
2507
|
+
nodeType: b.nodeType,
|
|
2508
|
+
p95: b.durations.sort((a, b2) => a - b2)[Math.floor(b.durations.length * 0.95)] ?? 0
|
|
2509
|
+
}));
|
|
2510
|
+
if (coreBottlenecks.length > bottlenecks.length) {
|
|
2511
|
+
bottlenecks.length = 0;
|
|
2512
|
+
bottlenecks.push(...coreBottlenecks);
|
|
2513
|
+
}
|
|
2514
|
+
} catch {
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
res.json({ model, variants, bottlenecks });
|
|
2518
|
+
} catch (error) {
|
|
2519
|
+
console.error("Process model error:", error);
|
|
2520
|
+
res.status(500).json({ error: "Failed to compute process model" });
|
|
2521
|
+
}
|
|
2522
|
+
});
|
|
1862
2523
|
this.app.get("/api/stats/:agentId", (req, res) => {
|
|
1863
2524
|
try {
|
|
1864
2525
|
const agentStats = this.stats.getAgentStats(req.params.agentId);
|
|
@@ -1871,6 +2532,7 @@ var DashboardServer = class {
|
|
|
1871
2532
|
}
|
|
1872
2533
|
});
|
|
1873
2534
|
this.app.get("/api/process-health", (_req, res) => {
|
|
2535
|
+
var _a, _b;
|
|
1874
2536
|
try {
|
|
1875
2537
|
const now = Date.now();
|
|
1876
2538
|
if (this.processHealthCache.result && now - this.processHealthCache.ts < 1e4) {
|
|
@@ -1881,67 +2543,211 @@ var DashboardServer = class {
|
|
|
1881
2543
|
path3.dirname(this.config.tracesDir),
|
|
1882
2544
|
...this.config.dataDirs || []
|
|
1883
2545
|
];
|
|
1884
|
-
const
|
|
1885
|
-
if (
|
|
2546
|
+
const configs = discoverAllProcessConfigs(discoveryDirs);
|
|
2547
|
+
if (configs.length === 0) {
|
|
1886
2548
|
return res.json(null);
|
|
1887
2549
|
}
|
|
1888
|
-
const
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
const
|
|
1904
|
-
|
|
1905
|
-
...openclawResult.osProcesses,
|
|
1906
|
-
...clawmetryResult.osProcesses
|
|
1907
|
-
];
|
|
2550
|
+
const services = [];
|
|
2551
|
+
const allKnownPids = /* @__PURE__ */ new Set();
|
|
2552
|
+
for (const config of configs) {
|
|
2553
|
+
const audit = auditProcesses(config);
|
|
2554
|
+
services.push({ name: config.processName, audit });
|
|
2555
|
+
if (((_a = audit.pidFile) == null ? void 0 : _a.pid) && !audit.pidFile.stale) allKnownPids.add(audit.pidFile.pid);
|
|
2556
|
+
if ((_b = audit.systemd) == null ? void 0 : _b.mainPid) allKnownPids.add(audit.systemd.mainPid);
|
|
2557
|
+
if (audit.workers) {
|
|
2558
|
+
if (audit.workers.orchestratorPid) allKnownPids.add(audit.workers.orchestratorPid);
|
|
2559
|
+
for (const w of audit.workers.workers) {
|
|
2560
|
+
if (w.pid) allKnownPids.add(w.pid);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
for (const p of audit.osProcesses) allKnownPids.add(p.pid);
|
|
2564
|
+
}
|
|
2565
|
+
const primary = services.find((s) => s.audit.pidFile) ?? services[0];
|
|
2566
|
+
const allOsProcesses = services.flatMap((s) => s.audit.osProcesses);
|
|
1908
2567
|
const uniqueProcesses = allOsProcesses.filter(
|
|
1909
2568
|
(proc, index, arr) => arr.findIndex((p) => p.pid === proc.pid) === index
|
|
1910
2569
|
);
|
|
2570
|
+
const orphans = uniqueProcesses.filter(
|
|
2571
|
+
(p) => !allKnownPids.has(p.pid) && p.pid !== process.pid && p.pid !== process.ppid
|
|
2572
|
+
);
|
|
2573
|
+
const problems = services.flatMap(
|
|
2574
|
+
(s) => s.audit.problems.map((p) => `[${s.name}] ${p}`)
|
|
2575
|
+
);
|
|
1911
2576
|
const result = {
|
|
1912
|
-
|
|
2577
|
+
// Backward-compatible fields from primary service
|
|
2578
|
+
pidFile: (primary == null ? void 0 : primary.audit.pidFile) ?? null,
|
|
2579
|
+
systemd: (primary == null ? void 0 : primary.audit.systemd) ?? null,
|
|
2580
|
+
workers: (primary == null ? void 0 : primary.audit.workers) ?? null,
|
|
1913
2581
|
osProcesses: uniqueProcesses,
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2582
|
+
orphans,
|
|
2583
|
+
problems,
|
|
2584
|
+
// All discovered services with their individual audit results + metrics
|
|
2585
|
+
services: services.map((s) => {
|
|
2586
|
+
var _a2, _b2;
|
|
2587
|
+
const mainPid = ((_a2 = s.audit.pidFile) == null ? void 0 : _a2.pid) ?? ((_b2 = s.audit.systemd) == null ? void 0 : _b2.mainPid);
|
|
2588
|
+
const osProc = mainPid ? uniqueProcesses.find((p) => p.pid === mainPid) : void 0;
|
|
2589
|
+
return {
|
|
2590
|
+
name: s.name,
|
|
2591
|
+
pidFile: s.audit.pidFile,
|
|
2592
|
+
systemd: s.audit.systemd,
|
|
2593
|
+
workers: s.audit.workers,
|
|
2594
|
+
problems: s.audit.problems,
|
|
2595
|
+
metrics: osProc ? { cpu: osProc.cpu, mem: osProc.mem, elapsed: osProc.elapsed } : void 0
|
|
2596
|
+
};
|
|
2597
|
+
}),
|
|
2598
|
+
// Topology edges: parent-child relationships from process ppid
|
|
2599
|
+
topology: uniqueProcesses.map((p) => {
|
|
2600
|
+
try {
|
|
2601
|
+
const statusContent = fs3.readFileSync(`/proc/${p.pid}/status`, "utf8");
|
|
2602
|
+
const ppidMatch = statusContent.match(/^PPid:\s+(\d+)/m);
|
|
2603
|
+
const ppid = ppidMatch ? parseInt(ppidMatch[1] ?? "0", 10) : 0;
|
|
2604
|
+
if (ppid > 1 && allKnownPids.has(ppid)) {
|
|
2605
|
+
return { source: ppid, target: p.pid };
|
|
1925
2606
|
}
|
|
2607
|
+
} catch {
|
|
1926
2608
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
})
|
|
2609
|
+
return null;
|
|
2610
|
+
}).filter(Boolean)
|
|
1930
2611
|
};
|
|
1931
|
-
const openclawProblems = [];
|
|
1932
|
-
if (openclawResult.osProcesses.length === 0) {
|
|
1933
|
-
openclawProblems.push("No OpenClaw gateway processes detected");
|
|
1934
|
-
}
|
|
1935
|
-
if (clawmetryResult.osProcesses.length === 0) {
|
|
1936
|
-
openclawProblems.push("No clawmetry processes detected");
|
|
1937
|
-
}
|
|
1938
|
-
result.problems = [...alfredResult.problems || [], ...openclawProblems];
|
|
1939
2612
|
this.processHealthCache = { result, ts: now };
|
|
1940
2613
|
res.json(result);
|
|
1941
2614
|
} catch (_error) {
|
|
1942
2615
|
res.status(500).json({ error: "Failed to audit processes" });
|
|
1943
2616
|
}
|
|
1944
2617
|
});
|
|
2618
|
+
this.app.get("/api/directories", (_req, res) => {
|
|
2619
|
+
try {
|
|
2620
|
+
const home = process.env.HOME ?? "/home/trader";
|
|
2621
|
+
const configPath = path3.join(home, ".agentflow/dashboard-config.json");
|
|
2622
|
+
let extraDirs = [];
|
|
2623
|
+
try {
|
|
2624
|
+
if (fs3.existsSync(configPath)) {
|
|
2625
|
+
const cfg = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
2626
|
+
extraDirs = cfg.extraDirs ?? [];
|
|
2627
|
+
}
|
|
2628
|
+
} catch {
|
|
2629
|
+
}
|
|
2630
|
+
const watched = [
|
|
2631
|
+
this.config.tracesDir,
|
|
2632
|
+
...this.config.dataDirs || [],
|
|
2633
|
+
...extraDirs
|
|
2634
|
+
];
|
|
2635
|
+
const discovered = [];
|
|
2636
|
+
try {
|
|
2637
|
+
const { execSync } = __require("child_process");
|
|
2638
|
+
const raw = execSync(
|
|
2639
|
+
"systemctl --user show --property=ExecStart --no-pager alfred.service openclaw-gateway.service 2>/dev/null",
|
|
2640
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
2641
|
+
);
|
|
2642
|
+
for (const line of raw.split("\n")) {
|
|
2643
|
+
const match = line.match(/path=([^\s;]+)/);
|
|
2644
|
+
if (match == null ? void 0 : match[1]) {
|
|
2645
|
+
const dir = path3.dirname(match[1]);
|
|
2646
|
+
if (fs3.existsSync(dir)) discovered.push(dir);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
} catch {
|
|
2650
|
+
}
|
|
2651
|
+
const commonPaths = [
|
|
2652
|
+
path3.join(home, ".alfred/traces"),
|
|
2653
|
+
path3.join(home, ".alfred/data"),
|
|
2654
|
+
path3.join(home, ".openclaw/workspace/traces"),
|
|
2655
|
+
path3.join(home, ".openclaw/subagents"),
|
|
2656
|
+
path3.join(home, ".openclaw/cron/runs"),
|
|
2657
|
+
path3.join(home, ".openclaw/cron"),
|
|
2658
|
+
path3.join(home, ".openclaw/agents/main/sessions"),
|
|
2659
|
+
path3.join(home, ".agentflow/traces")
|
|
2660
|
+
];
|
|
2661
|
+
for (const p of commonPaths) {
|
|
2662
|
+
if (fs3.existsSync(p) && !discovered.includes(p)) {
|
|
2663
|
+
discovered.push(p);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
const watchedSet = new Set(watched.map((w) => path3.resolve(w)));
|
|
2667
|
+
const suggested = discovered.filter((d) => !watchedSet.has(path3.resolve(d)));
|
|
2668
|
+
res.json({ watched, discovered, suggested });
|
|
2669
|
+
} catch (error) {
|
|
2670
|
+
console.error("Directory discovery error:", error);
|
|
2671
|
+
res.status(500).json({ error: "Failed to discover directories" });
|
|
2672
|
+
}
|
|
2673
|
+
});
|
|
2674
|
+
this.app.post("/api/directories", express.json(), (req, res) => {
|
|
2675
|
+
try {
|
|
2676
|
+
const { add, remove } = req.body;
|
|
2677
|
+
if (add && !fs3.existsSync(add)) {
|
|
2678
|
+
return res.status(400).json({ error: `Directory does not exist: ${add}` });
|
|
2679
|
+
}
|
|
2680
|
+
const configPath = path3.join(process.env.HOME ?? "/home/trader", ".agentflow/dashboard-config.json");
|
|
2681
|
+
let config = {};
|
|
2682
|
+
try {
|
|
2683
|
+
if (fs3.existsSync(configPath)) {
|
|
2684
|
+
config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
|
2685
|
+
}
|
|
2686
|
+
} catch {
|
|
2687
|
+
}
|
|
2688
|
+
if (!config.extraDirs) config.extraDirs = [];
|
|
2689
|
+
if (add && !config.extraDirs.includes(add)) {
|
|
2690
|
+
config.extraDirs.push(add);
|
|
2691
|
+
}
|
|
2692
|
+
if (remove) {
|
|
2693
|
+
config.extraDirs = config.extraDirs.filter((d) => d !== remove);
|
|
2694
|
+
}
|
|
2695
|
+
fs3.mkdirSync(path3.dirname(configPath), { recursive: true });
|
|
2696
|
+
fs3.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
2697
|
+
res.json({ ok: true, extraDirs: config.extraDirs });
|
|
2698
|
+
} catch (error) {
|
|
2699
|
+
console.error("Directory config error:", error);
|
|
2700
|
+
res.status(500).json({ error: "Failed to update directory config" });
|
|
2701
|
+
}
|
|
2702
|
+
});
|
|
2703
|
+
if (this.config.enableCollector !== false) {
|
|
2704
|
+
this.app.post("/v1/traces", express.json({ limit: "10mb" }), (req, res) => {
|
|
2705
|
+
try {
|
|
2706
|
+
if (this.config.collectorAuthToken) {
|
|
2707
|
+
const auth = req.headers.authorization;
|
|
2708
|
+
if (!auth || auth !== `Bearer ${this.config.collectorAuthToken}`) {
|
|
2709
|
+
return res.status(401).json({ error: "Unauthorized \u2014 provide Authorization: Bearer <token>" });
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
const traces = parseOtlpPayload(req.body);
|
|
2713
|
+
let ingested = 0;
|
|
2714
|
+
for (const trace of traces) {
|
|
2715
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
2716
|
+
for (const [id, node] of Object.entries(trace.nodes)) {
|
|
2717
|
+
nodes.set(id, { ...node, state: {} });
|
|
2718
|
+
}
|
|
2719
|
+
const watched = {
|
|
2720
|
+
id: trace.id,
|
|
2721
|
+
rootNodeId: Object.keys(trace.nodes)[0] ?? "",
|
|
2722
|
+
agentId: trace.agentId,
|
|
2723
|
+
name: trace.name,
|
|
2724
|
+
trigger: trace.trigger,
|
|
2725
|
+
startTime: trace.startTime,
|
|
2726
|
+
endTime: trace.endTime,
|
|
2727
|
+
status: trace.status,
|
|
2728
|
+
nodes,
|
|
2729
|
+
edges: [],
|
|
2730
|
+
events: [],
|
|
2731
|
+
metadata: { ...trace.metadata, adapterSource: "otel" },
|
|
2732
|
+
sessionEvents: [],
|
|
2733
|
+
sourceType: "session",
|
|
2734
|
+
filename: `otel-${trace.id}`,
|
|
2735
|
+
lastModified: Date.now(),
|
|
2736
|
+
sourceDir: "http-collector"
|
|
2737
|
+
};
|
|
2738
|
+
this.watcher.traces.set(`otel:${trace.id}`, watched);
|
|
2739
|
+
ingested++;
|
|
2740
|
+
}
|
|
2741
|
+
if (ingested > 0) {
|
|
2742
|
+
this.broadcast({ type: "traces-updated", count: ingested });
|
|
2743
|
+
}
|
|
2744
|
+
res.json({ ok: true, tracesIngested: ingested });
|
|
2745
|
+
} catch (error) {
|
|
2746
|
+
console.error("OTLP collector error:", error);
|
|
2747
|
+
res.status(400).json({ error: "Failed to parse OTLP payload" });
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
1945
2751
|
this.app.get("/health", (_req, res) => {
|
|
1946
2752
|
res.json({
|
|
1947
2753
|
status: "ok",
|
|
@@ -1953,10 +2759,18 @@ var DashboardServer = class {
|
|
|
1953
2759
|
this.app.get("/ready", (_req, res) => {
|
|
1954
2760
|
res.json({ status: "ready" });
|
|
1955
2761
|
});
|
|
2762
|
+
this.app.get("/v1/*", (_req, res) => {
|
|
2763
|
+
const legacyIndex = path3.join(__dirname, "../public/index.html");
|
|
2764
|
+
if (fs3.existsSync(legacyIndex)) {
|
|
2765
|
+
res.sendFile(legacyIndex);
|
|
2766
|
+
} else {
|
|
2767
|
+
res.status(404).send("Legacy dashboard not found");
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
1956
2770
|
this.app.get("*", (_req, res) => {
|
|
1957
|
-
const
|
|
1958
|
-
if (fs3.existsSync(
|
|
1959
|
-
res.sendFile(
|
|
2771
|
+
const clientIndex = path3.join(__dirname, "../dist/client/index.html");
|
|
2772
|
+
if (fs3.existsSync(clientIndex)) {
|
|
2773
|
+
res.sendFile(clientIndex);
|
|
1960
2774
|
} else {
|
|
1961
2775
|
res.status(404).send("Dashboard not found - public files may not be built");
|
|
1962
2776
|
}
|
|
@@ -2203,21 +3017,21 @@ var DashboardServer = class {
|
|
|
2203
3017
|
});
|
|
2204
3018
|
}
|
|
2205
3019
|
async start() {
|
|
2206
|
-
return new Promise((
|
|
3020
|
+
return new Promise((resolve4) => {
|
|
2207
3021
|
const host = this.config.host || "localhost";
|
|
2208
3022
|
this.server.listen(this.config.port, host, () => {
|
|
2209
3023
|
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
2210
3024
|
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
2211
|
-
|
|
3025
|
+
resolve4();
|
|
2212
3026
|
});
|
|
2213
3027
|
});
|
|
2214
3028
|
}
|
|
2215
3029
|
async stop() {
|
|
2216
|
-
return new Promise((
|
|
3030
|
+
return new Promise((resolve4) => {
|
|
2217
3031
|
this.watcher.stop();
|
|
2218
3032
|
this.server.close(() => {
|
|
2219
3033
|
console.log("Dashboard server stopped");
|
|
2220
|
-
|
|
3034
|
+
resolve4();
|
|
2221
3035
|
});
|
|
2222
3036
|
});
|
|
2223
3037
|
}
|