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
|
@@ -1,1289 +1,1685 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import {
|
|
2
|
+
graphToJson,
|
|
3
|
+
loadGraph
|
|
4
|
+
} from "./chunk-DY7YHFIB.js";
|
|
5
|
+
|
|
6
|
+
// src/graph-query.ts
|
|
7
|
+
function getNode(graph, nodeId) {
|
|
8
|
+
return graph.nodes.get(nodeId);
|
|
9
|
+
}
|
|
10
|
+
function getChildren(graph, nodeId) {
|
|
11
|
+
const node = graph.nodes.get(nodeId);
|
|
12
|
+
if (!node) return [];
|
|
13
|
+
const result = [];
|
|
14
|
+
for (const childId of node.children) {
|
|
15
|
+
const child = graph.nodes.get(childId);
|
|
16
|
+
if (child) result.push(child);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function getParent(graph, nodeId) {
|
|
21
|
+
const node = graph.nodes.get(nodeId);
|
|
22
|
+
if (!node || node.parentId === null) return void 0;
|
|
23
|
+
return graph.nodes.get(node.parentId);
|
|
24
|
+
}
|
|
25
|
+
function getFailures(graph) {
|
|
26
|
+
const failureStatuses = /* @__PURE__ */ new Set(["failed", "hung", "timeout"]);
|
|
27
|
+
return [...graph.nodes.values()].filter((node) => failureStatuses.has(node.status));
|
|
28
|
+
}
|
|
29
|
+
function getHungNodes(graph) {
|
|
30
|
+
return [...graph.nodes.values()].filter(
|
|
31
|
+
(node) => node.status === "running" && node.endTime === null
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function getCriticalPath(graph) {
|
|
35
|
+
const root = graph.nodes.get(graph.rootNodeId);
|
|
36
|
+
if (!root) return [];
|
|
37
|
+
function nodeDuration2(node) {
|
|
38
|
+
const end = node.endTime ?? Date.now();
|
|
39
|
+
return end - node.startTime;
|
|
40
|
+
}
|
|
41
|
+
function dfs(node) {
|
|
42
|
+
if (node.children.length === 0) {
|
|
43
|
+
return { duration: nodeDuration2(node), path: [node] };
|
|
9
44
|
}
|
|
10
|
-
|
|
45
|
+
let bestChild = { duration: -1, path: [] };
|
|
46
|
+
for (const childId of node.children) {
|
|
47
|
+
const child = graph.nodes.get(childId);
|
|
48
|
+
if (!child) continue;
|
|
49
|
+
const result = dfs(child);
|
|
50
|
+
if (result.duration > bestChild.duration) {
|
|
51
|
+
bestChild = result;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
duration: nodeDuration2(node) + bestChild.duration,
|
|
56
|
+
path: [node, ...bestChild.path]
|
|
57
|
+
};
|
|
11
58
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
59
|
+
return dfs(root).path;
|
|
60
|
+
}
|
|
61
|
+
function findWaitingOn(graph, nodeId) {
|
|
62
|
+
const results = [];
|
|
63
|
+
for (const edge of graph.edges) {
|
|
64
|
+
if (edge.from === nodeId && edge.type === "waited_on") {
|
|
65
|
+
const node = graph.nodes.get(edge.to);
|
|
66
|
+
if (node) results.push(node);
|
|
18
67
|
}
|
|
19
68
|
}
|
|
20
|
-
return
|
|
69
|
+
return results;
|
|
21
70
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
71
|
+
function getSubtree(graph, nodeId) {
|
|
72
|
+
const startNode = graph.nodes.get(nodeId);
|
|
73
|
+
if (!startNode) return [];
|
|
74
|
+
const result = [];
|
|
75
|
+
const queue = [...startNode.children];
|
|
76
|
+
while (queue.length > 0) {
|
|
77
|
+
const currentId = queue.shift();
|
|
78
|
+
if (currentId === void 0) break;
|
|
79
|
+
const current = graph.nodes.get(currentId);
|
|
80
|
+
if (!current) continue;
|
|
81
|
+
result.push(current);
|
|
82
|
+
queue.push(...current.children);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
28
85
|
}
|
|
29
|
-
function
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
function assertNotBuilt() {
|
|
45
|
-
if (built) {
|
|
46
|
-
throw new Error("GraphBuilder: cannot mutate after build() has been called");
|
|
86
|
+
function getDuration(graph) {
|
|
87
|
+
const end = graph.endTime ?? Date.now();
|
|
88
|
+
return end - graph.startTime;
|
|
89
|
+
}
|
|
90
|
+
function getDepth(graph) {
|
|
91
|
+
const root = graph.nodes.get(graph.rootNodeId);
|
|
92
|
+
if (!root) return -1;
|
|
93
|
+
function dfs(node, depth) {
|
|
94
|
+
if (node.children.length === 0) return depth;
|
|
95
|
+
let maxDepth = depth;
|
|
96
|
+
for (const childId of node.children) {
|
|
97
|
+
const child = graph.nodes.get(childId);
|
|
98
|
+
if (!child) continue;
|
|
99
|
+
const childDepth = dfs(child, depth + 1);
|
|
100
|
+
if (childDepth > maxDepth) maxDepth = childDepth;
|
|
47
101
|
}
|
|
102
|
+
return maxDepth;
|
|
48
103
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
104
|
+
return dfs(root, 0);
|
|
105
|
+
}
|
|
106
|
+
function getStats(graph) {
|
|
107
|
+
const byStatus = {
|
|
108
|
+
running: 0,
|
|
109
|
+
completed: 0,
|
|
110
|
+
failed: 0,
|
|
111
|
+
hung: 0,
|
|
112
|
+
timeout: 0
|
|
113
|
+
};
|
|
114
|
+
const byType = {
|
|
115
|
+
agent: 0,
|
|
116
|
+
tool: 0,
|
|
117
|
+
subagent: 0,
|
|
118
|
+
wait: 0,
|
|
119
|
+
decision: 0,
|
|
120
|
+
custom: 0
|
|
121
|
+
};
|
|
122
|
+
let failureCount = 0;
|
|
123
|
+
let hungCount = 0;
|
|
124
|
+
for (const node of graph.nodes.values()) {
|
|
125
|
+
byStatus[node.status]++;
|
|
126
|
+
byType[node.type]++;
|
|
127
|
+
if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
|
|
128
|
+
failureCount++;
|
|
129
|
+
}
|
|
130
|
+
if (node.status === "running" && node.endTime === null) {
|
|
131
|
+
hungCount++;
|
|
53
132
|
}
|
|
54
|
-
return node;
|
|
55
133
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
134
|
+
return {
|
|
135
|
+
totalNodes: graph.nodes.size,
|
|
136
|
+
byStatus,
|
|
137
|
+
byType,
|
|
138
|
+
depth: getDepth(graph),
|
|
139
|
+
duration: getDuration(graph),
|
|
140
|
+
failureCount,
|
|
141
|
+
hungCount
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/graph-stitch.ts
|
|
146
|
+
function groupByTraceId(graphs) {
|
|
147
|
+
const groups = /* @__PURE__ */ new Map();
|
|
148
|
+
for (const g of graphs) {
|
|
149
|
+
if (!g.traceId) continue;
|
|
150
|
+
const arr = groups.get(g.traceId) ?? [];
|
|
151
|
+
arr.push(g);
|
|
152
|
+
groups.set(g.traceId, arr);
|
|
63
153
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
154
|
+
return groups;
|
|
155
|
+
}
|
|
156
|
+
function stitchTrace(graphs) {
|
|
157
|
+
if (graphs.length === 0) throw new Error("No graphs to stitch");
|
|
158
|
+
const traceId = graphs[0].traceId ?? "";
|
|
159
|
+
const graphsBySpan = /* @__PURE__ */ new Map();
|
|
160
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
161
|
+
let rootGraph = null;
|
|
162
|
+
for (const g of graphs) {
|
|
163
|
+
if (g.spanId) graphsBySpan.set(g.spanId, g);
|
|
164
|
+
if (!g.parentSpanId) {
|
|
165
|
+
if (!rootGraph || g.startTime < rootGraph.startTime) rootGraph = g;
|
|
67
166
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
if (node.status === "running") {
|
|
75
|
-
graphStatus = "running";
|
|
76
|
-
}
|
|
167
|
+
if (g.parentSpanId) {
|
|
168
|
+
const siblings = childMap.get(g.parentSpanId) ?? [];
|
|
169
|
+
if (g.spanId) siblings.push(g.spanId);
|
|
170
|
+
childMap.set(g.parentSpanId, siblings);
|
|
77
171
|
}
|
|
78
|
-
const endTime = graphStatus === "running" ? null : Date.now();
|
|
79
|
-
const frozenNodes = new Map(
|
|
80
|
-
[...nodes.entries()].map(([id, mNode]) => [
|
|
81
|
-
id,
|
|
82
|
-
{
|
|
83
|
-
id: mNode.id,
|
|
84
|
-
type: mNode.type,
|
|
85
|
-
name: mNode.name,
|
|
86
|
-
startTime: mNode.startTime,
|
|
87
|
-
endTime: mNode.endTime,
|
|
88
|
-
status: mNode.status,
|
|
89
|
-
parentId: mNode.parentId,
|
|
90
|
-
children: [...mNode.children],
|
|
91
|
-
metadata: { ...mNode.metadata },
|
|
92
|
-
state: { ...mNode.state }
|
|
93
|
-
}
|
|
94
|
-
])
|
|
95
|
-
);
|
|
96
|
-
const graph = {
|
|
97
|
-
id: graphId,
|
|
98
|
-
rootNodeId,
|
|
99
|
-
nodes: frozenNodes,
|
|
100
|
-
edges: [...edges],
|
|
101
|
-
startTime,
|
|
102
|
-
endTime,
|
|
103
|
-
status: graphStatus,
|
|
104
|
-
trigger,
|
|
105
|
-
agentId,
|
|
106
|
-
events: [...events],
|
|
107
|
-
traceId,
|
|
108
|
-
spanId,
|
|
109
|
-
parentSpanId
|
|
110
|
-
};
|
|
111
|
-
return deepFreeze(graph);
|
|
112
172
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const parentId = opts.parentId ?? parentStack[parentStack.length - 1] ?? null;
|
|
124
|
-
if (parentId !== null && !nodes.has(parentId)) {
|
|
125
|
-
throw new Error(`GraphBuilder: parent node "${parentId}" does not exist`);
|
|
126
|
-
}
|
|
127
|
-
const node = {
|
|
128
|
-
id,
|
|
129
|
-
type: opts.type,
|
|
130
|
-
name: opts.name,
|
|
131
|
-
startTime: Date.now(),
|
|
132
|
-
endTime: null,
|
|
133
|
-
status: "running",
|
|
134
|
-
parentId,
|
|
135
|
-
children: [],
|
|
136
|
-
metadata: opts.metadata ? { ...opts.metadata } : {},
|
|
137
|
-
state: {}
|
|
138
|
-
};
|
|
139
|
-
nodes.set(id, node);
|
|
140
|
-
if (parentId !== null) {
|
|
141
|
-
const parent = nodes.get(parentId);
|
|
142
|
-
if (parent) {
|
|
143
|
-
parent.children.push(id);
|
|
144
|
-
}
|
|
145
|
-
edges.push({ from: parentId, to: id, type: "spawned" });
|
|
146
|
-
}
|
|
147
|
-
if (rootNodeId === null) {
|
|
148
|
-
rootNodeId = id;
|
|
149
|
-
}
|
|
150
|
-
recordEvent(id, "agent_start", { type: opts.type, name: opts.name });
|
|
151
|
-
return id;
|
|
152
|
-
},
|
|
153
|
-
endNode(nodeId, status = "completed") {
|
|
154
|
-
assertNotBuilt();
|
|
155
|
-
const node = getNode2(nodeId);
|
|
156
|
-
if (node.endTime !== null) {
|
|
157
|
-
throw new Error(
|
|
158
|
-
`GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
node.endTime = Date.now();
|
|
162
|
-
node.status = status;
|
|
163
|
-
recordEvent(nodeId, "agent_end", { status });
|
|
164
|
-
},
|
|
165
|
-
failNode(nodeId, error) {
|
|
166
|
-
assertNotBuilt();
|
|
167
|
-
const node = getNode2(nodeId);
|
|
168
|
-
if (node.endTime !== null) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
`GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
const errorMessage = error instanceof Error ? error.message : error;
|
|
174
|
-
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
175
|
-
node.endTime = Date.now();
|
|
176
|
-
node.status = "failed";
|
|
177
|
-
node.metadata.error = errorMessage;
|
|
178
|
-
if (errorStack) {
|
|
179
|
-
node.metadata.errorStack = errorStack;
|
|
180
|
-
}
|
|
181
|
-
recordEvent(nodeId, "tool_error", { error: errorMessage });
|
|
182
|
-
},
|
|
183
|
-
addEdge(from, to, type) {
|
|
184
|
-
assertNotBuilt();
|
|
185
|
-
getNode2(from);
|
|
186
|
-
getNode2(to);
|
|
187
|
-
edges.push({ from, to, type });
|
|
188
|
-
recordEvent(from, "custom", { to, type, action: "edge_add" });
|
|
189
|
-
},
|
|
190
|
-
pushEvent(event) {
|
|
191
|
-
assertNotBuilt();
|
|
192
|
-
getNode2(event.nodeId);
|
|
193
|
-
events.push({
|
|
194
|
-
...event,
|
|
195
|
-
timestamp: Date.now()
|
|
196
|
-
});
|
|
197
|
-
},
|
|
198
|
-
updateState(nodeId, state) {
|
|
199
|
-
assertNotBuilt();
|
|
200
|
-
const node = getNode2(nodeId);
|
|
201
|
-
Object.assign(node.state, state);
|
|
202
|
-
recordEvent(nodeId, "custom", { action: "state_update", ...state });
|
|
203
|
-
},
|
|
204
|
-
withParent(parentId, fn) {
|
|
205
|
-
assertNotBuilt();
|
|
206
|
-
getNode2(parentId);
|
|
207
|
-
parentStack.push(parentId);
|
|
208
|
-
try {
|
|
209
|
-
return fn();
|
|
210
|
-
} finally {
|
|
211
|
-
parentStack.pop();
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
getSnapshot() {
|
|
215
|
-
return buildGraph();
|
|
216
|
-
},
|
|
217
|
-
build() {
|
|
218
|
-
assertNotBuilt();
|
|
219
|
-
const graph = buildGraph();
|
|
220
|
-
built = true;
|
|
221
|
-
return graph;
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
return builder;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// src/loader.ts
|
|
228
|
-
function toNodesMap(raw) {
|
|
229
|
-
if (raw instanceof Map) return raw;
|
|
230
|
-
if (Array.isArray(raw)) {
|
|
231
|
-
return new Map(raw);
|
|
232
|
-
}
|
|
233
|
-
if (raw !== null && typeof raw === "object") {
|
|
234
|
-
return new Map(Object.entries(raw));
|
|
173
|
+
if (!rootGraph) rootGraph = graphs[0];
|
|
174
|
+
let status = "completed";
|
|
175
|
+
let endTime = 0;
|
|
176
|
+
let startTime = Infinity;
|
|
177
|
+
for (const g of graphs) {
|
|
178
|
+
startTime = Math.min(startTime, g.startTime);
|
|
179
|
+
if (g.status === "failed") status = "failed";
|
|
180
|
+
else if (g.status === "running" && status !== "failed") status = "running";
|
|
181
|
+
if (g.endTime === null) endTime = null;
|
|
182
|
+
else if (endTime !== null) endTime = Math.max(endTime, g.endTime);
|
|
235
183
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
endTime: raw.endTime ?? null,
|
|
248
|
-
status: raw.status ?? "completed",
|
|
249
|
-
trigger: raw.trigger ?? "unknown",
|
|
250
|
-
agentId: raw.agentId ?? "unknown",
|
|
251
|
-
events: raw.events ?? [],
|
|
252
|
-
traceId: raw.traceId,
|
|
253
|
-
spanId: raw.spanId,
|
|
254
|
-
parentSpanId: raw.parentSpanId
|
|
255
|
-
};
|
|
184
|
+
const frozenChildMap = /* @__PURE__ */ new Map();
|
|
185
|
+
for (const [k, v] of childMap) frozenChildMap.set(k, Object.freeze([...v]));
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
traceId,
|
|
188
|
+
graphs: graphsBySpan,
|
|
189
|
+
rootGraph,
|
|
190
|
+
childMap: frozenChildMap,
|
|
191
|
+
startTime,
|
|
192
|
+
endTime,
|
|
193
|
+
status
|
|
194
|
+
});
|
|
256
195
|
}
|
|
257
|
-
function
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
196
|
+
function getTraceTree(trace) {
|
|
197
|
+
const result = [];
|
|
198
|
+
function walk(spanId) {
|
|
199
|
+
const graph = trace.graphs.get(spanId);
|
|
200
|
+
if (graph) result.push(graph);
|
|
201
|
+
const children = trace.childMap.get(spanId) ?? [];
|
|
202
|
+
for (const childSpan of children) walk(childSpan);
|
|
261
203
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
nodes: nodesObj,
|
|
266
|
-
edges: graph.edges,
|
|
267
|
-
startTime: graph.startTime,
|
|
268
|
-
endTime: graph.endTime,
|
|
269
|
-
status: graph.status,
|
|
270
|
-
trigger: graph.trigger,
|
|
271
|
-
agentId: graph.agentId,
|
|
272
|
-
events: graph.events,
|
|
273
|
-
traceId: graph.traceId,
|
|
274
|
-
spanId: graph.spanId,
|
|
275
|
-
parentSpanId: graph.parentSpanId
|
|
276
|
-
};
|
|
204
|
+
if (trace.rootGraph.spanId) walk(trace.rootGraph.spanId);
|
|
205
|
+
else result.push(trace.rootGraph);
|
|
206
|
+
return result;
|
|
277
207
|
}
|
|
278
208
|
|
|
279
|
-
// src/
|
|
280
|
-
import {
|
|
281
|
-
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
209
|
+
// src/live.ts
|
|
210
|
+
import { existsSync, readdirSync, readFileSync, statSync, watch } from "fs";
|
|
282
211
|
import { basename, join, resolve } from "path";
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
212
|
+
var C = {
|
|
213
|
+
reset: "\x1B[0m",
|
|
214
|
+
bold: "\x1B[1m",
|
|
215
|
+
dim: "\x1B[90m",
|
|
216
|
+
under: "\x1B[4m",
|
|
217
|
+
red: "\x1B[31m",
|
|
218
|
+
green: "\x1B[32m",
|
|
219
|
+
yellow: "\x1B[33m",
|
|
220
|
+
blue: "\x1B[34m",
|
|
221
|
+
magenta: "\x1B[35m",
|
|
222
|
+
cyan: "\x1B[36m",
|
|
223
|
+
white: "\x1B[37m"
|
|
224
|
+
};
|
|
225
|
+
function parseArgs(argv) {
|
|
226
|
+
const config = { dirs: [], refreshMs: 3e3, recursive: false };
|
|
227
|
+
const args = argv.slice(0);
|
|
228
|
+
if (args[0] === "live") args.shift();
|
|
229
|
+
let i = 0;
|
|
230
|
+
while (i < args.length) {
|
|
231
|
+
const arg = args[i];
|
|
232
|
+
if (arg === "--help" || arg === "-h") {
|
|
233
|
+
printUsage();
|
|
234
|
+
process.exit(0);
|
|
235
|
+
} else if (arg === "--refresh" || arg === "-r") {
|
|
236
|
+
i++;
|
|
237
|
+
const v = parseInt(args[i] ?? "", 10);
|
|
238
|
+
if (!isNaN(v) && v > 0) config.refreshMs = v * 1e3;
|
|
239
|
+
i++;
|
|
240
|
+
} else if (arg === "--recursive" || arg === "-R") {
|
|
241
|
+
config.recursive = true;
|
|
242
|
+
i++;
|
|
243
|
+
} else if (!arg.startsWith("-")) {
|
|
244
|
+
config.dirs.push(resolve(arg));
|
|
245
|
+
i++;
|
|
246
|
+
} else {
|
|
247
|
+
i++;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (config.dirs.length === 0) config.dirs.push(resolve("."));
|
|
251
|
+
return config;
|
|
286
252
|
}
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
253
|
+
function printUsage() {
|
|
254
|
+
console.log(
|
|
255
|
+
`
|
|
256
|
+
AgentFlow Live Monitor \u2014 real-time terminal dashboard for agent systems.
|
|
257
|
+
|
|
258
|
+
Auto-detects agent traces, state files, job schedulers, and session logs
|
|
259
|
+
from any JSON/JSONL files in the watched directories.
|
|
260
|
+
|
|
261
|
+
Usage:
|
|
262
|
+
agentflow live [dir...] [options]
|
|
263
|
+
|
|
264
|
+
Arguments:
|
|
265
|
+
dir One or more directories to watch (default: .)
|
|
266
|
+
|
|
267
|
+
Options:
|
|
268
|
+
-r, --refresh <secs> Refresh interval in seconds (default: 3)
|
|
269
|
+
-R, --recursive Scan subdirectories (1 level deep)
|
|
270
|
+
-h, --help Show this help message
|
|
271
|
+
|
|
272
|
+
Examples:
|
|
273
|
+
agentflow live ./data
|
|
274
|
+
agentflow live ./traces ./cron ./workers --refresh 5
|
|
275
|
+
agentflow live /var/lib/myagent -R
|
|
276
|
+
`.trim()
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
function scanFiles(dirs, recursive) {
|
|
280
|
+
const results = [];
|
|
281
|
+
const seen = /* @__PURE__ */ new Set();
|
|
282
|
+
function scanDir(d, topLevel) {
|
|
293
283
|
try {
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
284
|
+
for (const f of readdirSync(d)) {
|
|
285
|
+
if (f.startsWith(".")) continue;
|
|
286
|
+
const fp = join(d, f);
|
|
287
|
+
if (seen.has(fp)) continue;
|
|
288
|
+
let stat;
|
|
289
|
+
try {
|
|
290
|
+
stat = statSync(fp);
|
|
291
|
+
} catch {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (stat.isDirectory() && recursive && topLevel) {
|
|
295
|
+
scanDir(fp, false);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!stat.isFile()) continue;
|
|
299
|
+
if (f.endsWith(".json")) {
|
|
300
|
+
seen.add(fp);
|
|
301
|
+
results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".json" });
|
|
302
|
+
} else if (f.endsWith(".jsonl")) {
|
|
303
|
+
seen.add(fp);
|
|
304
|
+
results.push({ filename: f, path: fp, mtime: stat.mtime.getTime(), ext: ".jsonl" });
|
|
305
|
+
}
|
|
297
306
|
}
|
|
298
307
|
} catch {
|
|
299
308
|
}
|
|
300
309
|
}
|
|
301
|
-
|
|
310
|
+
for (const dir of dirs) scanDir(dir, true);
|
|
311
|
+
results.sort((a, b) => b.mtime - a.mtime);
|
|
312
|
+
return results;
|
|
302
313
|
}
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
314
|
+
function safeReadJson(fp) {
|
|
315
|
+
try {
|
|
316
|
+
return JSON.parse(readFileSync(fp, "utf8"));
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
307
320
|
}
|
|
308
|
-
function
|
|
309
|
-
return "
|
|
321
|
+
function nameFromFile(filename) {
|
|
322
|
+
return basename(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
|
|
310
323
|
}
|
|
311
|
-
function
|
|
312
|
-
|
|
324
|
+
function normalizeStatus(val) {
|
|
325
|
+
if (typeof val !== "string") return "unknown";
|
|
326
|
+
const s = val.toLowerCase();
|
|
327
|
+
if (["ok", "success", "completed", "done", "passed", "healthy", "good"].includes(s)) return "ok";
|
|
328
|
+
if (["error", "failed", "failure", "crashed", "unhealthy", "bad", "timeout"].includes(s))
|
|
329
|
+
return "error";
|
|
330
|
+
if (["running", "active", "in_progress", "started", "pending", "processing"].includes(s))
|
|
331
|
+
return "running";
|
|
332
|
+
return "unknown";
|
|
313
333
|
}
|
|
314
|
-
|
|
315
|
-
const {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
watchPatterns = ["*.json"]
|
|
322
|
-
} = config;
|
|
323
|
-
if (command.length === 0) {
|
|
324
|
-
throw new Error("runTraced: command must not be empty");
|
|
325
|
-
}
|
|
326
|
-
const resolvedTracesDir = resolve(tracesDir);
|
|
327
|
-
const patterns = watchPatterns.map(globToRegex);
|
|
328
|
-
const orchestrator = createGraphBuilder({ agentId, trigger });
|
|
329
|
-
const { traceId, spanId } = orchestrator.traceContext;
|
|
330
|
-
const beforeSnapshots = /* @__PURE__ */ new Map();
|
|
331
|
-
for (const dir of watchDirs) {
|
|
332
|
-
beforeSnapshots.set(dir, snapshotDir(dir, patterns));
|
|
333
|
-
}
|
|
334
|
-
const rootId = orchestrator.startNode({ type: "agent", name: agentId });
|
|
335
|
-
const dispatchId = orchestrator.startNode({
|
|
336
|
-
type: "tool",
|
|
337
|
-
name: "dispatch-command",
|
|
338
|
-
parentId: rootId
|
|
339
|
-
});
|
|
340
|
-
orchestrator.updateState(dispatchId, { command: command.join(" ") });
|
|
341
|
-
const monitorId = orchestrator.startNode({
|
|
342
|
-
type: "tool",
|
|
343
|
-
name: "state-monitor",
|
|
344
|
-
parentId: rootId
|
|
345
|
-
});
|
|
346
|
-
orchestrator.updateState(monitorId, {
|
|
347
|
-
watchDirs,
|
|
348
|
-
watchPatterns
|
|
349
|
-
});
|
|
350
|
-
const startMs = Date.now();
|
|
351
|
-
const execCmd = command[0] ?? "";
|
|
352
|
-
const execArgs = command.slice(1);
|
|
353
|
-
process.env.AGENTFLOW_TRACE_ID = traceId;
|
|
354
|
-
process.env.AGENTFLOW_PARENT_SPAN_ID = spanId;
|
|
355
|
-
const result = spawnSync(execCmd, execArgs, { stdio: "inherit" });
|
|
356
|
-
delete process.env.AGENTFLOW_TRACE_ID;
|
|
357
|
-
delete process.env.AGENTFLOW_PARENT_SPAN_ID;
|
|
358
|
-
const exitCode = result.status ?? 1;
|
|
359
|
-
const duration = (Date.now() - startMs) / 1e3;
|
|
360
|
-
const stateChanges = [];
|
|
361
|
-
for (const dir of watchDirs) {
|
|
362
|
-
const before = beforeSnapshots.get(dir) ?? /* @__PURE__ */ new Map();
|
|
363
|
-
const after = snapshotDir(dir, patterns);
|
|
364
|
-
for (const [filePath, mtime] of after) {
|
|
365
|
-
const prevMtime = before.get(filePath);
|
|
366
|
-
if (prevMtime === void 0 || mtime > prevMtime) {
|
|
367
|
-
stateChanges.push(filePath);
|
|
334
|
+
function findStatus(obj) {
|
|
335
|
+
for (const key of ["status", "state", "lastRunStatus", "lastStatus", "health", "result"]) {
|
|
336
|
+
if (key in obj) {
|
|
337
|
+
const val = obj[key];
|
|
338
|
+
if (typeof val === "string") return normalizeStatus(val);
|
|
339
|
+
if (typeof val === "object" && val !== null && "status" in val) {
|
|
340
|
+
return normalizeStatus(val.status);
|
|
368
341
|
}
|
|
369
342
|
}
|
|
370
343
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const childAgentId = agentIdFromFilename(filePath);
|
|
392
|
-
const childBuilder = createGraphBuilder({
|
|
393
|
-
agentId: childAgentId,
|
|
394
|
-
trigger: "state-change",
|
|
395
|
-
traceId,
|
|
396
|
-
parentSpanId: spanId
|
|
397
|
-
});
|
|
398
|
-
const childRootId = childBuilder.startNode({
|
|
399
|
-
type: "agent",
|
|
400
|
-
name: childAgentId
|
|
401
|
-
});
|
|
402
|
-
childBuilder.updateState(childRootId, {
|
|
403
|
-
stateFile: filePath,
|
|
404
|
-
detectedBy: "runner-state-monitor"
|
|
405
|
-
});
|
|
406
|
-
childBuilder.endNode(childRootId);
|
|
407
|
-
allGraphs.push(childBuilder.build());
|
|
344
|
+
return "unknown";
|
|
345
|
+
}
|
|
346
|
+
function findTimestamp(obj) {
|
|
347
|
+
for (const key of [
|
|
348
|
+
"ts",
|
|
349
|
+
"timestamp",
|
|
350
|
+
"lastRunAtMs",
|
|
351
|
+
"last_run",
|
|
352
|
+
"lastExecution",
|
|
353
|
+
"updated_at",
|
|
354
|
+
"started_at",
|
|
355
|
+
"endTime",
|
|
356
|
+
"startTime"
|
|
357
|
+
]) {
|
|
358
|
+
const val = obj[key];
|
|
359
|
+
if (typeof val === "number") return val > 1e12 ? val : val * 1e3;
|
|
360
|
+
if (typeof val === "string") {
|
|
361
|
+
const d = Date.parse(val);
|
|
362
|
+
if (!isNaN(d)) return d;
|
|
363
|
+
}
|
|
408
364
|
}
|
|
409
|
-
|
|
410
|
-
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
function extractDetail(obj) {
|
|
368
|
+
const parts = [];
|
|
369
|
+
for (const key of [
|
|
370
|
+
"summary",
|
|
371
|
+
"message",
|
|
372
|
+
"description",
|
|
373
|
+
"lastError",
|
|
374
|
+
"error",
|
|
375
|
+
"name",
|
|
376
|
+
"jobId",
|
|
377
|
+
"id"
|
|
378
|
+
]) {
|
|
379
|
+
const val = obj[key];
|
|
380
|
+
if (typeof val === "string" && val.length > 0 && val.length < 200) {
|
|
381
|
+
parts.push(val.slice(0, 80));
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
411
384
|
}
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
tracePaths.push(outPath);
|
|
385
|
+
for (const key of ["totalExecutions", "runs", "count", "processed", "consecutiveErrors"]) {
|
|
386
|
+
const val = obj[key];
|
|
387
|
+
if (typeof val === "number") {
|
|
388
|
+
parts.push(`${key}: ${val}`);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
419
391
|
}
|
|
420
|
-
return
|
|
421
|
-
exitCode,
|
|
422
|
-
traceId,
|
|
423
|
-
spanId,
|
|
424
|
-
tracePaths,
|
|
425
|
-
stateChanges,
|
|
426
|
-
duration
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// src/graph-query.ts
|
|
431
|
-
function getNode(graph, nodeId) {
|
|
432
|
-
return graph.nodes.get(nodeId);
|
|
392
|
+
return parts.join(" | ") || "";
|
|
433
393
|
}
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
394
|
+
function tryLoadTrace(fp, raw) {
|
|
395
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
396
|
+
const obj = raw;
|
|
397
|
+
if (!("nodes" in obj)) return null;
|
|
398
|
+
if (!("agentId" in obj) && !("rootNodeId" in obj) && !("rootId" in obj)) return null;
|
|
399
|
+
try {
|
|
400
|
+
return loadGraph(obj);
|
|
401
|
+
} catch {
|
|
402
|
+
return null;
|
|
441
403
|
}
|
|
442
|
-
return result;
|
|
443
|
-
}
|
|
444
|
-
function getParent(graph, nodeId) {
|
|
445
|
-
const node = graph.nodes.get(nodeId);
|
|
446
|
-
if (!node || node.parentId === null) return void 0;
|
|
447
|
-
return graph.nodes.get(node.parentId);
|
|
448
|
-
}
|
|
449
|
-
function getFailures(graph) {
|
|
450
|
-
const failureStatuses = /* @__PURE__ */ new Set(["failed", "hung", "timeout"]);
|
|
451
|
-
return [...graph.nodes.values()].filter((node) => failureStatuses.has(node.status));
|
|
452
|
-
}
|
|
453
|
-
function getHungNodes(graph) {
|
|
454
|
-
return [...graph.nodes.values()].filter(
|
|
455
|
-
(node) => node.status === "running" && node.endTime === null
|
|
456
|
-
);
|
|
457
404
|
}
|
|
458
|
-
function
|
|
459
|
-
const
|
|
460
|
-
if (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
405
|
+
function processJsonFile(file) {
|
|
406
|
+
const raw = safeReadJson(file.path);
|
|
407
|
+
if (raw === null) return [];
|
|
408
|
+
const records = [];
|
|
409
|
+
const trace = tryLoadTrace(file.path, raw);
|
|
410
|
+
if (trace) {
|
|
411
|
+
try {
|
|
412
|
+
const fails = getFailures(trace);
|
|
413
|
+
const hung = getHungNodes(trace);
|
|
414
|
+
const stats = getStats(trace);
|
|
415
|
+
records.push({
|
|
416
|
+
id: trace.agentId,
|
|
417
|
+
source: "trace",
|
|
418
|
+
status: fails.length === 0 && hung.length === 0 ? "ok" : "error",
|
|
419
|
+
lastActive: file.mtime,
|
|
420
|
+
detail: `${stats.totalNodes} nodes [${trace.trigger}]`,
|
|
421
|
+
file: file.filename,
|
|
422
|
+
traceData: trace
|
|
423
|
+
});
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
return records;
|
|
464
427
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
428
|
+
if (typeof raw !== "object") return records;
|
|
429
|
+
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;
|
|
430
|
+
if (arr && arr.length > 0 && typeof arr[0] === "object" && arr[0] !== null) {
|
|
431
|
+
for (const item of arr.slice(0, 50)) {
|
|
432
|
+
const name = item.name ?? item.id ?? item.jobId ?? item.agentId;
|
|
433
|
+
if (!name) continue;
|
|
434
|
+
const state = typeof item.state === "object" && item.state !== null ? item.state : item;
|
|
435
|
+
const status2 = findStatus(state);
|
|
436
|
+
const ts2 = findTimestamp(state) || file.mtime;
|
|
437
|
+
const detail2 = extractDetail(state);
|
|
438
|
+
records.push({
|
|
439
|
+
id: String(name),
|
|
440
|
+
source: "jobs",
|
|
441
|
+
status: status2,
|
|
442
|
+
lastActive: ts2,
|
|
443
|
+
detail: detail2,
|
|
444
|
+
file: file.filename
|
|
445
|
+
});
|
|
468
446
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
447
|
+
return records;
|
|
448
|
+
}
|
|
449
|
+
const obj = raw;
|
|
450
|
+
for (const containerKey of ["tools", "workers", "services", "agents", "daemons"]) {
|
|
451
|
+
const container = obj[containerKey];
|
|
452
|
+
if (typeof container === "object" && container !== null && !Array.isArray(container)) {
|
|
453
|
+
for (const [name, info] of Object.entries(container)) {
|
|
454
|
+
if (typeof info !== "object" || info === null) continue;
|
|
455
|
+
const w = info;
|
|
456
|
+
const status2 = findStatus(w);
|
|
457
|
+
const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
|
|
458
|
+
const pid = w.pid;
|
|
459
|
+
const detail2 = pid ? `pid: ${pid}` : extractDetail(w);
|
|
460
|
+
records.push({
|
|
461
|
+
id: name,
|
|
462
|
+
source: "workers",
|
|
463
|
+
status: status2,
|
|
464
|
+
lastActive: ts2,
|
|
465
|
+
detail: detail2,
|
|
466
|
+
file: file.filename
|
|
467
|
+
});
|
|
476
468
|
}
|
|
469
|
+
return records;
|
|
477
470
|
}
|
|
478
|
-
return {
|
|
479
|
-
duration: nodeDuration(node) + bestChild.duration,
|
|
480
|
-
path: [node, ...bestChild.path]
|
|
481
|
-
};
|
|
482
471
|
}
|
|
483
|
-
|
|
472
|
+
const status = findStatus(obj);
|
|
473
|
+
const ts = findTimestamp(obj) || file.mtime;
|
|
474
|
+
const detail = extractDetail(obj);
|
|
475
|
+
records.push({
|
|
476
|
+
id: nameFromFile(file.filename),
|
|
477
|
+
source: "state",
|
|
478
|
+
status,
|
|
479
|
+
lastActive: ts,
|
|
480
|
+
detail,
|
|
481
|
+
file: file.filename
|
|
482
|
+
});
|
|
483
|
+
return records;
|
|
484
484
|
}
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
485
|
+
function processJsonlFile(file) {
|
|
486
|
+
try {
|
|
487
|
+
const content = readFileSync(file.path, "utf8").trim();
|
|
488
|
+
if (!content) return [];
|
|
489
|
+
const lines = content.split("\n");
|
|
490
|
+
const lineCount = lines.length;
|
|
491
|
+
const lastObj = JSON.parse(lines[lines.length - 1]);
|
|
492
|
+
const name = lastObj.jobId ?? lastObj.agentId ?? lastObj.name ?? lastObj.id ?? nameFromFile(file.filename);
|
|
493
|
+
if (lastObj.action !== void 0 || lastObj.jobId !== void 0) {
|
|
494
|
+
const status2 = findStatus(lastObj);
|
|
495
|
+
const ts2 = findTimestamp(lastObj) || file.mtime;
|
|
496
|
+
const action = lastObj.action;
|
|
497
|
+
const detail2 = action ? `${action} (${lineCount} entries)` : `${lineCount} entries`;
|
|
498
|
+
return [
|
|
499
|
+
{
|
|
500
|
+
id: String(name),
|
|
501
|
+
source: "session",
|
|
502
|
+
status: status2,
|
|
503
|
+
lastActive: ts2,
|
|
504
|
+
detail: detail2,
|
|
505
|
+
file: file.filename
|
|
506
|
+
}
|
|
507
|
+
];
|
|
491
508
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
509
|
+
const tail = lines.slice(Math.max(0, lineCount - 30));
|
|
510
|
+
let model = "";
|
|
511
|
+
let totalTokens = 0;
|
|
512
|
+
let totalCost = 0;
|
|
513
|
+
const toolCalls = [];
|
|
514
|
+
let lastUserMsg = "";
|
|
515
|
+
let lastAssistantMsg = "";
|
|
516
|
+
let errorCount = 0;
|
|
517
|
+
let lastRole = "";
|
|
518
|
+
let sessionId = "";
|
|
519
|
+
for (const line of tail) {
|
|
520
|
+
let entry;
|
|
521
|
+
try {
|
|
522
|
+
entry = JSON.parse(line);
|
|
523
|
+
} catch {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const entryType = entry.type;
|
|
527
|
+
if (entryType === "session") {
|
|
528
|
+
sessionId = entry.id ?? "";
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (entryType === "model_change") {
|
|
532
|
+
model = entry.modelId ?? "";
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (entryType !== "message") continue;
|
|
536
|
+
const msg = entry.message;
|
|
537
|
+
if (!msg) continue;
|
|
538
|
+
const role = msg.role;
|
|
539
|
+
lastRole = role;
|
|
540
|
+
if (msg.model && !model) model = msg.model;
|
|
541
|
+
const usage = msg.usage;
|
|
542
|
+
if (usage) {
|
|
543
|
+
const input = usage.input ?? 0;
|
|
544
|
+
const output = usage.output ?? 0;
|
|
545
|
+
totalTokens += input + output;
|
|
546
|
+
const cost = usage.cost;
|
|
547
|
+
if (cost && typeof cost.total === "number") totalCost += cost.total;
|
|
548
|
+
}
|
|
549
|
+
const msgContent = msg.content;
|
|
550
|
+
if (role === "user") {
|
|
551
|
+
if (typeof msgContent === "string") {
|
|
552
|
+
lastUserMsg = msgContent.slice(0, 80);
|
|
553
|
+
} else if (Array.isArray(msgContent)) {
|
|
554
|
+
for (const c of msgContent) {
|
|
555
|
+
if (c.type === "text" && typeof c.text === "string") {
|
|
556
|
+
lastUserMsg = c.text.slice(0, 80);
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (role === "assistant" && Array.isArray(msgContent)) {
|
|
563
|
+
for (const c of msgContent) {
|
|
564
|
+
if (c.type === "tool_use" || c.type === "toolCall") {
|
|
565
|
+
const toolName = c.name ?? "";
|
|
566
|
+
if (toolName && !toolCalls.includes(toolName)) toolCalls.push(toolName);
|
|
567
|
+
}
|
|
568
|
+
if (c.type === "text" && typeof c.text === "string" && c.text.length > 5) {
|
|
569
|
+
lastAssistantMsg = c.text.slice(0, 80);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (role === "toolResult") {
|
|
574
|
+
if (typeof msgContent === "string" && /error|ENOENT|failed|exception/i.test(msgContent)) {
|
|
575
|
+
errorCount++;
|
|
576
|
+
} else if (Array.isArray(msgContent)) {
|
|
577
|
+
for (const c of msgContent) {
|
|
578
|
+
if (typeof c.text === "string" && /error|ENOENT|failed|exception/i.test(c.text)) {
|
|
579
|
+
errorCount++;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
525
584
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
function getStats(graph) {
|
|
531
|
-
const byStatus = {
|
|
532
|
-
running: 0,
|
|
533
|
-
completed: 0,
|
|
534
|
-
failed: 0,
|
|
535
|
-
hung: 0,
|
|
536
|
-
timeout: 0
|
|
537
|
-
};
|
|
538
|
-
const byType = {
|
|
539
|
-
agent: 0,
|
|
540
|
-
tool: 0,
|
|
541
|
-
subagent: 0,
|
|
542
|
-
wait: 0,
|
|
543
|
-
decision: 0,
|
|
544
|
-
custom: 0
|
|
545
|
-
};
|
|
546
|
-
let failureCount = 0;
|
|
547
|
-
let hungCount = 0;
|
|
548
|
-
for (const node of graph.nodes.values()) {
|
|
549
|
-
byStatus[node.status]++;
|
|
550
|
-
byType[node.type]++;
|
|
551
|
-
if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
|
|
552
|
-
failureCount++;
|
|
585
|
+
const parts = [];
|
|
586
|
+
if (model) {
|
|
587
|
+
const shortModel = model.includes("/") ? model.split("/").pop() : model;
|
|
588
|
+
parts.push(shortModel.slice(0, 20));
|
|
553
589
|
}
|
|
554
|
-
if (
|
|
555
|
-
|
|
590
|
+
if (toolCalls.length > 0) {
|
|
591
|
+
const recent = toolCalls.slice(-3);
|
|
592
|
+
parts.push(`tools: ${recent.join(", ")}`);
|
|
556
593
|
}
|
|
594
|
+
if (totalCost > 0) {
|
|
595
|
+
parts.push(`$${totalCost.toFixed(3)}`);
|
|
596
|
+
} else if (totalTokens > 0) {
|
|
597
|
+
const k = (totalTokens / 1e3).toFixed(1);
|
|
598
|
+
parts.push(`${k}k tok`);
|
|
599
|
+
}
|
|
600
|
+
if (lastAssistantMsg && lastRole === "assistant") {
|
|
601
|
+
parts.push(lastAssistantMsg.slice(0, 40));
|
|
602
|
+
} else if (lastUserMsg && !parts.some((p) => p.startsWith("tools:"))) {
|
|
603
|
+
parts.push(`user: ${lastUserMsg.slice(0, 35)}`);
|
|
604
|
+
}
|
|
605
|
+
if (errorCount > 0) {
|
|
606
|
+
parts.push(`${errorCount} errors`);
|
|
607
|
+
}
|
|
608
|
+
const detail = parts.join(" | ") || `${lineCount} messages`;
|
|
609
|
+
const status = errorCount > lineCount / 4 ? "error" : lastRole === "assistant" ? "ok" : "running";
|
|
610
|
+
const ts = findTimestamp(lastObj) || file.mtime;
|
|
611
|
+
const sessionName = sessionId ? sessionId.slice(0, 8) : nameFromFile(file.filename);
|
|
612
|
+
return [
|
|
613
|
+
{
|
|
614
|
+
id: String(name !== "unknown" ? name : sessionName),
|
|
615
|
+
source: "session",
|
|
616
|
+
status,
|
|
617
|
+
lastActive: ts,
|
|
618
|
+
detail,
|
|
619
|
+
file: file.filename
|
|
620
|
+
}
|
|
621
|
+
];
|
|
622
|
+
} catch {
|
|
623
|
+
return [];
|
|
557
624
|
}
|
|
558
|
-
return {
|
|
559
|
-
totalNodes: graph.nodes.size,
|
|
560
|
-
byStatus,
|
|
561
|
-
byType,
|
|
562
|
-
depth: getDepth(graph),
|
|
563
|
-
duration: getDuration(graph),
|
|
564
|
-
failureCount,
|
|
565
|
-
hungCount
|
|
566
|
-
};
|
|
567
625
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const groups = /* @__PURE__ */ new Map();
|
|
572
|
-
for (const g of graphs) {
|
|
573
|
-
if (!g.traceId) continue;
|
|
574
|
-
const arr = groups.get(g.traceId) ?? [];
|
|
575
|
-
arr.push(g);
|
|
576
|
-
groups.set(g.traceId, arr);
|
|
577
|
-
}
|
|
578
|
-
return groups;
|
|
626
|
+
var K = "\x1B[K";
|
|
627
|
+
function writeLine(lines, text) {
|
|
628
|
+
lines.push(text + K);
|
|
579
629
|
}
|
|
580
|
-
function
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (g.spanId) siblings.push(g.spanId);
|
|
594
|
-
childMap.set(g.parentSpanId, siblings);
|
|
595
|
-
}
|
|
630
|
+
function flushLines(lines) {
|
|
631
|
+
process.stdout.write("\x1B[H");
|
|
632
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
633
|
+
process.stdout.write("\x1B[J");
|
|
634
|
+
}
|
|
635
|
+
var prevFileCount = 0;
|
|
636
|
+
var newExecCount = 0;
|
|
637
|
+
var sessionStart = Date.now();
|
|
638
|
+
var firstRender = true;
|
|
639
|
+
function render(config) {
|
|
640
|
+
const files = scanFiles(config.dirs, config.recursive);
|
|
641
|
+
if (files.length > prevFileCount && prevFileCount > 0) {
|
|
642
|
+
newExecCount += files.length - prevFileCount;
|
|
596
643
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
else if (endTime !== null) endTime = Math.max(endTime, g.endTime);
|
|
644
|
+
prevFileCount = files.length;
|
|
645
|
+
const allRecords = [];
|
|
646
|
+
const allTraces = [];
|
|
647
|
+
for (const f of files.slice(0, 300)) {
|
|
648
|
+
const records = f.ext === ".jsonl" ? processJsonlFile(f) : processJsonFile(f);
|
|
649
|
+
for (const r of records) {
|
|
650
|
+
allRecords.push(r);
|
|
651
|
+
if (r.traceData) allTraces.push(r.traceData);
|
|
652
|
+
}
|
|
607
653
|
}
|
|
608
|
-
const
|
|
609
|
-
for (const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
rootGraph,
|
|
614
|
-
childMap: frozenChildMap,
|
|
615
|
-
startTime,
|
|
616
|
-
endTime,
|
|
617
|
-
status
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
function getTraceTree(trace) {
|
|
621
|
-
const result = [];
|
|
622
|
-
function walk(spanId) {
|
|
623
|
-
const graph = trace.graphs.get(spanId);
|
|
624
|
-
if (graph) result.push(graph);
|
|
625
|
-
const children = trace.childMap.get(spanId) ?? [];
|
|
626
|
-
for (const childSpan of children) walk(childSpan);
|
|
654
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
655
|
+
for (const r of allRecords) {
|
|
656
|
+
const arr = byFile.get(r.file) ?? [];
|
|
657
|
+
arr.push(r);
|
|
658
|
+
byFile.set(r.file, arr);
|
|
627
659
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
blue: "\x1B[34m",
|
|
645
|
-
magenta: "\x1B[35m",
|
|
646
|
-
cyan: "\x1B[36m",
|
|
647
|
-
white: "\x1B[37m"
|
|
648
|
-
};
|
|
649
|
-
function parseArgs(argv) {
|
|
650
|
-
const config = { dirs: [], refreshMs: 3e3, recursive: false };
|
|
651
|
-
const args = argv.slice(0);
|
|
652
|
-
if (args[0] === "live") args.shift();
|
|
653
|
-
let i = 0;
|
|
654
|
-
while (i < args.length) {
|
|
655
|
-
const arg = args[i];
|
|
656
|
-
if (arg === "--help" || arg === "-h") {
|
|
657
|
-
printUsage();
|
|
658
|
-
process.exit(0);
|
|
659
|
-
} else if (arg === "--refresh" || arg === "-r") {
|
|
660
|
-
i++;
|
|
661
|
-
const v = parseInt(args[i] ?? "", 10);
|
|
662
|
-
if (!isNaN(v) && v > 0) config.refreshMs = v * 1e3;
|
|
663
|
-
i++;
|
|
664
|
-
} else if (arg === "--recursive" || arg === "-R") {
|
|
665
|
-
config.recursive = true;
|
|
666
|
-
i++;
|
|
667
|
-
} else if (!arg.startsWith("-")) {
|
|
668
|
-
config.dirs.push(resolve2(arg));
|
|
669
|
-
i++;
|
|
660
|
+
const groups = [];
|
|
661
|
+
for (const [file, records] of byFile) {
|
|
662
|
+
if (records.length === 1) {
|
|
663
|
+
const r = records[0];
|
|
664
|
+
groups.push({
|
|
665
|
+
name: r.id,
|
|
666
|
+
source: r.source,
|
|
667
|
+
status: r.status,
|
|
668
|
+
lastTs: r.lastActive,
|
|
669
|
+
detail: r.detail,
|
|
670
|
+
children: [],
|
|
671
|
+
ok: r.status === "ok" ? 1 : 0,
|
|
672
|
+
fail: r.status === "error" ? 1 : 0,
|
|
673
|
+
running: r.status === "running" ? 1 : 0,
|
|
674
|
+
total: 1
|
|
675
|
+
});
|
|
670
676
|
} else {
|
|
671
|
-
|
|
677
|
+
const groupName = nameFromFile(file);
|
|
678
|
+
let lastTs = 0;
|
|
679
|
+
let ok = 0, fail = 0, running = 0;
|
|
680
|
+
for (const r of records) {
|
|
681
|
+
if (r.lastActive > lastTs) lastTs = r.lastActive;
|
|
682
|
+
if (r.status === "ok") ok++;
|
|
683
|
+
else if (r.status === "error") fail++;
|
|
684
|
+
else if (r.status === "running") running++;
|
|
685
|
+
}
|
|
686
|
+
const status = fail > 0 ? "error" : running > 0 ? "running" : ok > 0 ? "ok" : "unknown";
|
|
687
|
+
groups.push({
|
|
688
|
+
name: groupName,
|
|
689
|
+
source: records[0].source,
|
|
690
|
+
status,
|
|
691
|
+
lastTs,
|
|
692
|
+
detail: `${records.length} agents`,
|
|
693
|
+
children: records.sort((a, b) => b.lastActive - a.lastActive),
|
|
694
|
+
ok,
|
|
695
|
+
fail,
|
|
696
|
+
running,
|
|
697
|
+
total: records.length
|
|
698
|
+
});
|
|
672
699
|
}
|
|
673
700
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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;
|
|
701
|
+
groups.sort((a, b) => b.lastTs - a.lastTs);
|
|
702
|
+
const totExec = allRecords.length;
|
|
703
|
+
const totFail = allRecords.filter((r) => r.status === "error").length;
|
|
704
|
+
const totRunning = allRecords.filter((r) => r.status === "running").length;
|
|
705
|
+
const uniqueAgents = new Set(allRecords.map((r) => r.id)).size;
|
|
706
|
+
const sysRate = totExec > 0 ? ((totExec - totFail) / totExec * 100).toFixed(1) : "100.0";
|
|
707
|
+
const now = Date.now();
|
|
708
|
+
const buckets = new Array(12).fill(0);
|
|
709
|
+
const failBuckets = new Array(12).fill(0);
|
|
710
|
+
for (const r of allRecords) {
|
|
711
|
+
const age = now - r.lastActive;
|
|
712
|
+
if (age > 36e5 || age < 0) continue;
|
|
713
|
+
const idx = 11 - Math.floor(age / 3e5);
|
|
714
|
+
if (idx >= 0 && idx < 12) {
|
|
715
|
+
buckets[idx]++;
|
|
716
|
+
if (r.status === "error") failBuckets[idx]++;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const maxBucket = Math.max(...buckets, 1);
|
|
720
|
+
const sparkChars = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
721
|
+
const spark = buckets.map((v, i) => {
|
|
722
|
+
const level = Math.round(v / maxBucket * 8);
|
|
723
|
+
return (failBuckets[i] > 0 ? C.red : C.green) + sparkChars[level] + C.reset;
|
|
724
|
+
}).join("");
|
|
725
|
+
const distributedTraces = [];
|
|
726
|
+
if (allTraces.length > 1) {
|
|
727
|
+
const traceGroups = groupByTraceId(allTraces);
|
|
728
|
+
for (const [_tid, graphs] of traceGroups) {
|
|
729
|
+
if (graphs.length > 1) {
|
|
711
730
|
try {
|
|
712
|
-
|
|
731
|
+
distributedTraces.push(stitchTrace(graphs));
|
|
713
732
|
} 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
733
|
}
|
|
728
734
|
}
|
|
729
|
-
} catch {
|
|
730
735
|
}
|
|
736
|
+
distributedTraces.sort((a, b) => b.startTime - a.startTime);
|
|
731
737
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
function
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
return
|
|
738
|
+
const upSec = Math.floor((Date.now() - sessionStart) / 1e3);
|
|
739
|
+
const upMin = Math.floor(upSec / 60);
|
|
740
|
+
const upStr = upMin > 0 ? `${upMin}m ${upSec % 60}s` : `${upSec}s`;
|
|
741
|
+
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
742
|
+
function statusIcon(s, recent) {
|
|
743
|
+
if (s === "error") return `${C.red}\u25CF${C.reset}`;
|
|
744
|
+
if (s === "running") return `${C.green}\u25CF${C.reset}`;
|
|
745
|
+
if (s === "ok" && recent) return `${C.green}\u25CF${C.reset}`;
|
|
746
|
+
if (s === "ok") return `${C.dim}\u25CB${C.reset}`;
|
|
747
|
+
return `${C.dim}\u25CB${C.reset}`;
|
|
741
748
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
749
|
+
function statusText(g) {
|
|
750
|
+
if (g.fail > 0 && g.ok === 0 && g.running === 0) return `${C.red}error${C.reset}`;
|
|
751
|
+
if (g.running > 0) return `${C.green}running${C.reset}`;
|
|
752
|
+
if (g.fail > 0) return `${C.yellow}${g.ok}ok/${g.fail}err${C.reset}`;
|
|
753
|
+
if (g.ok > 0)
|
|
754
|
+
return g.total > 1 ? `${C.green}${g.ok}/${g.total} ok${C.reset}` : `${C.green}ok${C.reset}`;
|
|
755
|
+
return `${C.dim}idle${C.reset}`;
|
|
756
|
+
}
|
|
757
|
+
function sourceTag(s) {
|
|
758
|
+
switch (s) {
|
|
759
|
+
case "trace":
|
|
760
|
+
return `${C.cyan}trace${C.reset}`;
|
|
761
|
+
case "jobs":
|
|
762
|
+
return `${C.blue}job${C.reset}`;
|
|
763
|
+
case "workers":
|
|
764
|
+
return `${C.magenta}worker${C.reset}`;
|
|
765
|
+
case "session":
|
|
766
|
+
return `${C.yellow}session${C.reset}`;
|
|
767
|
+
case "state":
|
|
768
|
+
return `${C.dim}state${C.reset}`;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function timeStr(ts) {
|
|
772
|
+
if (ts <= 0) return "n/a";
|
|
773
|
+
return new Date(ts).toLocaleTimeString();
|
|
774
|
+
}
|
|
775
|
+
function truncate(s, max) {
|
|
776
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
777
|
+
}
|
|
778
|
+
const termWidth = process.stdout.columns || 120;
|
|
779
|
+
const detailWidth = Math.max(20, termWidth - 60);
|
|
780
|
+
if (firstRender) {
|
|
781
|
+
process.stdout.write("\x1B[2J");
|
|
782
|
+
firstRender = false;
|
|
783
|
+
}
|
|
784
|
+
const L = [];
|
|
785
|
+
writeLine(L, `${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
|
|
786
|
+
writeLine(
|
|
787
|
+
L,
|
|
788
|
+
`${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}`
|
|
789
|
+
);
|
|
790
|
+
const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
|
|
791
|
+
const pad1 = Math.max(0, 64 - metaLine.length);
|
|
792
|
+
writeLine(
|
|
793
|
+
L,
|
|
794
|
+
`${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`
|
|
795
|
+
);
|
|
796
|
+
writeLine(L, `${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
|
|
797
|
+
const sc = totFail === 0 ? C.green : C.yellow;
|
|
798
|
+
writeLine(L, "");
|
|
799
|
+
writeLine(
|
|
800
|
+
L,
|
|
801
|
+
` ${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}`
|
|
802
|
+
);
|
|
803
|
+
writeLine(L, "");
|
|
804
|
+
writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
|
|
805
|
+
writeLine(L, "");
|
|
806
|
+
writeLine(
|
|
807
|
+
L,
|
|
808
|
+
` ${C.bold}${C.under}Agent Status Last Active Detail${C.reset}`
|
|
809
|
+
);
|
|
810
|
+
let lineCount = 0;
|
|
811
|
+
for (const g of groups) {
|
|
812
|
+
if (lineCount > 35) break;
|
|
813
|
+
const isRecent = Date.now() - g.lastTs < 3e5;
|
|
814
|
+
const icon = statusIcon(g.status, isRecent);
|
|
815
|
+
const active = isRecent ? `${C.green}${timeStr(g.lastTs)}${C.reset}` : `${C.dim}${timeStr(g.lastTs)}${C.reset}`;
|
|
816
|
+
if (g.children.length === 0) {
|
|
817
|
+
const name = truncate(g.name, 26).padEnd(26);
|
|
818
|
+
const st = statusText(g);
|
|
819
|
+
const det = truncate(g.detail, detailWidth);
|
|
820
|
+
writeLine(
|
|
821
|
+
L,
|
|
822
|
+
` ${icon} ${name} ${st.padEnd(20)} ${active.padEnd(20)} ${C.dim}${det}${C.reset}`
|
|
823
|
+
);
|
|
824
|
+
lineCount++;
|
|
825
|
+
} else {
|
|
826
|
+
const name = truncate(g.name, 24).padEnd(24);
|
|
827
|
+
const st = statusText(g);
|
|
828
|
+
const tag = sourceTag(g.source);
|
|
829
|
+
writeLine(
|
|
830
|
+
L,
|
|
831
|
+
` ${icon} ${C.bold}${name}${C.reset} ${st.padEnd(20)} ${active.padEnd(20)} ${tag} ${C.dim}(${g.children.length} agents)${C.reset}`
|
|
832
|
+
);
|
|
833
|
+
lineCount++;
|
|
834
|
+
const kids = g.children.slice(0, 12);
|
|
835
|
+
for (let i = 0; i < kids.length; i++) {
|
|
836
|
+
if (lineCount > 35) break;
|
|
837
|
+
const child = kids[i];
|
|
838
|
+
const isLast = i === kids.length - 1;
|
|
839
|
+
const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
840
|
+
const cIcon = statusIcon(child.status, Date.now() - child.lastActive < 3e5);
|
|
841
|
+
const cName = truncate(child.id, 22).padEnd(22);
|
|
842
|
+
const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
|
|
843
|
+
const cDet = truncate(child.detail, detailWidth - 5);
|
|
844
|
+
writeLine(
|
|
845
|
+
L,
|
|
846
|
+
` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`
|
|
847
|
+
);
|
|
848
|
+
lineCount++;
|
|
849
|
+
}
|
|
850
|
+
if (g.children.length > 12) {
|
|
851
|
+
writeLine(L, ` ${C.dim} ... +${g.children.length - 12} more${C.reset}`);
|
|
852
|
+
lineCount++;
|
|
761
853
|
}
|
|
762
854
|
}
|
|
763
855
|
}
|
|
764
|
-
|
|
856
|
+
if (distributedTraces.length > 0) {
|
|
857
|
+
writeLine(L, "");
|
|
858
|
+
writeLine(L, ` ${C.bold}${C.under}Distributed Traces${C.reset}`);
|
|
859
|
+
for (const dt of distributedTraces.slice(0, 3)) {
|
|
860
|
+
const traceTime = new Date(dt.startTime).toLocaleTimeString();
|
|
861
|
+
const si = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
862
|
+
const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
|
|
863
|
+
const tid = dt.traceId.slice(0, 8);
|
|
864
|
+
writeLine(
|
|
865
|
+
L,
|
|
866
|
+
` ${si} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`
|
|
867
|
+
);
|
|
868
|
+
const tree = getTraceTree(dt);
|
|
869
|
+
for (let i = 0; i < Math.min(tree.length, 6); i++) {
|
|
870
|
+
const tg = tree[i];
|
|
871
|
+
const depth = getDistDepth(dt, tg.spanId);
|
|
872
|
+
const indent = " " + "\u2502 ".repeat(Math.max(0, depth - 1));
|
|
873
|
+
const isLast = i === tree.length - 1 || getDistDepth(dt, tree[i + 1]?.spanId) <= depth;
|
|
874
|
+
const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
875
|
+
const gs = tg.status === "completed" ? `${C.green}\u2713${C.reset}` : tg.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
876
|
+
const gd = tg.endTime ? `${tg.endTime - tg.startTime}ms` : "running";
|
|
877
|
+
writeLine(
|
|
878
|
+
L,
|
|
879
|
+
`${indent}${conn}${gs} ${C.bold}${tg.agentId}${C.reset} ${C.dim}[${tg.trigger}] ${gd}${C.reset}`
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const recentRecords = allRecords.filter((r) => r.lastActive > 0).sort((a, b) => b.lastActive - a.lastActive).slice(0, 6);
|
|
885
|
+
if (recentRecords.length > 0) {
|
|
886
|
+
writeLine(L, "");
|
|
887
|
+
writeLine(L, ` ${C.bold}${C.under}Recent Activity${C.reset}`);
|
|
888
|
+
for (const r of recentRecords) {
|
|
889
|
+
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}`;
|
|
890
|
+
const t = new Date(r.lastActive).toLocaleTimeString();
|
|
891
|
+
const agent = truncate(r.id, 26).padEnd(26);
|
|
892
|
+
const age = Math.floor((Date.now() - r.lastActive) / 1e3);
|
|
893
|
+
const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
|
|
894
|
+
const det = truncate(r.detail, detailWidth);
|
|
895
|
+
writeLine(
|
|
896
|
+
L,
|
|
897
|
+
` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${det}${C.reset}`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (files.length === 0) {
|
|
902
|
+
writeLine(L, "");
|
|
903
|
+
writeLine(L, ` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
|
|
904
|
+
for (const d of config.dirs) writeLine(L, ` ${C.dim} ${d}${C.reset}`);
|
|
905
|
+
}
|
|
906
|
+
writeLine(L, "");
|
|
907
|
+
const dirLabel = config.dirs.length === 1 ? config.dirs[0] : `${config.dirs.length} directories`;
|
|
908
|
+
writeLine(L, ` ${C.dim}Watching: ${dirLabel}${C.reset}`);
|
|
909
|
+
writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
|
|
910
|
+
flushLines(L);
|
|
765
911
|
}
|
|
766
|
-
function
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
912
|
+
function getDistDepth(dt, spanId) {
|
|
913
|
+
if (!spanId) return 0;
|
|
914
|
+
const g = dt.graphs.get(spanId);
|
|
915
|
+
if (!g || !g.parentSpanId) return 0;
|
|
916
|
+
return 1 + getDistDepth(dt, g.parentSpanId);
|
|
917
|
+
}
|
|
918
|
+
function startLive(argv) {
|
|
919
|
+
const config = parseArgs(argv);
|
|
920
|
+
const valid = config.dirs.filter((d) => existsSync(d));
|
|
921
|
+
if (valid.length === 0) {
|
|
922
|
+
console.error(`No valid directories found: ${config.dirs.join(", ")}`);
|
|
923
|
+
console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
const invalid = config.dirs.filter((d) => !existsSync(d));
|
|
927
|
+
if (invalid.length > 0) {
|
|
928
|
+
console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
|
|
929
|
+
}
|
|
930
|
+
config.dirs = valid;
|
|
931
|
+
render(config);
|
|
932
|
+
let debounce = null;
|
|
933
|
+
for (const dir of config.dirs) {
|
|
934
|
+
try {
|
|
935
|
+
watch(dir, { recursive: config.recursive }, () => {
|
|
936
|
+
if (debounce) clearTimeout(debounce);
|
|
937
|
+
debounce = setTimeout(() => render(config), 500);
|
|
938
|
+
});
|
|
939
|
+
} catch {
|
|
773
940
|
}
|
|
774
941
|
}
|
|
775
|
-
|
|
942
|
+
setInterval(() => render(config), config.refreshMs);
|
|
943
|
+
process.on("SIGINT", () => {
|
|
944
|
+
console.log("\n" + C.dim + "Monitor stopped." + C.reset);
|
|
945
|
+
process.exit(0);
|
|
946
|
+
});
|
|
776
947
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
948
|
+
|
|
949
|
+
// src/graph-builder.ts
|
|
950
|
+
import { randomUUID } from "crypto";
|
|
951
|
+
function deepFreeze(obj) {
|
|
952
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
953
|
+
if (obj instanceof Map) {
|
|
954
|
+
Object.freeze(obj);
|
|
955
|
+
for (const value of obj.values()) {
|
|
956
|
+
deepFreeze(value);
|
|
784
957
|
}
|
|
958
|
+
return obj;
|
|
785
959
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
960
|
+
Object.freeze(obj);
|
|
961
|
+
const record = obj;
|
|
962
|
+
for (const key of Object.keys(record)) {
|
|
963
|
+
const value = record[key];
|
|
964
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
965
|
+
deepFreeze(value);
|
|
791
966
|
}
|
|
792
967
|
}
|
|
793
|
-
return
|
|
968
|
+
return obj;
|
|
794
969
|
}
|
|
795
|
-
function
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
return loadGraph(obj);
|
|
802
|
-
} catch {
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
970
|
+
function createCounterIdGenerator() {
|
|
971
|
+
let counter = 0;
|
|
972
|
+
return () => {
|
|
973
|
+
counter++;
|
|
974
|
+
return `node_${String(counter).padStart(3, "0")}`;
|
|
975
|
+
};
|
|
805
976
|
}
|
|
806
|
-
function
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
const
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
});
|
|
825
|
-
} catch {
|
|
977
|
+
function createGraphBuilder(config) {
|
|
978
|
+
const generateId = config?.idGenerator ?? createCounterIdGenerator();
|
|
979
|
+
const agentId = config?.agentId ?? "unknown";
|
|
980
|
+
const trigger = config?.trigger ?? "manual";
|
|
981
|
+
const spanId = randomUUID();
|
|
982
|
+
const traceId = config?.traceId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_TRACE_ID : void 0) ?? randomUUID();
|
|
983
|
+
const parentSpanId = config?.parentSpanId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_PARENT_SPAN_ID : void 0) ?? null;
|
|
984
|
+
const graphId = generateId();
|
|
985
|
+
const startTime = Date.now();
|
|
986
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
987
|
+
const edges = [];
|
|
988
|
+
const events = [];
|
|
989
|
+
const parentStack = [];
|
|
990
|
+
let rootNodeId = null;
|
|
991
|
+
let built = false;
|
|
992
|
+
function assertNotBuilt() {
|
|
993
|
+
if (built) {
|
|
994
|
+
throw new Error("GraphBuilder: cannot mutate after build() has been called");
|
|
826
995
|
}
|
|
827
|
-
return records;
|
|
828
996
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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 });
|
|
997
|
+
function getNode2(nodeId) {
|
|
998
|
+
const node = nodes.get(nodeId);
|
|
999
|
+
if (!node) {
|
|
1000
|
+
throw new Error(`GraphBuilder: node "${nodeId}" does not exist`);
|
|
840
1001
|
}
|
|
841
|
-
return
|
|
1002
|
+
return node;
|
|
842
1003
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1004
|
+
function recordEvent(nodeId, eventType, data = {}) {
|
|
1005
|
+
events.push({
|
|
1006
|
+
timestamp: Date.now(),
|
|
1007
|
+
eventType,
|
|
1008
|
+
nodeId,
|
|
1009
|
+
data
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function buildGraph() {
|
|
1013
|
+
if (rootNodeId === null) {
|
|
1014
|
+
throw new Error("GraphBuilder: cannot build a graph with no nodes");
|
|
1015
|
+
}
|
|
1016
|
+
let graphStatus = "completed";
|
|
1017
|
+
for (const node of nodes.values()) {
|
|
1018
|
+
if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
|
|
1019
|
+
graphStatus = "failed";
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
if (node.status === "running") {
|
|
1023
|
+
graphStatus = "running";
|
|
855
1024
|
}
|
|
856
|
-
return records;
|
|
857
1025
|
}
|
|
1026
|
+
const endTime = graphStatus === "running" ? null : Date.now();
|
|
1027
|
+
const frozenNodes = new Map(
|
|
1028
|
+
[...nodes.entries()].map(([id, mNode]) => [
|
|
1029
|
+
id,
|
|
1030
|
+
{
|
|
1031
|
+
id: mNode.id,
|
|
1032
|
+
type: mNode.type,
|
|
1033
|
+
name: mNode.name,
|
|
1034
|
+
startTime: mNode.startTime,
|
|
1035
|
+
endTime: mNode.endTime,
|
|
1036
|
+
status: mNode.status,
|
|
1037
|
+
parentId: mNode.parentId,
|
|
1038
|
+
children: [...mNode.children],
|
|
1039
|
+
metadata: { ...mNode.metadata },
|
|
1040
|
+
state: { ...mNode.state }
|
|
1041
|
+
}
|
|
1042
|
+
])
|
|
1043
|
+
);
|
|
1044
|
+
const graph = {
|
|
1045
|
+
id: graphId,
|
|
1046
|
+
rootNodeId,
|
|
1047
|
+
nodes: frozenNodes,
|
|
1048
|
+
edges: [...edges],
|
|
1049
|
+
startTime,
|
|
1050
|
+
endTime,
|
|
1051
|
+
status: graphStatus,
|
|
1052
|
+
trigger,
|
|
1053
|
+
agentId,
|
|
1054
|
+
events: [...events],
|
|
1055
|
+
traceId,
|
|
1056
|
+
spanId,
|
|
1057
|
+
parentSpanId
|
|
1058
|
+
};
|
|
1059
|
+
return deepFreeze(graph);
|
|
858
1060
|
}
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const name = lastObj.jobId ?? lastObj.agentId ?? lastObj.name ?? lastObj.id ?? nameFromFile(file.filename);
|
|
873
|
-
if (lastObj.action !== void 0 || lastObj.jobId !== void 0) {
|
|
874
|
-
const status2 = findStatus(lastObj);
|
|
875
|
-
const ts2 = findTimestamp(lastObj) || file.mtime;
|
|
876
|
-
const action = lastObj.action;
|
|
877
|
-
const detail2 = action ? `${action} (${lineCount} entries)` : `${lineCount} entries`;
|
|
878
|
-
return [{ id: String(name), source: "session", status: status2, lastActive: ts2, detail: detail2, file: file.filename }];
|
|
879
|
-
}
|
|
880
|
-
const tail = lines.slice(Math.max(0, lineCount - 30));
|
|
881
|
-
let model = "";
|
|
882
|
-
let totalTokens = 0;
|
|
883
|
-
let totalCost = 0;
|
|
884
|
-
let toolCalls = [];
|
|
885
|
-
let lastUserMsg = "";
|
|
886
|
-
let lastAssistantMsg = "";
|
|
887
|
-
let errorCount = 0;
|
|
888
|
-
let lastRole = "";
|
|
889
|
-
let sessionId = "";
|
|
890
|
-
for (const line of tail) {
|
|
891
|
-
let entry;
|
|
892
|
-
try {
|
|
893
|
-
entry = JSON.parse(line);
|
|
894
|
-
} catch {
|
|
895
|
-
continue;
|
|
1061
|
+
const builder = {
|
|
1062
|
+
get graphId() {
|
|
1063
|
+
return graphId;
|
|
1064
|
+
},
|
|
1065
|
+
get traceContext() {
|
|
1066
|
+
return { traceId, spanId };
|
|
1067
|
+
},
|
|
1068
|
+
startNode(opts) {
|
|
1069
|
+
assertNotBuilt();
|
|
1070
|
+
const id = generateId();
|
|
1071
|
+
const parentId = opts.parentId ?? parentStack[parentStack.length - 1] ?? null;
|
|
1072
|
+
if (parentId !== null && !nodes.has(parentId)) {
|
|
1073
|
+
throw new Error(`GraphBuilder: parent node "${parentId}" does not exist`);
|
|
896
1074
|
}
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1075
|
+
const node = {
|
|
1076
|
+
id,
|
|
1077
|
+
type: opts.type,
|
|
1078
|
+
name: opts.name,
|
|
1079
|
+
startTime: Date.now(),
|
|
1080
|
+
endTime: null,
|
|
1081
|
+
status: "running",
|
|
1082
|
+
parentId,
|
|
1083
|
+
children: [],
|
|
1084
|
+
metadata: opts.metadata ? { ...opts.metadata } : {},
|
|
1085
|
+
state: {}
|
|
1086
|
+
};
|
|
1087
|
+
nodes.set(id, node);
|
|
1088
|
+
if (parentId !== null) {
|
|
1089
|
+
const parent = nodes.get(parentId);
|
|
1090
|
+
if (parent) {
|
|
1091
|
+
parent.children.push(id);
|
|
1092
|
+
}
|
|
1093
|
+
edges.push({ from: parentId, to: id, type: "spawned" });
|
|
901
1094
|
}
|
|
902
|
-
if (
|
|
903
|
-
|
|
904
|
-
continue;
|
|
1095
|
+
if (rootNodeId === null) {
|
|
1096
|
+
rootNodeId = id;
|
|
905
1097
|
}
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
totalTokens += input + output;
|
|
917
|
-
const cost = usage.cost;
|
|
918
|
-
if (cost && typeof cost.total === "number") totalCost += cost.total;
|
|
1098
|
+
recordEvent(id, "agent_start", { type: opts.type, name: opts.name });
|
|
1099
|
+
return id;
|
|
1100
|
+
},
|
|
1101
|
+
endNode(nodeId, status = "completed") {
|
|
1102
|
+
assertNotBuilt();
|
|
1103
|
+
const node = getNode2(nodeId);
|
|
1104
|
+
if (node.endTime !== null) {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
`GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
|
|
1107
|
+
);
|
|
919
1108
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
1109
|
+
node.endTime = Date.now();
|
|
1110
|
+
node.status = status;
|
|
1111
|
+
recordEvent(nodeId, "agent_end", { status });
|
|
1112
|
+
},
|
|
1113
|
+
failNode(nodeId, error) {
|
|
1114
|
+
assertNotBuilt();
|
|
1115
|
+
const node = getNode2(nodeId);
|
|
1116
|
+
if (node.endTime !== null) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
`GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
|
|
1119
|
+
);
|
|
932
1120
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
lastAssistantMsg = c.text.slice(0, 80);
|
|
941
|
-
}
|
|
942
|
-
}
|
|
1121
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
1122
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
1123
|
+
node.endTime = Date.now();
|
|
1124
|
+
node.status = "failed";
|
|
1125
|
+
node.metadata.error = errorMessage;
|
|
1126
|
+
if (errorStack) {
|
|
1127
|
+
node.metadata.errorStack = errorStack;
|
|
943
1128
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1129
|
+
recordEvent(nodeId, "tool_error", { error: errorMessage });
|
|
1130
|
+
},
|
|
1131
|
+
addEdge(from, to, type) {
|
|
1132
|
+
assertNotBuilt();
|
|
1133
|
+
getNode2(from);
|
|
1134
|
+
getNode2(to);
|
|
1135
|
+
edges.push({ from, to, type });
|
|
1136
|
+
recordEvent(from, "custom", { to, type, action: "edge_add" });
|
|
1137
|
+
},
|
|
1138
|
+
pushEvent(event) {
|
|
1139
|
+
assertNotBuilt();
|
|
1140
|
+
getNode2(event.nodeId);
|
|
1141
|
+
events.push({
|
|
1142
|
+
...event,
|
|
1143
|
+
timestamp: Date.now()
|
|
1144
|
+
});
|
|
1145
|
+
},
|
|
1146
|
+
updateState(nodeId, state) {
|
|
1147
|
+
assertNotBuilt();
|
|
1148
|
+
const node = getNode2(nodeId);
|
|
1149
|
+
Object.assign(node.state, state);
|
|
1150
|
+
recordEvent(nodeId, "custom", { action: "state_update", ...state });
|
|
1151
|
+
},
|
|
1152
|
+
withParent(parentId, fn) {
|
|
1153
|
+
assertNotBuilt();
|
|
1154
|
+
getNode2(parentId);
|
|
1155
|
+
parentStack.push(parentId);
|
|
1156
|
+
try {
|
|
1157
|
+
return fn();
|
|
1158
|
+
} finally {
|
|
1159
|
+
parentStack.pop();
|
|
954
1160
|
}
|
|
1161
|
+
},
|
|
1162
|
+
getSnapshot() {
|
|
1163
|
+
return buildGraph();
|
|
1164
|
+
},
|
|
1165
|
+
build() {
|
|
1166
|
+
assertNotBuilt();
|
|
1167
|
+
const graph = buildGraph();
|
|
1168
|
+
built = true;
|
|
1169
|
+
return graph;
|
|
955
1170
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1171
|
+
};
|
|
1172
|
+
return builder;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// src/runner.ts
|
|
1176
|
+
import { spawnSync } from "child_process";
|
|
1177
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, statSync as statSync2, writeFileSync } from "fs";
|
|
1178
|
+
import { basename as basename2, join as join2, resolve as resolve2 } from "path";
|
|
1179
|
+
function globToRegex(pattern) {
|
|
1180
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1181
|
+
return new RegExp(`^${escaped}$`);
|
|
1182
|
+
}
|
|
1183
|
+
function snapshotDir(dir, patterns) {
|
|
1184
|
+
const result = /* @__PURE__ */ new Map();
|
|
1185
|
+
if (!existsSync2(dir)) return result;
|
|
1186
|
+
for (const entry of readdirSync2(dir)) {
|
|
1187
|
+
if (!patterns.some((re) => re.test(entry))) continue;
|
|
1188
|
+
const full = join2(dir, entry);
|
|
1189
|
+
try {
|
|
1190
|
+
const stat = statSync2(full);
|
|
1191
|
+
if (stat.isFile()) {
|
|
1192
|
+
result.set(full, stat.mtimeMs);
|
|
1193
|
+
}
|
|
1194
|
+
} catch {
|
|
978
1195
|
}
|
|
979
|
-
const detail = parts.join(" | ") || `${lineCount} messages`;
|
|
980
|
-
const status = errorCount > lineCount / 4 ? "error" : lastRole === "assistant" ? "ok" : "running";
|
|
981
|
-
const ts = findTimestamp(lastObj) || file.mtime;
|
|
982
|
-
const sessionName = sessionId ? sessionId.slice(0, 8) : nameFromFile(file.filename);
|
|
983
|
-
return [{ id: String(name !== "unknown" ? name : sessionName), source: "session", status, lastActive: ts, detail, file: file.filename }];
|
|
984
|
-
} catch {
|
|
985
|
-
return [];
|
|
986
1196
|
}
|
|
1197
|
+
return result;
|
|
987
1198
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1199
|
+
function agentIdFromFilename(filePath) {
|
|
1200
|
+
const base = basename2(filePath, ".json");
|
|
1201
|
+
const cleaned = base.replace(/-state$/, "");
|
|
1202
|
+
return `alfred-${cleaned}`;
|
|
991
1203
|
}
|
|
992
|
-
function
|
|
993
|
-
|
|
994
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
995
|
-
process.stdout.write("\x1B[J");
|
|
1204
|
+
function deriveAgentId(command) {
|
|
1205
|
+
return "orchestrator";
|
|
996
1206
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
for (const r of records) {
|
|
1012
|
-
allRecords.push(r);
|
|
1013
|
-
if (r.traceData) allTraces.push(r.traceData);
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
1017
|
-
for (const r of allRecords) {
|
|
1018
|
-
const arr = byFile.get(r.file) ?? [];
|
|
1019
|
-
arr.push(r);
|
|
1020
|
-
byFile.set(r.file, arr);
|
|
1021
|
-
}
|
|
1022
|
-
const groups = [];
|
|
1023
|
-
for (const [file, records] of byFile) {
|
|
1024
|
-
if (records.length === 1) {
|
|
1025
|
-
const r = records[0];
|
|
1026
|
-
groups.push({
|
|
1027
|
-
name: r.id,
|
|
1028
|
-
source: r.source,
|
|
1029
|
-
status: r.status,
|
|
1030
|
-
lastTs: r.lastActive,
|
|
1031
|
-
detail: r.detail,
|
|
1032
|
-
children: [],
|
|
1033
|
-
ok: r.status === "ok" ? 1 : 0,
|
|
1034
|
-
fail: r.status === "error" ? 1 : 0,
|
|
1035
|
-
running: r.status === "running" ? 1 : 0,
|
|
1036
|
-
total: 1
|
|
1037
|
-
});
|
|
1038
|
-
} else {
|
|
1039
|
-
const groupName = nameFromFile(file);
|
|
1040
|
-
let lastTs = 0;
|
|
1041
|
-
let ok = 0, fail = 0, running = 0;
|
|
1042
|
-
for (const r of records) {
|
|
1043
|
-
if (r.lastActive > lastTs) lastTs = r.lastActive;
|
|
1044
|
-
if (r.status === "ok") ok++;
|
|
1045
|
-
else if (r.status === "error") fail++;
|
|
1046
|
-
else if (r.status === "running") running++;
|
|
1047
|
-
}
|
|
1048
|
-
const status = fail > 0 ? "error" : running > 0 ? "running" : ok > 0 ? "ok" : "unknown";
|
|
1049
|
-
groups.push({
|
|
1050
|
-
name: groupName,
|
|
1051
|
-
source: records[0].source,
|
|
1052
|
-
status,
|
|
1053
|
-
lastTs,
|
|
1054
|
-
detail: `${records.length} agents`,
|
|
1055
|
-
children: records.sort((a, b) => b.lastActive - a.lastActive),
|
|
1056
|
-
ok,
|
|
1057
|
-
fail,
|
|
1058
|
-
running,
|
|
1059
|
-
total: records.length
|
|
1060
|
-
});
|
|
1061
|
-
}
|
|
1207
|
+
function fileTimestamp() {
|
|
1208
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
|
|
1209
|
+
}
|
|
1210
|
+
async function runTraced(config) {
|
|
1211
|
+
const {
|
|
1212
|
+
command,
|
|
1213
|
+
agentId = deriveAgentId(command),
|
|
1214
|
+
trigger = "cli",
|
|
1215
|
+
tracesDir = "./traces",
|
|
1216
|
+
watchDirs = [],
|
|
1217
|
+
watchPatterns = ["*.json"]
|
|
1218
|
+
} = config;
|
|
1219
|
+
if (command.length === 0) {
|
|
1220
|
+
throw new Error("runTraced: command must not be empty");
|
|
1062
1221
|
}
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
const
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const buckets = new Array(12).fill(0);
|
|
1071
|
-
const failBuckets = new Array(12).fill(0);
|
|
1072
|
-
for (const r of allRecords) {
|
|
1073
|
-
const age = now - r.lastActive;
|
|
1074
|
-
if (age > 36e5 || age < 0) continue;
|
|
1075
|
-
const idx = 11 - Math.floor(age / 3e5);
|
|
1076
|
-
if (idx >= 0 && idx < 12) {
|
|
1077
|
-
buckets[idx]++;
|
|
1078
|
-
if (r.status === "error") failBuckets[idx]++;
|
|
1079
|
-
}
|
|
1222
|
+
const resolvedTracesDir = resolve2(tracesDir);
|
|
1223
|
+
const patterns = watchPatterns.map(globToRegex);
|
|
1224
|
+
const orchestrator = createGraphBuilder({ agentId, trigger });
|
|
1225
|
+
const { traceId, spanId } = orchestrator.traceContext;
|
|
1226
|
+
const beforeSnapshots = /* @__PURE__ */ new Map();
|
|
1227
|
+
for (const dir of watchDirs) {
|
|
1228
|
+
beforeSnapshots.set(dir, snapshotDir(dir, patterns));
|
|
1080
1229
|
}
|
|
1081
|
-
const
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
})
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1230
|
+
const rootId = orchestrator.startNode({ type: "agent", name: agentId });
|
|
1231
|
+
const dispatchId = orchestrator.startNode({
|
|
1232
|
+
type: "tool",
|
|
1233
|
+
name: "dispatch-command",
|
|
1234
|
+
parentId: rootId
|
|
1235
|
+
});
|
|
1236
|
+
orchestrator.updateState(dispatchId, { command: command.join(" ") });
|
|
1237
|
+
const monitorId = orchestrator.startNode({
|
|
1238
|
+
type: "tool",
|
|
1239
|
+
name: "state-monitor",
|
|
1240
|
+
parentId: rootId
|
|
1241
|
+
});
|
|
1242
|
+
orchestrator.updateState(monitorId, {
|
|
1243
|
+
watchDirs,
|
|
1244
|
+
watchPatterns
|
|
1245
|
+
});
|
|
1246
|
+
const startMs = Date.now();
|
|
1247
|
+
const execCmd = command[0] ?? "";
|
|
1248
|
+
const execArgs = command.slice(1);
|
|
1249
|
+
process.env.AGENTFLOW_TRACE_ID = traceId;
|
|
1250
|
+
process.env.AGENTFLOW_PARENT_SPAN_ID = spanId;
|
|
1251
|
+
const result = spawnSync(execCmd, execArgs, { stdio: "inherit" });
|
|
1252
|
+
delete process.env.AGENTFLOW_TRACE_ID;
|
|
1253
|
+
delete process.env.AGENTFLOW_PARENT_SPAN_ID;
|
|
1254
|
+
const exitCode = result.status ?? 1;
|
|
1255
|
+
const duration = (Date.now() - startMs) / 1e3;
|
|
1256
|
+
const stateChanges = [];
|
|
1257
|
+
for (const dir of watchDirs) {
|
|
1258
|
+
const before = beforeSnapshots.get(dir) ?? /* @__PURE__ */ new Map();
|
|
1259
|
+
const after = snapshotDir(dir, patterns);
|
|
1260
|
+
for (const [filePath, mtime] of after) {
|
|
1261
|
+
const prevMtime = before.get(filePath);
|
|
1262
|
+
if (prevMtime === void 0 || mtime > prevMtime) {
|
|
1263
|
+
stateChanges.push(filePath);
|
|
1096
1264
|
}
|
|
1097
1265
|
}
|
|
1098
|
-
distributedTraces.sort((a, b) => b.startTime - a.startTime);
|
|
1099
1266
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
if (s === "running") return `${C.green}\u25CF${C.reset}`;
|
|
1107
|
-
if (s === "ok" && recent) return `${C.green}\u25CF${C.reset}`;
|
|
1108
|
-
if (s === "ok") return `${C.dim}\u25CB${C.reset}`;
|
|
1109
|
-
return `${C.dim}\u25CB${C.reset}`;
|
|
1267
|
+
orchestrator.updateState(monitorId, { stateChanges });
|
|
1268
|
+
orchestrator.endNode(monitorId);
|
|
1269
|
+
if (exitCode === 0) {
|
|
1270
|
+
orchestrator.endNode(dispatchId);
|
|
1271
|
+
} else {
|
|
1272
|
+
orchestrator.failNode(dispatchId, `Command exited with code ${exitCode}`);
|
|
1110
1273
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1274
|
+
orchestrator.updateState(rootId, {
|
|
1275
|
+
exitCode,
|
|
1276
|
+
duration,
|
|
1277
|
+
stateChanges
|
|
1278
|
+
});
|
|
1279
|
+
if (exitCode === 0) {
|
|
1280
|
+
orchestrator.endNode(rootId);
|
|
1281
|
+
} else {
|
|
1282
|
+
orchestrator.failNode(rootId, `Command exited with code ${exitCode}`);
|
|
1117
1283
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1284
|
+
const orchestratorGraph = orchestrator.build();
|
|
1285
|
+
const allGraphs = [orchestratorGraph];
|
|
1286
|
+
for (const filePath of stateChanges) {
|
|
1287
|
+
const childAgentId = agentIdFromFilename(filePath);
|
|
1288
|
+
const childBuilder = createGraphBuilder({
|
|
1289
|
+
agentId: childAgentId,
|
|
1290
|
+
trigger: "state-change",
|
|
1291
|
+
traceId,
|
|
1292
|
+
parentSpanId: spanId
|
|
1293
|
+
});
|
|
1294
|
+
const childRootId = childBuilder.startNode({
|
|
1295
|
+
type: "agent",
|
|
1296
|
+
name: childAgentId
|
|
1297
|
+
});
|
|
1298
|
+
childBuilder.updateState(childRootId, {
|
|
1299
|
+
stateFile: filePath,
|
|
1300
|
+
detectedBy: "runner-state-monitor"
|
|
1301
|
+
});
|
|
1302
|
+
childBuilder.endNode(childRootId);
|
|
1303
|
+
allGraphs.push(childBuilder.build());
|
|
1131
1304
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return new Date(ts).toLocaleTimeString();
|
|
1305
|
+
if (!existsSync2(resolvedTracesDir)) {
|
|
1306
|
+
mkdirSync(resolvedTracesDir, { recursive: true });
|
|
1135
1307
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1308
|
+
const ts = fileTimestamp();
|
|
1309
|
+
const tracePaths = [];
|
|
1310
|
+
for (const graph of allGraphs) {
|
|
1311
|
+
const filename = `${graph.agentId}-${ts}.json`;
|
|
1312
|
+
const outPath = join2(resolvedTracesDir, filename);
|
|
1313
|
+
writeFileSync(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
|
|
1314
|
+
tracePaths.push(outPath);
|
|
1138
1315
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
if (firstRender) {
|
|
1142
|
-
process.stdout.write("\x1B[2J");
|
|
1143
|
-
firstRender = false;
|
|
1316
|
+
if (tracePaths.length > 0) {
|
|
1317
|
+
console.log(`\u{1F50D} Run "agentflow trace show ${orchestratorGraph.id} --traces-dir ${resolvedTracesDir}" to inspect`);
|
|
1144
1318
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
for (let i = 0; i < kids.length; i++) {
|
|
1179
|
-
if (lineCount > 35) break;
|
|
1180
|
-
const child = kids[i];
|
|
1181
|
-
const isLast = i === kids.length - 1;
|
|
1182
|
-
const connector = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
1183
|
-
const cIcon = statusIcon(child.status, Date.now() - child.lastActive < 3e5);
|
|
1184
|
-
const cName = truncate(child.id, 22).padEnd(22);
|
|
1185
|
-
const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
|
|
1186
|
-
const cDet = truncate(child.detail, detailWidth - 5);
|
|
1187
|
-
writeLine(L, ` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`);
|
|
1188
|
-
lineCount++;
|
|
1189
|
-
}
|
|
1190
|
-
if (g.children.length > 12) {
|
|
1191
|
-
writeLine(L, ` ${C.dim} ... +${g.children.length - 12} more${C.reset}`);
|
|
1192
|
-
lineCount++;
|
|
1319
|
+
return {
|
|
1320
|
+
exitCode,
|
|
1321
|
+
traceId,
|
|
1322
|
+
spanId,
|
|
1323
|
+
tracePaths,
|
|
1324
|
+
stateChanges,
|
|
1325
|
+
duration
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// src/trace-store.ts
|
|
1330
|
+
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
1331
|
+
import { join as join3 } from "path";
|
|
1332
|
+
function createTraceStore(dir) {
|
|
1333
|
+
async function ensureDir() {
|
|
1334
|
+
await mkdir(dir, { recursive: true });
|
|
1335
|
+
}
|
|
1336
|
+
async function loadAll() {
|
|
1337
|
+
await ensureDir();
|
|
1338
|
+
let files;
|
|
1339
|
+
try {
|
|
1340
|
+
files = await readdir(dir);
|
|
1341
|
+
} catch {
|
|
1342
|
+
return [];
|
|
1343
|
+
}
|
|
1344
|
+
const graphs = [];
|
|
1345
|
+
for (const file of files) {
|
|
1346
|
+
if (!file.endsWith(".json")) continue;
|
|
1347
|
+
try {
|
|
1348
|
+
const content = await readFile(join3(dir, file), "utf-8");
|
|
1349
|
+
const graph = loadGraph(content);
|
|
1350
|
+
graphs.push(graph);
|
|
1351
|
+
} catch {
|
|
1193
1352
|
}
|
|
1194
1353
|
}
|
|
1354
|
+
return graphs;
|
|
1195
1355
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1356
|
+
return {
|
|
1357
|
+
async save(graph) {
|
|
1358
|
+
await ensureDir();
|
|
1359
|
+
const json = graphToJson(graph);
|
|
1360
|
+
const filePath = join3(dir, `${graph.id}.json`);
|
|
1361
|
+
await writeFile(filePath, JSON.stringify(json, null, 2), "utf-8");
|
|
1362
|
+
return filePath;
|
|
1363
|
+
},
|
|
1364
|
+
async get(graphId) {
|
|
1365
|
+
await ensureDir();
|
|
1366
|
+
const filePath = join3(dir, `${graphId}.json`);
|
|
1367
|
+
try {
|
|
1368
|
+
const content = await readFile(filePath, "utf-8");
|
|
1369
|
+
return loadGraph(content);
|
|
1370
|
+
} catch {
|
|
1371
|
+
}
|
|
1372
|
+
const all = await loadAll();
|
|
1373
|
+
return all.find((g) => g.id === graphId) ?? null;
|
|
1374
|
+
},
|
|
1375
|
+
async list(opts) {
|
|
1376
|
+
let graphs = await loadAll();
|
|
1377
|
+
if (opts?.status) {
|
|
1378
|
+
graphs = graphs.filter((g) => g.status === opts.status);
|
|
1379
|
+
}
|
|
1380
|
+
graphs.sort((a, b) => b.startTime - a.startTime);
|
|
1381
|
+
if (opts?.limit && opts.limit > 0) {
|
|
1382
|
+
graphs = graphs.slice(0, opts.limit);
|
|
1383
|
+
}
|
|
1384
|
+
return graphs;
|
|
1385
|
+
},
|
|
1386
|
+
async getStuckSpans() {
|
|
1387
|
+
const graphs = await loadAll();
|
|
1388
|
+
const stuck = [];
|
|
1389
|
+
for (const graph of graphs) {
|
|
1390
|
+
for (const node of graph.nodes.values()) {
|
|
1391
|
+
if (node.status === "running" || node.status === "hung" || node.status === "timeout") {
|
|
1392
|
+
stuck.push(node);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return stuck;
|
|
1397
|
+
},
|
|
1398
|
+
async getReasoningLoops(threshold = 25) {
|
|
1399
|
+
const graphs = await loadAll();
|
|
1400
|
+
const results = [];
|
|
1401
|
+
for (const graph of graphs) {
|
|
1402
|
+
const loops = findLoopsInGraph(graph, threshold);
|
|
1403
|
+
if (loops.length > 0) {
|
|
1404
|
+
results.push({ graphId: graph.id, nodes: loops });
|
|
1405
|
+
}
|
|
1215
1406
|
}
|
|
1407
|
+
return results;
|
|
1216
1408
|
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
function findLoopsInGraph(graph, threshold) {
|
|
1412
|
+
const loopNodes = [];
|
|
1413
|
+
function walk(nodeId, consecutiveCount, consecutiveType) {
|
|
1414
|
+
const node = graph.nodes.get(nodeId);
|
|
1415
|
+
if (!node) return;
|
|
1416
|
+
const newCount = node.type === consecutiveType ? consecutiveCount + 1 : 1;
|
|
1417
|
+
if (newCount > threshold) {
|
|
1418
|
+
loopNodes.push(node);
|
|
1419
|
+
}
|
|
1420
|
+
for (const childId of node.children) {
|
|
1421
|
+
walk(childId, newCount, node.type);
|
|
1230
1422
|
}
|
|
1231
1423
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
writeLine(L, ` ${C.dim}No JSON/JSONL files found. Waiting for data in:${C.reset}`);
|
|
1235
|
-
for (const d of config.dirs) writeLine(L, ` ${C.dim} ${d}${C.reset}`);
|
|
1236
|
-
}
|
|
1237
|
-
writeLine(L, "");
|
|
1238
|
-
const dirLabel = config.dirs.length === 1 ? config.dirs[0] : `${config.dirs.length} directories`;
|
|
1239
|
-
writeLine(L, ` ${C.dim}Watching: ${dirLabel}${C.reset}`);
|
|
1240
|
-
writeLine(L, ` ${C.dim}Press Ctrl+C to exit${C.reset}`);
|
|
1241
|
-
flushLines(L);
|
|
1424
|
+
walk(graph.rootNodeId, 0, null);
|
|
1425
|
+
return loopNodes;
|
|
1242
1426
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1427
|
+
|
|
1428
|
+
// src/visualize.ts
|
|
1429
|
+
var STATUS_ICONS = {
|
|
1430
|
+
completed: "\u2713",
|
|
1431
|
+
failed: "\u2717",
|
|
1432
|
+
running: "\u231B",
|
|
1433
|
+
hung: "\u231B",
|
|
1434
|
+
timeout: "\u231B"
|
|
1435
|
+
};
|
|
1436
|
+
function formatDuration(ms) {
|
|
1437
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1438
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
1439
|
+
if (ms < 36e5) return `${(ms / 6e4).toFixed(1)}m`;
|
|
1440
|
+
return `${(ms / 36e5).toFixed(1)}h`;
|
|
1248
1441
|
}
|
|
1249
|
-
function
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1442
|
+
function nodeDuration(node, graphEndTime) {
|
|
1443
|
+
const end = node.endTime ?? graphEndTime;
|
|
1444
|
+
return formatDuration(end - node.startTime);
|
|
1445
|
+
}
|
|
1446
|
+
function getGenAiInfo(node) {
|
|
1447
|
+
const parts = [];
|
|
1448
|
+
const meta = node.metadata;
|
|
1449
|
+
if (meta["gen_ai.request.model"]) {
|
|
1450
|
+
parts.push(String(meta["gen_ai.request.model"]));
|
|
1451
|
+
}
|
|
1452
|
+
const tokens = meta["gen_ai.usage.prompt_tokens"] ?? meta["gen_ai.usage.completion_tokens"];
|
|
1453
|
+
if (tokens !== void 0) {
|
|
1454
|
+
const prompt = meta["gen_ai.usage.prompt_tokens"] ?? 0;
|
|
1455
|
+
const completion = meta["gen_ai.usage.completion_tokens"] ?? 0;
|
|
1456
|
+
if (prompt || completion) {
|
|
1457
|
+
parts.push(`${prompt + completion} tok`);
|
|
1458
|
+
}
|
|
1260
1459
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1460
|
+
return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
|
|
1461
|
+
}
|
|
1462
|
+
function hasViolation(node, graph) {
|
|
1463
|
+
return graph.events.some(
|
|
1464
|
+
(e) => e.nodeId === node.id && e.eventType === "custom" && e.data.guardViolation !== void 0
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
function toAsciiTree(graph) {
|
|
1468
|
+
if (graph.nodes.size === 0) return "(empty graph)";
|
|
1469
|
+
const now = Date.now();
|
|
1470
|
+
const endTime = graph.endTime ?? now;
|
|
1471
|
+
const lines = [];
|
|
1472
|
+
function renderNode(nodeId, prefix, isLast, isRoot) {
|
|
1473
|
+
const node = graph.nodes.get(nodeId);
|
|
1474
|
+
if (!node) return;
|
|
1475
|
+
const icon = STATUS_ICONS[node.status];
|
|
1476
|
+
const duration = nodeDuration(node, endTime);
|
|
1477
|
+
const genAi = getGenAiInfo(node);
|
|
1478
|
+
const violation = hasViolation(node, graph) ? " \u26A0" : "";
|
|
1479
|
+
const errorInfo = node.status === "failed" && node.metadata.error ? ` \u2014 ${node.metadata.error}` : "";
|
|
1480
|
+
const timeoutInfo = node.status === "timeout" ? " [TIMEOUT]" : "";
|
|
1481
|
+
const hungInfo = node.status === "hung" ? " [HUNG]" : "";
|
|
1482
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1483
|
+
const line = `${prefix}${connector}${icon} ${node.name} (${node.type}) ${duration}${genAi}${violation}${timeoutInfo}${hungInfo}${errorInfo}`;
|
|
1484
|
+
lines.push(line);
|
|
1485
|
+
const children = getChildren(graph, nodeId);
|
|
1486
|
+
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
1487
|
+
for (let i = 0; i < children.length; i++) {
|
|
1488
|
+
renderNode(children[i].id, childPrefix, i === children.length - 1, false);
|
|
1271
1489
|
}
|
|
1272
1490
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1491
|
+
renderNode(graph.rootNodeId, "", true, true);
|
|
1492
|
+
return lines.join("\n");
|
|
1493
|
+
}
|
|
1494
|
+
function toTimeline(graph) {
|
|
1495
|
+
if (graph.nodes.size === 0) return "(empty graph)";
|
|
1496
|
+
const now = Date.now();
|
|
1497
|
+
const graphStart = graph.startTime;
|
|
1498
|
+
const graphEnd = graph.endTime ?? now;
|
|
1499
|
+
const totalDuration = graphEnd - graphStart;
|
|
1500
|
+
if (totalDuration <= 0) return "(zero duration)";
|
|
1501
|
+
const barWidth = 60;
|
|
1502
|
+
const lines = [];
|
|
1503
|
+
const scaleLabels = [];
|
|
1504
|
+
const tickCount = Math.min(5, Math.max(2, Math.floor(barWidth / 10)));
|
|
1505
|
+
for (let i = 0; i <= tickCount; i++) {
|
|
1506
|
+
const t = totalDuration * i / tickCount;
|
|
1507
|
+
scaleLabels.push(formatDuration(t));
|
|
1508
|
+
}
|
|
1509
|
+
let header = "";
|
|
1510
|
+
for (let i = 0; i < scaleLabels.length; i++) {
|
|
1511
|
+
const pos = Math.round(barWidth * i / tickCount);
|
|
1512
|
+
while (header.length < pos) header += " ";
|
|
1513
|
+
header += scaleLabels[i];
|
|
1514
|
+
}
|
|
1515
|
+
lines.push(header);
|
|
1516
|
+
let tickLine = "";
|
|
1517
|
+
for (let i = 0; i < barWidth; i++) {
|
|
1518
|
+
const tickPos = tickCount > 0 ? i * tickCount / barWidth : 0;
|
|
1519
|
+
if (Number.isInteger(Math.round(tickPos * 100) / 100) && Math.abs(tickPos - Math.round(tickPos)) < 0.01) {
|
|
1520
|
+
tickLine += "\u253C";
|
|
1521
|
+
} else {
|
|
1522
|
+
tickLine += "\u2500";
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
lines.push(tickLine);
|
|
1526
|
+
const orderedNodes = [];
|
|
1527
|
+
function collectNodes(nodeId) {
|
|
1528
|
+
const node = graph.nodes.get(nodeId);
|
|
1529
|
+
if (!node) return;
|
|
1530
|
+
orderedNodes.push(node);
|
|
1531
|
+
const children = getChildren(graph, nodeId);
|
|
1532
|
+
for (const child of children) {
|
|
1533
|
+
collectNodes(child.id);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
collectNodes(graph.rootNodeId);
|
|
1537
|
+
for (const node of orderedNodes) {
|
|
1538
|
+
const nodeStart = node.startTime - graphStart;
|
|
1539
|
+
const nodeEnd = (node.endTime ?? now) - graphStart;
|
|
1540
|
+
const startCol = Math.round(nodeStart / totalDuration * barWidth);
|
|
1541
|
+
const endCol = Math.max(startCol + 1, Math.round(nodeEnd / totalDuration * barWidth));
|
|
1542
|
+
let bar = "";
|
|
1543
|
+
for (let i = 0; i < barWidth; i++) {
|
|
1544
|
+
if (i >= startCol && i < endCol) {
|
|
1545
|
+
bar += "\u2588";
|
|
1546
|
+
} else {
|
|
1547
|
+
bar += " ";
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const icon = STATUS_ICONS[node.status];
|
|
1551
|
+
const duration = nodeDuration(node, graphEnd);
|
|
1552
|
+
const violation = hasViolation(node, graph) ? " \u26A0" : "";
|
|
1553
|
+
lines.push(`${bar} ${icon} ${node.name} (${duration})${violation}`);
|
|
1554
|
+
}
|
|
1555
|
+
return lines.join("\n");
|
|
1278
1556
|
}
|
|
1279
1557
|
|
|
1280
1558
|
// src/watch.ts
|
|
1281
1559
|
import { existsSync as existsSync4 } from "fs";
|
|
1282
|
-
import { resolve as resolve3, join as join3 } from "path";
|
|
1283
1560
|
import { hostname } from "os";
|
|
1561
|
+
import { join as join4, resolve as resolve3 } from "path";
|
|
1562
|
+
|
|
1563
|
+
// src/watch-alerts.ts
|
|
1564
|
+
import { exec } from "child_process";
|
|
1565
|
+
import { request as httpRequest } from "http";
|
|
1566
|
+
import { request as httpsRequest } from "https";
|
|
1567
|
+
function formatAlertMessage(payload) {
|
|
1568
|
+
const time = new Date(payload.timestamp).toISOString();
|
|
1569
|
+
const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
|
|
1570
|
+
return [
|
|
1571
|
+
`[ALERT] ${payload.condition}: "${payload.agentId}"`,
|
|
1572
|
+
` Status: ${arrow}`,
|
|
1573
|
+
payload.detail ? ` Detail: ${payload.detail}` : null,
|
|
1574
|
+
` File: ${payload.file}`,
|
|
1575
|
+
` Time: ${time}`
|
|
1576
|
+
].filter(Boolean).join("\n");
|
|
1577
|
+
}
|
|
1578
|
+
function formatTelegram(payload) {
|
|
1579
|
+
const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
|
|
1580
|
+
const time = new Date(payload.timestamp).toLocaleTimeString();
|
|
1581
|
+
return [
|
|
1582
|
+
`${icon} *AgentFlow Alert*`,
|
|
1583
|
+
`*${payload.condition}*: \`${payload.agentId}\``,
|
|
1584
|
+
`Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
|
|
1585
|
+
payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
|
|
1586
|
+
`Time: ${time}`
|
|
1587
|
+
].filter(Boolean).join("\n");
|
|
1588
|
+
}
|
|
1589
|
+
async function sendAlert(payload, channel) {
|
|
1590
|
+
try {
|
|
1591
|
+
switch (channel.type) {
|
|
1592
|
+
case "stdout":
|
|
1593
|
+
sendStdout(payload);
|
|
1594
|
+
break;
|
|
1595
|
+
case "telegram":
|
|
1596
|
+
await sendTelegram(payload, channel.botToken, channel.chatId);
|
|
1597
|
+
break;
|
|
1598
|
+
case "webhook":
|
|
1599
|
+
await sendWebhook(payload, channel.url);
|
|
1600
|
+
break;
|
|
1601
|
+
case "command":
|
|
1602
|
+
await sendCommand(payload, channel.cmd);
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1607
|
+
console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
function sendStdout(payload) {
|
|
1611
|
+
console.log(formatAlertMessage(payload));
|
|
1612
|
+
}
|
|
1613
|
+
function sendTelegram(payload, botToken, chatId) {
|
|
1614
|
+
const body = JSON.stringify({
|
|
1615
|
+
chat_id: chatId,
|
|
1616
|
+
text: formatTelegram(payload),
|
|
1617
|
+
parse_mode: "Markdown"
|
|
1618
|
+
});
|
|
1619
|
+
return new Promise((resolve4, reject) => {
|
|
1620
|
+
const req = httpsRequest(
|
|
1621
|
+
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1622
|
+
{
|
|
1623
|
+
method: "POST",
|
|
1624
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
|
|
1625
|
+
},
|
|
1626
|
+
(res) => {
|
|
1627
|
+
res.resume();
|
|
1628
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1629
|
+
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1630
|
+
}
|
|
1631
|
+
);
|
|
1632
|
+
req.on("error", reject);
|
|
1633
|
+
req.write(body);
|
|
1634
|
+
req.end();
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
function sendWebhook(payload, url) {
|
|
1638
|
+
const body = JSON.stringify(payload);
|
|
1639
|
+
const isHttps = url.startsWith("https");
|
|
1640
|
+
const doRequest = isHttps ? httpsRequest : httpRequest;
|
|
1641
|
+
return new Promise((resolve4, reject) => {
|
|
1642
|
+
const req = doRequest(
|
|
1643
|
+
url,
|
|
1644
|
+
{
|
|
1645
|
+
method: "POST",
|
|
1646
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
|
|
1647
|
+
},
|
|
1648
|
+
(res) => {
|
|
1649
|
+
res.resume();
|
|
1650
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1651
|
+
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1652
|
+
}
|
|
1653
|
+
);
|
|
1654
|
+
req.on("error", reject);
|
|
1655
|
+
req.setTimeout(1e4, () => {
|
|
1656
|
+
req.destroy(new Error("Webhook timeout"));
|
|
1657
|
+
});
|
|
1658
|
+
req.write(body);
|
|
1659
|
+
req.end();
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
function sendCommand(payload, cmd) {
|
|
1663
|
+
return new Promise((resolve4, reject) => {
|
|
1664
|
+
const env = {
|
|
1665
|
+
...process.env,
|
|
1666
|
+
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
1667
|
+
AGENTFLOW_ALERT_CONDITION: payload.condition,
|
|
1668
|
+
AGENTFLOW_ALERT_STATUS: payload.currentStatus,
|
|
1669
|
+
AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
|
|
1670
|
+
AGENTFLOW_ALERT_DETAIL: payload.detail,
|
|
1671
|
+
AGENTFLOW_ALERT_FILE: payload.file,
|
|
1672
|
+
AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
|
|
1673
|
+
};
|
|
1674
|
+
exec(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1675
|
+
if (err) reject(err);
|
|
1676
|
+
else resolve4();
|
|
1677
|
+
});
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1284
1680
|
|
|
1285
1681
|
// src/watch-state.ts
|
|
1286
|
-
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2
|
|
1682
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1287
1683
|
function parseDuration(input) {
|
|
1288
1684
|
const match = input.match(/^(\d+(?:\.\d+)?)\s*(s|m|h|d)$/i);
|
|
1289
1685
|
if (!match) {
|
|
@@ -1346,7 +1742,9 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1346
1742
|
const hasError = config.alertConditions.some((c) => c.type === "error");
|
|
1347
1743
|
const hasRecovery = config.alertConditions.some((c) => c.type === "recovery");
|
|
1348
1744
|
const staleConditions = config.alertConditions.filter((c) => c.type === "stale");
|
|
1349
|
-
const consecutiveConditions = config.alertConditions.filter(
|
|
1745
|
+
const consecutiveConditions = config.alertConditions.filter(
|
|
1746
|
+
(c) => c.type === "consecutive-errors"
|
|
1747
|
+
);
|
|
1350
1748
|
const byAgent = /* @__PURE__ */ new Map();
|
|
1351
1749
|
for (const r of currentRecords) {
|
|
1352
1750
|
const existing = byAgent.get(r.id);
|
|
@@ -1370,14 +1768,16 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1370
1768
|
for (const cond of consecutiveConditions) {
|
|
1371
1769
|
if (newConsec === cond.threshold) {
|
|
1372
1770
|
if (canAlert(prev, `consecutive-errors:${cond.threshold}`, config.cooldownMs, now)) {
|
|
1373
|
-
alerts.push(
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1771
|
+
alerts.push(
|
|
1772
|
+
makePayload(
|
|
1773
|
+
agentId,
|
|
1774
|
+
`consecutive-errors (${cond.threshold})`,
|
|
1775
|
+
prevStatus,
|
|
1776
|
+
currStatus,
|
|
1777
|
+
{ ...record, detail: `${newConsec} consecutive errors. ${record.detail}` },
|
|
1778
|
+
config.dirs
|
|
1779
|
+
)
|
|
1780
|
+
);
|
|
1381
1781
|
}
|
|
1382
1782
|
}
|
|
1383
1783
|
}
|
|
@@ -1386,14 +1786,16 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1386
1786
|
if (sinceActive > cond.durationMs && record.lastActive > 0) {
|
|
1387
1787
|
if (canAlert(prev, "stale", config.cooldownMs, now)) {
|
|
1388
1788
|
const mins = Math.floor(sinceActive / 6e4);
|
|
1389
|
-
alerts.push(
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1789
|
+
alerts.push(
|
|
1790
|
+
makePayload(
|
|
1791
|
+
agentId,
|
|
1792
|
+
"stale",
|
|
1793
|
+
prevStatus,
|
|
1794
|
+
currStatus,
|
|
1795
|
+
{ ...record, detail: `No update for ${mins}m. ${record.detail}` },
|
|
1796
|
+
config.dirs
|
|
1797
|
+
)
|
|
1798
|
+
);
|
|
1397
1799
|
}
|
|
1398
1800
|
}
|
|
1399
1801
|
}
|
|
@@ -1406,14 +1808,19 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1406
1808
|
if (canAlert(prev, "stale-auto", config.cooldownMs, now)) {
|
|
1407
1809
|
const mins = Math.floor(sinceActive / 6e4);
|
|
1408
1810
|
const expectedMins = Math.floor(expectedInterval / 6e4);
|
|
1409
|
-
alerts.push(
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1811
|
+
alerts.push(
|
|
1812
|
+
makePayload(
|
|
1813
|
+
agentId,
|
|
1814
|
+
"stale (auto)",
|
|
1815
|
+
prevStatus,
|
|
1816
|
+
currStatus,
|
|
1817
|
+
{
|
|
1818
|
+
...record,
|
|
1819
|
+
detail: `No update for ${mins}m (expected every ~${expectedMins}m). ${record.detail}`
|
|
1820
|
+
},
|
|
1821
|
+
config.dirs
|
|
1822
|
+
)
|
|
1823
|
+
);
|
|
1417
1824
|
}
|
|
1418
1825
|
}
|
|
1419
1826
|
}
|
|
@@ -1472,118 +1879,6 @@ function makePayload(agentId, condition, previousStatus, currentStatus, record,
|
|
|
1472
1879
|
};
|
|
1473
1880
|
}
|
|
1474
1881
|
|
|
1475
|
-
// src/watch-alerts.ts
|
|
1476
|
-
import { request as httpsRequest } from "https";
|
|
1477
|
-
import { request as httpRequest } from "http";
|
|
1478
|
-
import { exec } from "child_process";
|
|
1479
|
-
function formatAlertMessage(payload) {
|
|
1480
|
-
const time = new Date(payload.timestamp).toISOString();
|
|
1481
|
-
const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
|
|
1482
|
-
return [
|
|
1483
|
-
`[ALERT] ${payload.condition}: "${payload.agentId}"`,
|
|
1484
|
-
` Status: ${arrow}`,
|
|
1485
|
-
payload.detail ? ` Detail: ${payload.detail}` : null,
|
|
1486
|
-
` File: ${payload.file}`,
|
|
1487
|
-
` Time: ${time}`
|
|
1488
|
-
].filter(Boolean).join("\n");
|
|
1489
|
-
}
|
|
1490
|
-
function formatTelegram(payload) {
|
|
1491
|
-
const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
|
|
1492
|
-
const time = new Date(payload.timestamp).toLocaleTimeString();
|
|
1493
|
-
return [
|
|
1494
|
-
`${icon} *AgentFlow Alert*`,
|
|
1495
|
-
`*${payload.condition}*: \`${payload.agentId}\``,
|
|
1496
|
-
`Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
|
|
1497
|
-
payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
|
|
1498
|
-
`Time: ${time}`
|
|
1499
|
-
].filter(Boolean).join("\n");
|
|
1500
|
-
}
|
|
1501
|
-
async function sendAlert(payload, channel) {
|
|
1502
|
-
try {
|
|
1503
|
-
switch (channel.type) {
|
|
1504
|
-
case "stdout":
|
|
1505
|
-
sendStdout(payload);
|
|
1506
|
-
break;
|
|
1507
|
-
case "telegram":
|
|
1508
|
-
await sendTelegram(payload, channel.botToken, channel.chatId);
|
|
1509
|
-
break;
|
|
1510
|
-
case "webhook":
|
|
1511
|
-
await sendWebhook(payload, channel.url);
|
|
1512
|
-
break;
|
|
1513
|
-
case "command":
|
|
1514
|
-
await sendCommand(payload, channel.cmd);
|
|
1515
|
-
break;
|
|
1516
|
-
}
|
|
1517
|
-
} catch (err) {
|
|
1518
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1519
|
-
console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
function sendStdout(payload) {
|
|
1523
|
-
console.log(formatAlertMessage(payload));
|
|
1524
|
-
}
|
|
1525
|
-
function sendTelegram(payload, botToken, chatId) {
|
|
1526
|
-
const body = JSON.stringify({
|
|
1527
|
-
chat_id: chatId,
|
|
1528
|
-
text: formatTelegram(payload),
|
|
1529
|
-
parse_mode: "Markdown"
|
|
1530
|
-
});
|
|
1531
|
-
return new Promise((resolve4, reject) => {
|
|
1532
|
-
const req = httpsRequest(
|
|
1533
|
-
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1534
|
-
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1535
|
-
(res) => {
|
|
1536
|
-
res.resume();
|
|
1537
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1538
|
-
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1539
|
-
}
|
|
1540
|
-
);
|
|
1541
|
-
req.on("error", reject);
|
|
1542
|
-
req.write(body);
|
|
1543
|
-
req.end();
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
function sendWebhook(payload, url) {
|
|
1547
|
-
const body = JSON.stringify(payload);
|
|
1548
|
-
const isHttps = url.startsWith("https");
|
|
1549
|
-
const doRequest = isHttps ? httpsRequest : httpRequest;
|
|
1550
|
-
return new Promise((resolve4, reject) => {
|
|
1551
|
-
const req = doRequest(
|
|
1552
|
-
url,
|
|
1553
|
-
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1554
|
-
(res) => {
|
|
1555
|
-
res.resume();
|
|
1556
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve4();
|
|
1557
|
-
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1558
|
-
}
|
|
1559
|
-
);
|
|
1560
|
-
req.on("error", reject);
|
|
1561
|
-
req.setTimeout(1e4, () => {
|
|
1562
|
-
req.destroy(new Error("Webhook timeout"));
|
|
1563
|
-
});
|
|
1564
|
-
req.write(body);
|
|
1565
|
-
req.end();
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
|
-
function sendCommand(payload, cmd) {
|
|
1569
|
-
return new Promise((resolve4, reject) => {
|
|
1570
|
-
const env = {
|
|
1571
|
-
...process.env,
|
|
1572
|
-
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
1573
|
-
AGENTFLOW_ALERT_CONDITION: payload.condition,
|
|
1574
|
-
AGENTFLOW_ALERT_STATUS: payload.currentStatus,
|
|
1575
|
-
AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
|
|
1576
|
-
AGENTFLOW_ALERT_DETAIL: payload.detail,
|
|
1577
|
-
AGENTFLOW_ALERT_FILE: payload.file,
|
|
1578
|
-
AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
|
|
1579
|
-
};
|
|
1580
|
-
exec(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1581
|
-
if (err) reject(err);
|
|
1582
|
-
else resolve4();
|
|
1583
|
-
});
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
1882
|
// src/watch.ts
|
|
1588
1883
|
function parseWatchArgs(argv) {
|
|
1589
1884
|
const dirs = [];
|
|
@@ -1625,7 +1920,9 @@ function parseWatchArgs(argv) {
|
|
|
1625
1920
|
if (botToken && chatId) {
|
|
1626
1921
|
notifyChannels.push({ type: "telegram", botToken, chatId });
|
|
1627
1922
|
} else {
|
|
1628
|
-
console.error(
|
|
1923
|
+
console.error(
|
|
1924
|
+
"Warning: --notify telegram requires AGENTFLOW_TELEGRAM_BOT_TOKEN and AGENTFLOW_TELEGRAM_CHAT_ID env vars"
|
|
1925
|
+
);
|
|
1629
1926
|
}
|
|
1630
1927
|
} else if (val.startsWith("webhook:")) {
|
|
1631
1928
|
notifyChannels.push({ type: "webhook", url: val.slice(8) });
|
|
@@ -1664,7 +1961,7 @@ function parseWatchArgs(argv) {
|
|
|
1664
1961
|
}
|
|
1665
1962
|
notifyChannels.unshift({ type: "stdout" });
|
|
1666
1963
|
if (!stateFilePath) {
|
|
1667
|
-
stateFilePath =
|
|
1964
|
+
stateFilePath = join4(dirs[0], ".agentflow-watch-state.json");
|
|
1668
1965
|
}
|
|
1669
1966
|
return {
|
|
1670
1967
|
dirs,
|
|
@@ -1677,7 +1974,8 @@ function parseWatchArgs(argv) {
|
|
|
1677
1974
|
};
|
|
1678
1975
|
}
|
|
1679
1976
|
function printWatchUsage() {
|
|
1680
|
-
console.log(
|
|
1977
|
+
console.log(
|
|
1978
|
+
`
|
|
1681
1979
|
AgentFlow Watch \u2014 headless alert system for agent infrastructure.
|
|
1682
1980
|
|
|
1683
1981
|
Polls directories for JSON/JSONL files, detects failures and stale
|
|
@@ -1720,7 +2018,8 @@ Examples:
|
|
|
1720
2018
|
agentflow watch ./data ./cron --notify telegram --poll 60
|
|
1721
2019
|
agentflow watch ./traces --notify webhook:https://hooks.slack.com/... --alert-on consecutive-errors:3
|
|
1722
2020
|
agentflow watch ./data --notify "command:curl -X POST https://my-pagerduty/alert"
|
|
1723
|
-
`.trim()
|
|
2021
|
+
`.trim()
|
|
2022
|
+
);
|
|
1724
2023
|
}
|
|
1725
2024
|
function startWatch(argv) {
|
|
1726
2025
|
const config = parseWatchArgs(argv);
|
|
@@ -1749,7 +2048,9 @@ agentflow watch started`);
|
|
|
1749
2048
|
console.log(` Directories: ${valid.join(", ")}`);
|
|
1750
2049
|
console.log(` Poll: ${config.pollIntervalMs / 1e3}s`);
|
|
1751
2050
|
console.log(` Alert on: ${condLabels.join(", ")}`);
|
|
1752
|
-
console.log(
|
|
2051
|
+
console.log(
|
|
2052
|
+
` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`
|
|
2053
|
+
);
|
|
1753
2054
|
console.log(` Cooldown: ${Math.floor(config.cooldownMs / 6e4)}m`);
|
|
1754
2055
|
console.log(` State: ${config.stateFilePath}`);
|
|
1755
2056
|
console.log(` Hostname: ${hostname()}`);
|
|
@@ -1775,9 +2076,13 @@ agentflow watch started`);
|
|
|
1775
2076
|
if (pollCount % 10 === 0) {
|
|
1776
2077
|
const agentCount = Object.keys(state.agents).length;
|
|
1777
2078
|
const errorCount = Object.values(state.agents).filter((a) => a.lastStatus === "error").length;
|
|
1778
|
-
const runningCount = Object.values(state.agents).filter(
|
|
2079
|
+
const runningCount = Object.values(state.agents).filter(
|
|
2080
|
+
(a) => a.lastStatus === "running"
|
|
2081
|
+
).length;
|
|
1779
2082
|
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1780
|
-
console.log(
|
|
2083
|
+
console.log(
|
|
2084
|
+
`[${time}] heartbeat: ${agentCount} agents, ${runningCount} running, ${errorCount} errors, ${files.length} files`
|
|
2085
|
+
);
|
|
1781
2086
|
}
|
|
1782
2087
|
}
|
|
1783
2088
|
poll();
|
|
@@ -1794,10 +2099,6 @@ agentflow watch started`);
|
|
|
1794
2099
|
}
|
|
1795
2100
|
|
|
1796
2101
|
export {
|
|
1797
|
-
createGraphBuilder,
|
|
1798
|
-
loadGraph,
|
|
1799
|
-
graphToJson,
|
|
1800
|
-
runTraced,
|
|
1801
2102
|
getNode,
|
|
1802
2103
|
getChildren,
|
|
1803
2104
|
getParent,
|
|
@@ -1813,5 +2114,10 @@ export {
|
|
|
1813
2114
|
stitchTrace,
|
|
1814
2115
|
getTraceTree,
|
|
1815
2116
|
startLive,
|
|
2117
|
+
createGraphBuilder,
|
|
2118
|
+
runTraced,
|
|
2119
|
+
createTraceStore,
|
|
2120
|
+
toAsciiTree,
|
|
2121
|
+
toTimeline,
|
|
1816
2122
|
startWatch
|
|
1817
2123
|
};
|