agent-workflow-inspector 1.0.0 → 2.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.
package/dist/cli/index.js CHANGED
@@ -7,6 +7,91 @@ import { open, lstat, opendir, realpath, link, unlink } from "fs/promises";
7
7
  import { resolve, join, dirname, parse as parsePath } from "path";
8
8
  import { randomUUID } from "crypto";
9
9
 
10
+ // package.json
11
+ var package_default = {
12
+ name: "agent-workflow-inspector",
13
+ version: "2.0.0",
14
+ description: "Local-first, deterministic linter for AI agent traces: detect suspicious retries, repeated tool calls, execution cycles, and resource burn.",
15
+ type: "module",
16
+ license: "MIT",
17
+ author: "Jigar Prajapati",
18
+ homepage: "https://github.com/jigar48/awi#readme",
19
+ bugs: {
20
+ url: "https://github.com/jigar48/awi/issues"
21
+ },
22
+ funding: {
23
+ type: "individual",
24
+ url: "https://buymeacoffee.com/buildwithjigar"
25
+ },
26
+ keywords: [
27
+ "ai-agents",
28
+ "agent-observability",
29
+ "opentelemetry",
30
+ "developer-tools",
31
+ "typescript",
32
+ "cli",
33
+ "ai-debugging",
34
+ "trace-analysis"
35
+ ],
36
+ repository: {
37
+ type: "git",
38
+ url: "https://github.com/jigar48/awi.git"
39
+ },
40
+ engines: {
41
+ node: "^22.18.0 || ^24.11.0 || ^26.0.0"
42
+ },
43
+ packageManager: "pnpm@11.25.0",
44
+ bin: {
45
+ awi: "dist/cli/index.js"
46
+ },
47
+ main: "dist/index.js",
48
+ types: "dist/index.d.ts",
49
+ exports: {
50
+ ".": {
51
+ types: "./dist/index.d.ts",
52
+ import: "./dist/index.js"
53
+ }
54
+ },
55
+ files: [
56
+ "dist",
57
+ "docs",
58
+ "examples",
59
+ "README.md",
60
+ "LICENSE",
61
+ "awi.config.example.json",
62
+ "CHANGELOG.md",
63
+ "SECURITY.md",
64
+ "CONTRIBUTING.md",
65
+ "CODE_OF_CONDUCT.md"
66
+ ],
67
+ scripts: {
68
+ build: "tsup",
69
+ test: "vitest run",
70
+ typecheck: "tsc --noEmit",
71
+ lint: "eslint .",
72
+ format: "prettier --write .",
73
+ "format:check": "prettier --check .",
74
+ check: "pnpm typecheck && pnpm lint && pnpm test && pnpm build",
75
+ prepack: "pnpm build",
76
+ schemas: "pnpm build && node scripts/schemas.mjs",
77
+ "smoke:package": "node scripts/package-smoke.mjs",
78
+ "verify:quickstart": "node scripts/verify-quickstart.mjs"
79
+ },
80
+ dependencies: {
81
+ commander: "14.0.3",
82
+ zod: "4.6.7"
83
+ },
84
+ devDependencies: {
85
+ typescript: "~5.9.3",
86
+ "@types/node": "^22.19.0",
87
+ tsup: "9.0.0",
88
+ vitest: "5.0.5",
89
+ eslint: "10.13.0",
90
+ "typescript-eslint": "^8.46.0",
91
+ prettier: "^3.6.2"
92
+ }
93
+ };
94
+
10
95
  // src/core/analysis.ts
11
96
  import { z as z3 } from "zod";
12
97
 
@@ -435,14 +520,20 @@ var configSchema = z2.strictObject({
435
520
  maxOperations: positive.max(5e4).default(100),
436
521
  maxDepth: positive.max(1e3).default(20)
437
522
  }).default({ enabled: true, maxOperations: 100, maxDepth: 20 }),
438
- AWI006: z2.strictObject({ enabled: z2.boolean().default(true) }).default({ enabled: true })
523
+ AWI006: z2.strictObject({ enabled: z2.boolean().default(true) }).default({ enabled: true }),
524
+ AWI007: z2.strictObject({
525
+ enabled: z2.boolean().default(false),
526
+ threshold: positive.min(4).max(LIMITS.repetitionWindow).default(4),
527
+ minDistinctArguments: positive.min(3).max(LIMITS.repetitionWindow).default(3)
528
+ }).default({ enabled: false, threshold: 4, minDistinctArguments: 3 })
439
529
  }).default({
440
530
  AWI001: { enabled: true, threshold: 4 },
441
531
  AWI002: { enabled: true, threshold: 3 },
442
532
  AWI003: { enabled: true, minRepetitions: 3, maxCycleLength: 6 },
443
533
  AWI004: { enabled: true, threshold: 5, windowSeconds: 30 },
444
534
  AWI005: { enabled: true, maxOperations: 100, maxDepth: 20 },
445
- AWI006: { enabled: true }
535
+ AWI006: { enabled: true },
536
+ AWI007: { enabled: false, threshold: 4, minDistinctArguments: 3 }
446
537
  }),
