agentflow-core 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-M5CGDXAO.js → chunk-DHCTDCDI.js} +1612 -1306
- package/dist/chunk-DY7YHFIB.js +56 -0
- package/dist/cli.cjs +1185 -577
- package/dist/cli.js +217 -9
- package/dist/index.cjs +936 -416
- 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) {
|
|
@@ -1191,18 +1266,30 @@ function render(config) {
|
|
|
1191
1266
|
}
|
|
1192
1267
|
const L = [];
|
|
1193
1268
|
writeLine(L, `${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
|
|
1194
|
-
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
|
+
);
|
|
1195
1273
|
const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
|
|
1196
1274
|
const pad1 = Math.max(0, 64 - metaLine.length);
|
|
1197
|
-
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
|
+
);
|
|
1198
1279
|
writeLine(L, `${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
|
|
1199
1280
|
const sc = totFail === 0 ? C.green : C.yellow;
|
|
1200
1281
|
writeLine(L, "");
|
|
1201
|
-
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
|
+
);
|
|
1202
1286
|
writeLine(L, "");
|
|
1203
1287
|
writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
|
|
1204
1288
|
writeLine(L, "");
|
|
1205
|
-
writeLine(
|
|
1289
|
+
writeLine(
|
|
1290
|
+
L,
|
|
1291
|
+
` ${C.bold}${C.under}Agent Status Last Active Detail${C.reset}`
|
|
1292
|
+
);
|
|
1206
1293
|
let lineCount = 0;
|
|
1207
1294
|
for (const g of groups) {
|
|
1208
1295
|
if (lineCount > 35) break;
|
|
@@ -1213,13 +1300,19 @@ function render(config) {
|
|
|
1213
1300
|
const name = truncate(g.name, 26).padEnd(26);
|
|
1214
1301
|
const st = statusText(g);
|
|
1215
1302
|
const det = truncate(g.detail, detailWidth);
|
|
1216
|
-
writeLine(
|
|
1303
|
+
writeLine(
|
|
1304
|
+
L,
|
|
1305
|
+
` ${icon} ${name} ${st.padEnd(20)} ${active.padEnd(20)} ${C.dim}${det}${C.reset}`
|
|
1306
|
+
);
|
|
1217
1307
|
lineCount++;
|
|
1218
1308
|
} else {
|
|
1219
1309
|
const name = truncate(g.name, 24).padEnd(24);
|
|
1220
1310
|
const st = statusText(g);
|
|
1221
1311
|
const tag = sourceTag(g.source);
|
|
1222
|
-
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
|
+
);
|
|
1223
1316
|
lineCount++;
|
|
1224
1317
|
const kids = g.children.slice(0, 12);
|
|
1225
1318
|
for (let i = 0; i < kids.length; i++) {
|
|
@@ -1231,7 +1324,10 @@ function render(config) {
|
|
|
1231
1324
|
const cName = truncate(child.id, 22).padEnd(22);
|
|
1232
1325
|
const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
|
|
1233
1326
|
const cDet = truncate(child.detail, detailWidth - 5);
|
|
1234
|
-
writeLine(
|
|
1327
|
+
writeLine(
|
|
1328
|
+
L,
|
|
1329
|
+
` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`
|
|
1330
|
+
);
|
|
1235
1331
|
lineCount++;
|
|
1236
1332
|
}
|
|
1237
1333
|
if (g.children.length > 12) {
|
|
@@ -1248,7 +1344,10 @@ function render(config) {
|
|
|
1248
1344
|
const si = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
1249
1345
|
const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
|
|
1250
1346
|
const tid = dt.traceId.slice(0, 8);
|
|
1251
|
-
writeLine(
|
|
1347
|
+
writeLine(
|
|
1348
|
+
L,
|
|
1349
|
+
` ${si} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`
|
|
1350
|
+
);
|
|
1252
1351
|
const tree = getTraceTree(dt);
|
|
1253
1352
|
for (let i = 0; i < Math.min(tree.length, 6); i++) {
|
|
1254
1353
|
const tg = tree[i];
|
|
@@ -1258,7 +1357,10 @@ function render(config) {
|
|
|
1258
1357
|
const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1259
1358
|
const gs = tg.status === "completed" ? `${C.green}\u2713${C.reset}` : tg.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
1260
1359
|
const gd = tg.endTime ? `${tg.endTime - tg.startTime}ms` : "running";
|
|
1261
|
-
writeLine(
|
|
1360
|
+
writeLine(
|
|
1361
|
+
L,
|
|
1362
|
+
`${indent}${conn}${gs} ${C.bold}${tg.agentId}${C.reset} ${C.dim}[${tg.trigger}] ${gd}${C.reset}`
|
|
1363
|
+
);
|
|
1262
1364
|
}
|
|
1263
1365
|
}
|
|
1264
1366
|
}
|
|
@@ -1273,7 +1375,10 @@ function render(config) {
|
|
|
1273
1375
|
const age = Math.floor((Date.now() - r.lastActive) / 1e3);
|
|
1274
1376
|
const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
|
|
1275
1377
|
const det = truncate(r.detail, detailWidth);
|
|
1276
|
-
writeLine(
|
|
1378
|
+
writeLine(
|
|
1379
|
+
L,
|
|
1380
|
+
` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${det}${C.reset}`
|
|
1381
|
+
);
|
|
1277
1382
|
}
|
|
1278
1383
|
}
|
|
1279
1384
|
if (files.length === 0) {
|
|
@@ -1295,39 +1400,540 @@ function getDistDepth(dt, spanId) {
|
|
|
1295
1400
|
}
|
|
1296
1401
|
function startLive(argv) {
|
|
1297
1402
|
const config = parseArgs(argv);
|
|
1298
|
-
const valid = config.dirs.filter((d) => (0,
|
|
1403
|
+
const valid = config.dirs.filter((d) => (0, import_node_fs.existsSync)(d));
|
|
1299
1404
|
if (valid.length === 0) {
|
|
1300
1405
|
console.error(`No valid directories found: ${config.dirs.join(", ")}`);
|
|
1301
1406
|
console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
|
|
1302
1407
|
process.exit(1);
|
|
1303
1408
|
}
|
|
1304
|
-
const invalid = config.dirs.filter((d) => !(0,
|
|
1305
|
-
if (invalid.length > 0) {
|
|
1306
|
-
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
|
+
}
|
|
1307
1792
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
+
}
|
|
1318
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}`);
|
|
1319
1811
|
}
|
|
1320
|
-
|
|
1321
|
-
process.on("SIGINT", () => {
|
|
1322
|
-
console.log("\n" + C.dim + "Monitor stopped." + C.reset);
|
|
1323
|
-
process.exit(0);
|
|
1324
|
-
});
|
|
1812
|
+
return lines.join("\n");
|
|
1325
1813
|
}
|
|
1326
1814
|
|
|
1327
1815
|
// src/watch.ts
|
|
1328
1816
|
var import_node_fs4 = require("fs");
|
|
1329
|
-
var import_node_path3 = require("path");
|
|
1330
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
|
+
}
|
|
1331
1937
|
|
|
1332
1938
|
// src/watch-state.ts
|
|
1333
1939
|
var import_node_fs3 = require("fs");
|
|
@@ -1393,7 +1999,9 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1393
1999
|
const hasError = config.alertConditions.some((c) => c.type === "error");
|
|
1394
2000
|
const hasRecovery = config.alertConditions.some((c) => c.type === "recovery");
|
|
1395
2001
|
const staleConditions = config.alertConditions.filter((c) => c.type === "stale");
|
|
1396
|
-
const consecutiveConditions = config.alertConditions.filter(
|
|
2002
|
+
const consecutiveConditions = config.alertConditions.filter(
|
|
2003
|
+
(c) => c.type === "consecutive-errors"
|
|
2004
|
+
);
|
|
1397
2005
|
const byAgent = /* @__PURE__ */ new Map();
|
|
1398
2006
|
for (const r of currentRecords) {
|
|
1399
2007
|
const existing = byAgent.get(r.id);
|
|
@@ -1417,14 +2025,16 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1417
2025
|
for (const cond of consecutiveConditions) {
|
|
1418
2026
|
if (newConsec === cond.threshold) {
|
|
1419
2027
|
if (canAlert(prev, `consecutive-errors:${cond.threshold}`, config.cooldownMs, now)) {
|
|
1420
|
-
alerts.push(
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|
+
);
|
|
1428
2038
|
}
|
|
1429
2039
|
}
|
|
1430
2040
|
}
|
|
@@ -1433,14 +2043,16 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1433
2043
|
if (sinceActive > cond.durationMs && record.lastActive > 0) {
|
|
1434
2044
|
if (canAlert(prev, "stale", config.cooldownMs, now)) {
|
|
1435
2045
|
const mins = Math.floor(sinceActive / 6e4);
|
|
1436
|
-
alerts.push(
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
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
|
+
);
|
|
1444
2056
|
}
|
|
1445
2057
|
}
|
|
1446
2058
|
}
|
|
@@ -1453,14 +2065,19 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1453
2065
|
if (canAlert(prev, "stale-auto", config.cooldownMs, now)) {
|
|
1454
2066
|
const mins = Math.floor(sinceActive / 6e4);
|
|
1455
2067
|
const expectedMins = Math.floor(expectedInterval / 6e4);
|
|
1456
|
-
alerts.push(
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
+
);
|
|
1464
2081
|
}
|
|
1465
2082
|
}
|
|
1466
2083
|
}
|
|
@@ -1519,118 +2136,6 @@ function makePayload(agentId, condition, previousStatus, currentStatus, record,
|
|
|
1519
2136
|
};
|
|
1520
2137
|
}
|
|
1521
2138
|
|
|
1522
|
-
// src/watch-alerts.ts
|
|
1523
|
-
var import_node_https = require("https");
|
|
1524
|
-
var import_node_http = require("http");
|
|
1525
|
-
var import_node_child_process2 = require("child_process");
|
|
1526
|
-
function formatAlertMessage(payload) {
|
|
1527
|
-
const time = new Date(payload.timestamp).toISOString();
|
|
1528
|
-
const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
|
|
1529
|
-
return [
|
|
1530
|
-
`[ALERT] ${payload.condition}: "${payload.agentId}"`,
|
|
1531
|
-
` Status: ${arrow}`,
|
|
1532
|
-
payload.detail ? ` Detail: ${payload.detail}` : null,
|
|
1533
|
-
` File: ${payload.file}`,
|
|
1534
|
-
` Time: ${time}`
|
|
1535
|
-
].filter(Boolean).join("\n");
|
|
1536
|
-
}
|
|
1537
|
-
function formatTelegram(payload) {
|
|
1538
|
-
const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
|
|
1539
|
-
const time = new Date(payload.timestamp).toLocaleTimeString();
|
|
1540
|
-
return [
|
|
1541
|
-
`${icon} *AgentFlow Alert*`,
|
|
1542
|
-
`*${payload.condition}*: \`${payload.agentId}\``,
|
|
1543
|
-
`Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
|
|
1544
|
-
payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
|
|
1545
|
-
`Time: ${time}`
|
|
1546
|
-
].filter(Boolean).join("\n");
|
|
1547
|
-
}
|
|
1548
|
-
async function sendAlert(payload, channel) {
|
|
1549
|
-
try {
|
|
1550
|
-
switch (channel.type) {
|
|
1551
|
-
case "stdout":
|
|
1552
|
-
sendStdout(payload);
|
|
1553
|
-
break;
|
|
1554
|
-
case "telegram":
|
|
1555
|
-
await sendTelegram(payload, channel.botToken, channel.chatId);
|
|
1556
|
-
break;
|
|
1557
|
-
case "webhook":
|
|
1558
|
-
await sendWebhook(payload, channel.url);
|
|
1559
|
-
break;
|
|
1560
|
-
case "command":
|
|
1561
|
-
await sendCommand(payload, channel.cmd);
|
|
1562
|
-
break;
|
|
1563
|
-
}
|
|
1564
|
-
} catch (err) {
|
|
1565
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1566
|
-
console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
function sendStdout(payload) {
|
|
1570
|
-
console.log(formatAlertMessage(payload));
|
|
1571
|
-
}
|
|
1572
|
-
function sendTelegram(payload, botToken, chatId) {
|
|
1573
|
-
const body = JSON.stringify({
|
|
1574
|
-
chat_id: chatId,
|
|
1575
|
-
text: formatTelegram(payload),
|
|
1576
|
-
parse_mode: "Markdown"
|
|
1577
|
-
});
|
|
1578
|
-
return new Promise((resolve4, reject) => {
|
|
1579
|
-
const req = (0, import_node_https.request)(
|
|
1580
|
-
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1581
|
-
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1582
|
-
(res) => {
|
|
1583
|
-
res.resume();
|
|
1584
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1585
|
-
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1586
|
-
}
|
|
1587
|
-
);
|
|
1588
|
-
req.on("error", reject);
|
|
1589
|
-
req.write(body);
|
|
1590
|
-
req.end();
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
function sendWebhook(payload, url) {
|
|
1594
|
-
const body = JSON.stringify(payload);
|
|
1595
|
-
const isHttps = url.startsWith("https");
|
|
1596
|
-
const doRequest = isHttps ? import_node_https.request : import_node_http.request;
|
|
1597
|
-
return new Promise((resolve4, reject) => {
|
|
1598
|
-
const req = doRequest(
|
|
1599
|
-
url,
|
|
1600
|
-
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1601
|
-
(res) => {
|
|
1602
|
-
res.resume();
|
|
1603
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1604
|
-
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1605
|
-
}
|
|
1606
|
-
);
|
|
1607
|
-
req.on("error", reject);
|
|
1608
|
-
req.setTimeout(1e4, () => {
|
|
1609
|
-
req.destroy(new Error("Webhook timeout"));
|
|
1610
|
-
});
|
|
1611
|
-
req.write(body);
|
|
1612
|
-
req.end();
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
|
-
function sendCommand(payload, cmd) {
|
|
1616
|
-
return new Promise((resolve4, reject) => {
|
|
1617
|
-
const env = {
|
|
1618
|
-
...process.env,
|
|
1619
|
-
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
1620
|
-
AGENTFLOW_ALERT_CONDITION: payload.condition,
|
|
1621
|
-
AGENTFLOW_ALERT_STATUS: payload.currentStatus,
|
|
1622
|
-
AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
|
|
1623
|
-
AGENTFLOW_ALERT_DETAIL: payload.detail,
|
|
1624
|
-
AGENTFLOW_ALERT_FILE: payload.file,
|
|
1625
|
-
AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
|
|
1626
|
-
};
|
|
1627
|
-
(0, import_node_child_process2.exec)(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1628
|
-
if (err) reject(err);
|
|
1629
|
-
else resolve4();
|
|
1630
|
-
});
|
|
1631
|
-
});
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
2139
|
// src/watch.ts
|
|
1635
2140
|
function parseWatchArgs(argv) {
|
|
1636
2141
|
const dirs = [];
|
|
@@ -1672,7 +2177,9 @@ function parseWatchArgs(argv) {
|
|
|
1672
2177
|
if (botToken && chatId) {
|
|
1673
2178
|
notifyChannels.push({ type: "telegram", botToken, chatId });
|
|
1674
2179
|
} else {
|
|
1675
|
-
console.error(
|
|
2180
|
+
console.error(
|
|
2181
|
+
"Warning: --notify telegram requires AGENTFLOW_TELEGRAM_BOT_TOKEN and AGENTFLOW_TELEGRAM_CHAT_ID env vars"
|
|
2182
|
+
);
|
|
1676
2183
|
}
|
|
1677
2184
|
} else if (val.startsWith("webhook:")) {
|
|
1678
2185
|
notifyChannels.push({ type: "webhook", url: val.slice(8) });
|
|
@@ -1724,7 +2231,8 @@ function parseWatchArgs(argv) {
|
|
|
1724
2231
|
};
|
|
1725
2232
|
}
|
|
1726
2233
|
function printWatchUsage() {
|
|
1727
|
-
console.log(
|
|
2234
|
+
console.log(
|
|
2235
|
+
`
|
|
1728
2236
|
AgentFlow Watch \u2014 headless alert system for agent infrastructure.
|
|
1729
2237
|
|
|
1730
2238
|
Polls directories for JSON/JSONL files, detects failures and stale
|
|
@@ -1767,7 +2275,8 @@ Examples:
|
|
|
1767
2275
|
agentflow watch ./data ./cron --notify telegram --poll 60
|
|
1768
2276
|
agentflow watch ./traces --notify webhook:https://hooks.slack.com/... --alert-on consecutive-errors:3
|
|
1769
2277
|
agentflow watch ./data --notify "command:curl -X POST https://my-pagerduty/alert"
|
|
1770
|
-
`.trim()
|
|
2278
|
+
`.trim()
|
|
2279
|
+
);
|
|
1771
2280
|
}
|
|
1772
2281
|
function startWatch(argv) {
|
|
1773
2282
|
const config = parseWatchArgs(argv);
|
|
@@ -1796,7 +2305,9 @@ agentflow watch started`);
|
|
|
1796
2305
|
console.log(` Directories: ${valid.join(", ")}`);
|
|
1797
2306
|
console.log(` Poll: ${config.pollIntervalMs / 1e3}s`);
|
|
1798
2307
|
console.log(` Alert on: ${condLabels.join(", ")}`);
|
|
1799
|
-
console.log(
|
|
2308
|
+
console.log(
|
|
2309
|
+
` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`
|
|
2310
|
+
);
|
|
1800
2311
|
console.log(` Cooldown: ${Math.floor(config.cooldownMs / 6e4)}m`);
|
|
1801
2312
|
console.log(` State: ${config.stateFilePath}`);
|
|
1802
2313
|
console.log(` Hostname: ${(0, import_node_os.hostname)()}`);
|
|
@@ -1822,9 +2333,13 @@ agentflow watch started`);
|
|
|
1822
2333
|
if (pollCount % 10 === 0) {
|
|
1823
2334
|
const agentCount = Object.keys(state.agents).length;
|
|
1824
2335
|
const errorCount = Object.values(state.agents).filter((a) => a.lastStatus === "error").length;
|
|
1825
|
-
const runningCount = Object.values(state.agents).filter(
|
|
2336
|
+
const runningCount = Object.values(state.agents).filter(
|
|
2337
|
+
(a) => a.lastStatus === "running"
|
|
2338
|
+
).length;
|
|
1826
2339
|
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1827
|
-
console.log(
|
|
2340
|
+
console.log(
|
|
2341
|
+
`[${time}] heartbeat: ${agentCount} agents, ${runningCount} running, ${errorCount} errors, ${files.length} files`
|
|
2342
|
+
);
|
|
1828
2343
|
}
|
|
1829
2344
|
}
|
|
1830
2345
|
poll();
|
|
@@ -1841,7 +2356,9 @@ agentflow watch started`);
|
|
|
1841
2356
|
}
|
|
1842
2357
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1843
2358
|
0 && (module.exports = {
|
|
2359
|
+
checkGuards,
|
|
1844
2360
|
createGraphBuilder,
|
|
2361
|
+
createTraceStore,
|
|
1845
2362
|
findWaitingOn,
|
|
1846
2363
|
getChildren,
|
|
1847
2364
|
getCriticalPath,
|
|
@@ -1860,5 +2377,8 @@ agentflow watch started`);
|
|
|
1860
2377
|
runTraced,
|
|
1861
2378
|
startLive,
|
|
1862
2379
|
startWatch,
|
|
1863
|
-
stitchTrace
|
|
2380
|
+
stitchTrace,
|
|
2381
|
+
toAsciiTree,
|
|
2382
|
+
toTimeline,
|
|
2383
|
+
withGuards
|
|
1864
2384
|
});
|