agentflow-core 0.2.0 → 0.2.2

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.
@@ -632,7 +632,7 @@ function getTraceTree(trace) {
632
632
 
633
633
  // src/live.ts
634
634
  import { readdirSync as readdirSync2, readFileSync, statSync as statSync2, watch, existsSync as existsSync2 } from "fs";
635
- import { join as join2, resolve as resolve2 } from "path";
635
+ import { join as join2, resolve as resolve2, basename as basename2 } from "path";
636
636
  var C = {
637
637
  reset: "\x1B[0m",
638
638
  bold: "\x1B[1m",
@@ -647,10 +647,7 @@ var C = {
647
647
  white: "\x1B[37m"
648
648
  };
649
649
  function parseArgs(argv) {
650
- const config = {
651
- tracesDir: "./traces",
652
- refreshMs: 3e3
653
- };
650
+ const config = { dirs: [], refreshMs: 3e3, recursive: false };
654
651
  const args = argv.slice(0);
655
652
  if (args[0] === "live") args.shift();
656
653
  let i = 0;
@@ -661,138 +658,276 @@ function parseArgs(argv) {
661
658
  process.exit(0);
662
659
  } else if (arg === "--refresh" || arg === "-r") {
663
660
  i++;
664
- const val = parseInt(args[i] ?? "", 10);
665
- if (!isNaN(val) && val > 0) config.refreshMs = val * 1e3;
666
- i++;
667
- } else if (arg === "--traces-dir" || arg === "-t") {
661
+ const v = parseInt(args[i] ?? "", 10);
662
+ if (!isNaN(v) && v > 0) config.refreshMs = v * 1e3;
668
663
  i++;
669
- config.tracesDir = args[i] ?? config.tracesDir;
664
+ } else if (arg === "--recursive" || arg === "-R") {
665
+ config.recursive = true;
670
666
  i++;
671
667
  } else if (!arg.startsWith("-")) {
672
- config.tracesDir = arg;
668
+ config.dirs.push(resolve2(arg));
673
669
  i++;
674
670
  } else {
675
671
  i++;
676
672
  }
677
673
  }
678
- config.tracesDir = resolve2(config.tracesDir);
674
+ if (config.dirs.length === 0) config.dirs.push(resolve2("."));
679
675
  return config;
680
676
  }
681
677
  function printUsage() {
682
678
  console.log(`
683
679
  AgentFlow Live Monitor \u2014 real-time terminal dashboard for agent systems.
684
680
 
681
+ Auto-detects agent traces, state files, job schedulers, and session logs
682
+ from any JSON/JSONL files in the watched directories.
683
+
685
684
  Usage:
686
- agentflow live [traces-dir] [options]
685
+ agentflow live [dir...] [options]
687
686
 
688
687
  Arguments:
689
- traces-dir Path to the traces directory (default: ./traces)
688
+ dir One or more directories to watch (default: .)
690
689
 
691
690
  Options:
692
691
  -r, --refresh <secs> Refresh interval in seconds (default: 3)
693
- -t, --traces-dir <path> Explicit traces directory path
692
+ -R, --recursive Scan subdirectories (1 level deep)
694
693
  -h, --help Show this help message
695
694
 
696
695
  Examples:
697
- agentflow live
698
- agentflow live ./my-traces --refresh 5
699
- agentflow live /var/log/agentflow/traces -r 10
696
+ agentflow live ./data
697
+ agentflow live ./traces ./cron ./workers --refresh 5
698
+ agentflow live /var/lib/myagent -R
700
699
  `.trim());
701
700
  }
702
- function listTraceFiles(tracesDir) {
703
- try {
704
- return readdirSync2(tracesDir).filter((f) => f.endsWith(".json")).map((f) => {
705
- const fp = join2(tracesDir, f);
706
- const stat = statSync2(fp);
707
- return { filename: f, path: fp, mtime: stat.mtime.getTime() };
708
- }).sort((a, b) => b.mtime - a.mtime);
709
- } catch {
710
- return [];
701
+ function scanFiles(dirs, recursive) {
702
+ const results = [];
703
+ const seen = /* @__PURE__ */ new Set();
704
+ function scanDir(d, topLevel) {
705
+ try {
706
+ for (const f of readdirSync2(d)) {
707
+ if (f.startsWith(".")) continue;
708
+ const fp = join2(d, f);
709
+ if (seen.has(fp)) continue;
710
+ let stat;
711
+ try {
712
+ stat = statSync2(fp);
713
+ } catch {
714
+ continue;
715
+ }
716
+ if (stat.isDirectory() && recursive && topLevel) {
717
+ scanDir(fp, false);
718
+ continue;
719
+ }
720
+ if (!stat.isFile()) continue;
721
+ if (f.endsWith(".json")) {
722
+ seen.add(fp);
723
+ results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".json" });
724
+ } else if (f.endsWith(".jsonl")) {
725
+ seen.add(fp);
726
+ results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".jsonl" });
727
+ }
728
+ }
729
+ } catch {
730
+ }
711
731
  }
