agentflow-core 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VVWZJ63D.js → chunk-DHCTDCDI.js} +1613 -1305
- package/dist/chunk-DY7YHFIB.js +56 -0
- package/dist/cli.cjs +1190 -580
- package/dist/cli.js +217 -9
- package/dist/index.cjs +941 -419
- package/dist/index.d.cts +296 -132
- package/dist/index.d.ts +296 -132
- package/dist/index.js +173 -5
- package/dist/loader-LYRR6LMM.js +8 -0
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -1,241 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
for (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return obj;
|
|
22
|
-
}
|
|
23
|
-
Object.freeze(obj);
|
|
24
|
-
const record = obj;
|
|
25
|
-
for (const key of Object.keys(record)) {
|
|
26
|
-
const value = record[key];
|
|
27
|
-
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
28
|
-
deepFreeze(value);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return obj;
|
|
32
|
-
}
|
|
33
|
-
function createCounterIdGenerator() {
|
|
34
|
-
let counter = 0;
|
|
35
|
-
return () => {
|
|
36
|
-
counter++;
|
|
37
|
-
return `node_${String(counter).padStart(3, "0")}`;
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function createGraphBuilder(config) {
|
|
41
|
-
const generateId = config?.idGenerator ?? createCounterIdGenerator();
|
|
42
|
-
const agentId = config?.agentId ?? "unknown";
|
|
43
|
-
const trigger = config?.trigger ?? "manual";
|
|
44
|
-
const spanId = (0, import_crypto.randomUUID)();
|
|
45
|
-
const traceId = config?.traceId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_TRACE_ID : void 0) ?? (0, import_crypto.randomUUID)();
|
|
46
|
-
const parentSpanId = config?.parentSpanId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_PARENT_SPAN_ID : void 0) ?? null;
|
|
47
|
-
const graphId = generateId();
|
|
48
|
-
const startTime = Date.now();
|
|
49
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
50
|
-
const edges = [];
|
|
51
|
-
const events = [];
|
|
52
|
-
const parentStack = [];
|
|
53
|
-
let rootNodeId = null;
|
|
54
|
-
let built = false;
|
|
55
|
-
function assertNotBuilt() {
|
|
56
|
-
if (built) {
|
|
57
|
-
throw new Error("GraphBuilder: cannot mutate after build() has been called");
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function getNode(nodeId) {
|
|
61
|
-
const node = nodes.get(nodeId);
|
|
62
|
-
if (!node) {
|
|
63
|
-
throw new Error(`GraphBuilder: node "${nodeId}" does not exist`);
|
|
64
|
-
}
|
|
65
|
-
return node;
|
|
66
|
-
}
|
|
67
|
-
function recordEvent(nodeId, eventType, data = {}) {
|
|
68
|
-
events.push({
|
|
69
|
-
timestamp: Date.now(),
|
|
70
|
-
eventType,
|
|
71
|
-
nodeId,
|
|
72
|
-
data
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
function buildGraph() {
|
|
76
|
-
if (rootNodeId === null) {
|
|
77
|
-
throw new Error("GraphBuilder: cannot build a graph with no nodes");
|
|
78
|
-
}
|
|
79
|
-
let graphStatus = "completed";
|
|
80
|
-
for (const node of nodes.values()) {
|
|
81
|
-
if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
|
|
82
|
-
graphStatus = "failed";
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
if (node.status === "running") {
|
|
86
|
-
graphStatus = "running";
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
const endTime = graphStatus === "running" ? null : Date.now();
|
|
90
|
-
const frozenNodes = new Map(
|
|
91
|
-
[...nodes.entries()].map(([id, mNode]) => [
|
|
92
|
-
id,
|
|
93
|
-
{
|
|
94
|
-
id: mNode.id,
|
|
95
|
-
type: mNode.type,
|
|
96
|
-
name: mNode.name,
|
|
97
|
-
startTime: mNode.startTime,
|
|
98
|
-
endTime: mNode.endTime,
|
|
99
|
-
status: mNode.status,
|
|
100
|
-
parentId: mNode.parentId,
|
|
101
|
-
children: [...mNode.children],
|
|
102
|
-
metadata: { ...mNode.metadata },
|
|
103
|
-
state: { ...mNode.state }
|
|
104
|
-
}
|
|
105
|
-
])
|
|
106
|
-
);
|
|
107
|
-
const graph = {
|
|
108
|
-
id: graphId,
|
|
109
|
-
rootNodeId,
|
|
110
|
-
nodes: frozenNodes,
|
|
111
|
-
edges: [...edges],
|
|
112
|
-
startTime,
|
|
113
|
-
endTime,
|
|
114
|
-
status: graphStatus,
|
|
115
|
-
trigger,
|
|
116
|
-
agentId,
|
|
117
|
-
events: [...events],
|
|
118
|
-
traceId,
|
|
119
|
-
spanId,
|
|
120
|
-
parentSpanId
|
|
121
|
-
};
|
|
122
|
-
return deepFreeze(graph);
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
123
21
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const parentId = opts.parentId ?? parentStack[parentStack.length - 1] ?? null;
|
|
135
|
-
if (parentId !== null && !nodes.has(parentId)) {
|
|
136
|
-
throw new Error(`GraphBuilder: parent node "${parentId}" does not exist`);
|
|
137
|
-
}
|
|
138
|
-
const node = {
|
|
139
|
-
id,
|
|
140
|
-
type: opts.type,
|
|
141
|
-
name: opts.name,
|
|
142
|
-
startTime: Date.now(),
|
|
143
|
-
endTime: null,
|
|
144
|
-
status: "running",
|
|
145
|
-
parentId,
|
|
146
|
-
children: [],
|
|
147
|
-
metadata: opts.metadata ? { ...opts.metadata } : {},
|
|
148
|
-
state: {}
|
|
149
|
-
};
|
|
150
|
-
nodes.set(id, node);
|
|
151
|
-
if (parentId !== null) {
|
|
152
|
-
const parent = nodes.get(parentId);
|
|
153
|
-
if (parent) {
|
|
154
|
-
parent.children.push(id);
|
|
155
|
-
}
|
|
156
|
-
edges.push({ from: parentId, to: id, type: "spawned" });
|
|
157
|
-
}
|
|
158
|
-
if (rootNodeId === null) {
|
|
159
|
-
rootNodeId = id;
|
|
160
|
-
}
|
|
161
|
-
recordEvent(id, "agent_start", { type: opts.type, name: opts.name });
|
|
162
|
-
return id;
|
|
163
|
-
},
|
|
164
|
-
endNode(nodeId, status = "completed") {
|
|
165
|
-
assertNotBuilt();
|
|
166
|
-
const node = getNode(nodeId);
|
|
167
|
-
if (node.endTime !== null) {
|
|
168
|
-
throw new Error(
|
|
169
|
-
`GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
node.endTime = Date.now();
|
|
173
|
-
node.status = status;
|
|
174
|
-
recordEvent(nodeId, "agent_end", { status });
|
|
175
|
-
},
|
|
176
|
-
failNode(nodeId, error) {
|
|
177
|
-
assertNotBuilt();
|
|
178
|
-
const node = getNode(nodeId);
|
|
179
|
-
if (node.endTime !== null) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
`GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
const errorMessage = error instanceof Error ? error.message : error;
|
|
185
|
-
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
186
|
-
node.endTime = Date.now();
|
|
187
|
-
node.status = "failed";
|
|
188
|
-
node.metadata.error = errorMessage;
|
|
189
|
-
if (errorStack) {
|
|
190
|
-
node.metadata.errorStack = errorStack;
|
|
191
|
-
}
|
|
192
|
-
recordEvent(nodeId, "tool_error", { error: errorMessage });
|
|
193
|
-
},
|
|
194
|
-
addEdge(from, to, type) {
|
|
195
|
-
assertNotBuilt();
|
|
196
|
-
getNode(from);
|
|
197
|
-
getNode(to);
|
|
198
|
-
edges.push({ from, to, type });
|
|
199
|
-
recordEvent(from, "custom", { to, type, action: "edge_add" });
|
|
200
|
-
},
|
|
201
|
-
pushEvent(event) {
|
|
202
|
-
assertNotBuilt();
|
|
203
|
-
getNode(event.nodeId);
|
|
204
|
-
events.push({
|
|
205
|
-
...event,
|
|
206
|
-
timestamp: Date.now()
|
|
207
|
-
});
|
|
208
|
-
},
|
|
209
|
-
updateState(nodeId, state) {
|
|
210
|
-
assertNotBuilt();
|
|
211
|
-
const node = getNode(nodeId);
|
|
212
|
-
Object.assign(node.state, state);
|
|
213
|
-
recordEvent(nodeId, "custom", { action: "state_update", ...state });
|
|
214
|
-
},
|
|
215
|
-
withParent(parentId, fn) {
|
|
216
|
-
assertNotBuilt();
|
|
217
|
-
getNode(parentId);
|
|
218
|
-
parentStack.push(parentId);
|
|
219
|
-
try {
|
|
220
|
-
return fn();
|
|
221
|
-
} finally {
|
|
222
|
-
parentStack.pop();
|
|
223
|
-
}
|
|
224
|
-
},
|
|
225
|
-
getSnapshot() {
|
|
226
|
-
return buildGraph();
|
|
227
|
-
},
|
|
228
|
-
build() {
|
|
229
|
-
assertNotBuilt();
|
|
230
|
-
const graph = buildGraph();
|
|
231
|
-
built = true;
|
|
232
|
-
return graph;
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
return builder;
|
|
236
|
-
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
237
32
|
|
|
238
33
|
// src/loader.ts
|
|
34
|
+
var loader_exports = {};
|
|
35
|
+
__export(loader_exports, {
|
|
36
|
+
graphToJson: () => graphToJson,
|
|
37
|
+
loadGraph: () => loadGraph
|
|
38
|
+
});
|
|
239
39
|
function toNodesMap(raw) {
|
|
240
40
|
if (raw instanceof Map) return raw;
|
|
241
41
|
if (Array.isArray(raw)) {
|
|
@@ -286,160 +86,30 @@ function graphToJson(graph) {
|
|
|
286
86
|
parentSpanId: graph.parentSpanId
|
|
287
87
|
};
|
|
288
88
|
}
|
|
89
|
+
var init_loader = __esm({
|
|
90
|
+
"src/loader.ts"() {
|
|
91
|
+
"use strict";
|
|
92
|
+
}
|
|
93
|
+
});
|
|
289
94
|
|
|
290
|
-
// src/
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
95
|
+
// src/cli.ts
|
|
96
|
+
var import_path3 = require("path");
|
|
97
|
+
|
|
98
|
+
// src/live.ts
|
|
99
|
+
var import_node_fs = require("fs");
|
|
100
|
+
var import_node_path = require("path");
|
|
101
|
+
|
|
102
|
+
// src/graph-query.ts
|
|
103
|
+
function getChildren(graph, nodeId) {
|
|
104
|
+
const node = graph.nodes.get(nodeId);
|
|
105
|
+
if (!node) return [];
|
|
106
|
+
const result = [];
|
|
107
|
+
for (const childId of node.children) {
|
|
108
|
+
const child = graph.nodes.get(childId);
|
|
109
|
+
if (child) result.push(child);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
294
112
|
}
|
|
295
|
-
function snapshotDir(dir, patterns) {
|
|
296
|
-
const result = /* @__PURE__ */ new Map();
|
|
297
|
-
if (!(0, import_node_fs.existsSync)(dir)) return result;
|
|
298
|
-
for (const entry of (0, import_node_fs.readdirSync)(dir)) {
|
|
299
|
-
if (!patterns.some((re) => re.test(entry))) continue;
|
|
300
|
-
const full = (0, import_node_path.join)(dir, entry);
|
|
301
|
-
try {
|
|
302
|
-
const stat = (0, import_node_fs.statSync)(full);
|
|
303
|
-
if (stat.isFile()) {
|
|
304
|
-
result.set(full, stat.mtimeMs);
|
|
305
|
-
}
|
|
306
|
-
} catch {
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return result;
|
|
310
|
-
}
|
|
311
|
-
function agentIdFromFilename(filePath) {
|
|
312
|
-
const base = (0, import_node_path.basename)(filePath, ".json");
|
|
313
|
-
const cleaned = base.replace(/-state$/, "");
|
|
314
|
-
return `alfred-${cleaned}`;
|
|
315
|
-
}
|
|
316
|
-
function deriveAgentId(command) {
|
|
317
|
-
return "orchestrator";
|
|
318
|
-
}
|
|
319
|
-
function fileTimestamp() {
|
|
320
|
-
return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
|
|
321
|
-
}
|
|
322
|
-
async function runTraced(config) {
|
|
323
|
-
const {
|
|
324
|
-
command,
|
|
325
|
-
agentId = deriveAgentId(command),
|
|
326
|
-
trigger = "cli",
|
|
327
|
-
tracesDir = "./traces",
|
|
328
|
-
watchDirs = [],
|
|
329
|
-
watchPatterns = ["*.json"]
|
|
330
|
-
} = config;
|
|
331
|
-
if (command.length === 0) {
|
|
332
|
-
throw new Error("runTraced: command must not be empty");
|
|
333
|
-
}
|
|
334
|
-
const resolvedTracesDir = (0, import_node_path.resolve)(tracesDir);
|
|
335
|
-
const patterns = watchPatterns.map(globToRegex);
|
|
336
|
-
const orchestrator = createGraphBuilder({ agentId, trigger });
|
|
337
|
-
const { traceId, spanId } = orchestrator.traceContext;
|
|
338
|
-
const beforeSnapshots = /* @__PURE__ */ new Map();
|
|
339
|
-
for (const dir of watchDirs) {
|
|
340
|
-
beforeSnapshots.set(dir, snapshotDir(dir, patterns));
|
|
341
|
-
}
|
|
342
|
-
const rootId = orchestrator.startNode({ type: "agent", name: agentId });
|
|
343
|
-
const dispatchId = orchestrator.startNode({
|
|
344
|
-
type: "tool",
|
|
345
|
-
name: "dispatch-command",
|
|
346
|
-
parentId: rootId
|
|
347
|
-
});
|
|
348
|
-
orchestrator.updateState(dispatchId, { command: command.join(" ") });
|
|
349
|
-
const monitorId = orchestrator.startNode({
|
|
350
|
-
type: "tool",
|
|
351
|
-
name: "state-monitor",
|
|
352
|
-
parentId: rootId
|
|
353
|
-
});
|
|
354
|
-
orchestrator.updateState(monitorId, {
|
|
355
|
-
watchDirs,
|
|
356
|
-
watchPatterns
|
|
357
|
-
});
|
|
358
|
-
const startMs = Date.now();
|
|
359
|
-
const execCmd = command[0] ?? "";
|
|
360
|
-
const execArgs = command.slice(1);
|
|
361
|
-
process.env.AGENTFLOW_TRACE_ID = traceId;
|
|
362
|
-
process.env.AGENTFLOW_PARENT_SPAN_ID = spanId;
|
|
363
|
-
const result = (0, import_node_child_process.spawnSync)(execCmd, execArgs, { stdio: "inherit" });
|
|
364
|
-
delete process.env.AGENTFLOW_TRACE_ID;
|
|
365
|
-
delete process.env.AGENTFLOW_PARENT_SPAN_ID;
|
|
366
|
-
const exitCode = result.status ?? 1;
|
|
367
|
-
const duration = (Date.now() - startMs) / 1e3;
|
|
368
|
-
const stateChanges = [];
|
|
369
|
-
for (const dir of watchDirs) {
|
|
370
|
-
const before = beforeSnapshots.get(dir) ?? /* @__PURE__ */ new Map();
|
|
371
|
-
const after = snapshotDir(dir, patterns);
|
|
372
|
-
for (const [filePath, mtime] of after) {
|
|
373
|
-
const prevMtime = before.get(filePath);
|
|
374
|
-
if (prevMtime === void 0 || mtime > prevMtime) {
|
|
375
|
-
stateChanges.push(filePath);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
orchestrator.updateState(monitorId, { stateChanges });
|
|
380
|
-
orchestrator.endNode(monitorId);
|
|
381
|
-
if (exitCode === 0) {
|
|
382
|
-
orchestrator.endNode(dispatchId);
|
|
383
|
-
} else {
|
|
384
|
-
orchestrator.failNode(dispatchId, `Command exited with code ${exitCode}`);
|
|
385
|
-
}
|
|
386
|
-
orchestrator.updateState(rootId, {
|
|
387
|
-
exitCode,
|
|
388
|
-
duration,
|
|
389
|
-
stateChanges
|
|
390
|
-
});
|
|
391
|
-
if (exitCode === 0) {
|
|
392
|
-
orchestrator.endNode(rootId);
|
|
393
|
-
} else {
|
|
394
|
-
orchestrator.failNode(rootId, `Command exited with code ${exitCode}`);
|
|
395
|
-
}
|
|
396
|
-
const orchestratorGraph = orchestrator.build();
|
|
397
|
-
const allGraphs = [orchestratorGraph];
|
|
398
|
-
for (const filePath of stateChanges) {
|
|
399
|
-
const childAgentId = agentIdFromFilename(filePath);
|
|
400
|
-
const childBuilder = createGraphBuilder({
|
|
401
|
-
agentId: childAgentId,
|
|
402
|
-
trigger: "state-change",
|
|
403
|
-
traceId,
|
|
404
|
-
parentSpanId: spanId
|
|
405
|
-
});
|
|
406
|
-
const childRootId = childBuilder.startNode({
|
|
407
|
-
type: "agent",
|
|
408
|
-
name: childAgentId
|
|
409
|
-
});
|
|
410
|
-
childBuilder.updateState(childRootId, {
|
|
411
|
-
stateFile: filePath,
|
|
412
|
-
detectedBy: "runner-state-monitor"
|
|
413
|
-
});
|
|
414
|
-
childBuilder.endNode(childRootId);
|
|
415
|
-
allGraphs.push(childBuilder.build());
|
|
416
|
-
}
|
|
417
|
-
if (!(0, import_node_fs.existsSync)(resolvedTracesDir)) {
|
|
418
|
-
(0, import_node_fs.mkdirSync)(resolvedTracesDir, { recursive: true });
|
|
419
|
-
}
|
|
420
|
-
const ts = fileTimestamp();
|
|
421
|
-
const tracePaths = [];
|
|
422
|
-
for (const graph of allGraphs) {
|
|
423
|
-
const filename = `${graph.agentId}-${ts}.json`;
|
|
424
|
-
const outPath = (0, import_node_path.join)(resolvedTracesDir, filename);
|
|
425
|
-
(0, import_node_fs.writeFileSync)(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
|
|
426
|
-
tracePaths.push(outPath);
|
|
427
|
-
}
|
|
428
|
-
return {
|
|
429
|
-
exitCode,
|
|
430
|
-
traceId,
|
|
431
|
-
spanId,
|
|
432
|
-
tracePaths,
|
|
433
|
-
stateChanges,
|
|
434
|
-
duration
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// src/live.ts
|
|
439
|
-
var import_node_fs2 = require("fs");
|
|
440
|
-
var import_node_path2 = require("path");
|
|
441
|
-
|
|
442
|
-
// src/graph-query.ts
|
|
443
113
|
function getFailures(graph) {
|
|
444
114
|
const failureStatuses = /* @__PURE__ */ new Set(["failed", "hung", "timeout"]);
|
|
445
115
|
return [...graph.nodes.values()].filter((node) => failureStatuses.has(node.status));
|
|
@@ -573,6 +243,7 @@ function getTraceTree(trace) {
|
|
|
573
243
|
}
|
|
574
244
|
|
|
575
245
|
// src/live.ts
|
|
246
|
+
init_loader();
|
|
576
247
|
var C = {
|
|
577
248
|
reset: "\x1B[0m",
|
|
578
249
|
bold: "\x1B[1m",
|
|
@@ -605,17 +276,18 @@ function parseArgs(argv) {
|
|
|
605
276
|
config.recursive = true;
|
|
606
277
|
i++;
|
|
607
278
|
} else if (!arg.startsWith("-")) {
|
|
608
|
-
config.dirs.push((0,
|
|
279
|
+
config.dirs.push((0, import_node_path.resolve)(arg));
|
|
609
280
|
i++;
|
|
610
281
|
} else {
|
|
611
282
|
i++;
|
|
612
283
|
}
|
|
613
284
|
}
|
|
614
|
-
if (config.dirs.length === 0) config.dirs.push((0,
|
|
285
|
+
if (config.dirs.length === 0) config.dirs.push((0, import_node_path.resolve)("."));
|
|
615
286
|
return config;
|
|
616
287
|
}
|
|
617
288
|
function printUsage() {
|
|
618
|
-
console.log(
|
|
289
|
+
console.log(
|
|
290
|
+
`
|
|
619
291
|
AgentFlow Live Monitor \u2014 real-time terminal dashboard for agent systems.
|
|
620
292
|
|
|
621
293
|
Auto-detects agent traces, state files, job schedulers, and session logs
|
|
@@ -636,20 +308,21 @@ Examples:
|
|
|
636
308
|
agentflow live ./data
|
|
637
309
|
agentflow live ./traces ./cron ./workers --refresh 5
|
|
638
310
|
agentflow live /var/lib/myagent -R
|
|
639
|
-
`.trim()
|
|
311
|
+
`.trim()
|
|
312
|
+
);
|
|
640
313
|
}
|
|
641
314
|
function scanFiles(dirs, recursive) {
|
|
642
315
|
const results = [];
|
|
643
316
|
const seen = /* @__PURE__ */ new Set();
|
|
644
317
|
function scanDir(d, topLevel) {
|
|
645
318
|
try {
|
|
646
|
-
for (const f of (0,
|
|
319
|
+
for (const f of (0, import_node_fs.readdirSync)(d)) {
|
|
647
320
|
if (f.startsWith(".")) continue;
|
|
648
|
-
const fp = (0,
|
|
321
|
+
const fp = (0, import_node_path.join)(d, f);
|
|
649
322
|
if (seen.has(fp)) continue;
|
|
650
323
|
let stat;
|
|
651
324
|
try {
|
|
652
|
-
stat = (0,
|
|
325
|
+
stat = (0, import_node_fs.statSync)(fp);
|
|
653
326
|
} catch {
|
|
654
327
|
continue;
|
|
655
328
|
}
|
|
@@ -675,20 +348,22 @@ function scanFiles(dirs, recursive) {
|
|
|
675
348
|
}
|
|
676
349
|
function safeReadJson(fp) {
|
|
677
350
|
try {
|
|
678
|
-
return JSON.parse((0,
|
|
351
|
+
return JSON.parse((0, import_node_fs.readFileSync)(fp, "utf8"));
|
|
679
352
|
} catch {
|
|
680
353
|
return null;
|
|
681
354
|
}
|
|
682
355
|
}
|
|
683
356
|
function nameFromFile(filename) {
|
|
684
|
-
return (0,
|
|
357
|
+
return (0, import_node_path.basename)(filename).replace(/\.(json|jsonl)$/, "").replace(/-state$/, "");
|
|
685
358
|
}
|
|
686
359
|
function normalizeStatus(val) {
|
|
687
360
|
if (typeof val !== "string") return "unknown";
|
|
688
361
|
const s = val.toLowerCase();
|
|
689
362
|
if (["ok", "success", "completed", "done", "passed", "healthy", "good"].includes(s)) return "ok";
|
|
690
|
-
if (["error", "failed", "failure", "crashed", "unhealthy", "bad", "timeout"].includes(s))
|
|
691
|
-
|
|
363
|
+
if (["error", "failed", "failure", "crashed", "unhealthy", "bad", "timeout"].includes(s))
|
|
364
|
+
return "error";
|
|
365
|
+
if (["running", "active", "in_progress", "started", "pending", "processing"].includes(s))
|
|
366
|
+
return "running";
|
|
692
367
|
return "unknown";
|
|
693
368
|
}
|
|
694
369
|
function findStatus(obj) {
|
|
@@ -704,7 +379,17 @@ function findStatus(obj) {
|
|
|
704
379
|
return "unknown";
|
|
705
380
|
}
|
|
706
381
|
function findTimestamp(obj) {
|
|
707
|
-
for (const key of [
|
|
382
|
+
for (const key of [
|
|
383
|
+
"ts",
|
|
384
|
+
"timestamp",
|
|
385
|
+
"lastRunAtMs",
|
|
386
|
+
"last_run",
|
|
387
|
+
"lastExecution",
|
|
388
|
+
"updated_at",
|
|
389
|
+
"started_at",
|
|
390
|
+
"endTime",
|
|
391
|
+
"startTime"
|
|
392
|
+
]) {
|
|
708
393
|
const val = obj[key];
|
|
709
394
|
if (typeof val === "number") return val > 1e12 ? val : val * 1e3;
|
|
710
395
|
if (typeof val === "string") {
|
|
@@ -716,7 +401,16 @@ function findTimestamp(obj) {
|
|
|
716
401
|
}
|
|
717
402
|
function extractDetail(obj) {
|
|
718
403
|
const parts = [];
|
|
719
|
-
for (const key of [
|
|
404
|
+
for (const key of [
|
|
405
|
+
"summary",
|
|
406
|
+
"message",
|
|
407
|
+
"description",
|
|
408
|
+
"lastError",
|
|
409
|
+
"error",
|
|
410
|
+
"name",
|
|
411
|
+
"jobId",
|
|
412
|
+
"id"
|
|
413
|
+
]) {
|
|
720
414
|
const val = obj[key];
|
|
721
415
|
if (typeof val === "string" && val.length > 0 && val.length < 200) {
|
|
722
416
|
parts.push(val.slice(0, 80));
|
|
@@ -776,7 +470,14 @@ function processJsonFile(file) {
|
|
|
776
470
|
const status2 = findStatus(state);
|
|
777
471
|
const ts2 = findTimestamp(state) || file.mtime;
|
|
778
472
|
const detail2 = extractDetail(state);
|
|
779
|
-
records.push({
|
|
473
|
+
records.push({
|
|
474
|
+
id: String(name),
|
|
475
|
+
source: "jobs",
|
|
476
|
+
status: status2,
|
|
477
|
+
lastActive: ts2,
|
|
478
|
+
detail: detail2,
|
|
479
|
+
file: file.filename
|
|
480
|
+
});
|
|
780
481
|
}
|
|
781
482
|
return records;
|
|
782
483
|
}
|
|
@@ -791,7 +492,14 @@ function processJsonFile(file) {
|
|
|
791
492
|
const ts2 = findTimestamp(w) || findTimestamp(obj) || file.mtime;
|
|
792
493
|
const pid = w.pid;
|
|
793
494
|
const detail2 = pid ? `pid: ${pid}` : extractDetail(w);
|
|
794
|
-
records.push({
|
|
495
|
+
records.push({
|
|
496
|
+
id: name,
|
|
497
|
+
source: "workers",
|
|
498
|
+
status: status2,
|
|
499
|
+
lastActive: ts2,
|
|
500
|
+
detail: detail2,
|
|
501
|
+
file: file.filename
|
|
502
|
+
});
|
|
795
503
|
}
|
|
796
504
|
return records;
|
|
797
505
|
}
|
|
@@ -799,12 +507,19 @@ function processJsonFile(file) {
|
|
|
799
507
|
const status = findStatus(obj);
|
|
800
508
|
const ts = findTimestamp(obj) || file.mtime;
|
|
801
509
|
const detail = extractDetail(obj);
|
|
802
|
-
records.push({
|
|
510
|
+
records.push({
|
|
511
|
+
id: nameFromFile(file.filename),
|
|
512
|
+
source: "state",
|
|
513
|
+
status,
|
|
514
|
+
lastActive: ts,
|
|
515
|
+
detail,
|
|
516
|
+
file: file.filename
|
|
517
|
+
});
|
|
803
518
|
return records;
|
|
804
519
|
}
|
|
805
520
|
function processJsonlFile(file) {
|
|
806
521
|
try {
|
|
807
|
-
const content = (0,
|
|
522
|
+
const content = (0, import_node_fs.readFileSync)(file.path, "utf8").trim();
|
|
808
523
|
if (!content) return [];
|
|
809
524
|
const lines = content.split("\n");
|
|
810
525
|
const lineCount = lines.length;
|
|
@@ -815,13 +530,22 @@ function processJsonlFile(file) {
|
|
|
815
530
|
const ts2 = findTimestamp(lastObj) || file.mtime;
|
|
816
531
|
const action = lastObj.action;
|
|
817
532
|
const detail2 = action ? `${action} (${lineCount} entries)` : `${lineCount} entries`;
|
|
818
|
-
return [
|
|
533
|
+
return [
|
|
534
|
+
{
|
|
535
|
+
id: String(name),
|
|
536
|
+
source: "session",
|
|
537
|
+
status: status2,
|
|
538
|
+
lastActive: ts2,
|
|
539
|
+
detail: detail2,
|
|
540
|
+
file: file.filename
|
|
541
|
+
}
|
|
542
|
+
];
|
|
819
543
|
}
|
|
820
544
|
const tail = lines.slice(Math.max(0, lineCount - 30));
|
|
821
545
|
let model = "";
|
|
822
546
|
let totalTokens = 0;
|
|
823
547
|
let totalCost = 0;
|
|
824
|
-
|
|
548
|
+
const toolCalls = [];
|
|
825
549
|
let lastUserMsg = "";
|
|
826
550
|
let lastAssistantMsg = "";
|
|
827
551
|
let errorCount = 0;
|
|
@@ -920,7 +644,16 @@ function processJsonlFile(file) {
|
|
|
920
644
|
const status = errorCount > lineCount / 4 ? "error" : lastRole === "assistant" ? "ok" : "running";
|
|
921
645
|
const ts = findTimestamp(lastObj) || file.mtime;
|
|
922
646
|
const sessionName = sessionId ? sessionId.slice(0, 8) : nameFromFile(file.filename);
|
|
923
|
-
return [
|
|
647
|
+
return [
|
|
648
|
+
{
|
|
649
|
+
id: String(name !== "unknown" ? name : sessionName),
|
|
650
|
+
source: "session",
|
|
651
|
+
status,
|
|
652
|
+
lastActive: ts,
|
|
653
|
+
detail,
|
|
654
|
+
file: file.filename
|
|
655
|
+
}
|
|
656
|
+
];
|
|
924
657
|
} catch {
|
|
925
658
|
return [];
|
|
926
659
|
}
|
|
@@ -1052,7 +785,8 @@ function render(config) {
|
|
|
1052
785
|
if (g.fail > 0 && g.ok === 0 && g.running === 0) return `${C.red}error${C.reset}`;
|
|
1053
786
|
if (g.running > 0) return `${C.green}running${C.reset}`;
|
|
1054
787
|
if (g.fail > 0) return `${C.yellow}${g.ok}ok/${g.fail}err${C.reset}`;
|
|
1055
|
-
if (g.ok > 0)
|
|
788
|
+
if (g.ok > 0)
|
|
789
|
+
return g.total > 1 ? `${C.green}${g.ok}/${g.total} ok${C.reset}` : `${C.green}ok${C.reset}`;
|
|
1056
790
|
return `${C.dim}idle${C.reset}`;
|
|
1057
791
|
}
|
|
1058
792
|
function sourceTag(s) {
|
|
@@ -1076,24 +810,38 @@ function render(config) {
|
|
|
1076
810
|
function truncate(s, max) {
|
|
1077
811
|
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
1078
812
|
}
|
|
813
|
+
const termWidth = process.stdout.columns || 120;
|
|
814
|
+
const detailWidth = Math.max(20, termWidth - 60);
|
|
1079
815
|
if (firstRender) {
|
|
1080
816
|
process.stdout.write("\x1B[2J");
|
|
1081
817
|
firstRender = false;
|
|
1082
818
|
}
|
|
1083
819
|
const L = [];
|
|
1084
820
|
writeLine(L, `${C.bold}${C.cyan}\u2554${"\u2550".repeat(70)}\u2557${C.reset}`);
|
|
1085
|
-
writeLine(
|
|
821
|
+
writeLine(
|
|
822
|
+
L,
|
|
823
|
+
`${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}`
|
|
824
|
+
);
|
|
1086
825
|
const metaLine = `Refresh: ${config.refreshMs / 1e3}s \xB7 Up: ${upStr} \xB7 Files: ${files.length}`;
|
|
1087
826
|
const pad1 = Math.max(0, 64 - metaLine.length);
|
|
1088
|
-
writeLine(
|
|
827
|
+
writeLine(
|
|
828
|
+
L,
|
|
829
|
+
`${C.bold}${C.cyan}\u2551${C.reset} ${C.dim}${metaLine}${C.reset}${" ".repeat(pad1)}${C.bold}${C.cyan}\u2551${C.reset}`
|
|
830
|
+
);
|
|
1089
831
|
writeLine(L, `${C.bold}${C.cyan}\u255A${"\u2550".repeat(70)}\u255D${C.reset}`);
|
|
1090
832
|
const sc = totFail === 0 ? C.green : C.yellow;
|
|
1091
833
|
writeLine(L, "");
|
|
1092
|
-
writeLine(
|
|
834
|
+
writeLine(
|
|
835
|
+
L,
|
|
836
|
+
` ${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}`
|
|
837
|
+
);
|
|
1093
838
|
writeLine(L, "");
|
|
1094
839
|
writeLine(L, ` ${C.bold}Activity (1h)${C.reset} ${spark} ${C.dim}\u2190 now${C.reset}`);
|
|
1095
840
|
writeLine(L, "");
|
|
1096
|
-
writeLine(
|
|
841
|
+
writeLine(
|
|
842
|
+
L,
|
|
843
|
+
` ${C.bold}${C.under}Agent Status Last Active Detail${C.reset}`
|
|
844
|
+
);
|
|
1097
845
|
let lineCount = 0;
|
|
1098
846
|
for (const g of groups) {
|
|
1099
847
|
if (lineCount > 35) break;
|
|
@@ -1103,14 +851,20 @@ function render(config) {
|
|
|
1103
851
|
if (g.children.length === 0) {
|
|
1104
852
|
const name = truncate(g.name, 26).padEnd(26);
|
|
1105
853
|
const st = statusText(g);
|
|
1106
|
-
const det = truncate(g.detail,
|
|
1107
|
-
writeLine(
|
|
854
|
+
const det = truncate(g.detail, detailWidth);
|
|
855
|
+
writeLine(
|
|
856
|
+
L,
|
|
857
|
+
` ${icon} ${name} ${st.padEnd(20)} ${active.padEnd(20)} ${C.dim}${det}${C.reset}`
|
|
858
|
+
);
|
|
1108
859
|
lineCount++;
|
|
1109
860
|
} else {
|
|
1110
861
|
const name = truncate(g.name, 24).padEnd(24);
|
|
1111
862
|
const st = statusText(g);
|
|
1112
863
|
const tag = sourceTag(g.source);
|
|
1113
|
-
writeLine(
|
|
864
|
+
writeLine(
|
|
865
|
+
L,
|
|
866
|
+
` ${icon} ${C.bold}${name}${C.reset} ${st.padEnd(20)} ${active.padEnd(20)} ${tag} ${C.dim}(${g.children.length} agents)${C.reset}`
|
|
867
|
+
);
|
|
1114
868
|
lineCount++;
|
|
1115
869
|
const kids = g.children.slice(0, 12);
|
|
1116
870
|
for (let i = 0; i < kids.length; i++) {
|
|
@@ -1121,8 +875,11 @@ function render(config) {
|
|
|
1121
875
|
const cIcon = statusIcon(child.status, Date.now() - child.lastActive < 3e5);
|
|
1122
876
|
const cName = truncate(child.id, 22).padEnd(22);
|
|
1123
877
|
const cActive = `${C.dim}${timeStr(child.lastActive)}${C.reset}`;
|
|
1124
|
-
const cDet = truncate(child.detail,
|
|
1125
|
-
writeLine(
|
|
878
|
+
const cDet = truncate(child.detail, detailWidth - 5);
|
|
879
|
+
writeLine(
|
|
880
|
+
L,
|
|
881
|
+
` ${C.dim}${connector}${C.reset} ${cIcon} ${cName} ${cActive.padEnd(20)} ${C.dim}${cDet}${C.reset}`
|
|
882
|
+
);
|
|
1126
883
|
lineCount++;
|
|
1127
884
|
}
|
|
1128
885
|
if (g.children.length > 12) {
|
|
@@ -1139,7 +896,10 @@ function render(config) {
|
|
|
1139
896
|
const si = dt.status === "completed" ? `${C.green}\u2713${C.reset}` : dt.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
1140
897
|
const dur = dt.endTime ? `${dt.endTime - dt.startTime}ms` : "running";
|
|
1141
898
|
const tid = dt.traceId.slice(0, 8);
|
|
1142
|
-
writeLine(
|
|
899
|
+
writeLine(
|
|
900
|
+
L,
|
|
901
|
+
` ${si} ${C.magenta}trace:${tid}${C.reset} ${C.dim}${traceTime} ${dur} (${dt.graphs.size} agents)${C.reset}`
|
|
902
|
+
);
|
|
1143
903
|
const tree = getTraceTree(dt);
|
|
1144
904
|
for (let i = 0; i < Math.min(tree.length, 6); i++) {
|
|
1145
905
|
const tg = tree[i];
|
|
@@ -1149,7 +909,10 @@ function render(config) {
|
|
|
1149
909
|
const conn = depth === 0 ? " " : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1150
910
|
const gs = tg.status === "completed" ? `${C.green}\u2713${C.reset}` : tg.status === "failed" ? `${C.red}\u2717${C.reset}` : `${C.yellow}\u23F3${C.reset}`;
|
|
1151
911
|
const gd = tg.endTime ? `${tg.endTime - tg.startTime}ms` : "running";
|
|
1152
|
-
writeLine(
|
|
912
|
+
writeLine(
|
|
913
|
+
L,
|
|
914
|
+
`${indent}${conn}${gs} ${C.bold}${tg.agentId}${C.reset} ${C.dim}[${tg.trigger}] ${gd}${C.reset}`
|
|
915
|
+
);
|
|
1153
916
|
}
|
|
1154
917
|
}
|
|
1155
918
|
}
|
|
@@ -1163,8 +926,11 @@ function render(config) {
|
|
|
1163
926
|
const agent = truncate(r.id, 26).padEnd(26);
|
|
1164
927
|
const age = Math.floor((Date.now() - r.lastActive) / 1e3);
|
|
1165
928
|
const ageStr = age < 60 ? age + "s ago" : age < 3600 ? Math.floor(age / 60) + "m ago" : Math.floor(age / 3600) + "h ago";
|
|
1166
|
-
const det = truncate(r.detail,
|
|
1167
|
-
writeLine(
|
|
929
|
+
const det = truncate(r.detail, detailWidth);
|
|
930
|
+
writeLine(
|
|
931
|
+
L,
|
|
932
|
+
` ${icon} ${agent} ${C.dim}${t} ${ageStr.padStart(8)}${C.reset} ${C.dim}${det}${C.reset}`
|
|
933
|
+
);
|
|
1168
934
|
}
|
|
1169
935
|
}
|
|
1170
936
|
if (files.length === 0) {
|
|
@@ -1186,13 +952,13 @@ function getDistDepth(dt, spanId) {
|
|
|
1186
952
|
}
|
|
1187
953
|
function startLive(argv) {
|
|
1188
954
|
const config = parseArgs(argv);
|
|
1189
|
-
const valid = config.dirs.filter((d) => (0,
|
|
955
|
+
const valid = config.dirs.filter((d) => (0, import_node_fs.existsSync)(d));
|
|
1190
956
|
if (valid.length === 0) {
|
|
1191
957
|
console.error(`No valid directories found: ${config.dirs.join(", ")}`);
|
|
1192
958
|
console.error("Specify directories containing JSON/JSONL files: agentflow live <dir> [dir...]");
|
|
1193
959
|
process.exit(1);
|
|
1194
960
|
}
|
|
1195
|
-
const invalid = config.dirs.filter((d) => !(0,
|
|
961
|
+
const invalid = config.dirs.filter((d) => !(0, import_node_fs.existsSync)(d));
|
|
1196
962
|
if (invalid.length > 0) {
|
|
1197
963
|
console.warn(`Skipping non-existent: ${invalid.join(", ")}`);
|
|
1198
964
|
}
|
|
@@ -1201,24 +967,947 @@ function startLive(argv) {
|
|
|
1201
967
|
let debounce = null;
|
|
1202
968
|
for (const dir of config.dirs) {
|
|
1203
969
|
try {
|
|
1204
|
-
(0,
|
|
970
|
+
(0, import_node_fs.watch)(dir, { recursive: config.recursive }, () => {
|
|
1205
971
|
if (debounce) clearTimeout(debounce);
|
|
1206
972
|
debounce = setTimeout(() => render(config), 500);
|
|
1207
973
|
});
|
|
1208
974
|
} catch {
|
|
1209
975
|
}
|
|
1210
976
|
}
|
|
1211
|
-
setInterval(() => render(config), config.refreshMs);
|
|
1212
|
-
process.on("SIGINT", () => {
|
|
1213
|
-
console.log("\n" + C.dim + "Monitor stopped." + C.reset);
|
|
1214
|
-
process.exit(0);
|
|
1215
|
-
});
|
|
977
|
+
setInterval(() => render(config), config.refreshMs);
|
|
978
|
+
process.on("SIGINT", () => {
|
|
979
|
+
console.log("\n" + C.dim + "Monitor stopped." + C.reset);
|
|
980
|
+
process.exit(0);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// src/runner.ts
|
|
985
|
+
var import_node_child_process = require("child_process");
|
|
986
|
+
var import_node_fs2 = require("fs");
|
|
987
|
+
var import_node_path2 = require("path");
|
|
988
|
+
|
|
989
|
+
// src/graph-builder.ts
|
|
990
|
+
var import_crypto = require("crypto");
|
|
991
|
+
function deepFreeze(obj) {
|
|
992
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
993
|
+
if (obj instanceof Map) {
|
|
994
|
+
Object.freeze(obj);
|
|
995
|
+
for (const value of obj.values()) {
|
|
996
|
+
deepFreeze(value);
|
|
997
|
+
}
|
|
998
|
+
return obj;
|
|
999
|
+
}
|
|
1000
|
+
Object.freeze(obj);
|
|
1001
|
+
const record = obj;
|
|
1002
|
+
for (const key of Object.keys(record)) {
|
|
1003
|
+
const value = record[key];
|
|
1004
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
1005
|
+
deepFreeze(value);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return obj;
|
|
1009
|
+
}
|
|
1010
|
+
function createCounterIdGenerator() {
|
|
1011
|
+
let counter = 0;
|
|
1012
|
+
return () => {
|
|
1013
|
+
counter++;
|
|
1014
|
+
return `node_${String(counter).padStart(3, "0")}`;
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
function createGraphBuilder(config) {
|
|
1018
|
+
const generateId = config?.idGenerator ?? createCounterIdGenerator();
|
|
1019
|
+
const agentId = config?.agentId ?? "unknown";
|
|
1020
|
+
const trigger = config?.trigger ?? "manual";
|
|
1021
|
+
const spanId = (0, import_crypto.randomUUID)();
|
|
1022
|
+
const traceId = config?.traceId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_TRACE_ID : void 0) ?? (0, import_crypto.randomUUID)();
|
|
1023
|
+
const parentSpanId = config?.parentSpanId ?? (typeof process !== "undefined" ? process.env?.AGENTFLOW_PARENT_SPAN_ID : void 0) ?? null;
|
|
1024
|
+
const graphId = generateId();
|
|
1025
|
+
const startTime = Date.now();
|
|
1026
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1027
|
+
const edges = [];
|
|
1028
|
+
const events = [];
|
|
1029
|
+
const parentStack = [];
|
|
1030
|
+
let rootNodeId = null;
|
|
1031
|
+
let built = false;
|
|
1032
|
+
function assertNotBuilt() {
|
|
1033
|
+
if (built) {
|
|
1034
|
+
throw new Error("GraphBuilder: cannot mutate after build() has been called");
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function getNode(nodeId) {
|
|
1038
|
+
const node = nodes.get(nodeId);
|
|
1039
|
+
if (!node) {
|
|
1040
|
+
throw new Error(`GraphBuilder: node "${nodeId}" does not exist`);
|
|
1041
|
+
}
|
|
1042
|
+
return node;
|
|
1043
|
+
}
|
|
1044
|
+
function recordEvent(nodeId, eventType, data = {}) {
|
|
1045
|
+
events.push({
|
|
1046
|
+
timestamp: Date.now(),
|
|
1047
|
+
eventType,
|
|
1048
|
+
nodeId,
|
|
1049
|
+
data
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function buildGraph() {
|
|
1053
|
+
if (rootNodeId === null) {
|
|
1054
|
+
throw new Error("GraphBuilder: cannot build a graph with no nodes");
|
|
1055
|
+
}
|
|
1056
|
+
let graphStatus = "completed";
|
|
1057
|
+
for (const node of nodes.values()) {
|
|
1058
|
+
if (node.status === "failed" || node.status === "timeout" || node.status === "hung") {
|
|
1059
|
+
graphStatus = "failed";
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
if (node.status === "running") {
|
|
1063
|
+
graphStatus = "running";
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const endTime = graphStatus === "running" ? null : Date.now();
|
|
1067
|
+
const frozenNodes = new Map(
|
|
1068
|
+
[...nodes.entries()].map(([id, mNode]) => [
|
|
1069
|
+
id,
|
|
1070
|
+
{
|
|
1071
|
+
id: mNode.id,
|
|
1072
|
+
type: mNode.type,
|
|
1073
|
+
name: mNode.name,
|
|
1074
|
+
startTime: mNode.startTime,
|
|
1075
|
+
endTime: mNode.endTime,
|
|
1076
|
+
status: mNode.status,
|
|
1077
|
+
parentId: mNode.parentId,
|
|
1078
|
+
children: [...mNode.children],
|
|
1079
|
+
metadata: { ...mNode.metadata },
|
|
1080
|
+
state: { ...mNode.state }
|
|
1081
|
+
}
|
|
1082
|
+
])
|
|
1083
|
+
);
|
|
1084
|
+
const graph = {
|
|
1085
|
+
id: graphId,
|
|
1086
|
+
rootNodeId,
|
|
1087
|
+
nodes: frozenNodes,
|
|
1088
|
+
edges: [...edges],
|
|
1089
|
+
startTime,
|
|
1090
|
+
endTime,
|
|
1091
|
+
status: graphStatus,
|
|
1092
|
+
trigger,
|
|
1093
|
+
agentId,
|
|
1094
|
+
events: [...events],
|
|
1095
|
+
traceId,
|
|
1096
|
+
spanId,
|
|
1097
|
+
parentSpanId
|
|
1098
|
+
};
|
|
1099
|
+
return deepFreeze(graph);
|
|
1100
|
+
}
|
|
1101
|
+
const builder = {
|
|
1102
|
+
get graphId() {
|
|
1103
|
+
return graphId;
|
|
1104
|
+
},
|
|
1105
|
+
get traceContext() {
|
|
1106
|
+
return { traceId, spanId };
|
|
1107
|
+
},
|
|
1108
|
+
startNode(opts) {
|
|
1109
|
+
assertNotBuilt();
|
|
1110
|
+
const id = generateId();
|
|
1111
|
+
const parentId = opts.parentId ?? parentStack[parentStack.length - 1] ?? null;
|
|
1112
|
+
if (parentId !== null && !nodes.has(parentId)) {
|
|
1113
|
+
throw new Error(`GraphBuilder: parent node "${parentId}" does not exist`);
|
|
1114
|
+
}
|
|
1115
|
+
const node = {
|
|
1116
|
+
id,
|
|
1117
|
+
type: opts.type,
|
|
1118
|
+
name: opts.name,
|
|
1119
|
+
startTime: Date.now(),
|
|
1120
|
+
endTime: null,
|
|
1121
|
+
status: "running",
|
|
1122
|
+
parentId,
|
|
1123
|
+
children: [],
|
|
1124
|
+
metadata: opts.metadata ? { ...opts.metadata } : {},
|
|
1125
|
+
state: {}
|
|
1126
|
+
};
|
|
1127
|
+
nodes.set(id, node);
|
|
1128
|
+
if (parentId !== null) {
|
|
1129
|
+
const parent = nodes.get(parentId);
|
|
1130
|
+
if (parent) {
|
|
1131
|
+
parent.children.push(id);
|
|
1132
|
+
}
|
|
1133
|
+
edges.push({ from: parentId, to: id, type: "spawned" });
|
|
1134
|
+
}
|
|
1135
|
+
if (rootNodeId === null) {
|
|
1136
|
+
rootNodeId = id;
|
|
1137
|
+
}
|
|
1138
|
+
recordEvent(id, "agent_start", { type: opts.type, name: opts.name });
|
|
1139
|
+
return id;
|
|
1140
|
+
},
|
|
1141
|
+
endNode(nodeId, status = "completed") {
|
|
1142
|
+
assertNotBuilt();
|
|
1143
|
+
const node = getNode(nodeId);
|
|
1144
|
+
if (node.endTime !== null) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
`GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
node.endTime = Date.now();
|
|
1150
|
+
node.status = status;
|
|
1151
|
+
recordEvent(nodeId, "agent_end", { status });
|
|
1152
|
+
},
|
|
1153
|
+
failNode(nodeId, error) {
|
|
1154
|
+
assertNotBuilt();
|
|
1155
|
+
const node = getNode(nodeId);
|
|
1156
|
+
if (node.endTime !== null) {
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
`GraphBuilder: node "${nodeId}" has already ended (status: ${node.status})`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
const errorMessage = error instanceof Error ? error.message : error;
|
|
1162
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
1163
|
+
node.endTime = Date.now();
|
|
1164
|
+
node.status = "failed";
|
|
1165
|
+
node.metadata.error = errorMessage;
|
|
1166
|
+
if (errorStack) {
|
|
1167
|
+
node.metadata.errorStack = errorStack;
|
|
1168
|
+
}
|
|
1169
|
+
recordEvent(nodeId, "tool_error", { error: errorMessage });
|
|
1170
|
+
},
|
|
1171
|
+
addEdge(from, to, type) {
|
|
1172
|
+
assertNotBuilt();
|
|
1173
|
+
getNode(from);
|
|
1174
|
+
getNode(to);
|
|
1175
|
+
edges.push({ from, to, type });
|
|
1176
|
+
recordEvent(from, "custom", { to, type, action: "edge_add" });
|
|
1177
|
+
},
|
|
1178
|
+
pushEvent(event) {
|
|
1179
|
+
assertNotBuilt();
|
|
1180
|
+
getNode(event.nodeId);
|
|
1181
|
+
events.push({
|
|
1182
|
+
...event,
|
|
1183
|
+
timestamp: Date.now()
|
|
1184
|
+
});
|
|
1185
|
+
},
|
|
1186
|
+
updateState(nodeId, state) {
|
|
1187
|
+
assertNotBuilt();
|
|
1188
|
+
const node = getNode(nodeId);
|
|
1189
|
+
Object.assign(node.state, state);
|
|
1190
|
+
recordEvent(nodeId, "custom", { action: "state_update", ...state });
|
|
1191
|
+
},
|
|
1192
|
+
withParent(parentId, fn) {
|
|
1193
|
+
assertNotBuilt();
|
|
1194
|
+
getNode(parentId);
|
|
1195
|
+
parentStack.push(parentId);
|
|
1196
|
+
try {
|
|
1197
|
+
return fn();
|
|
1198
|
+
} finally {
|
|
1199
|
+
parentStack.pop();
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
getSnapshot() {
|
|
1203
|
+
return buildGraph();
|
|
1204
|
+
},
|
|
1205
|
+
build() {
|
|
1206
|
+
assertNotBuilt();
|
|
1207
|
+
const graph = buildGraph();
|
|
1208
|
+
built = true;
|
|
1209
|
+
return graph;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
return builder;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/runner.ts
|
|
1216
|
+
init_loader();
|
|
1217
|
+
function globToRegex(pattern) {
|
|
1218
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1219
|
+
return new RegExp(`^${escaped}$`);
|
|
1220
|
+
}
|
|
1221
|
+
function snapshotDir(dir, patterns) {
|
|
1222
|
+
const result = /* @__PURE__ */ new Map();
|
|
1223
|
+
if (!(0, import_node_fs2.existsSync)(dir)) return result;
|
|
1224
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir)) {
|
|
1225
|
+
if (!patterns.some((re) => re.test(entry))) continue;
|
|
1226
|
+
const full = (0, import_node_path2.join)(dir, entry);
|
|
1227
|
+
try {
|
|
1228
|
+
const stat = (0, import_node_fs2.statSync)(full);
|
|
1229
|
+
if (stat.isFile()) {
|
|
1230
|
+
result.set(full, stat.mtimeMs);
|
|
1231
|
+
}
|
|
1232
|
+
} catch {
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return result;
|
|
1236
|
+
}
|
|
1237
|
+
function agentIdFromFilename(filePath) {
|
|
1238
|
+
const base = (0, import_node_path2.basename)(filePath, ".json");
|
|
1239
|
+
const cleaned = base.replace(/-state$/, "");
|
|
1240
|
+
return `alfred-${cleaned}`;
|
|
1241
|
+
}
|
|
1242
|
+
function deriveAgentId(command) {
|
|
1243
|
+
return "orchestrator";
|
|
1244
|
+
}
|
|
1245
|
+
function fileTimestamp() {
|
|
1246
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\.\d+Z$/, "");
|
|
1247
|
+
}
|
|
1248
|
+
async function runTraced(config) {
|
|
1249
|
+
const {
|
|
1250
|
+
command,
|
|
1251
|
+
agentId = deriveAgentId(command),
|
|
1252
|
+
trigger = "cli",
|
|
1253
|
+
tracesDir = "./traces",
|
|
1254
|
+
watchDirs = [],
|
|
1255
|
+
watchPatterns = ["*.json"]
|
|
1256
|
+
} = config;
|
|
1257
|
+
if (command.length === 0) {
|
|
1258
|
+
throw new Error("runTraced: command must not be empty");
|
|
1259
|
+
}
|
|
1260
|
+
const resolvedTracesDir = (0, import_node_path2.resolve)(tracesDir);
|
|
1261
|
+
const patterns = watchPatterns.map(globToRegex);
|
|
1262
|
+
const orchestrator = createGraphBuilder({ agentId, trigger });
|
|
1263
|
+
const { traceId, spanId } = orchestrator.traceContext;
|
|
1264
|
+
const beforeSnapshots = /* @__PURE__ */ new Map();
|
|
1265
|
+
for (const dir of watchDirs) {
|
|
1266
|
+
beforeSnapshots.set(dir, snapshotDir(dir, patterns));
|
|
1267
|
+
}
|
|
1268
|
+
const rootId = orchestrator.startNode({ type: "agent", name: agentId });
|
|
1269
|
+
const dispatchId = orchestrator.startNode({
|
|
1270
|
+
type: "tool",
|
|
1271
|
+
name: "dispatch-command",
|
|
1272
|
+
parentId: rootId
|
|
1273
|
+
});
|
|
1274
|
+
orchestrator.updateState(dispatchId, { command: command.join(" ") });
|
|
1275
|
+
const monitorId = orchestrator.startNode({
|
|
1276
|
+
type: "tool",
|
|
1277
|
+
name: "state-monitor",
|
|
1278
|
+
parentId: rootId
|
|
1279
|
+
});
|
|
1280
|
+
orchestrator.updateState(monitorId, {
|
|
1281
|
+
watchDirs,
|
|
1282
|
+
watchPatterns
|
|
1283
|
+
});
|
|
1284
|
+
const startMs = Date.now();
|
|
1285
|
+
const execCmd = command[0] ?? "";
|
|
1286
|
+
const execArgs = command.slice(1);
|
|
1287
|
+
process.env.AGENTFLOW_TRACE_ID = traceId;
|
|
1288
|
+
process.env.AGENTFLOW_PARENT_SPAN_ID = spanId;
|
|
1289
|
+
const result = (0, import_node_child_process.spawnSync)(execCmd, execArgs, { stdio: "inherit" });
|
|
1290
|
+
delete process.env.AGENTFLOW_TRACE_ID;
|
|
1291
|
+
delete process.env.AGENTFLOW_PARENT_SPAN_ID;
|
|
1292
|
+
const exitCode = result.status ?? 1;
|
|
1293
|
+
const duration = (Date.now() - startMs) / 1e3;
|
|
1294
|
+
const stateChanges = [];
|
|
1295
|
+
for (const dir of watchDirs) {
|
|
1296
|
+
const before = beforeSnapshots.get(dir) ?? /* @__PURE__ */ new Map();
|
|
1297
|
+
const after = snapshotDir(dir, patterns);
|
|
1298
|
+
for (const [filePath, mtime] of after) {
|
|
1299
|
+
const prevMtime = before.get(filePath);
|
|
1300
|
+
if (prevMtime === void 0 || mtime > prevMtime) {
|
|
1301
|
+
stateChanges.push(filePath);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
orchestrator.updateState(monitorId, { stateChanges });
|
|
1306
|
+
orchestrator.endNode(monitorId);
|
|
1307
|
+
if (exitCode === 0) {
|
|
1308
|
+
orchestrator.endNode(dispatchId);
|
|
1309
|
+
} else {
|
|
1310
|
+
orchestrator.failNode(dispatchId, `Command exited with code ${exitCode}`);
|
|
1311
|
+
}
|
|
1312
|
+
orchestrator.updateState(rootId, {
|
|
1313
|
+
exitCode,
|
|
1314
|
+
duration,
|
|
1315
|
+
stateChanges
|
|
1316
|
+
});
|
|
1317
|
+
if (exitCode === 0) {
|
|
1318
|
+
orchestrator.endNode(rootId);
|
|
1319
|
+
} else {
|
|
1320
|
+
orchestrator.failNode(rootId, `Command exited with code ${exitCode}`);
|
|
1321
|
+
}
|
|
1322
|
+
const orchestratorGraph = orchestrator.build();
|
|
1323
|
+
const allGraphs = [orchestratorGraph];
|
|
1324
|
+
for (const filePath of stateChanges) {
|
|
1325
|
+
const childAgentId = agentIdFromFilename(filePath);
|
|
1326
|
+
const childBuilder = createGraphBuilder({
|
|
1327
|
+
agentId: childAgentId,
|
|
1328
|
+
trigger: "state-change",
|
|
1329
|
+
traceId,
|
|
1330
|
+
parentSpanId: spanId
|
|
1331
|
+
});
|
|
1332
|
+
const childRootId = childBuilder.startNode({
|
|
1333
|
+
type: "agent",
|
|
1334
|
+
name: childAgentId
|
|
1335
|
+
});
|
|
1336
|
+
childBuilder.updateState(childRootId, {
|
|
1337
|
+
stateFile: filePath,
|
|
1338
|
+
detectedBy: "runner-state-monitor"
|
|
1339
|
+
});
|
|
1340
|
+
childBuilder.endNode(childRootId);
|
|
1341
|
+
allGraphs.push(childBuilder.build());
|
|
1342
|
+
}
|
|
1343
|
+
if (!(0, import_node_fs2.existsSync)(resolvedTracesDir)) {
|
|
1344
|
+
(0, import_node_fs2.mkdirSync)(resolvedTracesDir, { recursive: true });
|
|
1345
|
+
}
|
|
1346
|
+
const ts = fileTimestamp();
|
|
1347
|
+
const tracePaths = [];
|
|
1348
|
+
for (const graph of allGraphs) {
|
|
1349
|
+
const filename = `${graph.agentId}-${ts}.json`;
|
|
1350
|
+
const outPath = (0, import_node_path2.join)(resolvedTracesDir, filename);
|
|
1351
|
+
(0, import_node_fs2.writeFileSync)(outPath, JSON.stringify(graphToJson(graph), null, 2), "utf-8");
|
|
1352
|
+
tracePaths.push(outPath);
|
|
1353
|
+
}
|
|
1354
|
+
if (tracePaths.length > 0) {
|
|
1355
|
+
console.log(`\u{1F50D} Run "agentflow trace show ${orchestratorGraph.id} --traces-dir ${resolvedTracesDir}" to inspect`);
|
|
1356
|
+
}
|
|
1357
|
+
return {
|
|
1358
|
+
exitCode,
|
|
1359
|
+
traceId,
|
|
1360
|
+
spanId,
|
|
1361
|
+
tracePaths,
|
|
1362
|
+
stateChanges,
|
|
1363
|
+
duration
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/trace-cli.ts
|
|
1368
|
+
var import_path2 = require("path");
|
|
1369
|
+
|
|
1370
|
+
// src/trace-store.ts
|
|
1371
|
+
var import_promises = require("fs/promises");
|
|
1372
|
+
var import_path = require("path");
|
|
1373
|
+
init_loader();
|
|
1374
|
+
function createTraceStore(dir) {
|
|
1375
|
+
async function ensureDir() {
|
|
1376
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
1377
|
+
}
|
|
1378
|
+
async function loadAll() {
|
|
1379
|
+
await ensureDir();
|
|
1380
|
+
let files;
|
|
1381
|
+
try {
|
|
1382
|
+
files = await (0, import_promises.readdir)(dir);
|
|
1383
|
+
} catch {
|
|
1384
|
+
return [];
|
|
1385
|
+
}
|
|
1386
|
+
const graphs = [];
|
|
1387
|
+
for (const file of files) {
|
|
1388
|
+
if (!file.endsWith(".json")) continue;
|
|
1389
|
+
try {
|
|
1390
|
+
const content = await (0, import_promises.readFile)((0, import_path.join)(dir, file), "utf-8");
|
|
1391
|
+
const graph = loadGraph(content);
|
|
1392
|
+
graphs.push(graph);
|
|
1393
|
+
} catch {
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return graphs;
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
async save(graph) {
|
|
1400
|
+
await ensureDir();
|
|
1401
|
+
const json = graphToJson(graph);
|
|
1402
|
+
const filePath = (0, import_path.join)(dir, `${graph.id}.json`);
|
|
1403
|
+
await (0, import_promises.writeFile)(filePath, JSON.stringify(json, null, 2), "utf-8");
|
|
1404
|
+
return filePath;
|
|
1405
|
+
},
|
|
1406
|
+
async get(graphId) {
|
|
1407
|
+
await ensureDir();
|
|
1408
|
+
const filePath = (0, import_path.join)(dir, `${graphId}.json`);
|
|
1409
|
+
try {
|
|
1410
|
+
const content = await (0, import_promises.readFile)(filePath, "utf-8");
|
|
1411
|
+
return loadGraph(content);
|
|
1412
|
+
} catch {
|
|
1413
|
+
}
|
|
1414
|
+
const all = await loadAll();
|
|
1415
|
+
return all.find((g) => g.id === graphId) ?? null;
|
|
1416
|
+
},
|
|
1417
|
+
async list(opts) {
|
|
1418
|
+
let graphs = await loadAll();
|
|
1419
|
+
if (opts?.status) {
|
|
1420
|
+
graphs = graphs.filter((g) => g.status === opts.status);
|
|
1421
|
+
}
|
|
1422
|
+
graphs.sort((a, b) => b.startTime - a.startTime);
|
|
1423
|
+
if (opts?.limit && opts.limit > 0) {
|
|
1424
|
+
graphs = graphs.slice(0, opts.limit);
|
|
1425
|
+
}
|
|
1426
|
+
return graphs;
|
|
1427
|
+
},
|
|
1428
|
+
async getStuckSpans() {
|
|
1429
|
+
const graphs = await loadAll();
|
|
1430
|
+
const stuck = [];
|
|
1431
|
+
for (const graph of graphs) {
|
|
1432
|
+
for (const node of graph.nodes.values()) {
|
|
1433
|
+
if (node.status === "running" || node.status === "hung" || node.status === "timeout") {
|
|
1434
|
+
stuck.push(node);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return stuck;
|
|
1439
|
+
},
|
|
1440
|
+
async getReasoningLoops(threshold = 25) {
|
|
1441
|
+
const graphs = await loadAll();
|
|
1442
|
+
const results = [];
|
|
1443
|
+
for (const graph of graphs) {
|
|
1444
|
+
const loops = findLoopsInGraph(graph, threshold);
|
|
1445
|
+
if (loops.length > 0) {
|
|
1446
|
+
results.push({ graphId: graph.id, nodes: loops });
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
return results;
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function findLoopsInGraph(graph, threshold) {
|
|
1454
|
+
const loopNodes = [];
|
|
1455
|
+
function walk(nodeId, consecutiveCount, consecutiveType) {
|
|
1456
|
+
const node = graph.nodes.get(nodeId);
|
|
1457
|
+
if (!node) return;
|
|
1458
|
+
const newCount = node.type === consecutiveType ? consecutiveCount + 1 : 1;
|
|
1459
|
+
if (newCount > threshold) {
|
|
1460
|
+
loopNodes.push(node);
|
|
1461
|
+
}
|
|
1462
|
+
for (const childId of node.children) {
|
|
1463
|
+
walk(childId, newCount, node.type);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
walk(graph.rootNodeId, 0, null);
|
|
1467
|
+
return loopNodes;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/visualize.ts
|
|
1471
|
+
var STATUS_ICONS = {
|
|
1472
|
+
completed: "\u2713",
|
|
1473
|
+
failed: "\u2717",
|
|
1474
|
+
running: "\u231B",
|
|
1475
|
+
hung: "\u231B",
|
|
1476
|
+
timeout: "\u231B"
|
|
1477
|
+
};
|
|
1478
|
+
function formatDuration(ms) {
|
|
1479
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
1480
|
+
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
1481
|
+
if (ms < 36e5) return `${(ms / 6e4).toFixed(1)}m`;
|
|
1482
|
+
return `${(ms / 36e5).toFixed(1)}h`;
|
|
1483
|
+
}
|
|
1484
|
+
function nodeDuration(node, graphEndTime) {
|
|
1485
|
+
const end = node.endTime ?? graphEndTime;
|
|
1486
|
+
return formatDuration(end - node.startTime);
|
|
1487
|
+
}
|
|
1488
|
+
function getGenAiInfo(node) {
|
|
1489
|
+
const parts = [];
|
|
1490
|
+
const meta = node.metadata;
|
|
1491
|
+
if (meta["gen_ai.request.model"]) {
|
|
1492
|
+
parts.push(String(meta["gen_ai.request.model"]));
|
|
1493
|
+
}
|
|
1494
|
+
const tokens = meta["gen_ai.usage.prompt_tokens"] ?? meta["gen_ai.usage.completion_tokens"];
|
|
1495
|
+
if (tokens !== void 0) {
|
|
1496
|
+
const prompt = meta["gen_ai.usage.prompt_tokens"] ?? 0;
|
|
1497
|
+
const completion = meta["gen_ai.usage.completion_tokens"] ?? 0;
|
|
1498
|
+
if (prompt || completion) {
|
|
1499
|
+
parts.push(`${prompt + completion} tok`);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
|
|
1503
|
+
}
|
|
1504
|
+
function hasViolation(node, graph) {
|
|
1505
|
+
return graph.events.some(
|
|
1506
|
+
(e) => e.nodeId === node.id && e.eventType === "custom" && e.data.guardViolation !== void 0
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
function toAsciiTree(graph) {
|
|
1510
|
+
if (graph.nodes.size === 0) return "(empty graph)";
|
|
1511
|
+
const now = Date.now();
|
|
1512
|
+
const endTime = graph.endTime ?? now;
|
|
1513
|
+
const lines = [];
|
|
1514
|
+
function renderNode(nodeId, prefix, isLast, isRoot) {
|
|
1515
|
+
const node = graph.nodes.get(nodeId);
|
|
1516
|
+
if (!node) return;
|
|
1517
|
+
const icon = STATUS_ICONS[node.status];
|
|
1518
|
+
const duration = nodeDuration(node, endTime);
|
|
1519
|
+
const genAi = getGenAiInfo(node);
|
|
1520
|
+
const violation = hasViolation(node, graph) ? " \u26A0" : "";
|
|
1521
|
+
const errorInfo = node.status === "failed" && node.metadata.error ? ` \u2014 ${node.metadata.error}` : "";
|
|
1522
|
+
const timeoutInfo = node.status === "timeout" ? " [TIMEOUT]" : "";
|
|
1523
|
+
const hungInfo = node.status === "hung" ? " [HUNG]" : "";
|
|
1524
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
1525
|
+
const line = `${prefix}${connector}${icon} ${node.name} (${node.type}) ${duration}${genAi}${violation}${timeoutInfo}${hungInfo}${errorInfo}`;
|
|
1526
|
+
lines.push(line);
|
|
1527
|
+
const children = getChildren(graph, nodeId);
|
|
1528
|
+
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
1529
|
+
for (let i = 0; i < children.length; i++) {
|
|
1530
|
+
renderNode(children[i].id, childPrefix, i === children.length - 1, false);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
renderNode(graph.rootNodeId, "", true, true);
|
|
1534
|
+
return lines.join("\n");
|
|
1535
|
+
}
|
|
1536
|
+
function toTimeline(graph) {
|
|
1537
|
+
if (graph.nodes.size === 0) return "(empty graph)";
|
|
1538
|
+
const now = Date.now();
|
|
1539
|
+
const graphStart = graph.startTime;
|
|
1540
|
+
const graphEnd = graph.endTime ?? now;
|
|
1541
|
+
const totalDuration = graphEnd - graphStart;
|
|
1542
|
+
if (totalDuration <= 0) return "(zero duration)";
|
|
1543
|
+
const barWidth = 60;
|
|
1544
|
+
const lines = [];
|
|
1545
|
+
const scaleLabels = [];
|
|
1546
|
+
const tickCount = Math.min(5, Math.max(2, Math.floor(barWidth / 10)));
|
|
1547
|
+
for (let i = 0; i <= tickCount; i++) {
|
|
1548
|
+
const t = totalDuration * i / tickCount;
|
|
1549
|
+
scaleLabels.push(formatDuration(t));
|
|
1550
|
+
}
|
|
1551
|
+
let header = "";
|
|
1552
|
+
for (let i = 0; i < scaleLabels.length; i++) {
|
|
1553
|
+
const pos = Math.round(barWidth * i / tickCount);
|
|
1554
|
+
while (header.length < pos) header += " ";
|
|
1555
|
+
header += scaleLabels[i];
|
|
1556
|
+
}
|
|
1557
|
+
lines.push(header);
|
|
1558
|
+
let tickLine = "";
|
|
1559
|
+
for (let i = 0; i < barWidth; i++) {
|
|
1560
|
+
const tickPos = tickCount > 0 ? i * tickCount / barWidth : 0;
|
|
1561
|
+
if (Number.isInteger(Math.round(tickPos * 100) / 100) && Math.abs(tickPos - Math.round(tickPos)) < 0.01) {
|
|
1562
|
+
tickLine += "\u253C";
|
|
1563
|
+
} else {
|
|
1564
|
+
tickLine += "\u2500";
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
lines.push(tickLine);
|
|
1568
|
+
const orderedNodes = [];
|
|
1569
|
+
function collectNodes(nodeId) {
|
|
1570
|
+
const node = graph.nodes.get(nodeId);
|
|
1571
|
+
if (!node) return;
|
|
1572
|
+
orderedNodes.push(node);
|
|
1573
|
+
const children = getChildren(graph, nodeId);
|
|
1574
|
+
for (const child of children) {
|
|
1575
|
+
collectNodes(child.id);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
collectNodes(graph.rootNodeId);
|
|
1579
|
+
for (const node of orderedNodes) {
|
|
1580
|
+
const nodeStart = node.startTime - graphStart;
|
|
1581
|
+
const nodeEnd = (node.endTime ?? now) - graphStart;
|
|
1582
|
+
const startCol = Math.round(nodeStart / totalDuration * barWidth);
|
|
1583
|
+
const endCol = Math.max(startCol + 1, Math.round(nodeEnd / totalDuration * barWidth));
|
|
1584
|
+
let bar = "";
|
|
1585
|
+
for (let i = 0; i < barWidth; i++) {
|
|
1586
|
+
if (i >= startCol && i < endCol) {
|
|
1587
|
+
bar += "\u2588";
|
|
1588
|
+
} else {
|
|
1589
|
+
bar += " ";
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
const icon = STATUS_ICONS[node.status];
|
|
1593
|
+
const duration = nodeDuration(node, graphEnd);
|
|
1594
|
+
const violation = hasViolation(node, graph) ? " \u26A0" : "";
|
|
1595
|
+
lines.push(`${bar} ${icon} ${node.name} (${duration})${violation}`);
|
|
1596
|
+
}
|
|
1597
|
+
return lines.join("\n");
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/trace-cli.ts
|
|
1601
|
+
function getTracesDir(argv) {
|
|
1602
|
+
const idx = argv.indexOf("--traces-dir");
|
|
1603
|
+
if (idx !== -1 && argv[idx + 1]) {
|
|
1604
|
+
return (0, import_path2.resolve)(argv[idx + 1]);
|
|
1605
|
+
}
|
|
1606
|
+
return (0, import_path2.resolve)("./traces");
|
|
1607
|
+
}
|
|
1608
|
+
function getFlag(argv, name) {
|
|
1609
|
+
const idx = argv.indexOf(name);
|
|
1610
|
+
if (idx !== -1 && argv[idx + 1]) {
|
|
1611
|
+
return argv[idx + 1];
|
|
1612
|
+
}
|
|
1613
|
+
return void 0;
|
|
1614
|
+
}
|
|
1615
|
+
function printTraceHelp() {
|
|
1616
|
+
console.log(
|
|
1617
|
+
`
|
|
1618
|
+
AgentFlow Trace \u2014 inspect saved execution traces.
|
|
1619
|
+
|
|
1620
|
+
Usage:
|
|
1621
|
+
agentflow trace <command> [options]
|
|
1622
|
+
|
|
1623
|
+
Commands:
|
|
1624
|
+
list [--status <status>] [--limit <n>] List saved traces
|
|
1625
|
+
show <graph-id> Show trace as ASCII tree
|
|
1626
|
+
timeline <graph-id> Show trace as timeline waterfall
|
|
1627
|
+
stuck Show all stuck/hung/timeout spans
|
|
1628
|
+
loops [--threshold <n>] Detect reasoning loops
|
|
1629
|
+
|
|
1630
|
+
Options:
|
|
1631
|
+
--traces-dir <path> Directory containing trace files (default: ./traces)
|
|
1632
|
+
|
|
1633
|
+
Examples:
|
|
1634
|
+
agentflow trace list --status failed --limit 10
|
|
1635
|
+
agentflow trace show abc-123
|
|
1636
|
+
agentflow trace timeline abc-123
|
|
1637
|
+
agentflow trace stuck
|
|
1638
|
+
agentflow trace loops --threshold 10
|
|
1639
|
+
`.trim()
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
async function traceList(argv) {
|
|
1643
|
+
const dir = getTracesDir(argv);
|
|
1644
|
+
const store = createTraceStore(dir);
|
|
1645
|
+
const status = getFlag(argv, "--status");
|
|
1646
|
+
const limitStr = getFlag(argv, "--limit");
|
|
1647
|
+
const limit = limitStr ? Number.parseInt(limitStr, 10) : void 0;
|
|
1648
|
+
const graphs = await store.list({ status, limit });
|
|
1649
|
+
if (graphs.length === 0) {
|
|
1650
|
+
console.log("No traces found.");
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
console.log(`Found ${graphs.length} trace(s) in ${dir}:
|
|
1654
|
+
`);
|
|
1655
|
+
for (const g of graphs) {
|
|
1656
|
+
const duration = g.endTime ? `${((g.endTime - g.startTime) / 1e3).toFixed(1)}s` : "running";
|
|
1657
|
+
const date = new Date(g.startTime).toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
1658
|
+
const icon = g.status === "completed" ? "\u2713" : g.status === "failed" ? "\u2717" : "\u231B";
|
|
1659
|
+
console.log(
|
|
1660
|
+
` ${icon} ${g.id} ${g.status.padEnd(10)} ${duration.padEnd(8)} ${date} ${g.agentId}`
|
|
1661
|
+
);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
async function traceShow(argv) {
|
|
1665
|
+
const dir = getTracesDir(argv);
|
|
1666
|
+
const store = createTraceStore(dir);
|
|
1667
|
+
const showIdx = argv.indexOf("show");
|
|
1668
|
+
const graphId = showIdx !== -1 ? argv[showIdx + 1] : void 0;
|
|
1669
|
+
if (!graphId || graphId.startsWith("--")) {
|
|
1670
|
+
console.error("Usage: agentflow trace show <graph-id>");
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
}
|
|
1673
|
+
let graph = await store.get(graphId);
|
|
1674
|
+
if (!graph) {
|
|
1675
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
1676
|
+
const { join: join5 } = await import("path");
|
|
1677
|
+
const fname = graphId.endsWith(".json") ? graphId : `${graphId}.json`;
|
|
1678
|
+
try {
|
|
1679
|
+
const { loadGraph: loadGraph2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
|
|
1680
|
+
const content = await readFile2(join5(dir, fname), "utf-8");
|
|
1681
|
+
graph = loadGraph2(content);
|
|
1682
|
+
} catch {
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
if (!graph) {
|
|
1686
|
+
console.error(`Trace "${graphId}" not found in ${dir}`);
|
|
1687
|
+
process.exit(1);
|
|
1688
|
+
}
|
|
1689
|
+
console.log(`Trace: ${graph.id} (${graph.status})
|
|
1690
|
+
`);
|
|
1691
|
+
console.log(toAsciiTree(graph));
|
|
1692
|
+
}
|
|
1693
|
+
async function traceTimeline(argv) {
|
|
1694
|
+
const dir = getTracesDir(argv);
|
|
1695
|
+
const store = createTraceStore(dir);
|
|
1696
|
+
const timelineIdx = argv.indexOf("timeline");
|
|
1697
|
+
const graphId = timelineIdx !== -1 ? argv[timelineIdx + 1] : void 0;
|
|
1698
|
+
if (!graphId || graphId.startsWith("--")) {
|
|
1699
|
+
console.error("Usage: agentflow trace timeline <graph-id>");
|
|
1700
|
+
process.exit(1);
|
|
1701
|
+
}
|
|
1702
|
+
let graph = await store.get(graphId);
|
|
1703
|
+
if (!graph) {
|
|
1704
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
1705
|
+
const { join: join5 } = await import("path");
|
|
1706
|
+
const fname = graphId.endsWith(".json") ? graphId : `${graphId}.json`;
|
|
1707
|
+
try {
|
|
1708
|
+
const { loadGraph: loadGraph2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
|
|
1709
|
+
const content = await readFile2(join5(dir, fname), "utf-8");
|
|
1710
|
+
graph = loadGraph2(content);
|
|
1711
|
+
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (!graph) {
|
|
1715
|
+
console.error(`Trace "${graphId}" not found in ${dir}`);
|
|
1716
|
+
process.exit(1);
|
|
1717
|
+
}
|
|
1718
|
+
console.log(`Trace: ${graph.id} (${graph.status})
|
|
1719
|
+
`);
|
|
1720
|
+
console.log(toTimeline(graph));
|
|
1721
|
+
}
|
|
1722
|
+
async function traceStuck(argv) {
|
|
1723
|
+
const dir = getTracesDir(argv);
|
|
1724
|
+
const store = createTraceStore(dir);
|
|
1725
|
+
const stuck = await store.getStuckSpans();
|
|
1726
|
+
if (stuck.length === 0) {
|
|
1727
|
+
console.log("No stuck spans found.");
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
console.log(`Found ${stuck.length} stuck span(s):
|
|
1731
|
+
`);
|
|
1732
|
+
for (const node of stuck) {
|
|
1733
|
+
const elapsed = Date.now() - node.startTime;
|
|
1734
|
+
const icon = node.status === "timeout" ? "\u231B" : node.status === "hung" ? "\u231B" : "\u231B";
|
|
1735
|
+
console.log(
|
|
1736
|
+
` ${icon} ${node.id} ${node.type.padEnd(10)} ${node.name.padEnd(20)} ${node.status.padEnd(8)} ${(elapsed / 1e3).toFixed(0)}s`
|
|
1737
|
+
);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
async function traceLoops(argv) {
|
|
1741
|
+
const dir = getTracesDir(argv);
|
|
1742
|
+
const store = createTraceStore(dir);
|
|
1743
|
+
const thresholdStr = getFlag(argv, "--threshold");
|
|
1744
|
+
const threshold = thresholdStr ? Number.parseInt(thresholdStr, 10) : void 0;
|
|
1745
|
+
const loops = await store.getReasoningLoops(threshold);
|
|
1746
|
+
if (loops.length === 0) {
|
|
1747
|
+
console.log("No reasoning loops detected.");
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1750
|
+
console.log(`Found reasoning loops in ${loops.length} trace(s):
|
|
1751
|
+
`);
|
|
1752
|
+
for (const { graphId, nodes } of loops) {
|
|
1753
|
+
console.log(` Graph: ${graphId}`);
|
|
1754
|
+
for (const node of nodes) {
|
|
1755
|
+
console.log(` - ${node.id} (${node.type}: ${node.name})`);
|
|
1756
|
+
}
|
|
1757
|
+
console.log("");
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async function handleTrace(argv) {
|
|
1761
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1762
|
+
printTraceHelp();
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
const traceIdx = argv.indexOf("trace");
|
|
1766
|
+
const subcommand = traceIdx !== -1 ? argv[traceIdx + 1] : void 0;
|
|
1767
|
+
switch (subcommand) {
|
|
1768
|
+
case "list":
|
|
1769
|
+
await traceList(argv);
|
|
1770
|
+
break;
|
|
1771
|
+
case "show":
|
|
1772
|
+
await traceShow(argv);
|
|
1773
|
+
break;
|
|
1774
|
+
case "timeline":
|
|
1775
|
+
await traceTimeline(argv);
|
|
1776
|
+
break;
|
|
1777
|
+
case "stuck":
|
|
1778
|
+
await traceStuck(argv);
|
|
1779
|
+
break;
|
|
1780
|
+
case "loops":
|
|
1781
|
+
await traceLoops(argv);
|
|
1782
|
+
break;
|
|
1783
|
+
default:
|
|
1784
|
+
printTraceHelp();
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1216
1787
|
}
|
|
1217
1788
|
|
|
1218
1789
|
// src/watch.ts
|
|
1219
1790
|
var import_node_fs4 = require("fs");
|
|
1220
|
-
var import_node_path3 = require("path");
|
|
1221
1791
|
var import_node_os = require("os");
|
|
1792
|
+
var import_node_path3 = require("path");
|
|
1793
|
+
|
|
1794
|
+
// src/watch-alerts.ts
|
|
1795
|
+
var import_node_child_process2 = require("child_process");
|
|
1796
|
+
var import_node_http = require("http");
|
|
1797
|
+
var import_node_https = require("https");
|
|
1798
|
+
function formatAlertMessage(payload) {
|
|
1799
|
+
const time = new Date(payload.timestamp).toISOString();
|
|
1800
|
+
const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
|
|
1801
|
+
return [
|
|
1802
|
+
`[ALERT] ${payload.condition}: "${payload.agentId}"`,
|
|
1803
|
+
` Status: ${arrow}`,
|
|
1804
|
+
payload.detail ? ` Detail: ${payload.detail}` : null,
|
|
1805
|
+
` File: ${payload.file}`,
|
|
1806
|
+
` Time: ${time}`
|
|
1807
|
+
].filter(Boolean).join("\n");
|
|
1808
|
+
}
|
|
1809
|
+
function formatTelegram(payload) {
|
|
1810
|
+
const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
|
|
1811
|
+
const time = new Date(payload.timestamp).toLocaleTimeString();
|
|
1812
|
+
return [
|
|
1813
|
+
`${icon} *AgentFlow Alert*`,
|
|
1814
|
+
`*${payload.condition}*: \`${payload.agentId}\``,
|
|
1815
|
+
`Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
|
|
1816
|
+
payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
|
|
1817
|
+
`Time: ${time}`
|
|
1818
|
+
].filter(Boolean).join("\n");
|
|
1819
|
+
}
|
|
1820
|
+
async function sendAlert(payload, channel) {
|
|
1821
|
+
try {
|
|
1822
|
+
switch (channel.type) {
|
|
1823
|
+
case "stdout":
|
|
1824
|
+
sendStdout(payload);
|
|
1825
|
+
break;
|
|
1826
|
+
case "telegram":
|
|
1827
|
+
await sendTelegram(payload, channel.botToken, channel.chatId);
|
|
1828
|
+
break;
|
|
1829
|
+
case "webhook":
|
|
1830
|
+
await sendWebhook(payload, channel.url);
|
|
1831
|
+
break;
|
|
1832
|
+
case "command":
|
|
1833
|
+
await sendCommand(payload, channel.cmd);
|
|
1834
|
+
break;
|
|
1835
|
+
}
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1838
|
+
console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function sendStdout(payload) {
|
|
1842
|
+
console.log(formatAlertMessage(payload));
|
|
1843
|
+
}
|
|
1844
|
+
function sendTelegram(payload, botToken, chatId) {
|
|
1845
|
+
const body = JSON.stringify({
|
|
1846
|
+
chat_id: chatId,
|
|
1847
|
+
text: formatTelegram(payload),
|
|
1848
|
+
parse_mode: "Markdown"
|
|
1849
|
+
});
|
|
1850
|
+
return new Promise((resolve6, reject) => {
|
|
1851
|
+
const req = (0, import_node_https.request)(
|
|
1852
|
+
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1853
|
+
{
|
|
1854
|
+
method: "POST",
|
|
1855
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
|
|
1856
|
+
},
|
|
1857
|
+
(res) => {
|
|
1858
|
+
res.resume();
|
|
1859
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve6();
|
|
1860
|
+
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1861
|
+
}
|
|
1862
|
+
);
|
|
1863
|
+
req.on("error", reject);
|
|
1864
|
+
req.write(body);
|
|
1865
|
+
req.end();
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
function sendWebhook(payload, url) {
|
|
1869
|
+
const body = JSON.stringify(payload);
|
|
1870
|
+
const isHttps = url.startsWith("https");
|
|
1871
|
+
const doRequest = isHttps ? import_node_https.request : import_node_http.request;
|
|
1872
|
+
return new Promise((resolve6, reject) => {
|
|
1873
|
+
const req = doRequest(
|
|
1874
|
+
url,
|
|
1875
|
+
{
|
|
1876
|
+
method: "POST",
|
|
1877
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) }
|
|
1878
|
+
},
|
|
1879
|
+
(res) => {
|
|
1880
|
+
res.resume();
|
|
1881
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve6();
|
|
1882
|
+
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1883
|
+
}
|
|
1884
|
+
);
|
|
1885
|
+
req.on("error", reject);
|
|
1886
|
+
req.setTimeout(1e4, () => {
|
|
1887
|
+
req.destroy(new Error("Webhook timeout"));
|
|
1888
|
+
});
|
|
1889
|
+
req.write(body);
|
|
1890
|
+
req.end();
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
function sendCommand(payload, cmd) {
|
|
1894
|
+
return new Promise((resolve6, reject) => {
|
|
1895
|
+
const env = {
|
|
1896
|
+
...process.env,
|
|
1897
|
+
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
1898
|
+
AGENTFLOW_ALERT_CONDITION: payload.condition,
|
|
1899
|
+
AGENTFLOW_ALERT_STATUS: payload.currentStatus,
|
|
1900
|
+
AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
|
|
1901
|
+
AGENTFLOW_ALERT_DETAIL: payload.detail,
|
|
1902
|
+
AGENTFLOW_ALERT_FILE: payload.file,
|
|
1903
|
+
AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
|
|
1904
|
+
};
|
|
1905
|
+
(0, import_node_child_process2.exec)(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1906
|
+
if (err) reject(err);
|
|
1907
|
+
else resolve6();
|
|
1908
|
+
});
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1222
1911
|
|
|
1223
1912
|
// src/watch-state.ts
|
|
1224
1913
|
var import_node_fs3 = require("fs");
|
|
@@ -1284,7 +1973,9 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1284
1973
|
const hasError = config.alertConditions.some((c) => c.type === "error");
|
|
1285
1974
|
const hasRecovery = config.alertConditions.some((c) => c.type === "recovery");
|
|
1286
1975
|
const staleConditions = config.alertConditions.filter((c) => c.type === "stale");
|
|
1287
|
-
const consecutiveConditions = config.alertConditions.filter(
|
|
1976
|
+
const consecutiveConditions = config.alertConditions.filter(
|
|
1977
|
+
(c) => c.type === "consecutive-errors"
|
|
1978
|
+
);
|
|
1288
1979
|
const byAgent = /* @__PURE__ */ new Map();
|
|
1289
1980
|
for (const r of currentRecords) {
|
|
1290
1981
|
const existing = byAgent.get(r.id);
|
|
@@ -1308,14 +1999,16 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1308
1999
|
for (const cond of consecutiveConditions) {
|
|
1309
2000
|
if (newConsec === cond.threshold) {
|
|
1310
2001
|
if (canAlert(prev, `consecutive-errors:${cond.threshold}`, config.cooldownMs, now)) {
|
|
1311
|
-
alerts.push(
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
2002
|
+
alerts.push(
|
|
2003
|
+
makePayload(
|
|
2004
|
+
agentId,
|
|
2005
|
+
`consecutive-errors (${cond.threshold})`,
|
|
2006
|
+
prevStatus,
|
|
2007
|
+
currStatus,
|
|
2008
|
+
{ ...record, detail: `${newConsec} consecutive errors. ${record.detail}` },
|
|
2009
|
+
config.dirs
|
|
2010
|
+
)
|
|
2011
|
+
);
|
|
1319
2012
|
}
|
|
1320
2013
|
}
|
|
1321
2014
|
}
|
|
@@ -1324,14 +2017,16 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1324
2017
|
if (sinceActive > cond.durationMs && record.lastActive > 0) {
|
|
1325
2018
|
if (canAlert(prev, "stale", config.cooldownMs, now)) {
|
|
1326
2019
|
const mins = Math.floor(sinceActive / 6e4);
|
|
1327
|
-
alerts.push(
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
2020
|
+
alerts.push(
|
|
2021
|
+
makePayload(
|
|
2022
|
+
agentId,
|
|
2023
|
+
"stale",
|
|
2024
|
+
prevStatus,
|
|
2025
|
+
currStatus,
|
|
2026
|
+
{ ...record, detail: `No update for ${mins}m. ${record.detail}` },
|
|
2027
|
+
config.dirs
|
|
2028
|
+
)
|
|
2029
|
+
);
|
|
1335
2030
|
}
|
|
1336
2031
|
}
|
|
1337
2032
|
}
|
|
@@ -1344,14 +2039,19 @@ function detectTransitions(previous, currentRecords, config, now) {
|
|
|
1344
2039
|
if (canAlert(prev, "stale-auto", config.cooldownMs, now)) {
|
|
1345
2040
|
const mins = Math.floor(sinceActive / 6e4);
|
|
1346
2041
|
const expectedMins = Math.floor(expectedInterval / 6e4);
|
|
1347
|
-
alerts.push(
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
2042
|
+
alerts.push(
|
|
2043
|
+
makePayload(
|
|
2044
|
+
agentId,
|
|
2045
|
+
"stale (auto)",
|
|
2046
|
+
prevStatus,
|
|
2047
|
+
currStatus,
|
|
2048
|
+
{
|
|
2049
|
+
...record,
|
|
2050
|
+
detail: `No update for ${mins}m (expected every ~${expectedMins}m). ${record.detail}`
|
|
2051
|
+
},
|
|
2052
|
+
config.dirs
|
|
2053
|
+
)
|
|
2054
|
+
);
|
|
1355
2055
|
}
|
|
1356
2056
|
}
|
|
1357
2057
|
}
|
|
@@ -1410,118 +2110,6 @@ function makePayload(agentId, condition, previousStatus, currentStatus, record,
|
|
|
1410
2110
|
};
|
|
1411
2111
|
}
|
|
1412
2112
|
|
|
1413
|
-
// src/watch-alerts.ts
|
|
1414
|
-
var import_node_https = require("https");
|
|
1415
|
-
var import_node_http = require("http");
|
|
1416
|
-
var import_node_child_process2 = require("child_process");
|
|
1417
|
-
function formatAlertMessage(payload) {
|
|
1418
|
-
const time = new Date(payload.timestamp).toISOString();
|
|
1419
|
-
const arrow = `${payload.previousStatus} \u2192 ${payload.currentStatus}`;
|
|
1420
|
-
return [
|
|
1421
|
-
`[ALERT] ${payload.condition}: "${payload.agentId}"`,
|
|
1422
|
-
` Status: ${arrow}`,
|
|
1423
|
-
payload.detail ? ` Detail: ${payload.detail}` : null,
|
|
1424
|
-
` File: ${payload.file}`,
|
|
1425
|
-
` Time: ${time}`
|
|
1426
|
-
].filter(Boolean).join("\n");
|
|
1427
|
-
}
|
|
1428
|
-
function formatTelegram(payload) {
|
|
1429
|
-
const icon = payload.condition === "recovery" ? "\u2705" : "\u26A0\uFE0F";
|
|
1430
|
-
const time = new Date(payload.timestamp).toLocaleTimeString();
|
|
1431
|
-
return [
|
|
1432
|
-
`${icon} *AgentFlow Alert*`,
|
|
1433
|
-
`*${payload.condition}*: \`${payload.agentId}\``,
|
|
1434
|
-
`Status: ${payload.previousStatus} \u2192 ${payload.currentStatus}`,
|
|
1435
|
-
payload.detail ? `Detail: ${payload.detail.slice(0, 200)}` : null,
|
|
1436
|
-
`Time: ${time}`
|
|
1437
|
-
].filter(Boolean).join("\n");
|
|
1438
|
-
}
|
|
1439
|
-
async function sendAlert(payload, channel) {
|
|
1440
|
-
try {
|
|
1441
|
-
switch (channel.type) {
|
|
1442
|
-
case "stdout":
|
|
1443
|
-
sendStdout(payload);
|
|
1444
|
-
break;
|
|
1445
|
-
case "telegram":
|
|
1446
|
-
await sendTelegram(payload, channel.botToken, channel.chatId);
|
|
1447
|
-
break;
|
|
1448
|
-
case "webhook":
|
|
1449
|
-
await sendWebhook(payload, channel.url);
|
|
1450
|
-
break;
|
|
1451
|
-
case "command":
|
|
1452
|
-
await sendCommand(payload, channel.cmd);
|
|
1453
|
-
break;
|
|
1454
|
-
}
|
|
1455
|
-
} catch (err) {
|
|
1456
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1457
|
-
console.error(`[agentflow] Failed to send ${channel.type} alert: ${msg}`);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
function sendStdout(payload) {
|
|
1461
|
-
console.log(formatAlertMessage(payload));
|
|
1462
|
-
}
|
|
1463
|
-
function sendTelegram(payload, botToken, chatId) {
|
|
1464
|
-
const body = JSON.stringify({
|
|
1465
|
-
chat_id: chatId,
|
|
1466
|
-
text: formatTelegram(payload),
|
|
1467
|
-
parse_mode: "Markdown"
|
|
1468
|
-
});
|
|
1469
|
-
return new Promise((resolve5, reject) => {
|
|
1470
|
-
const req = (0, import_node_https.request)(
|
|
1471
|
-
`https://api.telegram.org/bot${botToken}/sendMessage`,
|
|
1472
|
-
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1473
|
-
(res) => {
|
|
1474
|
-
res.resume();
|
|
1475
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
|
|
1476
|
-
else reject(new Error(`Telegram API returned ${res.statusCode}`));
|
|
1477
|
-
}
|
|
1478
|
-
);
|
|
1479
|
-
req.on("error", reject);
|
|
1480
|
-
req.write(body);
|
|
1481
|
-
req.end();
|
|
1482
|
-
});
|
|
1483
|
-
}
|
|
1484
|
-
function sendWebhook(payload, url) {
|
|
1485
|
-
const body = JSON.stringify(payload);
|
|
1486
|
-
const isHttps = url.startsWith("https");
|
|
1487
|
-
const doRequest = isHttps ? import_node_https.request : import_node_http.request;
|
|
1488
|
-
return new Promise((resolve5, reject) => {
|
|
1489
|
-
const req = doRequest(
|
|
1490
|
-
url,
|
|
1491
|
-
{ method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) } },
|
|
1492
|
-
(res) => {
|
|
1493
|
-
res.resume();
|
|
1494
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) resolve5();
|
|
1495
|
-
else reject(new Error(`Webhook returned ${res.statusCode}`));
|
|
1496
|
-
}
|
|
1497
|
-
);
|
|
1498
|
-
req.on("error", reject);
|
|
1499
|
-
req.setTimeout(1e4, () => {
|
|
1500
|
-
req.destroy(new Error("Webhook timeout"));
|
|
1501
|
-
});
|
|
1502
|
-
req.write(body);
|
|
1503
|
-
req.end();
|
|
1504
|
-
});
|
|
1505
|
-
}
|
|
1506
|
-
function sendCommand(payload, cmd) {
|
|
1507
|
-
return new Promise((resolve5, reject) => {
|
|
1508
|
-
const env = {
|
|
1509
|
-
...process.env,
|
|
1510
|
-
AGENTFLOW_ALERT_AGENT: payload.agentId,
|
|
1511
|
-
AGENTFLOW_ALERT_CONDITION: payload.condition,
|
|
1512
|
-
AGENTFLOW_ALERT_STATUS: payload.currentStatus,
|
|
1513
|
-
AGENTFLOW_ALERT_PREVIOUS_STATUS: payload.previousStatus,
|
|
1514
|
-
AGENTFLOW_ALERT_DETAIL: payload.detail,
|
|
1515
|
-
AGENTFLOW_ALERT_FILE: payload.file,
|
|
1516
|
-
AGENTFLOW_ALERT_TIMESTAMP: String(payload.timestamp)
|
|
1517
|
-
};
|
|
1518
|
-
(0, import_node_child_process2.exec)(cmd, { env, timeout: 3e4 }, (err) => {
|
|
1519
|
-
if (err) reject(err);
|
|
1520
|
-
else resolve5();
|
|
1521
|
-
});
|
|
1522
|
-
});
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
2113
|
// src/watch.ts
|
|
1526
2114
|
function parseWatchArgs(argv) {
|
|
1527
2115
|
const dirs = [];
|
|
@@ -1563,7 +2151,9 @@ function parseWatchArgs(argv) {
|
|
|
1563
2151
|
if (botToken && chatId) {
|
|
1564
2152
|
notifyChannels.push({ type: "telegram", botToken, chatId });
|
|
1565
2153
|
} else {
|
|
1566
|
-
console.error(
|
|
2154
|
+
console.error(
|
|
2155
|
+
"Warning: --notify telegram requires AGENTFLOW_TELEGRAM_BOT_TOKEN and AGENTFLOW_TELEGRAM_CHAT_ID env vars"
|
|
2156
|
+
);
|
|
1567
2157
|
}
|
|
1568
2158
|
} else if (val.startsWith("webhook:")) {
|
|
1569
2159
|
notifyChannels.push({ type: "webhook", url: val.slice(8) });
|
|
@@ -1615,7 +2205,8 @@ function parseWatchArgs(argv) {
|
|
|
1615
2205
|
};
|
|
1616
2206
|
}
|
|
1617
2207
|
function printWatchUsage() {
|
|
1618
|
-
console.log(
|
|
2208
|
+
console.log(
|
|
2209
|
+
`
|
|
1619
2210
|
AgentFlow Watch \u2014 headless alert system for agent infrastructure.
|
|
1620
2211
|
|
|
1621
2212
|
Polls directories for JSON/JSONL files, detects failures and stale
|
|
@@ -1658,7 +2249,8 @@ Examples:
|
|
|
1658
2249
|
agentflow watch ./data ./cron --notify telegram --poll 60
|
|
1659
2250
|
agentflow watch ./traces --notify webhook:https://hooks.slack.com/... --alert-on consecutive-errors:3
|
|
1660
2251
|
agentflow watch ./data --notify "command:curl -X POST https://my-pagerduty/alert"
|
|
1661
|
-
`.trim()
|
|
2252
|
+
`.trim()
|
|
2253
|
+
);
|
|
1662
2254
|
}
|
|
1663
2255
|
function startWatch(argv) {
|
|
1664
2256
|
const config = parseWatchArgs(argv);
|
|
@@ -1687,7 +2279,9 @@ agentflow watch started`);
|
|
|
1687
2279
|
console.log(` Directories: ${valid.join(", ")}`);
|
|
1688
2280
|
console.log(` Poll: ${config.pollIntervalMs / 1e3}s`);
|
|
1689
2281
|
console.log(` Alert on: ${condLabels.join(", ")}`);
|
|
1690
|
-
console.log(
|
|
2282
|
+
console.log(
|
|
2283
|
+
` Notify: stdout${channelLabels.length > 0 ? ", " + channelLabels.join(", ") : ""}`
|
|
2284
|
+
);
|
|
1691
2285
|
console.log(` Cooldown: ${Math.floor(config.cooldownMs / 6e4)}m`);
|
|
1692
2286
|
console.log(` State: ${config.stateFilePath}`);
|
|
1693
2287
|
console.log(` Hostname: ${(0, import_node_os.hostname)()}`);
|
|
@@ -1713,9 +2307,13 @@ agentflow watch started`);
|
|
|
1713
2307
|
if (pollCount % 10 === 0) {
|
|
1714
2308
|
const agentCount = Object.keys(state.agents).length;
|
|
1715
2309
|
const errorCount = Object.values(state.agents).filter((a) => a.lastStatus === "error").length;
|
|
1716
|
-
const runningCount = Object.values(state.agents).filter(
|
|
2310
|
+
const runningCount = Object.values(state.agents).filter(
|
|
2311
|
+
(a) => a.lastStatus === "running"
|
|
2312
|
+
).length;
|
|
1717
2313
|
const time = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
1718
|
-
console.log(
|
|
2314
|
+
console.log(
|
|
2315
|
+
`[${time}] heartbeat: ${agentCount} agents, ${runningCount} running, ${errorCount} errors, ${files.length} files`
|
|
2316
|
+
);
|
|
1719
2317
|
}
|
|
1720
2318
|
}
|
|
1721
2319
|
poll();
|
|
@@ -1733,7 +2331,8 @@ agentflow watch started`);
|
|
|
1733
2331
|
|
|
1734
2332
|
// src/cli.ts
|
|
1735
2333
|
function printHelp() {
|
|
1736
|
-
console.log(
|
|
2334
|
+
console.log(
|
|
2335
|
+
`
|
|
1737
2336
|
AgentFlow CLI \u2014 execution tracing and live monitoring for AI agent systems.
|
|
1738
2337
|
|
|
1739
2338
|
Usage:
|
|
@@ -1743,6 +2342,7 @@ Commands:
|
|
|
1743
2342
|
run [options] -- <cmd> Wrap a command with automatic execution tracing
|
|
1744
2343
|
live [dir...] [options] Real-time terminal monitor (auto-detects any JSON/JSONL)
|
|
1745
2344
|
watch [dir...] [options] Headless alert system \u2014 detects failures, sends notifications
|
|
2345
|
+
trace <command> [options] Inspect saved execution traces (list, show, timeline, stuck, loops)
|
|
1746
2346
|
|
|
1747
2347
|
Run \`agentflow <command> --help\` for command-specific options.
|
|
1748
2348
|
|
|
@@ -1752,7 +2352,8 @@ Examples:
|
|
|
1752
2352
|
agentflow live ./traces ./cron ./workers -R
|
|
1753
2353
|
agentflow watch ./data --alert-on error --notify telegram
|
|
1754
2354
|
agentflow watch ./data ./cron --alert-on stale:15m --notify webhook:https://...
|
|
1755
|
-
`.trim()
|
|
2355
|
+
`.trim()
|
|
2356
|
+
);
|
|
1756
2357
|
}
|
|
1757
2358
|
function parseRunArgs(argv) {
|
|
1758
2359
|
const result = {
|
|
@@ -1813,7 +2414,8 @@ function parseRunArgs(argv) {
|
|
|
1813
2414
|
return result;
|
|
1814
2415
|
}
|
|
1815
2416
|
function printRunUsage() {
|
|
1816
|
-
console.log(
|
|
2417
|
+
console.log(
|
|
2418
|
+
`
|
|
1817
2419
|
AgentFlow Run \u2014 wrap any command with automatic execution tracing.
|
|
1818
2420
|
|
|
1819
2421
|
Usage:
|
|
@@ -1831,7 +2433,8 @@ Examples:
|
|
|
1831
2433
|
agentflow run -- python -m myagent process
|
|
1832
2434
|
agentflow run --watch-dir ./data -- python worker.py
|
|
1833
2435
|
agentflow run --traces-dir ./my-traces --agent-id recon -- node agent.js
|
|
1834
|
-
`.trim()
|
|
2436
|
+
`.trim()
|
|
2437
|
+
);
|
|
1835
2438
|
}
|
|
1836
2439
|
async function runCommand(argv) {
|
|
1837
2440
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -1840,7 +2443,9 @@ async function runCommand(argv) {
|
|
|
1840
2443
|
}
|
|
1841
2444
|
const parsed = parseRunArgs(argv);
|
|
1842
2445
|
if (parsed.command.length === 0) {
|
|
1843
|
-
console.error(
|
|
2446
|
+
console.error(
|
|
2447
|
+
"Error: No command specified. Use -- to separate agentflow flags from the command."
|
|
2448
|
+
);
|
|
1844
2449
|
console.error("Example: agentflow run -- python -m myagent process");
|
|
1845
2450
|
process.exit(1);
|
|
1846
2451
|
}
|
|
@@ -1864,15 +2469,17 @@ async function runCommand(argv) {
|
|
|
1864
2469
|
try {
|
|
1865
2470
|
const result = await runTraced(config);
|
|
1866
2471
|
console.log("");
|
|
1867
|
-
console.log(
|
|
2472
|
+
console.log(
|
|
2473
|
+
`\u2705 Command completed (exit code ${result.exitCode}, ${result.duration.toFixed(1)}s)`
|
|
2474
|
+
);
|
|
1868
2475
|
if (result.tracePaths.length > 0) {
|
|
1869
2476
|
console.log("\u{1F4DD} Traces saved:");
|
|
1870
2477
|
const orchPath = result.tracePaths[0];
|
|
1871
|
-
const orchName = (0,
|
|
2478
|
+
const orchName = (0, import_path3.basename)(orchPath, ".json").split("-")[0] ?? "orchestrator";
|
|
1872
2479
|
console.log(` ${orchName.padEnd(14)} \u2192 ${orchPath}`);
|
|
1873
2480
|
for (let i = 1; i < result.tracePaths.length; i++) {
|
|
1874
2481
|
const tPath = result.tracePaths[i];
|
|
1875
|
-
const name = (0,
|
|
2482
|
+
const name = (0, import_path3.basename)(tPath, ".json").replace(/-\d{4}-.*$/, "");
|
|
1876
2483
|
const isLast = i === result.tracePaths.length - 1;
|
|
1877
2484
|
const prefix = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
1878
2485
|
console.log(` ${prefix} ${name.padEnd(12)} \u2192 ${tPath} (state changed)`);
|
|
@@ -1889,7 +2496,7 @@ async function runCommand(argv) {
|
|
|
1889
2496
|
}
|
|
1890
2497
|
async function main() {
|
|
1891
2498
|
const argv = process.argv.slice(2);
|
|
1892
|
-
const knownCommands = ["run", "live", "watch"];
|
|
2499
|
+
const knownCommands = ["run", "live", "watch", "trace"];
|
|
1893
2500
|
if (argv.length === 0 || !knownCommands.includes(argv[0]) && (argv.includes("--help") || argv.includes("-h"))) {
|
|
1894
2501
|
printHelp();
|
|
1895
2502
|
process.exit(0);
|
|
@@ -1905,6 +2512,9 @@ async function main() {
|
|
|
1905
2512
|
case "watch":
|
|
1906
2513
|
startWatch(argv);
|
|
1907
2514
|
break;
|
|
2515
|
+
case "trace":
|
|
2516
|
+
await handleTrace(argv);
|
|
2517
|
+
break;
|
|
1908
2518
|
default:
|
|
1909
2519
|
if (!subcommand?.startsWith("-")) {
|
|
1910
2520
|
startLive(["live", ...argv]);
|