agentflow-core 0.2.2 → 0.3.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/index.cjs CHANGED
@@ -38,6 +38,7 @@ __export(index_exports, {
38
38
  loadGraph: () => loadGraph,
39
39
  runTraced: () => runTraced,
40
40
  startLive: () => startLive,
41
+ startWatch: () => startWatch,
41
42
  stitchTrace: () => stitchTrace
42
43
  });
43
44
  module.exports = __toCommonJS(index_exports);
@@ -925,9 +926,19 @@ function processJsonlFile(file) {
925
926
  return [];
926
927
  }
927
928
  }
929
+ var K = "\x1B[K";
930
+ function writeLine(lines, text) {
931
+ lines.push(text + K);
932
+ }
933
+ function flushLines(lines) {
934
+ process.stdout.write("\x1B[H");
935
+ process.stdout.write(lines.join("\n") + "\n");
936
+ process.stdout.write("\x1B[J");
937
+ }
928
938
  var prevFileCount = 0;
929
939
  var newExecCount = 0;
930
940
  var sessionStart = Date.now();
941
+ var firstRender = true;
931
942
  function render(config) {
932
943
  const files = scanFiles(config.dirs, config.recursive);
933
944
  if (files.length > prevFileCount && prevFileCount > 0) {
@@ -943,26 +954,58 @@ function render(config) {
943
954
  if (r.traceData) allTraces.push(r.traceData);
944
955
  }
945
956
  }
946
- const agents = {};
957
+ const byFile = /* @__PURE__ */ new Map();
947
958
  for (const r of allRecords) {
948
- if (!agents[r.id]) {
949
- agents[r.id] = { name: r.id, total: 0, ok: 0, fail: 0, running: 0, lastTs: 0, source: r.source, detail: "" };
950
- }
951
- const ag = agents[r.id];
952
- ag.total++;
953
- if (r.status === "ok") ag.ok++;
954
- else if (r.status === "error") ag.fail++;
955
- else if (r.status === "running") ag.running++;
956
- if (r.lastActive > ag.lastTs) {
957
- ag.lastTs = r.lastActive;
958
- ag.detail = r.detail;
959
- ag.source = r.source;
959
+ const arr = byFile.get(r.file) ?? [];
960
+ arr.push(r);
961
+ byFile.set(r.file, arr);
962
+ }
963
+ const groups = [];
964
+ for (const [file, records] of byFile) {
965
+ if (records.length === 1) {
966
+ const r = records[0];
967
+ groups.push({
968
+ name: r.id,
969
+ source: r.source,
970
+ status: r.status,
971
+ lastTs: r.lastActive,
972
+ detail: r.detail,
973
+ children: [],
974
+ ok: r.status === "ok" ? 1 : 0,
975
+ fail: r.status === "error" ? 1 : 0,
976
+ running: r.status === "running" ? 1 : 0,
977
+ total: 1
978
+ });
979
+ } else {
980
+ const groupName = nameFromFile(file);
981
+ let lastTs = 0;
982
+ let ok = 0, fail = 0, running = 0;
983
+ for (const r of records) {
984
+ if (r.lastActive > lastTs) lastTs = r.lastActive;
985
+ if (r.status === "ok") ok++;
986
+ else if (r.status === "error") fail++;
987
+ else if (r.status === "running") running++;
988
+ }
989
+ const status = fail > 0 ? "error" : running > 0 ? "running" : ok > 0 ? "ok" : "unknown";
990
+ groups.push({
991
+ name: groupName,
992
+ source: records[0].source,
993
+ status,
994
+ lastTs,
995
+ detail: `${records.length} agents`,
996
+ children: records.sort((a, b) => b.lastActive - a.lastActive),
997
+ ok,
998
+ fail,
999
+ running,
1000
+ total: records.length
1001
+ });
960
1002
  }
961
1003
  }
962
- const agentList = Object.values(agents).sort((a, b) => b.lastTs - a.lastTs);
963
- const totExec = agentList.reduce((s, a) => s + a.total, 0);
964
- const totFail = agentList.reduce((s, a) => s + a.fail, 0);
965
- const totRunning = agentList.reduce((s, a) => s + a.running, 0);
1004
+ groups.sort((a, b) => b.lastTs - a.lastTs);
1005
+ const totExec = allRecords.length;
1006
+ const totFail = allRecords.filter((r) => r.status === "error").length;
1007
+ const totRunning = allRecords.filter((r) => r.status === "running").length;
1008
+ const uniqueAgents = new Set(allRecords.map((r) => r.id)).size;
966
1009
  const sysRate = totExec > 0 ? ((totExec - totFail) / totExec * 100).toFixed(1) : "100.0";
967
1010
  const now = Date.now();
968
1011
  const buckets = new Array(12).fill(0);
@@ -999,7 +1042,21 @@ function render(config) {
999
1042
  const upMin = Math.floor(upSec / 60);
1000
1043
  const upStr = upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`;
1001
1044
  const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1002
- const sourceTag = (s) => {
1045
+ function statusIcon(s, recent) {
1046
+ if (s === "error") return `${C.red}\u25CF${C.reset}`;
1047
+ if (s === "running") return `${C.green}\u25CF${C.reset}`;
1048
+ if (s === "ok" && recent) return `${C.green}\u25CF${C.reset}`;
1049
+ if (s === "ok") return `${C.dim}\u25CB${C.reset}`;
1050
+ return `${C.dim}\u25CB${C.reset}`;
1051
+ }
1052
+ function statusText(g) {
1053
+ if (g.fail > 0 && g.ok === 0 && g.running === 0) return `${C.red}error${C.reset}`;
1054
+ if (g.running > 0) return `${C.green}running${C.reset}`;
1055
+ if (g.fail > 0) return `${C.yellow}${g.ok}ok/${g.fail}err${C.reset}`;
1056
+ if (g.ok > 0) return g.total > 1 ? `${C.green}${g.ok}/${g.total} ok${C.reset}` : `${C.green}ok${C.reset}`;
1057
+ return `${C.dim}idle${C.reset}`;
1058
+ }
1059
+ function sourceTag(s) {
1003
1060
  switch (s) {
1004
1061
  case "trace":
1005
1062
  return `${C.cyan}trace${C.reset}`;
@@ -1012,93 +1069,115 @@ function render(config) {
1012
1069
  case "state":
1013
1070
  return `${C.dim}state${C.reset}`;
1014
1071
  }
1015
- };
1016
- process.stdout.write("\x1B[2J\x1B[H");
1017
- console.log(`${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
1018
- console.log(`${C.bold}${C.cyan}\u2551${C.reset} ${C.bold}${C.white}AGENTFLOW LIVE${C.reset} ${C.green}\u25CF LIVE${C.reset} ${C.dim}${time}${C.reset} ${C.bold}${C.cyan}\u2551${C.reset}`);
1072
+ }
1073
+ function timeStr(ts) {
1074
+ if (ts <= 0) return "n/a";
1075
+ return new Date(ts).toLocaleTimeString();
1076
+ }
1077
+ function truncate(s, max) {
1078
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
1079
+ }
1080
+ if (firstRender) {
1081
+ process.stdout.write("\x1B[2J");
1082
+ firstRender = false;
1083
+ }
1084
+ const L = [];
1085
+ writeLine(L, `${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
1086
+ writeLine(L, `${C.bold}${C.cyan}\u2551${C.reset} ${C.bold}${C.white}AGENTFLOW LIVE${C.reset} ${C.green}\u25CF LIVE${C.reset} ${C.dim}${time}${C.reset} ${C.bold}${C.cyan}\u2551${C.reset}`);
1019
1087
  const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
1020
1088
  const pad1 = Math.max(0, 64 - metaLine.length);
1021
- console.log(`${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`);
1022
- console.log(`${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
1089
+ writeLine(L, `${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`);
1090
+ writeLine(L, `${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
1023
1091
  const sc = totFail === 0 ? C.green : C.yellow;
1024
- console.log("");
1025
- console.log(` ${C.bold}Agents${C.reset} ${sc}${agentList.length}${C.reset} ${C.bold}Records${C.reset} ${sc}${totExec}${C.reset} ${C.bold}Success${C.reset} ${sc}${sysRate}%${C.reset} ${C.bold}Running${C.reset} ${C.green}${totRunning}${C.reset} ${C.bold}Errors${C.reset} ${totFail > 0 ? C.red : C.dim}${totFail}${C.reset} ${C.bold}New${C.reset} ${C.yellow}+${newExecCount}${C.reset}`);
1026
- console.log("");
1027
- console.log(` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
1028
- console.log("");
1029
- console.log(` ${C.bold}${C.under}Agent Type Status Last Active Detail${C.reset}`);
1030
- for (const ag of agentList.slice(0, 30)) {
1031
- const lastTime = ag.lastTs > 0 ? new Date(ag.lastTs).toLocaleTimeString() : "n/a";
1032
- const isRecent = Date.now() - ag.lastTs < 3e5;
1033
- let statusIcon;
1034
- let statusText;
1035
- if (ag.fail > 0 && ag.ok === 0 && ag.running === 0) {
1036
- statusIcon = `${C.red}\u25CF${C.reset}`;
1037
- statusText = `${C.red}error${C.reset}`;
1038
- } else if (ag.running > 0) {
1039
- statusIcon = `${C.green}\u25CF${C.reset}`;
1040
- statusText = `${C.green}running${C.reset}`;
1041
- } else if (ag.fail > 0) {
1042
- statusIcon = `${C.yellow}\u25CF${C.reset}`;
1043
- statusText = `${C.yellow}${ag.ok}ok/${ag.fail}err${C.reset}`;
1044
- } else if (ag.ok > 0) {
1045
- statusIcon = isRecent ? `${C.green}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
1046
- statusText = ag.total > 1 ? `${C.green}${ag.ok}/${ag.total}${C.reset}` : `${C.green}ok${C.reset}`;
1092
+ writeLine(L, "");
1093
+ writeLine(L, ` ${C.bold}Agents${C.reset} ${sc}${uniqueAgents}${C.reset} ${C.bold}Records${C.reset} ${sc}${totExec}${C.reset} ${C.bold}Success${C.reset} ${sc}${sysRate}%${C.reset} ${C.bold}Running${C.reset} ${C.green}${totRunning}${C.reset} ${C.bold}Errors${C.reset} ${totFail > 0 ? C.red : C.dim}${totFail}${C.reset} ${C.bold}New${C.reset} ${C.yellow}+${newExecCount}${C.reset}`);
1094
+ writeLine(L, "");
1095
+ writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
1096
+ writeLine(L, "");
1097
+ writeLine(L, ` ${C.bold}${C.under}Agent Status Last Active Detail${C.reset}`);
1098
+ let lineCount = 0;
1099
+ for (const g of groups) {
1100
+ if (lineCount > 35) break;
1101
+ const isRecent = Date.now() - g.lastTs < 3e5;
1102
+ const icon = statusIcon(g.status, isRecent);
1103
+ const active = isRecent ? `${C.green}${timeStr(g.lastTs)}${C.reset}` : `${C.dim}${timeStr(g.lastTs)}${C.reset}`;
1104
+ if (g.children.length === 0) {
1105
+ const name = truncate(g.name, 26).padEnd(26);
1106
+ const st = statusText(g);
1107
+ const det = truncate(g.detail, 30);
1108
+ writeLine(L, ` ${icon} ${name} ${st.padEnd(20)} ${active.padEnd(20)} ${C.dim}${det}${C.reset}`);
1109
+ lineCount++;
1047
1110
  } else {
1048
- statusIcon = `${C.dim}\u25CB${C.reset}`;
1049
- statusText = `${C.dim}idle${C.reset}`;
1111
+ const name = truncate(g.name, 24).padEnd(24);
1112
+ const st = statusText(g);
1113
+ const tag = sourceTag(g.source);
1114
+ writeLine(L, ` ${icon} ${C.bold}${name}${C.reset} ${st.padEnd(20)} ${active.padEnd(20)} ${tag} ${C.dim}(${g.children.length} agents)${C.reset}`);
1115
+ lineCount++;
1116
+ const kids = g.children.slice(0, 12);
1117
+ for (let i = 0; i < kids.length; i++) {
1118
+ if (lineCount > 35) break;
1119
+ const child = kids[i];
1120
+ const isLast = i === kids.length - 1;
1121
+ const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
1122
+ const cIcon = statusIcon(child.status, Date.now() - child.lastActive < 3e5);
1123
+ const cName = truncate(child.id, 22).padEnd(22);
1124
+ const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
1125
+ const cDet = truncate(child.detail, 25);
1126
+ writeLine(L, ` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`);
1127
+ lineCount++;
1128
+ }
1129
+ if (g.children.length > 12) {
1130
+ writeLine(L, ` ${C.dim} ... +${g.children.length - 12} more${C.reset}`);
1131
+ lineCount++;
1132
+ }
1050
1133
  }
1051
- const name = ag.name.length > 23 ? ag.name.slice(0, 22) + "\u2026" : ag.name.padEnd(23);
1052
- const src = sourceTag(ag.source).padEnd(16);
1053
- const active = isRecent ? `${C.green}${lastTime}${C.reset}` : `${C.dim}${lastTime}${C.reset}`;
1054
- const detail = ag.detail.length > 30 ? ag.detail.slice(0, 29) + "\u2026" : ag.detail;
1055
- console.log(` ${statusIcon} ${name} ${src} ${statusText.padEnd(18)} ${active.padEnd(20)} ${C.dim}${detail}${C.reset}`);
1056
1134
  }
1057
1135
  if (distributedTraces.length > 0) {
1058
- console.log("");
1059
- console.log(` ${C.bold}${C.under}Distributed Traces${C.reset}`);
1136
+ writeLine(L, "");
1137
+ writeLine(L, ` ${C.bold}${C.under}Distributed Traces${C.reset}`);
1060
1138
  for (const dt of distributedTraces.slice(0, 3)) {
1061
1139
  const traceTime = new Date(dt.startTime).toLocaleTimeString();
1062
- const statusIcon = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
1140
+ const si = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
1063
1141
  const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
1064
1142
  const tid = dt.traceId.slice(0, 8);
1065
- console.log(` ${statusIcon} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`);
1143
+ writeLine(L, ` ${si} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`);
1066
1144
  const tree = getTraceTree(dt);
1067
1145
  for (let i = 0; i < Math.min(tree.length, 6); i++) {
1068
- const g = tree[i];
1069
- const depth = getDistDepth(dt, g.spanId);
1146
+ const tg = tree[i];
1147
+ const depth = getDistDepth(dt, tg.spanId);
1070
1148
  const indent = " " + "\u2502 ".repeat(Math.max(0, depth - 1));
1071
1149
  const isLast = i === tree.length - 1 || getDistDepth(dt, tree[i + 1]?.spanId) <= depth;
1072
1150
  const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1073
- const gs = g.status === "completed" ? `${C.green}\u2713${C.reset}` : g.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
1074
- const gd = g.endTime ? `${g.endTime - g.startTime}ms` : "running";
1075
- console.log(`${indent}${conn}${gs} ${C.bold}${g.agentId}${C.reset} ${C.dim}[${g.trigger}] ${gd}${C.reset}`);
1151
+ const gs = tg.status === "completed" ? `${C.green}\u2713${C.reset}` : tg.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
1152
+ const gd = tg.endTime ? `${tg.endTime - tg.startTime}ms` : "running";
1153
+ writeLine(L, `${indent}${conn}${gs} ${C.bold}${tg.agentId}${C.reset} ${C.dim}[${tg.trigger}] ${gd}${C.reset}`);
1076
1154
  }
1077
1155
  }
1078
1156
  }
1079
- const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0, 8);
1157
+ const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0, 6);
1080
1158
  if (recentRecords.length > 0) {
1081
- console.log("");
1082
- console.log(` ${C.bold}${C.under}Recent Activity${C.reset}`);
1159
+ writeLine(L, "");
1160
+ writeLine(L, ` ${C.bold}${C.under}Recent Activity${C.reset}`);
1083
1161
  for (const r of recentRecords) {
1084
1162
  const icon = r.status === "ok" ? `${C.green}\u2713${C.reset}` : r.status === "error" ? `${C.red}\u2717${C.reset}` : r.status === "running" ? `${C.green}\u25B6${C.reset}` : `${C.dim}\u25CB${C.reset}`;
1085
1163
  const t = new Date(r.lastActive).toLocaleTimeString();
1086
- const agent = r.id.length > 26 ? r.id.slice(0, 25) + "\u2026" : r.id.padEnd(26);
1164
+ const agent = truncate(r.id, 26).padEnd(26);
1087
1165
  const age = Math.floor((Date.now() - r.lastActive) / 1e3);
1088
1166
  const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
1089
- const detail = r.detail.length > 25 ? r.detail.slice(0, 24) + "\u2026" : r.detail;
1090
- console.log(` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${detail}${C.reset}`);
1167
+ const det = truncate(r.detail, 25);
1168
+ writeLine(L, ` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${det}${C.reset}`);
1091
1169
  }
1092
1170
  }
1093
1171
  if (files.length === 0) {
1094
- console.log("");
1095
- console.log(` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
1096
- for (const d of config.dirs) console.log(` ${C.dim} ${d}${C.reset}`);
1172
+ writeLine(L, "");
1173
+ writeLine(L, ` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
1174
+ for (const d of config.dirs) writeLine(L, ` ${C.dim} ${d}${C.reset}`);
1097
1175
  }
1098
- console.log("");
1176
+ writeLine(L, "");
1099
1177
  const dirLabel = config.dirs.length === 1 ? config.dirs[0] : `${config.dirs.length} directories`;
1100
- console.log(` ${C.dim}Watching: ${dirLabel}${C.reset}`);
1101
- console.log(` ${C.dim}Press Ctrl+C to exit${C.reset}`);
1178
+ writeLine(L, ` ${C.dim}Watching: ${dirLabel}${C.reset}`);
1179
+ writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
1180
+ flushLines(L);
1102
1181
  }
1103
1182
  function getDistDepth(dt, spanId) {
1104
1183
  if (!spanId) return 0;
@@ -1136,6 +1215,522 @@ function startLive(argv) {
1136
1215
  process.exit(0);
1137
1216
  });
1138
1217
  }
1218
+
1219
+ // src/watch.ts
1220
+ var import_node_fs4 = require("fs");
1221
+ var import_node_path3 = require("path");
1222
+ var import_node_os = require("os");
1223
+
1224
+ // src/watch-state.ts
1225
+ var import_node_fs3 = require("fs");
1226
+ function parseDuration(input) {
1227
+ const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
1228
+ if (!match) {
1229
+ const n = parseInt(input, 10);
1230
+ return isNaN(n) ? 0 : n * 1e3;
1231
+ }
1232
+ const value = parseFloat(match[1]);
1233
+ switch (match[2].toLowerCase()) {
1234
+ case "s":
1235
+ return value * 1e3;
1236
+ case "m":
1237
+ return value * 6e4;
1238
+ case "h":
1239
+ return value * 36e5;
1240
+ case "d":
1241
+ return value * 864e5;
1242
+ default:
1243
+ return value * 1e3;
1244
+ }
1245
+ }
1246
+ function emptyState() {
1247
+ return { version: 1, agents: {}, lastPollTime: 0 };
1248
+ }
1249
+ function loadWatchState(filePath) {
1250
+ if (!(0, import_node_fs3.existsSync)(filePath)) return emptyState();
1251
+ try {
1252
+ const raw = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf8"));
1253
+ if (raw.version !== 1 || typeof raw.agents !== "object") return emptyState();
1254
+ return raw;
1255
+ } catch {
1256
+ return emptyState();
1257
+ }
1258
+ }
1259
+ function saveWatchState(filePath, state) {
1260
+ const tmp = filePath + ".tmp";
1261
+ try {
1262
+ (0, import_node_fs3.writeFileSync)(tmp, JSON.stringify(state, null, 2), "utf8");
1263
+ (0, import_node_fs3.renameSync)(tmp, filePath);
1264
+ } catch {
1265
+ try {
1266
+ (0, import_node_fs3.writeFileSync)(filePath, JSON.stringify(state, null, 2), "utf8");
1267
+ } catch {
1268
+ }
1269
+ }
1270
+ }
1271
+ function estimateInterval(history) {
1272
+ if (history.length < 3) return 0;
1273
+ const sorted = [...history].sort((a, b) => a - b);
1274
+ const deltas = [];
1275
+ for (let i = 1; i < sorted.length; i++) {
1276
+ const d = sorted[i] - sorted[i - 1];
1277
+ if (d > 0) deltas.push(d);
1278
+ }
1279
+ if (deltas.length === 0) return 0;
1280
+ deltas.sort((a, b) => a - b);
1281
+ return deltas[Math.floor(deltas.length / 2)];
1282
+ }
1283
+ function detectTransitions(previous, currentRecords, config, now) {
1284
+ const alerts = [];
1285
+ const hasError = config.alertConditions.some((c) => c.type === "error");
1286
+ const hasRecovery = config.alertConditions.some((c) => c.type === "recovery");
1287
+ const staleConditions = config.alertConditions.filter((c) => c.type === "stale");
1288
+ const consecutiveConditions = config.alertConditions.filter((c) => c.type === "consecutive-errors");
1289
+ const byAgent = /* @__PURE__ */ new Map();
1290
+ for (const r of currentRecords) {
1291
+ const existing = byAgent.get(r.id);
1292
+ if (!existing || r.lastActive > existing.lastActive) {
1293
+ byAgent.set(r.id, r);
1294
+ }
1295
+ }
1296
+ for (const [agentId, record] of byAgent) {
1297
+ const prev = previous.agents[agentId];
1298
+ const prevStatus = prev?.lastStatus ?? "unknown";
1299
+ const currStatus = record.status;
1300
+ if (hasError && currStatus === "error" && prevStatus !== "error") {
1301
+ if (canAlert(prev, "error", config.cooldownMs, now)) {
1302
+ alerts.push(makePayload(agentId, "error", prevStatus, currStatus, record, config.dirs));
1303
+ }
1304
+ }
1305
+ if (hasRecovery && currStatus === "ok" && prevStatus === "error") {
1306
+ alerts.push(makePayload(agentId, "recovery", prevStatus, currStatus, record, config.dirs));
1307
+ }
1308
+ const newConsec = currStatus === "error" ? (prev?.consecutiveErrors ?? 0) + 1 : 0;
1309
+ for (const cond of consecutiveConditions) {
1310
+ if (newConsec === cond.threshold) {
1311
+ if (canAlert(prev, `consecutive-errors:${cond.threshold}`, config.cooldownMs, now)) {
1312
+ alerts.push(makePayload(
1313
+ agentId,
1314
+ `consecutive-errors (${cond.threshold})`,
1315
+ prevStatus,
1316
+ currStatus,
1317
+ { ...record, detail: `${newConsec} consecutive errors. ${record.detail}` },
1318
+ config.dirs
1319
+ ));
1320
+ }
1321
+ }
1322
+ }
1323
+ for (const cond of staleConditions) {
1324
+ const sinceActive = now - record.lastActive;
1325
+ if (sinceActive > cond.durationMs && record.lastActive > 0) {
1326
+ if (canAlert(prev, "stale", config.cooldownMs, now)) {
1327
+ const mins = Math.floor(sinceActive / 6e4);
1328
+ alerts.push(makePayload(
1329
+ agentId,
1330
+ "stale",
1331
+ prevStatus,
1332
+ currStatus,
1333
+ { ...record, detail: `No update for ${mins}m. ${record.detail}` },
1334
+ config.dirs
1335
+ ));
1336
+ }
1337
+ }
1338
+ }
1339
+ if (staleConditions.length === 0) {
1340
+ const history = prev?.mtimeHistory ?? [];
1341
+ const expectedInterval = estimateInterval(history);
1342
+ if (expectedInterval > 0) {
1343
+ const sinceActive = now - record.lastActive;
1344
+ if (sinceActive > expectedInterval * 3) {
1345
+ if (canAlert(prev, "stale-auto", config.cooldownMs, now)) {
1346
+ const mins = Math.floor(sinceActive / 6e4);
1347
+ const expectedMins = Math.floor(expectedInterval / 6e4);
1348
+ alerts.push(makePayload(
1349
+ agentId,
1350
+ "stale (auto)",
1351
+ prevStatus,
1352
+ currStatus,
1353
+ { ...record, detail: `No update for ${mins}m (expected every ~${expectedMins}m). ${record.detail}` },
1354
+ config.dirs
1355
+ ));
1356
+ }
1357
+ }
1358
+ }
1359
+ }
1360
+ }
1361
+ return alerts;
1362
+ }
1363
+ function updateWatchState(state, records, alerts, now) {
1364
+ const agents = { ...state.agents };
1365
+ const alertsByAgent = /* @__PURE__ */ new Map();
1366
+ for (const a of alerts) alertsByAgent.set(a.agentId, a);
1367
+ const byAgent = /* @__PURE__ */ new Map();
1368
+ for (const r of records) {
1369
+ const existing = byAgent.get(r.id);
1370
+ if (!existing || r.lastActive > existing.lastActive) {
1371
+ byAgent.set(r.id, r);
1372
+ }
1373
+ }
1374
+ for (const [agentId, record] of byAgent) {
1375
+ const prev = agents[agentId];
1376
+ const history = prev?.mtimeHistory ?? [];
1377
+ const newHistory = [...history];
1378
+ if (newHistory.length === 0 || newHistory[newHistory.length - 1] !== record.lastActive) {
1379
+ newHistory.push(record.lastActive);
1380
+ }
1381
+ while (newHistory.length > 10) newHistory.shift();
1382
+ const alert = alertsByAgent.get(agentId);
1383
+ const consecutiveErrors = record.status === "error" ? (prev?.consecutiveErrors ?? 0) + 1 : 0;
1384
+ agents[agentId] = {
1385
+ id: agentId,
1386
+ lastStatus: record.status,
1387
+ lastActive: record.lastActive,
1388
+ lastAlertTime: alert ? now : prev?.lastAlertTime ?? 0,
1389
+ lastAlertReason: alert ? alert.condition : prev?.lastAlertReason ?? "",
1390
+ consecutiveErrors,
1391
+ mtimeHistory: newHistory
1392
+ };
1393
+ }
1394
+ return { version: 1, agents, lastPollTime: now };
1395
+ }
1396
+ function canAlert(prev, reason, cooldownMs, now) {
1397
+ if (!prev) return true;
1398
+ if (prev.lastAlertReason !== reason) return true;
1399
+ return now - prev.lastAlertTime > cooldownMs;
1400
+ }
1401
+ function makePayload(agentId, condition, previousStatus, currentStatus, record, dirs) {
1402
+ return {
1403
+ agentId,
1404
+ condition,
1405
+ previousStatus,
1406
+ currentStatus,
1407
+ detail: record.detail,
1408
+ file: record.file,
1409
+ timestamp: Date.now(),
1410
+ dirs
1411
+ };
1412
+ }
1413
+
1414
+ // src/watch-alerts.ts
1415
+ var import_node_https = require("https");
1416
+ var import_node_http = require("http");
1417
+ var import_node_child_process2 = require("child_process");
1418
+ function formatAlertMessage(payload) {
1419
+ const time = new Date(payload.timestamp).toISOString();
1420
+ const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
1421
+ return [
1422
+ `[ALERT] ${payload.condition}: "${payload.agentId}"`,
1423
+ ` Status: ${arrow}`,
1424
+ payload.detail ? ` Detail: ${payload.detail}` : null,
1425
+ ` File: ${payload.file}`,
1426
+ ` Time: ${time}`
1427
+ ].filter(Boolean).join("\n");
1428
+ }
1429
+ function formatTelegram(payload) {
1430
+ const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
1431
+ const time = new Date(payload.timestamp).toLocaleTimeString();
1432
+ return [
1433
+ `${icon} *AgentFlow Alert*`,
1434
+ `*${payload.condition}*: \`${payload.agentId}\``,
1435
+ `Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
1436
+ payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
1437
+ `Time: ${time}`
1438
+ ].filter(Boolean).join("\n");
1439
+ }
1440
+ async function sendAlert(payload, channel) {
1441
+ try {
1442
+ switch (channel.type) {
1443
+ case "stdout":
1444
+ sendStdout(payload);
1445
+ break;
1446
+ case "telegram":
1447
+ await sendTelegram(payload, channel.botToken, channel.chatId);
1448
+ break;
1449
+ case "webhook":
1450
+ await sendWebhook(payload, channel.url);
1451
+ break;
1452
+ case "command":
1453
+ await sendCommand(payload, channel.cmd);
1454
+ break;
1455
+ }
1456
+ } catch (err) {
1457
+ const msg = err instanceof Error ? err.message : String(err);
1458
+ console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
1459
+ }
1460
+ }
1461
+ function sendStdout(payload) {
1462
+ console.log(formatAlertMessage(payload));
1463
+ }
1464
+ function sendTelegram(payload, botToken, chatId) {
1465
+ const body = JSON.stringify({
1466
+ chat_id: chatId,
1467
+ text: formatTelegram(payload),
1468
+ parse_mode: "Markdown"
1469
+ });
1470
+ return new Promise((resolve4, reject) => {
1471
+ const req = (0, import_node_https.request)(
1472
+ `https://api.telegram.org/bot${botToken}/sendMessage`,
1473
+ { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
1474
+ (res) => {
1475
+ res.resume();
1476
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1477
+ else reject(new Error(`Telegram API returned ${res.statusCode}`));
1478
+ }
1479
+ );
1480
+ req.on("error", reject);
1481
+ req.write(body);
1482
+ req.end();
1483
+ });
1484
+ }
1485
+ function sendWebhook(payload, url) {
1486
+ const body = JSON.stringify(payload);
1487
+ const isHttps = url.startsWith("https");
1488
+ const doRequest = isHttps ? import_node_https.request : import_node_http.request;
1489
+ return new Promise((resolve4, reject) => {
1490
+ const req = doRequest(
1491
+ url,
1492
+ { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
1493
+ (res) => {
1494
+ res.resume();
1495
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
1496
+ else reject(new Error(`Webhook returned ${res.statusCode}`));
1497
+ }
1498
+ );
1499
+ req.on("error", reject);
1500
+ req.setTimeout(1e4, () => {
1501
+ req.destroy(new Error("Webhook timeout"));
1502
+ });
1503
+ req.write(body);
1504
+ req.end();
1505
+ });
1506
+ }
1507
+ function sendCommand(payload, cmd) {
1508
+ return new Promise((resolve4, reject) => {
1509
+ const env = {
1510
+ ...process.env,
1511
+ AGENTFLOW_ALERT_AGENT: payload.agentId,
1512
+ AGENTFLOW_ALERT_CONDITION: payload.condition,
1513
+ AGENTFLOW_ALERT_STATUS: payload.currentStatus,
1514
+ AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
1515
+ AGENTFLOW_ALERT_DETAIL: payload.detail,
1516
+ AGENTFLOW_ALERT_FILE: payload.file,
1517
+ AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
1518
+ };
1519
+ (0, import_node_child_process2.exec)(cmd, { env, timeout: 3e4 }, (err) => {
1520
+ if (err) reject(err);
1521
+ else resolve4();
1522
+ });
1523
+ });
1524
+ }
1525
+
1526
+ // src/watch.ts
1527
+ function parseWatchArgs(argv) {
1528
+ const dirs = [];
1529
+ const alertConditions = [];
1530
+ const notifyChannels = [];
1531
+ let recursive = false;
1532
+ let pollIntervalMs = 3e4;
1533
+ let cooldownMs = 30 * 6e4;
1534
+ let stateFilePath = "";
1535
+ const args = argv.slice(0);
1536
+ if (args[0] === "watch") args.shift();
1537
+ let i = 0;
1538
+ while (i < args.length) {
1539
+ const arg = args[i];
1540
+ if (arg === "--help" || arg === "-h") {
1541
+ printWatchUsage();
1542
+ process.exit(0);
1543
+ } else if (arg === "--alert-on") {
1544
+ i++;
1545
+ const val = args[i] ?? "";
1546
+ if (val === "error") {
1547
+ alertConditions.push({ type: "error" });
1548
+ } else if (val === "recovery") {
1549
+ alertConditions.push({ type: "recovery" });
1550
+ } else if (val.startsWith("stale:")) {
1551
+ const dur = parseDuration(val.slice(6));
1552
+ if (dur > 0) alertConditions.push({ type: "stale", durationMs: dur });
1553
+ } else if (val.startsWith("consecutive-errors:")) {
1554
+ const n = parseInt(val.slice(19), 10);
1555
+ if (n > 0) alertConditions.push({ type: "consecutive-errors", threshold: n });
1556
+ }
1557
+ i++;
1558
+ } else if (arg === "--notify") {
1559
+ i++;
1560
+ const val = args[i] ?? "";
1561
+ if (val === "telegram") {
1562
+ const botToken = process.env["AGENTFLOW_TELEGRAM_BOT_TOKEN"] ?? "";
1563
+ const chatId = process.env["AGENTFLOW_TELEGRAM_CHAT_ID"] ?? "";
1564
+ if (botToken && chatId) {
1565
+ notifyChannels.push({ type: "telegram", botToken, chatId });
1566
+ } else {
1567
+ console.error("Warning: --notify telegram requires AGENTFLOW_TELEGRAM_BOT_TOKEN and AGENTFLOW_TELEGRAM_CHAT_ID env vars");
1568
+ }
1569
+ } else if (val.startsWith("webhook:")) {
1570
+ notifyChannels.push({ type: "webhook", url: val.slice(8) });
1571
+ } else if (val.startsWith("command:")) {
1572
+ notifyChannels.push({ type: "command", cmd: val.slice(8) });
1573
+ }
1574
+ i++;
1575
+ } else if (arg === "--poll") {
1576
+ i++;
1577
+ const v = parseInt(args[i] ?? "", 10);
1578
+ if (!isNaN(v) && v > 0) pollIntervalMs = v * 1e3;
1579
+ i++;
1580
+ } else if (arg === "--cooldown") {
1581
+ i++;
1582
+ const dur = parseDuration(args[i] ?? "30m");
1583
+ if (dur > 0) cooldownMs = dur;
1584
+ i++;
1585
+ } else if (arg === "--state-file") {
1586
+ i++;
1587
+ stateFilePath = args[i] ?? "";
1588
+ i++;
1589
+ } else if (arg === "--recursive" || arg === "-R") {
1590
+ recursive = true;
1591
+ i++;
1592
+ } else if (!arg.startsWith("-")) {
1593
+ dirs.push((0, import_node_path3.resolve)(arg));
1594
+ i++;
1595
+ } else {
1596
+ i++;
1597
+ }
1598
+ }
1599
+ if (dirs.length === 0) dirs.push((0, import_node_path3.resolve)("."));
1600
+ if (alertConditions.length === 0) {
1601
+ alertConditions.push({ type: "error" });
1602
+ alertConditions.push({ type: "recovery" });
1603
+ }
1604
+ notifyChannels.unshift({ type: "stdout" });
1605
+ if (!stateFilePath) {
1606
+ stateFilePath = (0, import_node_path3.join)(dirs[0], ".agentflow-watch-state.json");
1607
+ }
1608
+ return {
1609
+ dirs,
1610
+ recursive,
1611
+ pollIntervalMs,
1612
+ alertConditions,
1613
+ notifyChannels,
1614
+ stateFilePath: (0, import_node_path3.resolve)(stateFilePath),
1615
+ cooldownMs
1616
+ };
1617
+ }
1618
+ function printWatchUsage() {
1619
+ console.log(`
1620
+ AgentFlow Watch \u2014 headless alert system for agent infrastructure.
1621
+
1622
+ Polls directories for JSON/JSONL files, detects failures and stale
1623
+ agents, sends alerts. Same auto-detection as \`agentflow live\`.
1624
+
1625
+ Usage:
1626
+ agentflow watch [dir...] [options]
1627
+
1628
+ Arguments:
1629
+ dir One or more directories to watch (default: .)
1630
+
1631
+ Alert conditions (--alert-on, repeatable):
1632
+ error Agent transitions to error status
1633
+ recovery Agent recovers from error to ok
1634
+ stale:DURATION No file update within duration (e.g. 15m, 1h)
1635
+ consecutive-errors:N N consecutive error observations
1636
+
1637
+ Default (if none specified): error + recovery
1638
+
1639
+ Notification channels (--notify, repeatable):
1640
+ telegram Telegram Bot API (needs env vars)
1641
+ webhook:URL POST JSON to any URL
1642
+ command:CMD Run shell command with alert env vars
1643
+
1644
+ Stdout alerts are always printed regardless of --notify flags.
1645
+
1646
+ Options:
1647
+ --poll <secs> Poll interval in seconds (default: 30)
1648
+ --cooldown <duration> Alert dedup cooldown (default: 30m)
1649
+ --state-file <path> Persistence file (default: <dir>/.agentflow-watch-state.json)
1650
+ -R, --recursive Scan subdirectories (1 level deep)
1651
+ -h, --help Show this help message
1652
+
1653
+ Environment variables:
1654
+ AGENTFLOW_TELEGRAM_BOT_TOKEN Telegram bot token (for --notify telegram)
1655
+ AGENTFLOW_TELEGRAM_CHAT_ID Telegram chat ID (for --notify telegram)
1656
+
1657
+ Examples:
1658
+ agentflow watch ./data --alert-on error --alert-on stale:15m
1659
+ agentflow watch ./data ./cron --notify telegram --poll 60
1660
+ agentflow watch ./traces --notify webhook:https://hooks.slack.com/... --alert-on consecutive-errors:3
1661
+ agentflow watch ./data --notify "command:curl -X POST https://my-pagerduty/alert"
1662
+ `.trim());
1663
+ }
1664
+ function startWatch(argv) {
1665
+ const config = parseWatchArgs(argv);
1666
+ const valid = config.dirs.filter((d) => (0, import_node_fs4.existsSync)(d));
1667
+ if (valid.length === 0) {
1668
+ console.error(`No valid directories found: ${config.dirs.join(", ")}`);
1669
+ process.exit(1);
1670
+ }
1671
+ const invalid = config.dirs.filter((d) => !(0, import_node_fs4.existsSync)(d));
1672
+ if (invalid.length > 0) {
1673
+ console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
1674
+ }
1675
+ let state = loadWatchState(config.stateFilePath);
1676
+ const condLabels = config.alertConditions.map((c) => {
1677
+ if (c.type === "stale") return `stale:${Math.floor(c.durationMs / 6e4)}m`;
1678
+ if (c.type === "consecutive-errors") return `consecutive-errors:${c.threshold}`;
1679
+ return c.type;
1680
+ });
1681
+ const channelLabels = config.notifyChannels.filter((c) => c.type !== "stdout").map((c) => {
1682
+ if (c.type === "webhook") return `webhook:${c.url.slice(0, 40)}...`;
1683
+ if (c.type === "command") return `command:${c.cmd.slice(0, 40)}`;
1684
+ return c.type;
1685
+ });
1686
+ console.log(`
1687
+ agentflow watch started`);
1688
+ console.log(` Directories: ${valid.join(", ")}`);
1689
+ console.log(` Poll: ${config.pollIntervalMs / 1e3}s`);
1690
+ console.log(` Alert on: ${condLabels.join(", ")}`);
1691
+ console.log(` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`);
1692
+ console.log(` Cooldown: ${Math.floor(config.cooldownMs / 6e4)}m`);
1693
+ console.log(` State: ${config.stateFilePath}`);
1694
+ console.log(` Hostname: ${(0, import_node_os.hostname)()}`);
1695
+ console.log("");
1696
+ let pollCount = 0;
1697
+ async function poll() {
1698
+ const now = Date.now();
1699
+ pollCount++;
1700
+ const files = scanFiles(valid, config.recursive);
1701
+ const records = [];
1702
+ for (const f of files.slice(0, 500)) {
1703
+ const recs = f.ext === ".jsonl" ? processJsonlFile(f) : processJsonFile(f);
1704
+ records.push(...recs);
1705
+ }
1706
+ const alerts = detectTransitions(state, records, config, now);
1707
+ for (const alert of alerts) {
1708
+ for (const channel of config.notifyChannels) {
1709
+ await sendAlert(alert, channel);
1710
+ }
1711
+ }
1712
+ state = updateWatchState(state, records, alerts, now);
1713
+ saveWatchState(config.stateFilePath, state);
1714
+ if (pollCount % 10 === 0) {
1715
+ const agentCount = Object.keys(state.agents).length;
1716
+ const errorCount = Object.values(state.agents).filter((a) => a.lastStatus === "error").length;
1717
+ const runningCount = Object.values(state.agents).filter((a) => a.lastStatus === "running").length;
1718
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1719
+ console.log(`[${time}] heartbeat: ${agentCount} agents, ${runningCount} running, ${errorCount} errors, ${files.length} files`);
1720
+ }
1721
+ }
1722
+ poll();
1723
+ setInterval(() => {
1724
+ poll();
1725
+ }, config.pollIntervalMs);
1726
+ function shutdown() {
1727
+ console.log("\nagentflow watch stopped.");
1728
+ saveWatchState(config.stateFilePath, state);
1729
+ process.exit(0);
1730
+ }
1731
+ process.on("SIGINT", shutdown);
1732
+ process.on("SIGTERM", shutdown);
1733
+ }
1139
1734
  // Annotate the CommonJS export names for ESM import in node:
1140
1735
  0 && (module.exports = {
1141
1736
  createGraphBuilder,
@@ -1156,5 +1751,6 @@ function startLive(argv) {
1156
1751
  loadGraph,
1157
1752
  runTraced,
1158
1753
  startLive,
1754
+ startWatch,
1159
1755
  stitchTrace
1160
1756
  });