@agent-inspect/circuit 2.5.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/LICENSE +21 -0
- package/dist/index.cjs +229 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.mjs +219 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentInspect contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// packages/circuit/src/analyze.ts
|
|
6
|
+
var ALL_RULES = [
|
|
7
|
+
"circuit.same-tool-repetition",
|
|
8
|
+
"circuit.same-args-repetition",
|
|
9
|
+
"circuit.max-loop-iterations",
|
|
10
|
+
"circuit.max-retries",
|
|
11
|
+
"circuit.tool-timeout",
|
|
12
|
+
"circuit.runaway-llm-loop",
|
|
13
|
+
"circuit.excessive-branch-width"
|
|
14
|
+
];
|
|
15
|
+
function closed(ruleId, message) {
|
|
16
|
+
return { ruleId, status: "closed", severity: "info", message, evidence: [] };
|
|
17
|
+
}
|
|
18
|
+
function open(ruleId, message, evidence, severity = "error") {
|
|
19
|
+
return {
|
|
20
|
+
ruleId,
|
|
21
|
+
status: severity === "warning" ? "warn" : "open",
|
|
22
|
+
severity,
|
|
23
|
+
message,
|
|
24
|
+
evidence
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function isToolEvent(event) {
|
|
28
|
+
const name = event.name.toLowerCase();
|
|
29
|
+
return event.kind === "tool" || name.startsWith("tool:") || name.startsWith("function:") || name.includes(".tool.") || name.startsWith("mcp:");
|
|
30
|
+
}
|
|
31
|
+
function isLlmEvent(event) {
|
|
32
|
+
const name = event.name.toLowerCase();
|
|
33
|
+
return event.kind === "llm" || name.startsWith("llm:") || name.includes(".llm.") || name.includes("generation");
|
|
34
|
+
}
|
|
35
|
+
function toolLabel(event) {
|
|
36
|
+
const attrs = event.attributes ?? {};
|
|
37
|
+
const fromAttr = attrs.toolName ?? attrs.tool ?? attrs.function;
|
|
38
|
+
if (typeof fromAttr === "string" && fromAttr.length > 0) return fromAttr;
|
|
39
|
+
return event.name.replace(/^(tool:|function:|mcp:)/i, "");
|
|
40
|
+
}
|
|
41
|
+
function argsHash(toolName, args) {
|
|
42
|
+
return crypto.createHash("sha256").update(`${toolName}:${JSON.stringify(args ?? null)}`).digest("hex").slice(0, 16);
|
|
43
|
+
}
|
|
44
|
+
function toolArgs(event) {
|
|
45
|
+
const attrs = event.attributes ?? {};
|
|
46
|
+
return attrs.arguments ?? attrs.args ?? attrs.input ?? attrs.parameters;
|
|
47
|
+
}
|
|
48
|
+
function durationMs(event) {
|
|
49
|
+
if (typeof event.durationMs === "number") return event.durationMs;
|
|
50
|
+
const attrs = event.attributes ?? {};
|
|
51
|
+
const fromAttr = attrs.durationMs ?? attrs.duration;
|
|
52
|
+
return typeof fromAttr === "number" ? fromAttr : void 0;
|
|
53
|
+
}
|
|
54
|
+
function attemptNumber(event) {
|
|
55
|
+
const attrs = event.attributes ?? {};
|
|
56
|
+
const value = attrs.attempt ?? attrs.retryAttempt ?? attrs.retryCount;
|
|
57
|
+
return typeof value === "number" ? value : void 0;
|
|
58
|
+
}
|
|
59
|
+
function evaluateSameToolRepetition(events, maxRepeats) {
|
|
60
|
+
const ruleId = "circuit.same-tool-repetition";
|
|
61
|
+
const counts = /* @__PURE__ */ new Map();
|
|
62
|
+
for (const event of events.filter(isToolEvent)) {
|
|
63
|
+
const label = toolLabel(event);
|
|
64
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
65
|
+
}
|
|
66
|
+
const evidence = [];
|
|
67
|
+
for (const [toolName, count] of counts) {
|
|
68
|
+
if (count > maxRepeats) {
|
|
69
|
+
evidence.push({ ruleId, toolName, count, threshold: maxRepeats });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (evidence.length === 0) {
|
|
73
|
+
return closed(ruleId, "Tool repetition within threshold.");
|
|
74
|
+
}
|
|
75
|
+
return open(ruleId, "Same tool repeated beyond threshold.", evidence);
|
|
76
|
+
}
|
|
77
|
+
function evaluateSameArgsRepetition(events, maxRepeats) {
|
|
78
|
+
const ruleId = "circuit.same-args-repetition";
|
|
79
|
+
const counts = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const event of events.filter(isToolEvent)) {
|
|
81
|
+
const label = toolLabel(event);
|
|
82
|
+
const hash = argsHash(label, toolArgs(event));
|
|
83
|
+
const key = `${label}:${hash}`;
|
|
84
|
+
const current = counts.get(key) ?? { toolName: label, count: 0 };
|
|
85
|
+
current.count += 1;
|
|
86
|
+
counts.set(key, current);
|
|
87
|
+
}
|
|
88
|
+
const evidence = [];
|
|
89
|
+
for (const entry of counts.values()) {
|
|
90
|
+
if (entry.count > maxRepeats) {
|
|
91
|
+
evidence.push({ ruleId, toolName: entry.toolName, count: entry.count, threshold: maxRepeats });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (evidence.length === 0) {
|
|
95
|
+
return closed(ruleId, "Tool argument repetition within threshold.");
|
|
96
|
+
}
|
|
97
|
+
return open(ruleId, "Same tool arguments repeated beyond threshold.", evidence);
|
|
98
|
+
}
|
|
99
|
+
function evaluateMaxLoopIterations(events, maxIterations) {
|
|
100
|
+
const ruleId = "circuit.max-loop-iterations";
|
|
101
|
+
const iterationEvents = events.filter((event) => {
|
|
102
|
+
const attrs = event.attributes ?? {};
|
|
103
|
+
return typeof attrs.iteration === "number" || event.name.toLowerCase().includes("loop");
|
|
104
|
+
});
|
|
105
|
+
const maxSeen = iterationEvents.reduce((max, event) => {
|
|
106
|
+
const attrs = event.attributes ?? {};
|
|
107
|
+
const iteration = typeof attrs.iteration === "number" ? attrs.iteration : max;
|
|
108
|
+
return Math.max(max, iteration);
|
|
109
|
+
}, iterationEvents.length);
|
|
110
|
+
if (maxSeen <= maxIterations) {
|
|
111
|
+
return closed(ruleId, "Loop iterations within threshold.");
|
|
112
|
+
}
|
|
113
|
+
return open(ruleId, "Loop iterations exceeded threshold.", [
|
|
114
|
+
{ ruleId, count: maxSeen, threshold: maxIterations }
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
function evaluateMaxRetries(events, maxRetries) {
|
|
118
|
+
const ruleId = "circuit.max-retries";
|
|
119
|
+
const attempts = events.map(attemptNumber).filter((value) => value !== void 0);
|
|
120
|
+
const maxAttempt = attempts.length > 0 ? Math.max(...attempts) : 0;
|
|
121
|
+
if (maxAttempt <= maxRetries) {
|
|
122
|
+
return closed(ruleId, "Retry count within threshold.");
|
|
123
|
+
}
|
|
124
|
+
return open(ruleId, "Retry count exceeded threshold.", [
|
|
125
|
+
{ ruleId, count: maxAttempt, threshold: maxRetries }
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
function evaluateToolTimeout(events, maxDurationMs) {
|
|
129
|
+
const ruleId = "circuit.tool-timeout";
|
|
130
|
+
const evidence = [];
|
|
131
|
+
for (const event of events.filter(isToolEvent)) {
|
|
132
|
+
const duration = durationMs(event);
|
|
133
|
+
if (duration !== void 0 && duration > maxDurationMs) {
|
|
134
|
+
evidence.push({
|
|
135
|
+
ruleId,
|
|
136
|
+
toolName: toolLabel(event),
|
|
137
|
+
count: duration,
|
|
138
|
+
threshold: maxDurationMs,
|
|
139
|
+
eventId: event.eventId
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (evidence.length === 0) {
|
|
144
|
+
return closed(ruleId, "Tool durations within timeout.");
|
|
145
|
+
}
|
|
146
|
+
return open(ruleId, "Tool call exceeded configured timeout.", evidence, "warning");
|
|
147
|
+
}
|
|
148
|
+
function evaluateRunawayLlmLoop(events, maxLlmCalls) {
|
|
149
|
+
const ruleId = "circuit.runaway-llm-loop";
|
|
150
|
+
const llmCount = events.filter(isLlmEvent).length;
|
|
151
|
+
const hasTerminal = events.some((event) => {
|
|
152
|
+
const status = (event.status ?? event.attributes?.status ?? "").toString().toLowerCase();
|
|
153
|
+
return status === "ok" || status === "success" || status === "completed";
|
|
154
|
+
});
|
|
155
|
+
if (llmCount <= maxLlmCalls || hasTerminal) {
|
|
156
|
+
return closed(ruleId, "LLM call count within threshold or run completed.");
|
|
157
|
+
}
|
|
158
|
+
return open(ruleId, "Runaway LLM loop detected.", [
|
|
159
|
+
{ ruleId, count: llmCount, threshold: maxLlmCalls }
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
function evaluateExcessiveBranchWidth(events, maxWidth) {
|
|
163
|
+
const ruleId = "circuit.excessive-branch-width";
|
|
164
|
+
const children = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const event of events) {
|
|
166
|
+
const parentId = event.parentId;
|
|
167
|
+
if (!parentId) continue;
|
|
168
|
+
children.set(parentId, (children.get(parentId) ?? 0) + 1);
|
|
169
|
+
}
|
|
170
|
+
const evidence = [];
|
|
171
|
+
for (const [parentId, count] of children) {
|
|
172
|
+
if (count > maxWidth) {
|
|
173
|
+
evidence.push({ ruleId, path: parentId, count, threshold: maxWidth });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (evidence.length === 0) {
|
|
177
|
+
return closed(ruleId, "Branch width within threshold.");
|
|
178
|
+
}
|
|
179
|
+
return open(ruleId, "Excessive parallel branch width detected.", evidence, "warning");
|
|
180
|
+
}
|
|
181
|
+
function runRule(ruleId, events, options) {
|
|
182
|
+
switch (ruleId) {
|
|
183
|
+
case "circuit.same-tool-repetition":
|
|
184
|
+
if (options.sameToolRepetition === void 0) return void 0;
|
|
185
|
+
return evaluateSameToolRepetition(events, options.sameToolRepetition.maxRepeats);
|
|
186
|
+
case "circuit.same-args-repetition":
|
|
187
|
+
if (options.sameArgsRepetition === void 0) return void 0;
|
|
188
|
+
return evaluateSameArgsRepetition(events, options.sameArgsRepetition.maxRepeats);
|
|
189
|
+
case "circuit.max-loop-iterations":
|
|
190
|
+
if (options.maxLoopIterations === void 0) return void 0;
|
|
191
|
+
return evaluateMaxLoopIterations(events, options.maxLoopIterations.maxIterations);
|
|
192
|
+
case "circuit.max-retries":
|
|
193
|
+
if (options.maxRetries === void 0) return void 0;
|
|
194
|
+
return evaluateMaxRetries(events, options.maxRetries.maxRetries);
|
|
195
|
+
case "circuit.tool-timeout":
|
|
196
|
+
if (options.toolTimeout === void 0) return void 0;
|
|
197
|
+
return evaluateToolTimeout(events, options.toolTimeout.maxDurationMs);
|
|
198
|
+
case "circuit.runaway-llm-loop":
|
|
199
|
+
if (options.runawayLlmLoop === void 0) return void 0;
|
|
200
|
+
return evaluateRunawayLlmLoop(events, options.runawayLlmLoop.maxLlmCalls);
|
|
201
|
+
case "circuit.excessive-branch-width":
|
|
202
|
+
if (options.excessiveBranchWidth === void 0) return void 0;
|
|
203
|
+
return evaluateExcessiveBranchWidth(events, options.excessiveBranchWidth.maxWidth);
|
|
204
|
+
default:
|
|
205
|
+
return void 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function runCircuits(events, options = {}) {
|
|
209
|
+
const selected = options.rules ?? ALL_RULES;
|
|
210
|
+
const results = [];
|
|
211
|
+
for (const ruleId of selected) {
|
|
212
|
+
const result = runRule(ruleId, events, options);
|
|
213
|
+
if (result) results.push(result);
|
|
214
|
+
}
|
|
215
|
+
const ok = !results.some((result) => result.status === "open" && result.severity === "error");
|
|
216
|
+
return { ok, results };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
exports.DEFAULT_CIRCUIT_RULES = ALL_RULES;
|
|
220
|
+
exports.evaluateExcessiveBranchWidth = evaluateExcessiveBranchWidth;
|
|
221
|
+
exports.evaluateMaxLoopIterations = evaluateMaxLoopIterations;
|
|
222
|
+
exports.evaluateMaxRetries = evaluateMaxRetries;
|
|
223
|
+
exports.evaluateRunawayLlmLoop = evaluateRunawayLlmLoop;
|
|
224
|
+
exports.evaluateSameArgsRepetition = evaluateSameArgsRepetition;
|
|
225
|
+
exports.evaluateSameToolRepetition = evaluateSameToolRepetition;
|
|
226
|
+
exports.evaluateToolTimeout = evaluateToolTimeout;
|
|
227
|
+
exports.runCircuits = runCircuits;
|
|
228
|
+
//# sourceMappingURL=index.cjs.map
|
|
229
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/analyze.ts"],"names":["createHash"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport type {\n CircuitEvidence,\n CircuitResult,\n CircuitRuleId,\n CircuitRunResult,\n CircuitTraceEvent,\n RunCircuitsOptions,\n} from \"./types.js\";\n\nconst ALL_RULES: CircuitRuleId[] = [\n \"circuit.same-tool-repetition\",\n \"circuit.same-args-repetition\",\n \"circuit.max-loop-iterations\",\n \"circuit.max-retries\",\n \"circuit.tool-timeout\",\n \"circuit.runaway-llm-loop\",\n \"circuit.excessive-branch-width\",\n];\n\nfunction closed(ruleId: string, message: string): CircuitResult {\n return { ruleId, status: \"closed\", severity: \"info\", message, evidence: [] };\n}\n\nfunction open(\n ruleId: string,\n message: string,\n evidence: CircuitEvidence[],\n severity: \"error\" | \"warning\" = \"error\",\n): CircuitResult {\n return {\n ruleId,\n status: severity === \"warning\" ? \"warn\" : \"open\",\n severity,\n message,\n evidence,\n };\n}\n\nfunction isToolEvent(event: CircuitTraceEvent): boolean {\n const name = event.name.toLowerCase();\n return (\n event.kind === \"tool\" ||\n name.startsWith(\"tool:\") ||\n name.startsWith(\"function:\") ||\n name.includes(\".tool.\") ||\n name.startsWith(\"mcp:\")\n );\n}\n\nfunction isLlmEvent(event: CircuitTraceEvent): boolean {\n const name = event.name.toLowerCase();\n return event.kind === \"llm\" || name.startsWith(\"llm:\") || name.includes(\".llm.\") || name.includes(\"generation\");\n}\n\nfunction toolLabel(event: CircuitTraceEvent): string {\n const attrs = event.attributes ?? {};\n const fromAttr = attrs.toolName ?? attrs.tool ?? attrs.function;\n if (typeof fromAttr === \"string\" && fromAttr.length > 0) return fromAttr;\n return event.name.replace(/^(tool:|function:|mcp:)/i, \"\");\n}\n\nfunction argsHash(toolName: string, args: unknown): string {\n return createHash(\"sha256\").update(`${toolName}:${JSON.stringify(args ?? null)}`).digest(\"hex\").slice(0, 16);\n}\n\nfunction toolArgs(event: CircuitTraceEvent): unknown {\n const attrs = event.attributes ?? {};\n return attrs.arguments ?? attrs.args ?? attrs.input ?? attrs.parameters;\n}\n\nfunction durationMs(event: CircuitTraceEvent): number | undefined {\n if (typeof event.durationMs === \"number\") return event.durationMs;\n const attrs = event.attributes ?? {};\n const fromAttr = attrs.durationMs ?? attrs.duration;\n return typeof fromAttr === \"number\" ? fromAttr : undefined;\n}\n\nfunction attemptNumber(event: CircuitTraceEvent): number | undefined {\n const attrs = event.attributes ?? {};\n const value = attrs.attempt ?? attrs.retryAttempt ?? attrs.retryCount;\n return typeof value === \"number\" ? value : undefined;\n}\n\nexport function evaluateSameToolRepetition(\n events: readonly CircuitTraceEvent[],\n maxRepeats: number,\n): CircuitResult {\n const ruleId = \"circuit.same-tool-repetition\";\n const counts = new Map<string, number>();\n for (const event of events.filter(isToolEvent)) {\n const label = toolLabel(event);\n counts.set(label, (counts.get(label) ?? 0) + 1);\n }\n const evidence: CircuitEvidence[] = [];\n for (const [toolName, count] of counts) {\n if (count > maxRepeats) {\n evidence.push({ ruleId, toolName, count, threshold: maxRepeats });\n }\n }\n if (evidence.length === 0) {\n return closed(ruleId, \"Tool repetition within threshold.\");\n }\n return open(ruleId, \"Same tool repeated beyond threshold.\", evidence);\n}\n\nexport function evaluateSameArgsRepetition(\n events: readonly CircuitTraceEvent[],\n maxRepeats: number,\n): CircuitResult {\n const ruleId = \"circuit.same-args-repetition\";\n const counts = new Map<string, { toolName: string; count: number }>();\n for (const event of events.filter(isToolEvent)) {\n const label = toolLabel(event);\n const hash = argsHash(label, toolArgs(event));\n const key = `${label}:${hash}`;\n const current = counts.get(key) ?? { toolName: label, count: 0 };\n current.count += 1;\n counts.set(key, current);\n }\n const evidence: CircuitEvidence[] = [];\n for (const entry of counts.values()) {\n if (entry.count > maxRepeats) {\n evidence.push({ ruleId, toolName: entry.toolName, count: entry.count, threshold: maxRepeats });\n }\n }\n if (evidence.length === 0) {\n return closed(ruleId, \"Tool argument repetition within threshold.\");\n }\n return open(ruleId, \"Same tool arguments repeated beyond threshold.\", evidence);\n}\n\nexport function evaluateMaxLoopIterations(\n events: readonly CircuitTraceEvent[],\n maxIterations: number,\n): CircuitResult {\n const ruleId = \"circuit.max-loop-iterations\";\n const iterationEvents = events.filter((event) => {\n const attrs = event.attributes ?? {};\n return typeof attrs.iteration === \"number\" || event.name.toLowerCase().includes(\"loop\");\n });\n const maxSeen = iterationEvents.reduce((max, event) => {\n const attrs = event.attributes ?? {};\n const iteration = typeof attrs.iteration === \"number\" ? attrs.iteration : max;\n return Math.max(max, iteration);\n }, iterationEvents.length);\n if (maxSeen <= maxIterations) {\n return closed(ruleId, \"Loop iterations within threshold.\");\n }\n return open(ruleId, \"Loop iterations exceeded threshold.\", [\n { ruleId, count: maxSeen, threshold: maxIterations },\n ]);\n}\n\nexport function evaluateMaxRetries(\n events: readonly CircuitTraceEvent[],\n maxRetries: number,\n): CircuitResult {\n const ruleId = \"circuit.max-retries\";\n const attempts = events.map(attemptNumber).filter((value): value is number => value !== undefined);\n const maxAttempt = attempts.length > 0 ? Math.max(...attempts) : 0;\n if (maxAttempt <= maxRetries) {\n return closed(ruleId, \"Retry count within threshold.\");\n }\n return open(ruleId, \"Retry count exceeded threshold.\", [\n { ruleId, count: maxAttempt, threshold: maxRetries },\n ]);\n}\n\nexport function evaluateToolTimeout(\n events: readonly CircuitTraceEvent[],\n maxDurationMs: number,\n): CircuitResult {\n const ruleId = \"circuit.tool-timeout\";\n const evidence: CircuitEvidence[] = [];\n for (const event of events.filter(isToolEvent)) {\n const duration = durationMs(event);\n if (duration !== undefined && duration > maxDurationMs) {\n evidence.push({\n ruleId,\n toolName: toolLabel(event),\n count: duration,\n threshold: maxDurationMs,\n eventId: event.eventId,\n });\n }\n }\n if (evidence.length === 0) {\n return closed(ruleId, \"Tool durations within timeout.\");\n }\n return open(ruleId, \"Tool call exceeded configured timeout.\", evidence, \"warning\");\n}\n\nexport function evaluateRunawayLlmLoop(\n events: readonly CircuitTraceEvent[],\n maxLlmCalls: number,\n): CircuitResult {\n const ruleId = \"circuit.runaway-llm-loop\";\n const llmCount = events.filter(isLlmEvent).length;\n const hasTerminal = events.some((event) => {\n const status = (event.status ?? event.attributes?.status ?? \"\").toString().toLowerCase();\n return status === \"ok\" || status === \"success\" || status === \"completed\";\n });\n if (llmCount <= maxLlmCalls || hasTerminal) {\n return closed(ruleId, \"LLM call count within threshold or run completed.\");\n }\n return open(ruleId, \"Runaway LLM loop detected.\", [\n { ruleId, count: llmCount, threshold: maxLlmCalls },\n ]);\n}\n\nexport function evaluateExcessiveBranchWidth(\n events: readonly CircuitTraceEvent[],\n maxWidth: number,\n): CircuitResult {\n const ruleId = \"circuit.excessive-branch-width\";\n const children = new Map<string, number>();\n for (const event of events) {\n const parentId = event.parentId;\n if (!parentId) continue;\n children.set(parentId, (children.get(parentId) ?? 0) + 1);\n }\n const evidence: CircuitEvidence[] = [];\n for (const [parentId, count] of children) {\n if (count > maxWidth) {\n evidence.push({ ruleId, path: parentId, count, threshold: maxWidth });\n }\n }\n if (evidence.length === 0) {\n return closed(ruleId, \"Branch width within threshold.\");\n }\n return open(ruleId, \"Excessive parallel branch width detected.\", evidence, \"warning\");\n}\n\nfunction runRule(\n ruleId: CircuitRuleId,\n events: readonly CircuitTraceEvent[],\n options: RunCircuitsOptions,\n): CircuitResult | undefined {\n switch (ruleId) {\n case \"circuit.same-tool-repetition\":\n if (options.sameToolRepetition === undefined) return undefined;\n return evaluateSameToolRepetition(events, options.sameToolRepetition.maxRepeats);\n case \"circuit.same-args-repetition\":\n if (options.sameArgsRepetition === undefined) return undefined;\n return evaluateSameArgsRepetition(events, options.sameArgsRepetition.maxRepeats);\n case \"circuit.max-loop-iterations\":\n if (options.maxLoopIterations === undefined) return undefined;\n return evaluateMaxLoopIterations(events, options.maxLoopIterations.maxIterations);\n case \"circuit.max-retries\":\n if (options.maxRetries === undefined) return undefined;\n return evaluateMaxRetries(events, options.maxRetries.maxRetries);\n case \"circuit.tool-timeout\":\n if (options.toolTimeout === undefined) return undefined;\n return evaluateToolTimeout(events, options.toolTimeout.maxDurationMs);\n case \"circuit.runaway-llm-loop\":\n if (options.runawayLlmLoop === undefined) return undefined;\n return evaluateRunawayLlmLoop(events, options.runawayLlmLoop.maxLlmCalls);\n case \"circuit.excessive-branch-width\":\n if (options.excessiveBranchWidth === undefined) return undefined;\n return evaluateExcessiveBranchWidth(events, options.excessiveBranchWidth.maxWidth);\n default:\n return undefined;\n }\n}\n\nexport function runCircuits(\n events: readonly CircuitTraceEvent[],\n options: RunCircuitsOptions = {},\n): CircuitRunResult {\n const selected = options.rules ?? ALL_RULES;\n const results: CircuitResult[] = [];\n for (const ruleId of selected) {\n const result = runRule(ruleId, events, options);\n if (result) results.push(result);\n }\n const ok = !results.some((result) => result.status === \"open\" && result.severity === \"error\");\n return { ok, results };\n}\n\nexport { ALL_RULES as DEFAULT_CIRCUIT_RULES };\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
type CircuitStatus = "closed" | "open" | "warn";
|
|
2
|
+
interface CircuitEvidence {
|
|
3
|
+
ruleId: string;
|
|
4
|
+
count?: number;
|
|
5
|
+
threshold?: number;
|
|
6
|
+
toolName?: string;
|
|
7
|
+
runId?: string;
|
|
8
|
+
eventId?: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
}
|
|
11
|
+
interface CircuitResult {
|
|
12
|
+
ruleId: string;
|
|
13
|
+
status: CircuitStatus;
|
|
14
|
+
severity: "error" | "warning" | "info";
|
|
15
|
+
message: string;
|
|
16
|
+
evidence: CircuitEvidence[];
|
|
17
|
+
}
|
|
18
|
+
interface CircuitRunResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
results: CircuitResult[];
|
|
21
|
+
}
|
|
22
|
+
type CircuitRuleId = "circuit.same-tool-repetition" | "circuit.same-args-repetition" | "circuit.max-loop-iterations" | "circuit.max-retries" | "circuit.tool-timeout" | "circuit.runaway-llm-loop" | "circuit.excessive-branch-width";
|
|
23
|
+
interface CircuitTraceEvent {
|
|
24
|
+
eventId?: string;
|
|
25
|
+
runId?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
kind?: string;
|
|
28
|
+
parentId?: string;
|
|
29
|
+
startedAt?: string;
|
|
30
|
+
endedAt?: string;
|
|
31
|
+
durationMs?: number;
|
|
32
|
+
attributes?: Record<string, unknown>;
|
|
33
|
+
status?: string;
|
|
34
|
+
}
|
|
35
|
+
interface ThresholdRule {
|
|
36
|
+
maxRepeats: number;
|
|
37
|
+
}
|
|
38
|
+
interface MaxIterationsRule {
|
|
39
|
+
maxIterations: number;
|
|
40
|
+
}
|
|
41
|
+
interface MaxRetriesRule {
|
|
42
|
+
maxRetries: number;
|
|
43
|
+
}
|
|
44
|
+
interface ToolTimeoutRule {
|
|
45
|
+
maxDurationMs: number;
|
|
46
|
+
}
|
|
47
|
+
interface RunawayLlmLoopRule {
|
|
48
|
+
maxLlmCalls: number;
|
|
49
|
+
}
|
|
50
|
+
interface ExcessiveBranchWidthRule {
|
|
51
|
+
maxWidth: number;
|
|
52
|
+
}
|
|
53
|
+
interface RunCircuitsOptions {
|
|
54
|
+
rules?: readonly CircuitRuleId[];
|
|
55
|
+
sameToolRepetition?: ThresholdRule;
|
|
56
|
+
sameArgsRepetition?: ThresholdRule;
|
|
57
|
+
maxLoopIterations?: MaxIterationsRule;
|
|
58
|
+
maxRetries?: MaxRetriesRule;
|
|
59
|
+
toolTimeout?: ToolTimeoutRule;
|
|
60
|
+
runawayLlmLoop?: RunawayLlmLoopRule;
|
|
61
|
+
excessiveBranchWidth?: ExcessiveBranchWidthRule;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
declare const ALL_RULES: CircuitRuleId[];
|
|
65
|
+
declare function evaluateSameToolRepetition(events: readonly CircuitTraceEvent[], maxRepeats: number): CircuitResult;
|
|
66
|
+
declare function evaluateSameArgsRepetition(events: readonly CircuitTraceEvent[], maxRepeats: number): CircuitResult;
|
|
67
|
+
declare function evaluateMaxLoopIterations(events: readonly CircuitTraceEvent[], maxIterations: number): CircuitResult;
|
|
68
|
+
declare function evaluateMaxRetries(events: readonly CircuitTraceEvent[], maxRetries: number): CircuitResult;
|
|
69
|
+
declare function evaluateToolTimeout(events: readonly CircuitTraceEvent[], maxDurationMs: number): CircuitResult;
|
|
70
|
+
declare function evaluateRunawayLlmLoop(events: readonly CircuitTraceEvent[], maxLlmCalls: number): CircuitResult;
|
|
71
|
+
declare function evaluateExcessiveBranchWidth(events: readonly CircuitTraceEvent[], maxWidth: number): CircuitResult;
|
|
72
|
+
declare function runCircuits(events: readonly CircuitTraceEvent[], options?: RunCircuitsOptions): CircuitRunResult;
|
|
73
|
+
|
|
74
|
+
export { type CircuitEvidence, type CircuitResult, type CircuitRuleId, type CircuitRunResult, type CircuitStatus, type CircuitTraceEvent, ALL_RULES as DEFAULT_CIRCUIT_RULES, type ExcessiveBranchWidthRule, type MaxIterationsRule, type MaxRetriesRule, type RunCircuitsOptions, type RunawayLlmLoopRule, type ThresholdRule, type ToolTimeoutRule, evaluateExcessiveBranchWidth, evaluateMaxLoopIterations, evaluateMaxRetries, evaluateRunawayLlmLoop, evaluateSameArgsRepetition, evaluateSameToolRepetition, evaluateToolTimeout, runCircuits };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
type CircuitStatus = "closed" | "open" | "warn";
|
|
2
|
+
interface CircuitEvidence {
|
|
3
|
+
ruleId: string;
|
|
4
|
+
count?: number;
|
|
5
|
+
threshold?: number;
|
|
6
|
+
toolName?: string;
|
|
7
|
+
runId?: string;
|
|
8
|
+
eventId?: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
}
|
|
11
|
+
interface CircuitResult {
|
|
12
|
+
ruleId: string;
|
|
13
|
+
status: CircuitStatus;
|
|
14
|
+
severity: "error" | "warning" | "info";
|
|
15
|
+
message: string;
|
|
16
|
+
evidence: CircuitEvidence[];
|
|
17
|
+
}
|
|
18
|
+
interface CircuitRunResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
results: CircuitResult[];
|
|
21
|
+
}
|
|
22
|
+
type CircuitRuleId = "circuit.same-tool-repetition" | "circuit.same-args-repetition" | "circuit.max-loop-iterations" | "circuit.max-retries" | "circuit.tool-timeout" | "circuit.runaway-llm-loop" | "circuit.excessive-branch-width";
|
|
23
|
+
interface CircuitTraceEvent {
|
|
24
|
+
eventId?: string;
|
|
25
|
+
runId?: string;
|
|
26
|
+
name: string;
|
|
27
|
+
kind?: string;
|
|
28
|
+
parentId?: string;
|
|
29
|
+
startedAt?: string;
|
|
30
|
+
endedAt?: string;
|
|
31
|
+
durationMs?: number;
|
|
32
|
+
attributes?: Record<string, unknown>;
|
|
33
|
+
status?: string;
|
|
34
|
+
}
|
|
35
|
+
interface ThresholdRule {
|
|
36
|
+
maxRepeats: number;
|
|
37
|
+
}
|
|
38
|
+
interface MaxIterationsRule {
|
|
39
|
+
maxIterations: number;
|
|
40
|
+
}
|
|
41
|
+
interface MaxRetriesRule {
|
|
42
|
+
maxRetries: number;
|
|
43
|
+
}
|
|
44
|
+
interface ToolTimeoutRule {
|
|
45
|
+
maxDurationMs: number;
|
|
46
|
+
}
|
|
47
|
+
interface RunawayLlmLoopRule {
|
|
48
|
+
maxLlmCalls: number;
|
|
49
|
+
}
|
|
50
|
+
interface ExcessiveBranchWidthRule {
|
|
51
|
+
maxWidth: number;
|
|
52
|
+
}
|
|
53
|
+
interface RunCircuitsOptions {
|
|
54
|
+
rules?: readonly CircuitRuleId[];
|
|
55
|
+
sameToolRepetition?: ThresholdRule;
|
|
56
|
+
sameArgsRepetition?: ThresholdRule;
|
|
57
|
+
maxLoopIterations?: MaxIterationsRule;
|
|
58
|
+
maxRetries?: MaxRetriesRule;
|
|
59
|
+
toolTimeout?: ToolTimeoutRule;
|
|
60
|
+
runawayLlmLoop?: RunawayLlmLoopRule;
|
|
61
|
+
excessiveBranchWidth?: ExcessiveBranchWidthRule;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
declare const ALL_RULES: CircuitRuleId[];
|
|
65
|
+
declare function evaluateSameToolRepetition(events: readonly CircuitTraceEvent[], maxRepeats: number): CircuitResult;
|
|
66
|
+
declare function evaluateSameArgsRepetition(events: readonly CircuitTraceEvent[], maxRepeats: number): CircuitResult;
|
|
67
|
+
declare function evaluateMaxLoopIterations(events: readonly CircuitTraceEvent[], maxIterations: number): CircuitResult;
|
|
68
|
+
declare function evaluateMaxRetries(events: readonly CircuitTraceEvent[], maxRetries: number): CircuitResult;
|
|
69
|
+
declare function evaluateToolTimeout(events: readonly CircuitTraceEvent[], maxDurationMs: number): CircuitResult;
|
|
70
|
+
declare function evaluateRunawayLlmLoop(events: readonly CircuitTraceEvent[], maxLlmCalls: number): CircuitResult;
|
|
71
|
+
declare function evaluateExcessiveBranchWidth(events: readonly CircuitTraceEvent[], maxWidth: number): CircuitResult;
|
|
72
|
+
declare function runCircuits(events: readonly CircuitTraceEvent[], options?: RunCircuitsOptions): CircuitRunResult;
|
|
73
|
+
|
|
74
|
+
export { type CircuitEvidence, type CircuitResult, type CircuitRuleId, type CircuitRunResult, type CircuitStatus, type CircuitTraceEvent, ALL_RULES as DEFAULT_CIRCUIT_RULES, type ExcessiveBranchWidthRule, type MaxIterationsRule, type MaxRetriesRule, type RunCircuitsOptions, type RunawayLlmLoopRule, type ThresholdRule, type ToolTimeoutRule, evaluateExcessiveBranchWidth, evaluateMaxLoopIterations, evaluateMaxRetries, evaluateRunawayLlmLoop, evaluateSameArgsRepetition, evaluateSameToolRepetition, evaluateToolTimeout, runCircuits };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// packages/circuit/src/analyze.ts
|
|
4
|
+
var ALL_RULES = [
|
|
5
|
+
"circuit.same-tool-repetition",
|
|
6
|
+
"circuit.same-args-repetition",
|
|
7
|
+
"circuit.max-loop-iterations",
|
|
8
|
+
"circuit.max-retries",
|
|
9
|
+
"circuit.tool-timeout",
|
|
10
|
+
"circuit.runaway-llm-loop",
|
|
11
|
+
"circuit.excessive-branch-width"
|
|
12
|
+
];
|
|
13
|
+
function closed(ruleId, message) {
|
|
14
|
+
return { ruleId, status: "closed", severity: "info", message, evidence: [] };
|
|
15
|
+
}
|
|
16
|
+
function open(ruleId, message, evidence, severity = "error") {
|
|
17
|
+
return {
|
|
18
|
+
ruleId,
|
|
19
|
+
status: severity === "warning" ? "warn" : "open",
|
|
20
|
+
severity,
|
|
21
|
+
message,
|
|
22
|
+
evidence
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function isToolEvent(event) {
|
|
26
|
+
const name = event.name.toLowerCase();
|
|
27
|
+
return event.kind === "tool" || name.startsWith("tool:") || name.startsWith("function:") || name.includes(".tool.") || name.startsWith("mcp:");
|
|
28
|
+
}
|
|
29
|
+
function isLlmEvent(event) {
|
|
30
|
+
const name = event.name.toLowerCase();
|
|
31
|
+
return event.kind === "llm" || name.startsWith("llm:") || name.includes(".llm.") || name.includes("generation");
|
|
32
|
+
}
|
|
33
|
+
function toolLabel(event) {
|
|
34
|
+
const attrs = event.attributes ?? {};
|
|
35
|
+
const fromAttr = attrs.toolName ?? attrs.tool ?? attrs.function;
|
|
36
|
+
if (typeof fromAttr === "string" && fromAttr.length > 0) return fromAttr;
|
|
37
|
+
return event.name.replace(/^(tool:|function:|mcp:)/i, "");
|
|
38
|
+
}
|
|
39
|
+
function argsHash(toolName, args) {
|
|
40
|
+
return createHash("sha256").update(`${toolName}:${JSON.stringify(args ?? null)}`).digest("hex").slice(0, 16);
|
|
41
|
+
}
|
|
42
|
+
function toolArgs(event) {
|
|
43
|
+
const attrs = event.attributes ?? {};
|
|
44
|
+
return attrs.arguments ?? attrs.args ?? attrs.input ?? attrs.parameters;
|
|
45
|
+
}
|
|
46
|
+
function durationMs(event) {
|
|
47
|
+
if (typeof event.durationMs === "number") return event.durationMs;
|
|
48
|
+
const attrs = event.attributes ?? {};
|
|
49
|
+
const fromAttr = attrs.durationMs ?? attrs.duration;
|
|
50
|
+
return typeof fromAttr === "number" ? fromAttr : void 0;
|
|
51
|
+
}
|
|
52
|
+
function attemptNumber(event) {
|
|
53
|
+
const attrs = event.attributes ?? {};
|
|
54
|
+
const value = attrs.attempt ?? attrs.retryAttempt ?? attrs.retryCount;
|
|
55
|
+
return typeof value === "number" ? value : void 0;
|
|
56
|
+
}
|
|
57
|
+
function evaluateSameToolRepetition(events, maxRepeats) {
|
|
58
|
+
const ruleId = "circuit.same-tool-repetition";
|
|
59
|
+
const counts = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const event of events.filter(isToolEvent)) {
|
|
61
|
+
const label = toolLabel(event);
|
|
62
|
+
counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
const evidence = [];
|
|
65
|
+
for (const [toolName, count] of counts) {
|
|
66
|
+
if (count > maxRepeats) {
|
|
67
|
+
evidence.push({ ruleId, toolName, count, threshold: maxRepeats });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (evidence.length === 0) {
|
|
71
|
+
return closed(ruleId, "Tool repetition within threshold.");
|
|
72
|
+
}
|
|
73
|
+
return open(ruleId, "Same tool repeated beyond threshold.", evidence);
|
|
74
|
+
}
|
|
75
|
+
function evaluateSameArgsRepetition(events, maxRepeats) {
|
|
76
|
+
const ruleId = "circuit.same-args-repetition";
|
|
77
|
+
const counts = /* @__PURE__ */ new Map();
|
|
78
|
+
for (const event of events.filter(isToolEvent)) {
|
|
79
|
+
const label = toolLabel(event);
|
|
80
|
+
const hash = argsHash(label, toolArgs(event));
|
|
81
|
+
const key = `${label}:${hash}`;
|
|
82
|
+
const current = counts.get(key) ?? { toolName: label, count: 0 };
|
|
83
|
+
current.count += 1;
|
|
84
|
+
counts.set(key, current);
|
|
85
|
+
}
|
|
86
|
+
const evidence = [];
|
|
87
|
+
for (const entry of counts.values()) {
|
|
88
|
+
if (entry.count > maxRepeats) {
|
|
89
|
+
evidence.push({ ruleId, toolName: entry.toolName, count: entry.count, threshold: maxRepeats });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (evidence.length === 0) {
|
|
93
|
+
return closed(ruleId, "Tool argument repetition within threshold.");
|
|
94
|
+
}
|
|
95
|
+
return open(ruleId, "Same tool arguments repeated beyond threshold.", evidence);
|
|
96
|
+
}
|
|
97
|
+
function evaluateMaxLoopIterations(events, maxIterations) {
|
|
98
|
+
const ruleId = "circuit.max-loop-iterations";
|
|
99
|
+
const iterationEvents = events.filter((event) => {
|
|
100
|
+
const attrs = event.attributes ?? {};
|
|
101
|
+
return typeof attrs.iteration === "number" || event.name.toLowerCase().includes("loop");
|
|
102
|
+
});
|
|
103
|
+
const maxSeen = iterationEvents.reduce((max, event) => {
|
|
104
|
+
const attrs = event.attributes ?? {};
|
|
105
|
+
const iteration = typeof attrs.iteration === "number" ? attrs.iteration : max;
|
|
106
|
+
return Math.max(max, iteration);
|
|
107
|
+
}, iterationEvents.length);
|
|
108
|
+
if (maxSeen <= maxIterations) {
|
|
109
|
+
return closed(ruleId, "Loop iterations within threshold.");
|
|
110
|
+
}
|
|
111
|
+
return open(ruleId, "Loop iterations exceeded threshold.", [
|
|
112
|
+
{ ruleId, count: maxSeen, threshold: maxIterations }
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
function evaluateMaxRetries(events, maxRetries) {
|
|
116
|
+
const ruleId = "circuit.max-retries";
|
|
117
|
+
const attempts = events.map(attemptNumber).filter((value) => value !== void 0);
|
|
118
|
+
const maxAttempt = attempts.length > 0 ? Math.max(...attempts) : 0;
|
|
119
|
+
if (maxAttempt <= maxRetries) {
|
|
120
|
+
return closed(ruleId, "Retry count within threshold.");
|
|
121
|
+
}
|
|
122
|
+
return open(ruleId, "Retry count exceeded threshold.", [
|
|
123
|
+
{ ruleId, count: maxAttempt, threshold: maxRetries }
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
126
|
+
function evaluateToolTimeout(events, maxDurationMs) {
|
|
127
|
+
const ruleId = "circuit.tool-timeout";
|
|
128
|
+
const evidence = [];
|
|
129
|
+
for (const event of events.filter(isToolEvent)) {
|
|
130
|
+
const duration = durationMs(event);
|
|
131
|
+
if (duration !== void 0 && duration > maxDurationMs) {
|
|
132
|
+
evidence.push({
|
|
133
|
+
ruleId,
|
|
134
|
+
toolName: toolLabel(event),
|
|
135
|
+
count: duration,
|
|
136
|
+
threshold: maxDurationMs,
|
|
137
|
+
eventId: event.eventId
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (evidence.length === 0) {
|
|
142
|
+
return closed(ruleId, "Tool durations within timeout.");
|
|
143
|
+
}
|
|
144
|
+
return open(ruleId, "Tool call exceeded configured timeout.", evidence, "warning");
|
|
145
|
+
}
|
|
146
|
+
function evaluateRunawayLlmLoop(events, maxLlmCalls) {
|
|
147
|
+
const ruleId = "circuit.runaway-llm-loop";
|
|
148
|
+
const llmCount = events.filter(isLlmEvent).length;
|
|
149
|
+
const hasTerminal = events.some((event) => {
|
|
150
|
+
const status = (event.status ?? event.attributes?.status ?? "").toString().toLowerCase();
|
|
151
|
+
return status === "ok" || status === "success" || status === "completed";
|
|
152
|
+
});
|
|
153
|
+
if (llmCount <= maxLlmCalls || hasTerminal) {
|
|
154
|
+
return closed(ruleId, "LLM call count within threshold or run completed.");
|
|
155
|
+
}
|
|
156
|
+
return open(ruleId, "Runaway LLM loop detected.", [
|
|
157
|
+
{ ruleId, count: llmCount, threshold: maxLlmCalls }
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
function evaluateExcessiveBranchWidth(events, maxWidth) {
|
|
161
|
+
const ruleId = "circuit.excessive-branch-width";
|
|
162
|
+
const children = /* @__PURE__ */ new Map();
|
|
163
|
+
for (const event of events) {
|
|
164
|
+
const parentId = event.parentId;
|
|
165
|
+
if (!parentId) continue;
|
|
166
|
+
children.set(parentId, (children.get(parentId) ?? 0) + 1);
|
|
167
|
+
}
|
|
168
|
+
const evidence = [];
|
|
169
|
+
for (const [parentId, count] of children) {
|
|
170
|
+
if (count > maxWidth) {
|
|
171
|
+
evidence.push({ ruleId, path: parentId, count, threshold: maxWidth });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (evidence.length === 0) {
|
|
175
|
+
return closed(ruleId, "Branch width within threshold.");
|
|
176
|
+
}
|
|
177
|
+
return open(ruleId, "Excessive parallel branch width detected.", evidence, "warning");
|
|
178
|
+
}
|
|
179
|
+
function runRule(ruleId, events, options) {
|
|
180
|
+
switch (ruleId) {
|
|
181
|
+
case "circuit.same-tool-repetition":
|
|
182
|
+
if (options.sameToolRepetition === void 0) return void 0;
|
|
183
|
+
return evaluateSameToolRepetition(events, options.sameToolRepetition.maxRepeats);
|
|
184
|
+
case "circuit.same-args-repetition":
|
|
185
|
+
if (options.sameArgsRepetition === void 0) return void 0;
|
|
186
|
+
return evaluateSameArgsRepetition(events, options.sameArgsRepetition.maxRepeats);
|
|
187
|
+
case "circuit.max-loop-iterations":
|
|
188
|
+
if (options.maxLoopIterations === void 0) return void 0;
|
|
189
|
+
return evaluateMaxLoopIterations(events, options.maxLoopIterations.maxIterations);
|
|
190
|
+
case "circuit.max-retries":
|
|
191
|
+
if (options.maxRetries === void 0) return void 0;
|
|
192
|
+
return evaluateMaxRetries(events, options.maxRetries.maxRetries);
|
|
193
|
+
case "circuit.tool-timeout":
|
|
194
|
+
if (options.toolTimeout === void 0) return void 0;
|
|
195
|
+
return evaluateToolTimeout(events, options.toolTimeout.maxDurationMs);
|
|
196
|
+
case "circuit.runaway-llm-loop":
|
|
197
|
+
if (options.runawayLlmLoop === void 0) return void 0;
|
|
198
|
+
return evaluateRunawayLlmLoop(events, options.runawayLlmLoop.maxLlmCalls);
|
|
199
|
+
case "circuit.excessive-branch-width":
|
|
200
|
+
if (options.excessiveBranchWidth === void 0) return void 0;
|
|
201
|
+
return evaluateExcessiveBranchWidth(events, options.excessiveBranchWidth.maxWidth);
|
|
202
|
+
default:
|
|
203
|
+
return void 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function runCircuits(events, options = {}) {
|
|
207
|
+
const selected = options.rules ?? ALL_RULES;
|
|
208
|
+
const results = [];
|
|
209
|
+
for (const ruleId of selected) {
|
|
210
|
+
const result = runRule(ruleId, events, options);
|
|
211
|
+
if (result) results.push(result);
|
|
212
|
+
}
|
|
213
|
+
const ok = !results.some((result) => result.status === "open" && result.severity === "error");
|
|
214
|
+
return { ok, results };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { ALL_RULES as DEFAULT_CIRCUIT_RULES, evaluateExcessiveBranchWidth, evaluateMaxLoopIterations, evaluateMaxRetries, evaluateRunawayLlmLoop, evaluateSameArgsRepetition, evaluateSameToolRepetition, evaluateToolTimeout, runCircuits };
|
|
218
|
+
//# sourceMappingURL=index.mjs.map
|
|
219
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/analyze.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport type {\n CircuitEvidence,\n CircuitResult,\n CircuitRuleId,\n CircuitRunResult,\n CircuitTraceEvent,\n RunCircuitsOptions,\n} from \"./types.js\";\n\nconst ALL_RULES: CircuitRuleId[] = [\n \"circuit.same-tool-repetition\",\n \"circuit.same-args-repetition\",\n \"circuit.max-loop-iterations\",\n \"circuit.max-retries\",\n \"circuit.tool-timeout\",\n \"circuit.runaway-llm-loop\",\n \"circuit.excessive-branch-width\",\n];\n\nfunction closed(ruleId: string, message: string): CircuitResult {\n return { ruleId, status: \"closed\", severity: \"info\", message, evidence: [] };\n}\n\nfunction open(\n ruleId: string,\n message: string,\n evidence: CircuitEvidence[],\n severity: \"error\" | \"warning\" = \"error\",\n): CircuitResult {\n return {\n ruleId,\n status: severity === \"warning\" ? \"warn\" : \"open\",\n severity,\n message,\n evidence,\n };\n}\n\nfunction isToolEvent(event: CircuitTraceEvent): boolean {\n const name = event.name.toLowerCase();\n return (\n event.kind === \"tool\" ||\n name.startsWith(\"tool:\") ||\n name.startsWith(\"function:\") ||\n name.includes(\".tool.\") ||\n name.startsWith(\"mcp:\")\n );\n}\n\nfunction isLlmEvent(event: CircuitTraceEvent): boolean {\n const name = event.name.toLowerCase();\n return event.kind === \"llm\" || name.startsWith(\"llm:\") || name.includes(\".llm.\") || name.includes(\"generation\");\n}\n\nfunction toolLabel(event: CircuitTraceEvent): string {\n const attrs = event.attributes ?? {};\n const fromAttr = attrs.toolName ?? attrs.tool ?? attrs.function;\n if (typeof fromAttr === \"string\" && fromAttr.length > 0) return fromAttr;\n return event.name.replace(/^(tool:|function:|mcp:)/i, \"\");\n}\n\nfunction argsHash(toolName: string, args: unknown): string {\n return createHash(\"sha256\").update(`${toolName}:${JSON.stringify(args ?? null)}`).digest(\"hex\").slice(0, 16);\n}\n\nfunction toolArgs(event: CircuitTraceEvent): unknown {\n const attrs = event.attributes ?? {};\n return attrs.arguments ?? attrs.args ?? attrs.input ?? attrs.parameters;\n}\n\nfunction durationMs(event: CircuitTraceEvent): number | undefined {\n if (typeof event.durationMs === \"number\") return event.durationMs;\n const attrs = event.attributes ?? {};\n const fromAttr = attrs.durationMs ?? attrs.duration;\n return typeof fromAttr === \"number\" ? fromAttr : undefined;\n}\n\nfunction attemptNumber(event: CircuitTraceEvent): number | undefined {\n const attrs = event.attributes ?? {};\n const value = attrs.attempt ?? attrs.retryAttempt ?? attrs.retryCount;\n return typeof value === \"number\" ? value : undefined;\n}\n\nexport function evaluateSameToolRepetition(\n events: readonly CircuitTraceEvent[],\n maxRepeats: number,\n): CircuitResult {\n const ruleId = \"circuit.same-tool-repetition\";\n const counts = new Map<string, number>();\n for (const event of events.filter(isToolEvent)) {\n const label = toolLabel(event);\n counts.set(label, (counts.get(label) ?? 0) + 1);\n }\n const evidence: CircuitEvidence[] = [];\n for (const [toolName, count] of counts) {\n if (count > maxRepeats) {\n evidence.push({ ruleId, toolName, count, threshold: maxRepeats });\n }\n }\n if (evidence.length === 0) {\n return closed(ruleId, \"Tool repetition within threshold.\");\n }\n return open(ruleId, \"Same tool repeated beyond threshold.\", evidence);\n}\n\nexport function evaluateSameArgsRepetition(\n events: readonly CircuitTraceEvent[],\n maxRepeats: number,\n): CircuitResult {\n const ruleId = \"circuit.same-args-repetition\";\n const counts = new Map<string, { toolName: string; count: number }>();\n for (const event of events.filter(isToolEvent)) {\n const label = toolLabel(event);\n const hash = argsHash(label, toolArgs(event));\n const key = `${label}:${hash}`;\n const current = counts.get(key) ?? { toolName: label, count: 0 };\n current.count += 1;\n counts.set(key, current);\n }\n const evidence: CircuitEvidence[] = [];\n for (const entry of counts.values()) {\n if (entry.count > maxRepeats) {\n evidence.push({ ruleId, toolName: entry.toolName, count: entry.count, threshold: maxRepeats });\n }\n }\n if (evidence.length === 0) {\n return closed(ruleId, \"Tool argument repetition within threshold.\");\n }\n return open(ruleId, \"Same tool arguments repeated beyond threshold.\", evidence);\n}\n\nexport function evaluateMaxLoopIterations(\n events: readonly CircuitTraceEvent[],\n maxIterations: number,\n): CircuitResult {\n const ruleId = \"circuit.max-loop-iterations\";\n const iterationEvents = events.filter((event) => {\n const attrs = event.attributes ?? {};\n return typeof attrs.iteration === \"number\" || event.name.toLowerCase().includes(\"loop\");\n });\n const maxSeen = iterationEvents.reduce((max, event) => {\n const attrs = event.attributes ?? {};\n const iteration = typeof attrs.iteration === \"number\" ? attrs.iteration : max;\n return Math.max(max, iteration);\n }, iterationEvents.length);\n if (maxSeen <= maxIterations) {\n return closed(ruleId, \"Loop iterations within threshold.\");\n }\n return open(ruleId, \"Loop iterations exceeded threshold.\", [\n { ruleId, count: maxSeen, threshold: maxIterations },\n ]);\n}\n\nexport function evaluateMaxRetries(\n events: readonly CircuitTraceEvent[],\n maxRetries: number,\n): CircuitResult {\n const ruleId = \"circuit.max-retries\";\n const attempts = events.map(attemptNumber).filter((value): value is number => value !== undefined);\n const maxAttempt = attempts.length > 0 ? Math.max(...attempts) : 0;\n if (maxAttempt <= maxRetries) {\n return closed(ruleId, \"Retry count within threshold.\");\n }\n return open(ruleId, \"Retry count exceeded threshold.\", [\n { ruleId, count: maxAttempt, threshold: maxRetries },\n ]);\n}\n\nexport function evaluateToolTimeout(\n events: readonly CircuitTraceEvent[],\n maxDurationMs: number,\n): CircuitResult {\n const ruleId = \"circuit.tool-timeout\";\n const evidence: CircuitEvidence[] = [];\n for (const event of events.filter(isToolEvent)) {\n const duration = durationMs(event);\n if (duration !== undefined && duration > maxDurationMs) {\n evidence.push({\n ruleId,\n toolName: toolLabel(event),\n count: duration,\n threshold: maxDurationMs,\n eventId: event.eventId,\n });\n }\n }\n if (evidence.length === 0) {\n return closed(ruleId, \"Tool durations within timeout.\");\n }\n return open(ruleId, \"Tool call exceeded configured timeout.\", evidence, \"warning\");\n}\n\nexport function evaluateRunawayLlmLoop(\n events: readonly CircuitTraceEvent[],\n maxLlmCalls: number,\n): CircuitResult {\n const ruleId = \"circuit.runaway-llm-loop\";\n const llmCount = events.filter(isLlmEvent).length;\n const hasTerminal = events.some((event) => {\n const status = (event.status ?? event.attributes?.status ?? \"\").toString().toLowerCase();\n return status === \"ok\" || status === \"success\" || status === \"completed\";\n });\n if (llmCount <= maxLlmCalls || hasTerminal) {\n return closed(ruleId, \"LLM call count within threshold or run completed.\");\n }\n return open(ruleId, \"Runaway LLM loop detected.\", [\n { ruleId, count: llmCount, threshold: maxLlmCalls },\n ]);\n}\n\nexport function evaluateExcessiveBranchWidth(\n events: readonly CircuitTraceEvent[],\n maxWidth: number,\n): CircuitResult {\n const ruleId = \"circuit.excessive-branch-width\";\n const children = new Map<string, number>();\n for (const event of events) {\n const parentId = event.parentId;\n if (!parentId) continue;\n children.set(parentId, (children.get(parentId) ?? 0) + 1);\n }\n const evidence: CircuitEvidence[] = [];\n for (const [parentId, count] of children) {\n if (count > maxWidth) {\n evidence.push({ ruleId, path: parentId, count, threshold: maxWidth });\n }\n }\n if (evidence.length === 0) {\n return closed(ruleId, \"Branch width within threshold.\");\n }\n return open(ruleId, \"Excessive parallel branch width detected.\", evidence, \"warning\");\n}\n\nfunction runRule(\n ruleId: CircuitRuleId,\n events: readonly CircuitTraceEvent[],\n options: RunCircuitsOptions,\n): CircuitResult | undefined {\n switch (ruleId) {\n case \"circuit.same-tool-repetition\":\n if (options.sameToolRepetition === undefined) return undefined;\n return evaluateSameToolRepetition(events, options.sameToolRepetition.maxRepeats);\n case \"circuit.same-args-repetition\":\n if (options.sameArgsRepetition === undefined) return undefined;\n return evaluateSameArgsRepetition(events, options.sameArgsRepetition.maxRepeats);\n case \"circuit.max-loop-iterations\":\n if (options.maxLoopIterations === undefined) return undefined;\n return evaluateMaxLoopIterations(events, options.maxLoopIterations.maxIterations);\n case \"circuit.max-retries\":\n if (options.maxRetries === undefined) return undefined;\n return evaluateMaxRetries(events, options.maxRetries.maxRetries);\n case \"circuit.tool-timeout\":\n if (options.toolTimeout === undefined) return undefined;\n return evaluateToolTimeout(events, options.toolTimeout.maxDurationMs);\n case \"circuit.runaway-llm-loop\":\n if (options.runawayLlmLoop === undefined) return undefined;\n return evaluateRunawayLlmLoop(events, options.runawayLlmLoop.maxLlmCalls);\n case \"circuit.excessive-branch-width\":\n if (options.excessiveBranchWidth === undefined) return undefined;\n return evaluateExcessiveBranchWidth(events, options.excessiveBranchWidth.maxWidth);\n default:\n return undefined;\n }\n}\n\nexport function runCircuits(\n events: readonly CircuitTraceEvent[],\n options: RunCircuitsOptions = {},\n): CircuitRunResult {\n const selected = options.rules ?? ALL_RULES;\n const results: CircuitResult[] = [];\n for (const ruleId of selected) {\n const result = runRule(ruleId, events, options);\n if (result) results.push(result);\n }\n const ok = !results.some((result) => result.status === \"open\" && result.severity === \"error\");\n return { ok, results };\n}\n\nexport { ALL_RULES as DEFAULT_CIRCUIT_RULES };\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-inspect/circuit",
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Local circuit-breaker analyzers for AgentInspect trace patterns",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/rajudandigam/agent-inspect.git",
|
|
10
|
+
"directory": "packages/circuit"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/rajudandigam/agent-inspect/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/rajudandigam/agent-inspect#readme",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.mjs",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.mjs"
|
|
25
|
+
},
|
|
26
|
+
"require": {
|
|
27
|
+
"types": "./dist/index.d.cts",
|
|
28
|
+
"default": "./dist/index.cjs"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "pnpm --workspace-root exec tsup --config tsup.circuit.config.ts",
|
|
41
|
+
"test": "vitest run -c ../../vitest.config.ts"
|
|
42
|
+
}
|
|
43
|
+
}
|