agentflow-core 0.3.3 → 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,1289 +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(", ")}`);
556
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)}`);
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
700
  }
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;
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);
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}`;
769
+ }
770
+ }
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++;
761
853
  }
762
854
  }
763
855
  }
764
- return "unknown";
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
+ }
882
+ }
883
+ }
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
+ );
899
+ }
900
+ }
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}`);
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);
765
911
  }
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;
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);
938
+ });
939
+ } catch {
773
940
  }
774
941
  }
775
- return 0;
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
+ });
776
947
  }
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;
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);
784
957
  }
958
+ return obj;
785
959
  }
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;
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);
791
966
  }
792
967
  }
793
- return parts.join(" | ") || "";
968
+ return obj;
794
969
  }
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;
804
- }
970
+ function createCounterIdGenerator() {
971
+ let counter = 0;
972
+ return () => {
973
+ counter++;
974
+ return `node_${String(counter).padStart(3, "0")}`;
975
+ };
805
976
  }
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
824
- });
825
- } catch {
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");
826
995
  }
827
- return records;
828
996
  }
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 });
997
+ function getNode2(nodeId) {
998
+ const node = nodes.get(nodeId);
999
+ if (!node) {
1000
+ throw new Error(`GraphBuilder: node "${nodeId}" does not exist`);
840
1001
  }
841
- return records;
1002
+ return node;
842
1003
  }
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 });
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;
1021
+ }
1022
+ if (node.status === "running") {
1023
+ graphStatus = "running";
855
1024
  }