732
+ for (const dir of dirs) scanDir(dir, true);
733
+ results.sort((a, b) => b.mtime - a.mtime);
734
+ return results;
712
735
  }
713
- function safeLoadTrace(fp) {
736
+ function safeReadJson(fp) {
714
737
  try {
715
- return loadGraph(readFileSync(fp, "utf8"));
738
+ return JSON.parse(readFileSync(fp, "utf8"));
716
739
  } catch {
717
740
  return null;
718
741
  }
719
742
  }
720
- function analyze(trace) {
743
+ function nameFromFile(filename) {
744
+ return basename2(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
745
+ }
746
+ function normalizeStatus(val) {
747
+ if (typeof val !== "string") return "unknown";
748
+ const s = val.toLowerCase();
749
+ if (["ok", "success", "completed", "done", "passed", "healthy", "good"].includes(s)) return "ok";
750
+ if (["error", "failed", "failure", "crashed", "unhealthy", "bad", "timeout"].includes(s)) return "error";
751
+ if (["running", "active", "in_progress", "started", "pending", "processing"].includes(s)) return "running";
752
+ return "unknown";
753
+ }
754
+ function findStatus(obj) {
755
+ for (const key of ["status", "state", "lastRunStatus", "lastStatus", "health", "result"]) {
756
+ if (key in obj) {
757
+ const val = obj[key];
758
+ if (typeof val === "string") return normalizeStatus(val);
759
+ if (typeof val === "object" && val !== null && "status" in val) {
760
+ return normalizeStatus(val.status);
761
+ }
762
+ }
763
+ }
764
+ return "unknown";
765
+ }
766
+ function findTimestamp(obj) {
767
+ for (const key of ["ts", "timestamp", "lastRunAtMs", "last_run", "lastExecution", "updated_at", "started_at", "endTime", "startTime"]) {
768
+ const val = obj[key];
769
+ if (typeof val === "number") return val > 1e12 ? val : val * 1e3;
770
+ if (typeof val === "string") {
771
+ const d = Date.parse(val);
772
+ if (!isNaN(d)) return d;
773
+ }
774
+ }
775
+ return 0;
776
+ }
777
+ function extractDetail(obj) {
778
+ const parts = [];
779
+ for (const key of ["summary", "message", "description", "lastError", "error", "name", "jobId", "id"]) {
780
+ const val = obj[key];
781
+ if (typeof val === "string" && val.length > 0 && val.length < 200) {
782
+ parts.push(val.slice(0, 80));
783
+ break;
784
+ }
785
+ }
786
+ for (const key of ["totalExecutions", "runs", "count", "processed", "consecutiveErrors"]) {
787
+ const val = obj[key];
788
+ if (typeof val === "number") {
789
+ parts.push(`${key}: ${val}`);
790
+ break;
791
+ }
792
+ }
793
+ return parts.join(" | ") || "";
794
+ }
795
+ function tryLoadTrace(fp, raw) {
796
+ if (typeof raw !== "object" || raw === null) return null;
797
+ const obj = raw;
798
+ if (!("nodes" in obj)) return null;
799
+ if (!("agentId" in obj) && !("rootNodeId" in obj) && !("rootId" in obj)) return null;
721
800
  try {
722
- const stats = getStats(trace);
723
- const fails = getFailures(trace);
724
- const hung = getHungNodes(trace);
725
- return {
726
- agentId: trace.agentId,
727
- trigger: trace.trigger,
728
- traceId: trace.traceId,
729
- spanId: trace.spanId,
730
- parentSpanId: trace.parentSpanId,
731
- nodes: stats.totalNodes,
732
- success: fails.length === 0 && hung.length === 0,
733
- failures: fails.length,
734
- hung: hung.length
735
- };
801
+ return loadGraph(obj);
736
802
  } catch {
737
803
  return null;
738
804
  }
739
805
  }
740
- function getDistributedDepth(dt, spanId) {
741
- if (!spanId) return 0;
742
- const graph = dt.graphs.get(spanId);
743
- if (!graph || !graph.parentSpanId) return 0;
744
- return 1 + getDistributedDepth(dt, graph.parentSpanId);
806
+ function processJsonFile(file) {
807
+ const raw = safeReadJson(file.path);
808
+ if (raw === null) return [];
809
+ const records = [];
810
+ const trace = tryLoadTrace(file.path, raw);
811
+ if (trace) {
812
+ try {
813
+ const fails = getFailures(trace);
814
+ const hung = getHungNodes(trace);
815
+ const stats = getStats(trace);
816
+ records.push({
817
+ id: trace.agentId,
818
+ source: "trace",
819
+ status: fails.length === 0 && hung.length === 0 ? "ok" : "error",
820
+ lastActive: file.mtime,
821
+ detail: `${stats.totalNodes} nodes [${trace.trigger}]`,
822
+ file: file.filename,
823
+ traceData: trace
824
+ });
825
+ } catch {
826
+ }
827
+ return records;
828
+ }
829
+ if (typeof raw !== "object") return records;
830
+ const arr = Array.isArray(raw) ? raw : Array.isArray(raw.jobs) ? raw.jobs : Array.isArray(raw.tasks) ? raw.tasks : Array.isArray(raw.items) ? raw.items : null;
831
+ if (arr && arr.length > 0 && typeof arr[0] === "object" && arr[0] !== null) {
832
+ for (const item of arr.slice(0, 50)) {
833
+ const name = item.name ?? item.id ?? item.jobId ?? item.agentId;
834
+ if (!name) continue;
835
+ const state = typeof item.state === "object" && item.state !== null ? item.state : item;
836
+ const status2 = findStatus(state);
837
+ const ts2 = findTimestamp(state) || file.mtime;
838
+ const detail2 = extractDetail(state);
839
+ records.push({ id: String(name), source: "jobs", status: status2, lastActive: ts2, detail: detail2, file: file.filename });
840
+ }
841
+ return records;
842
+ }
843
+ const obj = raw;
844
+ for (const containerKey of ["tools", "workers", "services", "agents", "daemons"]) {
845
+ const container = obj[containerKey];
846
+ if (typeof container === "object" && container !== null && !Array.isArray(container)) {
847
+ for (const [name, info] of Object.entries(container)) {
848
+ if (typeof info !== "object" || info === null) continue;
849
+ const w = info;
850
+ const status2 = findStatus(w);
851
+ const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
852
+ const pid = w.pid;
853
+ const detail2 = pid ? `pid: ${pid}` : extractDetail(w);
854
+ records.push({ id: name, source: "workers", status: status2, lastActive: ts2, detail: detail2, file: file.filename });
855
+ }
856
+ return records;
857
+ }
858
+ }
859
+ const status = findStatus(obj);
860
+ const ts = findTimestamp(obj) || file.mtime;
861
+ const detail = extractDetail(obj);
862
+ records.push({ id: nameFromFile(file.filename), source: "state", status, lastActive: ts, detail, file: file.filename });
863
+ return records;
864
+ }
865
+ function processJsonlFile(file) {
866
+ try {
867
+ const content = readFileSync(file.path, "utf8").trim();
868
+ if (!content) return [];
869
+ const lines = content.split("\n");
870
+ const lastLine = lines[lines.length - 1];
871
+ const obj = JSON.parse(lastLine);
872
+ const name = obj.jobId ?? obj.agentId ?? obj.name ?? obj.id ?? nameFromFile(file.filename);
873
+ const status = findStatus(obj);
874
+ const ts = findTimestamp(obj) || file.mtime;
875
+ const action = obj.action;
876
+ const detail = action ? `${action} (${lines.length} entries)` : `${lines.length} entries`;
877
+ return [{ id: String(name), source: "session", status, lastActive: ts, detail, file: file.filename }];
878
+ } catch {
879
+ return [];
880
+ }
745
881
  }
746
882
  var prevFileCount = 0;
747
883
  var newExecCount = 0;
748
884
  var sessionStart = Date.now();
749
885
  function render(config) {
750
- const files = listTraceFiles(config.tracesDir);
886
+ const files = scanFiles(config.dirs, config.recursive);
751
887
  if (files.length > prevFileCount && prevFileCount > 0) {
752
888
  newExecCount += files.length - prevFileCount;
753
889
  }
754
890
  prevFileCount = files.length;
891
+ const allRecords = [];
755
892
  const allTraces = [];
893
+ for (const f of files.slice(0, 300)) {
894
+ const records = f.ext === ".jsonl" ? processJsonlFile(f) : processJsonFile(f);
895
+ for (const r of records) {
896
+ allRecords.push(r);
897
+ if (r.traceData) allTraces.push(r.traceData);
898
+ }
899
+ }
756
900
  const agents = {};
757
- for (const f of files.slice(0, 200)) {
758
- const trace = safeLoadTrace(f.path);
759
- if (!trace) continue;
760
- allTraces.push(trace);
761
- const a = analyze(trace);
762
- if (!a) continue;
763
- if (!agents[a.agentId]) {
764
- agents[a.agentId] = { name: a.agentId, total: 0, ok: 0, fail: 0, lastTs: 0 };
765
- }
766
- const ag = agents[a.agentId];
901
+ for (const r of allRecords) {
902
+ if (!agents[r.id]) {
903
+ agents[r.id] = { name: r.id, total: 0, ok: 0, fail: 0, running: 0, lastTs: 0, source: r.source, detail: "" };
904
+ }
905
+ const ag = agents[r.id];
767
906
  ag.total++;
768
- a.success ? ag.ok++ : ag.fail++;
769
- if (f.mtime > ag.lastTs) ag.lastTs = f.mtime;
907
+ if (r.status === "ok") ag.ok++;
908
+ else if (r.status === "error") ag.fail++;
909
+ else if (r.status === "running") ag.running++;
910
+ if (r.lastActive > ag.lastTs) {
911
+ ag.lastTs = r.lastActive;
912
+ ag.detail = r.detail;
913
+ ag.source = r.source;
914
+ }
770
915
  }
771
- const agentList = Object.values(agents).sort((a, b) => b.total - a.total);
916
+ const agentList = Object.values(agents).sort((a, b) => b.lastTs - a.lastTs);
772
917
  const totExec = agentList.reduce((s, a) => s + a.total, 0);
773
918
  const totFail = agentList.reduce((s, a) => s + a.fail, 0);
919
+ const totRunning = agentList.reduce((s, a) => s + a.running, 0);
774
920
  const sysRate = totExec > 0 ? ((totExec - totFail) / totExec * 100).toFixed(1) : "100.0";
775
- const recent = [];
776
- for (const f of files.slice(0, 15)) {
777
- const trace = safeLoadTrace(f.path);
778
- if (!trace) continue;
779
- const a = analyze(trace);
780
- if (a) recent.push({ ...a, ts: f.mtime });
781
- }
782
921
  const now = Date.now();
783
922
  const buckets = new Array(12).fill(0);
784
923
  const failBuckets = new Array(12).fill(0);
785
- for (const f of files) {
786
- const age = now - f.mtime;
787
- if (age > 36e5) continue;
924
+ for (const r of allRecords) {
925
+ const age = now - r.lastActive;
926
+ if (age > 36e5 || age < 0) continue;
788
927
  const idx = 11 - Math.floor(age / 3e5);
789
928
  if (idx >= 0 && idx < 12) {
790
- const trace = safeLoadTrace(f.path);
791
- if (!trace) continue;
792
- const a = analyze(trace);
793
- if (!a) continue;
794
929
  buckets[idx]++;
795
- if (!a.success) failBuckets[idx]++;
930
+ if (r.status === "error") failBuckets[idx]++;
796
931
  }
797
932
  }
798
933
  const maxBucket = Math.max(...buckets, 1);
@@ -801,107 +936,153 @@ function render(config) {
801
936
  const level = Math.round(v / maxBucket * 8);
802
937
  return (failBuckets[i] > 0 ? C.red : C.green) + sparkChars[level] + C.reset;
803
938
  }).join("");
804
- const traceGroups = groupByTraceId(allTraces);
805
939
  const distributedTraces = [];
806
- for (const [_traceId, graphs] of traceGroups) {
807
- if (graphs.length > 1) {
808
- try {
809
- distributedTraces.push(stitchTrace(graphs));
810
- } catch {
940
+ if (allTraces.length > 1) {
941
+ const traceGroups = groupByTraceId(allTraces);
942
+ for (const [_tid, graphs] of traceGroups) {
943
+ if (graphs.length > 1) {
944
+ try {
945
+ distributedTraces.push(stitchTrace(graphs));
946
+ } catch {
947
+ }
811
948
  }
812
949
  }
950
+ distributedTraces.sort((a, b) => b.startTime - a.startTime);
813
951
  }
814
- distributedTraces.sort((a, b) => b.startTime - a.startTime);
815
952
  const upSec = Math.floor((Date.now() - sessionStart) / 1e3);
816
953
  const upMin = Math.floor(upSec / 60);
817
954
  const upStr = upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`;
818
955
  const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
956
+ const sourceTag = (s) => {
957
+ switch (s) {
958
+ case "trace":
959
+ return `${C.cyan}trace${C.reset}`;
960
+ case "jobs":
961
+ return `${C.blue}job${C.reset}`;
962
+ case "workers":
963
+ return `${C.magenta}worker${C.reset}`;
964
+ case "session":
965
+ return `${C.yellow}session${C.reset}`;
966
+ case "state":
967
+ return `${C.dim}state${C.reset}`;
968
+ }
969
+ };
819
970
  process.stdout.write("\x1B[2J\x1B[H");
820
- console.log(`${C.bold}${C.cyan}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${C.reset}`);
971
+ console.log(`${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
821
972
  console.log(`${C.bold}${C.cyan}\u2551${C.reset} ${C.bold}${C.white}AGENTFLOW LIVE${C.reset} ${C.green}\u25CF LIVE${C.reset} ${C.dim}${time}${C.reset} ${C.bold}${C.cyan}\u2551${C.reset}`);
822
- const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr}`;
973
+ const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
823
974
  const pad1 = Math.max(0, 64 - metaLine.length);
824
975
  console.log(`${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`);
825
- console.log(`${C.bold}${C.cyan}\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${C.reset}`);
976
+ console.log(`${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
826
977
  const sc = totFail === 0 ? C.green : C.yellow;
827
978
  console.log("");
828
- console.log(` ${C.bold}Agents${C.reset} ${sc}${agentList.length}${C.reset} ${C.bold}Executions${C.reset} ${sc}${totExec}${C.reset} ${C.bold}Success${C.reset} ${sc}${sysRate}%${C.reset} ${C.bold}Traces${C.reset} ${sc}${files.length}${C.reset} ${C.bold}New${C.reset} ${C.yellow}+${newExecCount}${C.reset} ${C.bold}Distributed${C.reset} ${C.magenta}${distributedTraces.length}${C.reset}`);
979
+ console.log(` ${C.bold}Agents${C.reset} ${sc}${agentList.length}${C.reset} ${C.bold}Records${C.reset} ${sc}${totExec}${C.reset} ${C.bold}Success${C.reset} ${sc}${sysRate}%${C.reset} ${C.bold}Running${C.reset} ${C.green}${totRunning}${C.reset} ${C.bold}Errors${C.reset} ${totFail > 0 ? C.red : C.dim}${totFail}${C.reset} ${C.bold}New${C.reset} ${C.yellow}+${newExecCount}${C.reset}`);
829
980
  console.log("");
830
981
  console.log(` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
831
982
  console.log("");
832
- console.log(` ${C.bold}${C.under}Agent Runs OK Fail Rate Last Active${C.reset}`);
833
- for (const ag of agentList) {
834
- const rate = (ag.ok / ag.total * 100).toFixed(0);
835
- const lastTime = new Date(ag.lastTs).toLocaleTimeString();
983
+ console.log(` ${C.bold}${C.under}Agent Type Status Last Active Detail${C.reset}`);
984
+ for (const ag of agentList.slice(0, 30)) {
985
+ const lastTime = ag.lastTs > 0 ? new Date(ag.lastTs).toLocaleTimeString() : "n/a";
836
986
  const isRecent = Date.now() - ag.lastTs < 3e5;
837
- let status;
838
- if (ag.fail > 0) status = `${C.red}\u25CF${C.reset}`;
839
- else if (isRecent) status = `${C.green}\u25CF${C.reset}`;
840
- else status = `${C.dim}\u25CB${C.reset}`;
841
- const name = ag.name.padEnd(28);
842
- const runs = String(ag.total).padStart(5);
843
- const ok = String(ag.ok).padStart(5);
844
- const fail = ag.fail > 0 ? `${C.red}${String(ag.fail).padStart(4)}${C.reset}` : String(ag.fail).padStart(4);
845
- const rateStr = (rate + "%").padStart(5);
846
- const activeStr = isRecent ? `${C.green}${lastTime}${C.reset}` : `${C.dim}${lastTime}${C.reset}`;
847
- console.log(` ${status} ${name}${runs}${ok}${fail} ${rateStr} ${activeStr}`);
987
+ let statusIcon;
988
+ let statusText;
989
+ if (ag.fail > 0 && ag.ok === 0 && ag.running === 0) {
990
+ statusIcon = `${C.red}\u25CF${C.reset}`;
991
+ statusText = `${C.red}error${C.reset}`;
992
+ } else if (ag.running > 0) {
993
+ statusIcon = `${C.green}\u25CF${C.reset}`;
994
+ statusText = `${C.green}running${C.reset}`;
995
+ } else if (ag.fail > 0) {
996
+ statusIcon = `${C.yellow}\u25CF${C.reset}`;
997
+ statusText = `${C.yellow}${ag.ok}ok/${ag.fail}err${C.reset}`;
998
+ } else if (ag.ok > 0) {
999
+ statusIcon = isRecent ? `${C.green}\u25CF${C.reset}` : `${C.dim}\u25CB${C.reset}`;
1000
+ statusText = ag.total > 1 ? `${C.green}${ag.ok}/${ag.total}${C.reset}` : `${C.green}ok${C.reset}`;
1001
+ } else {
1002
+ statusIcon = `${C.dim}\u25CB${C.reset}`;
1003
+ statusText = `${C.dim}idle${C.reset}`;
1004
+ }
1005
+ const name = ag.name.length > 23 ? ag.name.slice(0, 22) + "\u2026" : ag.name.padEnd(23);
1006
+ const src = sourceTag(ag.source).padEnd(16);
1007
+ const active = isRecent ? `${C.green}${lastTime}${C.reset}` : `${C.dim}${lastTime}${C.reset}`;
1008
+ const detail = ag.detail.length > 30 ? ag.detail.slice(0, 29) + "\u2026" : ag.detail;
1009
+ console.log(` ${statusIcon} ${name} ${src} ${statusText.padEnd(18)} ${active.padEnd(20)} ${C.dim}${detail}${C.reset}`);
848
1010
  }
849
1011
  if (distributedTraces.length > 0) {
850
1012
  console.log("");
851
- console.log(` ${C.bold}${C.under}Distributed Traces (multi-agent workflows)${C.reset}`);
852
- for (const dt of distributedTraces.slice(0, 5)) {
1013
+ console.log(` ${C.bold}${C.under}Distributed Traces${C.reset}`);
1014
+ for (const dt of distributedTraces.slice(0, 3)) {
853
1015
  const traceTime = new Date(dt.startTime).toLocaleTimeString();
854
1016
  const statusIcon = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
855
1017
  const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
856
1018
  const tid = dt.traceId.slice(0, 8);
857
- console.log(` ${statusIcon} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime}${C.reset} ${C.dim}${dur}${C.reset} ${C.dim}(${dt.graphs.size} agents)${C.reset}`);
1019
+ console.log(` ${statusIcon} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`);
858
1020
  const tree = getTraceTree(dt);
859
- for (let i = 0; i < tree.length; i++) {
1021
+ for (let i = 0; i < Math.min(tree.length, 6); i++) {
860
1022
  const g = tree[i];
861
- const depth = getDistributedDepth(dt, g.spanId);
1023
+ const depth = getDistDepth(dt, g.spanId);
862
1024
  const indent = " " + "\u2502 ".repeat(Math.max(0, depth - 1));
863
- const isLast = i === tree.length - 1 || getDistributedDepth(dt, tree[i + 1]?.spanId) <= depth;
864
- const connector = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
865
- const gStatus = g.status === "completed" ? `${C.green}\u2713${C.reset}` : g.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
866
- const gDur = g.endTime ? `${g.endTime - g.startTime}ms` : "running";
867
- console.log(`${indent}${connector}${gStatus} ${C.bold}${g.agentId}${C.reset} ${C.dim}[${g.trigger}] ${gDur}${C.reset}`);
1025
+ const isLast = i === tree.length - 1 || getDistDepth(dt, tree[i + 1]?.spanId) <= depth;
1026
+ const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
1027
+ const gs = g.status === "completed" ? `${C.green}\u2713${C.reset}` : g.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
1028
+ const gd = g.endTime ? `${g.endTime - g.startTime}ms` : "running";
1029
+ console.log(`${indent}${conn}${gs} ${C.bold}${g.agentId}${C.reset} ${C.dim}[${g.trigger}] ${gd}${C.reset}`);
868
1030
  }
869
1031
  }
870
1032
  }
871
- console.log("");
872
- console.log(` ${C.bold}${C.under}Recent Executions${C.reset}`);
873
- for (const ex of recent.slice(0, 8)) {
874
- const icon = ex.success ? `${C.green}\u2713${C.reset}` : `${C.red}\u2717${C.reset}`;
875
- const t = new Date(ex.ts).toLocaleTimeString();
876
- const agent = ex.agentId.padEnd(28);
877
- const age = Math.floor((Date.now() - ex.ts) / 1e3);
878
- const ageStr = age < 60 ? age + "s ago" : Math.floor(age / 60) + "m ago";
879
- const traceTag = ex.traceId ? ` ${C.magenta}\u29EB${C.reset}` : "";
880
- console.log(` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)} ${ex.nodes} nodes${C.reset}${traceTag}`);
1033
+ const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0, 8);
1034
+ if (recentRecords.length > 0) {
1035
+ console.log("");
1036
+ console.log(` ${C.bold}${C.under}Recent Activity${C.reset}`);
1037
+ for (const r of recentRecords) {
1038
+ const icon = r.status === "ok" ? `${C.green}\u2713${C.reset}` : r.status === "error" ? `${C.red}\u2717${C.reset}` : r.status === "running" ? `${C.green}\u25B6${C.reset}` : `${C.dim}\u25CB${C.reset}`;
1039
+ const t = new Date(r.lastActive).toLocaleTimeString();
1040
+ const agent = r.id.length > 26 ? r.id.slice(0, 25) + "\u2026" : r.id.padEnd(26);
1041
+ const age = Math.floor((Date.now() - r.lastActive) / 1e3);
1042
+ const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
1043
+ const detail = r.detail.length > 25 ? r.detail.slice(0, 24) + "\u2026" : r.detail;
1044
+ console.log(` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${detail}${C.reset}`);
1045
+ }
881
1046
  }
882
1047
  if (files.length === 0) {
883
- console.log(` ${C.dim}No trace files found. Waiting for traces in:${C.reset}`);
884
- console.log(` ${C.dim}${config.tracesDir}${C.reset}`);
1048
+ console.log("");
1049
+ console.log(` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
1050
+ for (const d of config.dirs) console.log(` ${C.dim} ${d}${C.reset}`);
885
1051
  }
886
1052
  console.log("");
887
- console.log(` ${C.dim}Watching: ${config.tracesDir}${C.reset}`);
1053
+ const dirLabel = config.dirs.length === 1 ? config.dirs[0] : `${config.dirs.length} directories`;
1054
+ console.log(` ${C.dim}Watching: ${dirLabel}${C.reset}`);
888
1055
  console.log(` ${C.dim}Press Ctrl+C to exit${C.reset}`);
889
1056
  }
1057
+ function getDistDepth(dt, spanId) {
1058
+ if (!spanId) return 0;
1059
+ const g = dt.graphs.get(spanId);
1060
+ if (!g || !g.parentSpanId) return 0;
1061
+ return 1 + getDistDepth(dt, g.parentSpanId);
1062
+ }
890
1063
  function startLive(argv) {
891
1064
  const config = parseArgs(argv);
892
- if (!existsSync2(config.tracesDir)) {
893
- console.error(`Traces directory does not exist: ${config.tracesDir}`);
894
- console.error("Create it or specify a different path: agentflow live <traces-dir>");
1065
+ const valid = config.dirs.filter((d) => existsSync2(d));
1066
+ if (valid.length === 0) {
1067
+ console.error(`No valid directories found: ${config.dirs.join(", ")}`);
1068
+ console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
895
1069
  process.exit(1);
896
1070
  }
1071
+ const invalid = config.dirs.filter((d) => !existsSync2(d));
1072
+ if (invalid.length > 0) {
1073
+ console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
1074
+ }
1075
+ config.dirs = valid;
897
1076
  render(config);
898
1077
  let debounce = null;
899
- try {
900
- watch(config.tracesDir, () => {
901
- if (debounce) clearTimeout(debounce);
902
- debounce = setTimeout(() => render(config), 500);
903
- });
904
- } catch {
1078
+ for (const dir of config.dirs) {
1079
+ try {
1080
+ watch(dir, { recursive: config.recursive }, () => {
1081
+ if (debounce) clearTimeout(debounce);
1082
+ debounce = setTimeout(() => render(config), 500);
1083
+ });
1084
+ } catch {
1085
+ }
905
1086
  }
906
1087
  setInterval(() => render(config), config.refreshMs);
907
1088
  process.on("SIGINT", () => {