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/dist/index.js CHANGED
@@ -51,10 +51,220 @@ function fromRecords(records, options) {
51
51
  });
52
52
  }
53
53
 
54
+ // src/adapters/crewai.ts
55
+ function fromCrewAITasks(outputs, options = {}) {
56
+ const findings = [];
57
+ for (const output of outputs) {
58
+ if (!output || typeof output !== "object") continue;
59
+ let agentId;
60
+ if (options.agentIdOf) {
61
+ agentId = options.agentIdOf(output);
62
+ } else if (typeof output.agent === "string") {
63
+ agentId = output.agent;
64
+ } else if (typeof output.agent === "object" && output.agent !== null) {
65
+ agentId = output.agent.role || output.agent.name;
66
+ }
67
+ if (!agentId || agentId.trim().length === 0) continue;
68
+ let score = 0;
69
+ if (options.scoreOf) {
70
+ score = options.scoreOf(output);
71
+ } else if (typeof output.score === "number") {
72
+ score = output.score;
73
+ } else if (output.json_dict && typeof output.json_dict.score === "number") {
74
+ score = output.json_dict.score;
75
+ }
76
+ const confidence = options.confidenceOf ? options.confidenceOf(output) : output.json_dict && typeof output.json_dict.confidence === "number" ? output.json_dict.confidence : void 0;
77
+ const cost = options.costOf ? options.costOf(output) : typeof output.cost === "number" ? output.cost : void 0;
78
+ const tokens = options.tokensOf ? options.tokensOf(output) : typeof output.tokens === "number" ? output.tokens : void 0;
79
+ const latencyMs = typeof output.latencyMs === "number" ? output.latencyMs : void 0;
80
+ const metadata = {
81
+ ...output.json_dict || {},
82
+ ...output.raw ? { raw: output.raw } : {}
83
+ };
84
+ const finding = {
85
+ agentId: agentId.trim(),
86
+ score,
87
+ metadata
88
+ };
89
+ if (confidence !== void 0) finding.confidence = confidence;
90
+ if (cost !== void 0) finding.cost = cost;
91
+ if (tokens !== void 0) finding.tokens = tokens;
92
+ if (latencyMs !== void 0) finding.latencyMs = latencyMs;
93
+ findings.push(finding);
94
+ }
95
+ return findings;
96
+ }
97
+
98
+ // src/adapters/autogen.ts
99
+ function fromAutoGenMessages(messages, options) {
100
+ const findings = [];
101
+ for (const message of messages) {
102
+ if (!message || typeof message !== "object") continue;
103
+ const agentId = message.name || (message.role && message.role !== "user" && message.role !== "system" ? message.role : void 0);
104
+ if (!agentId || agentId.trim().length === 0) continue;
105
+ const score = options.scoreOf(message);
106
+ const confidence = options.confidenceOf ? options.confidenceOf(message) : void 0;
107
+ const tokens = options.tokensOf ? options.tokensOf(message) : void 0;
108
+ const cost = options.costOf ? options.costOf(message) : void 0;
109
+ let metadata = {};
110
+ if (typeof message.content === "object" && message.content !== null && !Array.isArray(message.content)) {
111
+ metadata = { ...message.content };
112
+ } else if (message.content !== void 0) {
113
+ metadata = { raw: message.content };
114
+ }
115
+ if (message.context) {
116
+ metadata.context = message.context;
117
+ }
118
+ const finding = {
119
+ agentId: agentId.trim(),
120
+ score,
121
+ metadata
122
+ };
123
+ if (confidence !== void 0) finding.confidence = confidence;
124
+ if (tokens !== void 0) finding.tokens = tokens;
125
+ if (cost !== void 0) finding.cost = cost;
126
+ findings.push(finding);
127
+ }
128
+ return findings;
129
+ }
130
+
131
+ // src/adapters/vercel.ts
132
+ function fromAISDKSteps(steps, options) {
133
+ const findings = [];
134
+ for (const step of steps) {
135
+ if (!step || typeof step !== "object") continue;
136
+ const agentId = options.agentIdOf ? options.agentIdOf(step) : step.toolName || step.stepType;
137
+ if (!agentId || agentId.trim().length === 0) continue;
138
+ const score = options.scoreOf(step);
139
+ const confidence = options.confidenceOf ? options.confidenceOf(step) : void 0;
140
+ const tokens = step.usage?.totalTokens ?? ((step.usage?.promptTokens || 0) + (step.usage?.completionTokens || 0) || void 0);
141
+ const cost = options.costOf ? options.costOf(step) : void 0;
142
+ const latencyMs = typeof step.latencyMs === "number" ? step.latencyMs : void 0;
143
+ let metadata = {};
144
+ if (typeof step.result === "object" && step.result !== null && !Array.isArray(step.result)) {
145
+ metadata = { ...step.result };
146
+ } else if (step.result !== void 0) {
147
+ metadata = { result: step.result };
148
+ }
149
+ if (step.args) {
150
+ metadata.args = step.args;
151
+ }
152
+ const finding = {
153
+ agentId: agentId.trim(),
154
+ score,
155
+ metadata
156
+ };
157
+ if (confidence !== void 0) finding.confidence = confidence;
158
+ if (tokens !== void 0) finding.tokens = tokens;
159
+ if (cost !== void 0) finding.cost = cost;
160
+ if (latencyMs !== void 0) finding.latencyMs = latencyMs;
161
+ findings.push(finding);
162
+ }
163
+ return findings;
164
+ }
165
+
166
+ // src/reporters/index.ts
167
+ function formatMarkdownReport(summary, options = {}) {
168
+ const title = options.title || "Agent Ablation & ROI Evaluation Report";
169
+ const lines = [];
170
+ lines.push(`# ${title}`);
171
+ lines.push("");
172
+ lines.push(`- **Evaluated Cases:** ${summary.cases}`);
173
+ lines.push(`- **Average Load-Bearing Ratio:** ${(summary.averageLoadBearingRatio * 100).toFixed(1)}%`);
174
+ if (summary.accuracy !== void 0) {
175
+ lines.push(`- **Baseline Accuracy:** ${(summary.accuracy.baselineAccuracy * 100).toFixed(1)}%`);
176
+ }
177
+ lines.push("");
178
+ lines.push("### Per-Agent Influence Breakdown");
179
+ lines.push("");
180
+ lines.push("| Agent ID | Appearances | Verdict Flips | Influence Share | Net Accuracy Impact |");
181
+ lines.push("| :--- | :--- | :--- | :--- | :--- |");
182
+ const agentIds = Object.keys(summary.perAgentInfluence);
183
+ for (const id of agentIds) {
184
+ const influence = summary.perAgentInfluence[id] ?? 0;
185
+ const stats = summary.perAgentStats?.[id];
186
+ const appearances = stats?.appearances ?? "-";
187
+ const flips = stats?.flips ?? "-";
188
+ let netAccStr = "N/A";
189
+ if (stats?.netAccuracyImpact !== void 0) {
190
+ const sign = stats.netAccuracyImpact > 0 ? "+" : "";
191
+ netAccStr = `${sign}${(stats.netAccuracyImpact * 100).toFixed(1)}% (${stats.role || "Neutral"})`;
192
+ }
193
+ lines.push(
194
+ `| \`${id}\` | ${appearances} | ${flips} | ${(influence * 100).toFixed(1)}% | ${netAccStr} |`
195
+ );
196
+ }
197
+ lines.push("");
198
+ if (summary.roi && options.includeRoi !== false) {
199
+ lines.push("### Cost & Telemetry ROI Analysis");
200
+ lines.push("");
201
+ lines.push("| Agent ID | Total Cost | Total Tokens | Cost / Verdict Flip | Cost Share | ROI Efficiency |");
202
+ lines.push("| :--- | :--- | :--- | :--- | :--- | :--- |");
203
+ for (const [id, agentRoi] of Object.entries(summary.roi.agents)) {
204
+ const costStr = agentRoi.totalCost !== void 0 ? `$${agentRoi.totalCost.toFixed(4)}` : "N/A";
205
+ const tokenStr = agentRoi.totalTokens !== void 0 ? agentRoi.totalTokens.toLocaleString() : "N/A";
206
+ const costPerFlipStr = agentRoi.costPerVerdictFlip !== void 0 ? `$${agentRoi.costPerVerdictFlip.toFixed(4)}` : "N/A";
207
+ const costShareStr = agentRoi.costShare !== void 0 ? `${(agentRoi.costShare * 100).toFixed(1)}%` : "N/A";
208
+ const roiEffStr = agentRoi.efficiencyRatio !== void 0 ? `${agentRoi.efficiencyRatio.toFixed(2)}x` : "N/A";
209
+ lines.push(
210
+ `| \`${id}\` | ${costStr} | ${tokenStr} | ${costPerFlipStr} | ${costShareStr} | ${roiEffStr} |`
211
+ );
212
+ }
213
+ lines.push("");
214
+ }
215
+ if (summary.roi?.recommendations && summary.roi.recommendations.length > 0 && options.includeRecommendations !== false) {
216
+ lines.push("### Optimization & Pruning Recommendations");
217
+ lines.push("");
218
+ for (const rec of summary.roi.recommendations) {
219
+ lines.push(`- \u26A0\uFE0F **\`${rec.agentId}\`**: ${rec.reason}`);
220
+ }
221
+ lines.push("");
222
+ }
223
+ return lines.join("\n");
224
+ }
225
+ function formatAsciiTable(summary) {
226
+ const lines = [];
227
+ lines.push("================ AGENT ABLATION SUMMARY ================");
228
+ lines.push(`Cases: ${summary.cases} | Mean Load-Bearing Ratio: ${(summary.averageLoadBearingRatio * 100).toFixed(1)}%`);
229
+ lines.push("--------------------------------------------------------");
230
+ lines.push("Agent ID | Flips | Influence | Cost/Flip");
231
+ lines.push("--------------------------------------------------------");
232
+ for (const [id, influence] of Object.entries(summary.perAgentInfluence)) {
233
+ const paddedId = id.padEnd(23, " ").slice(0, 23);
234
+ const stats = summary.perAgentStats?.[id];
235
+ const flips = stats ? `${stats.flips}/${stats.appearances}`.padEnd(5, " ") : "N/A ";
236
+ const infStr = `${(influence * 100).toFixed(1)}%`.padEnd(9, " ");
237
+ const agentRoi = summary.roi?.agents[id];
238
+ const costPerFlip = agentRoi?.costPerVerdictFlip !== void 0 ? `$${agentRoi.costPerVerdictFlip.toFixed(3)}` : "N/A";
239
+ lines.push(`${paddedId} | ${flips} | ${infStr} | ${costPerFlip}`);
240
+ }
241
+ lines.push("========================================================");
242
+ return lines.join("\n");
243
+ }
244
+
54
245
  // src/index.ts