856
- return records;
857
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);
858
1060
  }
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;
864
- }
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 }];
879
- }
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;
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`);
896
1074
  }
897
- const entryType = entry.type;
898
- if (entryType === "session") {
899
- sessionId = entry.id ?? "";
900
- continue;
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);
1092
+ }
1093
+ edges.push({ from: parentId, to: id, type: "spawned" });
901
1094
  }
902
- if (entryType === "model_change") {
903
- model = entry.modelId ?? "";
904
- continue;
1095
+ if (rootNodeId === null) {
1096
+ rootNodeId = id;
905
1097
  }
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;
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
+ );
919
1108
  }
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
- }
931
- }
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
+ );
932
1120
  }
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
- }
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;
943
1128
  }
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
- }
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();
954
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
- const termWidth = process.stdout.columns || 120;
1140
- const detailWidth = Math.max(20, termWidth - 60);
1141
- if (firstRender) {
1142
- process.stdout.write("\x1B[2J");
1143
- firstRender = false;
1316
+ if (tracePaths.length > 0) {
1317
+ console.log(`\u{1F50D} Run "agentflow trace show ${orchestratorGraph.id} --traces-dir ${resolvedTracesDir}" to inspect`);
1144
1318
  }
1145
- const L = [];
1146
- writeLine(L, `${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
1147
- 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}`);
1148
- const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
1149
- const pad1 = Math.max(0, 64 - metaLine.length);
1150
- writeLine(L, `${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`);
1151
- writeLine(L, `${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
1152
- const sc = totFail === 0 ? C.green : C.yellow;
1153
- writeLine(L, "");
1154
- 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}`);
1155
- writeLine(L, "");
1156
- writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
1157
- writeLine(L, "");
1158
- writeLine(L, ` ${C.bold}${C.under}Agent Status Last Active Detail${C.reset}`);
1159
- let lineCount = 0;
1160
- for (const g of groups) {
1161
- if (lineCount > 35) break;
1162
- const isRecent = Date.now() - g.lastTs < 3e5;
1163
- const icon = statusIcon(g.status, isRecent);
1164
- const active = isRecent ? `${C.green}${timeStr(g.lastTs)}${C.reset}` : `${C.dim}${timeStr(g.lastTs)}${C.reset}`;
1165
- if (g.children.length === 0) {
1166
- const name = truncate(g.name, 26).padEnd(26);
1167
- const st = statusText(g);
1168
- const det = truncate(g.detail, detailWidth);
1169
- writeLine(L, ` ${icon} ${name} ${st.padEnd(20)} ${active.padEnd(20)} ${C.dim}${det}${C.reset}`);
1170
- lineCount++;
1171
- } else {
1172
- const name = truncate(g.name, 24).padEnd(24);
1173
- const st = statusText(g);
1174
- const tag = sourceTag(g.source);
1175
- writeLine(L, ` ${icon} ${C.bold}${name}${C.reset} ${st.padEnd(20)} ${active.padEnd(20)} ${tag} ${C.dim}(${g.children.length} agents)${C.reset}`);
1176
- lineCount++;
1177
- const kids = g.children.slice(0, 12);
1178
- for (let i = 0; i < kids.length; i++) {
1179
- if (lineCount > 35) break;
1180
- const child = kids[i];
1181
- const isLast = i === kids.length - 1;
1182
- const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
1183
- const cIcon = statusIcon(child.status, Date.now() - child.lastActive < 3e5);
1184
- const cName = truncate(child.id, 22).padEnd(22);
1185
- const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
1186
- const cDet = truncate(child.detail, detailWidth - 5);
1187
- writeLine(L, ` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`);
1188
- lineCount++;
1189
- }
1190
- if (g.children.length > 12) {
1191
- writeLine(L, ` ${C.dim} ... +${g.children.length - 12} more${C.reset}`);
1192
- 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 {
1193
1352
  }
1194
1353
  }
1354
+ return graphs;
1195
1355
  }
1196
- if (distributedTraces.length > 0) {
1197
- writeLine(L, "");
1198
- writeLine(L, ` ${C.bold}${C.under}Distributed Traces${C.reset}`);
1199
- for (const dt of distributedTraces.slice(0, 3)) {
1200
- const traceTime = new Date(dt.startTime).toLocaleTimeString();
1201
- const si = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
1202
- const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
1203
- const tid = dt.traceId.slice(0, 8);
1204
- writeLine(L, ` ${si} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`);
1205
- const tree = getTraceTree(dt);
1206
- for (let i = 0; i < Math.min(tree.length, 6); i++) {
1207
- const tg = tree[i];
1208
- const depth = getDistDepth(dt, tg.spanId);
1209
- const indent = " " + "\u2502 ".repeat(Math.max(0, depth - 1));
1210
- const isLast = i === tree.length - 1 || getDistDepth(dt, tree[i + 1]?.spanId) <= depth;
1211
- const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1212
- const gs = tg.status === "completed" ? `${C.green}\u2713${C.reset}` : tg.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
1213
- const gd = tg.endTime ? `${tg.endTime - tg.startTime}ms` : "running";
1214
- 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
+ }
1215
1406
  }
1407
+ return results;
1216
1408
  }
1217
- }
1218
- const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0, 6);
1219
- if (recentRecords.length > 0) {
1220
- writeLine(L, "");
1221
- writeLine(L, ` ${C.bold}${C.under}Recent Activity${C.reset}`);
1222
- for (const r of recentRecords) {
1223
- 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}`;
1224
- const t = new Date(r.lastActive).toLocaleTimeString();
1225
- const agent = truncate(r.id, 26).padEnd(26);
1226
- const age = Math.floor((Date.now() - r.lastActive) / 1e3);
1227
- const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
1228
- const det = truncate(r.detail, detailWidth);
1229
- 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);
1230
1422
  }
1231
1423
  }
1232
- if (files.length === 0) {
1233
- writeLine(L, "");
1234
- writeLine(L, ` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
1235
- for (const d of config.dirs) writeLine(L, ` ${C.dim} ${d}${C.reset}`);
1236
- }
1237
- writeLine(L, "");
1238
- const dirLabel = config.dirs.length === 1 ? config.dirs[0] : `${config.dirs.length} directories`;
1239
- writeLine(L, ` ${C.dim}Watching: ${dirLabel}${C.reset}`);
1240
- writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
1241
- flushLines(L);
1424
+ walk(graph.rootNodeId, 0, null);
1425
+ return loopNodes;
1242
1426
  }
1243
- function getDistDepth(dt, spanId) {
1244
- if (!spanId) return 0;
1245
- const g = dt.graphs.get(spanId);
1246
- if (!g || !g.parentSpanId) return 0;
1247
- 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`;
1248
1441
  }
1249
- function startLive(argv) {
1250
- const config = parseArgs(argv);
1251
- const valid = config.dirs.filter((d) => existsSync2(d));
1252
- if (valid.length === 0) {
1253
- console.error(`No valid directories found: ${config.dirs.join(", ")}`);
1254
- console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
1255
- process.exit(1);
1256
- }
1257
- const invalid = config.dirs.filter((d) => !existsSync2(d));
1258
- if (invalid.length > 0) {
1259
- 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
+ }
1260
1459
  }
1261
- config.dirs = valid;
1262
- render(config);
1263
- let debounce = null;
1264
- for (const dir of config.dirs) {
1265
- try {
1266
- watch(dir, { recursive: config.recursive }, () => {
1267
- if (debounce) clearTimeout(debounce);
1268
- debounce = setTimeout(() => render(config), 500);
1269
- });
1270
- } 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);
1271
1489
  }
