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.
@@ -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
+ };