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