1272
1490
  }
1273
- setInterval(() => render(config), config.refreshMs);
1274
- process.on("SIGINT", () => {
1275
- console.log("\n" + C.dim + "Monitor stopped." + C.reset);
1276
- process.exit(0);
1277
- });
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");
1278
1556
  }
1279
1557
 
1280
1558
  // src/watch.ts
1281
1559
  import { existsSync as existsSync4 } from "fs";
1282
- import { resolve as resolve3, join as join3 } from "path";
1283
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
+ }
1284
1680
 
1285
1681
  // src/watch-state.ts
1286
- 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";
1287
1683
  function parseDuration(input) {
1288
1684
  const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
1289
1685
  if (!match) {
@@ -1346,7 +1742,9 @@ function detectTransitions(previous, currentRecords, config, now) {
1346
1742
  const hasError = config.alertConditions.some((c) => c.type === "error");
1347
1743
  const hasRecovery = config.alertConditions.some((c) => c.type === "recovery");
1348
1744
  const staleConditions = config.alertConditions.filter((c) => c.type === "stale");
1349
- const consecutiveConditions = config.alertConditions.filter((c) => c.type === "consecutive-errors");
1745
+ const consecutiveConditions = config.alertConditions.filter(
1746
+ (c) => c.type === "consecutive-errors"
1747
+ );
1350
1748
  const byAgent = /* @__PURE__ */ new Map();
1351
1749
  for (const r of currentRecords) {
1352
1750
  const existing = byAgent.get(r.id);
@@ -1370,14 +1768,16 @@ function detectTransitions(previous, currentRecords, config, now) {
1370
1768
  for (const cond of consecutiveConditions) {
1371
1769
  if (newConsec === cond.threshold) {
1372
1770
  if (canAlert(prev, `consecutive-errors:${cond.threshold}`, config.cooldownMs, now)) {
1373
- alerts.push(makePayload(
1374
- agentId,
1375
- `consecutive-errors (${cond.threshold})`,
1376
- prevStatus,
1377
- currStatus,
1378
- { ...record, detail: `${newConsec} consecutive errors. ${record.detail}` },
1379
- config.dirs
1380
- ));
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
+ );
1381
1781
  }
1382
1782
  }
1383
1783
  }
@@ -1386,14 +1786,16 @@ function detectTransitions(previous, currentRecords, config, now) {
1386
1786
  if (sinceActive > cond.durationMs && record.lastActive > 0) {
1387
1787
  if (canAlert(prev, "stale", config.cooldownMs, now)) {
1388
1788
  const mins = Math.floor(sinceActive / 6e4);
1389
- alerts.push(makePayload(
1390
- agentId,
1391
- "stale",
1392
- prevStatus,
1393
- currStatus,
1394
- { ...record, detail: `No update for ${mins}m. ${record.detail}` },
1395
- config.dirs
1396
- ));
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
+ );
1397
1799
  }
1398
1800
  }
1399
1801
  }
@@ -1406,14 +1808,19 @@ function detectTransitions(previous, currentRecords, config, now) {
1406
1808
  if (canAlert(prev, "stale-auto", config.cooldownMs, now)) {
1407
1809
  const mins = Math.floor(sinceActive / 6e4);
1408
1810
  const expectedMins = Math.floor(expectedInterval / 6e4);
1409
- alerts.push(makePayload(
1410
- agentId,
1411
- "stale (auto)",
1412
- prevStatus,
1413
- currStatus,
1414
- { ...record, detail: `No update for ${mins}m (expected every ~${expectedMins}m). ${record.detail}` },
1415
- config.dirs
1416
- ));
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
+ );
1417
1824
  }
1418
1825
  }
1419
1826
  }
@@ -1472,118 +1879,6 @@ function makePayload(agentId, condition, previousStatus, currentStatus, record,
1472
1879
  };
1473
1880
  }
