agent-ablation 0.2.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 CHANGED
@@ -1,15 +1,19 @@
1
1
  # agent-ablation
2
2
 
3
- Leave-one-out ablation testing for multi-agent decision systems. You have a set of
4
- per-agent findings (scores, confidences, whatever your pipeline produces) and a
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
+ [![CI](https://github.com/AyushCipher/agent-ablation/actions/workflows/ci.yml/badge.svg)](https://github.com/AyushCipher/agent-ablation/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/agent-ablation.svg)](https://www.npmjs.com/package/agent-ablation)
8
5
 
9
- It removes each finding one at a time, re-runs your decision function on what's
10
- left, and reports which removals flipped the outcome. Zero runtime dependencies,
11
- zero opinions about how your agents work you supply the findings and the
12
- decision function, it does the leave-one-out loop and the bookkeeping.
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
- ## Quick example
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,206 +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); // fraction of agents whose removal changed the verdict
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
- For a batch of cases, `batchAblation` runs the same ablation over each one and
50
- aggregates the results — including, per agent, the fraction of cases in which
51
- removing that agent changed the outcome:
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
+ ];
76
+
77
+ const { results, summary } = batchAblation(cases, decide);
55
78
 
56
- const { results, summary } = batchAblation(allCases, decide);
79
+ console.log(summary.roi?.agents.expensive_reasoner.costPerVerdictFlip); // Cost per decision flip
80
+ console.log(summary.roi?.recommendations); // Automated pruning/downgrade advice
57
81
 
58
- console.log(summary.averageLoadBearingRatio);
59
- console.log(summary.perAgentInfluence); // { transaction_pattern: 0.17, identity_signal: 0.83, ... }
82
+ // Format into a Markdown report for PRs or documentation
83
+ console.log(formatMarkdownReport(summary));
60
84
  ```
61
85
 
62
- ## Using with LangGraph
86
+ ---
63
87
 
64
- If you are orchestrating multi-agent systems with LangGraph, `fromLangGraphMessages` provides a zero-dependency convenience mapper for common LangGraph message shapes (this is a structural mapper, not an official LangGraph integration). It extracts named agent messages from `state.messages` and converts them into `Finding[]`:
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:
65
91
 
66
92
  ```typescript
67
- import { runAblation, fromLangGraphMessages, type Finding } from "agent-ablation";
68
-
69
- // In your LangGraph supervisor node / decision step:
70
- function evaluateState(state: { messages: any[] }) {
71
- // Maps named specialist messages to Finding[]
72
- const findings = fromLangGraphMessages(state.messages, {
73
- scoreOf: (msg) => (msg.content as any).score,
74
- confidenceOf: (msg) => (msg.content as any).confidence,
75
- });
76
-
77
- const decide = (fs: Finding[]) => {
78
- const risk = 1 - fs.reduce((p, f) => p * (1 - f.score / 100), 1);
79
- return risk >= 0.7 ? "decline" : "approve";
80
- };
81
-
82
- const result = runAblation(findings, decide);
83
- return result;
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;
84
98
  }
99
+
100
+ const result = await runAblationAsync(findings, llmSupervisorDecide, {
101
+ samples: 5, // Runs 5 samples per ablation to filter out temperature noise
102
+ });
85
103
  ```
86
104
 
87
- For arbitrary custom structures or telemetry traces, `fromRecords()` is also available to map any record array with custom extraction callbacks:
105
+ ---
106
+
107
+ ### 4. Greedy Backward Elimination & Minimal Viable Panel
108
+
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.
110
+
111
+ `runBackwardElimination` iteratively eliminates agents one-by-one until removing any further agent flips the verdict, revealing the **minimal viable panel**:
88
112
 
89
113
  ```typescript
90
- import { fromRecords } from "agent-ablation";
114
+ import { runBackwardElimination } from "agent-ablation";
91
115
 
