agentflow-core 0.3.2 → 0.4.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.
@@ -1,1287 +1,1685 @@
1
- // src/graph-builder.ts
2
- import { randomUUID } from "crypto";
3
- function deepFreeze(obj) {
4
- if (obj === null || typeof obj !== "object") return obj;
5
- if (obj instanceof Map) {
6
- Object.freeze(obj);
7
- for (const value of obj.values()) {
8
- deepFreeze(value);
1
+ import {
2
+ graphToJson,
3
+ loadGraph
4
+ } from "./chunk-DY7YHFIB.js";
5
+
6
+ // src/graph-query.ts
7
+ function getNode(graph, nodeId) {
8
+ return graph.nodes.get(nodeId);
9
+ }
10
+ function getChildren(graph, nodeId) {
11
+ const node = graph.nodes.get(nodeId);
12
+ if (!node) return [];
13
+ const result = [];
14
+ for (const childId of node.children) {
15
+ const child = graph.nodes.get(childId);
16
+ if (child) result.push(child);
17
+ }
18
+ return result;
19
+ }
20
+ function getParent(graph, nodeId) {
21
+ const node = graph.nodes.get(nodeId);
22
+ if (!node || node.parentId === null) return void 0;
23
+ return graph.nodes.get(node.parentId);
24
+ }
25
+ function getFailures(graph) {
26
+ const failureStatuses = /* @__PURE__ */ new Set(["failed", "hung", "timeout"]);
27
+ return [...graph.nodes.values()].filter((node) => failureStatuses.has(node.status));
28
+ }
29
+ function getHungNodes(graph) {
30
+ return [...graph.nodes.values()].filter(
31
+ (node) => node.status === "running" && node.endTime === null
32
+ );
33
+ }
34
+ function getCriticalPath(graph) {
35
+ const root = graph.nodes.get(graph.rootNodeId);
36
+ if (!root) return [];
37
+ function nodeDuration2(node) {
38
+ const end = node.endTime ?? Date.now();
39
+ return end - node.startTime;
40
+ }
41
+ function dfs(node) {
42
+ if (node.children.length === 0) {
43
+ return { duration: nodeDuration2(node), path: [node] };
9
44
  }
10
- return obj;
45
+ let bestChild = { duration: -1, path: [] };
46
+ for (const childId of node.children) {
47
+ const child = graph.nodes.get(childId);
48
+ if (!child) continue;
49
+ const result = dfs(child);
50
+ if (result.duration > bestChild.duration) {
51
+ bestChild = result;
52
+ }
53
+ }
54
+ return {
55
+ duration: nodeDuration2(node) + bestChild.duration,
56
+ path: [node, ...bestChild.path]
57
+ };
11
58
  }
12
- Object.freeze(obj);
13
- const record = obj;
14
- for (const key of Object.keys(record)) {
15
- const value = record[key];
16
- if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
17
- deepFreeze(value);
59
+ return dfs(root).path;
60
+ }
61
+ function findWaitingOn(graph, nodeId) {
62
+ const results = [];
63
+ for (const edge of graph.edges) {
64
+ if (edge.from === nodeId && edge.type === "waited_on") {
65
+ const node = graph.nodes.get(edge.to);
66
+ if (node) results.push(node);
18
67
  }
19
68
  }
20
- return obj;
69
+ return results;
21
70
  }
22
- function createCounterIdGenerator() {
23
- let counter = 0;
24
- return () => {
25
- counter++;
26
- return `node_${String(counter).padStart(3, "0")}`;
27
- };
71
+ function getSubtree(graph, nodeId) {
72
+ const startNode = graph.nodes.get(nodeId);
73
+ if (!startNode) return [];
74
+ const result = [];
75
+ const queue = [...startNode.children];
76
+ while (queue.length > 0) {
77
+ const currentId = queue.shift();
78
+ if (currentId === void 0) break;
79
+ const current = graph.nodes.get(currentId);
80
+ if (!current) continue;
81
+ result.push(current);
82
+ queue.push(...current.children);
83
+ }
84
+ return result;
28
85
  }
29
- function createGraphBuilder(config) {
30
- const generateId = config?.idGenerator ?? createCounterIdGenerator();
31
- const agentId = config?.agentId ?? "unknown";
32
- const trigger = config?.trigger ?? "manual";
33
- const spanId = randomUUID();
34
- const traceId = config?.traceId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_TRACE_ID : void 0) ?? randomUUID();
35
- const parentSpanId = config?.parentSpanId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_PARENT_SPAN_ID : void 0) ?? null;
36
- const graphId = generateId();
37
- const startTime = Date.now();
38
- const nodes = /* @__PURE__ */ new Map();
39
- const edges = [];
40
- const events = [];
41
- const parentStack = [];
42
- let rootNodeId = null;
43
- let built = false;
44
- function assertNotBuilt() {
45
- if (built) {
46
- throw new Error("GraphBuilder: cannot mutate after build() has been called");
86
+ function getDuration(graph) {
87
+ const end = graph.endTime ?? Date.now();
88
+ return end - graph.startTime;
89
+ }
90
+ function getDepth(graph) {
91
+ const root = graph.nodes.get(graph.rootNodeId);
92
+ if (!root) return -1;
93
+ function dfs(node, depth) {
94
+ if (node.children.length === 0) return depth;
95
+ let maxDepth = depth;
96
+ for (const childId of node.children) {
97
+ const child = graph.nodes.get(childId);
98
+ if (!child) continue;
99
+ const childDepth = dfs(child, depth + 1);
100
+ if (childDepth > maxDepth) maxDepth = childDepth;
47
101
  }
102
+ return maxDepth;
48
103
  }
49
- function getNode2(nodeId) {
50
- const node = nodes.get(nodeId);
51
- if (!node) {
52
- throw new Error(`GraphBuilder: node "${nodeId}" does not exist`);
104
+ return dfs(root, 0);
105
+ }
106
+ function getStats(graph) {
107
+ const byStatus = {
108
+ running: 0,
109
+ completed: 0,
110
+ failed: 0,
111
+ hung: 0,
112
+ timeout: 0
113
+ };
114
+ const byType = {
115
+ agent: 0,
116
+ tool: 0,
117
+ subagent: 0,
118
+ wait: 0,
119
+ decision: 0,
120
+ custom: 0
121
+ };
122
+ let failureCount = 0;
123
+ let hungCount = 0;
124
+ for (const node of graph.nodes.values()) {
125
+ byStatus[node.status]++;
126
+ byType[node.type]++;
127
+ if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
128
+ failureCount++;
129
+ }
130
+ if (node.status === "running" && node.endTime === null) {
131
+ hungCount++;
53
132
  }
54
- return node;
55
133
  }
56
- function recordEvent(nodeId, eventType, data = {}) {
57
- events.push({
58
- timestamp: Date.now(),
59
- eventType,
60
- nodeId,
61
- data
62
- });
134
+ return {
135
+ totalNodes: graph.nodes.size,
136
+ byStatus,
137
+ byType,
138
+ depth: getDepth(graph),
139
+ duration: getDuration(graph),
140
+ failureCount,
141
+ hungCount
142
+ };
143
+ }
144
+
145
+ // src/graph-stitch.ts
146
+ function groupByTraceId(graphs) {
147
+ const groups = /* @__PURE__ */ new Map();
148
+ for (const g of graphs) {
149
+ if (!g.traceId) continue;
150
+ const arr = groups.get(g.traceId) ?? [];
151
+ arr.push(g);
152
+ groups.set(g.traceId, arr);
63
153
  }
64
- function buildGraph() {
65
- if (rootNodeId === null) {
66
- throw new Error("GraphBuilder: cannot build a graph with no nodes");
154
+ return groups;
155
+ }
156
+ function stitchTrace(graphs) {
157
+ if (graphs.length === 0) throw new Error("No graphs to stitch");
158
+ const traceId = graphs[0].traceId ?? "";
159
+ const graphsBySpan = /* @__PURE__ */ new Map();
160
+ const childMap = /* @__PURE__ */ new Map();
161
+ let rootGraph = null;
162
+ for (const g of graphs) {
163
+ if (g.spanId) graphsBySpan.set(g.spanId, g);
164
+ if (!g.parentSpanId) {
165
+ if (!rootGraph || g.startTime < rootGraph.startTime) rootGraph = g;
67
166
  }
68
- let graphStatus = "completed";
69
- for (const node of nodes.values()) {
70
- if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
71
- graphStatus = "failed";
72
- break;
73
- }
74
- if (node.status === "running") {
75
- graphStatus = "running";
76
- }
167
+ if (g.parentSpanId) {
168
+ const siblings = childMap.get(g.parentSpanId) ?? [];
169
+ if (g.spanId) siblings.push(g.spanId);
170
+ childMap.set(g.parentSpanId, siblings);
77
171
  }
78
- const endTime = graphStatus === "running" ? null : Date.now();
79
- const frozenNodes = new Map(
80
- [...nodes.entries()].map(([id, mNode]) => [
81
- id,
82
- {
83
- id: mNode.id,
84
- type: mNode.type,
85
- name: mNode.name,
86
- startTime: mNode.startTime,
87
- endTime: mNode.endTime,
88
- status: mNode.status,
89
- parentId: mNode.parentId,
90
- children: [...mNode.children],
91
- metadata: { ...mNode.metadata },
92
- state: { ...mNode.state }
93
- }
94
- ])
95
- );
96
- const graph = {
97
- id: graphId,
98
- rootNodeId,
99
- nodes: frozenNodes,
100
- edges: [...edges],
101
- startTime,
102
- endTime,
103
- status: graphStatus,
104
- trigger,
105
- agentId,
106
- events: [...events],
107
- traceId,
108
- spanId,
109
- parentSpanId
110
- };
111
- return deepFreeze(graph);
112
172
  }
113
- const builder = {
114
- get graphId() {
115
- return graphId;
116
- },
117
- get traceContext() {
118
- return { traceId, spanId };
119
- },
120
- startNode(opts) {
121
- assertNotBuilt();
122
- const id = generateId();
123
- const parentId = opts.parentId ?? parentStack[parentStack.length - 1] ?? null;
124
- if (parentId !== null && !nodes.has(parentId)) {
125
- throw new Error(`GraphBuilder: parent node "${parentId}" does not exist`);
126
- }
127
- const node = {
128
- id,
129
- type: opts.type,
130
- name: opts.name,
131
- startTime: Date.now(),
132
- endTime: null,
133
- status: "running",
134
- parentId,
135
- children: [],
136
- metadata: opts.metadata ? { ...opts.metadata } : {},
137
- state: {}
138
- };
139
- nodes.set(id, node);
140
- if (parentId !== null) {
141
- const parent = nodes.get(parentId);
142
- if (parent) {
143
- parent.children.push(id);
144
- }
145
- edges.push({ from: parentId, to: id, type: "spawned" });
146
- }
147
- if (rootNodeId === null) {
148
- rootNodeId = id;
149
- }
150
- recordEvent(id, "agent_start", { type: opts.type, name: opts.name });
151
- return id;
152
- },
153
- endNode(nodeId, status = "completed") {
154
- assertNotBuilt();
155
- const node = getNode2(nodeId);
156
- if (node.endTime !== null) {
157
- throw new Error(
158
- `GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
159
- );
160
- }
161
- node.endTime = Date.now();
162
- node.status = status;
163
- recordEvent(nodeId, "agent_end", { status });
164
- },
165
- failNode(nodeId, error) {
166
- assertNotBuilt();
167
- const node = getNode2(nodeId);
168
- if (node.endTime !== null) {
169
- throw new Error(
170
- `GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
171
- );
172
- }
173
- const errorMessage = error instanceof Error ? error.message : error;
174
- const errorStack = error instanceof Error ? error.stack : void 0;
175
- node.endTime = Date.now();
176
- node.status = "failed";
177
- node.metadata.error = errorMessage;
178
- if (errorStack) {
179
- node.metadata.errorStack = errorStack;
180
- }
181
- recordEvent(nodeId, "tool_error", { error: errorMessage });
182
- },
183
- addEdge(from, to, type) {
184
- assertNotBuilt();
185
- getNode2(from);
186
- getNode2(to);
187
- edges.push({ from, to, type });
188
- recordEvent(from, "custom", { to, type, action: "edge_add" });
189
- },
190
- pushEvent(event) {
191
- assertNotBuilt();
192
- getNode2(event.nodeId);
193
- events.push({
194
- ...event,
195
- timestamp: Date.now()
196
- });
197
- },
198
- updateState(nodeId, state) {
199
- assertNotBuilt();
200
- const node = getNode2(nodeId);
201
- Object.assign(node.state, state);
202
- recordEvent(nodeId, "custom", { action: "state_update", ...state });
203
- },
204
- withParent(parentId, fn) {
205
- assertNotBuilt();
206
- getNode2(parentId);
207
- parentStack.push(parentId);
208
- try {
209
- return fn();
210
- } finally {
211
- parentStack.pop();
212
- }
213
- },
214
- getSnapshot() {
215
- return buildGraph();
216
- },
217
- build() {
218
- assertNotBuilt();
219
- const graph = buildGraph();
220
- built = true;
221
- return graph;
222
- }
223
- };
224
- return builder;
225
- }
226
-
227
- // src/loader.ts
228
- function toNodesMap(raw) {
229
- if (raw instanceof Map) return raw;
230
- if (Array.isArray(raw)) {
231
- return new Map(raw);
232
- }
233
- if (raw !== null && typeof raw === "object") {
234
- return new Map(Object.entries(raw));
173
+ if (!rootGraph) rootGraph = graphs[0];
174
+ let status = "completed";
175
+ let endTime = 0;
176
+ let startTime = Infinity;
177
+ for (const g of graphs) {
178
+ startTime = Math.min(startTime, g.startTime);
179
+ if (g.status === "failed") status = "failed";
180
+ else if (g.status === "running" && status !== "failed") status = "running";
181
+ if (g.endTime === null) endTime = null;
182
+ else if (endTime !== null) endTime = Math.max(endTime, g.endTime);
235
183
  }
236
- return /* @__PURE__ */ new Map();
237
- }
238
- function loadGraph(input) {
239
- const raw = typeof input === "string" ? JSON.parse(input) : input;
240
- const nodes = toNodesMap(raw.nodes);
241
- return {
242
- id: raw.id ?? "",
243
- rootNodeId: raw.rootNodeId ?? raw.rootId ?? "",
244
- nodes,
245
- edges: raw.edges ?? [],
246
- startTime: raw.startTime ?? 0,
247
- endTime: raw.endTime ?? null,
248
- status: raw.status ?? "completed",
249
- trigger: raw.trigger ?? "unknown",
250
- agentId: raw.agentId ?? "unknown",
251
- events: raw.events ?? [],
252
- traceId: raw.traceId,
253
- spanId: raw.spanId,
254
- parentSpanId: raw.parentSpanId
255
- };
184
+ const frozenChildMap = /* @__PURE__ */ new Map();
185
+ for (const [k, v] of childMap) frozenChildMap.set(k, Object.freeze([...v]));
186
+ return Object.freeze({
187
+ traceId,
188
+ graphs: graphsBySpan,
189
+ rootGraph,
190
+ childMap: frozenChildMap,
191
+ startTime,
192
+ endTime,
193
+ status
194
+ });
256
195
  }
257
- function graphToJson(graph) {
258
- const nodesObj = {};
259
- for (const [id, node] of graph.nodes) {
260
- nodesObj[id] = node;
196
+ function getTraceTree(trace) {
197
+ const result = [];
198
+ function walk(spanId) {
199
+ const graph = trace.graphs.get(spanId);
200
+ if (graph) result.push(graph);
201
+ const children = trace.childMap.get(spanId) ?? [];
202
+ for (const childSpan of children) walk(childSpan);
261
203
  }
262
- return {
263
- id: graph.id,
264
- rootNodeId: graph.rootNodeId,
265
- nodes: nodesObj,
266
- edges: graph.edges,
267
- startTime: graph.startTime,
268
- endTime: graph.endTime,
269
- status: graph.status,
270
- trigger: graph.trigger,
271
- agentId: graph.agentId,
272
- events: graph.events,
273
- traceId: graph.traceId,
274
- spanId: graph.spanId,
275
- parentSpanId: graph.parentSpanId
276
- };
204
+ if (trace.rootGraph.spanId) walk(trace.rootGraph.spanId);
205
+ else result.push(trace.rootGraph);
206
+ return result;
277
207
  }
278
208
 
279
- // src/runner.ts
280
- import { spawnSync } from "child_process";
281
- import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "fs";
209
+ // src/live.ts
210
+ import { existsSync, readdirSync, readFileSync, statSync, watch } from "fs";
282
211
  import { basename, join, resolve } from "path";
283
- function globToRegex(pattern) {
284
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
285
- return new RegExp(`^${escaped}$`);
212
+ var C = {
213
+ reset: "\x1B[0m",
214
+ bold: "\x1B[1m",
215
+ dim: "\x1B[90m",
216
+ under: "\x1B[4m",
217
+ red: "\x1B[31m",
218
+ green: "\x1B[32m",
219
+ yellow: "\x1B[33m",
220
+ blue: "\x1B[34m",
221
+ magenta: "\x1B[35m",
222
+ cyan: "\x1B[36m",
223
+ white: "\x1B[37m"
224
+ };
225
+ function parseArgs(argv) {
226
+ const config = { dirs: [], refreshMs: 3e3, recursive: false };
227
+ const args = argv.slice(0);
228
+ if (args[0] === "live") args.shift();
229
+ let i = 0;
230
+ while (i < args.length) {
231
+ const arg = args[i];
232
+ if (arg === "--help" || arg === "-h") {
233
+ printUsage();
234
+ process.exit(0);
235
+ } else if (arg === "--refresh" || arg === "-r") {
236
+ i++;
237
+ const v = parseInt(args[i] ?? "", 10);
238
+ if (!isNaN(v) && v > 0) config.refreshMs = v * 1e3;
239
+ i++;
240
+ } else if (arg === "--recursive" || arg === "-R") {
241
+ config.recursive = true;
242
+ i++;
243
+ } else if (!arg.startsWith("-")) {
244
+ config.dirs.push(resolve(arg));
245
+ i++;
246
+ } else {
247
+ i++;
248
+ }
249
+ }
250
+ if (config.dirs.length === 0) config.dirs.push(resolve("."));
251
+ return config;
286
252
  }
287
- function snapshotDir(dir, patterns) {
288
- const result = /* @__PURE__ */ new Map();
289
- if (!existsSync(dir)) return result;
290
- for (const entry of readdirSync(dir)) {
291
- if (!patterns.some((re) => re.test(entry))) continue;
292
- const full = join(dir, entry);
253
+ function printUsage() {
254
+ console.log(
255
+ `
256
+ AgentFlow Live Monitor \u2014 real-time terminal dashboard for agent systems.
257
+
258
+ Auto-detects agent traces, state files, job schedulers, and session logs
259
+ from any JSON/JSONL files in the watched directories.
260
+
261
+ Usage:
262
+ agentflow live [dir...] [options]
263
+
264
+ Arguments:
265
+ dir One or more directories to watch (default: .)
266
+
267
+ Options:
268
+ -r, --refresh <secs> Refresh interval in seconds (default: 3)
269
+ -R, --recursive Scan subdirectories (1 level deep)
270
+ -h, --help Show this help message
271
+
272
+ Examples:
273
+ agentflow live ./data
274
+ agentflow live ./traces ./cron ./workers --refresh 5
275
+ agentflow live /var/lib/myagent -R
276
+ `.trim()
277
+ );
278
+ }
279
+ function scanFiles(dirs, recursive) {
280
+ const results = [];
281
+ const seen = /* @__PURE__ */ new Set();
282
+ function scanDir(d, topLevel) {
293
283
  try {
294
- const stat = statSync(full);
295
- if (stat.isFile()) {
296
- result.set(full, stat.mtimeMs);
284
+ for (const f of readdirSync(d)) {
285
+ if (f.startsWith(".")) continue;
286
+ const fp = join(d, f);
287
+ if (seen.has(fp)) continue;
288
+ let stat;
289
+ try {
290
+ stat = statSync(fp);
291
+ } catch {
292
+ continue;
293
+ }
294
+ if (stat.isDirectory() && recursive && topLevel) {
295
+ scanDir(fp, false);
296
+ continue;
297
+ }
298
+ if (!stat.isFile()) continue;
299
+ if (f.endsWith(".json")) {
300
+ seen.add(fp);
301
+ results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".json" });
302
+ } else if (f.endsWith(".jsonl")) {
303
+ seen.add(fp);
304
+ results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".jsonl" });
305
+ }
297
306
  }
298
307
  } catch {
299
308
  }
300
309
  }
301
- return result;
310
+ for (const dir of dirs) scanDir(dir, true);
311
+ results.sort((a, b) => b.mtime - a.mtime);
312
+ return results;
302
313
  }
303
- function agentIdFromFilename(filePath) {
304
- const base = basename(filePath, ".json");
305
- const cleaned = base.replace(/-state$/, "");
306
- return `alfred-${cleaned}`;
314
+ function safeReadJson(fp) {
315
+ try {
316
+ return JSON.parse(readFileSync(fp, "utf8"));
317
+ } catch {
318
+ return null;
319
+ }
307
320
  }
308
- function deriveAgentId(command) {
309
- return "orchestrator";
321
+ function nameFromFile(filename) {
322
+ return basename(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
310
323
  }
311
- function fileTimestamp() {
312
- return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
324
+ function normalizeStatus(val) {
325
+ if (typeof val !== "string") return "unknown";
326
+ const s = val.toLowerCase();
327
+ if (["ok", "success", "completed", "done", "passed", "healthy", "good"].includes(s)) return "ok";
328
+ if (["error", "failed", "failure", "crashed", "unhealthy", "bad", "timeout"].includes(s))
329
+ return "error";
330
+ if (["running", "active", "in_progress", "started", "pending", "processing"].includes(s))
331
+ return "running";
332
+ return "unknown";
313
333
  }
314
- async function runTraced(config) {
315
- const {
316
- command,
317
- agentId = deriveAgentId(command),
318
- trigger = "cli",
319
- tracesDir = "./traces",
320
- watchDirs = [],
321
- watchPatterns = ["*.json"]
322
- } = config;
323
- if (command.length === 0) {
324
- throw new Error("runTraced: command must not be empty");
325
- }
326
- const resolvedTracesDir = resolve(tracesDir);
327
- const patterns = watchPatterns.map(globToRegex);
328
- const orchestrator = createGraphBuilder({ agentId, trigger });
329
- const { traceId, spanId } = orchestrator.traceContext;
330
- const beforeSnapshots = /* @__PURE__ */ new Map();
331
- for (const dir of watchDirs) {
332
- beforeSnapshots.set(dir, snapshotDir(dir, patterns));
333
- }
334
- const rootId = orchestrator.startNode({ type: "agent", name: agentId });
335
- const dispatchId = orchestrator.startNode({
336
- type: "tool",
337
- name: "dispatch-command",
338
- parentId: rootId
339
- });
340
- orchestrator.updateState(dispatchId, { command: command.join(" ") });
341
- const monitorId = orchestrator.startNode({
342
- type: "tool",
343
- name: "state-monitor",
344
- parentId: rootId
345
- });
346
- orchestrator.updateState(monitorId, {
347
- watchDirs,
348
- watchPatterns
349
- });
350
- const startMs = Date.now();
351
- const execCmd = command[0] ?? "";
352
- const execArgs = command.slice(1);
353
- process.env.AGENTFLOW_TRACE_ID = traceId;
354
- process.env.AGENTFLOW_PARENT_SPAN_ID = spanId;
355
- const result = spawnSync(execCmd, execArgs, { stdio: "inherit" });
356
- delete process.env.AGENTFLOW_TRACE_ID;
357
- delete process.env.AGENTFLOW_PARENT_SPAN_ID;
358
- const exitCode = result.status ?? 1;
359
- const duration = (Date.now() - startMs) / 1e3;
360
- const stateChanges = [];
361
- for (const dir of watchDirs) {
362
- const before = beforeSnapshots.get(dir) ?? /* @__PURE__ */ new Map();
363
- const after = snapshotDir(dir, patterns);
364
- for (const [filePath, mtime] of after) {
365
- const prevMtime = before.get(filePath);
366
- if (prevMtime === void 0 || mtime > prevMtime) {
367
- stateChanges.push(filePath);
334
+ function findStatus(obj) {
335
+ for (const key of ["status", "state", "lastRunStatus", "lastStatus", "health", "result"]) {
336
+ if (key in obj) {
337
+ const val = obj[key];
338
+ if (typeof val === "string") return normalizeStatus(val);
339
+ if (typeof val === "object" && val !== null && "status" in val) {
340
+ return normalizeStatus(val.status);
368
341
  }
369
342
  }
370
343
  }
371
- orchestrator.updateState(monitorId, { stateChanges });
372
- orchestrator.endNode(monitorId);
373
- if (exitCode === 0) {
374
- orchestrator.endNode(dispatchId);
375
- } else {
376
- orchestrator.failNode(dispatchId, `Command exited with code ${exitCode}`);
377
- }
378
- orchestrator.updateState(rootId, {
379
- exitCode,
380
- duration,
381
- stateChanges
382
- });
383
- if (exitCode === 0) {
384
- orchestrator.endNode(rootId);
385
- } else {
386
- orchestrator.failNode(rootId, `Command exited with code ${exitCode}`);
387
- }
388
- const orchestratorGraph = orchestrator.build();
389
- const allGraphs = [orchestratorGraph];
390
- for (const filePath of stateChanges) {
391
- const childAgentId = agentIdFromFilename(filePath);
392
- const childBuilder = createGraphBuilder({
393
- agentId: childAgentId,
394
- trigger: "state-change",
395
- traceId,
396
- parentSpanId: spanId
397
- });
398
- const childRootId = childBuilder.startNode({
399
- type: "agent",
400
- name: childAgentId
401
- });
402
- childBuilder.updateState(childRootId, {
403
- stateFile: filePath,
404
- detectedBy: "runner-state-monitor"
405
- });
406
- childBuilder.endNode(childRootId);
407
- allGraphs.push(childBuilder.build());
344
+ return "unknown";
345
+ }
346
+ function findTimestamp(obj) {
347
+ for (const key of [
348
+ "ts",
349
+ "timestamp",
350
+ "lastRunAtMs",
351
+ "last_run",
352
+ "lastExecution",
353
+ "updated_at",
354
+ "started_at",
355
+ "endTime",
356
+ "startTime"
357
+ ]) {
358
+ const val = obj[key];
359
+ if (typeof val === "number") return val > 1e12 ? val : val * 1e3;
360
+ if (typeof val === "string") {
361
+ const d = Date.parse(val);
362
+ if (!isNaN(d)) return d;
363
+ }
408
364
  }
409
- if (!existsSync(resolvedTracesDir)) {
410
- mkdirSync(resolvedTracesDir, { recursive: true });
365
+ return 0;
366
+ }
367
+ function extractDetail(obj) {
368
+ const parts = [];
369
+ for (const key of [
370
+ "summary",
371
+ "message",
372
+ "description",
373
+ "lastError",
374
+ "error",
375
+ "name",
376
+ "jobId",
377
+ "id"
378
+ ]) {
379
+ const val = obj[key];
380
+ if (typeof val === "string" && val.length > 0 && val.length < 200) {
381
+ parts.push(val.slice(0, 80));
382
+ break;
383
+ }
411
384
  }
412
- const ts = fileTimestamp();
413
- const tracePaths = [];
414
- for (const graph of allGraphs) {
415
- const filename = `${graph.agentId}-${ts}.json`;
416
- const outPath = join(resolvedTracesDir, filename);
417
- writeFileSync(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
418
- tracePaths.push(outPath);
385
+ for (const key of ["totalExecutions", "runs", "count", "processed", "consecutiveErrors"]) {
386
+ const val = obj[key];
387
+ if (typeof val === "number") {
388
+ parts.push(`${key}: ${val}`);
389
+ break;
390
+ }
419
391
  }
420
- return {
421
- exitCode,
422
- traceId,
423
- spanId,
424
- tracePaths,
425
- stateChanges,
426
- duration
427
- };
428
- }
429
-
430
- // src/graph-query.ts
431
- function getNode(graph, nodeId) {
432
- return graph.nodes.get(nodeId);
392
+ return parts.join(" | ") || "";
433
393
  }
434
- function getChildren(graph, nodeId) {
435
- const node = graph.nodes.get(nodeId);
436
- if (!node) return [];
437
- const result = [];
438
- for (const childId of node.children) {
439
- const child = graph.nodes.get(childId);
440
- if (child) result.push(child);
394
+ function tryLoadTrace(fp, raw) {
395
+ if (typeof raw !== "object" || raw === null) return null;
396
+ const obj = raw;
397
+ if (!("nodes" in obj)) return null;
398
+ if (!("agentId" in obj) && !("rootNodeId" in obj) && !("rootId" in obj)) return null;
399
+ try {
400
+ return loadGraph(obj);
401
+ } catch {
402
+ return null;
441
403
  }
442
- return result;
443
- }
444
- function getParent(graph, nodeId) {
445
- const node = graph.nodes.get(nodeId);
446
- if (!node || node.parentId === null) return void 0;
447
- return graph.nodes.get(node.parentId);
448
- }
449
- function getFailures(graph) {
450
- const failureStatuses = /* @__PURE__ */ new Set(["failed", "hung", "timeout"]);
451
- return [...graph.nodes.values()].filter((node) => failureStatuses.has(node.status));
452
- }
453
- function getHungNodes(graph) {
454
- return [...graph.nodes.values()].filter(
455
- (node) => node.status === "running" && node.endTime === null
456
- );
457
404
  }
458
- function getCriticalPath(graph) {
459
- const root = graph.nodes.get(graph.rootNodeId);
460
- if (!root) return [];
461
- function nodeDuration(node) {
462
- const end = node.endTime ?? Date.now();
463
- return end - node.startTime;
405
+ function processJsonFile(file) {
406
+ const raw = safeReadJson(file.path);
407
+ if (raw === null) return [];
408
+ const records = [];
409
+ const trace = tryLoadTrace(file.path, raw);
410
+ if (trace) {
411
+ try {
412
+ const fails = getFailures(trace);
413
+ const hung = getHungNodes(trace);
414
+ const stats = getStats(trace);
415
+ records.push({
416
+ id: trace.agentId,
417
+ source: "trace",
418
+ status: fails.length === 0 && hung.length === 0 ? "ok" : "error",
419
+ lastActive: file.mtime,
420
+ detail: `${stats.totalNodes} nodes [${trace.trigger}]`,
421
+ file: file.filename,
422
+ traceData: trace
423
+ });
424
+ } catch {
425
+ }
426
+ return records;
464
427
  }
465
- function dfs(node) {
466
- if (node.children.length === 0) {
467
- return { duration: nodeDuration(node), path: [node] };
428
+ if (typeof raw !== "object") return records;
429
+ const arr = Array.isArray(raw) ? raw : Array.isArray(raw.jobs) ? raw.jobs : Array.isArray(raw.tasks) ? raw.tasks : Array.isArray(raw.items) ? raw.items : null;
430
+ if (arr && arr.length > 0 && typeof arr[0] === "object" && arr[0] !== null) {
431
+ for (const item of arr.slice(0, 50)) {
432
+ const name = item.name ?? item.id ?? item.jobId ?? item.agentId;
433
+ if (!name) continue;
434
+ const state = typeof item.state === "object" && item.state !== null ? item.state : item;
435
+ const status2 = findStatus(state);
436
+ const ts2 = findTimestamp(state) || file.mtime;
437
+ const detail2 = extractDetail(state);
438
+ records.push({
439
+ id: String(name),
440
+ source: "jobs",
441
+ status: status2,
442
+ lastActive: ts2,
443
+ detail: detail2,
444
+ file: file.filename
445
+ });
468
446
  }
469
- let bestChild = { duration: -1, path: [] };
470
- for (const childId of node.children) {
471
- const child = graph.nodes.get(childId);
472
- if (!child) continue;
473
- const result = dfs(child);
474
- if (result.duration > bestChild.duration) {
475
- bestChild = result;
447
+ return records;
448
+ }
449
+ const obj = raw;
450
+ for (const containerKey of ["tools", "workers", "services", "agents", "daemons"]) {
451
+ const container = obj[containerKey];
452
+ if (typeof container === "object" && container !== null && !Array.isArray(container)) {
453
+ for (const [name, info] of Object.entries(container)) {
454
+ if (typeof info !== "object" || info === null) continue;
455
+ const w = info;
456
+ const status2 = findStatus(w);
457
+ const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
458
+ const pid = w.pid;
459
+ const detail2 = pid ? `pid: ${pid}` : extractDetail(w);
460
+ records.push({
461
+ id: name,
462
+ source: "workers",
463
+ status: status2,
464
+ lastActive: ts2,
465
+ detail: detail2,
466
+ file: file.filename
467
+ });
476
468
  }
469
+ return records;
477
470
  }
478
- return {
479
- duration: nodeDuration(node) + bestChild.duration,
480
- path: [node, ...bestChild.path]
481
- };
482
471
  }
483
- return dfs(root).path;
472
+ const status = findStatus(obj);
473
+ const ts = findTimestamp(obj) || file.mtime;
474
+ const detail = extractDetail(obj);
475
+ records.push({
476
+ id: nameFromFile(file.filename),
477
+ source: "state",
478
+ status,
479
+ lastActive: ts,
480
+ detail,
481
+ file: file.filename
482
+ });
483
+ return records;
484
484
  }
485
- function findWaitingOn(graph, nodeId) {
486
- const results = [];
487
- for (const edge of graph.edges) {
488
- if (edge.from === nodeId && edge.type === "waited_on") {
489
- const node = graph.nodes.get(edge.to);
490
- if (node) results.push(node);
485
+ function processJsonlFile(file) {
486
+ try {
487
+ const content = readFileSync(file.path, "utf8").trim();
488
+ if (!content) return [];
489
+ const lines = content.split("\n");
490
+ const lineCount = lines.length;
491
+ const lastObj = JSON.parse(lines[lines.length - 1]);
492
+ const name = lastObj.jobId ?? lastObj.agentId ?? lastObj.name ?? lastObj.id ?? nameFromFile(file.filename);
493
+ if (lastObj.action !== void 0 || lastObj.jobId !== void 0) {
494
+ const status2 = findStatus(lastObj);
495
+ const ts2 = findTimestamp(lastObj) || file.mtime;
496
+ const action = lastObj.action;
497
+ const detail2 = action ? `${action} (${lineCount} entries)` : `${lineCount} entries`;
498
+ return [
499
+ {
500
+ id: String(name),
501
+ source: "session",
502
+ status: status2,
503
+ lastActive: ts2,
504
+ detail: detail2,
505
+ file: file.filename
506
+ }
507
+ ];
491
508
  }
492
- }
493
- return results;
494
- }
495
- function getSubtree(graph, nodeId) {
496
- const startNode = graph.nodes.get(nodeId);
497
- if (!startNode) return [];
498
- const result = [];
499
- const queue = [...startNode.children];
500
- while (queue.length > 0) {
501
- const currentId = queue.shift();
502
- if (currentId === void 0) break;
503
- const current = graph.nodes.get(currentId);
504
- if (!current) continue;
505
- result.push(current);
506
- queue.push(...current.children);
507
- }
508
- return result;
509
- }
510
- function getDuration(graph) {
511
- const end = graph.endTime ?? Date.now();
512
- return end - graph.startTime;
513
- }
514
- function getDepth(graph) {
515
- const root = graph.nodes.get(graph.rootNodeId);
516
- if (!root) return -1;
517
- function dfs(node, depth) {
518
- if (node.children.length === 0) return depth;
519
- let maxDepth = depth;
520
- for (const childId of node.children) {
521
- const child = graph.nodes.get(childId);
522
- if (!child) continue;
523
- const childDepth = dfs(child, depth + 1);
524
- if (childDepth > maxDepth) maxDepth = childDepth;
509
+ const tail = lines.slice(Math.max(0, lineCount - 30));
510
+ let model = "";
511
+ let totalTokens = 0;
512
+ let totalCost = 0;
513
+ const toolCalls = [];
514
+ let lastUserMsg = "";
515
+ let lastAssistantMsg = "";
516
+ let errorCount = 0;
517
+ let lastRole = "";
518
+ let sessionId = "";
519
+ for (const line of tail) {
520
+ let entry;
521
+ try {
522
+ entry = JSON.parse(line);
523
+ } catch {
524
+ continue;
525
+ }
526
+ const entryType = entry.type;
527
+ if (entryType === "session") {
528
+ sessionId = entry.id ?? "";
529
+ continue;
530
+ }
531
+ if (entryType === "model_change") {
532
+ model = entry.modelId ?? "";
533
+ continue;
534
+ }
535
+ if (entryType !== "message") continue;
536
+ const msg = entry.message;
537
+ if (!msg) continue;
538
+ const role = msg.role;
539
+ lastRole = role;
540
+ if (msg.model && !model) model = msg.model;
541
+ const usage = msg.usage;
542
+ if (usage) {
543
+ const input = usage.input ?? 0;
544
+ const output = usage.output ?? 0;
545
+ totalTokens += input + output;
546
+ const cost = usage.cost;
547
+ if (cost && typeof cost.total === "number") totalCost += cost.total;
548
+ }
549
+ const msgContent = msg.content;
550
+ if (role === "user") {
551
+ if (typeof msgContent === "string") {
552
+ lastUserMsg = msgContent.slice(0, 80);
553
+ } else if (Array.isArray(msgContent)) {
554
+ for (const c of msgContent) {
555
+ if (c.type === "text" && typeof c.text === "string") {
556
+ lastUserMsg = c.text.slice(0, 80);
557
+ break;
558
+ }
559
+ }
560
+ }
561
+ }
562
+ if (role === "assistant" && Array.isArray(msgContent)) {
563
+ for (const c of msgContent) {
564
+ if (c.type === "tool_use" || c.type === "toolCall") {
565
+ const toolName = c.name ?? "";
566
+ if (toolName && !toolCalls.includes(toolName)) toolCalls.push(toolName);
567
+ }
568
+ if (c.type === "text" && typeof c.text === "string" && c.text.length > 5) {
569
+ lastAssistantMsg = c.text.slice(0, 80);
570
+ }
571
+ }
572
+ }
573
+ if (role === "toolResult") {
574
+ if (typeof msgContent === "string" && /error|ENOENT|failed|exception/i.test(msgContent)) {
575
+ errorCount++;
576
+ } else if (Array.isArray(msgContent)) {
577
+ for (const c of msgContent) {
578
+ if (typeof c.text === "string" && /error|ENOENT|failed|exception/i.test(c.text)) {
579
+ errorCount++;
580
+ }
581
+ }
582
+ }
583
+ }
525
584
  }
526
- return maxDepth;
527
- }
528
- return dfs(root, 0);
529
- }
530
- function getStats(graph) {
531
- const byStatus = {
532
- running: 0,
533
- completed: 0,
534
- failed: 0,
535
- hung: 0,
536
- timeout: 0
537
- };
538
- const byType = {
539
- agent: 0,
540
- tool: 0,
541
- subagent: 0,
542
- wait: 0,
543
- decision: 0,
544
- custom: 0
545
- };
546
- let failureCount = 0;
547
- let hungCount = 0;
548
- for (const node of graph.nodes.values()) {
549
- byStatus[node.status]++;
550
- byType[node.type]++;
551
- if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
552
- failureCount++;
585
+ const parts = [];
586
+ if (model) {
587
+ const shortModel = model.includes("/") ? model.split("/").pop() : model;
588
+ parts.push(shortModel.slice(0, 20));
553
589
  }
554
- if (node.status === "running" && node.endTime === null) {
555
- hungCount++;
590
+ if (toolCalls.length > 0) {
591
+ const recent = toolCalls.slice(-3);
592
+ parts.push(`tools: ${recent.join(", ")}`);
593
+ }
594
+ if (totalCost > 0) {
595
+ parts.push(`$${totalCost.toFixed(3)}`);
596
+ } else if (totalTokens > 0) {
597
+ const k = (totalTokens / 1e3).toFixed(1);
598
+ parts.push(`${k}k tok`);
599
+ }
600
+ if (lastAssistantMsg && lastRole === "assistant") {
601
+ parts.push(lastAssistantMsg.slice(0, 40));
602
+ } else if (lastUserMsg && !parts.some((p) => p.startsWith("tools:"))) {
603
+ parts.push(`user: ${lastUserMsg.slice(0, 35)}`);
556
604
  }
605
+ if (errorCount > 0) {
606
+ parts.push(`${errorCount} errors`);
607
+ }
608
+ const detail = parts.join(" | ") || `${lineCount} messages`;
609
+ const status = errorCount > lineCount / 4 ? "error" : lastRole === "assistant" ? "ok" : "running";
610
+ const ts = findTimestamp(lastObj) || file.mtime;
611
+ const sessionName = sessionId ? sessionId.slice(0, 8) : nameFromFile(file.filename);
612
+ return [
613
+ {
614
+ id: String(name !== "unknown" ? name : sessionName),
615
+ source: "session",
616
+ status,
617
+ lastActive: ts,
618
+ detail,
619
+ file: file.filename
620
+ }
621
+ ];
622
+ } catch {
623
+ return [];
557
624
  }
558
- return {
559
- totalNodes: graph.nodes.size,
560
- byStatus,
561
- byType,
562
- depth: getDepth(graph),
563
- duration: getDuration(graph),
564
- failureCount,
565
- hungCount
566
- };
567
625
  }
568
-
569
- // src/graph-stitch.ts
570
- function groupByTraceId(graphs) {
571
- const groups = /* @__PURE__ */ new Map();
572
- for (const g of graphs) {
573
- if (!g.traceId) continue;
574
- const arr = groups.get(g.traceId) ?? [];
575
- arr.push(g);
576
- groups.set(g.traceId, arr);
577
- }
578
- return groups;
626
+ var K = "\x1B[K";
627
+ function writeLine(lines, text) {
628
+ lines.push(text + K);
579
629
  }
580
- function stitchTrace(graphs) {
581
- if (graphs.length === 0) throw new Error("No graphs to stitch");
582
- const traceId = graphs[0].traceId ?? "";
583
- const graphsBySpan = /* @__PURE__ */ new Map();
584
- const childMap = /* @__PURE__ */ new Map();
585
- let rootGraph = null;
586
- for (const g of graphs) {
587
- if (g.spanId) graphsBySpan.set(g.spanId, g);
588
- if (!g.parentSpanId) {
589
- if (!rootGraph || g.startTime < rootGraph.startTime) rootGraph = g;
590
- }
591
- if (g.parentSpanId) {
592
- const siblings = childMap.get(g.parentSpanId) ?? [];
593
- if (g.spanId) siblings.push(g.spanId);
594
- childMap.set(g.parentSpanId, siblings);
595
- }
630
+ function flushLines(lines) {
631
+ process.stdout.write("\x1B[H");
632
+ process.stdout.write(lines.join("\n") + "\n");
633
+ process.stdout.write("\x1B[J");
634
+ }
635
+ var prevFileCount = 0;
636
+ var newExecCount = 0;
637
+ var sessionStart = Date.now();
638
+ var firstRender = true;
639
+ function render(config) {
640
+ const files = scanFiles(config.dirs, config.recursive);
641
+ if (files.length > prevFileCount && prevFileCount > 0) {
642
+ newExecCount += files.length - prevFileCount;
596
643
  }
597
- if (!rootGraph) rootGraph = graphs[0];
598
- let status = "completed";
599
- let endTime = 0;
600
- let startTime = Infinity;
601
- for (const g of graphs) {
602
- startTime = Math.min(startTime, g.startTime);
603
- if (g.status === "failed") status = "failed";
604
- else if (g.status === "running" && status !== "failed") status = "running";
605
- if (g.endTime === null) endTime = null;
606
- else if (endTime !== null) endTime = Math.max(endTime, g.endTime);
644
+ prevFileCount = files.length;
645
+ const allRecords = [];
646
+ const allTraces = [];
647
+ for (const f of files.slice(0, 300)) {
648
+ const records = f.ext === ".jsonl" ? processJsonlFile(f) : processJsonFile(f);
649
+ for (const r of records) {
650
+ allRecords.push(r);
651
+ if (r.traceData) allTraces.push(r.traceData);
652
+ }
607
653
  }
608
- const frozenChildMap = /* @__PURE__ */ new Map();
609
- for (const [k, v] of childMap) frozenChildMap.set(k, Object.freeze([...v]));
610
- return Object.freeze({
611
- traceId,
612
- graphs: graphsBySpan,
613
- rootGraph,
614
- childMap: frozenChildMap,
615
- startTime,
616
- endTime,
617
- status
618
- });
619
- }
620
- function getTraceTree(trace) {
621
- const result = [];
622
- function walk(spanId) {
623
- const graph = trace.graphs.get(spanId);
624
- if (graph) result.push(graph);
625
- const children = trace.childMap.get(spanId) ?? [];
626
- for (const childSpan of children) walk(childSpan);
654
+ const byFile = /* @__PURE__ */ new Map();
655
+ for (const r of allRecords) {
656
+ const arr = byFile.get(r.file) ?? [];
657
+ arr.push(r);
658
+ byFile.set(r.file, arr);
627
659
  }
628
- if (trace.rootGraph.spanId) walk(trace.rootGraph.spanId);
629
- else result.push(trace.rootGraph);
630
- return result;
631
- }
632
-
633
- // src/live.ts
634
- import { readdirSync as readdirSync2, readFileSync, statSync as statSync2, watch, existsSync as existsSync2 } from "fs";
635
- import { join as join2, resolve as resolve2, basename as basename2 } from "path";
636
- var C = {
637
- reset: "\x1B[0m",
638
- bold: "\x1B[1m",
639
- dim: "\x1B[90m",
640
- under: "\x1B[4m",
641
- red: "\x1B[31m",
642
- green: "\x1B[32m",
643
- yellow: "\x1B[33m",
644
- blue: "\x1B[34m",
645
- magenta: "\x1B[35m",
646
- cyan: "\x1B[36m",
647
- white: "\x1B[37m"
648
- };
649
- function parseArgs(argv) {
650
- const config = { dirs: [], refreshMs: 3e3, recursive: false };
651
- const args = argv.slice(0);
652
- if (args[0] === "live") args.shift();
653
- let i = 0;
654
- while (i < args.length) {
655
- const arg = args[i];
656
- if (arg === "--help" || arg === "-h") {
657
- printUsage();
658
- process.exit(0);
659
- } else if (arg === "--refresh" || arg === "-r") {
660
- i++;
661
- const v = parseInt(args[i] ?? "", 10);
662
- if (!isNaN(v) && v > 0) config.refreshMs = v * 1e3;
663
- i++;
664
- } else if (arg === "--recursive" || arg === "-R") {
665
- config.recursive = true;
666
- i++;
667
- } else if (!arg.startsWith("-")) {
668
- config.dirs.push(resolve2(arg));
669
- i++;
660
+ const groups = [];
661
+ for (const [file, records] of byFile) {
662
+ if (records.length === 1) {
663
+ const r = records[0];
664
+ groups.push({
665
+ name: r.id,
666
+ source: r.source,
667
+ status: r.status,
668
+ lastTs: r.lastActive,
669
+ detail: r.detail,
670
+ children: [],
671
+ ok: r.status === "ok" ? 1 : 0,
672
+ fail: r.status === "error" ? 1 : 0,
673
+ running: r.status === "running" ? 1 : 0,
674
+ total: 1
675
+ });
670
676
  } else {
671
- i++;
677
+ const groupName = nameFromFile(file);
678
+ let lastTs = 0;
679
+ let ok = 0, fail = 0, running = 0;
680
+ for (const r of records) {
681
+ if (r.lastActive > lastTs) lastTs = r.lastActive;
682
+ if (r.status === "ok") ok++;
683
+ else if (r.status === "error") fail++;
684
+ else if (r.status === "running") running++;
685
+ }
686
+ const status = fail > 0 ? "error" : running > 0 ? "running" : ok > 0 ? "ok" : "unknown";
687
+ groups.push({
688
+ name: groupName,
689
+ source: records[0].source,
690
+ status,
691
+ lastTs,
692
+ detail: `${records.length} agents`,
693
+ children: records.sort((a, b) => b.lastActive - a.lastActive),
694
+ ok,
695
+ fail,
696
+ running,
697
+ total: records.length
698
+ });
672
699
  }
673
- }
674
- if (config.dirs.length === 0) config.dirs.push(resolve2("."));
675
- return config;
676
- }
677
- function printUsage() {
678
- console.log(`
679
- AgentFlow Live Monitor \u2014 real-time terminal dashboard for agent systems.
680
-
681
- Auto-detects agent traces, state files, job schedulers, and session logs
682
- from any JSON/JSONL files in the watched directories.
683
-
684
- Usage:
685
- agentflow live [dir...] [options]
686
-
687
- Arguments:
688
- dir One or more directories to watch (default: .)
689
-
690
- Options:
691
- -r, --refresh <secs> Refresh interval in seconds (default: 3)
692
- -R, --recursive Scan subdirectories (1 level deep)
693
- -h, --help Show this help message
694
-
695
- Examples:
696
- agentflow live ./data
697
- agentflow live ./traces ./cron ./workers --refresh 5
698
- agentflow live /var/lib/myagent -R
699
- `.trim());
700
- }
701
- function scanFiles(dirs, recursive) {
702
- const results = [];
703
- const seen = /* @__PURE__ */ new Set();
704
- function scanDir(d, topLevel) {
705
- try {
706
- for (const f of readdirSync2(d)) {
707
- if (f.startsWith(".")) continue;
708
- const fp = join2(d, f);
709
- if (seen.has(fp)) continue;
710
- let stat;
700
+ }
701
+ groups.sort((a, b) => b.lastTs - a.lastTs);
702
+ const totExec = allRecords.length;
703
+ const totFail = allRecords.filter((r) => r.status === "error").length;
704
+ const totRunning = allRecords.filter((r) => r.status === "running").length;
705
+ const uniqueAgents = new Set(allRecords.map((r) => r.id)).size;
706
+ const sysRate = totExec > 0 ? ((totExec - totFail) / totExec * 100).toFixed(1) : "100.0";
707
+ const now = Date.now();
708
+ const buckets = new Array(12).fill(0);
709
+ const failBuckets = new Array(12).fill(0);
710
+ for (const r of allRecords) {
711
+ const age = now - r.lastActive;
712
+ if (age > 36e5 || age < 0) continue;
713
+ const idx = 11 - Math.floor(age / 3e5);
714
+ if (idx >= 0 && idx < 12) {
715
+ buckets[idx]++;
716
+ if (r.status === "error") failBuckets[idx]++;
717
+ }
718
+ }
719
+ const maxBucket = Math.max(...buckets, 1);
720
+ const sparkChars = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
721
+ const spark = buckets.map((v, i) => {
722
+ const level = Math.round(v / maxBucket * 8);
723
+ return (failBuckets[i] > 0 ? C.red : C.green) + sparkChars[level] + C.reset;
724
+ }).join("");
725
+ const distributedTraces = [];
726
+ if (allTraces.length > 1) {
727
+ const traceGroups = groupByTraceId(allTraces);
728
+ for (const [_tid, graphs] of traceGroups) {
729
+ if (graphs.length > 1) {
711
730
  try {
712
- stat = statSync2(fp);
731
+ distributedTraces.push(stitchTrace(graphs));
713
732
  } catch {
714
- continue;
715
- }
716
- if (stat.isDirectory() && recursive && topLevel) {
717
- scanDir(fp, false);
718
- continue;
719
- }
720
- if (!stat.isFile()) continue;
721
- if (f.endsWith(".json")) {
722
- seen.add(fp);
723
- results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".json" });
724
- } else if (f.endsWith(".jsonl")) {
725
- seen.add(fp);
726
- results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".jsonl" });
727
733
  }
728
734
  }
729
- } catch {
730
735
  }
736
+ distributedTraces.sort((a, b) => b.startTime - a.startTime);
731
737
  }
732
- for (const dir of dirs) scanDir(dir, true);
733
- results.sort((a, b) => b.mtime - a.mtime);
734
- return results;
735
- }
736
- function safeReadJson(fp) {
737
- try {
738
- return JSON.parse(readFileSync(fp, "utf8"));
739
- } catch {
740
- return null;
738
+ const upSec = Math.floor((Date.now() - sessionStart) / 1e3);
739
+ const upMin = Math.floor(upSec / 60);
740
+ const upStr = upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`;
741
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
742
+ function statusIcon(s, recent) {
743
+ if (s === "error") return `${C.red}\u25CF${C.reset}`;
744
+ if (s === "running") return `${C.green}\u25CF${C.reset}`;
745
+ if (s === "ok" && recent) return `${C.green}\u25CF${C.reset}`;
746
+ if (s === "ok") return `${C.dim}\u25CB${C.reset}`;
747
+ return `${C.dim}\u25CB${C.reset}`;
741
748
  }
742
- }
743
- function nameFromFile(filename) {
744
- return basename2(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
745
- }
746
- function normalizeStatus(val) {
747
- if (typeof val !== "string") return "unknown";
748
- const s = val.toLowerCase();
749
- if (["ok", "success", "completed", "done", "passed", "healthy", "good"].includes(s)) return "ok";
750
- if (["error", "failed", "failure", "crashed", "unhealthy", "bad", "timeout"].includes(s)) return "error";
751
- if (["running", "active", "in_progress", "started", "pending", "processing"].includes(s)) return "running";
752
- return "unknown";
753
- }
754
- function findStatus(obj) {
755
- for (const key of ["status", "state", "lastRunStatus", "lastStatus", "health", "result"]) {
756
- if (key in obj) {
757
- const val = obj[key];
758
- if (typeof val === "string") return normalizeStatus(val);
759
- if (typeof val === "object" && val !== null && "status" in val) {
760
- return normalizeStatus(val.status);
761
- }
749
+ function statusText(g) {
750
+ if (g.fail > 0 && g.ok === 0 && g.running === 0) return `${C.red}error${C.reset}`;
751
+ if (g.running > 0) return `${C.green}running${C.reset}`;
752
+ if (g.fail > 0) return `${C.yellow}${g.ok}ok/${g.fail}err${C.reset}`;
753
+ if (g.ok > 0)
754
+ return g.total > 1 ? `${C.green}${g.ok}/${g.total} ok${C.reset}` : `${C.green}ok${C.reset}`;
755
+ return `${C.dim}idle${C.reset}`;
756
+ }
757
+ function sourceTag(s) {
758
+ switch (s) {
759
+ case "trace":
760
+ return `${C.cyan}trace${C.reset}`;
761
+ case "jobs":
762
+ return `${C.blue}job${C.reset}`;
763
+ case "workers":
764
+ return `${C.magenta}worker${C.reset}`;
765
+ case "session":
766
+ return `${C.yellow}session${C.reset}`;
767
+ case "state":
768
+ return `${C.dim}state${C.reset}`;
762
769
  }
763
770
  }
764
- return "unknown";
765
- }
766
- function findTimestamp(obj) {
767
- for (const key of ["ts", "timestamp", "lastRunAtMs", "last_run", "lastExecution", "updated_at", "started_at", "endTime", "startTime"]) {
768
- const val = obj[key];
769
- if (typeof val === "number") return val > 1e12 ? val : val * 1e3;
770
- if (typeof val === "string") {
771
- const d = Date.parse(val);
772
- if (!isNaN(d)) return d;
771
+ function timeStr(ts) {
772
+ if (ts <= 0) return "n/a";
773
+ return new Date(ts).toLocaleTimeString();
774
+ }
775
+ function truncate(s, max) {
776
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
777
+ }
778
+ const termWidth = process.stdout.columns || 120;
779
+ const detailWidth = Math.max(20, termWidth - 60);
780
+ if (firstRender) {
781
+ process.stdout.write("\x1B[2J");
782
+ firstRender = false;
783
+ }
784
+ const L = [];
785
+ writeLine(L, `${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
786
+ writeLine(
787
+ L,
788
+ `${C.bold}${C.cyan}\u2551${C.reset} ${C.bold}${C.white}AGENTFLOW LIVE${C.reset} ${C.green}\u25CF LIVE${C.reset} ${C.dim}${time}${C.reset} ${C.bold}${C.cyan}\u2551${C.reset}`
789
+ );
790
+ const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
791
+ const pad1 = Math.max(0, 64 - metaLine.length);
792
+ writeLine(
793
+ L,
794
+ `${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`
795
+ );
796
+ writeLine(L, `${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
797
+ const sc = totFail === 0 ? C.green : C.yellow;
798
+ writeLine(L, "");
799
+ writeLine(
800
+ L,
801
+ ` ${C.bold}Agents${C.reset} ${sc}${uniqueAgents}${C.reset} ${C.bold}Records${C.reset} ${sc}${totExec}${C.reset} ${C.bold}Success${C.reset} ${sc}${sysRate}%${C.reset} ${C.bold}Running${C.reset} ${C.green}${totRunning}${C.reset} ${C.bold}Errors${C.reset} ${totFail > 0 ? C.red : C.dim}${totFail}${C.reset} ${C.bold}New${C.reset} ${C.yellow}+${newExecCount}${C.reset}`
802
+ );
803
+ writeLine(L, "");
804
+ writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
805
+ writeLine(L, "");
806
+ writeLine(
807
+ L,
808
+ ` ${C.bold}${C.under}Agent Status Last Active Detail${C.reset}`
809
+ );
810
+ let lineCount = 0;
811
+ for (const g of groups) {
812
+ if (lineCount > 35) break;
813
+ const isRecent = Date.now() - g.lastTs < 3e5;
814
+ const icon = statusIcon(g.status, isRecent);
815
+ const active = isRecent ? `${C.green}${timeStr(g.lastTs)}${C.reset}` : `${C.dim}${timeStr(g.lastTs)}${C.reset}`;
816
+ if (g.children.length === 0) {
817
+ const name = truncate(g.name, 26).padEnd(26);
818
+ const st = statusText(g);
819
+ const det = truncate(g.detail, detailWidth);
820
+ writeLine(
821
+ L,
822
+ ` ${icon} ${name} ${st.padEnd(20)} ${active.padEnd(20)} ${C.dim}${det}${C.reset}`
823
+ );
824
+ lineCount++;
825
+ } else {
826
+ const name = truncate(g.name, 24).padEnd(24);
827
+ const st = statusText(g);
828
+ const tag = sourceTag(g.source);
829
+ writeLine(
830
+ L,
831
+ ` ${icon} ${C.bold}${name}${C.reset} ${st.padEnd(20)} ${active.padEnd(20)} ${tag} ${C.dim}(${g.children.length} agents)${C.reset}`
832
+ );
833
+ lineCount++;
834
+ const kids = g.children.slice(0, 12);
835
+ for (let i = 0; i < kids.length; i++) {
836
+ if (lineCount > 35) break;
837
+ const child = kids[i];
838
+ const isLast = i === kids.length - 1;
839
+ const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
840
+ const cIcon = statusIcon(child.status, Date.now() - child.lastActive < 3e5);
841
+ const cName = truncate(child.id, 22).padEnd(22);
842
+ const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
843
+ const cDet = truncate(child.detail, detailWidth - 5);
844
+ writeLine(
845
+ L,
846
+ ` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`
847
+ );
848
+ lineCount++;
849
+ }
850
+ if (g.children.length > 12) {
851
+ writeLine(L, ` ${C.dim} ... +${g.children.length - 12} more${C.reset}`);
852
+ lineCount++;
853
+ }
773
854
  }
774
855
  }
775
- return 0;
776
- }
777
- function extractDetail(obj) {
778
- const parts = [];
779
- for (const key of ["summary", "message", "description", "lastError", "error", "name", "jobId", "id"]) {
780
- const val = obj[key];
781
- if (typeof val === "string" && val.length > 0 && val.length < 200) {
782
- parts.push(val.slice(0, 80));
783
- break;
856
+ if (distributedTraces.length > 0) {
857
+ writeLine(L, "");
858
+ writeLine(L, ` ${C.bold}${C.under}Distributed Traces${C.reset}`);
859
+ for (const dt of distributedTraces.slice(0, 3)) {
860
+ const traceTime = new Date(dt.startTime).toLocaleTimeString();
861
+ const si = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
862
+ const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
863
+ const tid = dt.traceId.slice(0, 8);
864
+ writeLine(
865
+ L,
866
+ ` ${si} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`
867
+ );
868
+ const tree = getTraceTree(dt);
869
+ for (let i = 0; i < Math.min(tree.length, 6); i++) {
870
+ const tg = tree[i];
871
+ const depth = getDistDepth(dt, tg.spanId);
872
+ const indent = " " + "\u2502 ".repeat(Math.max(0, depth - 1));
873
+ const isLast = i === tree.length - 1 || getDistDepth(dt, tree[i + 1]?.spanId) <= depth;
874
+ const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
875
+ const gs = tg.status === "completed" ? `${C.green}\u2713${C.reset}` : tg.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
876
+ const gd = tg.endTime ? `${tg.endTime - tg.startTime}ms` : "running";
877
+ writeLine(
878
+ L,
879
+ `${indent}${conn}${gs} ${C.bold}${tg.agentId}${C.reset} ${C.dim}[${tg.trigger}] ${gd}${C.reset}`
880
+ );
881
+ }
784
882
  }
785
883
  }
786
- for (const key of ["totalExecutions", "runs", "count", "processed", "consecutiveErrors"]) {
787
- const val = obj[key];
788
- if (typeof val === "number") {
789
- parts.push(`${key}: ${val}`);
790
- break;
884
+ const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0, 6);
885
+ if (recentRecords.length > 0) {
886
+ writeLine(L, "");
887
+ writeLine(L, ` ${C.bold}${C.under}Recent Activity${C.reset}`);
888
+ for (const r of recentRecords) {
889
+ const icon = r.status === "ok" ? `${C.green}\u2713${C.reset}` : r.status === "error" ? `${C.red}\u2717${C.reset}` : r.status === "running" ? `${C.green}\u25B6${C.reset}` : `${C.dim}\u25CB${C.reset}`;
890
+ const t = new Date(r.lastActive).toLocaleTimeString();
891
+ const agent = truncate(r.id, 26).padEnd(26);
892
+ const age = Math.floor((Date.now() - r.lastActive) / 1e3);
893
+ const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
894
+ const det = truncate(r.detail, detailWidth);
895
+ writeLine(
896
+ L,
897
+ ` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${det}${C.reset}`
898
+ );
791
899
  }
792
900
  }
793
- return parts.join(" | ") || "";
794
- }
795
- function tryLoadTrace(fp, raw) {
796
- if (typeof raw !== "object" || raw === null) return null;
797
- const obj = raw;
798
- if (!("nodes" in obj)) return null;
799
- if (!("agentId" in obj) && !("rootNodeId" in obj) && !("rootId" in obj)) return null;
800
- try {
801
- return loadGraph(obj);
802
- } catch {
803
- return null;
901
+ if (files.length === 0) {
902
+ writeLine(L, "");
903
+ writeLine(L, ` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
904
+ for (const d of config.dirs) writeLine(L, ` ${C.dim} ${d}${C.reset}`);
804
905
  }
906
+ writeLine(L, "");
907
+ const dirLabel = config.dirs.length === 1 ? config.dirs[0] : `${config.dirs.length} directories`;
908
+ writeLine(L, ` ${C.dim}Watching: ${dirLabel}${C.reset}`);
909
+ writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
910
+ flushLines(L);
805
911
  }
806
- function processJsonFile(file) {
807
- const raw = safeReadJson(file.path);
808
- if (raw === null) return [];
809
- const records = [];
810
- const trace = tryLoadTrace(file.path, raw);
811
- if (trace) {
812
- try {
813
- const fails = getFailures(trace);
814
- const hung = getHungNodes(trace);
815
- const stats = getStats(trace);
816
- records.push({
817
- id: trace.agentId,
818
- source: "trace",
819
- status: fails.length === 0 && hung.length === 0 ? "ok" : "error",
820
- lastActive: file.mtime,
821
- detail: `${stats.totalNodes} nodes [${trace.trigger}]`,
822
- file: file.filename,
823
- traceData: trace
912
+ function getDistDepth(dt, spanId) {
913
+ if (!spanId) return 0;
914
+ const g = dt.graphs.get(spanId);
915
+ if (!g || !g.parentSpanId) return 0;
916
+ return 1 + getDistDepth(dt, g.parentSpanId);
917
+ }
918
+ function startLive(argv) {
919
+ const config = parseArgs(argv);
920
+ const valid = config.dirs.filter((d) => existsSync(d));
921
+ if (valid.length === 0) {
922
+ console.error(`No valid directories found: ${config.dirs.join(", ")}`);
923
+ console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
924
+ process.exit(1);
925
+ }
926
+ const invalid = config.dirs.filter((d) => !existsSync(d));
927
+ if (invalid.length > 0) {
928
+ console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
929
+ }
930
+ config.dirs = valid;
931
+ render(config);
932
+ let debounce = null;
933
+ for (const dir of config.dirs) {
934
+ try {
935
+ watch(dir, { recursive: config.recursive }, () => {
936
+ if (debounce) clearTimeout(debounce);
937
+ debounce = setTimeout(() => render(config), 500);
824
938
  });
825
939
  } catch {
826
940
  }
827
- return records;
828
941
  }
829
- if (typeof raw !== "object") return records;
830
- const arr = Array.isArray(raw) ? raw : Array.isArray(raw.jobs) ? raw.jobs : Array.isArray(raw.tasks) ? raw.tasks : Array.isArray(raw.items) ? raw.items : null;
831
- if (arr && arr.length > 0 && typeof arr[0] === "object" && arr[0] !== null) {
832
- for (const item of arr.slice(0, 50)) {
833
- const name = item.name ?? item.id ?? item.jobId ?? item.agentId;
834
- if (!name) continue;
835
- const state = typeof item.state === "object" && item.state !== null ? item.state : item;
836
- const status2 = findStatus(state);
837
- const ts2 = findTimestamp(state) || file.mtime;
838
- const detail2 = extractDetail(state);
839
- records.push({ id: String(name), source: "jobs", status: status2, lastActive: ts2, detail: detail2, file: file.filename });
942
+ setInterval(() => render(config), config.refreshMs);
943
+ process.on("SIGINT", () => {
944
+ console.log("\n" + C.dim + "Monitor stopped." + C.reset);
945
+ process.exit(0);
946
+ });
947
+ }
948
+
949
+ // src/graph-builder.ts
950
+ import { randomUUID } from "crypto";
951
+ function deepFreeze(obj) {
952
+ if (obj === null || typeof obj !== "object") return obj;
953
+ if (obj instanceof Map) {
954
+ Object.freeze(obj);
955
+ for (const value of obj.values()) {
956
+ deepFreeze(value);
840
957
  }
841
- return records;
958
+ return obj;
842
959
  }
843
- const obj = raw;
844
- for (const containerKey of ["tools", "workers", "services", "agents", "daemons"]) {
845
- const container = obj[containerKey];
846
- if (typeof container === "object" && container !== null && !Array.isArray(container)) {
847
- for (const [name, info] of Object.entries(container)) {
848
- if (typeof info !== "object" || info === null) continue;
849
- const w = info;
850
- const status2 = findStatus(w);
851
- const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
852
- const pid = w.pid;
853
- const detail2 = pid ? `pid: ${pid}` : extractDetail(w);
854
- records.push({ id: name, source: "workers", status: status2, lastActive: ts2, detail: detail2, file: file.filename });
855
- }
856
- return records;
960
+ Object.freeze(obj);
961
+ const record = obj;
962
+ for (const key of Object.keys(record)) {
963
+ const value = record[key];
964
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
965
+ deepFreeze(value);
857
966
  }
858
967
  }
859
- const status = findStatus(obj);
860
- const ts = findTimestamp(obj) || file.mtime;
861
- const detail = extractDetail(obj);
862
- records.push({ id: nameFromFile(file.filename), source: "state", status, lastActive: ts, detail, file: file.filename });
863
- return records;
968
+ return obj;
864
969
  }
865
- function processJsonlFile(file) {
866
- try {
867
- const content = readFileSync(file.path, "utf8").trim();
868
- if (!content) return [];
869
- const lines = content.split("\n");
870
- const lineCount = lines.length;
871
- const lastObj = JSON.parse(lines[lines.length - 1]);
872
- const name = lastObj.jobId ?? lastObj.agentId ?? lastObj.name ?? lastObj.id ?? nameFromFile(file.filename);
873
- if (lastObj.action !== void 0 || lastObj.jobId !== void 0) {
874
- const status2 = findStatus(lastObj);
875
- const ts2 = findTimestamp(lastObj) || file.mtime;
876
- const action = lastObj.action;
877
- const detail2 = action ? `${action} (${lineCount} entries)` : `${lineCount} entries`;
878
- return [{ id: String(name), source: "session", status: status2, lastActive: ts2, detail: detail2, file: file.filename }];
970
+ function createCounterIdGenerator() {
971
+ let counter = 0;
972
+ return () => {
973
+ counter++;
974
+ return `node_${String(counter).padStart(3, "0")}`;
975
+ };
976
+ }
977
+ function createGraphBuilder(config) {
978
+ const generateId = config?.idGenerator ?? createCounterIdGenerator();
979
+ const agentId = config?.agentId ?? "unknown";
980
+ const trigger = config?.trigger ?? "manual";
981
+ const spanId = randomUUID();
982
+ const traceId = config?.traceId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_TRACE_ID : void 0) ?? randomUUID();
983
+ const parentSpanId = config?.parentSpanId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_PARENT_SPAN_ID : void 0) ?? null;
984
+ const graphId = generateId();
985
+ const startTime = Date.now();
986
+ const nodes = /* @__PURE__ */ new Map();
987
+ const edges = [];
988
+ const events = [];
989
+ const parentStack = [];
990
+ let rootNodeId = null;
991
+ let built = false;
992
+ function assertNotBuilt() {
993
+ if (built) {
994
+ throw new Error("GraphBuilder: cannot mutate after build() has been called");
879
995
  }
880
- const tail = lines.slice(Math.max(0, lineCount - 30));
881
- let model = "";
882
- let totalTokens = 0;
883
- let totalCost = 0;
884
- let toolCalls = [];
885
- let lastUserMsg = "";
886
- let lastAssistantMsg = "";
887
- let errorCount = 0;
888
- let lastRole = "";
889
- let sessionId = "";
890
- for (const line of tail) {
891
- let entry;
892
- try {
893
- entry = JSON.parse(line);
894
- } catch {
895
- continue;
896
- }
897
- const entryType = entry.type;
898
- if (entryType === "session") {
899
- sessionId = entry.id ?? "";
900
- continue;
996
+ }
997
+ function getNode2(nodeId) {
998
+ const node = nodes.get(nodeId);
999
+ if (!node) {
1000
+ throw new Error(`GraphBuilder: node "${nodeId}" does not exist`);
1001
+ }
1002
+ return node;
1003
+ }
1004
+ function recordEvent(nodeId, eventType, data = {}) {
1005
+ events.push({
1006
+ timestamp: Date.now(),
1007
+ eventType,
1008
+ nodeId,
1009
+ data
1010
+ });
1011
+ }
1012
+ function buildGraph() {
1013
+ if (rootNodeId === null) {
1014
+ throw new Error("GraphBuilder: cannot build a graph with no nodes");
1015
+ }
1016
+ let graphStatus = "completed";
1017
+ for (const node of nodes.values()) {
1018
+ if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
1019
+ graphStatus = "failed";
1020
+ break;
901
1021
  }
902
- if (entryType === "model_change") {
903
- model = entry.modelId ?? "";
904
- continue;
1022
+ if (node.status === "running") {
1023
+ graphStatus = "running";
905
1024
  }
906
- if (entryType !== "message") continue;
907
- const msg = entry.message;
908
- if (!msg) continue;
909
- const role = msg.role;
910
- lastRole = role;
911
- if (msg.model && !model) model = msg.model;
912
- const usage = msg.usage;
913
- if (usage) {
914
- const input = usage.input ?? 0;
915
- const output = usage.output ?? 0;
916
- totalTokens += input + output;
917
- const cost = usage.cost;
918
- if (cost && typeof cost.total === "number") totalCost += cost.total;
1025
+ }
1026
+ const endTime = graphStatus === "running" ? null : Date.now();
1027
+ const frozenNodes = new Map(
1028
+ [...nodes.entries()].map(([id, mNode]) => [
1029
+ id,
1030
+ {
1031
+ id: mNode.id,
1032
+ type: mNode.type,
1033
+ name: mNode.name,
1034
+ startTime: mNode.startTime,
1035
+ endTime: mNode.endTime,
1036
+ status: mNode.status,
1037
+ parentId: mNode.parentId,
1038
+ children: [...mNode.children],
1039
+ metadata: { ...mNode.metadata },
1040
+ state: { ...mNode.state }
1041
+ }
1042
+ ])
1043
+ );
1044
+ const graph = {
1045
+ id: graphId,
1046
+ rootNodeId,
1047
+ nodes: frozenNodes,
1048
+ edges: [...edges],
1049
+ startTime,
1050
+ endTime,
1051
+ status: graphStatus,
1052
+ trigger,
1053
+ agentId,
1054
+ events: [...events],
1055
+ traceId,
1056
+ spanId,
1057
+ parentSpanId
1058
+ };
1059
+ return deepFreeze(graph);
1060
+ }
1061
+ const builder = {
1062
+ get graphId() {
1063
+ return graphId;
1064
+ },
1065
+ get traceContext() {
1066
+ return { traceId, spanId };
1067
+ },
1068
+ startNode(opts) {
1069
+ assertNotBuilt();
1070
+ const id = generateId();
1071
+ const parentId = opts.parentId ?? parentStack[parentStack.length - 1] ?? null;
1072
+ if (parentId !== null && !nodes.has(parentId)) {
1073
+ throw new Error(`GraphBuilder: parent node "${parentId}" does not exist`);
919
1074
  }
920
- const msgContent = msg.content;
921
- if (role === "user") {
922
- if (typeof msgContent === "string") {
923
- lastUserMsg = msgContent.slice(0, 80);
924
- } else if (Array.isArray(msgContent)) {
925
- for (const c of msgContent) {
926
- if (c.type === "text" && typeof c.text === "string") {
927
- lastUserMsg = c.text.slice(0, 80);
928
- break;
929
- }
930
- }
1075
+ const node = {
1076
+ id,
1077
+ type: opts.type,
1078
+ name: opts.name,
1079
+ startTime: Date.now(),
1080
+ endTime: null,
1081
+ status: "running",
1082
+ parentId,
1083
+ children: [],
1084
+ metadata: opts.metadata ? { ...opts.metadata } : {},
1085
+ state: {}
1086
+ };
1087
+ nodes.set(id, node);
1088
+ if (parentId !== null) {
1089
+ const parent = nodes.get(parentId);
1090
+ if (parent) {
1091
+ parent.children.push(id);
931
1092
  }
1093
+ edges.push({ from: parentId, to: id, type: "spawned" });
1094
+ }
1095
+ if (rootNodeId === null) {
1096
+ rootNodeId = id;
1097
+ }
1098
+ recordEvent(id, "agent_start", { type: opts.type, name: opts.name });
1099
+ return id;
1100
+ },
1101
+ endNode(nodeId, status = "completed") {
1102
+ assertNotBuilt();
1103
+ const node = getNode2(nodeId);
1104
+ if (node.endTime !== null) {
1105
+ throw new Error(
1106
+ `GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
1107
+ );
932
1108
  }
933
- if (role === "assistant" && Array.isArray(msgContent)) {
934
- for (const c of msgContent) {
935
- if (c.type === "tool_use" || c.type === "toolCall") {
936
- const toolName = c.name ?? "";
937
- if (toolName && !toolCalls.includes(toolName)) toolCalls.push(toolName);
938
- }
939
- if (c.type === "text" && typeof c.text === "string" && c.text.length > 5) {
940
- lastAssistantMsg = c.text.slice(0, 80);
941
- }
942
- }
1109
+ node.endTime = Date.now();
1110
+ node.status = status;
1111
+ recordEvent(nodeId, "agent_end", { status });
1112
+ },
1113
+ failNode(nodeId, error) {
1114
+ assertNotBuilt();
1115
+ const node = getNode2(nodeId);
1116
+ if (node.endTime !== null) {
1117
+ throw new Error(
1118
+ `GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
1119
+ );
943
1120
  }
944
- if (role === "toolResult") {
945
- if (typeof msgContent === "string" && /error|ENOENT|failed|exception/i.test(msgContent)) {
946
- errorCount++;
947
- } else if (Array.isArray(msgContent)) {
948
- for (const c of msgContent) {
949
- if (typeof c.text === "string" && /error|ENOENT|failed|exception/i.test(c.text)) {
950
- errorCount++;
951
- }
952
- }
953
- }
1121
+ const errorMessage = error instanceof Error ? error.message : error;
1122
+ const errorStack = error instanceof Error ? error.stack : void 0;
1123
+ node.endTime = Date.now();
1124
+ node.status = "failed";
1125
+ node.metadata.error = errorMessage;
1126
+ if (errorStack) {
1127
+ node.metadata.errorStack = errorStack;
954
1128
  }
1129
+ recordEvent(nodeId, "tool_error", { error: errorMessage });
1130
+ },
1131
+ addEdge(from, to, type) {
1132
+ assertNotBuilt();
1133
+ getNode2(from);
1134
+ getNode2(to);
1135
+ edges.push({ from, to, type });
1136
+ recordEvent(from, "custom", { to, type, action: "edge_add" });
1137
+ },
1138
+ pushEvent(event) {
1139
+ assertNotBuilt();
1140
+ getNode2(event.nodeId);
1141
+ events.push({
1142
+ ...event,
1143
+ timestamp: Date.now()
1144
+ });
1145
+ },
1146
+ updateState(nodeId, state) {
1147
+ assertNotBuilt();
1148
+ const node = getNode2(nodeId);
1149
+ Object.assign(node.state, state);
1150
+ recordEvent(nodeId, "custom", { action: "state_update", ...state });
1151
+ },
1152
+ withParent(parentId, fn) {
1153
+ assertNotBuilt();
1154
+ getNode2(parentId);
1155
+ parentStack.push(parentId);
1156
+ try {
1157
+ return fn();
1158
+ } finally {
1159
+ parentStack.pop();
1160
+ }
1161
+ },
1162
+ getSnapshot() {
1163
+ return buildGraph();
1164
+ },
1165
+ build() {
1166
+ assertNotBuilt();
1167
+ const graph = buildGraph();
1168
+ built = true;
1169
+ return graph;
955
1170
  }
956
- const parts = [];
957
- if (model) {
958
- const shortModel = model.includes("/") ? model.split("/").pop() : model;
959
- parts.push(shortModel.slice(0, 20));
960
- }
961
- if (toolCalls.length > 0) {
962
- const recent = toolCalls.slice(-3);
963
- parts.push(`tools: ${recent.join(", ")}`);
964
- }
965
- if (totalCost > 0) {
966
- parts.push(`$${totalCost.toFixed(3)}`);
967
- } else if (totalTokens > 0) {
968
- const k = (totalTokens / 1e3).toFixed(1);
969
- parts.push(`${k}k tok`);
970
- }
971
- if (lastAssistantMsg && lastRole === "assistant") {
972
- parts.push(lastAssistantMsg.slice(0, 40));
973
- } else if (lastUserMsg && !parts.some((p) => p.startsWith("tools:"))) {
974
- parts.push(`user: ${lastUserMsg.slice(0, 35)}`);
975
- }
976
- if (errorCount > 0) {
977
- parts.push(`${errorCount} errors`);
1171
+ };
1172
+ return builder;
1173
+ }
1174
+
1175
+ // src/runner.ts
1176
+ import { spawnSync } from "child_process";
1177
+ import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, statSync as statSync2, writeFileSync } from "fs";
1178
+ import { basename as basename2, join as join2, resolve as resolve2 } from "path";
1179
+ function globToRegex(pattern) {
1180
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1181
+ return new RegExp(`^${escaped}$`);
1182
+ }
1183
+ function snapshotDir(dir, patterns) {
1184
+ const result = /* @__PURE__ */ new Map();
1185
+ if (!existsSync2(dir)) return result;
1186
+ for (const entry of readdirSync2(dir)) {
1187
+ if (!patterns.some((re) => re.test(entry))) continue;
1188
+ const full = join2(dir, entry);
1189
+ try {
1190
+ const stat = statSync2(full);
1191
+ if (stat.isFile()) {
1192
+ result.set(full, stat.mtimeMs);
1193
+ }
1194
+ } catch {
978
1195
  }
979
- const detail = parts.join(" | ") || `${lineCount} messages`;
980
- const status = errorCount > lineCount / 4 ? "error" : lastRole === "assistant" ? "ok" : "running";
981
- const ts = findTimestamp(lastObj) || file.mtime;
982
- const sessionName = sessionId ? sessionId.slice(0, 8) : nameFromFile(file.filename);
983
- return [{ id: String(name !== "unknown" ? name : sessionName), source: "session", status, lastActive: ts, detail, file: file.filename }];
984
- } catch {
985
- return [];
986
1196
  }
1197
+ return result;
987
1198
  }
988
- var K = "\x1B[K";
989
- function writeLine(lines, text) {
990
- lines.push(text + K);
1199
+ function agentIdFromFilename(filePath) {
1200
+ const base = basename2(filePath, ".json");
1201
+ const cleaned = base.replace(/-state$/, "");
1202
+ return `alfred-${cleaned}`;
991
1203
  }
992
- function flushLines(lines) {
993
- process.stdout.write("\x1B[H");
994
- process.stdout.write(lines.join("\n") + "\n");
995
- process.stdout.write("\x1B[J");
1204
+ function deriveAgentId(command) {
1205
+ return "orchestrator";
996
1206
  }
997
- var prevFileCount = 0;
998
- var newExecCount = 0;
999
- var sessionStart = Date.now();
1000
- var firstRender = true;
1001
- function render(config) {
1002
- const files = scanFiles(config.dirs, config.recursive);
1003
- if (files.length > prevFileCount && prevFileCount > 0) {
1004
- newExecCount += files.length - prevFileCount;
1005
- }
1006
- prevFileCount = files.length;
1007
- const allRecords = [];
1008
- const allTraces = [];
1009
- for (const f of files.slice(0, 300)) {
1010
- const records = f.ext === ".jsonl" ? processJsonlFile(f) : processJsonFile(f);
1011
- for (const r of records) {
1012
- allRecords.push(r);
1013
- if (r.traceData) allTraces.push(r.traceData);
1014
- }
1015
- }
1016
- const byFile = /* @__PURE__ */ new Map();
1017
- for (const r of allRecords) {
1018
- const arr = byFile.get(r.file) ?? [];
1019
- arr.push(r);
1020
- byFile.set(r.file, arr);
1021
- }
1022
- const groups = [];
1023
- for (const [file, records] of byFile) {
1024
- if (records.length === 1) {
1025
- const r = records[0];
1026
- groups.push({
1027
- name: r.id,
1028
- source: r.source,
1029
- status: r.status,
1030
- lastTs: r.lastActive,
1031
- detail: r.detail,
1032
- children: [],
1033
- ok: r.status === "ok" ? 1 : 0,
1034
- fail: r.status === "error" ? 1 : 0,
1035
- running: r.status === "running" ? 1 : 0,
1036
- total: 1
1037
- });
1038
- } else {
1039
- const groupName = nameFromFile(file);
1040
- let lastTs = 0;
1041
- let ok = 0, fail = 0, running = 0;
1042
- for (const r of records) {
1043
- if (r.lastActive > lastTs) lastTs = r.lastActive;
1044
- if (r.status === "ok") ok++;
1045
- else if (r.status === "error") fail++;
1046
- else if (r.status === "running") running++;
1047
- }
1048
- const status = fail > 0 ? "error" : running > 0 ? "running" : ok > 0 ? "ok" : "unknown";
1049
- groups.push({
1050
- name: groupName,
1051
- source: records[0].source,
1052
- status,
1053
- lastTs,
1054
- detail: `${records.length} agents`,
1055
- children: records.sort((a, b) => b.lastActive - a.lastActive),
1056
- ok,
1057
- fail,
1058
- running,
1059
- total: records.length
1060
- });
1061
- }
1207
+ function fileTimestamp() {
1208
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
1209
+ }
1210
+ async function runTraced(config) {
1211
+ const {
1212
+ command,
1213
+ agentId = deriveAgentId(command),
1214
+ trigger = "cli",
1215
+ tracesDir = "./traces",
1216
+ watchDirs = [],
1217
+ watchPatterns = ["*.json"]
1218
+ } = config;
1219
+ if (command.length === 0) {
1220
+ throw new Error("runTraced: command must not be empty");
1062
1221
  }
1063
- groups.sort((a, b) => b.lastTs - a.lastTs);
1064
- const totExec = allRecords.length;
1065
- const totFail = allRecords.filter((r) => r.status === "error").length;
1066
- const totRunning = allRecords.filter((r) => r.status === "running").length;
1067
- const uniqueAgents = new Set(allRecords.map((r) => r.id)).size;
1068
- const sysRate = totExec > 0 ? ((totExec - totFail) / totExec * 100).toFixed(1) : "100.0";
1069
- const now = Date.now();
1070
- const buckets = new Array(12).fill(0);
1071
- const failBuckets = new Array(12).fill(0);
1072
- for (const r of allRecords) {
1073
- const age = now - r.lastActive;
1074
- if (age > 36e5 || age < 0) continue;
1075
- const idx = 11 - Math.floor(age / 3e5);
1076
- if (idx >= 0 && idx < 12) {
1077
- buckets[idx]++;
1078
- if (r.status === "error") failBuckets[idx]++;
1079
- }
1222
+ const resolvedTracesDir = resolve2(tracesDir);
1223
+ const patterns = watchPatterns.map(globToRegex);
1224
+ const orchestrator = createGraphBuilder({ agentId, trigger });
1225
+ const { traceId, spanId } = orchestrator.traceContext;
1226
+ const beforeSnapshots = /* @__PURE__ */ new Map();
1227
+ for (const dir of watchDirs) {
1228
+ beforeSnapshots.set(dir, snapshotDir(dir, patterns));
1080
1229
  }
1081
- const maxBucket = Math.max(...buckets, 1);
1082
- const sparkChars = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1083
- const spark = buckets.map((v, i) => {
1084
- const level = Math.round(v / maxBucket * 8);
1085
- return (failBuckets[i] > 0 ? C.red : C.green) + sparkChars[level] + C.reset;
1086
- }).join("");
1087
- const distributedTraces = [];
1088
- if (allTraces.length > 1) {
1089
- const traceGroups = groupByTraceId(allTraces);
1090
- for (const [_tid, graphs] of traceGroups) {
1091
- if (graphs.length > 1) {
1092
- try {
1093
- distributedTraces.push(stitchTrace(graphs));
1094
- } catch {
1095
- }
1230
+ const rootId = orchestrator.startNode({ type: "agent", name: agentId });
1231
+ const dispatchId = orchestrator.startNode({
1232
+ type: "tool",
1233
+ name: "dispatch-command",
1234
+ parentId: rootId
1235
+ });
1236
+ orchestrator.updateState(dispatchId, { command: command.join(" ") });
1237
+ const monitorId = orchestrator.startNode({
1238
+ type: "tool",
1239
+ name: "state-monitor",
1240
+ parentId: rootId
1241
+ });
1242
+ orchestrator.updateState(monitorId, {
1243
+ watchDirs,
1244
+ watchPatterns
1245
+ });
1246
+ const startMs = Date.now();
1247
+ const execCmd = command[0] ?? "";
1248
+ const execArgs = command.slice(1);
1249
+ process.env.AGENTFLOW_TRACE_ID = traceId;
1250
+ process.env.AGENTFLOW_PARENT_SPAN_ID = spanId;
1251
+ const result = spawnSync(execCmd, execArgs, { stdio: "inherit" });
1252
+ delete process.env.AGENTFLOW_TRACE_ID;
1253
+ delete process.env.AGENTFLOW_PARENT_SPAN_ID;
1254
+ const exitCode = result.status ?? 1;
1255
+ const duration = (Date.now() - startMs) / 1e3;
1256
+ const stateChanges = [];
1257
+ for (const dir of watchDirs) {
1258
+ const before = beforeSnapshots.get(dir) ?? /* @__PURE__ */ new Map();
1259
+ const after = snapshotDir(dir, patterns);
1260
+ for (const [filePath, mtime] of after) {
1261
+ const prevMtime = before.get(filePath);
1262
+ if (prevMtime === void 0 || mtime > prevMtime) {
1263
+ stateChanges.push(filePath);
1096
1264
  }
1097
1265
  }
1098
- distributedTraces.sort((a, b) => b.startTime - a.startTime);
1099
1266
  }
1100
- const upSec = Math.floor((Date.now() - sessionStart) / 1e3);
1101
- const upMin = Math.floor(upSec / 60);
1102
- const upStr = upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`;
1103
- const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1104
- function statusIcon(s, recent) {
1105
- if (s === "error") return `${C.red}\u25CF${C.reset}`;
1106
- if (s === "running") return `${C.green}\u25CF${C.reset}`;
1107
- if (s === "ok" && recent) return `${C.green}\u25CF${C.reset}`;
1108
- if (s === "ok") return `${C.dim}\u25CB${C.reset}`;
1109
- return `${C.dim}\u25CB${C.reset}`;
1267
+ orchestrator.updateState(monitorId, { stateChanges });
1268
+ orchestrator.endNode(monitorId);
1269
+ if (exitCode === 0) {
1270
+ orchestrator.endNode(dispatchId);
1271
+ } else {
1272
+ orchestrator.failNode(dispatchId, `Command exited with code ${exitCode}`);
1110
1273
  }
1111
- function statusText(g) {
1112
- if (g.fail > 0 && g.ok === 0 && g.running === 0) return `${C.red}error${C.reset}`;
1113
- if (g.running > 0) return `${C.green}running${C.reset}`;
1114
- if (g.fail > 0) return `${C.yellow}${g.ok}ok/${g.fail}err${C.reset}`;
1115
- if (g.ok > 0) return g.total > 1 ? `${C.green}${g.ok}/${g.total} ok${C.reset}` : `${C.green}ok${C.reset}`;
1116
- return `${C.dim}idle${C.reset}`;
1274
+ orchestrator.updateState(rootId, {
1275
+ exitCode,
1276
+ duration,
1277
+ stateChanges
1278
+ });
1279
+ if (exitCode === 0) {
1280
+ orchestrator.endNode(rootId);
1281
+ } else {
1282
+ orchestrator.failNode(rootId, `Command exited with code ${exitCode}`);
1117
1283
  }
1118
- function sourceTag(s) {
1119
- switch (s) {
1120
- case "trace":
1121
- return `${C.cyan}trace${C.reset}`;
1122
- case "jobs":
1123
- return `${C.blue}job${C.reset}`;
1124
- case "workers":
1125
- return `${C.magenta}worker${C.reset}`;
1126
- case "session":
1127
- return `${C.yellow}session${C.reset}`;
1128
- case "state":
1129
- return `${C.dim}state${C.reset}`;
1130
- }
1284
+ const orchestratorGraph = orchestrator.build();
1285
+ const allGraphs = [orchestratorGraph];
1286
+ for (const filePath of stateChanges) {
1287
+ const childAgentId = agentIdFromFilename(filePath);
1288
+ const childBuilder = createGraphBuilder({
1289
+ agentId: childAgentId,
1290
+ trigger: "state-change",
1291
+ traceId,
1292
+ parentSpanId: spanId
1293
+ });
1294
+ const childRootId = childBuilder.startNode({
1295
+ type: "agent",
1296
+ name: childAgentId
1297
+ });
1298
+ childBuilder.updateState(childRootId, {
1299
+ stateFile: filePath,
1300
+ detectedBy: "runner-state-monitor"
1301
+ });
1302
+ childBuilder.endNode(childRootId);
1303
+ allGraphs.push(childBuilder.build());
1131
1304
  }
1132
- function timeStr(ts) {
1133
- if (ts <= 0) return "n/a";
1134
- return new Date(ts).toLocaleTimeString();
1305
+ if (!existsSync2(resolvedTracesDir)) {
1306
+ mkdirSync(resolvedTracesDir, { recursive: true });
1135
1307
  }
1136
- function truncate(s, max) {
1137
- return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1308
+ const ts = fileTimestamp();
1309
+ const tracePaths = [];
1310
+ for (const graph of allGraphs) {
1311
+ const filename = `${graph.agentId}-${ts}.json`;
1312
+ const outPath = join2(resolvedTracesDir, filename);
1313
+ writeFileSync(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
1314
+ tracePaths.push(outPath);
1138
1315
  }
1139
- if (firstRender) {
1140
- process.stdout.write("\x1B[2J");
1141
- firstRender = false;
1316
+ if (tracePaths.length > 0) {
1317
+ console.log(`\u{1F50D} Run "agentflow trace show ${orchestratorGraph.id} --traces-dir ${resolvedTracesDir}" to inspect`);
1142
1318
  }
1143
- const L = [];
1144
- writeLine(L, `${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
1145
- writeLine(L, `${C.bold}${C.cyan}\u2551${C.reset} ${C.bold}${C.white}AGENTFLOW LIVE${C.reset} ${C.green}\u25CF LIVE${C.reset} ${C.dim}${time}${C.reset} ${C.bold}${C.cyan}\u2551${C.reset}`);
1146
- const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
1147
- const pad1 = Math.max(0, 64 - metaLine.length);
1148
- writeLine(L, `${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`);
1149
- writeLine(L, `${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
1150
- const sc = totFail === 0 ? C.green : C.yellow;
1151
- writeLine(L, "");
1152
- writeLine(L, ` ${C.bold}Agents${C.reset} ${sc}${uniqueAgents}${C.reset} ${C.bold}Records${C.reset} ${sc}${totExec}${C.reset} ${C.bold}Success${C.reset} ${sc}${sysRate}%${C.reset} ${C.bold}Running${C.reset} ${C.green}${totRunning}${C.reset} ${C.bold}Errors${C.reset} ${totFail > 0 ? C.red : C.dim}${totFail}${C.reset} ${C.bold}New${C.reset} ${C.yellow}+${newExecCount}${C.reset}`);
1153
- writeLine(L, "");
1154
- writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
1155
- writeLine(L, "");
1156
- writeLine(L, ` ${C.bold}${C.under}Agent Status Last Active Detail${C.reset}`);
1157
- let lineCount = 0;
1158
- for (const g of groups) {
1159
- if (lineCount > 35) break;
1160
- const isRecent = Date.now() - g.lastTs < 3e5;
1161
- const icon = statusIcon(g.status, isRecent);
1162
- const active = isRecent ? `${C.green}${timeStr(g.lastTs)}${C.reset}` : `${C.dim}${timeStr(g.lastTs)}${C.reset}`;
1163
- if (g.children.length === 0) {
1164
- const name = truncate(g.name, 26).padEnd(26);
1165
- const st = statusText(g);
1166
- const det = truncate(g.detail, 30);
1167
- writeLine(L, ` ${icon} ${name} ${st.padEnd(20)} ${active.padEnd(20)} ${C.dim}${det}${C.reset}`);
1168
- lineCount++;
1169
- } else {
1170
- const name = truncate(g.name, 24).padEnd(24);
1171
- const st = statusText(g);
1172
- const tag = sourceTag(g.source);
1173
- writeLine(L, ` ${icon} ${C.bold}${name}${C.reset} ${st.padEnd(20)} ${active.padEnd(20)} ${tag} ${C.dim}(${g.children.length} agents)${C.reset}`);
1174
- lineCount++;
1175
- const kids = g.children.slice(0, 12);
1176
- for (let i = 0; i < kids.length; i++) {
1177
- if (lineCount > 35) break;
1178
- const child = kids[i];
1179
- const isLast = i === kids.length - 1;
1180
- const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
1181
- const cIcon = statusIcon(child.status, Date.now() - child.lastActive < 3e5);
1182
- const cName = truncate(child.id, 22).padEnd(22);
1183
- const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
1184
- const cDet = truncate(child.detail, 25);
1185
- writeLine(L, ` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`);
1186
- lineCount++;
1187
- }
1188
- if (g.children.length > 12) {
1189
- writeLine(L, ` ${C.dim} ... +${g.children.length - 12} more${C.reset}`);
1190
- lineCount++;
1319
+ return {
1320
+ exitCode,
1321
+ traceId,
1322
+ spanId,
1323
+ tracePaths,
1324
+ stateChanges,
1325
+ duration
1326
+ };
1327
+ }
1328
+
1329
+ // src/trace-store.ts
1330
+ import { mkdir, readdir, readFile, writeFile } from "fs/promises";
1331
+ import { join as join3 } from "path";
1332
+ function createTraceStore(dir) {
1333
+ async function ensureDir() {
1334
+ await mkdir(dir, { recursive: true });
1335
+ }
1336
+ async function loadAll() {
1337
+ await ensureDir();
1338
+ let files;
1339
+ try {
1340
+ files = await readdir(dir);
1341
+ } catch {
1342
+ return [];
1343
+ }
1344
+ const graphs = [];
1345
+ for (const file of files) {
1346
+ if (!file.endsWith(".json")) continue;
1347
+ try {
1348
+ const content = await readFile(join3(dir, file), "utf-8");
1349
+ const graph = loadGraph(content);
1350
+ graphs.push(graph);
1351
+ } catch {
1191
1352
  }
1192
1353
  }
1354
+ return graphs;
1193
1355
  }
1194
- if (distributedTraces.length > 0) {
1195
- writeLine(L, "");
1196
- writeLine(L, ` ${C.bold}${C.under}Distributed Traces${C.reset}`);
1197
- for (const dt of distributedTraces.slice(0, 3)) {
1198
- const traceTime = new Date(dt.startTime).toLocaleTimeString();
1199
- const si = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
1200
- const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
1201
- const tid = dt.traceId.slice(0, 8);
1202
- writeLine(L, ` ${si} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`);
1203
- const tree = getTraceTree(dt);
1204
- for (let i = 0; i < Math.min(tree.length, 6); i++) {
1205
- const tg = tree[i];
1206
- const depth = getDistDepth(dt, tg.spanId);
1207
- const indent = " " + "\u2502 ".repeat(Math.max(0, depth - 1));
1208
- const isLast = i === tree.length - 1 || getDistDepth(dt, tree[i + 1]?.spanId) <= depth;
1209
- const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1210
- const gs = tg.status === "completed" ? `${C.green}\u2713${C.reset}` : tg.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
1211
- const gd = tg.endTime ? `${tg.endTime - tg.startTime}ms` : "running";
1212
- writeLine(L, `${indent}${conn}${gs} ${C.bold}${tg.agentId}${C.reset} ${C.dim}[${tg.trigger}] ${gd}${C.reset}`);
1356
+ return {
1357
+ async save(graph) {
1358
+ await ensureDir();
1359
+ const json = graphToJson(graph);
1360
+ const filePath = join3(dir, `${graph.id}.json`);
1361
+ await writeFile(filePath, JSON.stringify(json, null, 2), "utf-8");
1362
+ return filePath;
1363
+ },
1364
+ async get(graphId) {
1365
+ await ensureDir();
1366
+ const filePath = join3(dir, `${graphId}.json`);
1367
+ try {
1368
+ const content = await readFile(filePath, "utf-8");
1369
+ return loadGraph(content);
1370
+ } catch {
1371
+ }
1372
+ const all = await loadAll();
1373
+ return all.find((g) => g.id === graphId) ?? null;
1374
+ },
1375
+ async list(opts) {
1376
+ let graphs = await loadAll();
1377
+ if (opts?.status) {
1378
+ graphs = graphs.filter((g) => g.status === opts.status);
1379
+ }
1380
+ graphs.sort((a, b) => b.startTime - a.startTime);
1381
+ if (opts?.limit && opts.limit > 0) {
1382
+ graphs = graphs.slice(0, opts.limit);
1383
+ }
1384
+ return graphs;
1385
+ },
1386
+ async getStuckSpans() {
1387
+ const graphs = await loadAll();
1388
+ const stuck = [];
1389
+ for (const graph of graphs) {
1390
+ for (const node of graph.nodes.values()) {
1391
+ if (node.status === "running" || node.status === "hung" || node.status === "timeout") {
1392
+ stuck.push(node);
1393
+ }
1394
+ }
1395
+ }
1396
+ return stuck;
1397
+ },
1398
+ async getReasoningLoops(threshold = 25) {
1399
+ const graphs = await loadAll();
1400
+ const results = [];
1401
+ for (const graph of graphs) {
1402
+ const loops = findLoopsInGraph(graph, threshold);
1403
+ if (loops.length > 0) {
1404
+ results.push({ graphId: graph.id, nodes: loops });
1405
+ }
1213
1406
  }
1407
+ return results;
1214
1408
  }
1215
- }
1216
- const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0, 6);
1217
- if (recentRecords.length > 0) {
1218
- writeLine(L, "");
1219
- writeLine(L, ` ${C.bold}${C.under}Recent Activity${C.reset}`);
1220
- for (const r of recentRecords) {
1221
- const icon = r.status === "ok" ? `${C.green}\u2713${C.reset}` : r.status === "error" ? `${C.red}\u2717${C.reset}` : r.status === "running" ? `${C.green}\u25B6${C.reset}` : `${C.dim}\u25CB${C.reset}`;
1222
- const t = new Date(r.lastActive).toLocaleTimeString();
1223
- const agent = truncate(r.id, 26).padEnd(26);
1224
- const age = Math.floor((Date.now() - r.lastActive) / 1e3);
1225
- const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
1226
- const det = truncate(r.detail, 25);
1227
- writeLine(L, ` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${det}${C.reset}`);
1409
+ };
1410
+ }
1411
+ function findLoopsInGraph(graph, threshold) {
1412
+ const loopNodes = [];
1413
+ function walk(nodeId, consecutiveCount, consecutiveType) {
1414
+ const node = graph.nodes.get(nodeId);
1415
+ if (!node) return;
1416
+ const newCount = node.type === consecutiveType ? consecutiveCount + 1 : 1;
1417
+ if (newCount > threshold) {
1418
+ loopNodes.push(node);
1419
+ }
1420
+ for (const childId of node.children) {
1421
+ walk(childId, newCount, node.type);
1228
1422
  }
1229
1423
  }
1230
- if (files.length === 0) {
1231
- writeLine(L, "");
1232
- writeLine(L, ` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
1233
- for (const d of config.dirs) writeLine(L, ` ${C.dim} ${d}${C.reset}`);
1234
- }
1235
- writeLine(L, "");
1236
- const dirLabel = config.dirs.length === 1 ? config.dirs[0] : `${config.dirs.length} directories`;
1237
- writeLine(L, ` ${C.dim}Watching: ${dirLabel}${C.reset}`);
1238
- writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
1239
- flushLines(L);
1424
+ walk(graph.rootNodeId, 0, null);
1425
+ return loopNodes;
1240
1426
  }
1241
- function getDistDepth(dt, spanId) {
1242
- if (!spanId) return 0;
1243
- const g = dt.graphs.get(spanId);
1244
- if (!g || !g.parentSpanId) return 0;
1245
- return 1 + getDistDepth(dt, g.parentSpanId);
1427
+
1428
+ // src/visualize.ts
1429
+ var STATUS_ICONS = {
1430
+ completed: "\u2713",
1431
+ failed: "\u2717",
1432
+ running: "\u231B",
1433
+ hung: "\u231B",
1434
+ timeout: "\u231B"
1435
+ };
1436
+ function formatDuration(ms) {
1437
+ if (ms < 1e3) return `${ms}ms`;
1438
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
1439
+ if (ms < 36e5) return `${(ms / 6e4).toFixed(1)}m`;
1440
+ return `${(ms / 36e5).toFixed(1)}h`;
1246
1441
  }
1247
- function startLive(argv) {
1248
- const config = parseArgs(argv);
1249
- const valid = config.dirs.filter((d) => existsSync2(d));
1250
- if (valid.length === 0) {
1251
- console.error(`No valid directories found: ${config.dirs.join(", ")}`);
1252
- console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
1253
- process.exit(1);
1254
- }
1255
- const invalid = config.dirs.filter((d) => !existsSync2(d));
1256
- if (invalid.length > 0) {
1257
- console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
1442
+ function nodeDuration(node, graphEndTime) {
1443
+ const end = node.endTime ?? graphEndTime;
1444
+ return formatDuration(end - node.startTime);
1445
+ }
1446
+ function getGenAiInfo(node) {
1447
+ const parts = [];
1448
+ const meta = node.metadata;
1449
+ if (meta["gen_ai.request.model"]) {
1450
+ parts.push(String(meta["gen_ai.request.model"]));
1451
+ }
1452
+ const tokens = meta["gen_ai.usage.prompt_tokens"] ?? meta["gen_ai.usage.completion_tokens"];
1453
+ if (tokens !== void 0) {
1454
+ const prompt = meta["gen_ai.usage.prompt_tokens"] ?? 0;
1455
+ const completion = meta["gen_ai.usage.completion_tokens"] ?? 0;
1456
+ if (prompt || completion) {
1457
+ parts.push(`${prompt + completion} tok`);
1458
+ }
1258
1459
  }
1259
- config.dirs = valid;
1260
- render(config);
1261
- let debounce = null;
1262
- for (const dir of config.dirs) {
1263
- try {
1264
- watch(dir, { recursive: config.recursive }, () => {
1265
- if (debounce) clearTimeout(debounce);
1266
- debounce = setTimeout(() => render(config), 500);
1267
- });
1268
- } catch {
1460
+ return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
1461
+ }
1462
+ function hasViolation(node, graph) {
1463
+ return graph.events.some(
1464
+ (e) => e.nodeId === node.id && e.eventType === "custom" && e.data.guardViolation !== void 0
1465
+ );
1466
+ }
1467
+ function toAsciiTree(graph) {
1468
+ if (graph.nodes.size === 0) return "(empty graph)";
1469
+ const now = Date.now();
1470
+ const endTime = graph.endTime ?? now;
1471
+ const lines = [];
1472
+ function renderNode(nodeId, prefix, isLast, isRoot) {
1473
+ const node = graph.nodes.get(nodeId);
1474
+ if (!node) return;
1475
+ const icon = STATUS_ICONS[node.status];
1476
+ const duration = nodeDuration(node, endTime);
1477
+ const genAi = getGenAiInfo(node);
1478
+ const violation = hasViolation(node, graph) ? " \u26A0" : "";
1479
+ const errorInfo = node.status === "failed" && node.metadata.error ? ` \u2014 ${node.metadata.error}` : "";
1480
+ const timeoutInfo = node.status === "timeout" ? " [TIMEOUT]" : "";
1481
+ const hungInfo = node.status === "hung" ? " [HUNG]" : "";
1482
+ const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1483
+ const line = `${prefix}${connector}${icon} ${node.name} (${node.type}) ${duration}${genAi}${violation}${timeoutInfo}${hungInfo}${errorInfo}`;
1484
+ lines.push(line);
1485
+ const children = getChildren(graph, nodeId);
1486
+ const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
1487
+ for (let i = 0; i < children.length; i++) {
1488
+ renderNode(children[i].id, childPrefix, i === children.length - 1, false);
1269
1489
  }
1270
1490
  }
1271
- setInterval(() => render(config), config.refreshMs);
1272
- process.on("SIGINT", () => {
1273
- console.log("\n" + C.dim + "Monitor stopped." + C.reset);
1274
- process.exit(0);
1275
- });
1491
+ renderNode(graph.rootNodeId, "", true, true);
1492
+ return lines.join("\n");
1493
+ }
1494
+ function toTimeline(graph) {
1495
+ if (graph.nodes.size === 0) return "(empty graph)";
1496
+ const now = Date.now();
1497
+ const graphStart = graph.startTime;
1498
+ const graphEnd = graph.endTime ?? now;
1499
+ const totalDuration = graphEnd - graphStart;
1500
+ if (totalDuration <= 0) return "(zero duration)";
1501
+ const barWidth = 60;
1502
+ const lines = [];
1503
+ const scaleLabels = [];
1504
+ const tickCount = Math.min(5, Math.max(2, Math.floor(barWidth / 10)));
1505
+ for (let i = 0; i <= tickCount; i++) {
1506
+ const t = totalDuration * i / tickCount;
1507
+ scaleLabels.push(formatDuration(t));
1508
+ }
1509
+ let header = "";
1510
+ for (let i = 0; i < scaleLabels.length; i++) {
1511
+ const pos = Math.round(barWidth * i / tickCount);
1512
+ while (header.length < pos) header += " ";
1513
+ header += scaleLabels[i];
1514
+ }
1515
+ lines.push(header);
1516
+ let tickLine = "";
1517
+ for (let i = 0; i < barWidth; i++) {
1518
+ const tickPos = tickCount > 0 ? i * tickCount / barWidth : 0;
1519
+ if (Number.isInteger(Math.round(tickPos * 100) / 100) && Math.abs(tickPos - Math.round(tickPos)) < 0.01) {
1520
+ tickLine += "\u253C";
1521
+ } else {
1522
+ tickLine += "\u2500";
1523
+ }
1524
+ }
1525
+ lines.push(tickLine);
1526
+ const orderedNodes = [];
1527
+ function collectNodes(nodeId) {
1528
+ const node = graph.nodes.get(nodeId);
1529
+ if (!node) return;
1530
+ orderedNodes.push(node);
1531
+ const children = getChildren(graph, nodeId);
1532
+ for (const child of children) {
1533
+ collectNodes(child.id);
1534
+ }
1535
+ }
1536
+ collectNodes(graph.rootNodeId);
1537
+ for (const node of orderedNodes) {
1538
+ const nodeStart = node.startTime - graphStart;
1539
+ const nodeEnd = (node.endTime ?? now) - graphStart;
1540
+ const startCol = Math.round(nodeStart / totalDuration * barWidth);
1541
+ const endCol = Math.max(startCol + 1, Math.round(nodeEnd / totalDuration * barWidth));
1542
+ let bar = "";
1543
+ for (let i = 0; i < barWidth; i++) {
1544
+ if (i >= startCol && i < endCol) {
1545
+ bar += "\u2588";
1546
+ } else {
1547
+ bar += " ";
1548
+ }
1549
+ }
1550
+ const icon = STATUS_ICONS[node.status];
1551
+ const duration = nodeDuration(node, graphEnd);
1552
+ const violation = hasViolation(node, graph) ? " \u26A0" : "";
1553
+ lines.push(`${bar} ${icon} ${node.name} (${duration})${violation}`);
1554
+ }
1555
+ return lines.join("\n");
1276
1556
  }
1277
1557
 
1278
1558
  // src/watch.ts
1279
1559
  import { existsSync as existsSync4 } from "fs";
1280
- import { resolve as resolve3, join as join3 } from "path";
1281
1560
  import { hostname } from "os";
1561
+ import { join as join4, resolve as resolve3 } from "path";
1562
+
1563
+ // src/watch-alerts.ts
1564
+ import { exec } from "child_process";
1565
+ import { request as httpRequest } from "http";
1566
+ import { request as httpsRequest } from "https";
1567
+ function formatAlertMessage(payload) {
1568
+ const time = new Date(payload.timestamp).toISOString();
1569
+ const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
1570
+ return [
1571
+ `[ALERT] ${payload.condition}: "${payload.agentId}"`,
1572
+ ` Status: ${arrow}`,
1573
+ payload.detail ? ` Detail: ${payload.detail}` : null,
1574
+ ` File: ${payload.file}`,
1575
+ ` Time: ${time}`
1576
+ ].filter(Boolean).join("\n");
1577
+ }
1578
+ function formatTelegram(payload) {
1579
+ const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
1580
+ const time = new Date(payload.timestamp).toLocaleTimeString();
1581
+ return [
1582
+ `${icon} *AgentFlow Alert*`,
1583
+ `*${payload.condition}*: \`${payload.agentId}\``,
1584
+ `Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
1585
+ payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
1586
+ `Time: ${time}`
1587
+ ].filter(Boolean).join("\n");
1588
+ }
1589
+ async function sendAlert(payload, channel) {
1590
+ try {
1591
+ switch (channel.type) {
1592
+ case "stdout":
1593
+ sendStdout(payload);
1594
+ break;
1595
+ case "telegram":
1596
+ await sendTelegram(payload, channel.botToken, channel.chatId);
1597
+ break;
1598
+ case "webhook":
1599
+ await sendWebhook(payload, channel.url);
1600
+ break;
1601
+ case "command":
1602
+ await sendCommand(payload, channel.cmd);
1603
+ break;
1604
+ }
1605
+ } catch (err) {
1606
+ const msg = err instanceof Error ? err.message : String(err);
1607
+ console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
1608
+ }
1609
+ }
1610
+ function sendStdout(payload) {
1611
+ console.log(formatAlertMessage(payload));
1612
+ }
1613
+ function sendTelegram(payload, botToken, chatId) {
1614
+ const body = JSON.stringify({
1615
+ chat_id: chatId,
1616
+ text: formatTelegram(payload),
1617
+ parse_mode: "Markdown"
1618
+ });
1619
+ return new Promise((resolve4, reject) => {
1620
+ const req = httpsRequest(
1621
+ `https://api.telegram.org/bot${botToken}/sendMessage`,
1622
+ {
1623
+ method: "POST",
1624
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
1625
+ },
1626
+ (res) => {
1627
+ res.resume();
1628
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1629
+ else reject(new Error(`Telegram API returned ${res.statusCode}`));
1630
+ }
1631
+ );
1632
+ req.on("error", reject);
1633
+ req.write(body);
1634
+ req.end();
1635
+ });
1636
+ }
1637
+ function sendWebhook(payload, url) {
1638
+ const body = JSON.stringify(payload);
1639
+ const isHttps = url.startsWith("https");
1640
+ const doRequest = isHttps ? httpsRequest : httpRequest;
1641
+ return new Promise((resolve4, reject) => {
1642
+ const req = doRequest(
1643
+ url,
1644
+ {
1645
+ method: "POST",
1646
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
1647
+ },
1648
+ (res) => {
1649
+ res.resume();
1650
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1651
+ else reject(new Error(`Webhook returned ${res.statusCode}`));
1652
+ }
1653
+ );
1654
+ req.on("error", reject);
1655
+ req.setTimeout(1e4, () => {
1656
+ req.destroy(new Error("Webhook timeout"));
1657
+ });
1658
+ req.write(body);
1659
+ req.end();
1660
+ });
1661
+ }
1662
+ function sendCommand(payload, cmd) {
1663
+ return new Promise((resolve4, reject) => {
1664
+ const env = {
1665
+ ...process.env,
1666
+ AGENTFLOW_ALERT_AGENT: payload.agentId,
1667
+ AGENTFLOW_ALERT_CONDITION: payload.condition,
1668
+ AGENTFLOW_ALERT_STATUS: payload.currentStatus,
1669
+ AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
1670
+ AGENTFLOW_ALERT_DETAIL: payload.detail,
1671
+ AGENTFLOW_ALERT_FILE: payload.file,
1672
+ AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
1673
+ };
1674
+ exec(cmd, { env, timeout: 3e4 }, (err) => {
1675
+ if (err) reject(err);
1676
+ else resolve4();
1677
+ });
1678
+ });
1679
+ }
1282
1680
 
1283
1681
  // src/watch-state.ts
1284
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync } from "fs";
1682
+ import { existsSync as existsSync3, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
1285
1683
  function parseDuration(input) {
1286
1684
  const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
1287
1685
  if (!match) {
@@ -1344,7 +1742,9 @@ function detectTransitions(previous, currentRecords, config, now) {
1344
1742
  const hasError = config.alertConditions.some((c) => c.type === "error");
1345
1743
  const hasRecovery = config.alertConditions.some((c) => c.type === "recovery");
1346
1744
  const staleConditions = config.alertConditions.filter((c) => c.type === "stale");
1347
- const consecutiveConditions = config.alertConditions.filter((c) => c.type === "consecutive-errors");
1745
+ const consecutiveConditions = config.alertConditions.filter(
1746
+ (c) => c.type === "consecutive-errors"
1747
+ );
1348
1748
  const byAgent = /* @__PURE__ */ new Map();
1349
1749
  for (const r of currentRecords) {
1350
1750
  const existing = byAgent.get(r.id);
@@ -1368,14 +1768,16 @@ function detectTransitions(previous, currentRecords, config, now) {
1368
1768
  for (const cond of consecutiveConditions) {
1369
1769
  if (newConsec === cond.threshold) {
1370
1770
  if (canAlert(prev, `consecutive-errors:${cond.threshold}`, config.cooldownMs, now)) {
1371
- alerts.push(makePayload(
1372
- agentId,
1373
- `consecutive-errors (${cond.threshold})`,
1374
- prevStatus,
1375
- currStatus,
1376
- { ...record, detail: `${newConsec} consecutive errors. ${record.detail}` },
1377
- config.dirs
1378
- ));
1771
+ alerts.push(
1772
+ makePayload(
1773
+ agentId,
1774
+ `consecutive-errors (${cond.threshold})`,
1775
+ prevStatus,
1776
+ currStatus,
1777
+ { ...record, detail: `${newConsec} consecutive errors. ${record.detail}` },
1778
+ config.dirs
1779
+ )
1780
+ );
1379
1781
  }
1380
1782
  }
1381
1783
  }
@@ -1384,14 +1786,16 @@ function detectTransitions(previous, currentRecords, config, now) {
1384
1786
  if (sinceActive > cond.durationMs && record.lastActive > 0) {
1385
1787
  if (canAlert(prev, "stale", config.cooldownMs, now)) {
1386
1788
  const mins = Math.floor(sinceActive / 6e4);
1387
- alerts.push(makePayload(
1388
- agentId,
1389
- "stale",
1390
- prevStatus,
1391
- currStatus,
1392
- { ...record, detail: `No update for ${mins}m. ${record.detail}` },
1393
- config.dirs
1394
- ));
1789
+ alerts.push(
1790
+ makePayload(
1791
+ agentId,
1792
+ "stale",
1793
+ prevStatus,
1794
+ currStatus,
1795
+ { ...record, detail: `No update for ${mins}m. ${record.detail}` },
1796
+ config.dirs
1797
+ )
1798
+ );
1395
1799
  }
1396
1800
  }
1397
1801
  }
@@ -1404,14 +1808,19 @@ function detectTransitions(previous, currentRecords, config, now) {
1404
1808
  if (canAlert(prev, "stale-auto", config.cooldownMs, now)) {
1405
1809
  const mins = Math.floor(sinceActive / 6e4);
1406
1810
  const expectedMins = Math.floor(expectedInterval / 6e4);
1407
- alerts.push(makePayload(
1408
- agentId,
1409
- "stale (auto)",
1410
- prevStatus,
1411
- currStatus,
1412
- { ...record, detail: `No update for ${mins}m (expected every ~${expectedMins}m). ${record.detail}` },
1413
- config.dirs
1414
- ));
1811
+ alerts.push(
1812
+ makePayload(
1813
+ agentId,
1814
+ "stale (auto)",
1815
+ prevStatus,
1816
+ currStatus,
1817
+ {
1818
+ ...record,
1819
+ detail: `No update for ${mins}m (expected every ~${expectedMins}m). ${record.detail}`
1820
+ },
1821
+ config.dirs
1822
+ )
1823
+ );
1415
1824
  }
1416
1825
  }
1417
1826
  }
@@ -1470,118 +1879,6 @@ function makePayload(agentId, condition, previousStatus, currentStatus, record,
1470
1879
  };
1471
1880
  }
1472
1881
 
1473
- // src/watch-alerts.ts
1474
- import { request as httpsRequest } from "https";
1475
- import { request as httpRequest } from "http";
1476
- import { exec } from "child_process";
1477
- function formatAlertMessage(payload) {
1478
- const time = new Date(payload.timestamp).toISOString();
1479
- const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
1480
- return [
1481
- `[ALERT] ${payload.condition}: "${payload.agentId}"`,
1482
- ` Status: ${arrow}`,
1483
- payload.detail ? ` Detail: ${payload.detail}` : null,
1484
- ` File: ${payload.file}`,
1485
- ` Time: ${time}`
1486
- ].filter(Boolean).join("\n");
1487
- }
1488
- function formatTelegram(payload) {
1489
- const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
1490
- const time = new Date(payload.timestamp).toLocaleTimeString();
1491
- return [
1492
- `${icon} *AgentFlow Alert*`,
1493
- `*${payload.condition}*: \`${payload.agentId}\``,
1494
- `Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
1495
- payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
1496
- `Time: ${time}`
1497
- ].filter(Boolean).join("\n");
1498
- }
1499
- async function sendAlert(payload, channel) {
1500
- try {
1501
- switch (channel.type) {
1502
- case "stdout":
1503
- sendStdout(payload);
1504
- break;
1505
- case "telegram":
1506
- await sendTelegram(payload, channel.botToken, channel.chatId);
1507
- break;
1508
- case "webhook":
1509
- await sendWebhook(payload, channel.url);
1510
- break;
1511
- case "command":
1512
- await sendCommand(payload, channel.cmd);
1513
- break;
1514
- }
1515
- } catch (err) {
1516
- const msg = err instanceof Error ? err.message : String(err);
1517
- console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
1518
- }
1519
- }
1520
- function sendStdout(payload) {
1521
- console.log(formatAlertMessage(payload));
1522
- }
1523
- function sendTelegram(payload, botToken, chatId) {
1524
- const body = JSON.stringify({
1525
- chat_id: chatId,
1526
- text: formatTelegram(payload),
1527
- parse_mode: "Markdown"
1528
- });
1529
- return new Promise((resolve4, reject) => {
1530
- const req = httpsRequest(
1531
- `https://api.telegram.org/bot${botToken}/sendMessage`,
1532
- { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
1533
- (res) => {
1534
- res.resume();
1535
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1536
- else reject(new Error(`Telegram API returned ${res.statusCode}`));
1537
- }
1538
- );
1539
- req.on("error", reject);
1540
- req.write(body);
1541
- req.end();
1542
- });
1543
- }
1544
- function sendWebhook(payload, url) {
1545
- const body = JSON.stringify(payload);
1546
- const isHttps = url.startsWith("https");
1547
- const doRequest = isHttps ? httpsRequest : httpRequest;
1548
- return new Promise((resolve4, reject) => {
1549
- const req = doRequest(
1550
- url,
1551
- { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
1552
- (res) => {
1553
- res.resume();
1554
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1555
- else reject(new Error(`Webhook returned ${res.statusCode}`));
1556
- }
1557
- );
1558
- req.on("error", reject);
1559
- req.setTimeout(1e4, () => {
1560
- req.destroy(new Error("Webhook timeout"));
1561
- });
1562
- req.write(body);
1563
- req.end();
1564
- });
1565
- }
1566
- function sendCommand(payload, cmd) {
1567
- return new Promise((resolve4, reject) => {
1568
- const env = {
1569
- ...process.env,
1570
- AGENTFLOW_ALERT_AGENT: payload.agentId,
1571
- AGENTFLOW_ALERT_CONDITION: payload.condition,
1572
- AGENTFLOW_ALERT_STATUS: payload.currentStatus,
1573
- AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
1574
- AGENTFLOW_ALERT_DETAIL: payload.detail,
1575
- AGENTFLOW_ALERT_FILE: payload.file,
1576
- AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
1577
- };
1578
- exec(cmd, { env, timeout: 3e4 }, (err) => {
1579
- if (err) reject(err);
1580
- else resolve4();
1581
- });
1582
- });
1583
- }
1584
-
1585
1882
  // src/watch.ts
1586
1883
  function parseWatchArgs(argv) {
1587
1884
  const dirs = [];
@@ -1623,7 +1920,9 @@ function parseWatchArgs(argv) {
1623
1920
  if (botToken && chatId) {
1624
1921
  notifyChannels.push({ type: "telegram", botToken, chatId });
1625
1922
  } else {
1626
- console.error("Warning: --notify telegram requires AGENTFLOW_TELEGRAM_BOT_TOKEN and AGENTFLOW_TELEGRAM_CHAT_ID env vars");
1923
+ console.error(
1924
+ "Warning: --notify telegram requires AGENTFLOW_TELEGRAM_BOT_TOKEN and AGENTFLOW_TELEGRAM_CHAT_ID env vars"
1925
+ );
1627
1926
  }
1628
1927
  } else if (val.startsWith("webhook:")) {
1629
1928
  notifyChannels.push({ type: "webhook", url: val.slice(8) });
@@ -1662,7 +1961,7 @@ function parseWatchArgs(argv) {
1662
1961
  }
1663
1962
  notifyChannels.unshift({ type: "stdout" });
1664
1963
  if (!stateFilePath) {
1665
- stateFilePath = join3(dirs[0], ".agentflow-watch-state.json");
1964
+ stateFilePath = join4(dirs[0], ".agentflow-watch-state.json");
1666
1965
  }
1667
1966
  return {
1668
1967
  dirs,
@@ -1675,7 +1974,8 @@ function parseWatchArgs(argv) {
1675
1974
  };
1676
1975
  }
1677
1976
  function printWatchUsage() {
1678
- console.log(`
1977
+ console.log(
1978
+ `
1679
1979
  AgentFlow Watch \u2014 headless alert system for agent infrastructure.
1680
1980
 
1681
1981
  Polls directories for JSON/JSONL files, detects failures and stale
@@ -1718,7 +2018,8 @@ Examples:
1718
2018
  agentflow watch ./data ./cron --notify telegram --poll 60
1719
2019
  agentflow watch ./traces --notify webhook:https://hooks.slack.com/... --alert-on consecutive-errors:3
1720
2020
  agentflow watch ./data --notify "command:curl -X POST https://my-pagerduty/alert"
1721
- `.trim());
2021
+ `.trim()
2022
+ );
1722
2023
  }
1723
2024
  function startWatch(argv) {
1724
2025
  const config = parseWatchArgs(argv);
@@ -1747,7 +2048,9 @@ agentflow watch started`);
1747
2048
  console.log(` Directories: ${valid.join(", ")}`);
1748
2049
  console.log(` Poll: ${config.pollIntervalMs / 1e3}s`);
1749
2050
  console.log(` Alert on: ${condLabels.join(", ")}`);
1750
- console.log(` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`);
2051
+ console.log(
2052
+ ` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`
2053
+ );
1751
2054
  console.log(` Cooldown: ${Math.floor(config.cooldownMs / 6e4)}m`);
1752
2055
  console.log(` State: ${config.stateFilePath}`);
1753
2056
  console.log(` Hostname: ${hostname()}`);
@@ -1773,9 +2076,13 @@ agentflow watch started`);
1773
2076
  if (pollCount % 10 === 0) {
1774
2077
  const agentCount = Object.keys(state.agents).length;
1775
2078
  const errorCount = Object.values(state.agents).filter((a) => a.lastStatus === "error").length;
1776
- const runningCount = Object.values(state.agents).filter((a) => a.lastStatus === "running").length;
2079
+ const runningCount = Object.values(state.agents).filter(
2080
+ (a) => a.lastStatus === "running"
2081
+ ).length;
1777
2082
  const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1778
- console.log(`[${time}] heartbeat: ${agentCount} agents, ${runningCount} running, ${errorCount} errors, ${files.length} files`);
2083
+ console.log(
2084
+ `[${time}] heartbeat: ${agentCount} agents, ${runningCount} running, ${errorCount} errors, ${files.length} files`
2085
+ );
1779
2086
  }
1780
2087
  }
1781
2088
  poll();
@@ -1792,10 +2099,6 @@ agentflow watch started`);
1792
2099
  }
1793
2100
 
1794
2101
  export {
1795
- createGraphBuilder,
1796
- loadGraph,
1797
- graphToJson,
1798
- runTraced,
1799
2102
  getNode,
1800
2103
  getChildren,
1801
2104
  getParent,
@@ -1811,5 +2114,10 @@ export {
1811
2114
  stitchTrace,
1812
2115
  getTraceTree,
1813
2116
  startLive,
2117
+ createGraphBuilder,
2118
+ runTraced,
2119
+ createTraceStore,
2120
+ toAsciiTree,
2121
+ toTimeline,
1814
2122
  startWatch
1815
2123
  };