agentperf 0.1.0 → 0.2.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 +11 -4
- package/dist/{chunk-BGU3NILX.js → chunk-FCYQOMLH.js} +140 -47
- package/dist/cli.js +7 -3
- package/dist/index.d.ts +32 -3
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,10 +26,17 @@ round-trips, actions, success rate — as a markdown table and JSON under
|
|
|
26
26
|
|
|
27
27
|
Options: `--lane both|tools|dom` · `--runs N` · `--model <id>` (default
|
|
28
28
|
`gpt-5.6-luna`) · `--base-url <url>` for compatible providers · `--task
|
|
29
|
-
booking|path/to/task.json` · `--max-turns N` · `--
|
|
30
|
-
`{ id, prompt, successPattern, maxTurns }` — the
|
|
31
|
-
lanes verbatim and `successPattern` is a regex the
|
|
32
|
-
match when the task is truly done.
|
|
29
|
+
booking|path/to/task.json` · `--max-turns N` · `--max-snapshot-chars N` ·
|
|
30
|
+
`--out dir`. A task file is `{ id, prompt, successPattern, maxTurns }` — the
|
|
31
|
+
prompt is handed to both lanes verbatim and `successPattern` is a regex the
|
|
32
|
+
page's visible text must match when the task is truly done.
|
|
33
|
+
|
|
34
|
+
`--max-snapshot-chars` caps the accessibility snapshot the DOM lane receives
|
|
35
|
+
per `read_page`. It defaults to 120,000 — deliberately generous, so a heavy
|
|
36
|
+
page reaches the DOM agent whole and it pays for the page in tokens instead
|
|
37
|
+
of being handicapped by truncation. Runs that still hit the cap are flagged
|
|
38
|
+
in the report, because their measured cost is a floor rather than the real
|
|
39
|
+
number.
|
|
33
40
|
|
|
34
41
|
Instrument your app with [`@agentperf/react`](https://www.npmjs.com/package/@agentperf/react)
|
|
35
42
|
to give it the tools lane.
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
// src/llm.ts
|
|
2
|
+
var RETRY_STATUSES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
3
|
+
var MAX_ATTEMPTS = 5;
|
|
4
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2
5
|
async function chat(config, messages, tools) {
|
|
6
|
+
let lastError = "";
|
|
7
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
8
|
+
try {
|
|
9
|
+
return await chatOnce(config, messages, tools);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
12
|
+
const status = Number(/\((\d{3})\)/.exec(lastError)?.[1]);
|
|
13
|
+
if (!RETRY_STATUSES.has(status) || attempt === MAX_ATTEMPTS) throw error;
|
|
14
|
+
await sleep(2e3 * 2 ** (attempt - 1));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
throw new Error(lastError);
|
|
18
|
+
}
|
|
19
|
+
async function chatOnce(config, messages, tools) {
|
|
3
20
|
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
4
21
|
method: "POST",
|
|
5
22
|
headers: {
|
|
@@ -56,13 +73,8 @@ var TASK_COMPLETE = {
|
|
|
56
73
|
required: ["summary"]
|
|
57
74
|
}
|
|
58
75
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{ role: "system", content: options.systemPrompt },
|
|
62
|
-
{ role: "user", content: options.taskPrompt }
|
|
63
|
-
];
|
|
64
|
-
const tools = [...options.tools, TASK_COMPLETE];
|
|
65
|
-
const outcome = {
|
|
76
|
+
function newLoopOutcome() {
|
|
77
|
+
return {
|
|
66
78
|
turns: 0,
|
|
67
79
|
actions: 0,
|
|
68
80
|
promptTokens: 0,
|
|
@@ -70,6 +82,13 @@ async function runAgentLoop(options) {
|
|
|
70
82
|
claimedDone: false,
|
|
71
83
|
claimSummary: ""
|
|
72
84
|
};
|
|
85
|
+
}
|
|
86
|
+
async function runAgentLoop(options, outcome = newLoopOutcome()) {
|
|
87
|
+
const messages = [
|
|
88
|
+
{ role: "system", content: options.systemPrompt },
|
|
89
|
+
{ role: "user", content: options.taskPrompt }
|
|
90
|
+
];
|
|
91
|
+
const tools = [...options.tools, TASK_COMPLETE];
|
|
73
92
|
let nudged = false;
|
|
74
93
|
while (outcome.turns < options.maxTurns) {
|
|
75
94
|
outcome.turns += 1;
|
|
@@ -153,10 +172,17 @@ var HOST_SHIM = `
|
|
|
153
172
|
|
|
154
173
|
// src/lanes.ts
|
|
155
174
|
import { chromium } from "playwright";
|
|
156
|
-
async function
|
|
175
|
+
async function matchesSuccess(page, task) {
|
|
157
176
|
const text = await page.evaluate(() => document.body.innerText);
|
|
158
177
|
return new RegExp(task.successPattern).test(text);
|
|
159
178
|
}
|
|
179
|
+
async function assertNotAlreadySatisfied(page, task) {
|
|
180
|
+
if (await matchesSuccess(page, task)) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`successPattern /${task.successPattern}/ already matches the page before the agent acted \u2014 the task cannot be verified against this URL`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
160
186
|
function metrics(lane, task) {
|
|
161
187
|
return {
|
|
162
188
|
lane,
|
|
@@ -170,7 +196,7 @@ function metrics(lane, task) {
|
|
|
170
196
|
totalTokens: 0
|
|
171
197
|
};
|
|
172
198
|
}
|
|
173
|
-
function finish(run, startedMs, loop, verified, claimedDone) {
|
|
199
|
+
function finish(run, startedMs, loop, verified, claimedDone, override = {}) {
|
|
174
200
|
run.wallClockMs = Math.round(performance.now() - startedMs);
|
|
175
201
|
run.turns = loop.turns;
|
|
176
202
|
run.actions = loop.actions;
|
|
@@ -178,20 +204,23 @@ function finish(run, startedMs, loop, verified, claimedDone) {
|
|
|
178
204
|
run.completionTokens = loop.completionTokens;
|
|
179
205
|
run.totalTokens = loop.promptTokens + loop.completionTokens;
|
|
180
206
|
run.success = verified;
|
|
207
|
+
run.claimSummary = loop.claimSummary || void 0;
|
|
181
208
|
if (!verified) {
|
|
182
|
-
run.failure = loop.failure ?? (claimedDone ? "model claimed completion but the page does not show the expected outcome" : "run ended without completion");
|
|
209
|
+
run.failure = override.failure ?? loop.failure ?? (claimedDone ? "model claimed completion but the page does not show the expected outcome" : "run ended without completion");
|
|
183
210
|
}
|
|
184
211
|
return run;
|
|
185
212
|
}
|
|
186
213
|
async function runToolsLane(url, task, llm) {
|
|
187
214
|
const browser = await chromium.launch();
|
|
188
215
|
const run = metrics("tools", task);
|
|
216
|
+
const outcome = newLoopOutcome();
|
|
189
217
|
const startedMs = performance.now();
|
|
190
218
|
try {
|
|
191
219
|
const context = await browser.newContext();
|
|
192
220
|
await context.addInitScript(HOST_SHIM);
|
|
193
221
|
const page = await context.newPage();
|
|
194
222
|
await page.goto(url, { waitUntil: "load" });
|
|
223
|
+
await assertNotAlreadySatisfied(page, task);
|
|
195
224
|
await page.waitForFunction(() => window.__agentperf?.listTools().length > 0, void 0, {
|
|
196
225
|
timeout: 15e3
|
|
197
226
|
});
|
|
@@ -203,7 +232,7 @@ async function runToolsLane(url, task, llm) {
|
|
|
203
232
|
}));
|
|
204
233
|
const loop = await runAgentLoop({
|
|
205
234
|
llm,
|
|
206
|
-
systemPrompt: "You complete tasks on a web page through the page's own tools (WebMCP). Call get_page_state first to orient when it exists. Use only the tools. When the task is fully done, call task_complete.",
|
|
235
|
+
systemPrompt: "You complete tasks on a web page through the page's own tools (WebMCP). Call get_page_state first to orient when it exists. Use only the tools. Every action returns the updated result. When the task is fully done, call task_complete.",
|
|
207
236
|
taskPrompt: task.prompt,
|
|
208
237
|
tools,
|
|
209
238
|
maxTurns: task.maxTurns,
|
|
@@ -215,17 +244,18 @@ async function runToolsLane(url, task, llm) {
|
|
|
215
244
|
),
|
|
216
245
|
[name, args]
|
|
217
246
|
);
|
|
247
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
218
248
|
const text = result.content.map((c) => c.text).join("\n");
|
|
219
249
|
return result.isError ? `TOOL ERROR:
|
|
220
250
|
${text}` : text;
|
|
221
251
|
}
|
|
222
|
-
});
|
|
223
|
-
const verified = loop.claimedDone && await
|
|
252
|
+
}, outcome);
|
|
253
|
+
const verified = loop.claimedDone && await matchesSuccess(page, task);
|
|
224
254
|
return finish(run, startedMs, loop, verified, loop.claimedDone);
|
|
225
255
|
} catch (error) {
|
|
226
|
-
run
|
|
227
|
-
|
|
228
|
-
|
|
256
|
+
return finish(run, startedMs, outcome, false, outcome.claimedDone, {
|
|
257
|
+
failure: error instanceof Error ? error.message : String(error)
|
|
258
|
+
});
|
|
229
259
|
} finally {
|
|
230
260
|
await browser.close();
|
|
231
261
|
}
|
|
@@ -233,12 +263,12 @@ ${text}` : text;
|
|
|
233
263
|
var DOM_TOOLS = [
|
|
234
264
|
{
|
|
235
265
|
name: "read_page",
|
|
236
|
-
description: "Read the page's current accessibility tree (roles and names).
|
|
266
|
+
description: "Read the page's current accessibility tree (roles and names).",
|
|
237
267
|
parameters: { type: "object", properties: {} }
|
|
238
268
|
},
|
|
239
269
|
{
|
|
240
270
|
name: "click",
|
|
241
|
-
description: "Click the element with this ARIA role and accessible name.",
|
|
271
|
+
description: "Click the element with this ARIA role and accessible name. Returns the updated page.",
|
|
242
272
|
parameters: {
|
|
243
273
|
type: "object",
|
|
244
274
|
properties: {
|
|
@@ -250,7 +280,7 @@ var DOM_TOOLS = [
|
|
|
250
280
|
},
|
|
251
281
|
{
|
|
252
282
|
name: "fill",
|
|
253
|
-
description: "Type a value into the input with this ARIA role and accessible name (replaces content).",
|
|
283
|
+
description: "Type a value into the input with this ARIA role and accessible name (replaces content). Returns the updated page.",
|
|
254
284
|
parameters: {
|
|
255
285
|
type: "object",
|
|
256
286
|
properties: {
|
|
@@ -260,53 +290,88 @@ var DOM_TOOLS = [
|
|
|
260
290
|
},
|
|
261
291
|
required: ["role", "name", "value"]
|
|
262
292
|
}
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "select_option",
|
|
296
|
+
description: "Choose an option in a dropdown (select) by its accessible name and the option's visible label. Returns the updated page.",
|
|
297
|
+
parameters: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
name: { type: "string", description: "Accessible name of the dropdown" },
|
|
301
|
+
option: { type: "string", description: "Visible label of the option to choose" }
|
|
302
|
+
},
|
|
303
|
+
required: ["name", "option"]
|
|
304
|
+
}
|
|
263
305
|
}
|
|
264
306
|
];
|
|
265
|
-
var
|
|
266
|
-
|
|
307
|
+
var DEFAULT_MAX_SNAPSHOT_CHARS = 12e4;
|
|
308
|
+
var SETTLE_MS = 150;
|
|
309
|
+
async function runDomLane(url, task, llm, options = {}) {
|
|
310
|
+
const maxSnapshotChars = options.maxSnapshotChars ?? DEFAULT_MAX_SNAPSHOT_CHARS;
|
|
267
311
|
const browser = await chromium.launch();
|
|
268
312
|
const run = metrics("dom", task);
|
|
313
|
+
const outcome = newLoopOutcome();
|
|
269
314
|
const startedMs = performance.now();
|
|
270
315
|
try {
|
|
271
316
|
const context = await browser.newContext();
|
|
272
317
|
const page = await context.newPage();
|
|
273
318
|
await page.goto(url, { waitUntil: "load" });
|
|
319
|
+
await assertNotAlreadySatisfied(page, task);
|
|
274
320
|
const target = (role, name) => page.getByRole(String(role), {
|
|
275
321
|
name: String(name),
|
|
276
322
|
exact: false
|
|
277
323
|
}).first();
|
|
278
324
|
const loop = await runAgentLoop({
|
|
279
325
|
llm,
|
|
280
|
-
systemPrompt: "You complete tasks on a web page the way a browser-driving agent does: read the accessibility tree with read_page, then click and
|
|
326
|
+
systemPrompt: "You complete tasks on a web page the way a browser-driving agent does: read the accessibility tree with read_page, then click, fill and select_option by role and name. Every action returns the updated page. When the task is fully done, call task_complete.",
|
|
281
327
|
taskPrompt: task.prompt,
|
|
282
328
|
tools: DOM_TOOLS,
|
|
283
329
|
maxTurns: task.maxTurns,
|
|
284
330
|
invoke: async (name, args) => {
|
|
331
|
+
const snapshot = async () => {
|
|
332
|
+
const text = await page.locator("body").ariaSnapshot();
|
|
333
|
+
if (text.length <= maxSnapshotChars) return text;
|
|
334
|
+
run.snapshotTruncated = true;
|
|
335
|
+
return text.slice(0, maxSnapshotChars) + "\n\u2026 (truncated)";
|
|
336
|
+
};
|
|
285
337
|
switch (name) {
|
|
286
|
-
case "read_page":
|
|
287
|
-
|
|
288
|
-
return snapshot.length > MAX_SNAPSHOT_CHARS ? snapshot.slice(0, MAX_SNAPSHOT_CHARS) + "\n\u2026 (truncated)" : snapshot;
|
|
289
|
-
}
|
|
338
|
+
case "read_page":
|
|
339
|
+
return snapshot();
|
|
290
340
|
case "click": {
|
|
291
341
|
await target(args.role, args.name).click({ timeout: 5e3 });
|
|
292
|
-
await page.waitForTimeout(
|
|
293
|
-
return `Clicked ${String(args.role)} "${String(args.name)}"
|
|
342
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
343
|
+
return `Clicked ${String(args.role)} "${String(args.name)}".
|
|
344
|
+
|
|
345
|
+
${await snapshot()}`;
|
|
294
346
|
}
|
|
295
347
|
case "fill": {
|
|
296
348
|
await target(args.role, args.name).fill(String(args.value ?? ""), { timeout: 5e3 });
|
|
297
|
-
|
|
349
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
350
|
+
return `Filled ${String(args.role)} "${String(args.name)}".
|
|
351
|
+
|
|
352
|
+
${await snapshot()}`;
|
|
353
|
+
}
|
|
354
|
+
case "select_option": {
|
|
355
|
+
await target("combobox", args.name).selectOption(
|
|
356
|
+
{ label: String(args.option) },
|
|
357
|
+
{ timeout: 5e3 }
|
|
358
|
+
);
|
|
359
|
+
await page.waitForTimeout(SETTLE_MS);
|
|
360
|
+
return `Selected "${String(args.option)}" in "${String(args.name)}".
|
|
361
|
+
|
|
362
|
+
${await snapshot()}`;
|
|
298
363
|
}
|
|
299
364
|
default:
|
|
300
365
|
return `ACTION FAILED: unknown action "${name}"`;
|
|
301
366
|
}
|
|
302
367
|
}
|
|
303
|
-
});
|
|
304
|
-
const verified = loop.claimedDone && await
|
|
368
|
+
}, outcome);
|
|
369
|
+
const verified = loop.claimedDone && await matchesSuccess(page, task);
|
|
305
370
|
return finish(run, startedMs, loop, verified, loop.claimedDone);
|
|
306
371
|
} catch (error) {
|
|
307
|
-
run
|
|
308
|
-
|
|
309
|
-
|
|
372
|
+
return finish(run, startedMs, outcome, false, outcome.claimedDone, {
|
|
373
|
+
failure: error instanceof Error ? error.message : String(error)
|
|
374
|
+
});
|
|
310
375
|
} finally {
|
|
311
376
|
await browser.close();
|
|
312
377
|
}
|
|
@@ -321,19 +386,37 @@ function median(values) {
|
|
|
321
386
|
const mid = Math.floor(sorted.length / 2);
|
|
322
387
|
return sorted.length % 2 === 1 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);
|
|
323
388
|
}
|
|
389
|
+
function mean(values) {
|
|
390
|
+
if (values.length === 0) return 0;
|
|
391
|
+
return Math.round(values.reduce((sum, v) => sum + v, 0) / values.length);
|
|
392
|
+
}
|
|
393
|
+
function range(values) {
|
|
394
|
+
if (values.length === 0) return [0, 0];
|
|
395
|
+
return [Math.min(...values), Math.max(...values)];
|
|
396
|
+
}
|
|
324
397
|
function aggregate(lane, runs) {
|
|
398
|
+
const ok = runs.filter((r) => r.success);
|
|
399
|
+
const wall = ok.map((r) => r.wallClockMs);
|
|
400
|
+
const tokens = ok.map((r) => r.totalTokens);
|
|
325
401
|
return {
|
|
326
402
|
lane,
|
|
327
403
|
runs,
|
|
328
|
-
successRate: runs.length === 0 ? 0 :
|
|
329
|
-
medianWallClockMs: median(
|
|
330
|
-
medianTotalTokens: median(
|
|
331
|
-
medianTurns: median(
|
|
404
|
+
successRate: runs.length === 0 ? 0 : ok.length / runs.length,
|
|
405
|
+
medianWallClockMs: median(wall),
|
|
406
|
+
medianTotalTokens: median(tokens),
|
|
407
|
+
medianTurns: median(ok.map((r) => r.turns)),
|
|
408
|
+
meanWallClockMs: mean(wall),
|
|
409
|
+
meanTotalTokens: mean(tokens),
|
|
410
|
+
wallClockRangeMs: range(wall),
|
|
411
|
+
totalTokensRange: range(tokens)
|
|
332
412
|
};
|
|
333
413
|
}
|
|
334
414
|
function seconds(ms) {
|
|
335
415
|
return (ms / 1e3).toFixed(1) + "s";
|
|
336
416
|
}
|
|
417
|
+
function ratio(dom, tools) {
|
|
418
|
+
return tools > 0 ? (dom / tools).toFixed(2) + "x" : "n/a";
|
|
419
|
+
}
|
|
337
420
|
function toMarkdown(report) {
|
|
338
421
|
const lines = [
|
|
339
422
|
`# AgentPerf report \u2014 ${report.task.id}`,
|
|
@@ -343,30 +426,39 @@ function toMarkdown(report) {
|
|
|
343
426
|
`- **Task:** ${report.task.prompt}`,
|
|
344
427
|
`- **Started:** ${report.startedAt}`,
|
|
345
428
|
"",
|
|
346
|
-
"| lane | success | median wall-clock | median tokens |
|
|
347
|
-
"
|
|
429
|
+
"| lane | success | median wall-clock | mean wall-clock | median tokens | mean tokens | round-trips |",
|
|
430
|
+
"|------|---------|-------------------|-----------------|---------------|-------------|-------------|"
|
|
348
431
|
];
|
|
349
432
|
for (const lane of report.lanes) {
|
|
350
|
-
const
|
|
433
|
+
const passed = lane.runs.filter((r) => r.success).length;
|
|
351
434
|
lines.push(
|
|
352
|
-
`| ${lane.lane} | ${
|
|
435
|
+
`| ${lane.lane} | ${Math.round(lane.successRate * 100)}% (${passed}/${lane.runs.length}) | ${seconds(lane.medianWallClockMs)} | ${seconds(lane.meanWallClockMs)} | ${lane.medianTotalTokens.toLocaleString()} | ${lane.meanTotalTokens.toLocaleString()} | ${lane.medianTurns} |`
|
|
353
436
|
);
|
|
354
437
|
}
|
|
355
438
|
const tools = report.lanes.find((l) => l.lane === "tools");
|
|
356
439
|
const dom = report.lanes.find((l) => l.lane === "dom");
|
|
357
|
-
if (tools && dom && tools.medianTotalTokens > 0
|
|
440
|
+
if (tools && dom && tools.medianTotalTokens > 0) {
|
|
358
441
|
lines.push(
|
|
359
442
|
"",
|
|
360
|
-
`**DOM
|
|
443
|
+
`**DOM vs tools \u2014 tokens ${ratio(dom.medianTotalTokens, tools.medianTotalTokens)} (median) / ${ratio(dom.meanTotalTokens, tools.meanTotalTokens)} (mean); wall-clock ${ratio(dom.medianWallClockMs, tools.medianWallClockMs)} (median) / ${ratio(dom.meanWallClockMs, tools.meanWallClockMs)} (mean); round-trips ${dom.medianTurns} vs ${tools.medianTurns}.**`,
|
|
444
|
+
"",
|
|
445
|
+
`Spread \u2014 DOM ${seconds(dom.wallClockRangeMs[0])}\u2013${seconds(dom.wallClockRangeMs[1])}, ${dom.totalTokensRange[0].toLocaleString()}\u2013${dom.totalTokensRange[1].toLocaleString()} tokens; tools ${seconds(tools.wallClockRangeMs[0])}\u2013${seconds(tools.wallClockRangeMs[1])}, ${tools.totalTokensRange[0].toLocaleString()}\u2013${tools.totalTokensRange[1].toLocaleString()} tokens.`,
|
|
446
|
+
"",
|
|
447
|
+
"Central tendencies are over successful runs only; failures are counted in the success rate. Token counts are uncached prompt+completion totals as the API reports them \u2014 provider prompt caching may reduce billed cost, and does so unequally across lanes."
|
|
361
448
|
);
|
|
362
449
|
}
|
|
363
450
|
lines.push("", "## Runs", "");
|
|
364
|
-
lines.push("| lane | # | success | wall-clock | tokens | round-trips | actions |
|
|
365
|
-
lines.push("
|
|
451
|
+
lines.push("| lane | # | success | wall-clock | tokens | round-trips | actions | notes |");
|
|
452
|
+
lines.push("|------|---|---------|------------|--------|-------------|---------|-------|");
|
|
366
453
|
for (const lane of report.lanes) {
|
|
367
454
|
lane.runs.forEach((r, i) => {
|
|
455
|
+
const notes = [
|
|
456
|
+
r.failure,
|
|
457
|
+
r.snapshotTruncated ? "snapshot truncated \u2014 cost is a floor" : "",
|
|
458
|
+
r.claimSummary ? `claim: ${r.claimSummary.slice(0, 90)}` : ""
|
|
459
|
+
].filter(Boolean).join("; ");
|
|
368
460
|
lines.push(
|
|
369
|
-
`| ${r.lane} | ${i + 1} | ${r.success ? "\u2713" : "\u2717"} | ${seconds(r.wallClockMs)} | ${r.totalTokens.toLocaleString()} | ${r.turns} | ${r.actions} | ${
|
|
461
|
+
`| ${r.lane} | ${i + 1} | ${r.success ? "\u2713" : "\u2717"} | ${seconds(r.wallClockMs)} | ${r.totalTokens.toLocaleString()} | ${r.turns} | ${r.actions} | ${notes} |`
|
|
370
462
|
);
|
|
371
463
|
});
|
|
372
464
|
}
|
|
@@ -388,6 +480,7 @@ export {
|
|
|
388
480
|
runAgentLoop,
|
|
389
481
|
HOST_SHIM,
|
|
390
482
|
runToolsLane,
|
|
483
|
+
DEFAULT_MAX_SNAPSHOT_CHARS,
|
|
391
484
|
runDomLane,
|
|
392
485
|
aggregate,
|
|
393
486
|
toMarkdown,
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_MAX_SNAPSHOT_CHARS,
|
|
3
4
|
aggregate,
|
|
4
5
|
runDomLane,
|
|
5
6
|
runToolsLane,
|
|
6
7
|
toMarkdown,
|
|
7
8
|
writeReport
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-FCYQOMLH.js";
|
|
9
10
|
|
|
10
11
|
// src/cli.ts
|
|
11
12
|
import { readFileSync } from "fs";
|
|
@@ -43,6 +44,9 @@ async function main() {
|
|
|
43
44
|
const lanes = laneArg === "both" ? ["dom", "tools"] : [laneArg];
|
|
44
45
|
const runs = Number(arg("runs", "3"));
|
|
45
46
|
const outDir = arg("out", "results");
|
|
47
|
+
const laneOptions = {
|
|
48
|
+
maxSnapshotChars: Number(arg("max-snapshot-chars", String(DEFAULT_MAX_SNAPSHOT_CHARS)))
|
|
49
|
+
};
|
|
46
50
|
console.log(`AgentPerf \u2014 ${task.id} @ ${url}`);
|
|
47
51
|
console.log(`model ${llm.model}, ${runs} run(s) per lane: ${lanes.join(", ")}
|
|
48
52
|
`);
|
|
@@ -57,10 +61,10 @@ async function main() {
|
|
|
57
61
|
const results = [];
|
|
58
62
|
for (let i = 1; i <= runs; i++) {
|
|
59
63
|
process.stdout.write(`[${lane}] run ${i}/${runs}\u2026 `);
|
|
60
|
-
const result = lane === "tools" ? await runToolsLane(url, task, llm) : await runDomLane(url, task, llm);
|
|
64
|
+
const result = lane === "tools" ? await runToolsLane(url, task, llm) : await runDomLane(url, task, llm, laneOptions);
|
|
61
65
|
results.push(result);
|
|
62
66
|
console.log(
|
|
63
|
-
result.success ? `\u2713 ${(result.wallClockMs / 1e3).toFixed(1)}s, ${result.totalTokens} tokens, ${result.turns} turns` : `\u2717 ${result.failure}`
|
|
67
|
+
result.success ? `\u2713 ${(result.wallClockMs / 1e3).toFixed(1)}s, ${result.totalTokens} tokens, ${result.turns} turns${result.snapshotTruncated ? " (snapshot truncated \u2014 cost is a floor)" : ""}` : `\u2717 ${result.failure}`
|
|
64
68
|
);
|
|
65
69
|
}
|
|
66
70
|
report.lanes.push(aggregate(lane, results));
|
package/dist/index.d.ts
CHANGED
|
@@ -27,10 +27,22 @@ interface TaskSpec {
|
|
|
27
27
|
/** Hard cap on model round-trips before the run is failed. */
|
|
28
28
|
maxTurns: number;
|
|
29
29
|
}
|
|
30
|
+
interface LaneOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Cap on the accessibility snapshot handed to the DOM lane per `read_page`.
|
|
33
|
+
* Set it high enough that the page is never truncated: a truncated snapshot
|
|
34
|
+
* handicaps the DOM lane instead of letting it pay honestly in tokens.
|
|
35
|
+
*/
|
|
36
|
+
maxSnapshotChars?: number;
|
|
37
|
+
}
|
|
30
38
|
interface RunMetrics {
|
|
31
39
|
lane: Lane;
|
|
32
40
|
taskId: string;
|
|
33
41
|
success: boolean;
|
|
42
|
+
/** Set when the DOM lane's snapshot hit the cap — the run is then a floor, not a measurement. */
|
|
43
|
+
snapshotTruncated?: boolean;
|
|
44
|
+
/** What the model said it did, recorded so a reader can audit the run rather than trust it. */
|
|
45
|
+
claimSummary?: string;
|
|
34
46
|
/** Why a run failed, when it did. */
|
|
35
47
|
failure?: string;
|
|
36
48
|
wallClockMs: number;
|
|
@@ -46,9 +58,14 @@ interface LaneAggregate {
|
|
|
46
58
|
lane: Lane;
|
|
47
59
|
runs: RunMetrics[];
|
|
48
60
|
successRate: number;
|
|
61
|
+
/** Central tendencies are over SUCCESSFUL runs only; failures live in successRate. */
|
|
49
62
|
medianWallClockMs: number;
|
|
50
63
|
medianTotalTokens: number;
|
|
51
64
|
medianTurns: number;
|
|
65
|
+
meanWallClockMs: number;
|
|
66
|
+
meanTotalTokens: number;
|
|
67
|
+
wallClockRangeMs: [number, number];
|
|
68
|
+
totalTokensRange: [number, number];
|
|
52
69
|
}
|
|
53
70
|
interface BenchmarkReport {
|
|
54
71
|
url: string;
|
|
@@ -77,7 +94,14 @@ declare global {
|
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
96
|
declare function runToolsLane(url: string, task: TaskSpec, llm: LlmConfig): Promise<RunMetrics>;
|
|
80
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Deliberately generous: a heavy page must reach the DOM lane whole, so it
|
|
99
|
+
* pays for the page in tokens rather than being handicapped by truncation.
|
|
100
|
+
* Runs that still hit the cap are flagged — their cost is a floor, not a
|
|
101
|
+
* measurement.
|
|
102
|
+
*/
|
|
103
|
+
declare const DEFAULT_MAX_SNAPSHOT_CHARS = 120000;
|
|
104
|
+
declare function runDomLane(url: string, task: TaskSpec, llm: LlmConfig, options?: LaneOptions): Promise<RunMetrics>;
|
|
81
105
|
|
|
82
106
|
/**
|
|
83
107
|
* The agent loop both lanes share, so nothing about the driver differs
|
|
@@ -101,8 +125,13 @@ declare function runAgentLoop(options: {
|
|
|
101
125
|
tools: LlmToolDef[];
|
|
102
126
|
invoke: (name: string, args: Record<string, unknown>) => Promise<string>;
|
|
103
127
|
maxTurns: number;
|
|
104
|
-
}): Promise<LoopOutcome>;
|
|
128
|
+
}, outcome?: LoopOutcome): Promise<LoopOutcome>;
|
|
105
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Central tendencies are computed over successful runs only. A crashed run
|
|
132
|
+
* records near-zero cost, and averaging that in would drag a lane's reported
|
|
133
|
+
* cost toward zero — understating exactly the lane that failed.
|
|
134
|
+
*/
|
|
106
135
|
declare function aggregate(lane: Lane, runs: RunMetrics[]): LaneAggregate;
|
|
107
136
|
declare function toMarkdown(report: BenchmarkReport): string;
|
|
108
137
|
declare function writeReport(report: BenchmarkReport, outDir: string): {
|
|
@@ -118,4 +147,4 @@ declare function writeReport(report: BenchmarkReport, outDir: string): {
|
|
|
118
147
|
*/
|
|
119
148
|
declare const HOST_SHIM = "\n(() => {\n const tools = new Map();\n const api = {\n listTools: () =>\n Array.from(tools.values()).map((t) => ({\n name: t.name,\n description: t.description,\n inputSchema: t.inputSchema || { type: \"object\", properties: {} },\n annotations: t.annotations || {}\n })),\n call: async (name, args) => {\n const tool = tools.get(name);\n if (!tool) {\n return {\n content: [{ type: \"text\", text: \"No tool named \\\"\" + name + \"\\\" is registered on this page.\" }],\n isError: true\n };\n }\n const result = await tool.execute(args || {});\n if (typeof result === \"string\") return { content: [{ type: \"text\", text: result }] };\n return result;\n }\n };\n Object.defineProperty(window, \"__agentperf\", { value: api });\n\n const modelContext = {\n registerTool: (tool, options) => {\n tools.set(tool.name, tool);\n if (options && options.signal) {\n options.signal.addEventListener(\"abort\", () => { tools.delete(tool.name); });\n }\n },\n getTools: async () => api.listTools()\n };\n Object.defineProperty(document, \"modelContext\", { value: modelContext, configurable: true });\n Object.defineProperty(navigator, \"modelContext\", { value: modelContext, configurable: true });\n})();\n";
|
|
120
149
|
|
|
121
|
-
export { type BenchmarkReport, HOST_SHIM, type Lane, type LaneAggregate, type RunMetrics, type TaskSpec, aggregate, runAgentLoop, runDomLane, runToolsLane, toMarkdown, writeReport };
|
|
150
|
+
export { type BenchmarkReport, DEFAULT_MAX_SNAPSHOT_CHARS, HOST_SHIM, type Lane, type LaneAggregate, type LaneOptions, type RunMetrics, type TaskSpec, aggregate, runAgentLoop, runDomLane, runToolsLane, toMarkdown, writeReport };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_MAX_SNAPSHOT_CHARS,
|
|
2
3
|
HOST_SHIM,
|
|
3
4
|
aggregate,
|
|
4
5
|
runAgentLoop,
|
|
@@ -6,8 +7,9 @@ import {
|
|
|
6
7
|
runToolsLane,
|
|
7
8
|
toMarkdown,
|
|
8
9
|
writeReport
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-FCYQOMLH.js";
|
|
10
11
|
export {
|
|
12
|
+
DEFAULT_MAX_SNAPSHOT_CHARS,
|
|
11
13
|
HOST_SHIM,
|
|
12
14
|
aggregate,
|
|
13
15
|
runAgentLoop,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentperf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Measure what AI agents pay to use a website: the same task via DOM driving vs. WebMCP tool calls — wall-clock, tokens, round-trips, success rate.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|