447
538
  fingerprint: z2.strictObject({
448
539
  ignorePaths: z2.array(
@@ -625,7 +716,8 @@ var titles = {
625
716
  AWI003: "Repeating execution cycle",
626
717
  AWI004: "Retry storm",
627
718
  AWI005: "Excessive execution steps or depth",
628
- AWI006: "Recorded resource usage associated with suspicious execution"
719
+ AWI006: "Recorded resource usage associated with suspicious execution",
720
+ AWI007: "Repeated failure with argument drift"
629
721
  };
630
722
  function analyze(traces, configInput = {}, showContent = false) {
631
723
  const config = resolveConfig(configInput);
@@ -675,7 +767,7 @@ function analyze(traces, configInput = {}, showContent = false) {
675
767
  validated.push(result.data);
676
768
  }
677
769
  const report = {
678
- version: 1,
770
+ version: 2,
679
771
  fingerprintVersion: "awi-sha256-v1",
680
772
  complete: true,
681
773
  summaries: [],
@@ -702,7 +794,7 @@ function analyze(traces, configInput = {}, showContent = false) {
702
794
  title: titles[ruleId],
703
795
  description: "Heuristic finding based on recorded execution evidence; intentional behavior may match.",
704
796
  explanation,
705
- remediation: "Review the referenced operations and configured policy before changing the workflow.",
797
+ remediation: ruleId === "AWI007" ? "Review the shared error code and retry policy; consider bounded retries or escalation before varying arguments further." : "Review the referenced operations and configured policy before changing the workflow.",
706
798
  operationIds: ops.map((o) => o.id),
707
799
  evidence: {
708
800
  ...evidence,
@@ -713,9 +805,10 @@ function analyze(traces, configInput = {}, showContent = false) {
713
805
  }
714
806
  });
715
807
  }
716
- for (const trace of [...grouped.values()].sort(
717
- (a, b) => a.traceId < b.traceId ? -1 : a.traceId > b.traceId ? 1 : 0
718
- )) {
808
+ const orderedTraces = [...grouped.values()].sort(
809
+ (a, b) => compare(a.traceId, b.traceId)
810
+ );
811
+ for (const trace of orderedTraces) {
719
812
  trace.operations.sort((a, b) => compare(a.id, b.id));
720
813
  const normalized = normalize(trace);
721
814
  report.warnings.push(...normalized.warnings);
@@ -782,6 +875,43 @@ function analyze(traces, configInput = {}, showContent = false) {
782
875
  "Overlapping sibling operations excluded from sequence rules."
783
876
  );
784
877
  for (const run of runs) {
878
+ if (config.rules.AWI007.enabled) {
879
+ const policy2 = config.rules.AWI007;
880
+ let failures = [];
881
+ const distinct = /* @__PURE__ */ new Set();
882
+ let tool;
883
+ let code;
884
+ const flush = () => {
885
+ if (failures.length >= policy2.threshold && distinct.size >= policy2.minDistinctArguments)
886
+ add(
887
+ "AWI007",
888
+ trace.traceId,
889
+ failures,
890
+ {
891
+ failedCalls: failures.length,
892
+ distinctArgumentFingerprints: distinct.size
893
+ },
894
+ "Consecutive recorded failures share a tool and explicit error code while argument fingerprints vary. This does not establish intent or lack of progress."
895
+ );
896
+ failures = [];
897
+ distinct.clear();
898
+ };
899
+ for (const op of run) {
900
+ if (op.type !== "tool") continue;
901
+ const fp = fps.get(op.id);
902
+ if (op.status !== "error" || !op.error?.code?.trim() || op.arguments === void 0 || !fp) {
903
+ flush();
904
+ continue;
905
+ }
906
+ const name = op.toolName ?? op.name;
907
+ if (name !== tool || op.error.code !== code) flush();
908
+ tool = name;
909
+ code = op.error.code;
910
+ failures.push(op);
911
+ distinct.add(fp);
912
+ }
913
+ flush();
914
+ }
785
915
  let segment = [];
786
916
  const segments = [];
787
917
  for (const op of run) {
@@ -936,7 +1066,12 @@ function analyze(traces, configInput = {}, showContent = false) {
936
1066
  recorded,
937
1067
  {
938
1068
  recordedUsage: tokens,
939
- recordedCosts: costs,
1069
+ recordedCosts: Object.fromEntries(
1070
+ Object.keys(costs).sort(compare).map((currency, index) => [
1071
+ `currency-${index + 1}`,
1072
+ costs[currency]
1073
+ ])
1074
+ ),
940
1075
  coverage: "Only implicated leaf operations with recorded usage or cost",
941
1076
  relatedRules: [...new Set(related.map((f) => f.ruleId))]
942
1077
  },
@@ -952,6 +1087,28 @@ function analyze(traces, configInput = {}, showContent = false) {
952
1087
  report.findings.sort(
953
1088
  (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
1089
  );
1090
+ const traceRefs = new Map(
1091
+ orderedTraces.map((trace, index) => [
1092
+ trace.traceId,
1093
+ {
1094
+ id: `trace-${index + 1}`,
1095
+ operations: new Map(
1096
+ trace.operations.map((op, index2) => [op.id, `op-${index2 + 1}`])
1097
+ )
1098
+ }
1099
+ ])
1100
+ );
1101
+ for (const summary of report.summaries)
1102
+ summary.traceId = traceRefs.get(summary.traceId).id;
1103
+ for (const finding of report.findings) {
1104
+ const refs = traceRefs.get(finding.traceId);
1105
+ finding.traceId = refs.id;
1106
+ finding.operationIds = finding.operationIds.map(
1107
+ (id) => refs.operations.get(id)
1108
+ );
1109
+ for (const reference of finding.evidence.references)
1110
+ reference.operationId = refs.operations.get(reference.operationId);
1111
+ }
955
1112
  report.warnings = [...new Set(report.warnings)].sort();
956
1113
  try {
957
1114
  validateValue(report);
@@ -961,8 +1118,9 @@ function analyze(traces, configInput = {}, showContent = false) {
961
1118
  throw error;
962
1119
  }
963
1120
  if (showContent)
964
- report.content = [...grouped.values()].sort((a, b) => compare(a.traceId, b.traceId)).map((t) => ({
965
- traceId: t.traceId,
1121
+ report.content = orderedTraces.map((t) => ({
1122
+ traceId: traceRefs.get(t.traceId).id,
1123
+ recordedTraceId: t.traceId,
966
1124
  operations: redact(t.operations)
967
1125
  }));
968
1126
  return report;
@@ -990,7 +1148,8 @@ function humanReport(report, options = {}) {
990
1148
  ${paint("[" + f.severity.toUpperCase() + "]", f.severity)} ${safeText(f.ruleId)} ${safeText(f.title)} (${safeText(f.confidence)} evidence)`,
991
1149
  ` Trace: ${safeText(f.traceId)}; operations: ${f.operationIds.map(safeText).join(", ") || "trace-wide"}`,
992
1150
  ` ${safeText(f.explanation)}`,
993
- ` Evidence: ${safeText(JSON.stringify(f.evidence))}`
1151
+ ` Evidence: ${safeText(JSON.stringify(f.evidence))}`,
1152
+ ` Remediation: ${safeText(f.remediation)}`
994
1153
  );
995
1154
  for (const warning of report.warnings)
996
1155
  lines.push("Warning: " + safeText(warning));
@@ -1126,7 +1285,7 @@ var program = new Command().configureOutput({
1126
1285
  writeErr: (text) => {
1127
1286
  process.stderr.write(safeText(text) + "\n");
1128
1287
  }
1129
- }).name("awi").description("Find loops, retries, and wasted work in AI agent traces.").version("1.0.0");
1288
+ }).name("awi").description("Find loops, retries, and wasted work in AI agent traces.").version(package_default.version);
1130
1289
  program.command("inspect").argument("<paths...>", "JSON files or directories (recursive; no symlinks)").addOption(
1131
1290
  new Option("--format <format>").choices(["human", "json"]).default("human")
1132
1291
  ).addOption(
@@ -1138,7 +1297,7 @@ program.command("inspect").argument("<paths...>", "JSON files or directories (re
1138
1297
  "Create a new report file; existing paths are refused"
1139
1298
  ).option("--no-color", "Disable colors (output is plain text)").option(
1140
1299
  "--show-content",
1141
- "Include recorded content with key-based redaction; may expose private data"
1300
+ "Include recorded IDs, labels and payloads with key-based redaction; may expose private data"
1142
1301
  ).action(
1143
1302
  async (paths, options) => {
1144
1303
  const files = await collect(paths);