92
- const findings = fromRecords(customAuditRecords, {
93
- agentId: (r) => r.specialistId,
94
- scoreOf: (r) => r.riskScore,
95
- confidenceOf: (r) => r.confidenceLevel, // optional
116
+ const result = runBackwardElimination(allSpecialists, decide);
117
+
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
121
+ ```
122
+
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.
124
+
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"],
96
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
97
141
  ```
98
142
 
99
- ## Worked example: reproducing SentryMesh's 33% multi-signal-share finding
100
-
101
- [SentryMesh](https://github.com/AyushCipher/Sentry-Mesh) is a four-specialist
102
- multi-agent fraud investigation system. Its own eval harness runs an ablation over
103
- its 23-case bank and reports the result plainly in its README: of 9 cases the
104
- system auto-resolved without escalating to a human, **only 3 survive removal of
105
- their single loudest specialist — 6 collapse to `escalate`.** SentryMesh calls
106
- this "the most important number in the report," because it means two-thirds of
107
- those auto-decisions rested on one specialist's finding, with the other three
108
- specialists' LLM calls spent for nothing.
109
-
110
- Those six cases, straight from SentryMesh's ablation table:
111
-
112
- | Case | Decision | Remove | Becomes |
113
- |---|---|---|---|
114
- | SM-001 | auto_decline | `identity_signal` (100) | escalate |
115
- | SM-002 | auto_decline | `identity_signal` (75) | escalate |
116
- | SM-005 | auto_decline | `identity_signal` (80) | escalate |
117
- | SM-006 | auto_decline | `identity_signal` (70) | escalate |
118
- | SM-012 | auto_decline | `network_analysis` (90) | escalate |
119
- | SM-016 | auto_approve | `transaction_pattern` (26) | escalate |
120
-
121
- `tests/ablation.test.ts` in this repo reproduces all six as a worked example: it
122
- builds each case's four-specialist `Finding[]` (with the named specialist's score
123
- matching SentryMesh's table exactly), runs it through a decision function modeled
124
- on SentryMesh's own description of its aggregation — findings combine via
125
- noisy-OR, gated by panel confidence — and asserts that `runAblation` correctly
126
- identifies the named specialist's removal as the one that flips each case to
127
- `escalate`. It's the credibility anchor for this package: if `agent-ablation`
128
- couldn't reproduce a real, previously-published ablation result on real case
129
- data, it wouldn't be trustworthy on your data either.
130
-
131
- ## Limitations
132
-
133
- **This tool detects verdict *change*, not verdict quality.** A flipped verdict
134
- after removing an agent tells you that agent was load-bearing for that decision —
135
- it says nothing about whether the original verdict or the post-removal one was
136
- *correct*. Conversely, a low `loadBearingRatio` is not automatically a flaw:
137
- redundancy across agents can be exactly what you want (independent corroboration
138
- is the point of running more than one specialist), and this tool has no way to
139
- distinguish "healthy redundancy" from "wasted compute" for you.
140
-
141
- **Leave-one-out misses agents that only matter in pairs.** This is a real gap,
142
- not a hedge. If removing agent A alone doesn't flip the verdict, and removing
143
- agent B alone doesn't either, but removing *both together* would, leave-one-out
144
- ablation will report both as not load-bearing. Catching that requires ablating
145
- combinations, which this package deliberately does not do — the combinatorics
146
- blow up fast, and a leave-one-out pass over a modest agent panel is already the
147
- useful 80% case. If you suspect joint effects, ablate the suspected pair
148
- manually by filtering `findings` yourself before calling `decide`.
149
-
150
- **`decide()` must be pure and deterministic.** `runAblation` calls `decide` once
151
- per finding removed, expecting each call to depend only on the findings it's
152
- given. If your decision logic calls an LLM internally, this tool does not apply
153
- to that call — it only makes sense as a probe over a deterministic aggregation
154
- step that runs *after* the LLM reasoning is done. This mirrors SentryMesh's own
155
- architecture: its supervisor's model call interprets *why* specialists conflict
156
- and produces a combined risk and confidence, but turning those numbers into an
157
- auto-decline/auto-approve/escalate action is `decide()`, a pure function with no
158
- model call in it — "an LLM is a good place for the interpretation and a bad place
159
- for a threshold that compliance will one day have to explain in writing," in
160
- SentryMesh's own words. `agent-ablation` ablates that downstream pure function,
161
- not the LLM call that fed it.
162
-
163
- ## API reference
143
+ ---
144
+
145
+ ## Framework Adapters
164
146
 
147
+ Zero-dependency adapters to map telemetry and messages from popular agent frameworks directly into `Finding[]`:
148
+
149
+ ### LangGraph
165
150
  ```typescript
166
- interface Finding {
167
- agentId: string;
168
- score: number;
169
- confidence?: number;
170
- metadata?: Record<string, unknown>;
171
- }
151
+ import { fromLangGraphMessages } from "agent-ablation";
172
152
 
173
- type DecisionFn<TVerdict> = (findings: Finding[]) => TVerdict;
153
+ const findings = fromLangGraphMessages(state.messages, {
154
+ scoreOf: (msg) => (msg.content as any).score,
155
+ confidenceOf: (msg) => (msg.content as any).confidence,
156
+ });
157
+ ```
174
158
 
175
- interface PerAgentAblation<TVerdict> {
176
- removedAgentId: string;
177
- verdictWithout: TVerdict;
178
- changed: boolean;
179
- }
159
+ ### CrewAI
160
+ ```typescript
161
+ import { fromCrewAITasks } from "agent-ablation";
180
162
 
181
- interface AblationResult<TVerdict> {
182
- baseline: TVerdict;
183
- perAgent: PerAgentAblation<TVerdict>[];
184
- loadBearingCount: number;
185
- totalAgents: number;
186
- loadBearingRatio: number;
187
- }
163
+ const findings = fromCrewAITasks(crewOutput.tasks_output, {
164
+ scoreOf: (task) => task.json_dict?.score ?? 0,
165
+ });
166
+ ```
188
167
 
189
- function runAblation<TVerdict>(
190
- findings: Finding[],
191
- decide: DecisionFn<TVerdict>,
192
- equals?: (a: TVerdict, b: TVerdict) => boolean
193
- ): AblationResult<TVerdict>;
168
+ ### AutoGen
169
+ ```typescript
170
+ import { fromAutoGenMessages } from "agent-ablation";
194
171
 
195
- interface BatchAblationSummary {
196
- cases: number;
197
- averageLoadBearingRatio: number;
198
- perAgentInfluence: Record<string, number>;
199
- }
172
+ const findings = fromAutoGenMessages(chatHistory, {
173
+ scoreOf: (msg) => (msg.content as any).riskScore,
174
+ });
175
+ ```
200
176
 
201
- function batchAblation<TVerdict>(
202
- cases: Finding[][],
203
- decide: DecisionFn<TVerdict>,
204
- equals?: (a: TVerdict, b: TVerdict) => boolean
205
- ): { results: AblationResult<TVerdict>[]; summary: BatchAblationSummary };
177
+ ### Vercel AI SDK / Step Traces
178
+ ```typescript
179
+ import { fromAISDKSteps } from "agent-ablation";
206
180
 
207
- interface LangGraphAgentMessage {
208
- name?: string | null;
209
- content?: unknown;
210
- [key: string]: unknown;
211
- }
181
+ const findings = fromAISDKSteps(steps, {
182
+ scoreOf: (step) => (step.result as any).score,
183
+ });
184
+ ```
212
185
 
213
- interface LangGraphAdapterOptions<TMessage extends LangGraphAgentMessage = LangGraphAgentMessage> {
214
- scoreOf: (message: TMessage) => number;
215
- confidenceOf?: (message: TMessage) => number | undefined;
216
- }
186
+ ### Generic Custom Records
187
+ ```typescript
188
+ import { fromRecords } from "agent-ablation";
217
189
 
218
- function fromLangGraphMessages<TMessage extends LangGraphAgentMessage = LangGraphAgentMessage>(
219
- messages: readonly TMessage[] | TMessage[],
220
- options: LangGraphAdapterOptions<TMessage>
221
- ): Finding[];
222
-
223
- function fromRecords<T>(
224
- records: readonly T[] | T[],
225
- options: {
226
- agentId: (record: T, index: number) => string;
227
- scoreOf: (record: T, index: number) => number;
228
- confidenceOf?: (record: T, index: number) => number | undefined;
229
- }
230
- ): Finding[];
190
+ const findings = fromRecords(customAuditRecords, {
191
+ agentId: (r) => r.specialistId,
192
+ scoreOf: (r) => r.riskScore,
193
+ confidenceOf: (r) => r.confidenceLevel,
194
+ });
231
195
  ```
232
196
 
233
- `equals` defaults to `===`. If `TVerdict` is an object (or anything else compared
234
- by reference rather than value), you must supply your own `equals` — otherwise
235
- every ablation will read as "changed" purely because two structurally identical
236
- verdict objects are never `===` to each other, regardless of whether the
237
- decision actually differed.
197
+ ---
198
+
199
+ ## Worked Example: SentryMesh 33% Multi-Signal Finding
238
200
 
239
- ## License
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`.**
240
202
 
241
- MIT
203
+ `tests/ablation.test.ts` reproduces all 6 cases verbatim with `agent-ablation`.
242
204
 
243
205
  ---
244
206
 
245
- Ayush Verma ayushv3533e@gmail.com
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