1474
1881
 
1475
- // src/watch-alerts.ts
1476
- import { request as httpsRequest } from "https";
1477
- import { request as httpRequest } from "http";
1478
- import { exec } from "child_process";
1479
- function formatAlertMessage(payload) {
1480
- const time = new Date(payload.timestamp).toISOString();
1481
- const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
1482
- return [
1483
- `[ALERT] ${payload.condition}: "${payload.agentId}"`,
1484
- ` Status: ${arrow}`,
1485
- payload.detail ? ` Detail: ${payload.detail}` : null,
1486
- ` File: ${payload.file}`,
1487
- ` Time: ${time}`
1488
- ].filter(Boolean).join("\n");
1489
- }
1490
- function formatTelegram(payload) {
1491
- const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
1492
- const time = new Date(payload.timestamp).toLocaleTimeString();
1493
- return [
1494
- `${icon} *AgentFlow Alert*`,
1495
- `*${payload.condition}*: \`${payload.agentId}\``,
1496
- `Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
1497
- payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
1498
- `Time: ${time}`
1499
- ].filter(Boolean).join("\n");
1500
- }
1501
- async function sendAlert(payload, channel) {
1502
- try {
1503
- switch (channel.type) {
1504
- case "stdout":
1505
- sendStdout(payload);
1506
- break;
1507
- case "telegram":
1508
- await sendTelegram(payload, channel.botToken, channel.chatId);
1509
- break;
1510
- case "webhook":
1511
- await sendWebhook(payload, channel.url);
1512
- break;
1513
- case "command":
1514
- await sendCommand(payload, channel.cmd);
1515
- break;
1516
- }
1517
- } catch (err) {
1518
- const msg = err instanceof Error ? err.message : String(err);
1519
- console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
1520
- }
1521
- }
1522
- function sendStdout(payload) {
1523
- console.log(formatAlertMessage(payload));
1524
- }
1525
- function sendTelegram(payload, botToken, chatId) {
1526
- const body = JSON.stringify({
1527
- chat_id: chatId,
1528
- text: formatTelegram(payload),
1529
- parse_mode: "Markdown"
1530
- });
1531
- return new Promise((resolve4, reject) => {
1532
- const req = httpsRequest(
1533
- `https://api.telegram.org/bot${botToken}/sendMessage`,
1534
- { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
1535
- (res) => {
1536
- res.resume();
1537
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1538
- else reject(new Error(`Telegram API returned ${res.statusCode}`));
1539
- }
1540
- );
1541
- req.on("error", reject);
1542
- req.write(body);
1543
- req.end();
1544
- });
1545
- }
1546
- function sendWebhook(payload, url) {
1547
- const body = JSON.stringify(payload);
1548
- const isHttps = url.startsWith("https");
1549
- const doRequest = isHttps ? httpsRequest : httpRequest;
1550
- return new Promise((resolve4, reject) => {
1551
- const req = doRequest(
1552
- url,
1553
- { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
1554
- (res) => {
1555
- res.resume();
1556
- if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1557
- else reject(new Error(`Webhook returned ${res.statusCode}`));
1558
- }
1559
- );
1560
- req.on("error", reject);
1561
- req.setTimeout(1e4, () => {
1562
- req.destroy(new Error("Webhook timeout"));
1563
- });
1564
- req.write(body);
1565
- req.end();
1566
- });
1567
- }
1568
- function sendCommand(payload, cmd) {
1569
- return new Promise((resolve4, reject) => {
1570
- const env = {
1571
- ...process.env,
1572
- AGENTFLOW_ALERT_AGENT: payload.agentId,
1573
- AGENTFLOW_ALERT_CONDITION: payload.condition,
1574
- AGENTFLOW_ALERT_STATUS: payload.currentStatus,
1575
- AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
1576
- AGENTFLOW_ALERT_DETAIL: payload.detail,
1577
- AGENTFLOW_ALERT_FILE: payload.file,
1578
- AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
1579
- };
1580
- exec(cmd, { env, timeout: 3e4 }, (err) => {
1581
- if (err) reject(err);
1582
- else resolve4();
1583
- });
1584
- });
1585
- }
1586
-
1587
1882
  // src/watch.ts
1588
1883
  function parseWatchArgs(argv) {
1589
1884
  const dirs = [];
@@ -1625,7 +1920,9 @@ function parseWatchArgs(argv) {
1625
1920
  if (botToken && chatId) {
1626
1921
  notifyChannels.push({ type: "telegram", botToken, chatId });
1627
1922
  } else {
1628
- 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
+ );
1629
1926
  }
1630
1927
  } else if (val.startsWith("webhook:")) {
1631
1928
  notifyChannels.push({ type: "webhook", url: val.slice(8) });
@@ -1664,7 +1961,7 @@ function parseWatchArgs(argv) {
1664
1961
  }
1665
1962
  notifyChannels.unshift({ type: "stdout" });
1666
1963
  if (!stateFilePath) {
1667
- stateFilePath = join3(dirs[0], ".agentflow-watch-state.json");
1964
+ stateFilePath = join4(dirs[0], ".agentflow-watch-state.json");
1668
1965
  }
1669
1966
  return {
1670
1967
  dirs,
@@ -1677,7 +1974,8 @@ function parseWatchArgs(argv) {
1677
1974
  };
1678
1975
  }
1679
1976
  function printWatchUsage() {
1680
- console.log(`
1977
+ console.log(
1978
+ `
1681
1979
  AgentFlow Watch \u2014 headless alert system for agent infrastructure.
1682
1980
 
1683
1981
  Polls directories for JSON/JSONL files, detects failures and stale
@@ -1720,7 +2018,8 @@ Examples:
1720
2018
  agentflow watch ./data ./cron --notify telegram --poll 60
1721
2019
  agentflow watch ./traces --notify webhook:https://hooks.slack.com/... --alert-on consecutive-errors:3
1722
2020
  agentflow watch ./data --notify "command:curl -X POST https://my-pagerduty/alert"
1723
- `.trim());
2021
+ `.trim()
2022
+ );
1724
2023
  }
1725
2024
  function startWatch(argv) {
1726
2025
  const config = parseWatchArgs(argv);
@@ -1749,7 +2048,9 @@ agentflow watch started`);
1749
2048
  console.log(` Directories: ${valid.join(", ")}`);
1750
2049
  console.log(` Poll: ${config.pollIntervalMs / 1e3}s`);
1751
2050
  console.log(` Alert on: ${condLabels.join(", ")}`);
1752
- console.log(` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`);
2051
+ console.log(
2052
+ ` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`
2053
+ );
1753
2054
  console.log(` Cooldown: ${Math.floor(config.cooldownMs / 6e4)}m`);
1754
2055
  console.log(` State: ${config.stateFilePath}`);
1755
2056
  console.log(` Hostname: ${hostname()}`);
@@ -1775,9 +2076,13 @@ agentflow watch started`);
1775
2076
  if (pollCount % 10 === 0) {
1776
2077
  const agentCount = Object.keys(state.agents).length;
1777
2078
  const errorCount = Object.values(state.agents).filter((a) => a.lastStatus === "error").length;
1778
- 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;
1779
2082
  const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1780
- 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
+ );
1781
2086
  }
1782
2087
  }
1783
2088
  poll();
@@ -1794,10 +2099,6 @@ agentflow watch started`);
1794
2099
  }
1795
2100
 
1796
2101
  export {
1797
- createGraphBuilder,
1798
- loadGraph,
1799
- graphToJson,
1800
- runTraced,
1801
2102
  getNode,
1802
2103
  getChildren,
1803
2104
  getParent,
@@ -1813,5 +2114,10 @@ export {
1813
2114
  stitchTrace,
1814
2115
  getTraceTree,
1815
2116
  startLive,
2117
+ createGraphBuilder,
2118
+ runTraced,
2119
+ createTraceStore,
2120
+ toAsciiTree,
2121
+ toTimeline,
1816
2122
  startWatch
1817
2123
  };