agentflow-dashboard 0.3.0 → 0.3.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 +24 -7
- package/dist/{chunk-2FTN742J.js → chunk-EDHK4NJD.js} +168 -17
- package/dist/cli.cjs +4 -1
- package/dist/cli.js +2 -147
- package/dist/index.cjs +160 -12
- package/dist/index.js +1 -1
- package/dist/public/dashboard.js +401 -25
- package/dist/public/debug.html +43 -0
- package/dist/public/index.html +214 -0
- package/dist/server.cjs +1350 -0
- package/dist/server.js +6 -0
- package/package.json +2 -2
- package/public/dashboard.js +401 -25
- package/public/debug.html +43 -0
- package/public/index.html +214 -0
package/dist/server.cjs
ADDED
|
@@ -0,0 +1,1350 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/server.ts
|
|
30
|
+
var server_exports = {};
|
|
31
|
+
__export(server_exports, {
|
|
32
|
+
DashboardServer: () => DashboardServer
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(server_exports);
|
|
35
|
+
var import_express = __toESM(require("express"), 1);
|
|
36
|
+
var fs3 = __toESM(require("fs"), 1);
|
|
37
|
+
var import_http = require("http");
|
|
38
|
+
var path3 = __toESM(require("path"), 1);
|
|
39
|
+
var import_url = require("url");
|
|
40
|
+
var import_ws = require("ws");
|
|
41
|
+
var import_agentflow_core3 = require("agentflow-core");
|
|
42
|
+
|
|
43
|
+
// src/stats.ts
|
|
44
|
+
var import_agentflow_core = require("agentflow-core");
|
|
45
|
+
var AgentStats = class {
|
|
46
|
+
agentMetrics = /* @__PURE__ */ new Map();
|
|
47
|
+
processedTraces = /* @__PURE__ */ new Set();
|
|
48
|
+
processTrace(trace) {
|
|
49
|
+
const traceKey = `${trace.filename || trace.agentId}-${trace.startTime}`;
|
|
50
|
+
if (this.processedTraces.has(traceKey)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.processedTraces.add(traceKey);
|
|
54
|
+
const agentId = trace.agentId;
|
|
55
|
+
let metrics = this.agentMetrics.get(agentId);
|
|
56
|
+
if (!metrics) {
|
|
57
|
+
metrics = {
|
|
58
|
+
agentId,
|
|
59
|
+
totalExecutions: 0,
|
|
60
|
+
successfulExecutions: 0,
|
|
61
|
+
failedExecutions: 0,
|
|
62
|
+
successRate: 0,
|
|
63
|
+
avgExecutionTime: 0,
|
|
64
|
+
lastExecution: 0,
|
|
65
|
+
triggers: {},
|
|
66
|
+
recentActivity: []
|
|
67
|
+
};
|
|
68
|
+
this.agentMetrics.set(agentId, metrics);
|
|
69
|
+
}
|
|
70
|
+
const analysis = this.analyzeExecution(trace);
|
|
71
|
+
metrics.totalExecutions++;
|
|
72
|
+
metrics.lastExecution = Math.max(metrics.lastExecution, trace.startTime);
|
|
73
|
+
const trigger = trace.trigger || "unknown";
|
|
74
|
+
metrics.triggers[trigger] = (metrics.triggers[trigger] || 0) + 1;
|
|
75
|
+
if (analysis.success) {
|
|
76
|
+
metrics.successfulExecutions++;
|
|
77
|
+
} else {
|
|
78
|
+
metrics.failedExecutions++;
|
|
79
|
+
}
|
|
80
|
+
metrics.successRate = metrics.successfulExecutions / metrics.totalExecutions * 100;
|
|
81
|
+
if (analysis.executionTime > 0) {
|
|
82
|
+
const currentAvg = metrics.avgExecutionTime;
|
|
83
|
+
const count = metrics.totalExecutions;
|
|
84
|
+
metrics.avgExecutionTime = (currentAvg * (count - 1) + analysis.executionTime) / count;
|
|
85
|
+
}
|
|
86
|
+
metrics.recentActivity.push({
|
|
87
|
+
timestamp: trace.startTime,
|
|
88
|
+
success: analysis.success,
|
|
89
|
+
executionTime: analysis.executionTime,
|
|
90
|
+
trigger
|
|
91
|
+
});
|
|
92
|
+
if (metrics.recentActivity.length > 100) {
|
|
93
|
+
metrics.recentActivity = metrics.recentActivity.slice(-100);
|
|
94
|
+
}
|
|
95
|
+
metrics.recentActivity.sort((a, b) => b.timestamp - a.timestamp);
|
|
96
|
+
}
|
|
97
|
+
analyzeExecution(trace) {
|
|
98
|
+
try {
|
|
99
|
+
const stats = (0, import_agentflow_core.getStats)(trace);
|
|
100
|
+
const failures = (0, import_agentflow_core.getFailures)(trace);
|
|
101
|
+
const hungNodes = (0, import_agentflow_core.getHungNodes)(trace);
|
|
102
|
+
return {
|
|
103
|
+
success: failures.length === 0 && hungNodes.length === 0,
|
|
104
|
+
executionTime: stats.duration || 0,
|
|
105
|
+
nodeCount: stats.totalNodes || 0,
|
|
106
|
+
failureCount: failures.length,
|
|
107
|
+
hungCount: hungNodes.length
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.warn("Error analyzing trace with AgentFlow:", error);
|
|
111
|
+
const nodes = trace.nodes instanceof Map ? Array.from(trace.nodes.values()) : [];
|
|
112
|
+
const failedNodes = nodes.filter((node) => node.status === "failed").length;
|
|
113
|
+
return {
|
|
114
|
+
success: failedNodes === 0,
|
|
115
|
+
executionTime: 0,
|
|
116
|
+
nodeCount: nodes.length,
|
|
117
|
+
failureCount: failedNodes,
|
|
118
|
+
hungCount: 0
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
getAgentStats(agentId) {
|
|
123
|
+
return this.agentMetrics.get(agentId);
|
|
124
|
+
}
|
|
125
|
+
getAgentsList() {
|
|
126
|
+
return Array.from(this.agentMetrics.values()).sort((a, b) => b.lastExecution - a.lastExecution);
|
|
127
|
+
}
|
|
128
|
+
getGlobalStats() {
|
|
129
|
+
const agents = Array.from(this.agentMetrics.values());
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const oneHourAgo = now - 60 * 60 * 1e3;
|
|
132
|
+
const totalExecutions = agents.reduce((sum, agent) => sum + agent.totalExecutions, 0);
|
|
133
|
+
const totalSuccessful = agents.reduce((sum, agent) => sum + agent.successfulExecutions, 0);
|
|
134
|
+
const globalSuccessRate = totalExecutions > 0 ? totalSuccessful / totalExecutions * 100 : 0;
|
|
135
|
+
const activeAgents = agents.filter((agent) => agent.lastExecution > oneHourAgo).length;
|
|
136
|
+
const topAgents = agents.slice().sort((a, b) => b.totalExecutions - a.totalExecutions).slice(0, 10).map((agent) => ({
|
|
137
|
+
agentId: agent.agentId,
|
|
138
|
+
executionCount: agent.totalExecutions,
|
|
139
|
+
successRate: agent.successRate
|
|
140
|
+
}));
|
|
141
|
+
const recentActivity = [];
|
|
142
|
+
for (const agent of agents) {
|
|
143
|
+
for (const activity of agent.recentActivity.slice(0, 20)) {
|
|
144
|
+
recentActivity.push({
|
|
145
|
+
timestamp: activity.timestamp,
|
|
146
|
+
agentId: agent.agentId,
|
|
147
|
+
success: activity.success,
|
|
148
|
+
trigger: activity.trigger
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
recentActivity.sort((a, b) => b.timestamp - a.timestamp);
|
|
153
|
+
recentActivity.splice(200);
|
|
154
|
+
return {
|
|
155
|
+
totalAgents: agents.length,
|
|
156
|
+
totalExecutions,
|
|
157
|
+
globalSuccessRate,
|
|
158
|
+
activeAgents,
|
|
159
|
+
topAgents,
|
|
160
|
+
recentActivity
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
getPerformanceSummary() {
|
|
164
|
+
const global = this.getGlobalStats();
|
|
165
|
+
const agents = this.getAgentsList();
|
|
166
|
+
return {
|
|
167
|
+
overview: {
|
|
168
|
+
totalAgents: global.totalAgents,
|
|
169
|
+
totalExecutions: global.totalExecutions,
|
|
170
|
+
successRate: Math.round(global.globalSuccessRate * 100) / 100,
|
|
171
|
+
activeAgents: global.activeAgents
|
|
172
|
+
},
|
|
173
|
+
topPerformers: agents.slice(0, 5).map((agent) => ({
|
|
174
|
+
agentId: agent.agentId,
|
|
175
|
+
executions: agent.totalExecutions,
|
|
176
|
+
successRate: Math.round(agent.successRate * 100) / 100,
|
|
177
|
+
avgTime: Math.round(agent.avgExecutionTime * 100) / 100
|
|
178
|
+
})),
|
|
179
|
+
recentTrends: this.analyzeRecentTrends()
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
analyzeRecentTrends() {
|
|
183
|
+
const agents = Array.from(this.agentMetrics.values());
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const oneHourAgo = now - 60 * 60 * 1e3;
|
|
186
|
+
let recentExecutions = 0;
|
|
187
|
+
let recentFailures = 0;
|
|
188
|
+
for (const agent of agents) {
|
|
189
|
+
for (const activity of agent.recentActivity) {
|
|
190
|
+
if (activity.timestamp > oneHourAgo) {
|
|
191
|
+
recentExecutions++;
|
|
192
|
+
if (!activity.success) {
|
|
193
|
+
recentFailures++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
hourlyExecutions: recentExecutions,
|
|
200
|
+
hourlyFailures: recentFailures,
|
|
201
|
+
hourlySuccessRate: recentExecutions > 0 ? Math.round((recentExecutions - recentFailures) / recentExecutions * 1e4) / 100 : 0
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
cleanup() {
|
|
205
|
+
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
206
|
+
for (const [agentId, metrics] of this.agentMetrics.entries()) {
|
|
207
|
+
metrics.recentActivity = metrics.recentActivity.filter(
|
|
208
|
+
(activity) => activity.timestamp > cutoff
|
|
209
|
+
);
|
|
210
|
+
if (metrics.lastExecution < cutoff) {
|
|
211
|
+
this.agentMetrics.delete(agentId);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
console.log(`Cleaned up old metrics, ${this.agentMetrics.size} agents remaining`);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/watcher.ts
|
|
219
|
+
var import_agentflow_core2 = require("agentflow-core");
|
|
220
|
+
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
221
|
+
var import_events = require("events");
|
|
222
|
+
var fs = __toESM(require("fs"), 1);
|
|
223
|
+
var path = __toESM(require("path"), 1);
|
|
224
|
+
var TraceWatcher = class extends import_events.EventEmitter {
|
|
225
|
+
watchers = [];
|
|
226
|
+
traces = /* @__PURE__ */ new Map();
|
|
227
|
+
tracesDir;
|
|
228
|
+
dataDirs;
|
|
229
|
+
allWatchDirs;
|
|
230
|
+
constructor(tracesDirOrOptions) {
|
|
231
|
+
super();
|
|
232
|
+
if (typeof tracesDirOrOptions === "string") {
|
|
233
|
+
this.tracesDir = path.resolve(tracesDirOrOptions);
|
|
234
|
+
this.dataDirs = [];
|
|
235
|
+
} else {
|
|
236
|
+
this.tracesDir = path.resolve(tracesDirOrOptions.tracesDir);
|
|
237
|
+
this.dataDirs = (tracesDirOrOptions.dataDirs || []).map((d) => path.resolve(d));
|
|
238
|
+
}
|
|
239
|
+
this.allWatchDirs = [this.tracesDir, ...this.dataDirs];
|
|
240
|
+
this.ensureTracesDir();
|
|
241
|
+
this.loadExistingFiles();
|
|
242
|
+
this.startWatching();
|
|
243
|
+
}
|
|
244
|
+
ensureTracesDir() {
|
|
245
|
+
if (!fs.existsSync(this.tracesDir)) {
|
|
246
|
+
fs.mkdirSync(this.tracesDir, { recursive: true });
|
|
247
|
+
console.log(`Created traces directory: ${this.tracesDir}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
loadExistingFiles() {
|
|
251
|
+
let totalFiles = 0;
|
|
252
|
+
for (const dir of this.allWatchDirs) {
|
|
253
|
+
if (!fs.existsSync(dir)) continue;
|
|
254
|
+
try {
|
|
255
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".json") || f.endsWith(".jsonl"));
|
|
256
|
+
totalFiles += files.length;
|
|
257
|
+
for (const file of files) {
|
|
258
|
+
this.loadFile(path.join(dir, file));
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error(`Error scanning directory ${dir}:`, error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
console.log(`Scanned ${this.allWatchDirs.length} directories, loaded ${this.traces.size} items from ${totalFiles} files`);
|
|
265
|
+
}
|
|
266
|
+
/** Load a .json trace, .jsonl session file, or .log file. */
|
|
267
|
+
loadFile(filePath) {
|
|
268
|
+
if (filePath.endsWith(".jsonl")) {
|
|
269
|
+
return this.loadSessionFile(filePath);
|
|
270
|
+
}
|
|
271
|
+
if (filePath.endsWith(".log") || filePath.endsWith(".trace")) {
|
|
272
|
+
return this.loadLogFile(filePath);
|
|
273
|
+
}
|
|
274
|
+
return this.loadTraceFile(filePath);
|
|
275
|
+
}
|
|
276
|
+
loadLogFile(filePath) {
|
|
277
|
+
try {
|
|
278
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
279
|
+
const filename = path.basename(filePath);
|
|
280
|
+
const stats = fs.statSync(filePath);
|
|
281
|
+
const traces = this.parseUniversalLog(content, filename, filePath);
|
|
282
|
+
for (let i = 0; i < traces.length; i++) {
|
|
283
|
+
const trace = traces[i];
|
|
284
|
+
trace.filename = filename;
|
|
285
|
+
trace.lastModified = stats.mtime.getTime();
|
|
286
|
+
trace.sourceType = "trace";
|
|
287
|
+
trace.sourceDir = path.dirname(filePath);
|
|
288
|
+
const key = traces.length === 1 ? this.traceKey(filePath) : `${this.traceKey(filePath)}-${i}`;
|
|
289
|
+
this.traces.set(key, trace);
|
|
290
|
+
}
|
|
291
|
+
return traces.length > 0;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error(`Error loading log file ${filePath}:`, error);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/** Universal log parser - detects agent activities from any system */
|
|
298
|
+
parseUniversalLog(content, filename, filePath) {
|
|
299
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
300
|
+
const activities = /* @__PURE__ */ new Map();
|
|
301
|
+
for (const line of lines) {
|
|
302
|
+
const activity = this.detectActivityPattern(line);
|
|
303
|
+
if (!activity) continue;
|
|
304
|
+
const sessionId = this.extractSessionIdentifier(activity);
|
|
305
|
+
if (!activities.has(sessionId)) {
|
|
306
|
+
activities.set(sessionId, {
|
|
307
|
+
id: sessionId,
|
|
308
|
+
rootNodeId: "",
|
|
309
|
+
agentId: this.detectAgentIdentifier(activity, filename, filePath),
|
|
310
|
+
name: this.generateActivityName(activity, sessionId),
|
|
311
|
+
trigger: this.detectTrigger(activity),
|
|
312
|
+
startTime: activity.timestamp,
|
|
313
|
+
endTime: activity.timestamp,
|
|
314
|
+
status: "completed",
|
|
315
|
+
nodes: {},
|
|
316
|
+
edges: [],
|
|
317
|
+
events: [],
|
|
318
|
+
metadata: { sessionId, source: filename }
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const session = activities.get(sessionId);
|
|
322
|
+
this.addActivityNode(session, activity);
|
|
323
|
+
if (activity.timestamp > session.endTime) {
|
|
324
|
+
session.endTime = activity.timestamp;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const traces = Array.from(activities.values()).filter(
|
|
328
|
+
(session) => Object.keys(session.nodes).length > 0
|
|
329
|
+
);
|
|
330
|
+
if (traces.length === 0) {
|
|
331
|
+
const stats = fs.statSync(filePath);
|
|
332
|
+
traces.push({
|
|
333
|
+
id: "",
|
|
334
|
+
rootNodeId: "root",
|
|
335
|
+
nodes: {
|
|
336
|
+
"root": {
|
|
337
|
+
id: "root",
|
|
338
|
+
type: "log-file",
|
|
339
|
+
name: filename,
|
|
340
|
+
status: "completed",
|
|
341
|
+
startTime: stats.mtime.getTime(),
|
|
342
|
+
endTime: stats.mtime.getTime(),
|
|
343
|
+
metadata: { lineCount: lines.length, path: filePath }
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
edges: [],
|
|
347
|
+
startTime: stats.mtime.getTime(),
|
|
348
|
+
endTime: stats.mtime.getTime(),
|
|
349
|
+
status: "completed",
|
|
350
|
+
trigger: "file",
|
|
351
|
+
agentId: this.extractAgentFromPath(filePath),
|
|
352
|
+
events: [],
|
|
353
|
+
metadata: { type: "file-trace" }
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return traces;
|
|
357
|
+
}
|
|
358
|
+
/** Detect activity patterns in log lines using universal heuristics */
|
|
359
|
+
detectActivityPattern(line) {
|
|
360
|
+
let timestamp = this.extractTimestamp(line);
|
|
361
|
+
let level = this.extractLogLevel(line);
|
|
362
|
+
let action = this.extractAction(line);
|
|
363
|
+
let kvPairs = this.extractKeyValuePairs(line);
|
|
364
|
+
if (!timestamp) {
|
|
365
|
+
const jsonMatch = line.match(/\{.*\}/);
|
|
366
|
+
if (jsonMatch) {
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
369
|
+
timestamp = this.parseTimestamp(parsed.timestamp || parsed.time || parsed.ts) || Date.now();
|
|
370
|
+
level = parsed.level || parsed.severity || "info";
|
|
371
|
+
action = parsed.action || parsed.event || parsed.message || "";
|
|
372
|
+
kvPairs = parsed;
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (!timestamp) {
|
|
378
|
+
const kvMatches = line.match(/(\w+)=([^\s]+)/g);
|
|
379
|
+
if (kvMatches && kvMatches.length >= 2) {
|
|
380
|
+
const pairs = {};
|
|
381
|
+
kvMatches.forEach((match) => {
|
|
382
|
+
const [key, value] = match.split("=", 2);
|
|
383
|
+
pairs[key] = this.parseValue(value);
|
|
384
|
+
});
|
|
385
|
+
timestamp = this.parseTimestamp(pairs.timestamp || pairs.time) || Date.now();
|
|
386
|
+
level = pairs.level || "info";
|
|
387
|
+
action = pairs.action || pairs.event || "";
|
|
388
|
+
kvPairs = pairs;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!timestamp) {
|
|
392
|
+
const logMatch = line.match(/^(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)\s+(\w+)?\s*:?\s*(.+)/);
|
|
393
|
+
if (logMatch) {
|
|
394
|
+
timestamp = new Date(logMatch[1]).getTime();
|
|
395
|
+
level = logMatch[2] || "info";
|
|
396
|
+
action = logMatch[3] || "";
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
if (!timestamp) return null;
|
|
400
|
+
return {
|
|
401
|
+
timestamp,
|
|
402
|
+
level: (level == null ? void 0 : level.toLowerCase()) || "info",
|
|
403
|
+
action,
|
|
404
|
+
component: this.detectComponent(action, kvPairs),
|
|
405
|
+
operation: this.detectOperation(action, kvPairs),
|
|
406
|
+
...kvPairs
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
extractTimestamp(line) {
|
|
410
|
+
const coloredMatch = line.match(/^\[2m(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\[0m/);
|
|
411
|
+
if (coloredMatch) return new Date(coloredMatch[1]).getTime();
|
|
412
|
+
const isoMatch = line.match(/(\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}[.\d]*Z?)/);
|
|
413
|
+
if (isoMatch) return new Date(isoMatch[1]).getTime();
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
extractLogLevel(line) {
|
|
417
|
+
const coloredMatch = line.match(/\[\[(\d+)m\[\[1m(\w+)\s*\[0m\]/);
|
|
418
|
+
if (coloredMatch) return coloredMatch[2].toLowerCase();
|
|
419
|
+
const levelMatch = line.match(/\b(debug|info|warn|warning|error|fatal|trace)\b/i);
|
|
420
|
+
return levelMatch ? levelMatch[1].toLowerCase() : null;
|
|
421
|
+
}
|
|
422
|
+
extractAction(line) {
|
|
423
|
+
const coloredMatch = line.match(/\[1m([^\[]+?)\s*\[0m/);
|
|
424
|
+
if (coloredMatch) return coloredMatch[1].trim();
|
|
425
|
+
const afterLevel = line.replace(/^.*?(debug|info|warn|warning|error|fatal|trace)\s*:?\s*/i, "");
|
|
426
|
+
return afterLevel.split(" ")[0] || "";
|
|
427
|
+
}
|
|
428
|
+
extractKeyValuePairs(line) {
|
|
429
|
+
const pairs = {};
|
|
430
|
+
const coloredRegex = /\[36m(\w+)\[0m=\[35m([^\[]+?)\[0m/g;
|
|
431
|
+
let match;
|
|
432
|
+
while ((match = coloredRegex.exec(line)) !== null) {
|
|
433
|
+
pairs[match[1]] = this.parseValue(match[2]);
|
|
434
|
+
}
|
|
435
|
+
if (Object.keys(pairs).length === 0) {
|
|
436
|
+
const kvRegex = /(\w+)=([^\s]+)/g;
|
|
437
|
+
while ((match = kvRegex.exec(line)) !== null) {
|
|
438
|
+
pairs[match[1]] = this.parseValue(match[2]);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return pairs;
|
|
442
|
+
}
|
|
443
|
+
parseValue(value) {
|
|
444
|
+
if (value.match(/^\d+$/)) return parseInt(value);
|
|
445
|
+
if (value.match(/^\d+\.\d+$/)) return parseFloat(value);
|
|
446
|
+
if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
447
|
+
if (value.startsWith('"') && value.endsWith('"')) return value.slice(1, -1);
|
|
448
|
+
return value;
|
|
449
|
+
}
|
|
450
|
+
parseTimestamp(value) {
|
|
451
|
+
if (!value) return null;
|
|
452
|
+
if (typeof value === "number") return value;
|
|
453
|
+
try {
|
|
454
|
+
return new Date(value).getTime();
|
|
455
|
+
} catch {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
detectComponent(action, kvPairs) {
|
|
460
|
+
if (action.includes(".")) return action.split(".")[0];
|
|
461
|
+
if (kvPairs.component) return kvPairs.component;
|
|
462
|
+
if (kvPairs.service) return kvPairs.service;
|
|
463
|
+
if (kvPairs.module) return kvPairs.module;
|
|
464
|
+
return "unknown";
|
|
465
|
+
}
|
|
466
|
+
detectOperation(action, kvPairs) {
|
|
467
|
+
if (action.includes(".")) return action.split(".").slice(1).join(".");
|
|
468
|
+
if (kvPairs.operation) return kvPairs.operation;
|
|
469
|
+
if (kvPairs.method) return kvPairs.method;
|
|
470
|
+
return action || "activity";
|
|
471
|
+
}
|
|
472
|
+
extractSessionIdentifier(activity) {
|
|
473
|
+
return activity.run_id || activity.session_id || activity.request_id || activity.trace_id || activity.sweep_id || activity.transaction_id || "default";
|
|
474
|
+
}
|
|
475
|
+
detectAgentIdentifier(activity, filename, filePath) {
|
|
476
|
+
if (activity.component !== "unknown") {
|
|
477
|
+
const pathAgent = this.extractAgentFromPath(filePath);
|
|
478
|
+
if (pathAgent !== activity.component) {
|
|
479
|
+
return `${pathAgent}-${activity.component}`;
|
|
480
|
+
}
|
|
481
|
+
return activity.component;
|
|
482
|
+
}
|
|
483
|
+
return this.extractAgentFromPath(filePath);
|
|
484
|
+
}
|
|
485
|
+
extractAgentFromPath(filePath) {
|
|
486
|
+
const filename = path.basename(filePath, path.extname(filePath));
|
|
487
|
+
const pathParts = filePath.split(path.sep);
|
|
488
|
+
for (const part of pathParts.reverse()) {
|
|
489
|
+
if (part.match(/agent|worker|service|daemon|bot|ai|llm/i)) {
|
|
490
|
+
return part;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return filename;
|
|
494
|
+
}
|
|
495
|
+
generateActivityName(activity, sessionId) {
|
|
496
|
+
const component = activity.component !== "unknown" ? activity.component : "Activity";
|
|
497
|
+
const operation = activity.operation !== "activity" ? `: ${activity.operation}` : "";
|
|
498
|
+
return `${component}${operation} (${sessionId})`;
|
|
499
|
+
}
|
|
500
|
+
detectTrigger(activity) {
|
|
501
|
+
var _a, _b;
|
|
502
|
+
if (activity.trigger) return activity.trigger;
|
|
503
|
+
if (activity.method && activity.url) return "api-call";
|
|
504
|
+
if ((_a = activity.operation) == null ? void 0 : _a.includes("start")) return "startup";
|
|
505
|
+
if ((_b = activity.operation) == null ? void 0 : _b.includes("invoke")) return "invocation";
|
|
506
|
+
return "event";
|
|
507
|
+
}
|
|
508
|
+
addActivityNode(session, activity) {
|
|
509
|
+
const nodeId = `${activity.component}-${activity.operation}-${activity.timestamp}`;
|
|
510
|
+
const node = {
|
|
511
|
+
id: nodeId,
|
|
512
|
+
type: activity.component,
|
|
513
|
+
name: `${activity.component}: ${activity.operation}`,
|
|
514
|
+
status: this.getUniversalNodeStatus(activity),
|
|
515
|
+
startTime: activity.timestamp,
|
|
516
|
+
endTime: activity.timestamp,
|
|
517
|
+
metadata: activity
|
|
518
|
+
};
|
|
519
|
+
session.nodes[nodeId] = node;
|
|
520
|
+
if (!session.rootNodeId) {
|
|
521
|
+
session.rootNodeId = nodeId;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
getUniversalNodeStatus(activity) {
|
|
525
|
+
var _a, _b;
|
|
526
|
+
if (activity.level === "error" || activity.level === "fatal") return "failed";
|
|
527
|
+
if (activity.level === "warn" || activity.level === "warning") return "warning";
|
|
528
|
+
if ((_a = activity.operation) == null ? void 0 : _a.match(/start|begin|init/i)) return "running";
|
|
529
|
+
if ((_b = activity.operation) == null ? void 0 : _b.match(/complete|finish|end|done/i)) return "completed";
|
|
530
|
+
return "completed";
|
|
531
|
+
}
|
|
532
|
+
loadTraceFile(filePath) {
|
|
533
|
+
try {
|
|
534
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
535
|
+
const graph = (0, import_agentflow_core2.loadGraph)(content);
|
|
536
|
+
const filename = path.basename(filePath);
|
|
537
|
+
const stats = fs.statSync(filePath);
|
|
538
|
+
graph.filename = filename;
|
|
539
|
+
graph.lastModified = stats.mtime.getTime();
|
|
540
|
+
graph.sourceType = "trace";
|
|
541
|
+
graph.sourceDir = path.dirname(filePath);
|
|
542
|
+
this.traces.set(this.traceKey(filePath), graph);
|
|
543
|
+
return true;
|
|
544
|
+
} catch {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/** Parse a JSONL session log into a WatchedTrace (best-effort). */
|
|
549
|
+
loadSessionFile(filePath) {
|
|
550
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
551
|
+
try {
|
|
552
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
553
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
554
|
+
if (lines.length === 0) return false;
|
|
555
|
+
const rawEvents = [];
|
|
556
|
+
for (const line of lines) {
|
|
557
|
+
try {
|
|
558
|
+
rawEvents.push(JSON.parse(line));
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (rawEvents.length === 0) return false;
|
|
563
|
+
const sessionEvent = rawEvents.find((e) => e.type === "session");
|
|
564
|
+
const sessionId = (sessionEvent == null ? void 0 : sessionEvent.id) || path.basename(filePath, ".jsonl");
|
|
565
|
+
const sessionTimestamp = (sessionEvent == null ? void 0 : sessionEvent.timestamp) || ((_a = rawEvents[0]) == null ? void 0 : _a.timestamp);
|
|
566
|
+
const startTime = sessionTimestamp ? new Date(sessionTimestamp).getTime() : 0;
|
|
567
|
+
if (!startTime) return false;
|
|
568
|
+
const parentDir = path.basename(path.dirname(filePath));
|
|
569
|
+
const grandParentDir = path.basename(path.dirname(path.dirname(filePath)));
|
|
570
|
+
const agentId = grandParentDir === "agents" ? parentDir : parentDir;
|
|
571
|
+
const modelEvent = rawEvents.find((e) => e.type === "model_change");
|
|
572
|
+
const provider = (modelEvent == null ? void 0 : modelEvent.provider) || "";
|
|
573
|
+
const modelId = (modelEvent == null ? void 0 : modelEvent.modelId) || "";
|
|
574
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
575
|
+
let lastTimestamp = startTime;
|
|
576
|
+
for (const evt of rawEvents) {
|
|
577
|
+
if (evt.timestamp) {
|
|
578
|
+
const ts = new Date(evt.timestamp).getTime();
|
|
579
|
+
if (ts > lastTimestamp) lastTimestamp = ts;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
const firstMessage = rawEvents.find((e) => {
|
|
583
|
+
var _a2;
|
|
584
|
+
return e.type === "message" && ((_a2 = e.message) == null ? void 0 : _a2.role) === "user";
|
|
585
|
+
});
|
|
586
|
+
const userPrompt = ((_d = (_c = (_b = firstMessage == null ? void 0 : firstMessage.message) == null ? void 0 : _b.content) == null ? void 0 : _c[0]) == null ? void 0 : _d.text) || "";
|
|
587
|
+
const cronMatch = userPrompt.match(/\[cron:(\S+)\s+([^\]]+)\]/);
|
|
588
|
+
const triggerName = cronMatch ? cronMatch[2] : "";
|
|
589
|
+
const trigger = cronMatch ? "cron" : "message";
|
|
590
|
+
let totalInputTokens = 0;
|
|
591
|
+
let totalOutputTokens = 0;
|
|
592
|
+
let totalTokensSum = 0;
|
|
593
|
+
let totalCost = 0;
|
|
594
|
+
let userMessageCount = 0;
|
|
595
|
+
let assistantMessageCount = 0;
|
|
596
|
+
let toolCallCount = 0;
|
|
597
|
+
let thinkingBlockCount = 0;
|
|
598
|
+
const sessionEvents = [];
|
|
599
|
+
const toolCallMap = /* @__PURE__ */ new Map();
|
|
600
|
+
const rootId = `session-${sessionId.slice(0, 8)}`;
|
|
601
|
+
const rootName = triggerName || userPrompt.slice(0, 80) + (userPrompt.length > 80 ? "..." : "") || sessionId;
|
|
602
|
+
for (const evt of rawEvents) {
|
|
603
|
+
const evtTs = evt.timestamp ? new Date(evt.timestamp).getTime() : startTime;
|
|
604
|
+
if (evt.type === "session") {
|
|
605
|
+
sessionEvents.push({
|
|
606
|
+
type: "system",
|
|
607
|
+
timestamp: evtTs,
|
|
608
|
+
name: "Session Started",
|
|
609
|
+
content: `Version: ${evt.version || "unknown"}, CWD: ${evt.cwd || ""}`,
|
|
610
|
+
id: evt.id
|
|
611
|
+
});
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (evt.type === "model_change") {
|
|
615
|
+
sessionEvents.push({
|
|
616
|
+
type: "model_change",
|
|
617
|
+
timestamp: evtTs,
|
|
618
|
+
name: "Model Change",
|
|
619
|
+
model: evt.modelId,
|
|
620
|
+
provider: evt.provider,
|
|
621
|
+
content: `${evt.provider}/${evt.modelId}`,
|
|
622
|
+
id: evt.id
|
|
623
|
+
});
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (evt.type === "thinking_level_change") {
|
|
627
|
+
sessionEvents.push({
|
|
628
|
+
type: "system",
|
|
629
|
+
timestamp: evtTs,
|
|
630
|
+
name: "Thinking Level",
|
|
631
|
+
content: evt.thinkingLevel || "",
|
|
632
|
+
id: evt.id
|
|
633
|
+
});
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (evt.type === "custom" && evt.customType === "model-snapshot") {
|
|
637
|
+
sessionEvents.push({
|
|
638
|
+
type: "system",
|
|
639
|
+
timestamp: evtTs,
|
|
640
|
+
name: "Model Snapshot",
|
|
641
|
+
content: JSON.stringify(evt.data || {}).slice(0, 200),
|
|
642
|
+
id: evt.id
|
|
643
|
+
});
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (evt.type === "custom_message" && evt.customType === "openclaw.sessions_yield") {
|
|
647
|
+
sessionEvents.push({
|
|
648
|
+
type: "spawn",
|
|
649
|
+
timestamp: evtTs,
|
|
650
|
+
name: "Subagent Spawn",
|
|
651
|
+
content: ((_e = evt.data) == null ? void 0 : _e.sessionId) || "",
|
|
652
|
+
id: evt.id,
|
|
653
|
+
parentId: evt.parentId
|
|
654
|
+
});
|
|
655
|
+
const spawnId = `spawn-${toolCallCount + thinkingBlockCount + 1}`;
|
|
656
|
+
nodes.set(spawnId, {
|
|
657
|
+
id: spawnId,
|
|
658
|
+
type: "subagent",
|
|
659
|
+
name: "Subagent: " + (((_f = evt.data) == null ? void 0 : _f.sessionId) || "").slice(0, 12),
|
|
660
|
+
startTime: evtTs,
|
|
661
|
+
endTime: evtTs,
|
|
662
|
+
status: "completed",
|
|
663
|
+
parentId: rootId,
|
|
664
|
+
children: [],
|
|
665
|
+
metadata: { sessionId: (_g = evt.data) == null ? void 0 : _g.sessionId }
|
|
666
|
+
});
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (evt.type === "message" && evt.message) {
|
|
670
|
+
const msg = evt.message;
|
|
671
|
+
const role = msg.role;
|
|
672
|
+
const contentBlocks = Array.isArray(msg.content) ? msg.content : [];
|
|
673
|
+
if (role === "user") {
|
|
674
|
+
userMessageCount++;
|
|
675
|
+
const textContent = contentBlocks.filter((b) => b.type === "text").map((b) => b.text || "").join("\n");
|
|
676
|
+
sessionEvents.push({
|
|
677
|
+
type: "user",
|
|
678
|
+
timestamp: evtTs,
|
|
679
|
+
name: "User Message",
|
|
680
|
+
content: textContent,
|
|
681
|
+
id: evt.id,
|
|
682
|
+
parentId: evt.parentId
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (role === "assistant") {
|
|
686
|
+
assistantMessageCount++;
|
|
687
|
+
if (msg.usage) {
|
|
688
|
+
const u = msg.usage;
|
|
689
|
+
totalInputTokens += u.input || 0;
|
|
690
|
+
totalOutputTokens += u.output || 0;
|
|
691
|
+
totalTokensSum += u.totalTokens || 0;
|
|
692
|
+
if ((_h = u.cost) == null ? void 0 : _h.total) totalCost += u.cost.total;
|
|
693
|
+
}
|
|
694
|
+
for (const block of contentBlocks) {
|
|
695
|
+
if (block.type === "text" && block.text) {
|
|
696
|
+
sessionEvents.push({
|
|
697
|
+
type: "assistant",
|
|
698
|
+
timestamp: evtTs,
|
|
699
|
+
name: "Assistant",
|
|
700
|
+
content: block.text,
|
|
701
|
+
id: evt.id,
|
|
702
|
+
parentId: evt.parentId,
|
|
703
|
+
tokens: msg.usage ? {
|
|
704
|
+
input: msg.usage.input || 0,
|
|
705
|
+
output: msg.usage.output || 0,
|
|
706
|
+
total: msg.usage.totalTokens || 0,
|
|
707
|
+
cost: (_i = msg.usage.cost) == null ? void 0 : _i.total
|
|
708
|
+
} : void 0,
|
|
709
|
+
model: modelId,
|
|
710
|
+
provider
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
if (block.type === "thinking" && block.thinking) {
|
|
714
|
+
thinkingBlockCount++;
|
|
715
|
+
const thinkId = `thinking-${thinkingBlockCount}`;
|
|
716
|
+
sessionEvents.push({
|
|
717
|
+
type: "thinking",
|
|
718
|
+
timestamp: evtTs,
|
|
719
|
+
name: "Thinking",
|
|
720
|
+
content: block.thinking,
|
|
721
|
+
id: thinkId,
|
|
722
|
+
parentId: evt.id
|
|
723
|
+
});
|
|
724
|
+
nodes.set(thinkId, {
|
|
725
|
+
id: thinkId,
|
|
726
|
+
type: "decision",
|
|
727
|
+
name: "Thinking",
|
|
728
|
+
startTime: evtTs,
|
|
729
|
+
endTime: evtTs,
|
|
730
|
+
status: "completed",
|
|
731
|
+
parentId: rootId,
|
|
732
|
+
children: [],
|
|
733
|
+
metadata: { preview: block.thinking.slice(0, 100) }
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
if (block.type === "toolCall") {
|
|
737
|
+
toolCallCount++;
|
|
738
|
+
const toolName = block.name || "unknown";
|
|
739
|
+
const toolId = `tool-${toolCallCount}`;
|
|
740
|
+
const toolCallId = block.id || toolId;
|
|
741
|
+
sessionEvents.push({
|
|
742
|
+
type: "tool_call",
|
|
743
|
+
timestamp: evtTs,
|
|
744
|
+
name: toolName,
|
|
745
|
+
toolName,
|
|
746
|
+
toolArgs: block.arguments,
|
|
747
|
+
id: toolCallId,
|
|
748
|
+
parentId: evt.id
|
|
749
|
+
});
|
|
750
|
+
toolCallMap.set(toolCallId, sessionEvents.length - 1);
|
|
751
|
+
nodes.set(toolId, {
|
|
752
|
+
id: toolId,
|
|
753
|
+
type: "tool",
|
|
754
|
+
name: toolName,
|
|
755
|
+
startTime: evtTs,
|
|
756
|
+
endTime: evtTs,
|
|
757
|
+
// updated when result arrives
|
|
758
|
+
status: "running",
|
|
759
|
+
parentId: rootId,
|
|
760
|
+
children: [],
|
|
761
|
+
metadata: {
|
|
762
|
+
toolCallId,
|
|
763
|
+
args: block.arguments
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (role === "toolResult") {
|
|
770
|
+
const toolCallId = ((_j = contentBlocks[0]) == null ? void 0 : _j.toolCallId) || evt.parentId;
|
|
771
|
+
const resultContent = contentBlocks.map((b) => b.text || b.content || "").join("\n");
|
|
772
|
+
const hasError = contentBlocks.some((b) => b.isError || b.error);
|
|
773
|
+
const errorText = hasError ? resultContent : void 0;
|
|
774
|
+
sessionEvents.push({
|
|
775
|
+
type: "tool_result",
|
|
776
|
+
timestamp: evtTs,
|
|
777
|
+
name: "Tool Result",
|
|
778
|
+
toolResult: resultContent.slice(0, 2e3),
|
|
779
|
+
toolError: errorText == null ? void 0 : errorText.slice(0, 500),
|
|
780
|
+
id: evt.id,
|
|
781
|
+
parentId: toolCallId
|
|
782
|
+
});
|
|
783
|
+
for (const [nodeId, node] of nodes) {
|
|
784
|
+
if (node.type === "tool" && ((_k = node.metadata) == null ? void 0 : _k.toolCallId) === toolCallId) {
|
|
785
|
+
node.endTime = evtTs;
|
|
786
|
+
node.status = hasError ? "failed" : "completed";
|
|
787
|
+
if (hasError) node.metadata.error = errorText == null ? void 0 : errorText.slice(0, 500);
|
|
788
|
+
const callIdx = toolCallMap.get(toolCallId);
|
|
789
|
+
if (callIdx !== void 0 && sessionEvents[callIdx]) {
|
|
790
|
+
const callTs = sessionEvents[callIdx].timestamp;
|
|
791
|
+
sessionEvents[sessionEvents.length - 1].duration = evtTs - callTs;
|
|
792
|
+
sessionEvents[callIdx].duration = evtTs - callTs;
|
|
793
|
+
}
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const fileStat = fs.statSync(filePath);
|
|
801
|
+
const fileAge = Date.now() - fileStat.mtime.getTime();
|
|
802
|
+
const lastEvt = rawEvents[rawEvents.length - 1];
|
|
803
|
+
const hasToolError = sessionEvents.some((e) => e.type === "tool_result" && e.toolError);
|
|
804
|
+
const lastIsAssistant = (lastEvt == null ? void 0 : lastEvt.type) === "message" && ((_l = lastEvt == null ? void 0 : lastEvt.message) == null ? void 0 : _l.role) === "assistant";
|
|
805
|
+
const isRecentlyModified = fileAge < 5 * 60 * 1e3;
|
|
806
|
+
let status;
|
|
807
|
+
if (hasToolError) {
|
|
808
|
+
status = "failed";
|
|
809
|
+
} else if (lastIsAssistant) {
|
|
810
|
+
status = "completed";
|
|
811
|
+
} else if (isRecentlyModified) {
|
|
812
|
+
status = "running";
|
|
813
|
+
} else {
|
|
814
|
+
status = "completed";
|
|
815
|
+
}
|
|
816
|
+
const tokenUsage = {
|
|
817
|
+
input: totalInputTokens,
|
|
818
|
+
output: totalOutputTokens,
|
|
819
|
+
total: totalTokensSum || totalInputTokens + totalOutputTokens,
|
|
820
|
+
cost: totalCost
|
|
821
|
+
};
|
|
822
|
+
nodes.set(rootId, {
|
|
823
|
+
id: rootId,
|
|
824
|
+
type: "agent",
|
|
825
|
+
name: rootName,
|
|
826
|
+
startTime,
|
|
827
|
+
endTime: lastTimestamp,
|
|
828
|
+
status,
|
|
829
|
+
parentId: void 0,
|
|
830
|
+
children: Array.from(nodes.keys()).filter((k) => k !== rootId),
|
|
831
|
+
metadata: {
|
|
832
|
+
provider,
|
|
833
|
+
model: modelId,
|
|
834
|
+
sessionId,
|
|
835
|
+
trigger,
|
|
836
|
+
totalTokens: tokenUsage.total,
|
|
837
|
+
inputTokens: tokenUsage.input,
|
|
838
|
+
outputTokens: tokenUsage.output,
|
|
839
|
+
cost: tokenUsage.cost,
|
|
840
|
+
userMessages: userMessageCount,
|
|
841
|
+
assistantMessages: assistantMessageCount,
|
|
842
|
+
toolCalls: toolCallCount,
|
|
843
|
+
thinkingBlocks: thinkingBlockCount,
|
|
844
|
+
"gen_ai.system": provider,
|
|
845
|
+
"gen_ai.request.model": modelId
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
const filename = path.basename(filePath);
|
|
849
|
+
const trace = {
|
|
850
|
+
id: sessionId,
|
|
851
|
+
nodes,
|
|
852
|
+
edges: [],
|
|
853
|
+
events: [],
|
|
854
|
+
startTime,
|
|
855
|
+
agentId,
|
|
856
|
+
trigger,
|
|
857
|
+
name: rootName,
|
|
858
|
+
traceId: sessionId,
|
|
859
|
+
spanId: sessionId,
|
|
860
|
+
filename,
|
|
861
|
+
lastModified: fileStat.mtime.getTime(),
|
|
862
|
+
sourceType: "session",
|
|
863
|
+
sourceDir: path.dirname(filePath),
|
|
864
|
+
sessionEvents,
|
|
865
|
+
tokenUsage,
|
|
866
|
+
metadata: {
|
|
867
|
+
provider,
|
|
868
|
+
model: modelId,
|
|
869
|
+
userMessages: userMessageCount,
|
|
870
|
+
assistantMessages: assistantMessageCount,
|
|
871
|
+
toolCalls: toolCallCount,
|
|
872
|
+
thinkingBlocks: thinkingBlockCount,
|
|
873
|
+
totalEvents: rawEvents.length,
|
|
874
|
+
sessionVersion: sessionEvent == null ? void 0 : sessionEvent.version
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
this.traces.set(this.traceKey(filePath), trace);
|
|
878
|
+
return true;
|
|
879
|
+
} catch {
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/** Unique key for a file across directories. */
|
|
884
|
+
traceKey(filePath) {
|
|
885
|
+
for (const dir of this.allWatchDirs) {
|
|
886
|
+
if (filePath.startsWith(dir)) {
|
|
887
|
+
return path.relative(dir, filePath).replace(/\\/g, "/") + "@" + path.basename(dir);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return filePath;
|
|
891
|
+
}
|
|
892
|
+
startWatching() {
|
|
893
|
+
for (const dir of this.allWatchDirs) {
|
|
894
|
+
if (!fs.existsSync(dir)) continue;
|
|
895
|
+
const watcher = import_chokidar.default.watch(dir, {
|
|
896
|
+
ignored: /^\./,
|
|
897
|
+
persistent: true,
|
|
898
|
+
ignoreInitial: true,
|
|
899
|
+
depth: 0
|
|
900
|
+
// don't recurse into subdirectories
|
|
901
|
+
});
|
|
902
|
+
watcher.on("add", (filePath) => {
|
|
903
|
+
if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
|
|
904
|
+
console.log(`New file: ${path.basename(filePath)}`);
|
|
905
|
+
if (this.loadFile(filePath)) {
|
|
906
|
+
const key = this.traceKey(filePath);
|
|
907
|
+
const trace = this.traces.get(key);
|
|
908
|
+
if (trace) {
|
|
909
|
+
this.emit("trace-added", trace);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
watcher.on("change", (filePath) => {
|
|
915
|
+
if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
|
|
916
|
+
if (this.loadFile(filePath)) {
|
|
917
|
+
const key = this.traceKey(filePath);
|
|
918
|
+
const trace = this.traces.get(key);
|
|
919
|
+
if (trace) {
|
|
920
|
+
this.emit("trace-updated", trace);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
watcher.on("unlink", (filePath) => {
|
|
926
|
+
if (filePath.endsWith(".json") || filePath.endsWith(".jsonl") || filePath.endsWith(".log") || filePath.endsWith(".trace")) {
|
|
927
|
+
const key = this.traceKey(filePath);
|
|
928
|
+
this.traces.delete(key);
|
|
929
|
+
this.emit("trace-removed", key);
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
watcher.on("error", (error) => {
|
|
933
|
+
console.error(`Watcher error on ${dir}:`, error);
|
|
934
|
+
});
|
|
935
|
+
this.watchers.push(watcher);
|
|
936
|
+
}
|
|
937
|
+
console.log(`Watching ${this.allWatchDirs.length} directories for JSON/JSONL files`);
|
|
938
|
+
}
|
|
939
|
+
getAllTraces() {
|
|
940
|
+
return Array.from(this.traces.values()).sort((a, b) => {
|
|
941
|
+
return (b.lastModified || b.startTime) - (a.lastModified || a.startTime);
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
getTrace(filename) {
|
|
945
|
+
const exact = this.traces.get(filename);
|
|
946
|
+
if (exact) return exact;
|
|
947
|
+
for (const [key, trace] of this.traces) {
|
|
948
|
+
if (trace.filename === filename || key.endsWith(filename)) {
|
|
949
|
+
return trace;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
954
|
+
getTracesByAgent(agentId) {
|
|
955
|
+
return this.getAllTraces().filter((trace) => trace.agentId === agentId);
|
|
956
|
+
}
|
|
957
|
+
getRecentTraces(limit = 50) {
|
|
958
|
+
return this.getAllTraces().slice(0, limit);
|
|
959
|
+
}
|
|
960
|
+
getTraceCount() {
|
|
961
|
+
return this.traces.size;
|
|
962
|
+
}
|
|
963
|
+
getAgentIds() {
|
|
964
|
+
const agentIds = /* @__PURE__ */ new Set();
|
|
965
|
+
for (const trace of this.traces.values()) {
|
|
966
|
+
agentIds.add(trace.agentId);
|
|
967
|
+
}
|
|
968
|
+
return Array.from(agentIds).sort();
|
|
969
|
+
}
|
|
970
|
+
stop() {
|
|
971
|
+
for (const w of this.watchers) {
|
|
972
|
+
w.close();
|
|
973
|
+
}
|
|
974
|
+
this.watchers = [];
|
|
975
|
+
console.log("Stopped watching all directories");
|
|
976
|
+
}
|
|
977
|
+
getTraceStats() {
|
|
978
|
+
const total = this.traces.size;
|
|
979
|
+
const agentCount = this.getAgentIds().length;
|
|
980
|
+
const recentCount = this.getRecentTraces(24).length;
|
|
981
|
+
const triggers = /* @__PURE__ */ new Map();
|
|
982
|
+
for (const trace of this.traces.values()) {
|
|
983
|
+
const trigger = trace.trigger || "unknown";
|
|
984
|
+
triggers.set(trigger, (triggers.get(trigger) || 0) + 1);
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
total,
|
|
988
|
+
agentCount,
|
|
989
|
+
recentCount,
|
|
990
|
+
triggers: Object.fromEntries(triggers)
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
// src/cli.ts
|
|
996
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
997
|
+
var os = __toESM(require("os"), 1);
|
|
998
|
+
var path2 = __toESM(require("path"), 1);
|
|
999
|
+
var VERSION = "0.3.1";
|
|
1000
|
+
function getLanAddress() {
|
|
1001
|
+
const interfaces = os.networkInterfaces();
|
|
1002
|
+
for (const name of Object.keys(interfaces)) {
|
|
1003
|
+
for (const iface of interfaces[name] || []) {
|
|
1004
|
+
if (iface.family === "IPv4" && !iface.internal) {
|
|
1005
|
+
return iface.address;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
return null;
|
|
1010
|
+
}
|
|
1011
|
+
function printBanner(config, traceCount, stats) {
|
|
1012
|
+
var _a;
|
|
1013
|
+
const lan = getLanAddress();
|
|
1014
|
+
const host = config.host || "localhost";
|
|
1015
|
+
const port = config.port;
|
|
1016
|
+
const isPublic = host === "0.0.0.0";
|
|
1017
|
+
console.log(`
|
|
1018
|
+
___ _ _____ _
|
|
1019
|
+
/ _ \\ __ _ ___ _ __ | |_| ___| | _____ __
|
|
1020
|
+
| |_| |/ _\` |/ _ \\ '_ \\| __| |_ | |/ _ \\ \\ /\\ / /
|
|
1021
|
+
| _ | (_| | __/ | | | |_| _| | | (_) \\ V V /
|
|
1022
|
+
|_| |_|\\__, |\\___|_| |_|\\__|_| |_|\\___/ \\_/\\_/
|
|
1023
|
+
|___/ dashboard v${VERSION}
|
|
1024
|
+
|
|
1025
|
+
See your agents think.
|
|
1026
|
+
|
|
1027
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
1028
|
+
\u2502 \u{1F916} Agents \u2502 TRACE FILES \u2502 \u{1F4CA} AgentFlow \u2502 SHOWS YOU \u2502 \u{1F310} Your browser \u2502
|
|
1029
|
+
\u2502 Execute tasks, \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500> \u2502 Reads traces, \u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500> \u2502 Interactive \u2502
|
|
1030
|
+
\u2502 write JSON \u2502 \u2502 builds graphs, \u2502 \u2502 graph, timeline, \u2502
|
|
1031
|
+
\u2502 trace files. \u2502 \u2502 serves dashboard.\u2502 \u2502 metrics, health. \u2502
|
|
1032
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
1033
|
+
|
|
1034
|
+
Runs locally. Your data never leaves your machine.
|
|
1035
|
+
|
|
1036
|
+
Tabs: \u{1F3AF} Graph \xB7 \u23F1\uFE0F Timeline \xB7 \u{1F4CA} Metrics \xB7 \u{1F6E0}\uFE0F Process Health \xB7 \u26A0\uFE0F Errors
|
|
1037
|
+
|
|
1038
|
+
Traces: ${config.tracesDir}${((_a = config.dataDirs) == null ? void 0 : _a.length) ? "\n Data dirs: " + config.dataDirs.join("\n ") : ""}
|
|
1039
|
+
Loaded: ${traceCount} traces \xB7 ${stats.totalAgents} agents \xB7 ${stats.totalExecutions} executions
|
|
1040
|
+
Success: ${stats.globalSuccessRate.toFixed(1)}%${stats.activeAgents > 0 ? ` \xB7 ${stats.activeAgents} active now` : ""}
|
|
1041
|
+
CORS: ${config.enableCors ? "enabled" : "disabled"}
|
|
1042
|
+
WebSocket: live updates enabled
|
|
1043
|
+
|
|
1044
|
+
\u2192 http://localhost:${port}${isPublic && lan ? `
|
|
1045
|
+
\u2192 http://${lan}:${port} (LAN)` : ""}
|
|
1046
|
+
`);
|
|
1047
|
+
}
|
|
1048
|
+
async function startDashboard() {
|
|
1049
|
+
const args = process.argv.slice(2);
|
|
1050
|
+
const config = {
|
|
1051
|
+
port: 3e3,
|
|
1052
|
+
tracesDir: "./traces",
|
|
1053
|
+
host: "localhost",
|
|
1054
|
+
enableCors: false
|
|
1055
|
+
};
|
|
1056
|
+
for (let i = 0; i < args.length; i++) {
|
|
1057
|
+
switch (args[i]) {
|
|
1058
|
+
case "--port":
|
|
1059
|
+
case "-p":
|
|
1060
|
+
config.port = parseInt(args[++i]) || 3e3;
|
|
1061
|
+
break;
|
|
1062
|
+
case "--traces":
|
|
1063
|
+
case "-t":
|
|
1064
|
+
config.tracesDir = args[++i];
|
|
1065
|
+
break;
|
|
1066
|
+
case "--host":
|
|
1067
|
+
case "-h":
|
|
1068
|
+
config.host = args[++i];
|
|
1069
|
+
break;
|
|
1070
|
+
case "--data-dir":
|
|
1071
|
+
if (!config.dataDirs) config.dataDirs = [];
|
|
1072
|
+
config.dataDirs.push(args[++i]);
|
|
1073
|
+
break;
|
|
1074
|
+
case "--cors":
|
|
1075
|
+
config.enableCors = true;
|
|
1076
|
+
break;
|
|
1077
|
+
case "--help":
|
|
1078
|
+
printHelp();
|
|
1079
|
+
process.exit(0);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
const tracesPath = path2.resolve(config.tracesDir);
|
|
1083
|
+
if (!fs2.existsSync(tracesPath)) {
|
|
1084
|
+
fs2.mkdirSync(tracesPath, { recursive: true });
|
|
1085
|
+
}
|
|
1086
|
+
config.tracesDir = tracesPath;
|
|
1087
|
+
console.log("\nStarting AgentFlow Dashboard...\n");
|
|
1088
|
+
const dashboard = new DashboardServer(config);
|
|
1089
|
+
process.on("SIGINT", async () => {
|
|
1090
|
+
console.log("\n\u{1F6D1} Shutting down dashboard...");
|
|
1091
|
+
await dashboard.stop();
|
|
1092
|
+
process.exit(0);
|
|
1093
|
+
});
|
|
1094
|
+
process.on("SIGTERM", async () => {
|
|
1095
|
+
await dashboard.stop();
|
|
1096
|
+
process.exit(0);
|
|
1097
|
+
});
|
|
1098
|
+
try {
|
|
1099
|
+
await dashboard.start();
|
|
1100
|
+
setTimeout(() => {
|
|
1101
|
+
const stats = dashboard.getStats();
|
|
1102
|
+
const traces = dashboard.getTraces();
|
|
1103
|
+
printBanner(config, traces.length, stats);
|
|
1104
|
+
}, 1500);
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
console.error("\u274C Failed to start dashboard:", error);
|
|
1107
|
+
process.exit(1);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function printHelp() {
|
|
1111
|
+
console.log(`
|
|
1112
|
+
\u{1F4CA} AgentFlow Dashboard v${VERSION} \u2014 See your agents think.
|
|
1113
|
+
|
|
1114
|
+
Usage:
|
|
1115
|
+
agentflow-dashboard [options]
|
|
1116
|
+
npx agentflow-dashboard [options]
|
|
1117
|
+
|
|
1118
|
+
Options:
|
|
1119
|
+
-p, --port <number> Server port (default: 3000)
|
|
1120
|
+
-t, --traces <path> Traces directory (default: ./traces)
|
|
1121
|
+
-h, --host <address> Host address (default: localhost)
|
|
1122
|
+
--data-dir <path> Extra data directory for process discovery (repeatable)
|
|
1123
|
+
--cors Enable CORS headers
|
|
1124
|
+
--help Show this help message
|
|
1125
|
+
|
|
1126
|
+
Examples:
|
|
1127
|
+
agentflow-dashboard --traces ./traces --host 0.0.0.0 --cors
|
|
1128
|
+
agentflow-dashboard -p 8080 -t /var/log/agentflow
|
|
1129
|
+
agentflow-dashboard --traces ./traces --data-dir ./workers --data-dir ./cron
|
|
1130
|
+
|
|
1131
|
+
Tabs:
|
|
1132
|
+
\u{1F3AF} Graph Interactive Cytoscape.js execution graph
|
|
1133
|
+
\u23F1\uFE0F Timeline Waterfall view of node durations
|
|
1134
|
+
\u{1F4CA} Metrics Success rates, durations, node breakdown
|
|
1135
|
+
\u{1F6E0}\uFE0F Process Health PID files, systemd, workers, orphans
|
|
1136
|
+
\u26A0\uFE0F Errors Failed and hung nodes with metadata
|
|
1137
|
+
`);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// src/server.ts
|
|
1141
|
+
var import_meta = {};
|
|
1142
|
+
var __filename = (0, import_url.fileURLToPath)(import_meta.url);
|
|
1143
|
+
var __dirname = path3.dirname(__filename);
|
|
1144
|
+
var DashboardServer = class {
|
|
1145
|
+
constructor(config) {
|
|
1146
|
+
this.config = config;
|
|
1147
|
+
this.watcher = new TraceWatcher({
|
|
1148
|
+
tracesDir: config.tracesDir,
|
|
1149
|
+
dataDirs: config.dataDirs
|
|
1150
|
+
});
|
|
1151
|
+
this.stats = new AgentStats();
|
|
1152
|
+
this.setupExpress();
|
|
1153
|
+
this.setupWebSocket();
|
|
1154
|
+
this.setupTraceWatcher();
|
|
1155
|
+
}
|
|
1156
|
+
app = (0, import_express.default)();
|
|
1157
|
+
server = (0, import_http.createServer)(this.app);
|
|
1158
|
+
wss = new import_ws.WebSocketServer({ server: this.server });
|
|
1159
|
+
watcher;
|
|
1160
|
+
stats;
|
|
1161
|
+
processHealthCache = { result: null, ts: 0 };
|
|
1162
|
+
setupExpress() {
|
|
1163
|
+
if (this.config.enableCors) {
|
|
1164
|
+
this.app.use((req, res, next) => {
|
|
1165
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
1166
|
+
res.header(
|
|
1167
|
+
"Access-Control-Allow-Headers",
|
|
1168
|
+
"Origin, X-Requested-With, Content-Type, Accept"
|
|
1169
|
+
);
|
|
1170
|
+
next();
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
const publicDir = path3.join(__dirname, "../public");
|
|
1174
|
+
if (fs3.existsSync(publicDir)) {
|
|
1175
|
+
this.app.use(import_express.default.static(publicDir));
|
|
1176
|
+
}
|
|
1177
|
+
this.app.get("/api/traces", (req, res) => {
|
|
1178
|
+
try {
|
|
1179
|
+
const traces = this.watcher.getAllTraces();
|
|
1180
|
+
res.json(traces);
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
res.status(500).json({ error: "Failed to load traces" });
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
this.app.get("/api/traces/:filename", (req, res) => {
|
|
1186
|
+
try {
|
|
1187
|
+
const trace = this.watcher.getTrace(req.params.filename);
|
|
1188
|
+
if (!trace) {
|
|
1189
|
+
return res.status(404).json({ error: "Trace not found" });
|
|
1190
|
+
}
|
|
1191
|
+
res.json(trace);
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
res.status(500).json({ error: "Failed to load trace" });
|
|
1194
|
+
}
|
|
1195
|
+
});
|
|
1196
|
+
this.app.get("/api/traces/:filename/events", (req, res) => {
|
|
1197
|
+
try {
|
|
1198
|
+
const trace = this.watcher.getTrace(req.params.filename);
|
|
1199
|
+
if (!trace) {
|
|
1200
|
+
return res.status(404).json({ error: "Trace not found" });
|
|
1201
|
+
}
|
|
1202
|
+
res.json({
|
|
1203
|
+
events: trace.sessionEvents || [],
|
|
1204
|
+
tokenUsage: trace.tokenUsage || null,
|
|
1205
|
+
sourceType: trace.sourceType || "trace"
|
|
1206
|
+
});
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
res.status(500).json({ error: "Failed to load trace events" });
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
this.app.get("/api/agents", (req, res) => {
|
|
1212
|
+
try {
|
|
1213
|
+
const agents = this.stats.getAgentsList();
|
|
1214
|
+
res.json(agents);
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
res.status(500).json({ error: "Failed to load agents" });
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
this.app.get("/api/stats", (req, res) => {
|
|
1220
|
+
try {
|
|
1221
|
+
const globalStats = this.stats.getGlobalStats();
|
|
1222
|
+
res.json(globalStats);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
res.status(500).json({ error: "Failed to load statistics" });
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
this.app.get("/api/stats/:agentId", (req, res) => {
|
|
1228
|
+
try {
|
|
1229
|
+
const agentStats = this.stats.getAgentStats(req.params.agentId);
|
|
1230
|
+
if (!agentStats) {
|
|
1231
|
+
return res.status(404).json({ error: "Agent not found" });
|
|
1232
|
+
}
|
|
1233
|
+
res.json(agentStats);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
res.status(500).json({ error: "Failed to load agent statistics" });
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
this.app.get("/api/process-health", (req, res) => {
|
|
1239
|
+
try {
|
|
1240
|
+
const now = Date.now();
|
|
1241
|
+
if (this.processHealthCache.result && now - this.processHealthCache.ts < 1e4) {
|
|
1242
|
+
return res.json(this.processHealthCache.result);
|
|
1243
|
+
}
|
|
1244
|
+
const discoveryDirs = [
|
|
1245
|
+
this.config.tracesDir,
|
|
1246
|
+
path3.dirname(this.config.tracesDir),
|
|
1247
|
+
...this.config.dataDirs || []
|
|
1248
|
+
];
|
|
1249
|
+
const processConfig = (0, import_agentflow_core3.discoverProcessConfig)(discoveryDirs);
|
|
1250
|
+
if (!processConfig) {
|
|
1251
|
+
return res.json(null);
|
|
1252
|
+
}
|
|
1253
|
+
const result = (0, import_agentflow_core3.auditProcesses)(processConfig);
|
|
1254
|
+
this.processHealthCache = { result, ts: now };
|
|
1255
|
+
res.json(result);
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
res.status(500).json({ error: "Failed to audit processes" });
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
this.app.get("*", (req, res) => {
|
|
1261
|
+
const indexPath = path3.join(__dirname, "../public/index.html");
|
|
1262
|
+
if (fs3.existsSync(indexPath)) {
|
|
1263
|
+
res.sendFile(indexPath);
|
|
1264
|
+
} else {
|
|
1265
|
+
res.status(404).send("Dashboard not found - public files may not be built");
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
setupWebSocket() {
|
|
1270
|
+
this.wss.on("connection", (ws) => {
|
|
1271
|
+
console.log("Dashboard client connected");
|
|
1272
|
+
ws.send(
|
|
1273
|
+
JSON.stringify({
|
|
1274
|
+
type: "init",
|
|
1275
|
+
data: {
|
|
1276
|
+
traces: this.watcher.getAllTraces(),
|
|
1277
|
+
stats: this.stats.getGlobalStats()
|
|
1278
|
+
}
|
|
1279
|
+
})
|
|
1280
|
+
);
|
|
1281
|
+
ws.on("close", () => {
|
|
1282
|
+
console.log("Dashboard client disconnected");
|
|
1283
|
+
});
|
|
1284
|
+
ws.on("error", (error) => {
|
|
1285
|
+
console.error("WebSocket error:", error);
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
setupTraceWatcher() {
|
|
1290
|
+
this.watcher.on("trace-added", (trace) => {
|
|
1291
|
+
this.stats.processTrace(trace);
|
|
1292
|
+
this.broadcast({
|
|
1293
|
+
type: "trace-added",
|
|
1294
|
+
data: trace
|
|
1295
|
+
});
|
|
1296
|
+
});
|
|
1297
|
+
this.watcher.on("trace-updated", (trace) => {
|
|
1298
|
+
this.stats.processTrace(trace);
|
|
1299
|
+
this.broadcast({
|
|
1300
|
+
type: "trace-updated",
|
|
1301
|
+
data: trace
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
this.watcher.on("stats-updated", () => {
|
|
1305
|
+
this.broadcast({
|
|
1306
|
+
type: "stats-updated",
|
|
1307
|
+
data: this.stats.getGlobalStats()
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
broadcast(message) {
|
|
1312
|
+
this.wss.clients.forEach((client) => {
|
|
1313
|
+
if (client.readyState === 1) {
|
|
1314
|
+
client.send(JSON.stringify(message));
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
async start() {
|
|
1319
|
+
return new Promise((resolve3) => {
|
|
1320
|
+
const host = this.config.host || "localhost";
|
|
1321
|
+
this.server.listen(this.config.port, host, () => {
|
|
1322
|
+
console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
|
|
1323
|
+
console.log(`Watching traces in: ${this.config.tracesDir}`);
|
|
1324
|
+
resolve3();
|
|
1325
|
+
});
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
async stop() {
|
|
1329
|
+
return new Promise((resolve3) => {
|
|
1330
|
+
this.watcher.stop();
|
|
1331
|
+
this.server.close(() => {
|
|
1332
|
+
console.log("Dashboard server stopped");
|
|
1333
|
+
resolve3();
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
getStats() {
|
|
1338
|
+
return this.stats.getGlobalStats();
|
|
1339
|
+
}
|
|
1340
|
+
getTraces() {
|
|
1341
|
+
return this.watcher.getAllTraces();
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
if (import_meta.url === `file://${process.argv[1]}`) {
|
|
1345
|
+
startDashboard().catch(console.error);
|
|
1346
|
+
}
|
|
1347
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1348
|
+
0 && (module.exports = {
|
|
1349
|
+
DashboardServer
|
|
1350
|
+
});
|