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