agent-ablation 0.1.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/README.md +183 -0
- package/dist/index.cjs +81 -0
- package/dist/index.d.cts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +55 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ayush Verma
|
|
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/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# agent-ablation
|
|
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?**
|
|
8
|
+
|
|
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.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install agent-ablation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick example
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { runAblation, type Finding } from "agent-ablation";
|
|
24
|
+
|
|
25
|
+
type Verdict = "approve" | "decline" | "escalate";
|
|
26
|
+
|
|
27
|
+
function decide(findings: Finding[]): Verdict {
|
|
28
|
+
const risk = 1 - findings.reduce((p, f) => p * (1 - f.score / 100), 1);
|
|
29
|
+
if (risk >= 0.7) return "decline";
|
|
30
|
+
if (risk <= 0.3) return "approve";
|
|
31
|
+
return "escalate";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const findings: Finding[] = [
|
|
35
|
+
{ agentId: "transaction_pattern", score: 25 },
|
|
36
|
+
{ agentId: "identity_signal", score: 90 },
|
|
37
|
+
{ agentId: "network_analysis", score: 20 },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const result = runAblation(findings, decide);
|
|
41
|
+
|
|
42
|
+
console.log(result.baseline); // "decline"
|
|
43
|
+
console.log(result.loadBearingRatio); // fraction of agents whose removal changed the verdict
|
|
44
|
+
for (const p of result.perAgent) {
|
|
45
|
+
console.log(p.removedAgentId, "->", p.verdictWithout, p.changed ? "(load-bearing)" : "");
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
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:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { batchAblation } from "agent-ablation";
|
|
55
|
+
|
|
56
|
+
const { results, summary } = batchAblation(allCases, decide);
|
|
57
|
+
|
|
58
|
+
console.log(summary.averageLoadBearingRatio);
|
|
59
|
+
console.log(summary.perAgentInfluence); // { transaction_pattern: 0.17, identity_signal: 0.83, ... }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Worked example: reproducing SentryMesh's 33% multi-signal-share finding
|
|
63
|
+
|
|
64
|
+
[SentryMesh](https://github.com/AyushCipher/Sentry-Mesh) is a four-specialist
|
|
65
|
+
multi-agent fraud investigation system. Its own eval harness runs an ablation over
|
|
66
|
+
its 23-case bank and reports the result plainly in its README: of 9 cases the
|
|
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
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
interface Finding {
|
|
130
|
+
agentId: string;
|
|
131
|
+
score: number;
|
|
132
|
+
confidence?: number;
|
|
133
|
+
metadata?: Record<string, unknown>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type DecisionFn<TVerdict> = (findings: Finding[]) => TVerdict;
|
|
137
|
+
|
|
138
|
+
interface PerAgentAblation<TVerdict> {
|
|
139
|
+
removedAgentId: string;
|
|
140
|
+
verdictWithout: TVerdict;
|
|
141
|
+
changed: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface AblationResult<TVerdict> {
|
|
145
|
+
baseline: TVerdict;
|
|
146
|
+
perAgent: PerAgentAblation<TVerdict>[];
|
|
147
|
+
loadBearingCount: number;
|
|
148
|
+
totalAgents: number;
|
|
149
|
+
loadBearingRatio: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function runAblation<TVerdict>(
|
|
153
|
+
findings: Finding[],
|
|
154
|
+
decide: DecisionFn<TVerdict>,
|
|
155
|
+
equals?: (a: TVerdict, b: TVerdict) => boolean
|
|
156
|
+
): AblationResult<TVerdict>;
|
|
157
|
+
|
|
158
|
+
interface BatchAblationSummary {
|
|
159
|
+
cases: number;
|
|
160
|
+
averageLoadBearingRatio: number;
|
|
161
|
+
perAgentInfluence: Record<string, number>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function batchAblation<TVerdict>(
|
|
165
|
+
cases: Finding[][],
|
|
166
|
+
decide: DecisionFn<TVerdict>,
|
|
167
|
+
equals?: (a: TVerdict, b: TVerdict) => boolean
|
|
168
|
+
): { results: AblationResult<TVerdict>[]; summary: BatchAblationSummary };
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`equals` defaults to `===`. If `TVerdict` is an object (or anything else compared
|
|
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.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
Ayush Verma — ayushv3533e@gmail.com
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
batchAblation: () => batchAblation,
|
|
24
|
+
runAblation: () => runAblation
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
function defaultEquals(a, b) {
|
|
28
|
+
return a === b;
|
|
29
|
+
}
|
|
30
|
+
function runAblation(findings, decide, equals = defaultEquals) {
|
|
31
|
+
const baseline = decide(findings.slice());
|
|
32
|
+
const perAgent = findings.map((finding, index) => {
|
|
33
|
+
const without = findings.slice(0, index).concat(findings.slice(index + 1));
|
|
34
|
+
const verdictWithout = decide(without);
|
|
35
|
+
return {
|
|
36
|
+
removedAgentId: finding.agentId,
|
|
37
|
+
verdictWithout,
|
|
38
|
+
changed: !equals(baseline, verdictWithout)
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
const totalAgents = findings.length;
|
|
42
|
+
const loadBearingCount = perAgent.filter((p) => p.changed).length;
|
|
43
|
+
const loadBearingRatio = totalAgents === 0 ? 0 : loadBearingCount / totalAgents;
|
|
44
|
+
return {
|
|
45
|
+
baseline,
|
|
46
|
+
perAgent,
|
|
47
|
+
loadBearingCount,
|
|
48
|
+
totalAgents,
|
|
49
|
+
loadBearingRatio
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function batchAblation(cases, decide, equals = defaultEquals) {
|
|
53
|
+
const results = cases.map((findings) => runAblation(findings, decide, equals));
|
|
54
|
+
const appearances = /* @__PURE__ */ new Map();
|
|
55
|
+
const changedCounts = /* @__PURE__ */ new Map();
|
|
56
|
+
for (const result of results) {
|
|
57
|
+
for (const perAgent of result.perAgent) {
|
|
58
|
+
const id = perAgent.removedAgentId;
|
|
59
|
+
appearances.set(id, (appearances.get(id) ?? 0) + 1);
|
|
60
|
+
if (perAgent.changed) {
|
|
61
|
+
changedCounts.set(id, (changedCounts.get(id) ?? 0) + 1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const perAgentInfluence = {};
|
|
66
|
+
for (const [id, count] of appearances) {
|
|
67
|
+
perAgentInfluence[id] = (changedCounts.get(id) ?? 0) / count;
|
|
68
|
+
}
|
|
69
|
+
const averageLoadBearingRatio = results.length === 0 ? 0 : results.reduce((sum, r) => sum + r.loadBearingRatio, 0) / results.length;
|
|
70
|
+
const summary = {
|
|
71
|
+
cases: cases.length,
|
|
72
|
+
averageLoadBearingRatio,
|
|
73
|
+
perAgentInfluence
|
|
74
|
+
};
|
|
75
|
+
return { results, summary };
|
|
76
|
+
}
|
|
77
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
78
|
+
0 && (module.exports = {
|
|
79
|
+
batchAblation,
|
|
80
|
+
runAblation
|
|
81
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface Finding {
|
|
2
|
+
agentId: string;
|
|
3
|
+
score: number;
|
|
4
|
+
confidence?: number;
|
|
5
|
+
metadata?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
type DecisionFn<TVerdict> = (findings: Finding[]) => TVerdict;
|
|
8
|
+
interface PerAgentAblation<TVerdict> {
|
|
9
|
+
removedAgentId: string;
|
|
10
|
+
verdictWithout: TVerdict;
|
|
11
|
+
changed: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface AblationResult<TVerdict> {
|
|
14
|
+
baseline: TVerdict;
|
|
15
|
+
perAgent: PerAgentAblation<TVerdict>[];
|
|
16
|
+
loadBearingCount: number;
|
|
17
|
+
totalAgents: number;
|
|
18
|
+
loadBearingRatio: number;
|
|
19
|
+
}
|
|
20
|
+
interface BatchAblationSummary {
|
|
21
|
+
cases: number;
|
|
22
|
+
averageLoadBearingRatio: number;
|
|
23
|
+
perAgentInfluence: Record<string, number>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Runs a leave-one-out ablation over `findings`: computes the baseline verdict,
|
|
27
|
+
* then re-runs `decide` once per finding with that finding removed, comparing each
|
|
28
|
+
* result back to the baseline via `equals`.
|
|
29
|
+
*
|
|
30
|
+
* `equals` defaults to `===`. If TVerdict is an object or otherwise compared by
|
|
31
|
+
* reference (not a primitive like a string or number), you MUST supply your own
|
|
32
|
+
* `equals` — otherwise every ablation will spuriously read as "changed" because two
|
|
33
|
+
* structurally identical objects are never `===` to one another, even when nothing
|
|
34
|
+
* about the decision actually differed.
|
|
35
|
+
*/
|
|
36
|
+
declare function runAblation<TVerdict>(findings: Finding[], decide: DecisionFn<TVerdict>, equals?: (a: TVerdict, b: TVerdict) => boolean): AblationResult<TVerdict>;
|
|
37
|
+
/**
|
|
38
|
+
* Runs `runAblation` over a batch of independent cases and aggregates the results:
|
|
39
|
+
* the mean load-bearing ratio across cases, and, per agent ID, the fraction of the
|
|
40
|
+
* cases containing that agent in which removing it flipped the verdict.
|
|
41
|
+
*/
|
|
42
|
+
declare function batchAblation<TVerdict>(cases: Finding[][], decide: DecisionFn<TVerdict>, equals?: (a: TVerdict, b: TVerdict) => boolean): {
|
|
43
|
+
results: AblationResult<TVerdict>[];
|
|
44
|
+
summary: BatchAblationSummary;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export { type AblationResult, type BatchAblationSummary, type DecisionFn, type Finding, type PerAgentAblation, batchAblation, runAblation };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface Finding {
|
|
2
|
+
agentId: string;
|
|
3
|
+
score: number;
|
|
4
|
+
confidence?: number;
|
|
5
|
+
metadata?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
type DecisionFn<TVerdict> = (findings: Finding[]) => TVerdict;
|
|
8
|
+
interface PerAgentAblation<TVerdict> {
|
|
9
|
+
removedAgentId: string;
|
|
10
|
+
verdictWithout: TVerdict;
|
|
11
|
+
changed: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface AblationResult<TVerdict> {
|
|
14
|
+
baseline: TVerdict;
|
|
15
|
+
perAgent: PerAgentAblation<TVerdict>[];
|
|
16
|
+
loadBearingCount: number;
|
|
17
|
+
totalAgents: number;
|
|
18
|
+
loadBearingRatio: number;
|
|
19
|
+
}
|
|
20
|
+
interface BatchAblationSummary {
|
|
21
|
+
cases: number;
|
|
22
|
+
averageLoadBearingRatio: number;
|
|
23
|
+
perAgentInfluence: Record<string, number>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Runs a leave-one-out ablation over `findings`: computes the baseline verdict,
|
|
27
|
+
* then re-runs `decide` once per finding with that finding removed, comparing each
|
|
28
|
+
* result back to the baseline via `equals`.
|
|
29
|
+
*
|
|
30
|
+
* `equals` defaults to `===`. If TVerdict is an object or otherwise compared by
|
|
31
|
+
* reference (not a primitive like a string or number), you MUST supply your own
|
|
32
|
+
* `equals` — otherwise every ablation will spuriously read as "changed" because two
|
|
33
|
+
* structurally identical objects are never `===` to one another, even when nothing
|
|
34
|
+
* about the decision actually differed.
|
|
35
|
+
*/
|
|
36
|
+
declare function runAblation<TVerdict>(findings: Finding[], decide: DecisionFn<TVerdict>, equals?: (a: TVerdict, b: TVerdict) => boolean): AblationResult<TVerdict>;
|
|
37
|
+
/**
|
|
38
|
+
* Runs `runAblation` over a batch of independent cases and aggregates the results:
|
|
39
|
+
* the mean load-bearing ratio across cases, and, per agent ID, the fraction of the
|
|
40
|
+
* cases containing that agent in which removing it flipped the verdict.
|
|
41
|
+
*/
|
|
42
|
+
declare function batchAblation<TVerdict>(cases: Finding[][], decide: DecisionFn<TVerdict>, equals?: (a: TVerdict, b: TVerdict) => boolean): {
|
|
43
|
+
results: AblationResult<TVerdict>[];
|
|
44
|
+
summary: BatchAblationSummary;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export { type AblationResult, type BatchAblationSummary, type DecisionFn, type Finding, type PerAgentAblation, batchAblation, runAblation };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function defaultEquals(a, b) {
|
|
3
|
+
return a === b;
|
|
4
|
+
}
|
|
5
|
+
function runAblation(findings, decide, equals = defaultEquals) {
|
|
6
|
+
const baseline = decide(findings.slice());
|
|
7
|
+
const perAgent = findings.map((finding, index) => {
|
|
8
|
+
const without = findings.slice(0, index).concat(findings.slice(index + 1));
|
|
9
|
+
const verdictWithout = decide(without);
|
|
10
|
+
return {
|
|
11
|
+
removedAgentId: finding.agentId,
|
|
12
|
+
verdictWithout,
|
|
13
|
+
changed: !equals(baseline, verdictWithout)
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
const totalAgents = findings.length;
|
|
17
|
+
const loadBearingCount = perAgent.filter((p) => p.changed).length;
|
|
18
|
+
const loadBearingRatio = totalAgents === 0 ? 0 : loadBearingCount / totalAgents;
|
|
19
|
+
return {
|
|
20
|
+
baseline,
|
|
21
|
+
perAgent,
|
|
22
|
+
loadBearingCount,
|
|
23
|
+
totalAgents,
|
|
24
|
+
loadBearingRatio
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function batchAblation(cases, decide, equals = defaultEquals) {
|
|
28
|
+
const results = cases.map((findings) => runAblation(findings, decide, equals));
|
|
29
|
+
const appearances = /* @__PURE__ */ new Map();
|
|
30
|
+
const changedCounts = /* @__PURE__ */ new Map();
|
|
31
|
+
for (const result of results) {
|
|
32
|
+
for (const perAgent of result.perAgent) {
|
|
33
|
+
const id = perAgent.removedAgentId;
|
|
34
|
+
appearances.set(id, (appearances.get(id) ?? 0) + 1);
|
|
35
|
+
if (perAgent.changed) {
|
|
36
|
+
changedCounts.set(id, (changedCounts.get(id) ?? 0) + 1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const perAgentInfluence = {};
|
|
41
|
+
for (const [id, count] of appearances) {
|
|
42
|
+
perAgentInfluence[id] = (changedCounts.get(id) ?? 0) / count;
|
|
43
|
+
}
|
|
44
|
+
const averageLoadBearingRatio = results.length === 0 ? 0 : results.reduce((sum, r) => sum + r.loadBearingRatio, 0) / results.length;
|
|
45
|
+
const summary = {
|
|
46
|
+
cases: cases.length,
|
|
47
|
+
averageLoadBearingRatio,
|
|
48
|
+
perAgentInfluence
|
|
49
|
+
};
|
|
50
|
+
return { results, summary };
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
batchAblation,
|
|
54
|
+
runAblation
|
|
55
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-ablation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Leave-one-out ablation testing for multi-agent decision systems — find out which agents' findings actually change the outcome.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"multi-agent",
|
|
28
|
+
"ai-agents",
|
|
29
|
+
"ablation",
|
|
30
|
+
"explainability",
|
|
31
|
+
"llm-agents",
|
|
32
|
+
"agent-orchestration",
|
|
33
|
+
"interpretability"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/AyushCipher/agent-ablation.git"
|
|
39
|
+
},
|
|
40
|
+
"author": "Ayush Verma <ayushv3533e@gmail.com>",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^26.2.0",
|
|
43
|
+
"tsup": "^8.5.1",
|
|
44
|
+
"typescript": "^5.7.3",
|
|
45
|
+
"vitest": "^4.1.10"
|
|
46
|
+
}
|
|
47
|
+
}
|