agent-workflow-inspector 0.0.0-stage → 1.0.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.
@@ -0,0 +1,1179 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { constants } from "fs";
5
+ import { Command, Option } from "commander";
6
+ import { open, lstat, opendir, realpath, link, unlink } from "fs/promises";
7
+ import { resolve, join, dirname, parse as parsePath } from "path";
8
+ import { randomUUID } from "crypto";
9
+
10
+ // src/core/analysis.ts
11
+ import { z as z3 } from "zod";
12
+
13
+ // src/security/index.ts
14
+ var LIMITS = Object.freeze({
15
+ fileBytes: 16 * 1024 * 1024,
16
+ spans: 5e4,
17
+ traces: 5e4,
18
+ repetitionWindow: 100,
19
+ aggregateBytes: 64 * 1024 * 1024,
20
+ processedBytes: 64 * 1024 * 1024,
21
+ depth: 64,
22
+ stringLength: 65536,
23
+ nodes: 1e6,
24
+ files: 256,
25
+ findings: 1e3
26
+ });
27
+ var AwiError = class extends Error {
28
+ };
29
+ function safeText(value) {
30
+ return value.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, "?");
31
+ }
32
+ function validateValue(value, allowUndefinedProperties = false) {
33
+ let nodes = 0;
34
+ let bytes = 0;
35
+ function charge(amount) {
36
+ bytes += amount;
37
+ if (bytes > LIMITS.processedBytes)
38
+ throw new AwiError("JSON content exceeds processed size limit");
39
+ }
40
+ function visit(item, depth) {
41
+ if (++nodes > LIMITS.nodes || depth > LIMITS.depth)
42
+ throw new AwiError("JSON structure exceeds resource limits");
43
+ if (typeof item === "string" && item.length > LIMITS.stringLength)
44
+ throw new AwiError("JSON string exceeds resource limit");
45
+ if (typeof item === "number" && !Number.isFinite(item))
46
+ throw new AwiError("Non-finite number");
47
+ if (item !== null && typeof item === "object") {
48
+ charge(2);
49
+ const array = Array.isArray(item);
50
+ const prototype = Object.getPrototypeOf(item);
51
+ if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null)
52
+ throw new AwiError("Only plain JSON objects and arrays are supported");
53
+ if (array && nodes + item.length > LIMITS.nodes)
54
+ throw new AwiError("JSON structure exceeds resource limits");
55
+ const keys = Reflect.ownKeys(item);
56
+ if (nodes + keys.length - (array ? 1 : 0) > LIMITS.nodes)
57
+ throw new AwiError("JSON structure exceeds resource limits");
58
+ let arrayIndex = 0;
59
+ for (const key of keys) {
60
+ if (typeof key === "symbol")
61
+ throw new AwiError("Symbol keys are not JSON");
62
+ if (array && key === "length") continue;
63
+ if (array && key !== String(arrayIndex++))
64
+ throw new AwiError("Sparse or extended arrays are not JSON");
65
+ if (key.length > LIMITS.stringLength)
66
+ throw new AwiError("JSON key exceeds resource limit");
67
+ if (["__proto__", "prototype", "constructor"].includes(key))
68
+ throw new AwiError("Unsafe object key");
69
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
70
+ if (descriptor.get || descriptor.set)
71
+ throw new AwiError("Accessors are not JSON");
72
+ if (!descriptor.enumerable)
73
+ throw new AwiError("Non-enumerable properties are not JSON");
74
+ if (descriptor.value === void 0 && allowUndefinedProperties && !array)
75
+ continue;
76
+ if (!array) charge(Buffer.byteLength(JSON.stringify(key)) + 1);
77
+ charge(1);
78
+ visit(descriptor.value, depth + 1);
79
+ }
80
+ if (array && arrayIndex !== item.length)
81
+ throw new AwiError("Sparse or extended arrays are not JSON");
82
+ } else if (!["string", "number", "boolean"].includes(typeof item) && item !== null)
83
+ throw new AwiError("Unsupported JSON value");
84
+ else charge(Buffer.byteLength(JSON.stringify(item)));
85
+ }
86
+ visit(value, 0);
87
+ }
88
+ function parseJson(text) {
89
+ if (Buffer.byteLength(text) > LIMITS.fileBytes)
90
+ throw new AwiError("Input exceeds maximum file size");
91
+ let value;
92
+ try {
93
+ value = JSON.parse(text);
94
+ } catch {
95
+ throw new AwiError("Invalid JSON");
96
+ }
97
+ validateValue(value);
98
+ return value;
99
+ }
100
+ function redact(value) {
101
+ if (Array.isArray(value)) return value.map(redact);
102
+ if (value && typeof value === "object")
103
+ return Object.fromEntries(
104
+ Object.entries(value).map(([k, v]) => [
105
+ k,
106
+ /authorization|password|secret|token|api.?key|cookie|credential|prompt|message|output|result/i.test(
107
+ k
108
+ ) ? "[REDACTED]" : redact(v)
109
+ ])
110
+ );
111
+ return typeof value === "string" ? safeText(value) : value;
112
+ }
113
+ function validateIgnorePaths(paths) {
114
+ if (!Array.isArray(paths) || paths.length > 100 || paths.some(
115
+ (path) => typeof path !== "string" || path.length > LIMITS.stringLength || !/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(path) || path.split(".").length > LIMITS.depth || path.split(".").some(
116
+ (key) => ["__proto__", "constructor", "prototype"].includes(key)
117
+ )
118
+ ))
119
+ throw new AwiError("Invalid fingerprint ignore paths");
120
+ }
121
+
122
+ // src/core/timestamps.ts
123
+ function timeNanoseconds(value) {
124
+ const milliseconds = Date.parse(value);
125
+ const fraction = /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(value)?.[1] ?? "";
126
+ if (!Number.isFinite(milliseconds) || fraction.length > 9)
127
+ throw new AwiError("Invalid or unsupported timestamp precision");
128
+ return BigInt(milliseconds) * 1000000n + BigInt(fraction.padEnd(9, "0").slice(3) || "0");
129
+ }
130
+ function validTime(value) {
131
+ try {
132
+ timeNanoseconds(value);
133
+ return true;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ // src/adapters/index.ts
140
+ import { z } from "zod";
141
+ var time = z.string().datetime({ offset: true }).refine(validTime);
142
+ var count = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
143
+ var operation = z.strictObject({
144
+ id: z.string().min(1),
145
+ parentId: z.string().min(1).optional(),
146
+ type: z.enum(["tool", "agent", "model", "workflow", "generic"]),
147
+ name: z.string().min(1),
148
+ toolName: z.string().optional(),
149
+ startTime: time.optional(),
150
+ endTime: time.optional(),
151
+ status: z.enum(["ok", "error", "unknown"]).default("unknown"),
152
+ error: z.strictObject({
153
+ code: z.string().optional(),
154
+ message: z.string().optional()
155
+ }).optional(),
156
+ arguments: z.unknown().optional(),
157
+ resultMetadata: z.record(z.string(), z.unknown()).optional(),
158
+ usage: z.strictObject({
159
+ inputTokens: count.optional(),
160
+ outputTokens: count.optional(),
161
+ totalTokens: count.optional()
162
+ }).optional(),
163
+ cost: z.strictObject({
164
+ amount: z.number().finite().nonnegative(),
165
+ currency: z.string().regex(/^[A-Z]{3}$/)
166
+ }).optional(),
167
+ metadata: z.record(z.string(), z.unknown()).optional()
168
+ });
169
+ var traceSchema = z.strictObject({
170
+ version: z.literal(1),
171
+ traceId: z.string().min(1),
172
+ metadata: z.record(z.string(), z.unknown()).optional(),
173
+ operations: z.array(operation).max(LIMITS.spans)
174
+ });
175
+ var record = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : {};
176
+ function list(v) {
177
+ if (!Array.isArray(v)) throw new AwiError("Invalid OTLP array");
178
+ return v;
179
+ }
180
+ function string(v) {
181
+ return typeof v === "string" ? v : void 0;
182
+ }
183
+ function anyValue(v) {
184
+ const r = record(v);
185
+ const fields = [
186
+ "stringValue",
187
+ "boolValue",
188
+ "intValue",
189
+ "doubleValue",
190
+ "bytesValue",
191
+ "arrayValue",
192
+ "kvlistValue"
193
+ ].filter((key) => key in r);
194
+ if (!fields.length) return void 0;
195
+ if (fields.length !== 1) throw new AwiError("Invalid OTLP AnyValue");
196
+ const field = fields[0];
197
+ const value = r[field];
198
+ if (field === "stringValue" || field === "bytesValue") {
199
+ if (typeof value !== "string")
200
+ throw new AwiError("Invalid OTLP string value");
201
+ return value;
202
+ }
203
+ if (field === "boolValue") {
204
+ if (typeof value !== "boolean")
205
+ throw new AwiError("Invalid OTLP boolean value");
206
+ return value;
207
+ }
208
+ if (field === "doubleValue") {
209
+ if (typeof value !== "number" || !Number.isFinite(value))
210
+ throw new AwiError("Invalid OTLP double value");
211
+ return value;
212
+ }
213
+ if (field === "intValue") {
214
+ if ((typeof value !== "string" || !/^[-]?\d{1,19}$/.test(value)) && (typeof value !== "number" || !Number.isSafeInteger(value)))
215
+ throw new AwiError("Invalid OTLP integer value");
216
+ const integer = BigInt(value);
217
+ if (integer < -9223372036854775808n || integer > 9223372036854775807n)
218
+ throw new AwiError("OTLP integer out of range");
219
+ const number = Number(integer);
220
+ return Number.isSafeInteger(number) ? number : String(value);
221
+ }
222
+ if (value === null || typeof value !== "object" || Array.isArray(value))
223
+ throw new AwiError("Invalid OTLP structured value");
224
+ if (field === "arrayValue") {
225
+ const items = list(record(value).values ?? []).map(anyValue);
226
+ return items.some((item) => item === void 0) ? void 0 : items;
227
+ }
228
+ return attributes(record(value).values, true);
229
+ }
230
+ function attributes(v, complete = false) {
231
+ const entries = [];
232
+ for (const attribute of list(v ?? [])) {
233
+ const raw = record(attribute);
234
+ if (typeof raw.key !== "string")
235
+ throw new AwiError("Invalid OTLP attribute key");
236
+ const value = anyValue(raw.value);
237
+ if (value === void 0 && complete) return void 0;
238
+ entries.push([raw.key, value]);
239
+ }
240
+ return Object.fromEntries(
241
+ [...new Map(entries)].filter(([, value]) => value !== void 0)
242
+ );
243
+ }
244
+ function attributeEncoding(values, key) {
245
+ const attribute = list(values ?? []).findLast(
246
+ (item) => record(item).key === key
247
+ );
248
+ return record(record(attribute).value);
249
+ }
250
+ function textAttribute(values, key) {
251
+ return string(attributeEncoding(values, key).stringValue);
252
+ }
253
+ function hasBinaryValue(value) {
254
+ return value !== null && typeof value === "object" && ("bytesValue" in value || Object.values(value).some(hasBinaryValue));
255
+ }
256
+ function timestamp(v) {
257
+ if (v === void 0 || v === "0" || v === 0) return void 0;
258
+ if ((typeof v !== "string" || !/^\d{1,20}$/.test(v)) && (typeof v !== "number" || !Number.isSafeInteger(v)))
259
+ throw new AwiError("Invalid OTLP nanosecond timestamp");
260
+ const ns = BigInt(v);
261
+ if (ns === 0n) return void 0;
262
+ if (ns < 0n || ns > 18446744073709551615n)
263
+ throw new AwiError("OTLP timestamp out of range");
264
+ const iso = new Date(Number(ns / 1000000n)).toISOString();
265
+ return ns % 1000000n === 0n ? iso : iso.replace(
266
+ /\.\d{3}Z$/,
267
+ "." + (ns % 1000000000n).toString().padStart(9, "0") + "Z"
268
+ );
269
+ }
270
+ function otel(value) {
271
+ const grouped = /* @__PURE__ */ new Map();
272
+ let total = 0;
273
+ for (const resource of list(record(value).resourceSpans)) {
274
+ const r = record(resource);
275
+ const resourceAttributes = attributes(record(r.resource).attributes);
276
+ for (const scope of list(r.scopeSpans ?? []))
277
+ for (const raw of list(record(scope).spans ?? [])) {
278
+ if (++total > LIMITS.spans)
279
+ throw new AwiError("Maximum span count exceeded");
280
+ const s = record(raw);
281
+ const traceId = string(s.traceId), id = string(s.spanId);
282
+ if (!traceId || !/^[a-fA-F0-9]{32}$/.test(traceId) || /^0+$/.test(traceId) || !id || !/^[a-fA-F0-9]{16}$/.test(id) || /^0+$/.test(id))
283
+ throw new AwiError("Invalid OTLP trace/span identity");
284
+ const a = attributes(s.attributes);
285
+ const kind = textAttribute(s.attributes, "gen_ai.operation.name");
286
+ const tool = textAttribute(s.attributes, "gen_ai.tool.name") ?? textAttribute(s.attributes, "tool.name");
287
+ const type = tool || kind === "execute_tool" ? "tool" : kind === "invoke_agent" ? "agent" : [
288
+ "chat",
289
+ "generate_content",
290
+ "text_completion",
291
+ "embeddings"
292
+ ].includes(typeof kind === "string" ? kind : "") ? "model" : kind === "workflow" ? "workflow" : "generic";
293
+ const statusRecord = record(s.status);
294
+ const status = statusRecord.code;
295
+ const events = list(s.events ?? []).map(record);
296
+ const exception = events.find((e) => e.name === "exception");
297
+ if (exception) attributes(exception.attributes);
298
+ const exceptionType = textAttribute(
299
+ exception?.attributes,
300
+ "exception.type"
301
+ ), exceptionMessage = textAttribute(
302
+ exception?.attributes,
303
+ "exception.message"
304
+ );
305
+ const usage = {};
306
+ for (const [target, key2] of [
307
+ ["inputTokens", "gen_ai.usage.input_tokens"],
308
+ ["outputTokens", "gen_ai.usage.output_tokens"],
309
+ ["totalTokens", "gen_ai.usage.total_tokens"]
310
+ ]) {
311
+ const v = a[key2];
312
+ if (typeof v === "number" && Number.isSafeInteger(v) && v >= 0)
313
+ usage[target] = v;
314
+ }
315
+ let args = hasBinaryValue(
316
+ attributeEncoding(s.attributes, "gen_ai.tool.call.arguments")
317
+ ) ? void 0 : a["gen_ai.tool.call.arguments"];
318
+ if (typeof args === "string") {
319
+ try {
320
+ args = JSON.parse(args);
321
+ } catch {
322
+ }
323
+ }
324
+ const startTime = timestamp(s.startTimeUnixNano), endTime = timestamp(s.endTimeUnixNano);
325
+ const parent = string(s.parentSpanId);
326
+ if (s.parentSpanId !== void 0 && typeof s.parentSpanId !== "string")
327
+ throw new AwiError("Invalid OTLP parent identity");
328
+ if (parent && !/^[a-fA-F0-9]{16}$/.test(parent))
329
+ throw new AwiError("Invalid OTLP parent identity");
330
+ const op = {
331
+ id: id.toLowerCase(),
332
+ type,
333
+ name: string(s.name) || "unnamed",
334
+ status: status === 2 || status === "STATUS_CODE_ERROR" ? "error" : status === 1 || status === "STATUS_CODE_OK" ? "ok" : "unknown",
335
+ ...parent && !/^0+$/.test(parent) ? { parentId: parent.toLowerCase() } : {},
336
+ ...tool ? { toolName: tool } : {},
337
+ ...startTime ? { startTime } : {},
338
+ ...endTime ? { endTime } : {},
339
+ ...args !== void 0 ? { arguments: args } : {},
340
+ ...Object.keys(usage).length ? { usage } : {},
341
+ ...exception || (status === 2 || status === "STATUS_CODE_ERROR") && typeof statusRecord.message === "string" ? {
342
+ error: {
343
+ ...exceptionType !== void 0 ? { code: exceptionType } : {},
344
+ ...exceptionMessage !== void 0 ? { message: exceptionMessage } : typeof statusRecord.message === "string" ? { message: statusRecord.message } : {}
345
+ }
346
+ } : {},
347
+ metadata: {
348
+ "awi.operation_name_recorded": typeof s.name === "string" && s.name.length > 0,
349
+ attributes: a,
350
+ resource: resourceAttributes,
351
+ scope: record(scope).scope ?? {},
352
+ events: s.events ?? []
353
+ },
354
+ source: { format: "otel", index: total - 1 }
355
+ };
356
+ if (args !== void 0) validateValue(args);
357
+ const key = traceId.toLowerCase();
358
+ if (!grouped.has(key))
359
+ grouped.set(key, { version: 1, traceId: key, operations: [] });
360
+ grouped.get(key).operations.push(op);
361
+ }
362
+ }
363
+ const result = [...grouped.values()];
364
+ validateValue(result);
365
+ return result;
366
+ }
367
+ function ingest(value, format = "auto") {
368
+ validateValue(value);
369
+ const root = record(value);
370
+ const isOtel = "resourceSpans" in root;
371
+ const isAwi = "operations" in root || "traces" in root;
372
+ if (format === "auto") {
373
+ if (isOtel === isAwi)
374
+ throw new AwiError("Ambiguous or unsupported trace format");
375
+ format = isOtel ? "otel" : "awi";
376
+ }
377
+ if (format === "otel") return otel(value);
378
+ if (format !== "awi") throw new AwiError("Unsupported input format");
379
+ const collection = "traces" in root ? z.strictObject({
380
+ version: z.literal(1),
381
+ traces: z.array(traceSchema).max(LIMITS.traces)
382
+ }).safeParse(value) : null;
383
+ if (collection) {
384
+ if (!collection.success) throw new AwiError("Invalid AWI trace collection");
385
+ if (collection.data.traces.reduce((n, t) => n + t.operations.length, 0) > LIMITS.spans)
386
+ throw new AwiError("Maximum total span count exceeded");
387
+ return collection.data.traces.map((t) => ({
388
+ ...t,
389
+ operations: t.operations.map((op, index) => ({
390
+ ...op,
391
+ source: { format: "awi", index }
392
+ }))
393
+ }));
394
+ }
395
+ const parsed = traceSchema.safeParse(value);
396
+ if (!parsed.success)
397
+ throw new AwiError(
398
+ "Invalid AWI trace schema: " + parsed.error.issues.map((i) => i.path.join(".") + ": " + i.code).join("; ")
399
+ );
400
+ return [
401
+ {
402
+ ...parsed.data,
403
+ operations: parsed.data.operations.map((op, index) => ({
404
+ ...op,
405
+ source: { format: "awi", index }
406
+ }))
407
+ }
408
+ ];
409
+ }
410
+
411
+ // src/config/index.ts
412
+ import { z as z2 } from "zod";
413
+ var positive = z2.number().int().min(1);
414
+ var repeat = (threshold) => z2.strictObject({
415
+ enabled: z2.boolean().default(true),
416
+ threshold: positive.min(2).max(LIMITS.repetitionWindow).default(threshold)
417
+ });
418
+ var configSchema = z2.strictObject({
419
+ version: z2.literal(1).default(1),
420
+ rules: z2.strictObject({
421
+ AWI001: repeat(4).default({ enabled: true, threshold: 4 }),
422
+ AWI002: repeat(3).default({ enabled: true, threshold: 3 }),
423
+ AWI003: z2.strictObject({
424
+ enabled: z2.boolean().default(true),
425
+ minRepetitions: positive.min(2).max(100).default(3),
426
+ maxCycleLength: positive.min(2).max(16).default(6)
427
+ }).default({ enabled: true, minRepetitions: 3, maxCycleLength: 6 }),
428
+ AWI004: z2.strictObject({
429
+ enabled: z2.boolean().default(true),
430
+ threshold: positive.min(2).max(LIMITS.repetitionWindow).default(5),
431
+ windowSeconds: positive.max(3600).default(30)
432
+ }).default({ enabled: true, threshold: 5, windowSeconds: 30 }),
433
+ AWI005: z2.strictObject({
434
+ enabled: z2.boolean().default(true),
435
+ maxOperations: positive.max(5e4).default(100),
436
+ maxDepth: positive.max(1e3).default(20)
437
+ }).default({ enabled: true, maxOperations: 100, maxDepth: 20 }),
438
+ AWI006: z2.strictObject({ enabled: z2.boolean().default(true) }).default({ enabled: true })
439
+ }).default({
440
+ AWI001: { enabled: true, threshold: 4 },
441
+ AWI002: { enabled: true, threshold: 3 },
442
+ AWI003: { enabled: true, minRepetitions: 3, maxCycleLength: 6 },
443
+ AWI004: { enabled: true, threshold: 5, windowSeconds: 30 },
444
+ AWI005: { enabled: true, maxOperations: 100, maxDepth: 20 },
445
+ AWI006: { enabled: true }
446
+ }),
447
+ fingerprint: z2.strictObject({
448
+ ignorePaths: z2.array(
449
+ z2.string().regex(/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/).refine(
450
+ (p) => p.split(".").length <= LIMITS.depth && !p.split(".").some(
451
+ (k) => ["__proto__", "constructor", "prototype"].includes(k)
452
+ )
453
+ )
454
+ ).max(100).default([])
455
+ }).default({ ignorePaths: [] })
456
+ });
457
+ function resolveConfig(value = {}) {
458
+ validateValue(value);
459
+ const parsed = configSchema.safeParse(value);
460
+ if (!parsed.success)
461
+ throw new AwiError(
462
+ "Invalid configuration: " + parsed.error.issues.map((i) => i.path.join(".") + ": " + i.code).join("; ")
463
+ );
464
+ return parsed.data;
465
+ }
466
+
467
+ // src/core/fingerprinting.ts
468
+ import { createHash } from "crypto";
469
+ var ignoredPathCache = /* @__PURE__ */ new WeakMap();
470
+ function compileIgnorePaths(paths) {
471
+ if (!Array.isArray(paths) || Object.getPrototypeOf(paths) !== Array.prototype || paths.length > 100)
472
+ throw new AwiError("Invalid fingerprint ignore paths");
473
+ const values = [];
474
+ if (Reflect.ownKeys(paths).length !== paths.length + 1)
475
+ throw new AwiError("Invalid fingerprint ignore paths");
476
+ for (let index = 0; index < paths.length; index++) {
477
+ const descriptor = Object.getOwnPropertyDescriptor(paths, String(index));
478
+ if (!descriptor || descriptor.get || descriptor.set || typeof descriptor.value !== "string")
479
+ throw new AwiError("Invalid fingerprint ignore paths");
480
+ values.push(descriptor.value);
481
+ }
482
+ const cached = ignoredPathCache.get(paths);
483
+ if (cached && cached.snapshot.length === values.length && values.every((value, index) => value === cached.snapshot[index]))
484
+ return cached.root;
485
+ validateIgnorePaths(values);
486
+ const root = values.length ? { ignored: false, children: /* @__PURE__ */ new Map() } : void 0;
487
+ for (const path of values) {
488
+ let node = root;
489
+ for (const part of path.split(".")) {
490
+ let child = node.children.get(part);
491
+ if (!child) {
492
+ child = { ignored: false, children: /* @__PURE__ */ new Map() };
493
+ node.children.set(part, child);
494
+ }
495
+ node = child;
496
+ }
497
+ node.ignored = true;
498
+ }
499
+ ignoredPathCache.set(paths, { snapshot: values, root });
500
+ return root;
501
+ }
502
+ function canonicalize(value, ignorePaths = []) {
503
+ validateValue(value);
504
+ const root = compileIgnorePaths(ignorePaths);
505
+ function encode(v, node) {
506
+ if (Array.isArray(v))
507
+ return "[" + v.map((item) => encode(item, void 0)).join(",") + "]";
508
+ if (v && typeof v === "object")
509
+ return "{" + Object.keys(v).sort().filter((key) => !node?.children.get(key)?.ignored).map(
510
+ (key) => JSON.stringify(key) + ":" + encode(
511
+ v[key],
512
+ node?.children.get(key)
513
+ )
514
+ ).join(",") + "}";
515
+ return JSON.stringify(v);
516
+ }
517
+ return encode(value, root);
518
+ }
519
+ function hash(value) {
520
+ return createHash("sha256").update(canonicalize(value)).digest("hex");
521
+ }
522
+ function fingerprint(op, ignorePaths = []) {
523
+ validateValue(op, true);
524
+ compileIgnorePaths(ignorePaths);
525
+ const tool = op.toolName ?? op.name;
526
+ if (typeof tool !== "string" || !["tool", "agent", "model", "workflow", "generic"].includes(op.type))
527
+ throw new AwiError("Invalid operation fingerprint identity");
528
+ const digest = createHash("sha256").update('{"arguments":');
529
+ if (op.arguments === void 0) digest.update('{"present":false}');
530
+ else
531
+ digest.update('{"present":true,"value":').update(JSON.stringify(canonicalize(op.arguments, ignorePaths))).update("}");
532
+ return digest.update(',"tool":').update(JSON.stringify(tool)).update(',"type":').update(JSON.stringify(op.type)).update(',"version":"awi-sha256-v1"}').digest("hex");
533
+ }
534
+
535
+ // src/core/normalization.ts
536
+ function normalize(trace) {
537
+ if (trace.operations.length > LIMITS.spans)
538
+ throw new AwiError("Maximum span count exceeded");
539
+ const ids = /* @__PURE__ */ new Map();
540
+ const warnings = [];
541
+ for (const op of trace.operations) {
542
+ if (ids.has(op.id)) throw new AwiError("Duplicate operation identity");
543
+ ids.set(op.id, op);
544
+ if (op.startTime && op.endTime && timeNanoseconds(op.endTime) < timeNanoseconds(op.startTime))
545
+ throw new AwiError("End timestamp precedes start timestamp");
546
+ }
547
+ const depths = /* @__PURE__ */ new Map();
548
+ let depth = 0;
549
+ for (const op of trace.operations) {
550
+ const path = [];
551
+ const seen = /* @__PURE__ */ new Set();
552
+ let current = op;
553
+ while (current && !depths.has(current.id)) {
554
+ if (seen.has(current.id)) throw new AwiError("Cyclic parent references");
555
+ seen.add(current.id);
556
+ path.push(current);
557
+ const parent = current.parentId ? ids.get(current.parentId) : void 0;
558
+ if (current.parentId && !parent)
559
+ warnings.push(
560
+ "Missing parent reference; observed depth is a lower bound."
561
+ );
562
+ current = parent;
563
+ }
564
+ let d = current ? depths.get(current.id) : 0;
565
+ for (const item of path.reverse()) {
566
+ depths.set(item.id, ++d);
567
+ depth = Math.max(depth, d);
568
+ }
569
+ }
570
+ const children = /* @__PURE__ */ new Map();
571
+ for (const op of trace.operations) {
572
+ const key = op.parentId ?? "";
573
+ const group = children.get(key) ?? [];
574
+ group.push(op);
575
+ children.set(key, group);
576
+ }
577
+ return { trace, depth, warnings: [...new Set(warnings)], children };
578
+ }
579
+ function sequentialRuns(ops) {
580
+ if (ops.some(
581
+ (o) => !o.startTime || !o.endTime || timeNanoseconds(o.endTime) <= timeNanoseconds(o.startTime)
582
+ ))
583
+ return [];
584
+ const timed = ops.map((op) => ({
585
+ op,
586
+ start: timeNanoseconds(op.startTime),
587
+ end: timeNanoseconds(op.endTime)
588
+ })).sort(
589
+ (a, b) => a.start < b.start ? -1 : a.start > b.start ? 1 : a.op.id < b.op.id ? -1 : a.op.id > b.op.id ? 1 : 0
590
+ );
591
+ const runs = [];
592
+ let run = [];
593
+ let overlapEnd;
594
+ for (const item of timed) {
595
+ if (overlapEnd !== void 0) {
596
+ if (item.start < overlapEnd) {
597
+ if (item.end > overlapEnd) overlapEnd = item.end;
598
+ continue;
599
+ }
600
+ overlapEnd = void 0;
601
+ }
602
+ const previous = run.at(-1);
603
+ if (previous && item.start < previous.end) {
604
+ run.pop();
605
+ if (run.length) runs.push(run.map((value) => value.op));
606
+ run = [];
607
+ overlapEnd = previous.end > item.end ? previous.end : item.end;
608
+ continue;
609
+ }
610
+ run.push(item);
611
+ }
612
+ if (run.length) runs.push(run.map((value) => value.op));
613
+ return runs;
614
+ }
615
+
616
+ // src/core/analysis.ts
617
+ var severityRank = {
618
+ low: 1,
619
+ medium: 2,
620
+ high: 3
621
+ };
622
+ var titles = {
623
+ AWI001: "Repeated identical tool call",
624
+ AWI002: "Repeated identical failing tool call",
625
+ AWI003: "Repeating execution cycle",
626
+ AWI004: "Retry storm",
627
+ AWI005: "Excessive execution steps or depth",
628
+ AWI006: "Recorded resource usage associated with suspicious execution"
629
+ };
630
+ function analyze(traces, configInput = {}, showContent = false) {
631
+ const config = resolveConfig(configInput);
632
+ validateValue(traces, true);
633
+ if (!Array.isArray(traces) || traces.length > LIMITS.traces)
634
+ throw new AwiError("Invalid or oversized trace collection");
635
+ const analysisTraceSchema = traceSchema.extend({
636
+ operations: z3.array(
637
+ traceSchema.shape.operations.element.extend({
638
+ source: z3.strictObject({
639
+ format: z3.enum(["awi", "otel"]),
640
+ index: z3.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER)
641
+ }).optional()
642
+ })
643
+ ).max(LIMITS.spans)
644
+ });
645
+ const validated = [];
646
+ let total = 0;
647
+ for (const trace of traces) {
648
+ if (!trace || !Array.isArray(trace.operations))
649
+ throw new AwiError("Invalid canonical trace supplied to analysis");
650
+ total += trace.operations.length;
651
+ if (total > LIMITS.spans)
652
+ throw new AwiError("Maximum total span count exceeded");
653
+ const result = analysisTraceSchema.safeParse({
654
+ ...trace,
655
+ operations: trace.operations.map(
656
+ (op) => op && typeof op === "object" ? Object.fromEntries(
657
+ Object.entries(op).filter(([, value]) => value !== void 0)
658
+ ) : op
659
+ )
660
+ });
661
+ if (!result.success)
662
+ throw new AwiError("Invalid canonical trace supplied to analysis");
663
+ if (result.data.metadata === void 0) delete result.data.metadata;
664
+ for (const op of result.data.operations) {
665
+ if (op.error)
666
+ op.error = Object.fromEntries(
667
+ Object.entries(op.error).filter(([, value]) => value !== void 0)
668
+ );
669
+ if (op.usage)
670
+ op.usage = Object.fromEntries(
671
+ Object.entries(op.usage).filter(([, value]) => value !== void 0)
672
+ );
673
+ }
674
+ validateValue(result.data);
675
+ validated.push(result.data);
676
+ }
677
+ const report = {
678
+ version: 1,
679
+ fingerprintVersion: "awi-sha256-v1",
680
+ complete: true,
681
+ summaries: [],
682
+ findings: [],
683
+ warnings: []
684
+ };
685
+ const grouped = /* @__PURE__ */ new Map();
686
+ for (const t of validated) {
687
+ const existing = grouped.get(t.traceId);
688
+ if (existing) for (const op of t.operations) existing.operations.push(op);
689
+ else grouped.set(t.traceId, { ...t, operations: [...t.operations] });
690
+ }
691
+ let capped = false;
692
+ function add(ruleId, traceId, ops, evidence, explanation, confidence = "moderate") {
693
+ if (report.findings.length >= LIMITS.findings) {
694
+ capped = true;
695
+ return;
696
+ }
697
+ report.findings.push({
698
+ ruleId,
699
+ traceId,
700
+ severity: ruleId === "AWI002" || ruleId === "AWI004" ? "high" : "medium",
701
+ confidence,
702
+ title: titles[ruleId],
703
+ description: "Heuristic finding based on recorded execution evidence; intentional behavior may match.",
704
+ explanation,
705
+ remediation: "Review the referenced operations and configured policy before changing the workflow.",
706
+ operationIds: ops.map((o) => o.id),
707
+ evidence: {
708
+ ...evidence,
709
+ references: ops.map((op) => ({
710
+ operationId: op.id,
711
+ ...op.source ? { source: { format: op.source.format, index: op.source.index } } : {}
712
+ }))
713
+ }
714
+ });
715
+ }
716
+ for (const trace of [...grouped.values()].sort(
717
+ (a, b) => a.traceId < b.traceId ? -1 : a.traceId > b.traceId ? 1 : 0
718
+ )) {
719
+ trace.operations.sort((a, b) => compare(a.id, b.id));
720
+ const normalized = normalize(trace);
721
+ report.warnings.push(...normalized.warnings);
722
+ const starts = trace.operations.flatMap(
723
+ (o) => o.startTime ? [timeNanoseconds(o.startTime)] : []
724
+ ), ends = trace.operations.flatMap(
725
+ (o) => o.endTime ? [timeNanoseconds(o.endTime)] : []
726
+ );
727
+ const leaves = trace.operations.filter(
728
+ (o) => !normalized.children.has(o.id)
729
+ );
730
+ const usage = {};
731
+ for (const key of ["inputTokens", "outputTokens", "totalTokens"]) {
732
+ const values = leaves.flatMap(
733
+ (o) => o.usage?.[key] !== void 0 ? [o.usage[key]] : []
734
+ );
735
+ if (values.length) {
736
+ const total2 = values.reduce((a, b) => a + b, 0);
737
+ if (!Number.isSafeInteger(total2))
738
+ throw new AwiError(
739
+ "Recorded token aggregate exceeds safe integer range"
740
+ );
741
+ usage[key] = total2;
742
+ }
743
+ }
744
+ report.summaries.push({
745
+ traceId: trace.traceId,
746
+ operations: trace.operations.length,
747
+ toolCalls: trace.operations.filter((o) => o.type === "tool").length,
748
+ maxDepth: normalized.depth,
749
+ ...starts.length === trace.operations.length && ends.length === trace.operations.length && starts.length ? {
750
+ durationMs: Number(
751
+ ends.reduce((a, b) => a > b ? a : b) - starts.reduce((a, b) => a < b ? a : b)
752
+ ) / 1e6
753
+ } : {},
754
+ ...Object.keys(usage).length ? { usage } : {}
755
+ });
756
+ if (trace.operations.some(
757
+ (o) => o.type === "tool" && o.arguments === void 0
758
+ ))
759
+ report.warnings.push(
760
+ "Tool calls without recorded arguments excluded from identity-based rules."
761
+ );
762
+ if (trace.operations.some((o) => o.type === "tool" && !toolIdentityKnown(o)))
763
+ report.warnings.push(
764
+ "Tool calls without recorded tool identity excluded from identity-based rules."
765
+ );
766
+ const before = report.findings.length;
767
+ const fps = new Map(
768
+ trace.operations.filter((o) => o.type === "tool" && toolIdentityKnown(o)).map((o) => [o.id, fingerprint(o, config.fingerprint.ignorePaths)])
769
+ );
770
+ for (const siblings of normalized.children.values()) {
771
+ if (siblings.some(
772
+ (o) => !o.startTime || !o.endTime || timeNanoseconds(o.endTime) <= timeNanoseconds(o.startTime)
773
+ ))
774
+ report.warnings.push(
775
+ "Sibling groups with missing or zero-duration timing excluded from sequence rules; recorded order does not establish sequential execution."
776
+ );
777
+ const runs = sequentialRuns(siblings);
778
+ if (runs.flat().length < siblings.length && siblings.every(
779
+ (o) => o.startTime && o.endTime && timeNanoseconds(o.endTime) > timeNanoseconds(o.startTime)
780
+ ))
781
+ report.warnings.push(
782
+ "Overlapping sibling operations excluded from sequence rules."
783
+ );
784
+ for (const run of runs) {
785
+ let segment = [];
786
+ const segments = [];
787
+ for (const op of run) {
788
+ if (op.type !== "tool") continue;
789
+ if (op.arguments !== void 0 && toolIdentityKnown(op))
790
+ segment.push(op);
791
+ else if (segment.length) {
792
+ segments.push(segment);
793
+ segment = [];
794
+ }
795
+ }
796
+ if (segment.length) segments.push(segment);
797
+ for (const tools of segments) {
798
+ const groups = /* @__PURE__ */ new Map();
799
+ for (let base = 0; base < tools.length; base += LIMITS.repetitionWindow) {
800
+ groups.clear();
801
+ for (const op of tools.slice(
802
+ base,
803
+ base + LIMITS.repetitionWindow
804
+ )) {
805
+ const fp = fps.get(op.id);
806
+ const group = groups.get(fp) ?? [];
807
+ group.push(op);
808
+ groups.set(fp, group);
809
+ }
810
+ for (const group of groups.values()) {
811
+ const failures = /* @__PURE__ */ new Map();
812
+ for (const op of group)
813
+ if (op.status === "error" && (!!op.error?.code?.trim() || !!op.error?.message?.trim())) {
814
+ const key = hash(op.error);
815
+ const bucket = failures.get(key) ?? [];
816
+ bucket.push(op);
817
+ failures.set(key, bucket);
818
+ }
819
+ const specific = /* @__PURE__ */ new Set();
820
+ if (config.rules.AWI002.enabled) {
821
+ for (const bucket of failures.values())
822
+ if (bucket.length >= config.rules.AWI002.threshold) {
823
+ add(
824
+ "AWI002",
825
+ trace.traceId,
826
+ bucket,
827
+ {
828
+ occurrences: bucket.length,
829
+ identicalArguments: true,
830
+ equivalentFailureOutcomes: true
831
+ },
832
+ "Equivalent recorded tool calls failed with matching error evidence. This does not establish a defective retry policy.",
833
+ "high"
834
+ );
835
+ bucket.forEach((o) => specific.add(o.id));
836
+ }
837
+ }
838
+ const general = group.filter((o) => !specific.has(o.id));
839
+ if (config.rules.AWI001.enabled && general.length >= config.rules.AWI001.threshold)
840
+ add(
841
+ "AWI001",
842
+ trace.traceId,
843
+ general,
844
+ { occurrences: general.length, identicalArguments: true },
845
+ "Identical calls occur in a bounded group of sequential sibling tool calls. Polling may be intentional.",
846
+ "high"
847
+ );
848
+ if (config.rules.AWI004.enabled) {
849
+ let cursor = 0;
850
+ while (cursor < group.length) {
851
+ const first = group[cursor];
852
+ let end = cursor + 1;
853
+ while (end < group.length && group[end - 1].status === "error" && timeNanoseconds(group[end].startTime) - timeNanoseconds(first.startTime) <= BigInt(config.rules.AWI004.windowSeconds) * 1000000000n)
854
+ end++;
855
+ const attempts = group.slice(cursor, end);
856
+ if (attempts.length >= config.rules.AWI004.threshold && attempts.slice(0, -1).every((o) => o.status === "error")) {
857
+ add(
858
+ "AWI004",
859
+ trace.traceId,
860
+ attempts,
861
+ {
862
+ attempts: attempts.length,
863
+ windowSeconds: config.rules.AWI004.windowSeconds,
864
+ priorAttemptsExplicitlyFailed: true
865
+ },
866
+ "Rapid equivalent attempts follow explicit failures; retry intent remains inferred."
867
+ );
868
+ cursor = end;
869
+ } else cursor++;
870
+ }
871
+ }
872
+ }
873
+ }
874
+ if (config.rules.AWI003.enabled) {
875
+ const { maxCycleLength, minRepetitions } = config.rules.AWI003;
876
+ for (let i = 0; i < tools.length; ) {
877
+ let consumed = 0;
878
+ for (let length = 2; length <= maxCycleLength && i + length * minRepetitions <= tools.length; length++) {
879
+ const pattern = tools.slice(i, i + length).map((o) => fps.get(o.id));
880
+ if (new Set(pattern).size < 2) continue;
881
+ let repeats = 1;
882
+ while (i + (repeats + 1) * length <= tools.length && tools.slice(i + repeats * length, i + (repeats + 1) * length).every((o, j) => fps.get(o.id) === pattern[j]))
883
+ repeats++;
884
+ if (repeats >= minRepetitions) {
885
+ consumed = length * repeats;
886
+ add(
887
+ "AWI003",
888
+ trace.traceId,
889
+ tools.slice(i, i + consumed),
890
+ { cycleLength: length, repetitions: repeats },
891
+ "A bounded tool fingerprint sequence repeats among timed, non-overlapping siblings. Intent and causality are unknown."
892
+ );
893
+ break;
894
+ }
895
+ }
896
+ i += consumed || 1;
897
+ }
898
+ }
899
+ }
900
+ }
901
+ }
902
+ const policy = config.rules.AWI005;
903
+ if (policy.enabled && (trace.operations.length > policy.maxOperations || normalized.depth > policy.maxDepth))
904
+ add(
905
+ "AWI005",
906
+ trace.traceId,
907
+ [],
908
+ {
909
+ operations: trace.operations.length,
910
+ maxDepth: normalized.depth,
911
+ limits: { operations: policy.maxOperations, depth: policy.maxDepth }
912
+ },
913
+ "Observed workflow size exceeds configured policy; complexity alone is not a defect.",
914
+ "high"
915
+ );
916
+ if (config.rules.AWI006.enabled) {
917
+ const related = report.findings.slice(before).filter((f) => f.ruleId !== "AWI005");
918
+ const implicated = new Set(related.flatMap((f) => f.operationIds));
919
+ const recorded = leaves.filter(
920
+ (o) => implicated.has(o.id) && (Object.keys(o.usage ?? {}).length > 0 || o.cost)
921
+ );
922
+ if (recorded.length) {
923
+ const tokens = {};
924
+ const costs = {};
925
+ for (const op of recorded) {
926
+ for (const [key, n] of Object.entries(op.usage ?? {}))
927
+ if (n !== void 0) tokens[key] = (tokens[key] ?? 0) + n;
928
+ if (op.cost)
929
+ costs[op.cost.currency] = (costs[op.cost.currency] ?? 0) + op.cost.amount;
930
+ if (Object.values(costs).some((n) => !Number.isFinite(n)))
931
+ throw new AwiError("Recorded cost aggregate exceeds numeric range");
932
+ }
933
+ add(
934
+ "AWI006",
935
+ trace.traceId,
936
+ recorded,
937
+ {
938
+ recordedUsage: tokens,
939
+ recordedCosts: costs,
940
+ coverage: "Only implicated leaf operations with recorded usage or cost",
941
+ relatedRules: [...new Set(related.map((f) => f.ruleId))]
942
+ },
943
+ "Recorded resources belong directly to operations in suspicious patterns. Association does not establish waste."
944
+ );
945
+ }
946
+ }
947
+ }
948
+ if (capped) {
949
+ report.complete = false;
950
+ report.warnings.push("Finding limit reached; report is truncated.");
951
+ }
952
+ report.findings.sort(
953
+ (a, b) => severityRank[b.severity] - severityRank[a.severity] || compare(a.traceId, b.traceId) || compare(a.ruleId, b.ruleId) || compare(a.operationIds.join("\0"), b.operationIds.join("\0"))
954
+ );
955
+ report.warnings = [...new Set(report.warnings)].sort();
956
+ try {
957
+ validateValue(report);
958
+ } catch (error) {
959
+ if (error instanceof AwiError)
960
+ throw new AwiError("Report exceeds resource limits");
961
+ throw error;
962
+ }
963
+ if (showContent)
964
+ report.content = [...grouped.values()].sort((a, b) => compare(a.traceId, b.traceId)).map((t) => ({
965
+ traceId: t.traceId,
966
+ operations: redact(t.operations)
967
+ }));
968
+ return report;
969
+ }
970
+ function compare(a, b) {
971
+ return a < b ? -1 : a > b ? 1 : 0;
972
+ }
973
+ function toolIdentityKnown(op) {
974
+ return op.source?.format !== "otel" || !!op.toolName || op.metadata?.["awi.operation_name_recorded"] !== false;
975
+ }
976
+
977
+ // src/core/reporting.ts
978
+ function humanReport(report, options = {}) {
979
+ const paint = (text, severity) => options.color ? `\x1B[${severity === "high" ? 31 : 33}m${text}\x1B[0m` : text;
980
+ const lines = [
981
+ "Agent Workflow Inspector \u2014 Find loops, retries, and wasted work in AI agent traces."
982
+ ];
983
+ for (const t of report.summaries)
984
+ lines.push(
985
+ `Trace ${safeText(t.traceId)}: ${t.operations} operations, ${t.toolCalls} tool calls, depth ${t.maxDepth}; duration ${t.durationMs === void 0 ? "unknown" : t.durationMs + " ms"}; recorded leaf usage ${t.usage ? JSON.stringify(t.usage) : "unknown"}`
986
+ );
987
+ for (const f of report.findings)
988
+ lines.push(
989
+ `
990
+ ${paint("[" + f.severity.toUpperCase() + "]", f.severity)} ${safeText(f.ruleId)} ${safeText(f.title)} (${safeText(f.confidence)} evidence)`,
991
+ ` Trace: ${safeText(f.traceId)}; operations: ${f.operationIds.map(safeText).join(", ") || "trace-wide"}`,
992
+ ` ${safeText(f.explanation)}`,
993
+ ` Evidence: ${safeText(JSON.stringify(f.evidence))}`
994
+ );
995
+ for (const warning of report.warnings)
996
+ lines.push("Warning: " + safeText(warning));
997
+ if (report.content)
998
+ lines.push(
999
+ "Content (opt-in, key-based redaction): " + safeText(JSON.stringify(report.content))
1000
+ );
1001
+ lines.push(
1002
+ `
1003
+ ${report.findings.length} findings; analysis ${report.complete ? "complete" : "INCOMPLETE"}.`
1004
+ );
1005
+ return lines.join("\n") + "\n";
1006
+ }
1007
+ function jsonReport(report) {
1008
+ return JSON.stringify(report, null, 2).replace(
1009
+ /[\u007f-\u009f\p{Cf}\p{Zl}\p{Zp}]/gu,
1010
+ (value) => value.split("").map((c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0")).join("")
1011
+ ) + "\n";
1012
+ }
1013
+
1014
+ // src/cli/index.ts
1015
+ async function noSymlinkComponents(path) {
1016
+ const absolute = resolve(path), root = parsePath(absolute).root;
1017
+ const parts = [];
1018
+ let current = absolute;
1019
+ while (current !== root) {
1020
+ parts.push(current);
1021
+ current = dirname(current);
1022
+ }
1023
+ for (const part of parts.reverse())
1024
+ if ((await lstat(part)).isSymbolicLink())
1025
+ throw new AwiError("Symlinks are not supported in paths");
1026
+ }
1027
+ async function readJson(path, maxBytes = LIMITS.fileBytes) {
1028
+ await noSymlinkComponents(path);
1029
+ const stat = await lstat(path);
1030
+ if (!stat.isFile() || stat.isSymbolicLink())
1031
+ throw new AwiError("Input must be a regular non-symlink file");
1032
+ const limit = Math.min(LIMITS.fileBytes, maxBytes);
1033
+ if (stat.size > limit)
1034
+ throw new AwiError("Input exceeds maximum allowed file or aggregate size");
1035
+ const file = await open(
1036
+ path,
1037
+ constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)
1038
+ );
1039
+ try {
1040
+ const after = await file.stat();
1041
+ if (!after.isFile() || after.dev !== stat.dev || after.ino !== stat.ino || after.size > limit)
1042
+ throw new AwiError("Invalid or changed input file");
1043
+ const chunks = [];
1044
+ let size = 0;
1045
+ while (size <= limit) {
1046
+ const chunk = Buffer.alloc(Math.min(65536, limit + 1 - size));
1047
+ const result = await file.read(chunk, 0, chunk.length, null);
1048
+ if (!result.bytesRead) break;
1049
+ size += result.bytesRead;
1050
+ if (size > limit)
1051
+ throw new AwiError(
1052
+ "Input exceeds maximum allowed file or aggregate size"
1053
+ );
1054
+ chunks.push(chunk.subarray(0, result.bytesRead));
1055
+ }
1056
+ let text;
1057
+ try {
1058
+ text = new TextDecoder("utf-8", { fatal: true }).decode(
1059
+ Buffer.concat(chunks, size)
1060
+ );
1061
+ } catch {
1062
+ throw new AwiError("Input is not valid UTF-8");
1063
+ }
1064
+ return { value: parseJson(text), bytes: size };
1065
+ } finally {
1066
+ await file.close();
1067
+ }
1068
+ }
1069
+ async function collect(paths) {
1070
+ const files = /* @__PURE__ */ new Set();
1071
+ let entries = 0;
1072
+ async function visit(path, depth) {
1073
+ if (++entries > 2048 || depth > 16)
1074
+ throw new AwiError("Directory traversal limit exceeded");
1075
+ const stat = await lstat(path);
1076
+ if (stat.isSymbolicLink()) throw new AwiError("Symlinks are not supported");
1077
+ if (stat.isDirectory()) {
1078
+ const directory = await opendir(path);
1079
+ const names = [];
1080
+ for await (const entry of directory) {
1081
+ if (entries + names.length >= 2048)
1082
+ throw new AwiError("Directory traversal limit exceeded");
1083
+ names.push(entry.name);
1084
+ }
1085
+ if (!names.length) throw new AwiError("Empty input directory");
1086
+ for (const name of names.sort()) await visit(join(path, name), depth + 1);
1087
+ } else {
1088
+ if (!stat.isFile() || !path.toLowerCase().endsWith(".json"))
1089
+ throw new AwiError("Only regular .json input files are supported");
1090
+ files.add(await realpath(path));
1091
+ if (files.size > LIMITS.files)
1092
+ throw new AwiError("Maximum input file count exceeded");
1093
+ }
1094
+ }
1095
+ for (const path of paths) {
1096
+ await noSymlinkComponents(path);
1097
+ await visit(resolve(path), 0);
1098
+ }
1099
+ return [...files].sort();
1100
+ }
1101
+ async function writeReport(path, body) {
1102
+ const target = resolve(path), temporary = join(dirname(target), ".awi-report-" + randomUUID() + ".tmp");
1103
+ await noSymlinkComponents(dirname(target));
1104
+ try {
1105
+ const file = await open(temporary, "wx", 384);
1106
+ try {
1107
+ await file.writeFile(body, "utf8");
1108
+ await file.sync();
1109
+ } finally {
1110
+ await file.close();
1111
+ }
1112
+ await link(temporary, target);
1113
+ } finally {
1114
+ await unlink(temporary).catch(() => {
1115
+ });
1116
+ }
1117
+ }
1118
+ process.stderr.on("error", () => {
1119
+ process.exitCode = 1;
1120
+ });
1121
+ process.stdout.on("error", () => {
1122
+ process.exitCode = 1;
1123
+ process.stderr.write("awi: Unable to write report output\n");
1124
+ });
1125
+ var program = new Command().configureOutput({
1126
+ writeErr: (text) => {
1127
+ process.stderr.write(safeText(text) + "\n");
1128
+ }
1129
+ }).name("awi").description("Find loops, retries, and wasted work in AI agent traces.").version("1.0.0");
1130
+ program.command("inspect").argument("<paths...>", "JSON files or directories (recursive; no symlinks)").addOption(
1131
+ new Option("--format <format>").choices(["human", "json"]).default("human")
1132
+ ).addOption(
1133
+ new Option("--input-format <format>").choices(["auto", "awi", "otel"]).default("auto")
1134
+ ).option("--config <path>", "Strict JSON configuration").addOption(
1135
+ new Option("--fail-on <severity>", "Finding threshold; default: high").choices(["low", "medium", "high", "none"]).default("high")
1136
+ ).option(
1137
+ "--output <path>",
1138
+ "Create a new report file; existing paths are refused"
1139
+ ).option("--no-color", "Disable colors (output is plain text)").option(
1140
+ "--show-content",
1141
+ "Include recorded content with key-based redaction; may expose private data"
1142
+ ).action(
1143
+ async (paths, options) => {
1144
+ const files = await collect(paths);
1145
+ const config = resolveConfig(
1146
+ options.config ? (await readJson(resolve(options.config))).value : {}
1147
+ );
1148
+ const traces = [];
1149
+ let count2 = 0;
1150
+ let totalBytes = 0;
1151
+ for (const path of files) {
1152
+ const input = await readJson(path, LIMITS.aggregateBytes - totalBytes);
1153
+ totalBytes += input.bytes;
1154
+ const imported = ingest(input.value, options.inputFormat);
1155
+ count2 += imported.reduce((n, t) => n + t.operations.length, 0);
1156
+ if (count2 > LIMITS.spans)
1157
+ throw new AwiError("Maximum total span count exceeded");
1158
+ if (traces.length + imported.length > LIMITS.traces)
1159
+ throw new AwiError("Maximum trace count exceeded");
1160
+ for (const trace of imported) traces.push(trace);
1161
+ }
1162
+ if (!traces.length) throw new AwiError("No traces found");
1163
+ const report = analyze(traces, config, options.showContent);
1164
+ const body = options.format === "json" ? jsonReport(report) : humanReport(report, {
1165
+ color: !options.output && process.env.TERM !== "dumb" && options.color !== false && !!process.stdout.isTTY && !("NO_COLOR" in process.env)
1166
+ });
1167
+ if (options.output) await writeReport(options.output, body);
1168
+ else process.stdout.write(body);
1169
+ process.exitCode = !report.complete ? 1 : options.failOn !== "none" && report.findings.some(
1170
+ (f) => severityRank[f.severity] >= severityRank[options.failOn]
1171
+ ) ? 2 : 0;
1172
+ }
1173
+ );
1174
+ program.parseAsync().catch((error) => {
1175
+ const message = error instanceof AwiError ? error.message : "Filesystem or internal operation failed";
1176
+ process.stderr.write("awi: " + safeText(message) + "\n");
1177
+ process.exitCode = 1;
1178
+ });
1179
+ //# sourceMappingURL=index.js.map