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/CHANGELOG.md +12 -2
- package/LICENSE +21 -21
- package/README.md +297 -51
- package/SECURITY.md +1 -1
- package/awi.config.example.json +5 -0
- package/dist/cli/index.js +173 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +29 -5
- package/dist/index.js +112 -24
- package/dist/index.js.map +1 -1
- package/docs/architecture.md +1 -1
- package/docs/assets/architecture.svg +139 -0
- package/docs/assets/banner.svg +105 -0
- package/docs/assets/detection-patterns.svg +134 -0
- package/docs/assets/terminal-preview.svg +93 -0
- package/docs/ci-integration.md +33 -5
- package/docs/configuration.md +2 -0
- package/docs/detection-rules.md +11 -1
- package/docs/getting-started.md +43 -2
- package/docs/limitations.md +4 -0
- package/docs/releases/v1.0.0.md +3 -3
- package/docs/releasing.md +16 -105
- package/docs/report-schema.md +14 -7
- package/docs/schemas/config.schema.json +40 -2
- package/docs/schemas/report.schema.json +49 -8
- package/docs/security.md +26 -3
- package/docs/verification.md +9 -3
- package/examples/argument-drift.config.json +9 -0
- package/examples/suspicious/argument-drift.json +62 -0
- package/package.json +3 -2
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:
|
|
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
|
-
|
|
717
|
-
(a, b) => a.traceId
|
|
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:
|
|
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 =
|
|
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(
|
|
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
|
|
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);
|