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