agent-ablation 0.1.0 → 0.3.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/README.md +177 -124
- package/dist/index.cjs +593 -7
- package/dist/index.d.cts +406 -9
- package/dist/index.d.ts +406 -9
- package/dist/index.js +576 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
# agent-ablation
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function that turns those findings into a verdict. `agent-ablation` answers one
|
|
6
|
-
question: **which of my agents' findings actually changed that verdict, and which
|
|
7
|
-
were along for the ride?**
|
|
3
|
+
[](https://github.com/AyushCipher/agent-ablation/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/agent-ablation)
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
Leave-one-out ablation testing, backward elimination, and ROI evaluation for multi-agent systems. You have a set of per-agent findings (scores, confidences, telemetry) and a function or model that turns those findings into a verdict. `agent-ablation` answers the key production questions:
|
|
7
|
+
|
|
8
|
+
1. **Load-Bearing Influence:** Which agents' findings actually changed the verdict, and which were along for the ride?
|
|
9
|
+
2. **Cost & Token ROI:** How many dollars and tokens did each specialist burn per verdict flip? Does that small accuracy bump justify the API bill?
|
|
10
|
+
3. **Correlated Agents & Pruning:** Which redundant agents can be safely pruned via greedy backward elimination without breaking the final verdict?
|
|
11
|
+
4. **Protective vs. Harmful Signals:** When ground truth is provided, did an agent's presence prevent an error (protective), or did it cause a hallucination/false positive (harmful)?
|
|
12
|
+
5. **Stochastic & Async LLM Judges:** Handles async decisions and repeated sampling with majority voting to filter out LLM temperature variance.
|
|
13
|
+
|
|
14
|
+
Zero runtime dependencies. Native TypeScript.
|
|
15
|
+
|
|
16
|
+
---
|
|
13
17
|
|
|
14
18
|
## Install
|
|
15
19
|
|
|
@@ -17,7 +21,11 @@ decision function, it does the leave-one-out loop and the bookkeeping.
|
|
|
17
21
|
npm install agent-ablation
|
|
18
22
|
```
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Core Features & Usage
|
|
27
|
+
|
|
28
|
+
### 1. Basic Leave-One-Out Ablation
|
|
21
29
|
|
|
22
30
|
```typescript
|
|
23
31
|
import { runAblation, type Finding } from "agent-ablation";
|
|
@@ -40,144 +48,189 @@ const findings: Finding[] = [
|
|
|
40
48
|
const result = runAblation(findings, decide);
|
|
41
49
|
|
|
42
50
|
console.log(result.baseline); // "decline"
|
|
43
|
-
console.log(result.loadBearingRatio); //
|
|
51
|
+
console.log(result.loadBearingRatio); // 0.33 (1 out of 3 agents flipped the outcome)
|
|
44
52
|
for (const p of result.perAgent) {
|
|
45
53
|
console.log(p.removedAgentId, "->", p.verdictWithout, p.changed ? "(load-bearing)" : "");
|
|
46
54
|
}
|
|
47
55
|
```
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### 2. Cost & Token ROI Analysis ("Cost per Verdict Flip")
|
|
60
|
+
|
|
61
|
+
Pass telemetry (`cost`, `tokens`, `latencyMs`) inside your findings. `batchAblation` computes the exact ROI metrics and identifies expensive agents with low decision impact:
|
|
52
62
|
|
|
53
63
|
```typescript
|
|
54
|
-
import { batchAblation } from "agent-ablation";
|
|
64
|
+
import { batchAblation, formatMarkdownReport, type Finding } from "agent-ablation";
|
|
65
|
+
|
|
66
|
+
const cases: Finding[][] = [
|
|
67
|
+
[
|
|
68
|
+
{ agentId: "expensive_reasoner", score: 90, cost: 0.15, tokens: 3000 },
|
|
69
|
+
{ agentId: "cheap_heuristic", score: 10, cost: 0.002, tokens: 50 },
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
{ agentId: "expensive_reasoner", score: 20, cost: 0.15, tokens: 3000 },
|
|
73
|
+
{ agentId: "cheap_heuristic", score: 85, cost: 0.002, tokens: 50 },
|
|
74
|
+
],
|
|
75
|
+
];
|
|
55
76
|
|
|
56
|
-
const { results, summary } = batchAblation(
|
|
77
|
+
const { results, summary } = batchAblation(cases, decide);
|
|
57
78
|
|
|
58
|
-
console.log(summary.
|
|
59
|
-
console.log(summary.
|
|
79
|
+
console.log(summary.roi?.agents.expensive_reasoner.costPerVerdictFlip); // Cost per decision flip
|
|
80
|
+
console.log(summary.roi?.recommendations); // Automated pruning/downgrade advice
|
|
81
|
+
|
|
82
|
+
// Format into a Markdown report for PRs or documentation
|
|
83
|
+
console.log(formatMarkdownReport(summary));
|
|
60
84
|
```
|
|
61
85
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
system auto-resolved without escalating to a human, **only 3 survive removal of
|
|
68
|
-
their single loudest specialist — 6 collapse to `escalate`.** SentryMesh calls
|
|
69
|
-
this "the most important number in the report," because it means two-thirds of
|
|
70
|
-
those auto-decisions rested on one specialist's finding, with the other three
|
|
71
|
-
specialists' LLM calls spent for nothing.
|
|
72
|
-
|
|
73
|
-
Those six cases, straight from SentryMesh's ablation table:
|
|
74
|
-
|
|
75
|
-
| Case | Decision | Remove | Becomes |
|
|
76
|
-
|---|---|---|---|
|
|
77
|
-
| SM-001 | auto_decline | `identity_signal` (100) | escalate |
|
|
78
|
-
| SM-002 | auto_decline | `identity_signal` (75) | escalate |
|
|
79
|
-
| SM-005 | auto_decline | `identity_signal` (80) | escalate |
|
|
80
|
-
| SM-006 | auto_decline | `identity_signal` (70) | escalate |
|
|
81
|
-
| SM-012 | auto_decline | `network_analysis` (90) | escalate |
|
|
82
|
-
| SM-016 | auto_approve | `transaction_pattern` (26) | escalate |
|
|
83
|
-
|
|
84
|
-
`tests/ablation.test.ts` in this repo reproduces all six as a worked example: it
|
|
85
|
-
builds each case's four-specialist `Finding[]` (with the named specialist's score
|
|
86
|
-
matching SentryMesh's table exactly), runs it through a decision function modeled
|
|
87
|
-
on SentryMesh's own description of its aggregation — findings combine via
|
|
88
|
-
noisy-OR, gated by panel confidence — and asserts that `runAblation` correctly
|
|
89
|
-
identifies the named specialist's removal as the one that flips each case to
|
|
90
|
-
`escalate`. It's the credibility anchor for this package: if `agent-ablation`
|
|
91
|
-
couldn't reproduce a real, previously-published ablation result on real case
|
|
92
|
-
data, it wouldn't be trustworthy on your data either.
|
|
93
|
-
|
|
94
|
-
## Limitations
|
|
95
|
-
|
|
96
|
-
**This tool detects verdict *change*, not verdict quality.** A flipped verdict
|
|
97
|
-
after removing an agent tells you that agent was load-bearing for that decision —
|
|
98
|
-
it says nothing about whether the original verdict or the post-removal one was
|
|
99
|
-
*correct*. Conversely, a low `loadBearingRatio` is not automatically a flaw:
|
|
100
|
-
redundancy across agents can be exactly what you want (independent corroboration
|
|
101
|
-
is the point of running more than one specialist), and this tool has no way to
|
|
102
|
-
distinguish "healthy redundancy" from "wasted compute" for you.
|
|
103
|
-
|
|
104
|
-
**Leave-one-out misses agents that only matter in pairs.** This is a real gap,
|
|
105
|
-
not a hedge. If removing agent A alone doesn't flip the verdict, and removing
|
|
106
|
-
agent B alone doesn't either, but removing *both together* would, leave-one-out
|
|
107
|
-
ablation will report both as not load-bearing. Catching that requires ablating
|
|
108
|
-
combinations, which this package deliberately does not do — the combinatorics
|
|
109
|
-
blow up fast, and a leave-one-out pass over a modest agent panel is already the
|
|
110
|
-
useful 80% case. If you suspect joint effects, ablate the suspected pair
|
|
111
|
-
manually by filtering `findings` yourself before calling `decide`.
|
|
112
|
-
|
|
113
|
-
**`decide()` must be pure and deterministic.** `runAblation` calls `decide` once
|
|
114
|
-
per finding removed, expecting each call to depend only on the findings it's
|
|
115
|
-
given. If your decision logic calls an LLM internally, this tool does not apply
|
|
116
|
-
to that call — it only makes sense as a probe over a deterministic aggregation
|
|
117
|
-
step that runs *after* the LLM reasoning is done. This mirrors SentryMesh's own
|
|
118
|
-
architecture: its supervisor's model call interprets *why* specialists conflict
|
|
119
|
-
and produces a combined risk and confidence, but turning those numbers into an
|
|
120
|
-
auto-decline/auto-approve/escalate action is `decide()`, a pure function with no
|
|
121
|
-
model call in it — "an LLM is a good place for the interpretation and a bad place
|
|
122
|
-
for a threshold that compliance will one day have to explain in writing," in
|
|
123
|
-
SentryMesh's own words. `agent-ablation` ablates that downstream pure function,
|
|
124
|
-
not the LLM call that fed it.
|
|
125
|
-
|
|
126
|
-
## API reference
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### 3. Async & Stochastic Decision Functions (LLM-as-a-Judge)
|
|
89
|
+
|
|
90
|
+
When your decision step is an async LLM call with temperature, use `runAblationAsync` or `batchAblationAsync`. Set `samples: k` to take a majority-vote consensus across runs to eliminate sampling noise:
|
|
127
91
|
|
|
128
92
|
```typescript
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
93
|
+
import { runAblationAsync } from "agent-ablation";
|
|
94
|
+
|
|
95
|
+
async function llmSupervisorDecide(findings: Finding[]): Promise<string> {
|
|
96
|
+
const res = await callLlmJudge(findings);
|
|
97
|
+
return res.verdict;
|
|
134
98
|
}
|
|
135
99
|
|
|
136
|
-
|
|
100
|
+
const result = await runAblationAsync(findings, llmSupervisorDecide, {
|
|
101
|
+
samples: 5, // Runs 5 samples per ablation to filter out temperature noise
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
137
106
|
|
|
138
|
-
|
|
139
|
-
removedAgentId: string;
|
|
140
|
-
verdictWithout: TVerdict;
|
|
141
|
-
changed: boolean;
|
|
142
|
-
}
|
|
107
|
+
### 4. Greedy Backward Elimination & Minimal Viable Panel
|
|
143
108
|
|
|
144
|
-
|
|
145
|
-
baseline: TVerdict;
|
|
146
|
-
perAgent: PerAgentAblation<TVerdict>[];
|
|
147
|
-
loadBearingCount: number;
|
|
148
|
-
totalAgents: number;
|
|
149
|
-
loadBearingRatio: number;
|
|
150
|
-
}
|
|
109
|
+
If you have correlated or redundant agents (e.g., three critics looking at the same context), simple leave-one-out might mark all of them as not load-bearing because the others compensate.
|
|
151
110
|
|
|
152
|
-
|
|
153
|
-
findings: Finding[],
|
|
154
|
-
decide: DecisionFn<TVerdict>,
|
|
155
|
-
equals?: (a: TVerdict, b: TVerdict) => boolean
|
|
156
|
-
): AblationResult<TVerdict>;
|
|
111
|
+
`runBackwardElimination` iteratively eliminates agents one-by-one until removing any further agent flips the verdict, revealing the **minimal viable panel**:
|
|
157
112
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
113
|
+
```typescript
|
|
114
|
+
import { runBackwardElimination } from "agent-ablation";
|
|
115
|
+
|
|
116
|
+
const result = runBackwardElimination(allSpecialists, decide);
|
|
163
117
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
equals?: (a: TVerdict, b: TVerdict) => boolean
|
|
168
|
-
): { results: AblationResult<TVerdict>[]; summary: BatchAblationSummary };
|
|
118
|
+
console.log(result.minimalAgentIds); // ["critic_1", "security_auditor"]
|
|
119
|
+
console.log(result.eliminatedAgentIds); // ["critic_2", "critic_3", "scout_noisy"]
|
|
120
|
+
console.log(result.steps); // Step-by-step elimination trace
|
|
169
121
|
```
|
|
170
122
|
|
|
171
|
-
`
|
|
172
|
-
by reference rather than value), you must supply your own `equals` — otherwise
|
|
173
|
-
every ablation will read as "changed" purely because two structurally identical
|
|
174
|
-
verdict objects are never `===` to each other, regardless of whether the
|
|
175
|
-
decision actually differed.
|
|
123
|
+
For detecting 2nd-order joint dependencies, `runPairwiseAblation(findings, decide)` evaluates all pairs $(A, B)$ to catch cases where neither agent alone is load-bearing, but removing both together flips the outcome.
|
|
176
124
|
|
|
177
|
-
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### 5. Ground-Truth & Net Accuracy Impact ("Protective vs. Harmful")
|
|
128
|
+
|
|
129
|
+
Supply ground truth labels in `batchAblation` to measure whether an agent's load-bearing presence actually **improved** accuracy or **injected errors / hallucinations**:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const { summary } = batchAblation(cases, decide, {
|
|
133
|
+
groundTruth: ["approve", "decline", "approve", "escalate"],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Per-agent stats:
|
|
137
|
+
// - Protective: removing the agent caused a correct verdict to become incorrect
|
|
138
|
+
// - Harmful: removing the agent fixed an incorrect verdict
|
|
139
|
+
console.log(summary.perAgentStats?.["hallucinating_agent"]?.role); // "Harmful"
|
|
140
|
+
console.log(summary.perAgentStats?.["hallucinating_agent"]?.netAccuracyImpact); // -0.25
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Framework Adapters
|
|
178
146
|
|
|
179
|
-
|
|
147
|
+
Zero-dependency adapters to map telemetry and messages from popular agent frameworks directly into `Finding[]`:
|
|
148
|
+
|
|
149
|
+
### LangGraph
|
|
150
|
+
```typescript
|
|
151
|
+
import { fromLangGraphMessages } from "agent-ablation";
|
|
152
|
+
|
|
153
|
+
const findings = fromLangGraphMessages(state.messages, {
|
|
154
|
+
scoreOf: (msg) => (msg.content as any).score,
|
|
155
|
+
confidenceOf: (msg) => (msg.content as any).confidence,
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### CrewAI
|
|
160
|
+
```typescript
|
|
161
|
+
import { fromCrewAITasks } from "agent-ablation";
|
|
162
|
+
|
|
163
|
+
const findings = fromCrewAITasks(crewOutput.tasks_output, {
|
|
164
|
+
scoreOf: (task) => task.json_dict?.score ?? 0,
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### AutoGen
|
|
169
|
+
```typescript
|
|
170
|
+
import { fromAutoGenMessages } from "agent-ablation";
|
|
171
|
+
|
|
172
|
+
const findings = fromAutoGenMessages(chatHistory, {
|
|
173
|
+
scoreOf: (msg) => (msg.content as any).riskScore,
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Vercel AI SDK / Step Traces
|
|
178
|
+
```typescript
|
|
179
|
+
import { fromAISDKSteps } from "agent-ablation";
|
|
180
|
+
|
|
181
|
+
const findings = fromAISDKSteps(steps, {
|
|
182
|
+
scoreOf: (step) => (step.result as any).score,
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Generic Custom Records
|
|
187
|
+
```typescript
|
|
188
|
+
import { fromRecords } from "agent-ablation";
|
|
189
|
+
|
|
190
|
+
const findings = fromRecords(customAuditRecords, {
|
|
191
|
+
agentId: (r) => r.specialistId,
|
|
192
|
+
scoreOf: (r) => r.riskScore,
|
|
193
|
+
confidenceOf: (r) => r.confidenceLevel,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
180
196
|
|
|
181
197
|
---
|
|
182
198
|
|
|
183
|
-
|
|
199
|
+
## Worked Example: SentryMesh 33% Multi-Signal Finding
|
|
200
|
+
|
|
201
|
+
[SentryMesh](https://github.com/AyushCipher/Sentry-Mesh) is a four-specialist multi-agent fraud investigation system. Its eval harness runs an ablation over its 23-case bank and reports: of 9 cases auto-resolved without human escalation, **only 3 survive removal of their single loudest specialist — 6 collapse to `escalate`.**
|
|
202
|
+
|
|
203
|
+
`tests/ablation.test.ts` reproduces all 6 cases verbatim with `agent-ablation`.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Architectural Note: Leaf Ablation vs. DAG Subgraph Replay
|
|
208
|
+
|
|
209
|
+
* **Leaf Finding Ablation (This Package):** Best for **parallel / fan-out / fan-in** panels where specialists independently produce findings that feed a decision gate. Because findings are generated independently, dropping an item at `decide()` measures causal weight with **zero LLM re-invocation cost**.
|
|
210
|
+
* **Sequential DAG Replay:** If your pipeline is sequential (Agent A feeds intermediate prompt context to Agent B), removing Agent A at the final decision gate misses that Agent B's output already reflects Agent A. Measuring sequential pipelines requires replaying downstream subgraphs or injecting mock messages into the trace.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## API Summary
|
|
215
|
+
|
|
216
|
+
| Function | Description |
|
|
217
|
+
| :--- | :--- |
|
|
218
|
+
| `runAblation(findings, decide, equals?)` | Synchronous leave-one-out ablation for a single case. |
|
|
219
|
+
| `runAblationAsync(findings, decide, options?)` | Async leave-one-out ablation with optional $K$-sampling / majority voting. |
|
|
220
|
+
| `batchAblation(cases, decide, options?)` | Batch ablation with telemetry ROI and ground-truth metrics. |
|
|
221
|
+
| `batchAblationAsync(cases, decide, options?)` | Async batch ablation. |
|
|
222
|
+
| `runBackwardElimination(findings, decide, equals?)` | Greedy backward elimination to find the minimal agent panel. |
|
|
223
|
+
| `runPairwiseAblation(findings, decide, equals?)` | Evaluates all 2-agent pairs to detect interaction/redundancy effects. |
|
|
224
|
+
| `formatMarkdownReport(summary, options?)` | Formats summary into a GitHub/Dev.to markdown report with ROI tables. |
|
|
225
|
+
| `formatAsciiTable(summary)` | Formats summary into a clean terminal ASCII table. |
|
|
226
|
+
| `fromLangGraphMessages(...)` | Adapter for LangGraph message arrays. |
|
|
227
|
+
| `fromCrewAITasks(...)` | Adapter for CrewAI task outputs. |
|
|
228
|
+
| `fromAutoGenMessages(...)` | Adapter for AutoGen chat histories. |
|
|
229
|
+
| `fromAISDKSteps(...)` | Adapter for Vercel AI SDK step traces. |
|
|
230
|
+
| `fromRecords(...)` | Generic record mapper. |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
MIT © Ayush Verma — ayushv3533e@gmail.com
|