55
246
  function defaultEquals(a, b) {
56
247
  return a === b;
57
248
  }
249
+ function majorityVote(samples, equals = defaultEquals) {
250
+ if (samples.length === 0) {
251
+ throw new Error("Cannot compute majority vote of empty sample array");
252
+ }
253
+ if (samples.length === 1) {
254
+ return samples[0];
255
+ }
256
+ const clusters = [];
257
+ for (const sample of samples) {
258
+ const existing = clusters.find((c) => equals(c.value, sample));
259
+ if (existing) {
260
+ existing.count += 1;
261
+ } else {
262
+ clusters.push({ value: sample, count: 1 });
263
+ }
264
+ }
265
+ clusters.sort((a, b) => b.count - a.count);
266
+ return clusters[0].value;
267
+ }
58
268
  function runAblation(findings, decide, equals = defaultEquals) {
59
269
  const baseline = decide(findings.slice());
60
270
  const perAgent = findings.map((finding, index) => {
@@ -77,34 +287,339 @@ function runAblation(findings, decide, equals = defaultEquals) {
77
287
  loadBearingRatio
78
288
  };
79
289
  }
80
- function batchAblation(cases, decide, equals = defaultEquals) {
81
- const results = cases.map((findings) => runAblation(findings, decide, equals));
290
+ async function runAblationAsync(findings, decide, options = {}) {
291
+ const equals = options.equals || defaultEquals;
292
+ const samples = options.samples ?? 1;
293
+ async function evaluate(fs) {
294
+ if (samples <= 1) {
295
+ return await decide(fs.slice());
296
+ }
297
+ const sampleResults = [];
298
+ for (let i = 0; i < samples; i++) {
299
+ sampleResults.push(await decide(fs.slice()));
300
+ }
301
+ return options.aggregateSamples ? options.aggregateSamples(sampleResults) : majorityVote(sampleResults, equals);
302
+ }
303
+ const baseline = await evaluate(findings);
304
+ const perAgent = [];
305
+ for (let index = 0; index < findings.length; index++) {
306
+ const finding = findings[index];
307
+ const without = findings.slice(0, index).concat(findings.slice(index + 1));
308
+ const verdictWithout = await evaluate(without);
309
+ perAgent.push({
310
+ removedAgentId: finding.agentId,
311
+ verdictWithout,
312
+ changed: !equals(baseline, verdictWithout)
313
+ });
314
+ }
315
+ const totalAgents = findings.length;
316
+ const loadBearingCount = perAgent.filter((p) => p.changed).length;
317
+ const loadBearingRatio = totalAgents === 0 ? 0 : loadBearingCount / totalAgents;
318
+ return {
319
+ baseline,
320
+ perAgent,
321
+ loadBearingCount,
322
+ totalAgents,
323
+ loadBearingRatio
324
+ };
325
+ }
326
+ function aggregateBatchResults(cases, results, options = {}) {
327
+ const equals = options.equals || defaultEquals;
82
328
  const appearances = /* @__PURE__ */ new Map();
83
329
  const changedCounts = /* @__PURE__ */ new Map();
84
- for (const result of results) {
330
+ const protectiveCounts = /* @__PURE__ */ new Map();
331
+ const correctiveCounts = /* @__PURE__ */ new Map();
332
+ const totalCostByAgent = /* @__PURE__ */ new Map();
333
+ const totalTokensByAgent = /* @__PURE__ */ new Map();
334
+ const totalLatencyByAgent = /* @__PURE__ */ new Map();
335
+ let hasTelemetry = false;
336
+ for (let caseIdx = 0; caseIdx < cases.length; caseIdx++) {
337
+ const caseFindings = cases[caseIdx];
338
+ const result = results[caseIdx];
339
+ const gt = options.groundTruth ? options.groundTruth[caseIdx] : void 0;
340
+ const baselineCorrect = gt !== void 0 ? equals(result.baseline, gt) : void 0;
341
+ for (const f of caseFindings) {
342
+ if (f.cost !== void 0) {
343
+ totalCostByAgent.set(f.agentId, (totalCostByAgent.get(f.agentId) ?? 0) + f.cost);
344
+ hasTelemetry = true;
345
+ }
346
+ if (f.tokens !== void 0) {
347
+ totalTokensByAgent.set(f.agentId, (totalTokensByAgent.get(f.agentId) ?? 0) + f.tokens);
348
+ hasTelemetry = true;
349
+ }
350
+ if (f.latencyMs !== void 0) {
351
+ totalLatencyByAgent.set(f.agentId, (totalLatencyByAgent.get(f.agentId) ?? 0) + f.latencyMs);
352
+ hasTelemetry = true;
353
+ }
354
+ }
85
355
  for (const perAgent of result.perAgent) {
86
356
  const id = perAgent.removedAgentId;
87
357
  appearances.set(id, (appearances.get(id) ?? 0) + 1);
88
358
  if (perAgent.changed) {
89
359
  changedCounts.set(id, (changedCounts.get(id) ?? 0) + 1);
90
360
  }
361
+ if (gt !== void 0 && baselineCorrect !== void 0) {
362
+ const withoutCorrect = equals(perAgent.verdictWithout, gt);
363
+ if (baselineCorrect && !withoutCorrect) {
364
+ protectiveCounts.set(id, (protectiveCounts.get(id) ?? 0) + 1);
365
+ } else if (!baselineCorrect && withoutCorrect) {
366
+ correctiveCounts.set(id, (correctiveCounts.get(id) ?? 0) + 1);
367
+ }
368
+ }
91
369
  }
92
370
  }
93
371
  const perAgentInfluence = {};
372
+ const perAgentStats = {};
94
373
  for (const [id, count] of appearances) {
95
- perAgentInfluence[id] = (changedCounts.get(id) ?? 0) / count;
374
+ const flips = changedCounts.get(id) ?? 0;
375
+ const influence = flips / count;
376
+ perAgentInfluence[id] = influence;
377
+ const stats = {
378
+ appearances: count,
379
+ flips
380
+ };
381
+ if (options.groundTruth) {
382
+ const protective = protectiveCounts.get(id) ?? 0;
383
+ const corrective = correctiveCounts.get(id) ?? 0;
384
+ const net = (protective - corrective) / count;
385
+ stats.protectiveFlips = protective;
386
+ stats.correctiveFlips = corrective;
387
+ stats.netAccuracyImpact = net;
388
+ stats.role = net > 0.01 ? "Protective" : net < -0.01 ? "Harmful" : "Neutral";
389
+ }
390
+ perAgentStats[id] = stats;
96
391
  }
97
392
  const averageLoadBearingRatio = results.length === 0 ? 0 : results.reduce((sum, r) => sum + r.loadBearingRatio, 0) / results.length;
98
393
  const summary = {
99
394
  cases: cases.length,
100
395
  averageLoadBearingRatio,
101
- perAgentInfluence
396
+ perAgentInfluence,
397
+ perAgentStats
102
398
  };
399
+ if (options.groundTruth && options.groundTruth.length === results.length) {
400
+ const correctCount = results.filter((r, idx) => equals(r.baseline, options.groundTruth[idx])).length;
401
+ summary.accuracy = {
402
+ baselineAccuracy: results.length === 0 ? 0 : correctCount / results.length,
403
+ correctBaselineCount: correctCount,
404
+ totalEvaluated: results.length
405
+ };
406
+ }
407
+ if (hasTelemetry) {
408
+ let pipelineTotalCost = 0;
409
+ let pipelineTotalTokens = 0;
410
+ for (const c of totalCostByAgent.values()) pipelineTotalCost += c;
411
+ for (const t of totalTokensByAgent.values()) pipelineTotalTokens += t;
412
+ const agentsRoi = {};
413
+ const recommendations = [];
414
+ for (const [id, count] of appearances) {
415
+ const agentCost = totalCostByAgent.get(id);
416
+ const agentTokens = totalTokensByAgent.get(id);
417
+ const agentLatency = totalLatencyByAgent.get(id);
418
+ const flips = changedCounts.get(id) ?? 0;
419
+ const influence = perAgentInfluence[id] ?? 0;
420
+ const roi = {};
421
+ if (agentCost !== void 0) {
422
+ roi.totalCost = agentCost;
423
+ if (flips > 0) roi.costPerVerdictFlip = agentCost / flips;
424
+ if (pipelineTotalCost > 0) roi.costShare = agentCost / pipelineTotalCost;
425
+ }
426
+ if (agentTokens !== void 0) {
427
+ roi.totalTokens = agentTokens;
428
+ if (flips > 0) roi.tokensPerVerdictFlip = agentTokens / flips;
429
+ }
430
+ if (agentLatency !== void 0) {
431
+ roi.averageLatencyMs = agentLatency / count;
432
+ }
433
+ if (roi.costShare !== void 0 && roi.costShare > 0) {
434
+ roi.efficiencyRatio = influence / roi.costShare;
435
+ }
436
+ agentsRoi[id] = roi;
437
+ if (roi.costShare !== void 0 && roi.costShare >= 0.2 && influence <= 0.05) {
438
+ recommendations.push({
439
+ agentId: id,
440
+ recommendation: "prune",
441
+ reason: `High compute cost (${(roi.costShare * 100).toFixed(1)}% of total cost) with negligible verdict impact (${(influence * 100).toFixed(1)}% influence).`
442
+ });
443
+ } else if (roi.efficiencyRatio !== void 0 && roi.efficiencyRatio < 0.25 && (roi.totalCost ?? 0) > 0.1) {
444
+ recommendations.push({
445
+ agentId: id,
446
+ recommendation: "downgrade_model",
447
+ reason: `Low efficiency ratio (${roi.efficiencyRatio.toFixed(2)}x). Consider replacing with a smaller/cheaper model.`
448
+ });
449
+ }
450
+ }
451
+ summary.roi = {
452
+ totalCost: pipelineTotalCost,
453
+ totalTokens: pipelineTotalTokens,
454
+ agents: agentsRoi,
455
+ recommendations
456
+ };
457
+ }
458
+ return summary;
459
+ }
460
+ function batchAblation(cases, decide, options = defaultEquals) {
461
+ const opts = typeof options === "function" ? { equals: options } : options;
462
+ const equals = opts.equals || defaultEquals;
463
+ const results = cases.map((findings) => runAblation(findings, decide, equals));
464
+ const summary = aggregateBatchResults(cases, results, opts);
465
+ return { results, summary };
466
+ }
467
+ async function batchAblationAsync(cases, decide, options = {}) {
468
+ const results = [];
469
+ for (const c of cases) {
470
+ results.push(await runAblationAsync(c, decide, options));
471
+ }
472
+ const summary = aggregateBatchResults(cases, results, options);
103
473
  return { results, summary };
104
474
  }
475
+ function runBackwardElimination(findings, decide, equals = defaultEquals) {
476
+ const baseline = decide(findings.slice());
477
+ let currentFindings = findings.slice();
478
+ const eliminatedAgentIds = [];
479
+ const steps = [];
480
+ while (currentFindings.length > 1) {
481
+ let candidateIndexToRemove = -1;
482
+ let candidateVerdict;
483
+ for (let i = 0; i < currentFindings.length; i++) {
484
+ const withoutCandidate = currentFindings.slice(0, i).concat(currentFindings.slice(i + 1));
485
+ const v = decide(withoutCandidate);
486
+ if (equals(baseline, v)) {
487
+ candidateIndexToRemove = i;
488
+ candidateVerdict = v;
489
+ break;
490
+ }
491
+ }
492
+ if (candidateIndexToRemove === -1) {
493
+ return {
494
+ baseline,
495
+ minimalFindings: currentFindings,
496
+ minimalAgentIds: currentFindings.map((f) => f.agentId),
497
+ eliminatedAgentIds,
498
+ steps,
499
+ stoppedDueToVerdictFlip: true
500
+ };
501
+ }
502
+ const removed = currentFindings[candidateIndexToRemove];
503
+ currentFindings = currentFindings.slice(0, candidateIndexToRemove).concat(currentFindings.slice(candidateIndexToRemove + 1));
504
+ eliminatedAgentIds.push(removed.agentId);
505
+ steps.push({
506
+ step: steps.length + 1,
507
+ eliminatedAgentId: removed.agentId,
508
+ remainingAgentIds: currentFindings.map((f) => f.agentId),
509
+ verdict: candidateVerdict
510
+ });
511
+ }
512
+ return {
513
+ baseline,
514
+ minimalFindings: currentFindings,
515
+ minimalAgentIds: currentFindings.map((f) => f.agentId),
516
+ eliminatedAgentIds,
517
+ steps,
518
+ stoppedDueToVerdictFlip: false
519
+ };
520
+ }
521
+ async function runBackwardEliminationAsync(findings, decide, options = {}) {
522
+ const equals = options.equals || defaultEquals;
523
+ const samples = options.samples ?? 1;
524
+ async function evaluate(fs) {
525
+ if (samples <= 1) return await decide(fs.slice());
526
+ const sampleResults = [];
527
+ for (let i = 0; i < samples; i++) sampleResults.push(await decide(fs.slice()));
528
+ return options.aggregateSamples ? options.aggregateSamples(sampleResults) : majorityVote(sampleResults, equals);
529
+ }
530
+ const baseline = await evaluate(findings);
531
+ let currentFindings = findings.slice();
532
+ const eliminatedAgentIds = [];
533
+ const steps = [];
534
+ while (currentFindings.length > 1) {
535
+ let candidateIndexToRemove = -1;
536
+ let candidateVerdict;
537
+ for (let i = 0; i < currentFindings.length; i++) {
538
+ const withoutCandidate = currentFindings.slice(0, i).concat(currentFindings.slice(i + 1));
539
+ const v = await evaluate(withoutCandidate);
540
+ if (equals(baseline, v)) {
541
+ candidateIndexToRemove = i;
542
+ candidateVerdict = v;
543
+ break;
544
+ }
545
+ }
546
+ if (candidateIndexToRemove === -1) {
547
+ return {
548
+ baseline,
549
+ minimalFindings: currentFindings,
550
+ minimalAgentIds: currentFindings.map((f) => f.agentId),
551
+ eliminatedAgentIds,
552
+ steps,
553
+ stoppedDueToVerdictFlip: true
554
+ };
555
+ }
556
+ const removed = currentFindings[candidateIndexToRemove];
557
+ currentFindings = currentFindings.slice(0, candidateIndexToRemove).concat(currentFindings.slice(candidateIndexToRemove + 1));
558
+ eliminatedAgentIds.push(removed.agentId);
559
+ steps.push({
560
+ step: steps.length + 1,
561
+ eliminatedAgentId: removed.agentId,
562
+ remainingAgentIds: currentFindings.map((f) => f.agentId),
563
+ verdict: candidateVerdict
564
+ });
565
+ }
566
+ return {
567
+ baseline,
568
+ minimalFindings: currentFindings,
569
+ minimalAgentIds: currentFindings.map((f) => f.agentId),
570
+ eliminatedAgentIds,
571
+ steps,
572
+ stoppedDueToVerdictFlip: false
573
+ };
574
+ }
575
+ function runPairwiseAblation(findings, decide, equals = defaultEquals) {
576
+ const singleResult = runAblation(findings, decide, equals);
577
+ const baseline = singleResult.baseline;
578
+ const singleChangedMap = /* @__PURE__ */ new Map();
579
+ for (const p of singleResult.perAgent) {
580
+ singleChangedMap.set(p.removedAgentId, p.changed);
581
+ }
582
+ const pairs = [];
583
+ for (let i = 0; i < findings.length; i++) {
584
+ for (let j = i + 1; j < findings.length; j++) {
585
+ const agentI = findings[i];
586
+ const agentJ = findings[j];
587
+ const withoutPair = findings.filter((_, idx) => idx !== i && idx !== j);
588
+ const verdictWithout = decide(withoutPair);
589
+ const changed = !equals(baseline, verdictWithout);
590
+ const singleIChanged = singleChangedMap.get(agentI.agentId) ?? false;
591
+ const singleJChanged = singleChangedMap.get(agentJ.agentId) ?? false;
592
+ const isInteraction = changed && !singleIChanged && !singleJChanged;
593
+ pairs.push({
594
+ pair: [agentI.agentId, agentJ.agentId],
595
+ verdictWithout,
596
+ changed,
597
+ isInteraction
598
+ });
599
+ }
600
+ }
601
+ const interactionCount = pairs.filter((p) => p.isInteraction).length;
602
+ return {
603
+ baseline,
604
+ pairs,
605
+ interactionCount
606
+ };
607
+ }
105
608
  export {
609
+ aggregateBatchResults,
106
610
  batchAblation,
611
+ batchAblationAsync,
612
+ formatAsciiTable,
613
+ formatMarkdownReport,
614
+ fromAISDKSteps,
615
+ fromAutoGenMessages,
616
+ fromCrewAITasks,
107
617
  fromLangGraphMessages,
108
618
  fromRecords,
109
- runAblation
619
+ majorityVote,
620
+ runAblation,
621
+ runAblationAsync,
622
+ runBackwardElimination,
623
+ runBackwardEliminationAsync,
624
+ runPairwiseAblation
110
625
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-ablation",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Leave-one-out ablation testing for multi-agent decision systems — find out which agents' findings actually change the outcome.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",