agentflow-core 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VVWZJ63D.js → chunk-DHCTDCDI.js} +1613 -1305
- package/dist/chunk-DY7YHFIB.js +56 -0
- package/dist/cli.cjs +1190 -580
- package/dist/cli.js +217 -9
- package/dist/index.cjs +941 -419
- package/dist/index.d.cts +296 -132
- package/dist/index.d.ts +296 -132
- package/dist/index.js +173 -5
- package/dist/loader-LYRR6LMM.js +8 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
checkGuards: () => checkGuards,
|
|
23
24
|
createGraphBuilder: () => createGraphBuilder,
|
|
25
|
+
createTraceStore: () => createTraceStore,
|
|
24
26
|
findWaitingOn: () => findWaitingOn,
|
|
25
27
|
getChildren: () => getChildren,
|
|
26
28
|
getCriticalPath: () => getCriticalPath,
|
|
@@ -39,7 +41,10 @@ __export(index_exports, {
|
|
|
39
41
|
runTraced: () => runTraced,
|
|
40
42
|
startLive: () => startLive,
|
|
41
43
|
startWatch: () => startWatch,
|
|
42
|
-
stitchTrace: () => stitchTrace
|
|
44
|
+
stitchTrace: () => stitchTrace,
|
|
45
|
+
toAsciiTree: () => toAsciiTree,
|
|
46
|
+
toTimeline: () => toTimeline,
|
|
47
|
+
withGuards: () => withGuards
|
|
43
48
|
});
|
|
44
49
|
module.exports = __toCommonJS(index_exports);
|
|
45
50
|
|
|
@@ -269,213 +274,6 @@ function createGraphBuilder(config) {
|
|
|
269
274
|
return builder;
|
|
270
275
|
}
|
|
271
276
|
|
|
272
|
-
// src/loader.ts
|
|
273
|
-
function toNodesMap(raw) {
|
|
274
|
-
if (raw instanceof Map) return raw;
|
|
275
|
-
if (Array.isArray(raw)) {
|
|
276
|
-
return new Map(raw);
|
|
277
|
-
}
|
|
278
|
-
if (raw !== null && typeof raw === "object") {
|
|
279
|
-
return new Map(Object.entries(raw));
|
|
280
|
-
}
|
|
281
|
-
return /* @__PURE__ */ new Map();
|
|
282
|
-
}
|
|
283
|
-
function loadGraph(input) {
|
|
284
|
-
const raw = typeof input === "string" ? JSON.parse(input) : input;
|
|
285
|
-
const nodes = toNodesMap(raw.nodes);
|
|
286
|
-
return {
|
|
287
|
-
id: raw.id ?? "",
|
|
288
|
-
rootNodeId: raw.rootNodeId ?? raw.rootId ?? "",
|
|
289
|
-
nodes,
|
|
290
|
-
edges: raw.edges ?? [],
|
|
291
|
-
startTime: raw.startTime ?? 0,
|
|
292
|
-
endTime: raw.endTime ?? null,
|
|
293
|
-
status: raw.status ?? "completed",
|
|
294
|
-
trigger: raw.trigger ?? "unknown",
|
|
295
|
-
agentId: raw.agentId ?? "unknown",
|
|
296
|
-
events: raw.events ?? [],
|
|
297
|
-
traceId: raw.traceId,
|
|
298
|
-
spanId: raw.spanId,
|
|
299
|
-
parentSpanId: raw.parentSpanId
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
function graphToJson(graph) {
|
|
303
|
-
const nodesObj = {};
|
|
304
|
-
for (const [id, node] of graph.nodes) {
|
|
305
|
-
nodesObj[id] = node;
|
|
306
|
-
}
|
|
307
|
-
return {
|
|
308
|
-
id: graph.id,
|
|
309
|
-
rootNodeId: graph.rootNodeId,
|
|
310
|
-
nodes: nodesObj,
|
|
311
|
-
edges: graph.edges,
|
|
312
|
-
startTime: graph.startTime,
|
|
313
|
-
endTime: graph.endTime,
|
|
314
|
-
status: graph.status,
|
|
315
|
-
trigger: graph.trigger,
|
|
316
|
-
agentId: graph.agentId,
|
|
317
|
-
events: graph.events,
|
|
318
|
-
traceId: graph.traceId,
|
|
319
|
-
spanId: graph.spanId,
|
|
320
|
-
parentSpanId: graph.parentSpanId
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// src/runner.ts
|
|
325
|
-
var import_node_child_process = require("child_process");
|
|
326
|
-
var import_node_fs = require("fs");
|
|
327
|
-
var import_node_path = require("path");
|
|
328
|
-
function globToRegex(pattern) {
|
|
329
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
330
|
-
return new RegExp(`^${escaped}$`);
|
|
331
|
-
}
|
|
332
|
-
function snapshotDir(dir, patterns) {
|
|
333
|
-
const result = /* @__PURE__ */ new Map();
|
|
334
|
-
if (!(0, import_node_fs.existsSync)(dir)) return result;
|
|
335
|
-
for (const entry of (0, import_node_fs.readdirSync)(dir)) {
|
|
336
|
-
if (!patterns.some((re) => re.test(entry))) continue;
|
|
337
|
-
const full = (0, import_node_path.join)(dir, entry);
|
|
338
|
-
try {
|
|
339
|
-
const stat = (0, import_node_fs.statSync)(full);
|
|
340
|
-
if (stat.isFile()) {
|
|
341
|
-
result.set(full, stat.mtimeMs);
|
|
342
|
-
}
|
|
343
|
-
} catch {
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
return result;
|
|
347
|
-
}
|
|
348
|
-
function agentIdFromFilename(filePath) {
|
|
349
|
-
const base = (0, import_node_path.basename)(filePath, ".json");
|
|
350
|
-
const cleaned = base.replace(/-state$/, "");
|
|
351
|
-
return `alfred-${cleaned}`;
|
|
352
|
-
}
|
|
353
|
-
function deriveAgentId(command) {
|
|
354
|
-
return "orchestrator";
|
|
355
|
-
}
|
|
356
|
-
function fileTimestamp() {
|
|
357
|
-
return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
|
|
358
|
-
}
|
|
359
|
-
async function runTraced(config) {
|
|
360
|
-
const {
|
|
361
|
-
command,
|
|
362
|
-
agentId = deriveAgentId(command),
|
|
363
|
-
trigger = "cli",
|
|
364
|
-
tracesDir = "./traces",
|
|
365
|
-
watchDirs = [],
|
|
366
|
-
watchPatterns = ["*.json"]
|
|
367
|
-
} = config;
|
|
368
|
-
if (command.length === 0) {
|
|
369
|
-
throw new Error("runTraced: command must not be empty");
|
|
370
|
-
}
|
|
371
|
-
const resolvedTracesDir = (0, import_node_path.resolve)(tracesDir);
|
|
372
|
-
const patterns = watchPatterns.map(globToRegex);
|
|
373
|
-
const orchestrator = createGraphBuilder({ agentId, trigger });
|
|
374
|
-
const { traceId, spanId } = orchestrator.traceContext;
|
|
375
|
-
const beforeSnapshots = /* @__PURE__ */ new Map();
|
|
376
|
-
for (const dir of watchDirs) {
|
|
377
|
-
beforeSnapshots.set(dir, snapshotDir(dir, patterns));
|
|
378
|
-
}
|
|
379
|
-
const rootId = orchestrator.startNode({ type: "agent", name: agentId });
|
|
380
|
-
const dispatchId = orchestrator.startNode({
|
|
381
|
-
type: "tool",
|
|
382
|
-
name: "dispatch-command",
|
|
383
|
-
parentId: rootId
|
|
384
|
-
});
|
|
385
|
-
orchestrator.updateState(dispatchId, { command: command.join(" ") });
|
|
386
|
-
const monitorId = orchestrator.startNode({
|
|
387
|
-
type: "tool",
|
|
388
|
-
name: "state-monitor",
|
|
389
|
-
parentId: rootId
|
|
390
|
-
});
|
|
391
|
-
orchestrator.updateState(monitorId, {
|
|
392
|
-
watchDirs,
|
|
393
|
-
watchPatterns
|
|
394
|
-
});
|
|
395
|
-
const startMs = Date.now();
|
|
396
|
-
const execCmd = command[0] ?? "";
|
|
397
|
-
const execArgs = command.slice(1);
|
|
398
|
-
process.env.AGENTFLOW_TRACE_ID = traceId;
|
|
399
|
-
process.env.AGENTFLOW_PARENT_SPAN_ID = spanId;
|
|
400
|
-
const result = (0, import_node_child_process.spawnSync)(execCmd, execArgs, { stdio: "inherit" });
|
|
401
|
-
delete process.env.AGENTFLOW_TRACE_ID;
|
|
402
|
-
delete process.env.AGENTFLOW_PARENT_SPAN_ID;
|
|
403
|
-
const exitCode = result.status ?? 1;
|
|
404
|
-
const duration = (Date.now() - startMs) / 1e3;
|
|
405
|
-
const stateChanges = [];
|
|
406
|
-
for (const dir of watchDirs) {
|
|
407
|
-
const before = beforeSnapshots.get(dir) ?? /* @__PURE__ */ new Map();
|
|
408
|
-
const after = snapshotDir(dir, patterns);
|
|
409
|
-
for (const [filePath, mtime] of after) {
|
|
410
|
-
const prevMtime = before.get(filePath);
|
|
411
|
-
if (prevMtime === void 0 || mtime > prevMtime) {
|
|
412
|
-
stateChanges.push(filePath);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
orchestrator.updateState(monitorId, { stateChanges });
|
|
417
|
-
orchestrator.endNode(monitorId);
|
|
418
|
-
if (exitCode === 0) {
|
|
419
|
-
orchestrator.endNode(dispatchId);
|
|
420
|
-
} else {
|
|
421
|
-
orchestrator.failNode(dispatchId, `Command exited with code ${exitCode}`);
|
|
422
|
-
}
|
|
423
|
-
orchestrator.updateState(rootId, {
|
|
424
|
-
exitCode,
|
|
425
|
-
duration,
|
|
426
|
-
stateChanges
|
|
427
|
-
});
|
|
428
|
-
if (exitCode === 0) {
|
|
429
|
-
orchestrator.endNode(rootId);
|
|
430
|
-
} else {
|
|
431
|
-
orchestrator.failNode(rootId, `Command exited with code ${exitCode}`);
|
|
432
|
-
}
|
|
433
|
-
const orchestratorGraph = orchestrator.build();
|
|
434
|
-
const allGraphs = [orchestratorGraph];
|
|
435
|
-
for (const filePath of stateChanges) {
|
|
436
|
-
const childAgentId = agentIdFromFilename(filePath);
|
|
437
|
-
const childBuilder = createGraphBuilder({
|
|
438
|
-
agentId: childAgentId,
|
|
439
|
-
trigger: "state-change",
|
|
440
|
-
traceId,
|
|
441
|
-
parentSpanId: spanId
|
|
442
|
-
});
|
|
443
|
-
const childRootId = childBuilder.startNode({
|
|
444
|
-
type: "agent",
|
|
445
|
-
name: childAgentId
|
|
446
|
-
});
|
|
447
|
-
childBuilder.updateState(childRootId, {
|
|
448
|
-
stateFile: filePath,
|
|
449
|
-
detectedBy: "runner-state-monitor"
|
|
450
|
-
});
|
|
451
|
-
childBuilder.endNode(childRootId);
|
|
452
|
-
allGraphs.push(childBuilder.build());
|
|
453
|
-
}
|
|
454
|
-
if (!(0, import_node_fs.existsSync)(resolvedTracesDir)) {
|
|
455
|
-
(0, import_node_fs.mkdirSync)(resolvedTracesDir, { recursive: true });
|
|
456
|
-
}
|
|
457
|
-
const ts = fileTimestamp();
|
|
458
|
-
const tracePaths = [];
|
|
459
|
-
for (const graph of allGraphs) {
|
|
460
|
-
const filename = `${graph.agentId}-${ts}.json`;
|
|
461
|
-
const outPath = (0, import_node_path.join)(resolvedTracesDir, filename);
|
|
462
|
-
(0, import_node_fs.writeFileSync)(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
|
|
463
|
-
tracePaths.push(outPath);
|
|
464
|
-
}
|
|
465
|
-
return {
|
|
466
|
-
exitCode,
|
|
467
|
-
traceId,
|
|
468
|
-
spanId,
|
|
469
|
-
tracePaths,
|
|
470
|
-
stateChanges,
|
|
471
|
-
duration
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// src/live.ts
|
|
476
|
-
var import_node_fs2 = require("fs");
|
|
477
|
-
var import_node_path2 = require("path");
|
|
478
|
-
|
|
479
277
|
// src/graph-query.ts
|
|
480
278
|
function getNode(graph, nodeId) {
|
|
481
279
|
return graph.nodes.get(nodeId);
|
|
@@ -507,13 +305,13 @@ function getHungNodes(graph) {
|
|
|
507
305
|
function getCriticalPath(graph) {
|
|
508
306
|
const root = graph.nodes.get(graph.rootNodeId);
|
|
509
307
|
if (!root) return [];
|
|
510
|
-
function
|
|
308
|
+
function nodeDuration2(node) {
|
|
511
309
|
const end = node.endTime ?? Date.now();
|
|
512
310
|
return end - node.startTime;
|
|
513
311
|
}
|
|
514
312
|
function dfs(node) {
|
|
515
313
|
if (node.children.length === 0) {
|
|
516
|
-
return { duration:
|
|
314
|
+
return { duration: nodeDuration2(node), path: [node] };
|
|
517
315
|
}
|
|
518
316
|
let bestChild = { duration: -1, path: [] };
|
|
519
317
|
for (const childId of node.children) {
|
|
@@ -525,7 +323,7 @@ function getCriticalPath(graph) {
|
|
|
525
323
|
}
|
|
526
324
|
}
|
|
527
325
|
return {
|
|
528
|
-
duration:
|
|
326
|
+
duration: nodeDuration2(node) + bestChild.duration,
|
|
529
327
|
path: [node, ...bestChild.path]
|
|
530
328
|
};
|
|
531
329
|
}
|
|
@@ -666,17 +464,231 @@ function stitchTrace(graphs) {
|
|
|
666
464
|
status
|
|
667
465
|
});
|
|
668
466
|
}
|
|
669
|
-
function getTraceTree(trace) {
|
|
670
|
-
const result = [];
|
|
671
|
-
function walk(spanId) {
|
|
672
|
-
const graph = trace.graphs.get(spanId);
|
|
673
|
-
if (graph) result.push(graph);
|
|
674
|
-
const children = trace.childMap.get(spanId) ?? [];
|
|
675
|
-
for (const childSpan of children) walk(childSpan);
|
|
467
|
+
function getTraceTree(trace) {
|
|
468
|
+
const result = [];
|
|
469
|
+
function walk(spanId) {
|
|
470
|
+
const graph = trace.graphs.get(spanId);
|
|
471
|
+
if (graph) result.push(graph);
|
|
472
|
+
const children = trace.childMap.get(spanId) ?? [];
|
|
473
|
+
for (const childSpan of children) walk(childSpan);
|
|
474
|
+
}
|
|
475
|
+
if (trace.rootGraph.spanId) walk(trace.rootGraph.spanId);
|
|
476
|
+
else result.push(trace.rootGraph);
|
|
477
|
+
return result;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/guards.ts
|
|
481
|
+
var DEFAULT_TIMEOUTS = {
|
|
482
|
+
tool: 3e4,
|
|
483
|
+
// 30s
|
|
484
|
+
agent: 3e5,
|
|
485
|
+
// 5m
|
|
486
|
+
subagent: 3e5,
|
|
487
|
+
// 5m
|
|
488
|
+
wait: 6e5,
|
|
489
|
+
// 10m
|
|
490
|
+
decision: 3e4,
|
|
491
|
+
// 30s
|
|
492
|
+
custom: 3e4
|
|
493
|
+
// 30s
|
|
494
|
+
};
|
|
495
|
+
function checkGuards(graph, config) {
|
|
496
|
+
const violations = [];
|
|
497
|
+
const now = Date.now();
|
|
498
|
+
const timeouts = { ...DEFAULT_TIMEOUTS, ...config?.timeouts };
|
|
499
|
+
const maxReasoningSteps = config?.maxReasoningSteps ?? 25;
|
|
500
|
+
const maxDepth = config?.maxDepth ?? 10;
|
|
501
|
+
const maxAgentSpawns = config?.maxAgentSpawns ?? 50;
|
|
502
|
+
for (const node of graph.nodes.values()) {
|
|
503
|
+
if (node.status === "running" && node.endTime === null) {
|
|
504
|
+
const timeoutThreshold = timeouts[node.type];
|
|
505
|
+
const elapsed = now - node.startTime;
|
|
506
|
+
if (elapsed > timeoutThreshold) {
|
|
507
|
+
violations.push({
|
|
508
|
+
type: "timeout",
|
|
509
|
+
nodeId: node.id,
|
|
510
|
+
message: `Node ${node.id} (${node.type}: ${node.name}) has been running for ${elapsed}ms, exceeding timeout of ${timeoutThreshold}ms`,
|
|
511
|
+
timestamp: now
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const depth = getDepth(graph);
|
|
517
|
+
if (depth > maxDepth) {
|
|
518
|
+
violations.push({
|
|
519
|
+
type: "spawn-explosion",
|
|
520
|
+
nodeId: graph.rootNodeId,
|
|
521
|
+
message: `Graph depth ${depth} exceeds maximum depth of ${maxDepth}`,
|
|
522
|
+
timestamp: now
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
let agentCount = 0;
|
|
526
|
+
for (const node of graph.nodes.values()) {
|
|
527
|
+
if (node.type === "agent" || node.type === "subagent") {
|
|
528
|
+
agentCount++;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (agentCount > maxAgentSpawns) {
|
|
532
|
+
violations.push({
|
|
533
|
+
type: "spawn-explosion",
|
|
534
|
+
nodeId: graph.rootNodeId,
|
|
535
|
+
message: `Total agent/subagent count ${agentCount} exceeds maximum of ${maxAgentSpawns}`,
|
|
536
|
+
timestamp: now
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
violations.push(...detectReasoningLoops(graph, maxReasoningSteps, now));
|
|
540
|
+
return violations;
|
|
541
|
+
}
|
|
542
|
+
function detectReasoningLoops(graph, maxSteps, timestamp) {
|
|
543
|
+
const violations = [];
|
|
544
|
+
const reported = /* @__PURE__ */ new Set();
|
|
545
|
+
function walk(nodeId, consecutiveCount, consecutiveType) {
|
|
546
|
+
const node = getNode(graph, nodeId);
|
|
547
|
+
if (!node) return;
|
|
548
|
+
let newCount;
|
|
549
|
+
let newType;
|
|
550
|
+
if (node.type === consecutiveType) {
|
|
551
|
+
newCount = consecutiveCount + 1;
|
|
552
|
+
newType = node.type;
|
|
553
|
+
} else {
|
|
554
|
+
newCount = 1;
|
|
555
|
+
newType = node.type;
|
|
556
|
+
}
|
|
557
|
+
if (newCount > maxSteps && !reported.has(newType)) {
|
|
558
|
+
reported.add(newType);
|
|
559
|
+
violations.push({
|
|
560
|
+
type: "reasoning-loop",
|
|
561
|
+
nodeId: node.id,
|
|
562
|
+
message: `Detected ${newCount} consecutive ${newType} nodes along path to ${node.name}`,
|
|
563
|
+
timestamp
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
const children = getChildren(graph, nodeId);
|
|
567
|
+
for (const child of children) {
|
|
568
|
+
walk(child.id, newCount, newType);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
walk(graph.rootNodeId, 0, null);
|
|
572
|
+
return violations;
|
|
573
|
+
}
|
|
574
|
+
function withGuards(builder, config) {
|
|
575
|
+
const logger = config?.logger ?? ((msg) => console.warn(`[AgentFlow Guard] ${msg}`));
|
|
576
|
+
const onViolation = config?.onViolation ?? "warn";
|
|
577
|
+
function handleViolations(violations) {
|
|
578
|
+
if (violations.length === 0) return;
|
|
579
|
+
for (const violation of violations) {
|
|
580
|
+
const message = `Guard violation: ${violation.message}`;
|
|
581
|
+
switch (onViolation) {
|
|
582
|
+
case "warn":
|
|
583
|
+
logger(message);
|
|
584
|
+
break;
|
|
585
|
+
case "error":
|
|
586
|
+
logger(message);
|
|
587
|
+
builder.pushEvent({
|
|
588
|
+
eventType: "custom",
|
|
589
|
+
nodeId: violation.nodeId,
|
|
590
|
+
data: {
|
|
591
|
+
guardViolation: violation.type,
|
|
592
|
+
message: violation.message,
|
|
593
|
+
severity: "error"
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
break;
|
|
597
|
+
case "abort":
|
|
598
|
+
throw new Error(`AgentFlow guard violation: ${violation.message}`);
|
|
599
|
+
default:
|
|
600
|
+
logger(message);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
get graphId() {
|
|
606
|
+
return builder.graphId;
|
|
607
|
+
},
|
|
608
|
+
get traceContext() {
|
|
609
|
+
return builder.traceContext;
|
|
610
|
+
},
|
|
611
|
+
startNode: (opts) => builder.startNode(opts),
|
|
612
|
+
endNode: (nodeId, status) => {
|
|
613
|
+
builder.endNode(nodeId, status);
|
|
614
|
+
const snapshot = builder.getSnapshot();
|
|
615
|
+
const violations = checkGuards(snapshot, config);
|
|
616
|
+
handleViolations(violations);
|
|
617
|
+
},
|
|
618
|
+
failNode: (nodeId, error) => {
|
|
619
|
+
builder.failNode(nodeId, error);
|
|
620
|
+
const snapshot = builder.getSnapshot();
|
|
621
|
+
const violations = checkGuards(snapshot, config);
|
|
622
|
+
handleViolations(violations);
|
|
623
|
+
},
|
|
624
|
+
addEdge: (from, to, type) => builder.addEdge(from, to, type),
|
|
625
|
+
pushEvent: (event) => builder.pushEvent(event),
|
|
626
|
+
updateState: (nodeId, state) => builder.updateState(nodeId, state),
|
|
627
|
+
withParent: (parentId, fn) => builder.withParent(parentId, fn),
|
|
628
|
+
getSnapshot: () => builder.getSnapshot(),
|
|
629
|
+
build: () => {
|
|
630
|
+
const snapshot = builder.getSnapshot();
|
|
631
|
+
const violations = checkGuards(snapshot, config);
|
|
632
|
+
handleViolations(violations);
|
|
633
|
+
return builder.build();
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/live.ts
|
|
639
|
+
var import_node_fs = require("fs");
|
|
640
|
+
var import_node_path = require("path");
|
|
641
|
+
|
|
642
|
+
// src/loader.ts
|
|
643
|
+
function toNodesMap(raw) {
|
|
644
|
+
if (raw instanceof Map) return raw;
|
|
645
|
+
if (Array.isArray(raw)) {
|
|
646
|
+
return new Map(raw);
|
|
647
|
+
}
|
|
648
|
+
if (raw !== null && typeof raw === "object") {
|
|
649
|
+
return new Map(Object.entries(raw));
|
|
650
|
+
}
|
|
651
|
+
return /* @__PURE__ */ new Map();
|
|
652
|
+
}
|
|
653
|
+
function loadGraph(input) {
|
|
654
|
+
const raw = typeof input === "string" ? JSON.parse(input) : input;
|
|
655
|
+
const nodes = toNodesMap(raw.nodes);
|
|
656
|
+
return {
|
|
657
|
+
id: raw.id ?? "",
|
|
658
|
+
rootNodeId: raw.rootNodeId ?? raw.rootId ?? "",
|
|
659
|
+
nodes,
|
|
660
|
+
edges: raw.edges ?? [],
|
|
661
|
+
startTime: raw.startTime ?? 0,
|
|
662
|
+
endTime: raw.endTime ?? null,
|
|
663
|
+
status: raw.status ?? "completed",
|
|
664
|
+
trigger: raw.trigger ?? "unknown",
|
|
665
|
+
agentId: raw.agentId ?? "unknown",
|
|
666
|
+
events: raw.events ?? [],
|
|
667
|
+
traceId: raw.traceId,
|
|
668
|
+
spanId: raw.spanId,
|
|
669
|
+
parentSpanId: raw.parentSpanId
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function graphToJson(graph) {
|
|
673
|
+
const nodesObj = {};
|
|
674
|
+
for (const [id, node] of graph.nodes) {
|
|
675
|
+
nodesObj[id] = node;
|
|
676
676
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
677
|
+
return {
|
|
678
|
+
id: graph.id,
|
|
679
|
+
rootNodeId: graph.rootNodeId,
|
|
680
|
+
nodes: nodesObj,
|
|
681
|
+
edges: graph.edges,
|
|
682
|
+
startTime: graph.startTime,
|
|
683
|
+
endTime: graph.endTime,
|
|
684
|
+
status: graph.status,
|
|
685
|
+
trigger: graph.trigger,
|
|
686
|
+
agentId: graph.agentId,
|
|
687
|
+
events: graph.events,
|
|
688
|
+
traceId: graph.traceId,
|
|
689
|
+
spanId: graph.spanId,
|
|
690
|
+
parentSpanId: graph.parentSpanId
|
|
691
|
+
};
|
|
680
692
|
}
|
|
681
693
|
|
|
682
694
|
// src/live.ts
|
|
@@ -712,17 +724,18 @@ function parseArgs(argv) {
|
|
|
712
724
|
config.recursive = true;
|
|
713
725
|
i++;
|
|
714
726
|
} else if (!arg.startsWith("-")) {
|
|
715
|
-
config.dirs.push((0,
|
|
727
|
+
config.dirs.push((0, import_node_path.resolve)(arg));
|
|
716
728
|
i++;
|
|
717
729
|
} else {
|
|
718
730
|
i++;
|
|
719
731
|
}
|
|
720
732
|
}
|
|
721
|
-
if (config.dirs.length === 0) config.dirs.push((0,
|
|
733
|
+
if (config.dirs.length === 0) config.dirs.push((0, import_node_path.resolve)("."));
|
|
722
734
|
return config;
|
|
723
735
|
}
|
|
724
736
|
function printUsage() {
|
|
725
|
-
console.log(
|
|
737
|
+
console.log(
|
|
738
|
+
`
|
|
726
739
|
AgentFlow Live Monitor \u2014 real-time terminal dashboard for agent systems.
|
|
727
740
|
|
|
728
741
|
Auto-detects agent traces, state files, job schedulers, and session logs
|
|
@@ -743,20 +756,21 @@ Examples:
|
|
|
743
756
|
agentflow live ./data
|
|
744
757
|
agentflow live ./traces ./cron ./workers --refresh 5
|
|
745
758
|
agentflow live /var/lib/myagent -R
|
|
746
|
-
`.trim()
|
|
759
|
+
`.trim()
|
|
760
|
+
);
|
|
747
761
|
}
|
|
748
762
|
function scanFiles(dirs, recursive) {
|
|
749
763
|
const results = [];
|
|
750
764
|
const seen = /* @__PURE__ */ new Set();
|
|
751
765
|
function scanDir(d, topLevel) {
|
|
752
766
|
try {
|
|
753
|
-
for (const f of (0,
|
|
767
|
+
for (const f of (0, import_node_fs.readdirSync)(d)) {
|
|
754
768
|
if (f.startsWith(".")) continue;
|
|
755
|
-
const fp = (0,
|
|
769
|
+
const fp = (0, import_node_path.join)(d, f);
|
|
756
770
|
if (seen.has(fp)) continue;
|
|
757
771
|
let stat;
|
|
758
772
|
try {
|
|
759
|
-
stat = (0,
|
|
773
|
+
stat = (0, import_node_fs.statSync)(fp);
|
|
760
774
|
} catch {
|
|
761
775
|
continue;
|
|
762
776
|
}
|
|
@@ -782,20 +796,22 @@ function scanFiles(dirs, recursive) {
|
|
|
782
796
|
}
|
|
783
797
|
function safeReadJson(fp) {
|
|
784
798
|
try {
|
|
785
|
-
return JSON.parse((0,
|
|
799
|
+
return JSON.parse((0, import_node_fs.readFileSync)(fp, "utf8"));
|
|
786
800
|
} catch {
|
|
787
801
|
return null;
|
|
788
802
|
}
|
|
789
803
|
}
|
|
790
804
|
function nameFromFile(filename) {
|
|
791
|
-
return (0,
|
|
805
|
+
return (0, import_node_path.basename)(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
|
|
792
806
|
}
|
|
793
807
|
function normalizeStatus(val) {
|
|
794
808
|
if (typeof val !== "string") return "unknown";
|
|
795
809
|
const s = val.toLowerCase();
|
|
796
810
|
if (["ok", "success", "completed", "done", "passed", "healthy", "good"].includes(s)) return "ok";
|
|
797
|
-
if (["error", "failed", "failure", "crashed", "unhealthy", "bad", "timeout"].includes(s))
|
|
798
|
-
|
|
811
|
+
if (["error", "failed", "failure", "crashed", "unhealthy", "bad", "timeout"].includes(s))
|
|
812
|
+
return "error";
|
|
813
|
+
if (["running", "active", "in_progress", "started", "pending", "processing"].includes(s))
|
|
814
|
+
return "running";
|
|
799
815
|
return "unknown";
|
|
800
816
|
}
|
|
801
817
|
function findStatus(obj) {
|
|
@@ -811,7 +827,17 @@ function findStatus(obj) {
|
|
|
811
827
|
return "unknown";
|
|
812
828
|
}
|
|
813
829
|
function findTimestamp(obj) {
|
|
814
|
-
for (const key of [
|
|
830
|
+
for (const key of [
|
|
831
|
+
"ts",
|
|
832
|
+
"timestamp",
|
|
833
|
+
"lastRunAtMs",
|
|
834
|
+
"last_run",
|
|
835
|
+
"lastExecution",
|
|
836
|
+
"updated_at",
|
|
837
|
+
"started_at",
|
|
838
|
+
"endTime",
|
|
839
|
+
"startTime"
|
|
840
|
+
]) {
|
|
815
841
|
const val = obj[key];
|
|
816
842
|
if (typeof val === "number") return val > 1e12 ? val : val * 1e3;
|
|
817
843
|
if (typeof val === "string") {
|
|
@@ -823,7 +849,16 @@ function findTimestamp(obj) {
|
|
|
823
849
|
}
|
|
824
850
|
function extractDetail(obj) {
|
|
825
851
|
const parts = [];
|
|
826
|
-
for (const key of [
|
|
852
|
+
for (const key of [
|
|
853
|
+
"summary",
|
|
854
|
+
"message",
|
|
855
|
+
"description",
|
|
856
|
+
"lastError",
|
|
857
|
+
"error",
|
|
858
|
+
"name",
|
|
859
|
+
"jobId",
|
|
860
|
+
"id"
|
|
861
|
+
]) {
|
|
827
862
|
const val = obj[key];
|
|
828
863
|
if (typeof val === "string" && val.length > 0 && val.length < 200) {
|
|
829
864
|
parts.push(val.slice(0, 80));
|
|
@@ -883,7 +918,14 @@ function processJsonFile(file) {
|
|
|
883
918
|
const status2 = findStatus(state);
|
|
884
919
|
const ts2 = findTimestamp(state) || file.mtime;
|
|
885
920
|
const detail2 = extractDetail(state);
|
|
886
|
-
records.push({
|
|
921
|
+
records.push({
|
|
922
|
+
id: String(name),
|
|
923
|
+
source: "jobs",
|
|
924
|
+
status: status2,
|
|
925
|
+
lastActive: ts2,
|
|
926
|
+
detail: detail2,
|
|
927
|
+
file: file.filename
|
|
928
|
+
});
|
|
887
929
|
}
|
|
888
930
|
return records;
|
|
889
931
|
}
|
|
@@ -898,7 +940,14 @@ function processJsonFile(file) {
|
|
|
898
940
|
const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
|
|
899
941
|
const pid = w.pid;
|
|
900
942
|
const detail2 = pid ? `pid: ${pid}` : extractDetail(w);
|
|
901
|
-
records.push({
|
|
943
|
+
records.push({
|
|
944
|
+
id: name,
|
|
945
|
+
source: "workers",
|
|
946
|
+
status: status2,
|
|
947
|
+
lastActive: ts2,
|
|
948
|
+
detail: detail2,
|
|
949
|
+
file: file.filename
|
|
950
|
+
});
|
|
902
951
|
}
|
|
903
952
|
return records;
|
|
904
953
|
}
|
|
@@ -906,12 +955,19 @@ function processJsonFile(file) {
|
|
|
906
955
|
const status = findStatus(obj);
|
|
907
956
|
const ts = findTimestamp(obj) || file.mtime;
|
|
908
957
|
const detail = extractDetail(obj);
|
|
909
|
-
records.push({
|
|
958
|
+
records.push({
|
|
959
|
+
id: nameFromFile(file.filename),
|
|
960
|
+
source: "state",
|
|
961
|
+
status,
|
|
962
|
+
lastActive: ts,
|
|
963
|
+
detail,
|
|
964
|
+
file: file.filename
|
|
965
|
+
});
|
|
910
966
|
return records;
|
|
911
967
|
}
|
|
912
968
|
function processJsonlFile(file) {
|
|
913
969
|
try {
|
|
914
|
-
const content = (0,
|
|
970
|
+
const content = (0, import_node_fs.readFileSync)(file.path, "utf8").trim();
|
|
915
971
|
if (!content) return [];
|
|
916
972
|
const lines = content.split("\n");
|
|
917
973
|
const lineCount = lines.length;
|
|
@@ -922,13 +978,22 @@ function processJsonlFile(file) {
|
|
|
922
978
|
const ts2 = findTimestamp(lastObj) || file.mtime;
|
|
923
979
|
const action = lastObj.action;
|
|
924
980
|
const detail2 = action ? `${action} (${lineCount} entries)` : `${lineCount} entries`;
|
|
925
|
-
return [
|
|
981
|
+
return [
|
|
982
|
+
{
|
|
983
|
+
id: String(name),
|
|
984
|
+
source: "session",
|
|
985
|
+
status: status2,
|
|
986
|
+
lastActive: ts2,
|
|
987
|
+
detail: detail2,
|
|
988
|
+
file: file.filename
|
|
989
|
+
}
|
|
990
|
+
];
|
|
926
991
|
}
|
|
927
992
|
const tail = lines.slice(Math.max(0, lineCount - 30));
|
|
928
993
|
let model = "";
|
|
929
994
|
let totalTokens = 0;
|
|
930
995
|
let totalCost = 0;
|
|
931
|
-
|
|
996
|
+
const toolCalls = [];
|
|
932
997
|
let lastUserMsg = "";
|
|
933
998
|
let lastAssistantMsg = "";
|
|
934
999
|
let errorCount = 0;
|
|
@@ -1027,7 +1092,16 @@ function processJsonlFile(file) {
|
|
|
1027
1092
|
const status = errorCount > lineCount / 4 ? "error" : lastRole === "assistant" ? "ok" : "running";
|
|
1028
1093
|
const ts = findTimestamp(lastObj) || file.mtime;
|
|
1029
1094
|
const sessionName = sessionId ? sessionId.slice(0, 8) : nameFromFile(file.filename);
|
|
1030
|
-
return [
|
|
1095
|
+
return [
|
|
1096
|
+
{
|
|
1097
|
+
id: String(name !== "unknown" ? name : sessionName),
|
|
1098
|
+
source: "session",
|
|
1099
|
+
status,
|
|
1100
|
+
lastActive: ts,
|
|
1101
|
+
detail,
|
|
1102
|
+
file: file.filename
|
|
1103
|
+
}
|
|
1104
|
+
];
|
|
1031
1105
|
} catch {
|
|
1032
1106
|
return [];
|
|
1033
1107
|
}
|
|
@@ -1159,7 +1233,8 @@ function render(config) {
|
|
|
1159
1233
|
if (g.fail > 0 && g.ok === 0 && g.running === 0) return `${C.red}error${C.reset}`;
|
|
1160
1234
|
if (g.running > 0) return `${C.green}running${C.reset}`;
|
|
1161
1235
|
if (g.fail > 0) return `${C.yellow}${g.ok}ok/${g.fail}err${C.reset}`;
|
|
1162
|
-
if (g.ok > 0)
|
|
1236
|
+
if (g.ok > 0)
|
|
1237
|
+
return g.total > 1 ? `${C.green}${g.ok}/${g.total} ok${C.reset}` : `${C.green}ok${C.reset}`;
|
|
1163
1238
|
return `${C.dim}idle${C.reset}`;
|
|
1164
1239
|
}
|
|
1165
1240
|
function sourceTag(s) {
|
|
@@ -1183,24 +1258,38 @@ function render(config) {
|
|
|
1183
1258
|
function truncate(s, max) {
|
|
1184
1259
|
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
1185
1260
|
}
|
|
1261
|
+
const termWidth = process.stdout.columns || 120;
|
|
1262
|
+
const detailWidth = Math.max(20, termWidth - 60);
|
|
1186
1263
|
if (firstRender) {
|
|
1187
1264
|
process.stdout.write("\x1B[2J");
|
|
1188
1265
|
firstRender = false;
|
|
1189
1266
|
}
|
|
1190
1267
|
const L = [];
|
|
1191
1268
|
writeLine(L, `${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
|
|
1192
|
-
writeLine(
|
|
1269
|
+
writeLine(
|
|
1270
|
+
L,
|
|
1271
|
+
`${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}`
|
|
1272
|
+
);
|
|
1193
1273
|
const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
|
|
1194
1274
|
const pad1 = Math.max(0, 64 - metaLine.length);
|
|
1195
|
-
writeLine(
|
|
1275
|
+
writeLine(
|
|
1276
|
+
L,
|
|
1277
|
+
`${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`
|
|
1278
|
+
);
|
|
1196
1279
|
writeLine(L, `${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
|
|
1197
1280
|
const sc = totFail === 0 ? C.green : C.yellow;
|
|
1198
1281
|
writeLine(L, "");
|
|
1199
|
-
writeLine(
|
|
1282
|
+
writeLine(
|
|
1283
|
+
L,
|
|
1284
|
+
` ${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}`
|
|
1285
|
+
);
|
|
1200
1286
|
writeLine(L, "");
|
|
1201
1287
|
writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
|
|
1202
1288
|
writeLine(L, "");
|
|
1203
|
-
writeLine(
|
|
1289
|
+
writeLine(
|
|
1290
|
+
L,
|
|
1291
|
+
` ${C.bold}${C.under}Agent Status Last Active Detail${C.reset}`
|
|
1292
|
+
);
|
|
1204
1293
|
let lineCount = 0;
|
|
1205
1294
|
for (const g of groups) {
|
|
1206
1295
|
if (lineCount > 35) break;
|
|
@@ -1210,14 +1299,20 @@ function render(config) {
|
|
|
1210
1299
|
if (g.children.length === 0) {
|
|
1211
1300
|
const name = truncate(g.name, 26).padEnd(26);
|
|
1212
1301
|
const st = statusText(g);
|
|
1213
|
-
const det = truncate(g.detail,
|
|
1214
|
-
writeLine(
|
|
1302
|
+
const det = truncate(g.detail, detailWidth);
|
|
1303
|
+
writeLine(
|
|
1304
|
+
L,
|
|
1305
|
+
` ${icon} ${name} ${st.padEnd(20)} ${active.padEnd(20)} ${C.dim}${det}${C.reset}`
|
|
1306
|
+
);
|
|
1215
1307
|
lineCount++;
|
|
1216
1308
|
} else {
|
|
1217
1309
|
const name = truncate(g.name, 24).padEnd(24);
|
|
1218
1310
|
const st = statusText(g);
|
|
1219
1311
|
const tag = sourceTag(g.source);
|
|
1220
|
-
writeLine(
|
|
1312
|
+
writeLine(
|
|
1313
|
+
L,
|
|
1314
|
+
` ${icon} ${C.bold}${name}${C.reset} ${st.padEnd(20)} ${active.padEnd(20)} ${tag} ${C.dim}(${g.children.length} agents)${C.reset}`
|
|
1315
|
+
);
|
|
1221
1316
|
lineCount++;
|
|
1222
1317
|
const kids = g.children.slice(0, 12);
|
|
1223
1318
|
for (let i = 0; i < kids.length; i++) {
|
|
@@ -1228,8 +1323,11 @@ function render(config) {
|
|
|
1228
1323
|
const cIcon = statusIcon(child.status, Date.now() - child.lastActive < 3e5);
|
|
1229
1324
|
const cName = truncate(child.id, 22).padEnd(22);
|
|
1230
1325
|
const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
|
|
1231
|
-
const cDet = truncate(child.detail,
|
|
1232
|
-
writeLine(
|
|
1326
|
+
const cDet = truncate(child.detail, detailWidth - 5);
|
|
1327
|
+
writeLine(
|
|
1328
|
+
L,
|
|
1329
|
+
` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`
|
|
1330
|
+
);
|
|
1233
1331
|
lineCount++;
|
|
1234
1332
|
}
|
|
1235
1333
|
if (g.children.length > 12) {
|
|
@@ -1246,7 +1344,10 @@ function render(config) {
|
|
|
1246
1344
|
const si = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
1247
1345
|
const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
|
|
1248
1346
|
const tid = dt.traceId.slice(0, 8);
|
|
1249
|
-
writeLine(
|
|
1347
|
+
writeLine(
|
|
1348
|
+
L,
|
|
1349
|
+
` ${si} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`
|
|
1350
|
+
);
|
|
1250
1351
|
const tree = getTraceTree(dt);
|
|
1251
1352
|
for (let i = 0; i < Math.min(tree.length, 6); i++) {
|
|
1252
1353
|
const tg = tree[i];
|
|
@@ -1256,7 +1357,10 @@ function render(config) {
|
|
|
1256
1357
|
const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1257
1358
|
const gs = tg.status === "completed" ? `${C.green}\u2713${C.reset}` : tg.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
1258
1359
|
const gd = tg.endTime ? `${tg.endTime - tg.startTime}ms` : "running";
|
|
1259
|
-
writeLine(
|
|
1360
|
+
writeLine(
|
|
1361
|
+
L,
|
|
1362
|
+
`${indent}${conn}${gs} ${C.bold}${tg.agentId}${C.reset} ${C.dim}[${tg.trigger}] ${gd}${C.reset}`
|
|
1363
|
+
);
|
|
1260
1364
|
}
|
|
1261
1365
|
}
|
|
1262
1366
|
}
|
|
@@ -1270,8 +1374,11 @@ function render(config) {
|
|
|
1270
1374
|
const agent = truncate(r.id, 26).padEnd(26);
|
|
1271
1375
|
const age = Math.floor((Date.now() - r.lastActive) / 1e3);
|
|
1272
1376
|
const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
|
|
1273
|
-
const det = truncate(r.detail,
|
|
1274
|
-
writeLine(
|
|
1377
|
+
const det = truncate(r.detail, detailWidth);
|
|
1378
|
+
writeLine(
|
|
1379
|
+
L,
|
|
1380
|
+
` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${det}${C.reset}`
|
|
1381
|
+
);
|
|
1275
1382
|
}
|
|
1276
1383
|
}
|
|
1277
1384
|
if (files.length === 0) {
|
|
@@ -1293,39 +1400,540 @@ function getDistDepth(dt, spanId) {
|
|
|
1293
1400
|
}
|
|
1294
1401
|
function startLive(argv) {
|
|
1295
1402
|
const config = parseArgs(argv);
|
|
1296
|
-
const valid = config.dirs.filter((d) => (0,
|
|
1403
|
+
const valid = config.dirs.filter((d) => (0, import_node_fs.existsSync)(d));
|
|
1297
1404
|
if (valid.length === 0) {
|
|
1298
1405
|
console.error(`No valid directories found: ${config.dirs.join(", ")}`);
|
|
1299
1406
|
console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
|
|
1300
1407
|
process.exit(1);
|
|
1301
1408
|
}
|
|
1302
|
-
const invalid = config.dirs.filter((d) => !(0,
|
|
1303
|
-
if (invalid.length > 0) {
|
|
1304
|
-
console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
|
|
1409
|
+
const invalid = config.dirs.filter((d) => !(0, import_node_fs.existsSync)(d));
|
|
1410
|
+
if (invalid.length > 0) {
|
|
1411
|
+
console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
|
|
1412
|
+
}
|
|
1413
|
+
config.dirs = valid;
|
|
1414
|
+
render(config);
|
|
1415
|
+
let debounce = null;
|
|
1416
|
+
for (const dir of config.dirs) {
|
|
1417
|
+
try {
|
|
1418
|
+
(0, import_node_fs.watch)(dir, { recursive: config.recursive }, () => {
|
|
1419
|
+
if (debounce) clearTimeout(debounce);
|
|
1420
|
+
debounce = setTimeout(() => render(config), 500);
|
|
1421
|
+
});
|
|
1422
|
+
} catch {
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
setInterval(() => render(config), config.refreshMs);
|
|
1426
|
+
process.on("SIGINT", () => {
|
|
1427
|
+
console.log("\n" + C.dim + "Monitor stopped." + C.reset);
|
|
1428
|
+
process.exit(0);
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/runner.ts
|
|
1433
|
+
var import_node_child_process = require("child_process");
|
|
1434
|
+
var import_node_fs2 = require("fs");
|
|
1435
|
+
var import_node_path2 = require("path");
|
|
1436
|
+
function globToRegex(pattern) {
|
|
1437
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1438
|
+
return new RegExp(`^${escaped}$`);
|
|
1439
|
+
}
|
|
1440
|
+
function snapshotDir(dir, patterns) {
|
|
1441
|
+
const result = /* @__PURE__ */ new Map();
|
|
1442
|
+
if (!(0, import_node_fs2.existsSync)(dir)) return result;
|
|
1443
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir)) {
|
|
1444
|
+
if (!patterns.some((re) => re.test(entry))) continue;
|
|
1445
|
+
const full = (0, import_node_path2.join)(dir, entry);
|
|
1446
|
+
try {
|
|
1447
|
+
const stat = (0, import_node_fs2.statSync)(full);
|
|
1448
|
+
if (stat.isFile()) {
|
|
1449
|
+
result.set(full, stat.mtimeMs);
|
|
1450
|
+
}
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return result;
|
|
1455
|
+
}
|
|
1456
|
+
function agentIdFromFilename(filePath) {
|
|
1457
|
+
const base = (0, import_node_path2.basename)(filePath, ".json");
|
|
1458
|
+
const cleaned = base.replace(/-state$/, "");
|
|
1459
|
+
return `alfred-${cleaned}`;
|
|
1460
|
+
}
|
|
1461
|
+
function deriveAgentId(command) {
|
|
1462
|
+
return "orchestrator";
|
|
1463
|
+
}
|
|
1464
|
+
function fileTimestamp() {
|
|
1465
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
|
|
1466
|
+
}
|
|
1467
|
+
async function runTraced(config) {
|
|
1468
|
+
const {
|
|
1469
|
+
command,
|
|
1470
|
+
agentId = deriveAgentId(command),
|
|
1471
|
+
trigger = "cli",
|
|
1472
|
+
tracesDir = "./traces",
|
|
1473
|
+
watchDirs = [],
|
|
1474
|
+
watchPatterns = ["*.json"]
|
|
1475
|
+
} = config;
|
|
1476
|
+
if (command.length === 0) {
|
|
1477
|
+
throw new Error("runTraced: command must not be empty");
|
|
1478
|
+
}
|
|
1479
|
+
const resolvedTracesDir = (0, import_node_path2.resolve)(tracesDir);
|
|
1480
|
+
const patterns = watchPatterns.map(globToRegex);
|
|
1481
|
+
const orchestrator = createGraphBuilder({ agentId, trigger });
|
|
1482
|
+
const { traceId, spanId } = orchestrator.traceContext;
|
|
1483
|
+
const beforeSnapshots = /* @__PURE__ */ new Map();
|
|
1484
|
+
for (const dir of watchDirs) {
|
|
1485
|
+
beforeSnapshots.set(dir, snapshotDir(dir, patterns));
|
|
1486
|
+
}
|
|
1487
|
+
const rootId = orchestrator.startNode({ type: "agent", name: agentId });
|
|
1488
|
+
const dispatchId = orchestrator.startNode({
|
|
1489
|
+
type: "tool",
|
|
1490
|
+
name: "dispatch-command",
|
|
1491
|
+
parentId: rootId
|
|
1492
|
+
});
|
|
1493
|
+
orchestrator.updateState(dispatchId, { command: command.join(" ") });
|
|
1494
|
+
const monitorId = orchestrator.startNode({
|
|
1495
|
+
type: "tool",
|
|
1496
|
+
name: "state-monitor",
|
|
1497
|
+
parentId: rootId
|
|
1498
|
+
});
|
|
1499
|
+
orchestrator.updateState(monitorId, {
|
|
1500
|
+
watchDirs,
|
|
1501
|
+
watchPatterns
|
|
1502
|
+
});
|
|
1503
|
+
const startMs = Date.now();
|
|
1504
|
+
const execCmd = command[0] ?? "";
|
|
1505
|
+
const execArgs = command.slice(1);
|
|
1506
|
+
process.env.AGENTFLOW_TRACE_ID = traceId;
|
|
1507
|
+
process.env.AGENTFLOW_PARENT_SPAN_ID = spanId;
|
|
1508
|
+
const result = (0, import_node_child_process.spawnSync)(execCmd, execArgs, { stdio: "inherit" });
|
|
1509
|
+
delete process.env.AGENTFLOW_TRACE_ID;
|
|
1510
|
+
delete process.env.AGENTFLOW_PARENT_SPAN_ID;
|
|
1511
|
+
const exitCode = result.status ?? 1;
|
|
1512
|
+
const duration = (Date.now() - startMs) / 1e3;
|
|
1513
|
+
const stateChanges = [];
|
|
1514
|
+
for (const dir of watchDirs) {
|
|
1515
|
+
const before = beforeSnapshots.get(dir) ?? /* @__PURE__ */ new Map();
|
|
1516
|
+
const after = snapshotDir(dir, patterns);
|
|
1517
|
+
for (const [filePath, mtime] of after) {
|
|
1518
|
+
const prevMtime = before.get(filePath);
|
|
1519
|
+
if (prevMtime === void 0 || mtime > prevMtime) {
|
|
1520
|
+
stateChanges.push(filePath);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
orchestrator.updateState(monitorId, { stateChanges });
|
|
1525
|
+
orchestrator.endNode(monitorId);
|
|
1526
|
+
if (exitCode === 0) {
|
|
1527
|
+
orchestrator.endNode(dispatchId);
|
|
1528
|
+
} else {
|
|
1529
|
+
orchestrator.failNode(dispatchId, `Command exited with code ${exitCode}`);
|
|
1530
|
+
}
|
|
1531
|
+
orchestrator.updateState(rootId, {
|
|
1532
|
+
exitCode,
|
|
1533
|
+
duration,
|
|
1534
|
+
stateChanges
|
|
1535
|
+
});
|
|
1536
|
+
if (exitCode === 0) {
|
|
1537
|
+
orchestrator.endNode(rootId);
|
|
1538
|
+
} else {
|
|
1539
|
+
orchestrator.failNode(rootId, `Command exited with code ${exitCode}`);
|
|
1540
|
+
}
|
|
1541
|
+
const orchestratorGraph = orchestrator.build();
|
|
1542
|
+
const allGraphs = [orchestratorGraph];
|
|
1543
|
+
for (const filePath of stateChanges) {
|
|
1544
|
+
const childAgentId = agentIdFromFilename(filePath);
|
|
1545
|
+
const childBuilder = createGraphBuilder({
|
|
1546
|
+
agentId: childAgentId,
|
|
1547
|
+
trigger: "state-change",
|
|
1548
|
+
traceId,
|
|
1549
|
+
parentSpanId: spanId
|
|
1550
|
+
});
|
|
1551
|
+
const childRootId = childBuilder.startNode({
|
|
1552
|
+
type: "agent",
|
|
1553
|
+
name: childAgentId
|
|
1554
|
+
});
|
|
1555
|
+
childBuilder.updateState(childRootId, {
|
|
1556
|
+
stateFile: filePath,
|
|
1557
|
+
detectedBy: "runner-state-monitor"
|
|
1558
|
+
});
|
|
1559
|
+
childBuilder.endNode(childRootId);
|
|
1560
|
+
allGraphs.push(childBuilder.build());
|
|
1561
|
+
}
|
|
1562
|
+
if (!(0, import_node_fs2.existsSync)(resolvedTracesDir)) {
|
|
1563
|
+
(0, import_node_fs2.mkdirSync)(resolvedTracesDir, { recursive: true });
|
|
1564
|
+
}
|
|
1565
|
+
const ts = fileTimestamp();
|
|
1566
|
+
const tracePaths = [];
|
|
1567
|
+
for (const graph of allGraphs) {
|
|
1568
|
+
const filename = `${graph.agentId}-${ts}.json`;
|
|
1569
|
+
const outPath = (0, import_node_path2.join)(resolvedTracesDir, filename);
|
|
1570
|
+
(0, import_node_fs2.writeFileSync)(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
|
|
1571
|
+
tracePaths.push(outPath);
|
|
1572
|
+
}
|
|
1573
|
+
if (tracePaths.length > 0) {
|
|
1574
|
+
console.log(`\u{1F50D} Run "agentflow trace show ${orchestratorGraph.id} --traces-dir ${resolvedTracesDir}" to inspect`);
|
|
1575
|
+
}
|
|
1576
|
+
return {
|
|
1577
|
+
exitCode,
|
|
1578
|
+
traceId,
|
|
1579
|
+
spanId,
|
|
1580
|
+
tracePaths,
|
|
1581
|
+
stateChanges,
|
|
1582
|
+
duration
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/trace-store.ts
|
|
1587
|
+
var import_promises = require("fs/promises");
|
|
1588
|
+
var import_path = require("path");
|
|
1589
|
+
function createTraceStore(dir) {
|
|
1590
|
+
async function ensureDir() {
|
|
1591
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
1592
|
+
}
|
|
1593
|
+
async function loadAll() {
|
|
1594
|
+
await ensureDir();
|
|
1595
|
+
let files;
|
|
1596
|
+
try {
|
|
1597
|
+
files = await (0, import_promises.readdir)(dir);
|
|
1598
|
+
} catch {
|
|
1599
|
+
return [];
|
|
1600
|
+
}
|
|
1601
|
+
const graphs = [];
|
|
1602
|
+
for (const file of files) {
|
|
1603
|
+
if (!file.endsWith(".json")) continue;
|
|
1604
|
+
try {
|
|
1605
|
+
const content = await (0, import_promises.readFile)((0, import_path.join)(dir, file), "utf-8");
|
|
1606
|
+
const graph = loadGraph(content);
|
|
1607
|
+
graphs.push(graph);
|
|
1608
|
+
} catch {
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return graphs;
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
async save(graph) {
|
|
1615
|
+
await ensureDir();
|
|
1616
|
+
const json = graphToJson(graph);
|
|
1617
|
+
const filePath = (0, import_path.join)(dir, `${graph.id}.json`);
|
|
1618
|
+
await (0, import_promises.writeFile)(filePath, JSON.stringify(json, null, 2), "utf-8");
|
|
1619
|
+
return filePath;
|
|
1620
|
+
},
|
|
1621
|
+
async get(graphId) {
|
|
1622
|
+
await ensureDir();
|
|
1623
|
+
const filePath = (0, import_path.join)(dir, `${graphId}.json`);
|
|
1624
|
+
try {
|
|
1625
|
+
const content = await (0, import_promises.readFile)(filePath, "utf-8");
|
|
1626
|
+
return loadGraph(content);
|
|
1627
|
+
} catch {
|
|
1628
|
+
}
|
|
1629
|
+
const all = await loadAll();
|
|
1630
|
+
return all.find((g) => g.id === graphId) ?? null;
|
|
1631
|
+
},
|
|
1632
|
+
async list(opts) {
|
|
1633
|
+
let graphs = await loadAll();
|
|
1634
|
+
if (opts?.status) {
|
|
1635
|
+
graphs = graphs.filter((g) => g.status === opts.status);
|
|
1636
|
+
}
|
|
1637
|
+
graphs.sort((a, b) => b.startTime - a.startTime);
|
|
1638
|
+
if (opts?.limit && opts.limit > 0) {
|
|
1639
|
+
graphs = graphs.slice(0, opts.limit);
|
|
1640
|
+
}
|
|
1641
|
+
return graphs;
|
|
1642
|
+
},
|
|
1643
|
+
async getStuckSpans() {
|
|
1644
|
+
const graphs = await loadAll();
|
|
1645
|
+
const stuck = [];
|
|
1646
|
+
for (const graph of graphs) {
|
|
1647
|
+
for (const node of graph.nodes.values()) {
|
|
1648
|
+
if (node.status === "running" || node.status === "hung" || node.status === "timeout") {
|
|
1649
|
+
stuck.push(node);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return stuck;
|
|
1654
|
+
},
|
|
1655
|
+
async getReasoningLoops(threshold = 25) {
|
|
1656
|
+
const graphs = await loadAll();
|
|
1657
|
+
const results = [];
|
|
1658
|
+
for (const graph of graphs) {
|
|
1659
|
+
const loops = findLoopsInGraph(graph, threshold);
|
|
1660
|
+
if (loops.length > 0) {
|
|
1661
|
+
results.push({ graphId: graph.id, nodes: loops });
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return results;
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function findLoopsInGraph(graph, threshold) {
|
|
1669
|
+
const loopNodes = [];
|
|
1670
|
+
function walk(nodeId, consecutiveCount, consecutiveType) {
|
|
1671
|
+
const node = graph.nodes.get(nodeId);
|
|
1672
|
+
if (!node) return;
|
|
1673
|
+
const newCount = node.type === consecutiveType ? consecutiveCount + 1 : 1;
|
|
1674
|
+
if (newCount > threshold) {
|
|
1675
|
+
loopNodes.push(node);
|
|
1676
|
+
}
|
|
1677
|
+
for (const childId of node.children) {
|
|
1678
|
+
walk(childId, newCount, node.type);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
walk(graph.rootNodeId, 0, null);
|
|
1682
|
+
return loopNodes;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/visualize.ts
|
|
1686
|
+
var STATUS_ICONS = {
|
|
1687
|
+
completed: "\u2713",
|
|
1688
|
+
failed: "\u2717",
|
|
1689
|
+
running: "\u231B",
|
|
1690
|
+
hung: "\u231B",
|
|
1691
|
+
timeout: "\u231B"
|
|
1692
|
+
};
|
|
1693
|
+
function formatDuration(ms) {
|
|
1694
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1695
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
1696
|
+
if (ms < 36e5) return `${(ms / 6e4).toFixed(1)}m`;
|
|
1697
|
+
return `${(ms / 36e5).toFixed(1)}h`;
|
|
1698
|
+
}
|
|
1699
|
+
function nodeDuration(node, graphEndTime) {
|
|
1700
|
+
const end = node.endTime ?? graphEndTime;
|
|
1701
|
+
return formatDuration(end - node.startTime);
|
|
1702
|
+
}
|
|
1703
|
+
function getGenAiInfo(node) {
|
|
1704
|
+
const parts = [];
|
|
1705
|
+
const meta = node.metadata;
|
|
1706
|
+
if (meta["gen_ai.request.model"]) {
|
|
1707
|
+
parts.push(String(meta["gen_ai.request.model"]));
|
|
1708
|
+
}
|
|
1709
|
+
const tokens = meta["gen_ai.usage.prompt_tokens"] ?? meta["gen_ai.usage.completion_tokens"];
|
|
1710
|
+
if (tokens !== void 0) {
|
|
1711
|
+
const prompt = meta["gen_ai.usage.prompt_tokens"] ?? 0;
|
|
1712
|
+
const completion = meta["gen_ai.usage.completion_tokens"] ?? 0;
|
|
1713
|
+
if (prompt || completion) {
|
|
1714
|
+
parts.push(`${prompt + completion} tok`);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
|
|
1718
|
+
}
|
|
1719
|
+
function hasViolation(node, graph) {
|
|
1720
|
+
return graph.events.some(
|
|
1721
|
+
(e) => e.nodeId === node.id && e.eventType === "custom" && e.data.guardViolation !== void 0
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
function toAsciiTree(graph) {
|
|
1725
|
+
if (graph.nodes.size === 0) return "(empty graph)";
|
|
1726
|
+
const now = Date.now();
|
|
1727
|
+
const endTime = graph.endTime ?? now;
|
|
1728
|
+
const lines = [];
|
|
1729
|
+
function renderNode(nodeId, prefix, isLast, isRoot) {
|
|
1730
|
+
const node = graph.nodes.get(nodeId);
|
|
1731
|
+
if (!node) return;
|
|
1732
|
+
const icon = STATUS_ICONS[node.status];
|
|
1733
|
+
const duration = nodeDuration(node, endTime);
|
|
1734
|
+
const genAi = getGenAiInfo(node);
|
|
1735
|
+
const violation = hasViolation(node, graph) ? " \u26A0" : "";
|
|
1736
|
+
const errorInfo = node.status === "failed" && node.metadata.error ? ` \u2014 ${node.metadata.error}` : "";
|
|
1737
|
+
const timeoutInfo = node.status === "timeout" ? " [TIMEOUT]" : "";
|
|
1738
|
+
const hungInfo = node.status === "hung" ? " [HUNG]" : "";
|
|
1739
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1740
|
+
const line = `${prefix}${connector}${icon} ${node.name} (${node.type}) ${duration}${genAi}${violation}${timeoutInfo}${hungInfo}${errorInfo}`;
|
|
1741
|
+
lines.push(line);
|
|
1742
|
+
const children = getChildren(graph, nodeId);
|
|
1743
|
+
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
1744
|
+
for (let i = 0; i < children.length; i++) {
|
|
1745
|
+
renderNode(children[i].id, childPrefix, i === children.length - 1, false);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
renderNode(graph.rootNodeId, "", true, true);
|
|
1749
|
+
return lines.join("\n");
|
|
1750
|
+
}
|
|
1751
|
+
function toTimeline(graph) {
|
|
1752
|
+
if (graph.nodes.size === 0) return "(empty graph)";
|
|
1753
|
+
const now = Date.now();
|
|
1754
|
+
const graphStart = graph.startTime;
|
|
1755
|
+
const graphEnd = graph.endTime ?? now;
|
|
1756
|
+
const totalDuration = graphEnd - graphStart;
|
|
1757
|
+
if (totalDuration <= 0) return "(zero duration)";
|
|
1758
|
+
const barWidth = 60;
|
|
1759
|
+
const lines = [];
|
|
1760
|
+
const scaleLabels = [];
|
|
1761
|
+
const tickCount = Math.min(5, Math.max(2, Math.floor(barWidth / 10)));
|
|
1762
|
+
for (let i = 0; i <= tickCount; i++) {
|
|
1763
|
+
const t = totalDuration * i / tickCount;
|
|
1764
|
+
scaleLabels.push(formatDuration(t));
|
|
1765
|
+
}
|
|
1766
|
+
let header = "";
|
|
1767
|
+
for (let i = 0; i < scaleLabels.length; i++) {
|
|
1768
|
+
const pos = Math.round(barWidth * i / tickCount);
|
|
1769
|
+
while (header.length < pos) header += " ";
|
|
1770
|
+
header += scaleLabels[i];
|
|
1771
|
+
}
|
|
1772
|
+
lines.push(header);
|
|
1773
|
+
let tickLine = "";
|
|
1774
|
+
for (let i = 0; i < barWidth; i++) {
|
|
1775
|
+
const tickPos = tickCount > 0 ? i * tickCount / barWidth : 0;
|
|
1776
|
+
if (Number.isInteger(Math.round(tickPos * 100) / 100) && Math.abs(tickPos - Math.round(tickPos)) < 0.01) {
|
|
1777
|
+
tickLine += "\u253C";
|
|
1778
|
+
} else {
|
|
1779
|
+
tickLine += "\u2500";
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
lines.push(tickLine);
|
|
1783
|
+
const orderedNodes = [];
|
|
1784
|
+
function collectNodes(nodeId) {
|
|
1785
|
+
const node = graph.nodes.get(nodeId);
|
|
1786
|
+
if (!node) return;
|
|
1787
|
+
orderedNodes.push(node);
|
|
1788
|
+
const children = getChildren(graph, nodeId);
|
|
1789
|
+
for (const child of children) {
|
|
1790
|
+
collectNodes(child.id);
|
|
1791
|
+
}
|
|
1305
1792
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1793
|
+
collectNodes(graph.rootNodeId);
|
|
1794
|
+
for (const node of orderedNodes) {
|
|
1795
|
+
const nodeStart = node.startTime - graphStart;
|
|
1796
|
+
const nodeEnd = (node.endTime ?? now) - graphStart;
|
|
1797
|
+
const startCol = Math.round(nodeStart / totalDuration * barWidth);
|
|
1798
|
+
const endCol = Math.max(startCol + 1, Math.round(nodeEnd / totalDuration * barWidth));
|
|
1799
|
+
let bar = "";
|
|
1800
|
+
for (let i = 0; i < barWidth; i++) {
|
|
1801
|
+
if (i >= startCol && i < endCol) {
|
|
1802
|
+
bar += "\u2588";
|
|
1803
|
+
} else {
|
|
1804
|
+
bar += " ";
|
|
1805
|
+
}
|
|
1316
1806
|
}
|
|
1807
|
+
const icon = STATUS_ICONS[node.status];
|
|
1808
|
+
const duration = nodeDuration(node, graphEnd);
|
|
1809
|
+
const violation = hasViolation(node, graph) ? " \u26A0" : "";
|
|
1810
|
+
lines.push(`${bar} ${icon} ${node.name} (${duration})${violation}`);
|
|
1317
1811
|
}
|
|
1318
|
-
|
|
1319
|
-
process.on("SIGINT", () => {
|
|
1320
|
-
console.log("\n" + C.dim + "Monitor stopped." + C.reset);
|
|
1321
|
-
process.exit(0);
|
|
1322
|
-
});
|
|
1812
|
+
return lines.join("\n");
|
|
1323
1813
|
}
|
|
1324
1814
|
|
|
1325
1815
|
// src/watch.ts
|
|
1326
1816
|
var import_node_fs4 = require("fs");
|
|
1327
|
-
var import_node_path3 = require("path");
|
|
1328
1817
|
var import_node_os = require("os");
|
|
1818
|
+
var import_node_path3 = require("path");
|
|
1819
|
+
|
|
1820
|
+
// src/watch-alerts.ts
|
|
1821
|
+
var import_node_child_process2 = require("child_process");
|
|
1822
|
+
var import_node_http = require("http");
|
|
1823
|
+
var import_node_https = require("https");
|
|
1824
|
+
function formatAlertMessage(payload) {
|
|
1825
|
+
const time = new Date(payload.timestamp).toISOString();
|
|
1826
|
+
const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
|
|
1827
|
+
return [
|
|
1828
|
+
`[ALERT] ${payload.condition}: "${payload.agentId}"`,
|
|
1829
|
+
` Status: ${arrow}`,
|
|
1830
|
+
payload.detail ? ` Detail: ${payload.detail}` : null,
|
|
1831
|
+
` File: ${payload.file}`,
|
|
1832
|
+
` Time: ${time}`
|
|
1833
|
+
].filter(Boolean).join("\n");
|
|
1834
|
+
}
|
|
1835
|
+
function formatTelegram(payload) {
|
|
1836
|
+
const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
|
|
1837
|
+
const time = new Date(payload.timestamp).toLocaleTimeString();
|
|
1838
|
+
return [
|
|
1839
|
+
`${icon} *AgentFlow Alert*`,
|
|
1840
|
+
`*${payload.condition}*: \`${payload.agentId}\``,
|
|
1841
|
+
`Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
|
|
1842
|
+
payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
|
|
1843
|
+
`Time: ${time}`
|
|
1844
|
+
].filter(Boolean).join("\n");
|
|
1845
|
+
}
|
|
1846
|
+
async function sendAlert(payload, channel) {
|
|
1847
|
+
try {
|
|
1848
|
+
switch (channel.type) {
|
|
1849
|
+
case "stdout":
|
|
1850
|
+
sendStdout(payload);
|
|
1851
|
+
break;
|
|
1852
|
+
case "telegram":
|
|
1853
|
+
await sendTelegram(payload, channel.botToken, channel.chatId);
|
|
1854
|
+
break;
|
|
1855
|
+
case "webhook":
|
|
1856
|
+
await sendWebhook(payload, channel.url);
|
|
1857
|
+
break;
|
|
1858
|
+
case "command":
|
|
1859
|
+
await sendCommand(payload, channel.cmd);
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1864
|
+
console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
function sendStdout(payload) {
|
|
1868
|
+
console.log(formatAlertMessage(payload));
|
|
1869
|
+
}
|
|
1870
|
+
function sendTelegram(payload, botToken, chatId) {
|
|
1871
|
+
const body = JSON.stringify({
|
|
1872
|
+
chat_id: chatId,
|
|
1873
|
+
text: formatTelegram(payload),
|
|
1874
|
+
parse_mode: "Markdown"
|
|
1875
|
+
});
|
|
1876
|
+
return new Promise((resolve4, reject) => {
|
|
1877
|
+
const req = (0, import_node_https.request)(
|
|
1878
|
+
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1879
|
+
{
|
|
1880
|
+
method: "POST",
|
|
1881
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
|
|
1882
|
+
},
|
|
1883
|
+
(res) => {
|
|
1884
|
+
res.resume();
|
|
1885
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1886
|
+
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1887
|
+
}
|
|
1888
|
+
);
|
|
1889
|
+
req.on("error", reject);
|
|
1890
|
+
req.write(body);
|
|
1891
|
+
req.end();
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
function sendWebhook(payload, url) {
|
|
1895
|
+
const body = JSON.stringify(payload);
|
|
1896
|
+
const isHttps = url.startsWith("https");
|
|
1897
|
+
const doRequest = isHttps ? import_node_https.request : import_node_http.request;
|
|
1898
|
+
return new Promise((resolve4, reject) => {
|
|
1899
|
+
const req = doRequest(
|
|
1900
|
+
url,
|
|
1901
|
+
{
|
|
1902
|
+
method: "POST",
|
|
1903
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
|
|
1904
|
+
},
|
|
1905
|
+
(res) => {
|
|
1906
|
+
res.resume();
|
|
1907
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1908
|
+
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1909
|
+
}
|
|
1910
|
+
);
|
|
1911
|
+
req.on("error", reject);
|
|
1912
|
+
req.setTimeout(1e4, () => {
|
|
1913
|
+
req.destroy(new Error("Webhook timeout"));
|
|
1914
|
+
});
|
|
1915
|
+
req.write(body);
|
|
1916
|
+
req.end();
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
function sendCommand(payload, cmd) {
|
|
1920
|
+
return new Promise((resolve4, reject) => {
|
|
1921
|
+
const env = {
|
|
1922
|
+
...process.env,
|
|
1923
|
+
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
1924
|
+
AGENTFLOW_ALERT_CONDITION: payload.condition,
|
|
1925
|
+
AGENTFLOW_ALERT_STATUS: payload.currentStatus,
|
|
1926
|
+
AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
|
|
1927
|
+
AGENTFLOW_ALERT_DETAIL: payload.detail,
|
|
1928
|
+
AGENTFLOW_ALERT_FILE: payload.file,
|
|
1929
|
+
AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
|
|
1930
|
+
};
|
|
1931
|
+
(0, import_node_child_process2.exec)(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1932
|
+
if (err) reject(err);
|
|
1933
|
+
else resolve4();
|
|
1934
|
+
});
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1329
1937
|
|
|
1330
1938
|
// src/watch-state.ts
|
|
1331
1939
|
var import_node_fs3 = require("fs");
|
|
@@ -1391,7 +1999,9 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1391
1999
|
const hasError = config.alertConditions.some((c) => c.type === "error");
|
|
1392
2000
|
const hasRecovery = config.alertConditions.some((c) => c.type === "recovery");
|
|
1393
2001
|
const staleConditions = config.alertConditions.filter((c) => c.type === "stale");
|
|
1394
|
-
const consecutiveConditions = config.alertConditions.filter(
|
|
2002
|
+
const consecutiveConditions = config.alertConditions.filter(
|
|
2003
|
+
(c) => c.type === "consecutive-errors"
|
|
2004
|
+
);
|
|
1395
2005
|
const byAgent = /* @__PURE__ */ new Map();
|
|
1396
2006
|
for (const r of currentRecords) {
|
|
1397
2007
|
const existing = byAgent.get(r.id);
|
|
@@ -1415,14 +2025,16 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1415
2025
|
for (const cond of consecutiveConditions) {
|
|
1416
2026
|
if (newConsec === cond.threshold) {
|
|
1417
2027
|
if (canAlert(prev, `consecutive-errors:${cond.threshold}`, config.cooldownMs, now)) {
|
|
1418
|
-
alerts.push(
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
2028
|
+
alerts.push(
|
|
2029
|
+
makePayload(
|
|
2030
|
+
agentId,
|
|
2031
|
+
`consecutive-errors (${cond.threshold})`,
|
|
2032
|
+
prevStatus,
|
|
2033
|
+
currStatus,
|
|
2034
|
+
{ ...record, detail: `${newConsec} consecutive errors. ${record.detail}` },
|
|
2035
|
+
config.dirs
|
|
2036
|
+
)
|
|
2037
|
+
);
|
|
1426
2038
|
}
|
|
1427
2039
|
}
|
|
1428
2040
|
}
|
|
@@ -1431,14 +2043,16 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1431
2043
|
if (sinceActive > cond.durationMs && record.lastActive > 0) {
|
|
1432
2044
|
if (canAlert(prev, "stale", config.cooldownMs, now)) {
|
|
1433
2045
|
const mins = Math.floor(sinceActive / 6e4);
|
|
1434
|
-
alerts.push(
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
2046
|
+
alerts.push(
|
|
2047
|
+
makePayload(
|
|
2048
|
+
agentId,
|
|
2049
|
+
"stale",
|
|
2050
|
+
prevStatus,
|
|
2051
|
+
currStatus,
|
|
2052
|
+
{ ...record, detail: `No update for ${mins}m. ${record.detail}` },
|
|
2053
|
+
config.dirs
|
|
2054
|
+
)
|
|
2055
|
+
);
|
|
1442
2056
|
}
|
|
1443
2057
|
}
|
|
1444
2058
|
}
|
|
@@ -1451,14 +2065,19 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1451
2065
|
if (canAlert(prev, "stale-auto", config.cooldownMs, now)) {
|
|
1452
2066
|
const mins = Math.floor(sinceActive / 6e4);
|
|
1453
2067
|
const expectedMins = Math.floor(expectedInterval / 6e4);
|
|
1454
|
-
alerts.push(
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
2068
|
+
alerts.push(
|
|
2069
|
+
makePayload(
|
|
2070
|
+
agentId,
|
|
2071
|
+
"stale (auto)",
|
|
2072
|
+
prevStatus,
|
|
2073
|
+
currStatus,
|
|
2074
|
+
{
|
|
2075
|
+
...record,
|
|
2076
|
+
detail: `No update for ${mins}m (expected every ~${expectedMins}m). ${record.detail}`
|
|
2077
|
+
},
|
|
2078
|
+
config.dirs
|
|
2079
|
+
)
|
|
2080
|
+
);
|
|
1462
2081
|
}
|
|
1463
2082
|
}
|
|
1464
2083
|
}
|
|
@@ -1517,118 +2136,6 @@ function makePayload(agentId, condition, previousStatus, currentStatus, record,
|
|
|
1517
2136
|
};
|
|
1518
2137
|
}
|
|
1519
2138
|
|
|
1520
|
-
// src/watch-alerts.ts
|
|
1521
|
-
var import_node_https = require("https");
|
|
1522
|
-
var import_node_http = require("http");
|
|
1523
|
-
var import_node_child_process2 = require("child_process");
|
|
1524
|
-
function formatAlertMessage(payload) {
|
|
1525
|
-
const time = new Date(payload.timestamp).toISOString();
|
|
1526
|
-
const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
|
|
1527
|
-
return [
|
|
1528
|
-
`[ALERT] ${payload.condition}: "${payload.agentId}"`,
|
|
1529
|
-
` Status: ${arrow}`,
|
|
1530
|
-
payload.detail ? ` Detail: ${payload.detail}` : null,
|
|
1531
|
-
` File: ${payload.file}`,
|
|
1532
|
-
` Time: ${time}`
|
|
1533
|
-
].filter(Boolean).join("\n");
|
|
1534
|
-
}
|
|
1535
|
-
function formatTelegram(payload) {
|
|
1536
|
-
const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
|
|
1537
|
-
const time = new Date(payload.timestamp).toLocaleTimeString();
|
|
1538
|
-
return [
|
|
1539
|
-
`${icon} *AgentFlow Alert*`,
|
|
1540
|
-
`*${payload.condition}*: \`${payload.agentId}\``,
|
|
1541
|
-
`Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
|
|
1542
|
-
payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
|
|
1543
|
-
`Time: ${time}`
|
|
1544
|
-
].filter(Boolean).join("\n");
|
|
1545
|
-
}
|
|
1546
|
-
async function sendAlert(payload, channel) {
|
|
1547
|
-
try {
|
|
1548
|
-
switch (channel.type) {
|
|
1549
|
-
case "stdout":
|
|
1550
|
-
sendStdout(payload);
|
|
1551
|
-
break;
|
|
1552
|
-
case "telegram":
|
|
1553
|
-
await sendTelegram(payload, channel.botToken, channel.chatId);
|
|
1554
|
-
break;
|
|
1555
|
-
case "webhook":
|
|
1556
|
-
await sendWebhook(payload, channel.url);
|
|
1557
|
-
break;
|
|
1558
|
-
case "command":
|
|
1559
|
-
await sendCommand(payload, channel.cmd);
|
|
1560
|
-
break;
|
|
1561
|
-
}
|
|
1562
|
-
} catch (err) {
|
|
1563
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1564
|
-
console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
function sendStdout(payload) {
|
|
1568
|
-
console.log(formatAlertMessage(payload));
|
|
1569
|
-
}
|
|
1570
|
-
function sendTelegram(payload, botToken, chatId) {
|
|
1571
|
-
const body = JSON.stringify({
|
|
1572
|
-
chat_id: chatId,
|
|
1573
|
-
text: formatTelegram(payload),
|
|
1574
|
-
parse_mode: "Markdown"
|
|
1575
|
-
});
|
|
1576
|
-
return new Promise((resolve4, reject) => {
|
|
1577
|
-
const req = (0, import_node_https.request)(
|
|
1578
|
-
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1579
|
-
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1580
|
-
(res) => {
|
|
1581
|
-
res.resume();
|
|
1582
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1583
|
-
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1584
|
-
}
|
|
1585
|
-
);
|
|
1586
|
-
req.on("error", reject);
|
|
1587
|
-
req.write(body);
|
|
1588
|
-
req.end();
|
|
1589
|
-
});
|
|
1590
|
-
}
|
|
1591
|
-
function sendWebhook(payload, url) {
|
|
1592
|
-
const body = JSON.stringify(payload);
|
|
1593
|
-
const isHttps = url.startsWith("https");
|
|
1594
|
-
const doRequest = isHttps ? import_node_https.request : import_node_http.request;
|
|
1595
|
-
return new Promise((resolve4, reject) => {
|
|
1596
|
-
const req = doRequest(
|
|
1597
|
-
url,
|
|
1598
|
-
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1599
|
-
(res) => {
|
|
1600
|
-
res.resume();
|
|
1601
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1602
|
-
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1603
|
-
}
|
|
1604
|
-
);
|
|
1605
|
-
req.on("error", reject);
|
|
1606
|
-
req.setTimeout(1e4, () => {
|
|
1607
|
-
req.destroy(new Error("Webhook timeout"));
|
|
1608
|
-
});
|
|
1609
|
-
req.write(body);
|
|
1610
|
-
req.end();
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
|
-
function sendCommand(payload, cmd) {
|
|
1614
|
-
return new Promise((resolve4, reject) => {
|
|
1615
|
-
const env = {
|
|
1616
|
-
...process.env,
|
|
1617
|
-
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
1618
|
-
AGENTFLOW_ALERT_CONDITION: payload.condition,
|
|
1619
|
-
AGENTFLOW_ALERT_STATUS: payload.currentStatus,
|
|
1620
|
-
AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
|
|
1621
|
-
AGENTFLOW_ALERT_DETAIL: payload.detail,
|
|
1622
|
-
AGENTFLOW_ALERT_FILE: payload.file,
|
|
1623
|
-
AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
|
|
1624
|
-
};
|
|
1625
|
-
(0, import_node_child_process2.exec)(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1626
|
-
if (err) reject(err);
|
|
1627
|
-
else resolve4();
|
|
1628
|
-
});
|
|
1629
|
-
});
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
2139
|
// src/watch.ts
|
|
1633
2140
|
function parseWatchArgs(argv) {
|
|
1634
2141
|
const dirs = [];
|
|
@@ -1670,7 +2177,9 @@ function parseWatchArgs(argv) {
|
|
|
1670
2177
|
if (botToken && chatId) {
|
|
1671
2178
|
notifyChannels.push({ type: "telegram", botToken, chatId });
|
|
1672
2179
|
} else {
|
|
1673
|
-
console.error(
|
|
2180
|
+
console.error(
|
|
2181
|
+
"Warning: --notify telegram requires AGENTFLOW_TELEGRAM_BOT_TOKEN and AGENTFLOW_TELEGRAM_CHAT_ID env vars"
|
|
2182
|
+
);
|
|
1674
2183
|
}
|
|
1675
2184
|
} else if (val.startsWith("webhook:")) {
|
|
1676
2185
|
notifyChannels.push({ type: "webhook", url: val.slice(8) });
|
|
@@ -1722,7 +2231,8 @@ function parseWatchArgs(argv) {
|
|
|
1722
2231
|
};
|
|
1723
2232
|
}
|
|
1724
2233
|
function printWatchUsage() {
|
|
1725
|
-
console.log(
|
|
2234
|
+
console.log(
|
|
2235
|
+
`
|
|
1726
2236
|
AgentFlow Watch \u2014 headless alert system for agent infrastructure.
|
|
1727
2237
|
|
|
1728
2238
|
Polls directories for JSON/JSONL files, detects failures and stale
|
|
@@ -1765,7 +2275,8 @@ Examples:
|
|
|
1765
2275
|
agentflow watch ./data ./cron --notify telegram --poll 60
|
|
1766
2276
|
agentflow watch ./traces --notify webhook:https://hooks.slack.com/... --alert-on consecutive-errors:3
|
|
1767
2277
|
agentflow watch ./data --notify "command:curl -X POST https://my-pagerduty/alert"
|
|
1768
|
-
`.trim()
|
|
2278
|
+
`.trim()
|
|
2279
|
+
);
|
|
1769
2280
|
}
|
|
1770
2281
|
function startWatch(argv) {
|
|
1771
2282
|
const config = parseWatchArgs(argv);
|
|
@@ -1794,7 +2305,9 @@ agentflow watch started`);
|
|
|
1794
2305
|
console.log(` Directories: ${valid.join(", ")}`);
|
|
1795
2306
|
console.log(` Poll: ${config.pollIntervalMs / 1e3}s`);
|
|
1796
2307
|
console.log(` Alert on: ${condLabels.join(", ")}`);
|
|
1797
|
-
console.log(
|
|
2308
|
+
console.log(
|
|
2309
|
+
` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`
|
|
2310
|
+
);
|
|
1798
2311
|
console.log(` Cooldown: ${Math.floor(config.cooldownMs / 6e4)}m`);
|
|
1799
2312
|
console.log(` State: ${config.stateFilePath}`);
|
|
1800
2313
|
console.log(` Hostname: ${(0, import_node_os.hostname)()}`);
|
|
@@ -1820,9 +2333,13 @@ agentflow watch started`);
|
|
|
1820
2333
|
if (pollCount % 10 === 0) {
|
|
1821
2334
|
const agentCount = Object.keys(state.agents).length;
|
|
1822
2335
|
const errorCount = Object.values(state.agents).filter((a) => a.lastStatus === "error").length;
|
|
1823
|
-
const runningCount = Object.values(state.agents).filter(
|
|
2336
|
+
const runningCount = Object.values(state.agents).filter(
|
|
2337
|
+
(a) => a.lastStatus === "running"
|
|
2338
|
+
).length;
|
|
1824
2339
|
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1825
|
-
console.log(
|
|
2340
|
+
console.log(
|
|
2341
|
+
`[${time}] heartbeat: ${agentCount} agents, ${runningCount} running, ${errorCount} errors, ${files.length} files`
|
|
2342
|
+
);
|
|
1826
2343
|
}
|
|
1827
2344
|
}
|
|
1828
2345
|
poll();
|
|
@@ -1839,7 +2356,9 @@ agentflow watch started`);
|
|
|
1839
2356
|
}
|
|
1840
2357
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1841
2358
|
0 && (module.exports = {
|
|
2359
|
+
checkGuards,
|
|
1842
2360
|
createGraphBuilder,
|
|
2361
|
+
createTraceStore,
|
|
1843
2362
|
findWaitingOn,
|
|
1844
2363
|
getChildren,
|
|
1845
2364
|
getCriticalPath,
|
|
@@ -1858,5 +2377,8 @@ agentflow watch started`);
|
|
|
1858
2377
|
runTraced,
|
|
1859
2378
|
startLive,
|
|
1860
2379
|
startWatch,
|
|
1861
|
-
stitchTrace
|
|
2380
|
+
stitchTrace,
|
|
2381
|
+
toAsciiTree,
|
|
2382
|
+
toTimeline,
|
|
2383
|
+
withGuards
|
|
1862
2384
|
});
|