agentflow-dashboard 0.1.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/index.js ADDED
@@ -0,0 +1,560 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import express from "express";
5
+ import { WebSocketServer } from "ws";
6
+ import { createServer } from "http";
7
+ import * as path2 from "path";
8
+ import * as fs2 from "fs";
9
+
10
+ // src/watcher.ts
11
+ import chokidar from "chokidar";
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import { EventEmitter } from "events";
15
+ var TraceWatcher = class extends EventEmitter {
16
+ watcher;
17
+ traces = /* @__PURE__ */ new Map();
18
+ tracesDir;
19
+ constructor(tracesDir) {
20
+ super();
21
+ this.tracesDir = path.resolve(tracesDir);
22
+ this.ensureTracesDir();
23
+ this.loadExistingTraces();
24
+ this.startWatching();
25
+ }
26
+ ensureTracesDir() {
27
+ if (!fs.existsSync(this.tracesDir)) {
28
+ fs.mkdirSync(this.tracesDir, { recursive: true });
29
+ console.log(`Created traces directory: ${this.tracesDir}`);
30
+ }
31
+ }
32
+ loadExistingTraces() {
33
+ try {
34
+ const files = fs.readdirSync(this.tracesDir).filter((file) => file.endsWith(".json"));
35
+ console.log(`Loading ${files.length} existing trace files...`);
36
+ for (const file of files) {
37
+ this.loadTraceFile(path.join(this.tracesDir, file));
38
+ }
39
+ console.log(`Loaded ${this.traces.size} traces`);
40
+ } catch (error) {
41
+ console.error("Error loading existing traces:", error);
42
+ }
43
+ }
44
+ loadTraceFile(filePath) {
45
+ try {
46
+ const content = fs.readFileSync(filePath, "utf8");
47
+ const trace = JSON.parse(content);
48
+ const filename = path.basename(filePath);
49
+ const stats = fs.statSync(filePath);
50
+ trace.filename = filename;
51
+ trace.lastModified = stats.mtime.getTime();
52
+ if (Array.isArray(trace.nodes)) {
53
+ const nodesMap = new Map(trace.nodes);
54
+ trace.nodes = nodesMap;
55
+ }
56
+ this.traces.set(filename, trace);
57
+ return true;
58
+ } catch (error) {
59
+ console.error(`Error loading trace file ${filePath}:`, error);
60
+ return false;
61
+ }
62
+ }
63
+ startWatching() {
64
+ this.watcher = chokidar.watch(this.tracesDir, {
65
+ ignored: /^\\./,
66
+ persistent: true,
67
+ ignoreInitial: true
68
+ });
69
+ this.watcher.on("add", (filePath) => {
70
+ if (filePath.endsWith(".json")) {
71
+ console.log(`New trace file: ${path.basename(filePath)}`);
72
+ if (this.loadTraceFile(filePath)) {
73
+ const filename = path.basename(filePath);
74
+ const trace = this.traces.get(filename);
75
+ if (trace) {
76
+ this.emit("trace-added", trace);
77
+ }
78
+ }
79
+ }
80
+ });
81
+ this.watcher.on("change", (filePath) => {
82
+ if (filePath.endsWith(".json")) {
83
+ console.log(`Trace file updated: ${path.basename(filePath)}`);
84
+ if (this.loadTraceFile(filePath)) {
85
+ const filename = path.basename(filePath);
86
+ const trace = this.traces.get(filename);
87
+ if (trace) {
88
+ this.emit("trace-updated", trace);
89
+ }
90
+ }
91
+ }
92
+ });
93
+ this.watcher.on("unlink", (filePath) => {
94
+ if (filePath.endsWith(".json")) {
95
+ const filename = path.basename(filePath);
96
+ console.log(`Trace file removed: ${filename}`);
97
+ this.traces.delete(filename);
98
+ this.emit("trace-removed", filename);
99
+ }
100
+ });
101
+ this.watcher.on("error", (error) => {
102
+ console.error("Trace watcher error:", error);
103
+ });
104
+ console.log(`Started watching traces directory: ${this.tracesDir}`);
105
+ }
106
+ getAllTraces() {
107
+ return Array.from(this.traces.values()).sort((a, b) => {
108
+ return (b.lastModified || b.timestamp) - (a.lastModified || a.timestamp);
109
+ });
110
+ }
111
+ getTrace(filename) {
112
+ return this.traces.get(filename);
113
+ }
114
+ getTracesByAgent(agentId) {
115
+ return this.getAllTraces().filter((trace) => trace.agentId === agentId);
116
+ }
117
+ getRecentTraces(limit = 50) {
118
+ return this.getAllTraces().slice(0, limit);
119
+ }
120
+ getTraceCount() {
121
+ return this.traces.size;
122
+ }
123
+ getAgentIds() {
124
+ const agentIds = /* @__PURE__ */ new Set();
125
+ for (const trace of this.traces.values()) {
126
+ agentIds.add(trace.agentId);
127
+ }
128
+ return Array.from(agentIds).sort();
129
+ }
130
+ stop() {
131
+ if (this.watcher) {
132
+ this.watcher.close();
133
+ this.watcher = void 0;
134
+ console.log("Stopped watching traces directory");
135
+ }
136
+ }
137
+ // Utility method to get trace statistics
138
+ getTraceStats() {
139
+ const total = this.traces.size;
140
+ const agentCount = this.getAgentIds().length;
141
+ const recentCount = this.getRecentTraces(24).length;
142
+ const triggers = /* @__PURE__ */ new Map();
143
+ for (const trace of this.traces.values()) {
144
+ const trigger = trace.trigger || "unknown";
145
+ triggers.set(trigger, (triggers.get(trigger) || 0) + 1);
146
+ }
147
+ return {
148
+ total,
149
+ agentCount,
150
+ recentCount,
151
+ triggers: Object.fromEntries(triggers)
152
+ };
153
+ }
154
+ };
155
+
156
+ // ../core/dist/index.js
157
+ function getFailures(graph) {
158
+ const failureStatuses = /* @__PURE__ */ new Set(["failed", "hung", "timeout"]);
159
+ return [...graph.nodes.values()].filter((node) => failureStatuses.has(node.status));
160
+ }
161
+ function getHungNodes(graph) {
162
+ return [...graph.nodes.values()].filter(
163
+ (node) => node.status === "running" && node.endTime === null
164
+ );
165
+ }
166
+ function getDuration(graph) {
167
+ const end = graph.endTime ?? Date.now();
168
+ return end - graph.startTime;
169
+ }
170
+ function getDepth(graph) {
171
+ const root = graph.nodes.get(graph.rootNodeId);
172
+ if (!root) return -1;
173
+ function dfs(node, depth) {
174
+ if (node.children.length === 0) return depth;
175
+ let maxDepth = depth;
176
+ for (const childId of node.children) {
177
+ const child = graph.nodes.get(childId);
178
+ if (!child) continue;
179
+ const childDepth = dfs(child, depth + 1);
180
+ if (childDepth > maxDepth) maxDepth = childDepth;
181
+ }
182
+ return maxDepth;
183
+ }
184
+ return dfs(root, 0);
185
+ }
186
+ function getStats(graph) {
187
+ const byStatus = {
188
+ running: 0,
189
+ completed: 0,
190
+ failed: 0,
191
+ hung: 0,
192
+ timeout: 0
193
+ };
194
+ const byType = {
195
+ agent: 0,
196
+ tool: 0,
197
+ subagent: 0,
198
+ wait: 0,
199
+ decision: 0,
200
+ custom: 0
201
+ };
202
+ let failureCount = 0;
203
+ let hungCount = 0;
204
+ for (const node of graph.nodes.values()) {
205
+ byStatus[node.status]++;
206
+ byType[node.type]++;
207
+ if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
208
+ failureCount++;
209
+ }
210
+ if (node.status === "running" && node.endTime === null) {
211
+ hungCount++;
212
+ }
213
+ }
214
+ return {
215
+ totalNodes: graph.nodes.size,
216
+ byStatus,
217
+ byType,
218
+ depth: getDepth(graph),
219
+ duration: getDuration(graph),
220
+ failureCount,
221
+ hungCount
222
+ };
223
+ }
224
+
225
+ // src/stats.ts
226
+ var AgentStats = class {
227
+ agentMetrics = /* @__PURE__ */ new Map();
228
+ processedTraces = /* @__PURE__ */ new Set();
229
+ processTrace(trace) {
230
+ const traceKey = `${trace.filename || trace.agentId}-${trace.timestamp}`;
231
+ if (this.processedTraces.has(traceKey)) {
232
+ return;
233
+ }
234
+ this.processedTraces.add(traceKey);
235
+ const agentId = trace.agentId;
236
+ let metrics = this.agentMetrics.get(agentId);
237
+ if (!metrics) {
238
+ metrics = {
239
+ agentId,
240
+ totalExecutions: 0,
241
+ successfulExecutions: 0,
242
+ failedExecutions: 0,
243
+ successRate: 0,
244
+ avgExecutionTime: 0,
245
+ lastExecution: 0,
246
+ triggers: {},
247
+ recentActivity: []
248
+ };
249
+ this.agentMetrics.set(agentId, metrics);
250
+ }
251
+ const analysis = this.analyzeExecution(trace);
252
+ metrics.totalExecutions++;
253
+ metrics.lastExecution = Math.max(metrics.lastExecution, trace.timestamp);
254
+ const trigger = trace.trigger || "unknown";
255
+ metrics.triggers[trigger] = (metrics.triggers[trigger] || 0) + 1;
256
+ if (analysis.success) {
257
+ metrics.successfulExecutions++;
258
+ } else {
259
+ metrics.failedExecutions++;
260
+ }
261
+ metrics.successRate = metrics.successfulExecutions / metrics.totalExecutions * 100;
262
+ if (analysis.executionTime > 0) {
263
+ const currentAvg = metrics.avgExecutionTime;
264
+ const count = metrics.totalExecutions;
265
+ metrics.avgExecutionTime = (currentAvg * (count - 1) + analysis.executionTime) / count;
266
+ }
267
+ metrics.recentActivity.push({
268
+ timestamp: trace.timestamp,
269
+ success: analysis.success,
270
+ executionTime: analysis.executionTime,
271
+ trigger
272
+ });
273
+ if (metrics.recentActivity.length > 100) {
274
+ metrics.recentActivity = metrics.recentActivity.slice(-100);
275
+ }
276
+ metrics.recentActivity.sort((a, b) => b.timestamp - a.timestamp);
277
+ }
278
+ analyzeExecution(trace) {
279
+ try {
280
+ const stats = getStats(trace);
281
+ const failures = getFailures(trace);
282
+ const hungNodes = getHungNodes(trace);
283
+ return {
284
+ success: failures.length === 0 && hungNodes.length === 0,
285
+ executionTime: stats.totalTime || 0,
286
+ nodeCount: stats.totalNodes || 0,
287
+ failureCount: failures.length,
288
+ hungCount: hungNodes.length
289
+ };
290
+ } catch (error) {
291
+ console.warn("Error analyzing trace with AgentFlow:", error);
292
+ const nodes = trace.nodes instanceof Map ? Array.from(trace.nodes.values()) : Array.isArray(trace.nodes) ? trace.nodes.map(([, node]) => node) : [];
293
+ const failedNodes = nodes.filter((node) => node.status === "failed").length;
294
+ const success = failedNodes === 0;
295
+ return {
296
+ success,
297
+ executionTime: 0,
298
+ nodeCount: nodes.length,
299
+ failureCount: failedNodes,
300
+ hungCount: 0
301
+ };
302
+ }
303
+ }
304
+ getAgentStats(agentId) {
305
+ return this.agentMetrics.get(agentId);
306
+ }
307
+ getAgentsList() {
308
+ return Array.from(this.agentMetrics.values()).sort((a, b) => b.lastExecution - a.lastExecution);
309
+ }
310
+ getGlobalStats() {
311
+ const agents = Array.from(this.agentMetrics.values());
312
+ const now = Date.now();
313
+ const oneHourAgo = now - 60 * 60 * 1e3;
314
+ const totalExecutions = agents.reduce((sum, agent) => sum + agent.totalExecutions, 0);
315
+ const totalSuccessful = agents.reduce((sum, agent) => sum + agent.successfulExecutions, 0);
316
+ const globalSuccessRate = totalExecutions > 0 ? totalSuccessful / totalExecutions * 100 : 0;
317
+ const activeAgents = agents.filter((agent) => agent.lastExecution > oneHourAgo).length;
318
+ const topAgents = agents.slice().sort((a, b) => b.totalExecutions - a.totalExecutions).slice(0, 10).map((agent) => ({
319
+ agentId: agent.agentId,
320
+ executionCount: agent.totalExecutions,
321
+ successRate: agent.successRate
322
+ }));
323
+ const recentActivity = [];
324
+ for (const agent of agents) {
325
+ for (const activity of agent.recentActivity.slice(0, 20)) {
326
+ recentActivity.push({
327
+ timestamp: activity.timestamp,
328
+ agentId: agent.agentId,
329
+ success: activity.success,
330
+ trigger: activity.trigger
331
+ });
332
+ }
333
+ }
334
+ recentActivity.sort((a, b) => b.timestamp - a.timestamp);
335
+ recentActivity.splice(200);
336
+ return {
337
+ totalAgents: agents.length,
338
+ totalExecutions,
339
+ globalSuccessRate,
340
+ activeAgents,
341
+ topAgents,
342
+ recentActivity
343
+ };
344
+ }
345
+ getPerformanceSummary() {
346
+ const global = this.getGlobalStats();
347
+ const agents = this.getAgentsList();
348
+ return {
349
+ overview: {
350
+ totalAgents: global.totalAgents,
351
+ totalExecutions: global.totalExecutions,
352
+ successRate: Math.round(global.globalSuccessRate * 100) / 100,
353
+ activeAgents: global.activeAgents
354
+ },
355
+ topPerformers: agents.slice(0, 5).map((agent) => ({
356
+ agentId: agent.agentId,
357
+ executions: agent.totalExecutions,
358
+ successRate: Math.round(agent.successRate * 100) / 100,
359
+ avgTime: Math.round(agent.avgExecutionTime * 100) / 100
360
+ })),
361
+ recentTrends: this.analyzeRecentTrends()
362
+ };
363
+ }
364
+ analyzeRecentTrends() {
365
+ const agents = Array.from(this.agentMetrics.values());
366
+ const now = Date.now();
367
+ const oneHourAgo = now - 60 * 60 * 1e3;
368
+ let recentExecutions = 0;
369
+ let recentFailures = 0;
370
+ for (const agent of agents) {
371
+ for (const activity of agent.recentActivity) {
372
+ if (activity.timestamp > oneHourAgo) {
373
+ recentExecutions++;
374
+ if (!activity.success) {
375
+ recentFailures++;
376
+ }
377
+ }
378
+ }
379
+ }
380
+ return {
381
+ hourlyExecutions: recentExecutions,
382
+ hourlyFailures: recentFailures,
383
+ hourlySuccessRate: recentExecutions > 0 ? Math.round((recentExecutions - recentFailures) / recentExecutions * 1e4) / 100 : 0
384
+ };
385
+ }
386
+ // Clear old data (useful for long-running dashboard instances)
387
+ cleanup() {
388
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
389
+ for (const [agentId, metrics] of this.agentMetrics.entries()) {
390
+ metrics.recentActivity = metrics.recentActivity.filter(
391
+ (activity) => activity.timestamp > cutoff
392
+ );
393
+ if (metrics.lastExecution < cutoff) {
394
+ this.agentMetrics.delete(agentId);
395
+ }
396
+ }
397
+ console.log(`Cleaned up old metrics, ${this.agentMetrics.size} agents remaining`);
398
+ }
399
+ };
400
+
401
+ // src/server.ts
402
+ var DashboardServer = class {
403
+ constructor(config) {
404
+ this.config = config;
405
+ this.watcher = new TraceWatcher(config.tracesDir);
406
+ this.stats = new AgentStats();
407
+ this.setupExpress();
408
+ this.setupWebSocket();
409
+ this.setupTraceWatcher();
410
+ }
411
+ app = express();
412
+ server = createServer(this.app);
413
+ wss = new WebSocketServer({ server: this.server });
414
+ watcher;
415
+ stats;
416
+ setupExpress() {
417
+ if (this.config.enableCors) {
418
+ this.app.use((req, res, next) => {
419
+ res.header("Access-Control-Allow-Origin", "*");
420
+ res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
421
+ next();
422
+ });
423
+ }
424
+ const publicDir = path2.join(__dirname, "../public");
425
+ if (fs2.existsSync(publicDir)) {
426
+ this.app.use(express.static(publicDir));
427
+ }
428
+ this.app.get("/api/traces", (req, res) => {
429
+ try {
430
+ const traces = this.watcher.getAllTraces();
431
+ res.json(traces);
432
+ } catch (error) {
433
+ res.status(500).json({ error: "Failed to load traces" });
434
+ }
435
+ });
436
+ this.app.get("/api/traces/:filename", (req, res) => {
437
+ try {
438
+ const trace = this.watcher.getTrace(req.params.filename);
439
+ if (!trace) {
440
+ return res.status(404).json({ error: "Trace not found" });
441
+ }
442
+ res.json(trace);
443
+ } catch (error) {
444
+ res.status(500).json({ error: "Failed to load trace" });
445
+ }
446
+ });
447
+ this.app.get("/api/agents", (req, res) => {
448
+ try {
449
+ const agents = this.stats.getAgentsList();
450
+ res.json(agents);
451
+ } catch (error) {
452
+ res.status(500).json({ error: "Failed to load agents" });
453
+ }
454
+ });
455
+ this.app.get("/api/stats", (req, res) => {
456
+ try {
457
+ const globalStats = this.stats.getGlobalStats();
458
+ res.json(globalStats);
459
+ } catch (error) {
460
+ res.status(500).json({ error: "Failed to load statistics" });
461
+ }
462
+ });
463
+ this.app.get("/api/stats/:agentId", (req, res) => {
464
+ try {
465
+ const agentStats = this.stats.getAgentStats(req.params.agentId);
466
+ if (!agentStats) {
467
+ return res.status(404).json({ error: "Agent not found" });
468
+ }
469
+ res.json(agentStats);
470
+ } catch (error) {
471
+ res.status(500).json({ error: "Failed to load agent statistics" });
472
+ }
473
+ });
474
+ this.app.get("*", (req, res) => {
475
+ const indexPath = path2.join(__dirname, "../public/index.html");
476
+ if (fs2.existsSync(indexPath)) {
477
+ res.sendFile(indexPath);
478
+ } else {
479
+ res.status(404).send("Dashboard not found - public files may not be built");
480
+ }
481
+ });
482
+ }
483
+ setupWebSocket() {
484
+ this.wss.on("connection", (ws) => {
485
+ console.log("Dashboard client connected");
486
+ ws.send(JSON.stringify({
487
+ type: "init",
488
+ data: {
489
+ traces: this.watcher.getAllTraces(),
490
+ stats: this.stats.getGlobalStats()
491
+ }
492
+ }));
493
+ ws.on("close", () => {
494
+ console.log("Dashboard client disconnected");
495
+ });
496
+ ws.on("error", (error) => {
497
+ console.error("WebSocket error:", error);
498
+ });
499
+ });
500
+ }
501
+ setupTraceWatcher() {
502
+ this.watcher.on("trace-added", (trace) => {
503
+ this.stats.processTrace(trace);
504
+ this.broadcast({
505
+ type: "trace-added",
506
+ data: trace
507
+ });
508
+ });
509
+ this.watcher.on("trace-updated", (trace) => {
510
+ this.stats.processTrace(trace);
511
+ this.broadcast({
512
+ type: "trace-updated",
513
+ data: trace
514
+ });
515
+ });
516
+ this.watcher.on("stats-updated", () => {
517
+ this.broadcast({
518
+ type: "stats-updated",
519
+ data: this.stats.getGlobalStats()
520
+ });
521
+ });
522
+ }
523
+ broadcast(message) {
524
+ this.wss.clients.forEach((client) => {
525
+ if (client.readyState === 1) {
526
+ client.send(JSON.stringify(message));
527
+ }
528
+ });
529
+ }
530
+ async start() {
531
+ return new Promise((resolve2) => {
532
+ const host = this.config.host || "localhost";
533
+ this.server.listen(this.config.port, host, () => {
534
+ console.log(`AgentFlow Dashboard running at http://${host}:${this.config.port}`);
535
+ console.log(`Watching traces in: ${this.config.tracesDir}`);
536
+ resolve2();
537
+ });
538
+ });
539
+ }
540
+ async stop() {
541
+ return new Promise((resolve2) => {
542
+ this.watcher.stop();
543
+ this.server.close(() => {
544
+ console.log("Dashboard server stopped");
545
+ resolve2();
546
+ });
547
+ });
548
+ }
549
+ getStats() {
550
+ return this.stats.getGlobalStats();
551
+ }
552
+ getTraces() {
553
+ return this.watcher.getAllTraces();
554
+ }
555
+ };
556
+ export {
557
+ AgentStats,
558
+ DashboardServer,
559
+ TraceWatcher
560
+ };