agentflow-core 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-FJVQYJFB.js +1105 -0
- package/dist/cli.cjs +670 -12
- package/dist/cli.js +57 -14
- package/dist/index.cjs +519 -63
- package/dist/index.d.cts +19 -1
- package/dist/index.d.ts +19 -1
- package/dist/index.js +18 -205
- package/package.json +1 -1
- package/dist/chunk-TT5DLU73.js +0 -435
|
@@ -0,0 +1,1105 @@
|
|
|
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);
|
|
9
|
+
}
|
|
10
|
+
return obj;
|
|
11
|
+
}
|
|
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);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return obj;
|
|
21
|
+
}
|
|
22
|
+
function createCounterIdGenerator() {
|
|
23
|
+
let counter = 0;
|
|
24
|
+
return () => {
|
|
25
|
+
counter++;
|
|
26
|
+
return `node_${String(counter).padStart(3, "0")}`;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
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");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function getNode2(nodeId) {
|
|
50
|
+
const node = nodes.get(nodeId);
|
|
51
|
+
if (!node) {
|
|
52
|
+
throw new Error(`GraphBuilder: node "${nodeId}" does not exist`);
|
|
53
|
+
}
|
|
54
|
+
return node;
|
|
55
|
+
}
|
|
56
|
+
function recordEvent(nodeId, eventType, data = {}) {
|
|
57
|
+
events.push({
|
|
58
|
+
timestamp: Date.now(),
|
|
59
|
+
eventType,
|
|
60
|
+
nodeId,
|
|
61
|
+
data
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function buildGraph() {
|
|
65
|
+
if (rootNodeId === null) {
|
|
66
|
+
throw new Error("GraphBuilder: cannot build a graph with no nodes");
|
|
67
|
+
}
|
|
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
|
+
}
|
|
77
|
+
}
|
|
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
|
+
}
|
|
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));
|
|
235
|
+
}
|
|
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
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function graphToJson(graph) {
|
|
258
|
+
const nodesObj = {};
|
|
259
|
+
for (const [id, node] of graph.nodes) {
|
|
260
|
+
nodesObj[id] = node;
|
|
261
|
+
}
|
|
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
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/runner.ts
|
|
280
|
+
import { spawnSync } from "child_process";
|
|
281
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
282
|
+
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}$`);
|
|
286
|
+
}
|
|
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);
|
|
293
|
+
try {
|
|
294
|
+
const stat = statSync(full);
|
|
295
|
+
if (stat.isFile()) {
|
|
296
|
+
result.set(full, stat.mtimeMs);
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
function agentIdFromFilename(filePath) {
|
|
304
|
+
const base = basename(filePath, ".json");
|
|
305
|
+
const cleaned = base.replace(/-state$/, "");
|
|
306
|
+
return `alfred-${cleaned}`;
|
|
307
|
+
}
|
|
308
|
+
function deriveAgentId(command) {
|
|
309
|
+
return "orchestrator";
|
|
310
|
+
}
|
|
311
|
+
function fileTimestamp() {
|
|
312
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
|
|
313
|
+
}
|
|
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);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
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());
|
|
408
|
+
}
|
|
409
|
+
if (!existsSync(resolvedTracesDir)) {
|
|
410
|
+
mkdirSync(resolvedTracesDir, { recursive: true });
|
|
411
|
+
}
|
|
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);
|
|
419
|
+
}
|
|
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);
|
|
433
|
+
}
|
|
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);
|
|
441
|
+
}
|
|
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
|
+
}
|
|
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;
|
|
464
|
+
}
|
|
465
|
+
function dfs(node) {
|
|
466
|
+
if (node.children.length === 0) {
|
|
467
|
+
return { duration: nodeDuration(node), path: [node] };
|
|
468
|
+
}
|
|
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;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
duration: nodeDuration(node) + bestChild.duration,
|
|
480
|
+
path: [node, ...bestChild.path]
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return dfs(root).path;
|
|
484
|
+
}
|
|
485
|
+
function findWaitingOn(graph, nodeId) {
|
|
486
|
+
const results = [];
|
|
487
|
+
for (const edge of graph.edges) {
|
|
488
|
+
if (edge.from === nodeId && edge.type === "waited_on") {
|
|
489
|
+
const node = graph.nodes.get(edge.to);
|
|
490
|
+
if (node) results.push(node);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return results;
|
|
494
|
+
}
|
|
495
|
+
function getSubtree(graph, nodeId) {
|
|
496
|
+
const startNode = graph.nodes.get(nodeId);
|
|
497
|
+
if (!startNode) return [];
|
|
498
|
+
const result = [];
|
|
499
|
+
const queue = [...startNode.children];
|
|
500
|
+
while (queue.length > 0) {
|
|
501
|
+
const currentId = queue.shift();
|
|
502
|
+
if (currentId === void 0) break;
|
|
503
|
+
const current = graph.nodes.get(currentId);
|
|
504
|
+
if (!current) continue;
|
|
505
|
+
result.push(current);
|
|
506
|
+
queue.push(...current.children);
|
|
507
|
+
}
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
function getDuration(graph) {
|
|
511
|
+
const end = graph.endTime ?? Date.now();
|
|
512
|
+
return end - graph.startTime;
|
|
513
|
+
}
|
|
514
|
+
function getDepth(graph) {
|
|
515
|
+
const root = graph.nodes.get(graph.rootNodeId);
|
|
516
|
+
if (!root) return -1;
|
|
517
|
+
function dfs(node, depth) {
|
|
518
|
+
if (node.children.length === 0) return depth;
|
|
519
|
+
let maxDepth = depth;
|
|
520
|
+
for (const childId of node.children) {
|
|
521
|
+
const child = graph.nodes.get(childId);
|
|
522
|
+
if (!child) continue;
|
|
523
|
+
const childDepth = dfs(child, depth + 1);
|
|
524
|
+
if (childDepth > maxDepth) maxDepth = childDepth;
|
|
525
|
+
}
|
|
526
|
+
return maxDepth;
|
|
527
|
+
}
|
|
528
|
+
return dfs(root, 0);
|
|
529
|
+
}
|
|
530
|
+
function getStats(graph) {
|
|
531
|
+
const byStatus = {
|
|
532
|
+
running: 0,
|
|
533
|
+
completed: 0,
|
|
534
|
+
failed: 0,
|
|
535
|
+
hung: 0,
|
|
536
|
+
timeout: 0
|
|
537
|
+
};
|
|
538
|
+
const byType = {
|
|
539
|
+
agent: 0,
|
|
540
|
+
tool: 0,
|
|
541
|
+
subagent: 0,
|
|
542
|
+
wait: 0,
|
|
543
|
+
decision: 0,
|
|
544
|
+
custom: 0
|
|
545
|
+
};
|
|
546
|
+
let failureCount = 0;
|
|
547
|
+
let hungCount = 0;
|
|
548
|
+
for (const node of graph.nodes.values()) {
|
|
549
|
+
byStatus[node.status]++;
|
|
550
|
+
byType[node.type]++;
|
|
551
|
+
if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
|
|
552
|
+
failureCount++;
|
|
553
|
+
}
|
|
554
|
+
if (node.status === "running" && node.endTime === null) {
|
|
555
|
+
hungCount++;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
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;
|
|
590
|
+
}
|
|
591
|
+
if (g.parentSpanId) {
|
|
592
|
+
const siblings = childMap.get(g.parentSpanId) ?? [];
|
|
593
|
+
if (g.spanId) siblings.push(g.spanId);
|
|
594
|
+
childMap.set(g.parentSpanId, siblings);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
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
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function getTraceTree(trace) {
|
|
621
|
+
const result = [];
|
|
622
|
+
function walk(spanId) {
|
|
623
|
+
const graph = trace.graphs.get(spanId);
|
|
624
|
+
if (graph) result.push(graph);
|
|
625
|
+
const children = trace.childMap.get(spanId) ?? [];
|
|
626
|
+
for (const childSpan of children) walk(childSpan);
|
|
627
|
+
}
|
|
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 = { tracesDir: ".", 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 === "--traces-dir" || arg === "-t") {
|
|
668
|
+
i++;
|
|
669
|
+
config.tracesDir = args[i] ?? config.tracesDir;
|
|
670
|
+
i++;
|
|
671
|
+
} else if (!arg.startsWith("-")) {
|
|
672
|
+
config.tracesDir = arg;
|
|
673
|
+
i++;
|
|
674
|
+
} else {
|
|
675
|
+
i++;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
config.tracesDir = resolve2(config.tracesDir);
|
|
679
|
+
return config;
|
|
680
|
+
}
|
|
681
|
+
function printUsage() {
|
|
682
|
+
console.log(`
|
|
683
|
+
AgentFlow Live Monitor \u2014 real-time terminal dashboard for agent systems.
|
|
684
|
+
|
|
685
|
+
Auto-detects agent traces, state files, job schedulers, and session logs
|
|
686
|
+
from any JSON/JSONL files in the watched directory.
|
|
687
|
+
|
|
688
|
+
Usage:
|
|
689
|
+
agentflow live [directory] [options]
|
|
690
|
+
|
|
691
|
+
Arguments:
|
|
692
|
+
directory Directory to watch (default: current directory)
|
|
693
|
+
|
|
694
|
+
Options:
|
|
695
|
+
-r, --refresh <secs> Refresh interval in seconds (default: 3)
|
|
696
|
+
-R, --recursive Scan subdirectories (1 level deep)
|
|
697
|
+
-h, --help Show this help message
|
|
698
|
+
|
|
699
|
+
Examples:
|
|
700
|
+
agentflow live ./data
|
|
701
|
+
agentflow live ./traces --refresh 5
|
|
702
|
+
agentflow live /var/lib/myagent -R
|
|
703
|
+
`.trim());
|
|
704
|
+
}
|
|
705
|
+
function scanFiles(dir, recursive) {
|
|
706
|
+
const results = [];
|
|
707
|
+
function scanDir(d) {
|
|
708
|
+
try {
|
|
709
|
+
for (const f of readdirSync2(d)) {
|
|
710
|
+
if (f.startsWith(".")) continue;
|
|
711
|
+
const fp = join2(d, f);
|
|
712
|
+
let stat;
|
|
713
|
+
try {
|
|
714
|
+
stat = statSync2(fp);
|
|
715
|
+
} catch {
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (stat.isDirectory() && recursive && d === dir) {
|
|
719
|
+
scanDir(fp);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
if (!stat.isFile()) continue;
|
|
723
|
+
if (f.endsWith(".json")) {
|
|
724
|
+
results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".json" });
|
|
725
|
+
} else if (f.endsWith(".jsonl")) {
|
|
726
|
+
results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".jsonl" });
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
} catch {
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
scanDir(dir);
|
|
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;
|
|
741
|
+
}
|
|
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
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
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;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
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;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
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;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
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;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function processJsonFile(file) {
|
|
807
|
+
const raw = safeReadJson(file.path);
|
|
808
|
+
if (raw === null) return [];
|
|
809
|
+
const records = [];
|
|
810
|
+
const trace = tryLoadTrace(file.path, raw);
|
|
811
|
+
if (trace) {
|
|
812
|
+
try {
|
|
813
|
+
const fails = getFailures(trace);
|
|
814
|
+
const hung = getHungNodes(trace);
|
|
815
|
+
const stats = getStats(trace);
|
|
816
|
+
records.push({
|
|
817
|
+
id: trace.agentId,
|
|
818
|
+
source: "trace",
|
|
819
|
+
status: fails.length === 0 && hung.length === 0 ? "ok" : "error",
|
|
820
|
+
lastActive: file.mtime,
|
|
821
|
+
detail: `${stats.totalNodes} nodes [${trace.trigger}]`,
|
|
822
|
+
file: file.filename,
|
|
823
|
+
traceData: trace
|
|
824
|
+
});
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
827
|
+
return records;
|
|
828
|
+
}
|
|
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 });
|
|
840
|
+
}
|
|
841
|
+
return records;
|
|
842
|
+
}
|
|
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;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
const status = findStatus(obj);
|
|
860
|
+
const ts = findTimestamp(obj) || file.mtime;
|
|
861
|
+
const detail = extractDetail(obj);
|
|
862
|
+
records.push({ id: nameFromFile(file.filename), source: "state", status, lastActive: ts, detail, file: file.filename });
|
|
863
|
+
return records;
|
|
864
|
+
}
|
|
865
|
+
function processJsonlFile(file) {
|
|
866
|
+
try {
|
|
867
|
+
const content = readFileSync(file.path, "utf8").trim();
|
|
868
|
+
if (!content) return [];
|
|
869
|
+
const lines = content.split("\n");
|
|
870
|
+
const lastLine = lines[lines.length - 1];
|
|
871
|
+
const obj = JSON.parse(lastLine);
|
|
872
|
+
const name = obj.jobId ?? obj.agentId ?? obj.name ?? obj.id ?? nameFromFile(file.filename);
|
|
873
|
+
const status = findStatus(obj);
|
|
874
|
+
const ts = findTimestamp(obj) || file.mtime;
|
|
875
|
+
const action = obj.action;
|
|
876
|
+
const detail = action ? `${action} (${lines.length} entries)` : `${lines.length} entries`;
|
|
877
|
+
return [{ id: String(name), source: "session", status, lastActive: ts, detail, file: file.filename }];
|
|
878
|
+
} catch {
|
|
879
|
+
return [];
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
var prevFileCount = 0;
|
|
883
|
+
var newExecCount = 0;
|
|
884
|
+
var sessionStart = Date.now();
|
|
885
|
+
function render(config) {
|
|
886
|
+
const files = scanFiles(config.tracesDir, config.recursive);
|
|
887
|
+
if (files.length > prevFileCount && prevFileCount > 0) {
|
|
888
|
+
newExecCount += files.length - prevFileCount;
|
|
889
|
+
}
|
|
890
|
+
prevFileCount = files.length;
|
|
891
|
+
const allRecords = [];
|
|
892
|
+
const allTraces = [];
|
|
893
|
+
for (const f of files.slice(0, 300)) {
|
|
894
|
+
const records = f.ext === ".jsonl" ? processJsonlFile(f) : processJsonFile(f);
|
|
895
|
+
for (const r of records) {
|
|
896
|
+
allRecords.push(r);
|
|
897
|
+
if (r.traceData) allTraces.push(r.traceData);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const agents = {};
|
|
901
|
+
for (const r of allRecords) {
|
|
902
|
+
if (!agents[r.id]) {
|
|
903
|
+
agents[r.id] = { name: r.id, total: 0, ok: 0, fail: 0, running: 0, lastTs: 0, source: r.source, detail: "" };
|
|
904
|
+
}
|
|
905
|
+
const ag = agents[r.id];
|
|
906
|
+
ag.total++;
|
|
907
|
+
if (r.status === "ok") ag.ok++;
|
|
908
|
+
else if (r.status === "error") ag.fail++;
|
|
909
|
+
else if (r.status === "running") ag.running++;
|
|
910
|
+
if (r.lastActive > ag.lastTs) {
|
|
911
|
+
ag.lastTs = r.lastActive;
|
|
912
|
+
ag.detail = r.detail;
|
|
913
|
+
ag.source = r.source;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const agentList = Object.values(agents).sort((a, b) => b.lastTs - a.lastTs);
|
|
917
|
+
const totExec = agentList.reduce((s, a) => s + a.total, 0);
|
|
918
|
+
const totFail = agentList.reduce((s, a) => s + a.fail, 0);
|
|
919
|
+
const totRunning = agentList.reduce((s, a) => s + a.running, 0);
|
|
920
|
+
const sysRate = totExec > 0 ? ((totExec - totFail) / totExec * 100).toFixed(1) : "100.0";
|
|
921
|
+
const now = Date.now();
|
|
922
|
+
const buckets = new Array(12).fill(0);
|
|
923
|
+
const failBuckets = new Array(12).fill(0);
|
|
924
|
+
for (const r of allRecords) {
|
|
925
|
+
const age = now - r.lastActive;
|
|
926
|
+
if (age > 36e5 || age < 0) continue;
|
|
927
|
+
const idx = 11 - Math.floor(age / 3e5);
|
|
928
|
+
if (idx >= 0 && idx < 12) {
|
|
929
|
+
buckets[idx]++;
|
|
930
|
+
if (r.status === "error") failBuckets[idx]++;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
const maxBucket = Math.max(...buckets, 1);
|
|
934
|
+
const sparkChars = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
935
|
+
const spark = buckets.map((v, i) => {
|
|
936
|
+
const level = Math.round(v / maxBucket * 8);
|
|
937
|
+
return (failBuckets[i] > 0 ? C.red : C.green) + sparkChars[level] + C.reset;
|
|
938
|
+
}).join("");
|
|
939
|
+
const distributedTraces = [];
|
|
940
|
+
if (allTraces.length > 1) {
|
|
941
|
+
const traceGroups = groupByTraceId(allTraces);
|
|
942
|
+
for (const [_tid, graphs] of traceGroups) {
|
|
943
|
+
if (graphs.length > 1) {
|
|
944
|
+
try {
|
|
945
|
+
distributedTraces.push(stitchTrace(graphs));
|
|
946
|
+
} catch {
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
distributedTraces.sort((a, b) => b.startTime - a.startTime);
|
|
951
|
+
}
|
|
952
|
+
const upSec = Math.floor((Date.now() - sessionStart) / 1e3);
|
|
953
|
+
const upMin = Math.floor(upSec / 60);
|
|
954
|
+
const upStr = upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`;
|
|
955
|
+
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
956
|
+
const sourceTag = (s) => {
|
|
957
|
+
switch (s) {
|
|
958
|
+
case "trace":
|
|
959
|
+
return `${C.cyan}trace${C.reset}`;
|
|
960
|
+
case "jobs":
|
|
961
|
+
return `${C.blue}job${C.reset}`;
|
|
962
|
+
case "workers":
|
|
963
|
+
return `${C.magenta}worker${C.reset}`;
|
|
964
|
+
case "session":
|
|
965
|
+
return `${C.yellow}session${C.reset}`;
|
|
966
|
+
case "state":
|
|
967
|
+
return `${C.dim}state${C.reset}`;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
971
|
+
console.log(`${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
|
|
972
|
+
console.log(`${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}`);
|
|
973
|
+
const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
|
|
974
|
+
const pad1 = Math.max(0, 64 - metaLine.length);
|
|
975
|
+
console.log(`${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`);
|
|
976
|
+
console.log(`${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
|
|
977
|
+
const sc = totFail === 0 ? C.green : C.yellow;
|
|
978
|
+
console.log("");
|
|
979
|
+
console.log(` ${C.bold}Agents${C.reset} ${sc}${agentList.length}${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}`);
|
|
980
|
+
console.log("");
|
|
981
|
+
console.log(` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
|
|
982
|
+
console.log("");
|
|
983
|
+
console.log(` ${C.bold}${C.under}Agent Type Status Last Active Detail${C.reset}`);
|
|
984
|
+
for (const ag of agentList.slice(0, 30)) {
|
|
985
|
+
const lastTime = ag.lastTs > 0 ? new Date(ag.lastTs).toLocaleTimeString() : "n/a";
|
|
986
|
+
const isRecent = Date.now() - ag.lastTs < 3e5;
|
|
987
|
+
let statusIcon;
|
|
988
|
+
let statusText;
|
|
989
|
+
if (ag.fail > 0 && ag.ok === 0 && ag.running === 0) {
|
|
990
|
+
statusIcon = `${C.red}\u25CF${C.reset}`;
|
|
991
|
+
statusText = `${C.red}error${C.reset}`;
|
|
992
|
+
} else if (ag.running > 0) {
|
|
993
|
+
statusIcon = `${C.green}\u25CF${C.reset}`;
|
|
994
|
+
statusText = `${C.green}running${C.reset}`;
|
|
995
|
+
} else if (ag.fail > 0) {
|
|
996
|
+
statusIcon = `${C.yellow}\u25CF${C.reset}`;
|
|
997
|
+
statusText = `${C.yellow}${ag.ok}ok/${ag.fail}err${C.reset}`;
|
|
998
|
+
} else if (ag.ok > 0) {
|
|
999
|
+
statusIcon = isRecent ? `${C.green}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
|
|
1000
|
+
statusText = ag.total > 1 ? `${C.green}${ag.ok}/${ag.total}${C.reset}` : `${C.green}ok${C.reset}`;
|
|
1001
|
+
} else {
|
|
1002
|
+
statusIcon = `${C.dim}\u25CB${C.reset}`;
|
|
1003
|
+
statusText = `${C.dim}idle${C.reset}`;
|
|
1004
|
+
}
|
|
1005
|
+
const name = ag.name.length > 23 ? ag.name.slice(0, 22) + "\u2026" : ag.name.padEnd(23);
|
|
1006
|
+
const src = sourceTag(ag.source).padEnd(16);
|
|
1007
|
+
const active = isRecent ? `${C.green}${lastTime}${C.reset}` : `${C.dim}${lastTime}${C.reset}`;
|
|
1008
|
+
const detail = ag.detail.length > 30 ? ag.detail.slice(0, 29) + "\u2026" : ag.detail;
|
|
1009
|
+
console.log(` ${statusIcon} ${name} ${src} ${statusText.padEnd(18)} ${active.padEnd(20)} ${C.dim}${detail}${C.reset}`);
|
|
1010
|
+
}
|
|
1011
|
+
if (distributedTraces.length > 0) {
|
|
1012
|
+
console.log("");
|
|
1013
|
+
console.log(` ${C.bold}${C.under}Distributed Traces${C.reset}`);
|
|
1014
|
+
for (const dt of distributedTraces.slice(0, 3)) {
|
|
1015
|
+
const traceTime = new Date(dt.startTime).toLocaleTimeString();
|
|
1016
|
+
const statusIcon = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
1017
|
+
const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
|
|
1018
|
+
const tid = dt.traceId.slice(0, 8);
|
|
1019
|
+
console.log(` ${statusIcon} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`);
|
|
1020
|
+
const tree = getTraceTree(dt);
|
|
1021
|
+
for (let i = 0; i < Math.min(tree.length, 6); i++) {
|
|
1022
|
+
const g = tree[i];
|
|
1023
|
+
const depth = getDistDepth(dt, g.spanId);
|
|
1024
|
+
const indent = " " + "\u2502 ".repeat(Math.max(0, depth - 1));
|
|
1025
|
+
const isLast = i === tree.length - 1 || getDistDepth(dt, tree[i + 1]?.spanId) <= depth;
|
|
1026
|
+
const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1027
|
+
const gs = g.status === "completed" ? `${C.green}\u2713${C.reset}` : g.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
1028
|
+
const gd = g.endTime ? `${g.endTime - g.startTime}ms` : "running";
|
|
1029
|
+
console.log(`${indent}${conn}${gs} ${C.bold}${g.agentId}${C.reset} ${C.dim}[${g.trigger}] ${gd}${C.reset}`);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0, 8);
|
|
1034
|
+
if (recentRecords.length > 0) {
|
|
1035
|
+
console.log("");
|
|
1036
|
+
console.log(` ${C.bold}${C.under}Recent Activity${C.reset}`);
|
|
1037
|
+
for (const r of recentRecords) {
|
|
1038
|
+
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}`;
|
|
1039
|
+
const t = new Date(r.lastActive).toLocaleTimeString();
|
|
1040
|
+
const agent = r.id.length > 26 ? r.id.slice(0, 25) + "\u2026" : r.id.padEnd(26);
|
|
1041
|
+
const age = Math.floor((Date.now() - r.lastActive) / 1e3);
|
|
1042
|
+
const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
|
|
1043
|
+
const detail = r.detail.length > 25 ? r.detail.slice(0, 24) + "\u2026" : r.detail;
|
|
1044
|
+
console.log(` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${detail}${C.reset}`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
if (files.length === 0) {
|
|
1048
|
+
console.log("");
|
|
1049
|
+
console.log(` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
|
|
1050
|
+
console.log(` ${C.dim}${config.tracesDir}${C.reset}`);
|
|
1051
|
+
}
|
|
1052
|
+
console.log("");
|
|
1053
|
+
console.log(` ${C.dim}Watching: ${config.tracesDir}${C.reset}`);
|
|
1054
|
+
console.log(` ${C.dim}Press Ctrl+C to exit${C.reset}`);
|
|
1055
|
+
}
|
|
1056
|
+
function getDistDepth(dt, spanId) {
|
|
1057
|
+
if (!spanId) return 0;
|
|
1058
|
+
const g = dt.graphs.get(spanId);
|
|
1059
|
+
if (!g || !g.parentSpanId) return 0;
|
|
1060
|
+
return 1 + getDistDepth(dt, g.parentSpanId);
|
|
1061
|
+
}
|
|
1062
|
+
function startLive(argv) {
|
|
1063
|
+
const config = parseArgs(argv);
|
|
1064
|
+
if (!existsSync2(config.tracesDir)) {
|
|
1065
|
+
console.error(`Directory does not exist: ${config.tracesDir}`);
|
|
1066
|
+
console.error("Specify a directory containing JSON/JSONL files: agentflow live <dir>");
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
render(config);
|
|
1070
|
+
let debounce = null;
|
|
1071
|
+
try {
|
|
1072
|
+
watch(config.tracesDir, { recursive: config.recursive }, () => {
|
|
1073
|
+
if (debounce) clearTimeout(debounce);
|
|
1074
|
+
debounce = setTimeout(() => render(config), 500);
|
|
1075
|
+
});
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
setInterval(() => render(config), config.refreshMs);
|
|
1079
|
+
process.on("SIGINT", () => {
|
|
1080
|
+
console.log("\n" + C.dim + "Monitor stopped." + C.reset);
|
|
1081
|
+
process.exit(0);
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
export {
|
|
1086
|
+
createGraphBuilder,
|
|
1087
|
+
loadGraph,
|
|
1088
|
+
graphToJson,
|
|
1089
|
+
runTraced,
|
|
1090
|
+
getNode,
|
|
1091
|
+
getChildren,
|
|
1092
|
+
getParent,
|
|
1093
|
+
getFailures,
|
|
1094
|
+
getHungNodes,
|
|
1095
|
+
getCriticalPath,
|
|
1096
|
+
findWaitingOn,
|
|
1097
|
+
getSubtree,
|
|
1098
|
+
getDuration,
|
|
1099
|
+
getDepth,
|
|
1100
|
+
getStats,
|
|
1101
|
+
groupByTraceId,
|
|
1102
|
+
stitchTrace,
|
|
1103
|
+
getTraceTree,
|
|
1104
|
+
startLive
|
|
1105
|
+
};
|