pi-agent-browser-native 0.2.24 → 0.2.26
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/CHANGELOG.md +48 -1
- package/README.md +137 -13
- package/docs/ARCHITECTURE.md +54 -7
- package/docs/COMMAND_REFERENCE.md +586 -42
- package/docs/RELEASE.md +61 -7
- package/docs/REQUIREMENTS.md +14 -1
- package/docs/SUPPORT_MATRIX.md +85 -0
- package/docs/TOOL_CONTRACT.md +301 -24
- package/extensions/agent-browser/index.ts +1983 -38
- package/extensions/agent-browser/lib/playbook.ts +23 -12
- package/extensions/agent-browser/lib/results/presentation.ts +706 -37
- package/extensions/agent-browser/lib/results/shared.ts +437 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +69 -9
- package/extensions/agent-browser/lib/results.ts +12 -0
- package/extensions/agent-browser/lib/runtime.ts +82 -10
- package/package.json +4 -2
- package/scripts/agent-browser-capability-baseline.mjs +499 -110
- package/scripts/doctor.mjs +1 -1
|
@@ -19,6 +19,55 @@ export interface AgentBrowserBatchResult {
|
|
|
19
19
|
success?: boolean;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export type AgentBrowserResultCategory = "failure" | "success";
|
|
23
|
+
|
|
24
|
+
export type AgentBrowserSuccessCategory = "artifact-saved" | "artifact-unverified" | "completed" | "inspection";
|
|
25
|
+
|
|
26
|
+
export type AgentBrowserFailureCategory =
|
|
27
|
+
| "aborted"
|
|
28
|
+
| "confirmation-required"
|
|
29
|
+
| "download-not-verified"
|
|
30
|
+
| "missing-binary"
|
|
31
|
+
| "parse-failure"
|
|
32
|
+
| "qa-failure"
|
|
33
|
+
| "selector-not-found"
|
|
34
|
+
| "selector-unsupported"
|
|
35
|
+
| "stale-ref"
|
|
36
|
+
| "tab-drift"
|
|
37
|
+
| "timeout"
|
|
38
|
+
| "upstream-error"
|
|
39
|
+
| "validation-error";
|
|
40
|
+
|
|
41
|
+
export interface AgentBrowserResultCategoryDetails {
|
|
42
|
+
failureCategory?: AgentBrowserFailureCategory;
|
|
43
|
+
resultCategory: AgentBrowserResultCategory;
|
|
44
|
+
successCategory?: AgentBrowserSuccessCategory;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AgentBrowserPageChangeSummary {
|
|
48
|
+
artifactCount?: number;
|
|
49
|
+
changeType: "artifact" | "confirmation" | "mutation" | "navigation";
|
|
50
|
+
command?: string;
|
|
51
|
+
nextActionIds?: string[];
|
|
52
|
+
savedFilePath?: string;
|
|
53
|
+
summary: string;
|
|
54
|
+
title?: string;
|
|
55
|
+
url?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AgentBrowserNextAction {
|
|
59
|
+
artifactPath?: string;
|
|
60
|
+
id: string;
|
|
61
|
+
params?: {
|
|
62
|
+
args: string[];
|
|
63
|
+
sessionMode?: "auto" | "fresh";
|
|
64
|
+
stdin?: string;
|
|
65
|
+
};
|
|
66
|
+
reason: string;
|
|
67
|
+
safety?: string;
|
|
68
|
+
tool: "agent_browser";
|
|
69
|
+
}
|
|
70
|
+
|
|
22
71
|
export type FileArtifactKind = "download" | "file" | "har" | "image" | "pdf" | "profile" | "trace" | "video";
|
|
23
72
|
|
|
24
73
|
export type FileArtifactStatus = "missing" | "repaired-from-temp" | "saved" | "upstream-temp-only";
|
|
@@ -41,6 +90,32 @@ export interface FileArtifactMetadata {
|
|
|
41
90
|
tempPath?: string;
|
|
42
91
|
}
|
|
43
92
|
|
|
93
|
+
export type ArtifactVerificationState = "missing" | "pending" | "unverified" | "verified";
|
|
94
|
+
|
|
95
|
+
export interface ArtifactVerificationEntry {
|
|
96
|
+
absolutePath?: string;
|
|
97
|
+
exists?: boolean;
|
|
98
|
+
kind: FileArtifactKind | "spill";
|
|
99
|
+
limitation?: string;
|
|
100
|
+
mediaType?: string;
|
|
101
|
+
path: string;
|
|
102
|
+
requestedPath?: string;
|
|
103
|
+
retentionState?: ArtifactRetentionState;
|
|
104
|
+
sizeBytes?: number;
|
|
105
|
+
state: ArtifactVerificationState;
|
|
106
|
+
status?: FileArtifactStatus;
|
|
107
|
+
storageScope?: ArtifactStorageScope;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ArtifactVerificationSummary {
|
|
111
|
+
artifacts: ArtifactVerificationEntry[];
|
|
112
|
+
missingCount: number;
|
|
113
|
+
pendingCount: number;
|
|
114
|
+
unverifiedCount: number;
|
|
115
|
+
verified: boolean;
|
|
116
|
+
verifiedCount: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
44
119
|
export interface SavedFilePresentationDetails {
|
|
45
120
|
command: "download" | "pdf" | "wait";
|
|
46
121
|
kind: "download" | "pdf";
|
|
@@ -187,18 +262,24 @@ export function mergeSessionArtifactManifest(options: {
|
|
|
187
262
|
}
|
|
188
263
|
|
|
189
264
|
export interface BatchStepPresentationDetails {
|
|
265
|
+
artifactVerification?: ArtifactVerificationSummary;
|
|
190
266
|
artifacts?: FileArtifactMetadata[];
|
|
191
267
|
command?: string[];
|
|
192
268
|
commandText: string;
|
|
193
269
|
data?: unknown;
|
|
270
|
+
failureCategory?: AgentBrowserFailureCategory;
|
|
271
|
+
nextActions?: AgentBrowserNextAction[];
|
|
272
|
+
pageChangeSummary?: AgentBrowserPageChangeSummary;
|
|
194
273
|
fullOutputPath?: string;
|
|
195
274
|
fullOutputPaths?: string[];
|
|
196
275
|
imagePath?: string;
|
|
197
276
|
imagePaths?: string[];
|
|
198
277
|
index: number;
|
|
278
|
+
resultCategory: AgentBrowserResultCategory;
|
|
199
279
|
savedFile?: SavedFilePresentationDetails;
|
|
200
280
|
savedFilePath?: string;
|
|
201
281
|
success: boolean;
|
|
282
|
+
successCategory?: AgentBrowserSuccessCategory;
|
|
202
283
|
summary: string;
|
|
203
284
|
text: string;
|
|
204
285
|
}
|
|
@@ -213,20 +294,376 @@ export interface BatchFailurePresentationDetails {
|
|
|
213
294
|
export interface ToolPresentation {
|
|
214
295
|
artifactManifest?: SessionArtifactManifest;
|
|
215
296
|
artifactRetentionSummary?: string;
|
|
297
|
+
artifactVerification?: ArtifactVerificationSummary;
|
|
216
298
|
artifacts?: FileArtifactMetadata[];
|
|
217
299
|
batchFailure?: BatchFailurePresentationDetails;
|
|
218
300
|
batchSteps?: BatchStepPresentationDetails[];
|
|
219
301
|
content: Array<{ text: string; type: "text" } | { data: string; mimeType: string; type: "image" }>;
|
|
220
302
|
data?: unknown;
|
|
303
|
+
failureCategory?: AgentBrowserFailureCategory;
|
|
221
304
|
fullOutputPath?: string;
|
|
222
305
|
fullOutputPaths?: string[];
|
|
223
306
|
imagePath?: string;
|
|
224
307
|
imagePaths?: string[];
|
|
308
|
+
nextActions?: AgentBrowserNextAction[];
|
|
309
|
+
pageChangeSummary?: AgentBrowserPageChangeSummary;
|
|
310
|
+
resultCategory?: AgentBrowserResultCategory;
|
|
225
311
|
savedFile?: SavedFilePresentationDetails;
|
|
226
312
|
savedFilePath?: string;
|
|
313
|
+
successCategory?: AgentBrowserSuccessCategory;
|
|
227
314
|
summary: string;
|
|
228
315
|
}
|
|
229
316
|
|
|
317
|
+
function isPendingFileArtifact(artifact: FileArtifactMetadata): boolean {
|
|
318
|
+
return artifact.command === "record" && artifact.subcommand === "start" && artifact.kind === "video";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function hasUnverifiedFileArtifact(artifacts: FileArtifactMetadata[] | undefined): boolean {
|
|
322
|
+
return (artifacts ?? []).some((artifact) => !isPendingFileArtifact(artifact) && artifact.exists !== true);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function classifyAgentBrowserSuccessCategory(options: {
|
|
326
|
+
artifacts?: FileArtifactMetadata[];
|
|
327
|
+
inspection?: boolean;
|
|
328
|
+
savedFile?: SavedFilePresentationDetails;
|
|
329
|
+
}): AgentBrowserSuccessCategory {
|
|
330
|
+
if (options.inspection) return "inspection";
|
|
331
|
+
if ((options.artifacts ?? []).length > 0) return hasUnverifiedFileArtifact(options.artifacts) ? "artifact-unverified" : "artifact-saved";
|
|
332
|
+
if (options.savedFile) return "artifact-saved";
|
|
333
|
+
return "completed";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function classifyAgentBrowserFailureCategory(options: {
|
|
337
|
+
args?: string[];
|
|
338
|
+
command?: string;
|
|
339
|
+
confirmationRequired?: boolean;
|
|
340
|
+
errorText?: string;
|
|
341
|
+
parseError?: string;
|
|
342
|
+
spawnError?: string;
|
|
343
|
+
stderr?: string;
|
|
344
|
+
tabDrift?: boolean;
|
|
345
|
+
timedOut?: boolean;
|
|
346
|
+
validationError?: string;
|
|
347
|
+
}): AgentBrowserFailureCategory {
|
|
348
|
+
const text = [options.errorText, options.validationError, options.parseError, options.spawnError, options.stderr].filter(Boolean).join("\n");
|
|
349
|
+
const command = options.command ?? "";
|
|
350
|
+
const usedRef = options.args?.some((arg) => /^@e\d+\b/.test(arg)) ?? false;
|
|
351
|
+
if (options.confirmationRequired || /confirmation required|pending confirmation|requires confirmation/i.test(text)) return "confirmation-required";
|
|
352
|
+
if (options.timedOut || /timeout|timed out|watchdog|IPC read timeout|must stay under its 30s IPC read timeout/i.test(text)) return "timeout";
|
|
353
|
+
if (/ENOENT|not found on PATH|could not find.*agent-browser|agent-browser is required but was not found/i.test(text)) return "missing-binary";
|
|
354
|
+
if (options.parseError || /invalid JSON|missing boolean success|success field must be boolean|returned no JSON output/i.test(text)) return "parse-failure";
|
|
355
|
+
if (/aborted/i.test(text)) return "aborted";
|
|
356
|
+
if (options.tabDrift || /could not re-select the intended tab|about:blank|selected tab looks wrong|tab drift|tab.*wrong/i.test(text)) return "tab-drift";
|
|
357
|
+
if (/\bUnknown ref\b|\bstale ref\b|@ref may be stale|\bref\b.*\b(?:not found|missing|expired)\b/i.test(text)) return "stale-ref";
|
|
358
|
+
if (usedRef && /could not locate element|element not found|no element/i.test(text)) return "stale-ref";
|
|
359
|
+
const mentionsPlaywrightSelectorDialect = /(?:\btext=|:has-text\(|\bgetByRole\b|\bgetByText\b)/i.test(text);
|
|
360
|
+
const reportsSelectorMatchFailure =
|
|
361
|
+
/\b(?:no elements? found|failed to find|could not find|unable to find)\b.*\b(?:selector|locator)\b/i.test(text) ||
|
|
362
|
+
/\b(?:selector|locator)\b.*\b(?:no elements? found|not found|missing|failed to find|could not find|unable to find)\b/i.test(text);
|
|
363
|
+
if (
|
|
364
|
+
/\b(?:unsupported|unknown|invalid)\s+(?:selector|locator)\b/i.test(text) ||
|
|
365
|
+
/\bfailed to parse selector\b/i.test(text) ||
|
|
366
|
+
/\bselector\b.*\b(?:parse|syntax|unsupported|invalid)\b/i.test(text) ||
|
|
367
|
+
(mentionsPlaywrightSelectorDialect && reportsSelectorMatchFailure)
|
|
368
|
+
) {
|
|
369
|
+
return "selector-unsupported";
|
|
370
|
+
}
|
|
371
|
+
if (command === "find" && /could not locate element|element not found|no elements? found|unable to find/i.test(text)) return "selector-not-found";
|
|
372
|
+
if (reportsSelectorMatchFailure) return "selector-not-found";
|
|
373
|
+
if ((command === "download" || text.includes("wait --download") || /\bdownload\b/i.test(text)) && /missing|not verified|not found|failed|timeout|timed out/i.test(text)) {
|
|
374
|
+
return "download-not-verified";
|
|
375
|
+
}
|
|
376
|
+
if (options.validationError) return "validation-error";
|
|
377
|
+
return "upstream-error";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function buildNextToolAction(options: {
|
|
381
|
+
args: string[];
|
|
382
|
+
id: string;
|
|
383
|
+
reason: string;
|
|
384
|
+
safety?: string;
|
|
385
|
+
sessionMode?: "auto" | "fresh";
|
|
386
|
+
stdin?: string;
|
|
387
|
+
}): AgentBrowserNextAction {
|
|
388
|
+
return {
|
|
389
|
+
id: options.id,
|
|
390
|
+
params: {
|
|
391
|
+
args: options.args,
|
|
392
|
+
...(options.sessionMode ? { sessionMode: options.sessionMode } : {}),
|
|
393
|
+
...(options.stdin ? { stdin: options.stdin } : {}),
|
|
394
|
+
},
|
|
395
|
+
reason: options.reason,
|
|
396
|
+
...(options.safety ? { safety: options.safety } : {}),
|
|
397
|
+
tool: "agent_browser",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function buildArtifactAction(path: string): AgentBrowserNextAction {
|
|
402
|
+
return {
|
|
403
|
+
artifactPath: path,
|
|
404
|
+
id: "use-saved-artifact",
|
|
405
|
+
reason: "Use the saved artifact path from the structured result instead of scraping it from text.",
|
|
406
|
+
safety: "Verify artifact metadata such as exists/status before treating the file as durable.",
|
|
407
|
+
tool: "agent_browser",
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function buildArtifactVerificationAction(artifact: FileArtifactMetadata): AgentBrowserNextAction {
|
|
412
|
+
return {
|
|
413
|
+
artifactPath: artifact.path,
|
|
414
|
+
id: "verify-artifact-path",
|
|
415
|
+
reason: "The wrapper has artifact metadata but did not verify this file as present on disk.",
|
|
416
|
+
safety: "Check details.artifactVerification and the filesystem before treating the artifact as durable.",
|
|
417
|
+
tool: "agent_browser",
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const MUTATING_COMMANDS = new Set([
|
|
422
|
+
"back",
|
|
423
|
+
"check",
|
|
424
|
+
"click",
|
|
425
|
+
"dblclick",
|
|
426
|
+
"dialog",
|
|
427
|
+
"fill",
|
|
428
|
+
"forward",
|
|
429
|
+
"hover",
|
|
430
|
+
"press",
|
|
431
|
+
"pushstate",
|
|
432
|
+
"reload",
|
|
433
|
+
"scroll",
|
|
434
|
+
"scrollintoview",
|
|
435
|
+
"select",
|
|
436
|
+
"swipe",
|
|
437
|
+
"tap",
|
|
438
|
+
"type",
|
|
439
|
+
"uncheck",
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
function getDownloadRetryPath(args: string[] | undefined, fallback: string | undefined): string | undefined {
|
|
443
|
+
if (fallback) return fallback;
|
|
444
|
+
if (!args || args.length === 0) return undefined;
|
|
445
|
+
const downloadFlagIndex = args.indexOf("--download");
|
|
446
|
+
if (downloadFlagIndex >= 0) {
|
|
447
|
+
const candidate = args[downloadFlagIndex + 1];
|
|
448
|
+
return candidate && !candidate.startsWith("-") ? candidate : undefined;
|
|
449
|
+
}
|
|
450
|
+
const downloadCommandIndex = args.indexOf("download");
|
|
451
|
+
if (downloadCommandIndex >= 0 && args.length > downloadCommandIndex + 2) {
|
|
452
|
+
return args[args.length - 1];
|
|
453
|
+
}
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function buildAgentBrowserNextActions(options: {
|
|
458
|
+
artifacts?: FileArtifactMetadata[];
|
|
459
|
+
args?: string[];
|
|
460
|
+
command?: string;
|
|
461
|
+
confirmationId?: string;
|
|
462
|
+
failureCategory?: AgentBrowserFailureCategory;
|
|
463
|
+
resultCategory: AgentBrowserResultCategory;
|
|
464
|
+
savedFilePath?: string;
|
|
465
|
+
successCategory?: AgentBrowserSuccessCategory;
|
|
466
|
+
}): AgentBrowserNextAction[] | undefined {
|
|
467
|
+
const actions: AgentBrowserNextAction[] = [];
|
|
468
|
+
if (options.resultCategory === "success") {
|
|
469
|
+
if (options.command === "open") {
|
|
470
|
+
actions.push(buildNextToolAction({
|
|
471
|
+
args: ["snapshot", "-i"],
|
|
472
|
+
id: "inspect-opened-page",
|
|
473
|
+
reason: "Inspect the opened page before choosing interactive refs.",
|
|
474
|
+
}));
|
|
475
|
+
} else if (options.command && MUTATING_COMMANDS.has(options.command)) {
|
|
476
|
+
actions.push(buildNextToolAction({
|
|
477
|
+
args: ["snapshot", "-i"],
|
|
478
|
+
id: "inspect-after-mutation",
|
|
479
|
+
reason: "Refresh interactive refs after a browser mutation, navigation, scroll, or rerender.",
|
|
480
|
+
safety: "Do not reuse prior @refs until a fresh snapshot confirms they still exist.",
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
const artifacts = options.artifacts ?? [];
|
|
484
|
+
const savedFileArtifact = options.savedFilePath ? artifacts.find((artifact) => artifact.path === options.savedFilePath) : undefined;
|
|
485
|
+
if (options.savedFilePath && savedFileArtifact?.exists !== false) {
|
|
486
|
+
actions.push(buildArtifactAction(options.savedFilePath));
|
|
487
|
+
}
|
|
488
|
+
for (const artifact of artifacts) {
|
|
489
|
+
if (isPendingFileArtifact(artifact)) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (artifact.exists === false) {
|
|
493
|
+
if (artifact.kind === "download") {
|
|
494
|
+
actions.push(buildNextToolAction({
|
|
495
|
+
args: ["wait", "--download", artifact.path],
|
|
496
|
+
id: "wait-for-download",
|
|
497
|
+
reason: "Upstream reported a download path, but the wrapper did not verify the file on disk.",
|
|
498
|
+
safety: "Use a bounded wait timeout that stays below the native wrapper IPC budget.",
|
|
499
|
+
}));
|
|
500
|
+
} else {
|
|
501
|
+
actions.push(buildArtifactVerificationAction(artifact));
|
|
502
|
+
}
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (artifact.path !== options.savedFilePath) {
|
|
506
|
+
actions.push(buildArtifactAction(artifact.path));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
switch (options.failureCategory) {
|
|
511
|
+
case "confirmation-required":
|
|
512
|
+
if (options.confirmationId) {
|
|
513
|
+
actions.push(
|
|
514
|
+
buildNextToolAction({
|
|
515
|
+
args: ["confirm", options.confirmationId],
|
|
516
|
+
id: "approve-confirmation",
|
|
517
|
+
reason: "Approve the pending upstream confirmation when the requested action is safe.",
|
|
518
|
+
safety: "Only confirm after reviewing the guarded action shown in the result.",
|
|
519
|
+
}),
|
|
520
|
+
buildNextToolAction({
|
|
521
|
+
args: ["deny", options.confirmationId],
|
|
522
|
+
id: "deny-confirmation",
|
|
523
|
+
reason: "Deny the pending upstream confirmation when the guarded action is unsafe or unintended.",
|
|
524
|
+
}),
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
break;
|
|
528
|
+
case "stale-ref":
|
|
529
|
+
case "selector-not-found":
|
|
530
|
+
case "selector-unsupported":
|
|
531
|
+
actions.push(buildNextToolAction({
|
|
532
|
+
args: ["snapshot", "-i"],
|
|
533
|
+
id: "refresh-interactive-refs",
|
|
534
|
+
reason: "Get current interactive refs before retrying the element action.",
|
|
535
|
+
safety: "Prefer a current @ref or a stable find locator; do not retry stale refs blindly.",
|
|
536
|
+
}));
|
|
537
|
+
break;
|
|
538
|
+
case "download-not-verified":
|
|
539
|
+
{
|
|
540
|
+
const retryPath = getDownloadRetryPath(options.args, options.savedFilePath);
|
|
541
|
+
actions.push(buildNextToolAction({
|
|
542
|
+
args: retryPath ? ["wait", "--download", retryPath] : ["wait", "--download"],
|
|
543
|
+
id: "wait-for-download",
|
|
544
|
+
reason: "Wait for the browser download and let the wrapper verify saved-file metadata.",
|
|
545
|
+
safety: "Use a bounded wait timeout that stays below the native wrapper IPC budget.",
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
case "tab-drift":
|
|
550
|
+
actions.push(
|
|
551
|
+
buildNextToolAction({
|
|
552
|
+
args: ["tab", "list"],
|
|
553
|
+
id: "list-tabs-for-recovery",
|
|
554
|
+
reason: "Inspect available tabs before selecting the intended target.",
|
|
555
|
+
}),
|
|
556
|
+
buildNextToolAction({
|
|
557
|
+
args: ["snapshot", "-i"],
|
|
558
|
+
id: "inspect-current-tab",
|
|
559
|
+
reason: "Inspect the currently selected tab after tab recovery.",
|
|
560
|
+
}),
|
|
561
|
+
);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return actions.length > 0 ? actions : undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function buildAgentBrowserResultCategoryDetails(options: {
|
|
569
|
+
artifacts?: FileArtifactMetadata[];
|
|
570
|
+
args?: string[];
|
|
571
|
+
command?: string;
|
|
572
|
+
confirmationRequired?: boolean;
|
|
573
|
+
errorText?: string;
|
|
574
|
+
failureCategory?: AgentBrowserFailureCategory;
|
|
575
|
+
inspection?: boolean;
|
|
576
|
+
parseError?: string;
|
|
577
|
+
savedFile?: SavedFilePresentationDetails;
|
|
578
|
+
spawnError?: string;
|
|
579
|
+
succeeded: boolean;
|
|
580
|
+
tabDrift?: boolean;
|
|
581
|
+
timedOut?: boolean;
|
|
582
|
+
validationError?: string;
|
|
583
|
+
}): AgentBrowserResultCategoryDetails {
|
|
584
|
+
if (options.succeeded) {
|
|
585
|
+
return {
|
|
586
|
+
resultCategory: "success",
|
|
587
|
+
successCategory: classifyAgentBrowserSuccessCategory(options),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
failureCategory: options.failureCategory ?? classifyAgentBrowserFailureCategory(options),
|
|
592
|
+
resultCategory: "failure",
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export type NetworkFailureImpact = "actionable" | "benign";
|
|
597
|
+
|
|
598
|
+
export interface NetworkFailureClassification {
|
|
599
|
+
impact: NetworkFailureImpact;
|
|
600
|
+
reason: string;
|
|
601
|
+
resourceType?: string;
|
|
602
|
+
status?: number;
|
|
603
|
+
url?: string;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export interface NetworkFailureSummary {
|
|
607
|
+
actionableCount: number;
|
|
608
|
+
benignCount: number;
|
|
609
|
+
failures: NetworkFailureClassification[];
|
|
610
|
+
totalCount: number;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function getStringRecordField(value: Record<string, unknown>, key: string): string | undefined {
|
|
614
|
+
const field = value[key];
|
|
615
|
+
return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function getNetworkRequestUrlPath(url: string | undefined): string | undefined {
|
|
619
|
+
if (!url) return undefined;
|
|
620
|
+
try {
|
|
621
|
+
return new URL(url).pathname;
|
|
622
|
+
} catch {
|
|
623
|
+
const withoutQuery = url.split(/[?#]/, 1)[0];
|
|
624
|
+
return withoutQuery.length > 0 ? withoutQuery : undefined;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function isFailedNetworkRequest(request: Record<string, unknown>): boolean {
|
|
629
|
+
return (typeof request.status === "number" && request.status >= 400) || request.failed === true || typeof request.error === "string";
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function isBenignAssetFailure(request: Record<string, unknown>, url: string | undefined, resourceType: string | undefined): boolean {
|
|
633
|
+
const path = getNetworkRequestUrlPath(url);
|
|
634
|
+
if (!path) return false;
|
|
635
|
+
const normalizedResourceType = resourceType?.toLowerCase();
|
|
636
|
+
return /(?:^|\/)(?:favicon(?:[-.\w]*)?\.(?:ico|png|svg)|apple-touch-icon(?:[-.\w]*)?\.png)$/i.test(path)
|
|
637
|
+
&& (request.status === 404 || request.failed === true || typeof request.error === "string")
|
|
638
|
+
&& (!normalizedResourceType || ["image", "img", "other"].includes(normalizedResourceType) || normalizedResourceType.startsWith("image/"));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export function classifyNetworkRequestFailure(request: Record<string, unknown>): NetworkFailureClassification | undefined {
|
|
642
|
+
if (!isFailedNetworkRequest(request)) return undefined;
|
|
643
|
+
const url = getStringRecordField(request, "url");
|
|
644
|
+
const resourceType = getStringRecordField(request, "resourceType") ?? getStringRecordField(request, "mimeType");
|
|
645
|
+
const status = typeof request.status === "number" ? request.status : undefined;
|
|
646
|
+
if (isBenignAssetFailure(request, url, resourceType)) {
|
|
647
|
+
return { impact: "benign", reason: "low-impact browser icon asset", resourceType, status, url };
|
|
648
|
+
}
|
|
649
|
+
return { impact: "actionable", reason: "document, script, API, or non-benign request failure", resourceType, status, url };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export function summarizeNetworkFailures(requests: unknown[]): NetworkFailureSummary {
|
|
653
|
+
const failures = requests.flatMap((request) => {
|
|
654
|
+
if (!isRecord(request)) return [];
|
|
655
|
+
const classification = classifyNetworkRequestFailure(request);
|
|
656
|
+
return classification ? [classification] : [];
|
|
657
|
+
});
|
|
658
|
+
const benignCount = failures.filter((failure) => failure.impact === "benign").length;
|
|
659
|
+
return {
|
|
660
|
+
actionableCount: failures.length - benignCount,
|
|
661
|
+
benignCount,
|
|
662
|
+
failures,
|
|
663
|
+
totalCount: failures.length,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
230
667
|
export function stringifyUnknown(value: unknown): string {
|
|
231
668
|
if (typeof value === "string") return value;
|
|
232
669
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
@@ -34,6 +34,7 @@ const SNAPSHOT_SECTION_PREVIEW_LINES = 2;
|
|
|
34
34
|
const SNAPSHOT_MAX_ADDITIONAL_SECTIONS = 2;
|
|
35
35
|
const SNAPSHOT_KEY_REF_MAX_LINES = 8;
|
|
36
36
|
const SNAPSHOT_OTHER_REF_MAX_LINES = 4;
|
|
37
|
+
const SNAPSHOT_HIGH_VALUE_REF_MAX_LINES = 10;
|
|
37
38
|
const SNAPSHOT_ROLE_COUNT_MAX_ENTRIES = 4;
|
|
38
39
|
const SNAPSHOT_FALLBACK_PREVIEW_MAX_LINES = 12;
|
|
39
40
|
const SNAPSHOT_NAME_MAX_CHARS = 96;
|
|
@@ -58,6 +59,7 @@ const SNAPSHOT_SIGNAL_ROLES = new Set([
|
|
|
58
59
|
"radio",
|
|
59
60
|
"region",
|
|
60
61
|
"row",
|
|
62
|
+
"searchbox",
|
|
61
63
|
"tab",
|
|
62
64
|
"textbox",
|
|
63
65
|
]);
|
|
@@ -69,14 +71,15 @@ const SNAPSHOT_ROLE_PRIORITY: Record<string, number> = {
|
|
|
69
71
|
menu: 3,
|
|
70
72
|
region: 4,
|
|
71
73
|
heading: 5,
|
|
72
|
-
|
|
74
|
+
searchbox: 6,
|
|
73
75
|
textbox: 7,
|
|
74
76
|
combobox: 8,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
button: 9,
|
|
78
|
+
checkbox: 10,
|
|
79
|
+
radio: 11,
|
|
80
|
+
tab: 12,
|
|
81
|
+
option: 13,
|
|
82
|
+
link: 14,
|
|
80
83
|
listitem: 14,
|
|
81
84
|
row: 15,
|
|
82
85
|
gridcell: 16,
|
|
@@ -103,6 +106,28 @@ const SNAPSHOT_CHROME_SECTION_PATTERNS = [
|
|
|
103
106
|
/\brecommended\b/i,
|
|
104
107
|
/\bsuggested\b/i,
|
|
105
108
|
];
|
|
109
|
+
const SNAPSHOT_HIGH_VALUE_CONTROL_ROLES = new Set([
|
|
110
|
+
"button",
|
|
111
|
+
"checkbox",
|
|
112
|
+
"combobox",
|
|
113
|
+
"menuitem",
|
|
114
|
+
"option",
|
|
115
|
+
"radio",
|
|
116
|
+
"searchbox",
|
|
117
|
+
"tab",
|
|
118
|
+
"textbox",
|
|
119
|
+
]);
|
|
120
|
+
const SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY: Record<string, number> = {
|
|
121
|
+
searchbox: 0,
|
|
122
|
+
textbox: 1,
|
|
123
|
+
combobox: 2,
|
|
124
|
+
button: 3,
|
|
125
|
+
tab: 4,
|
|
126
|
+
checkbox: 5,
|
|
127
|
+
radio: 6,
|
|
128
|
+
option: 7,
|
|
129
|
+
menuitem: 8,
|
|
130
|
+
};
|
|
106
131
|
|
|
107
132
|
interface SnapshotRefEntry {
|
|
108
133
|
id: string;
|
|
@@ -458,6 +483,25 @@ function formatCompactRef(entry: SnapshotRefEntry): string {
|
|
|
458
483
|
return `- ${entry.id} ${entry.role}${suffix}`;
|
|
459
484
|
}
|
|
460
485
|
|
|
486
|
+
function isHighValueControlRef(entry: SnapshotRefEntry): boolean {
|
|
487
|
+
if (!SNAPSHOT_HIGH_VALUE_CONTROL_ROLES.has(entry.role)) return false;
|
|
488
|
+
if (isNoiseName(entry.name) || isChromeSectionName(entry.name)) return false;
|
|
489
|
+
return entry.name.length > 0 || entry.role === "searchbox" || entry.role === "textbox" || entry.role === "combobox";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function rankHighValueControlRefs(left: SnapshotRefEntry, right: SnapshotRefEntry): number {
|
|
493
|
+
const rolePriority =
|
|
494
|
+
(SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY[left.role] ?? 50) -
|
|
495
|
+
(SNAPSHOT_HIGH_VALUE_CONTROL_ROLE_PRIORITY[right.role] ?? 50);
|
|
496
|
+
if (rolePriority !== 0) return rolePriority;
|
|
497
|
+
|
|
498
|
+
const leftHasName = left.name.length > 0 ? 0 : 1;
|
|
499
|
+
const rightHasName = right.name.length > 0 ? 0 : 1;
|
|
500
|
+
if (leftHasName !== rightHasName) return leftHasName - rightHasName;
|
|
501
|
+
|
|
502
|
+
return compareRefIds(left.id, right.id);
|
|
503
|
+
}
|
|
504
|
+
|
|
461
505
|
function shouldCompactSnapshot(rawText: string, data: Record<string, unknown>): boolean {
|
|
462
506
|
const snapshot = getSnapshotText(data) ?? "";
|
|
463
507
|
const refEntries = getSnapshotRefEntries(data);
|
|
@@ -611,7 +655,16 @@ export async function buildSnapshotPresentation(
|
|
|
611
655
|
const otherRefEntries = visibleRankedRefEntries
|
|
612
656
|
.filter((entry) => !keyRefIdSet.has(entry.id))
|
|
613
657
|
.slice(0, SNAPSHOT_OTHER_REF_MAX_LINES);
|
|
614
|
-
const
|
|
658
|
+
const displayedRefIdSet = new Set([...keyRefEntries, ...otherRefEntries].map((entry) => entry.id));
|
|
659
|
+
const omittedHighValueControlEntries = visibleRankedRefEntries
|
|
660
|
+
.filter((entry) => !displayedRefIdSet.has(entry.id) && isHighValueControlRef(entry))
|
|
661
|
+
.sort(rankHighValueControlRefs);
|
|
662
|
+
const visibleHighValueControlEntries = omittedHighValueControlEntries.slice(0, SNAPSHOT_HIGH_VALUE_REF_MAX_LINES);
|
|
663
|
+
const omittedHighValueControls = Math.max(0, omittedHighValueControlEntries.length - visibleHighValueControlEntries.length);
|
|
664
|
+
const omittedNonHighlightedRefs = Math.max(
|
|
665
|
+
0,
|
|
666
|
+
visibleRankedRefEntries.length - keyRefEntries.length - otherRefEntries.length - omittedHighValueControlEntries.length,
|
|
667
|
+
);
|
|
615
668
|
const origin = getSnapshotOrigin(data);
|
|
616
669
|
|
|
617
670
|
const lines: string[] = [
|
|
@@ -658,8 +711,14 @@ export async function buildSnapshotPresentation(
|
|
|
658
711
|
if (otherRefEntries.length > 0) {
|
|
659
712
|
lines.push("", "Other refs:", ...otherRefEntries.map(formatCompactRef));
|
|
660
713
|
}
|
|
661
|
-
if (
|
|
662
|
-
lines.push(`- ... (${
|
|
714
|
+
if (omittedNonHighlightedRefs > 0) {
|
|
715
|
+
lines.push(`- ... (${omittedNonHighlightedRefs} additional refs omitted)`);
|
|
716
|
+
}
|
|
717
|
+
if (visibleHighValueControlEntries.length > 0) {
|
|
718
|
+
lines.push("", "Omitted high-value controls:", ...visibleHighValueControlEntries.map(formatCompactRef));
|
|
719
|
+
if (omittedHighValueControls > 0) {
|
|
720
|
+
lines.push(`- ... (${omittedHighValueControls} additional high-value controls omitted)`);
|
|
721
|
+
}
|
|
663
722
|
}
|
|
664
723
|
|
|
665
724
|
lines.push(
|
|
@@ -689,6 +748,7 @@ export async function buildSnapshotPresentation(
|
|
|
689
748
|
previewMode: fallbackPreview ? "outline" : "structured",
|
|
690
749
|
spillError: spillErrorText,
|
|
691
750
|
previewRefIds: [...previewRefIds],
|
|
751
|
+
highValueControlRefIds: visibleHighValueControlEntries.map((entry) => entry.id),
|
|
692
752
|
additionalSectionsOmitted: omittedAdditionalSectionCount,
|
|
693
753
|
previewSections: [
|
|
694
754
|
...(primarySegment
|
|
@@ -8,9 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|
export { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "./results/envelope.js";
|
|
10
10
|
export { buildToolPresentation } from "./results/presentation.js";
|
|
11
|
+
export {
|
|
12
|
+
buildAgentBrowserNextActions,
|
|
13
|
+
buildAgentBrowserResultCategoryDetails,
|
|
14
|
+
classifyAgentBrowserFailureCategory,
|
|
15
|
+
classifyAgentBrowserSuccessCategory,
|
|
16
|
+
} from "./results/shared.js";
|
|
11
17
|
export type {
|
|
12
18
|
AgentBrowserBatchResult,
|
|
13
19
|
AgentBrowserEnvelope,
|
|
20
|
+
AgentBrowserFailureCategory,
|
|
21
|
+
AgentBrowserResultCategory,
|
|
22
|
+
AgentBrowserNextAction,
|
|
23
|
+
AgentBrowserPageChangeSummary,
|
|
24
|
+
AgentBrowserResultCategoryDetails,
|
|
25
|
+
AgentBrowserSuccessCategory,
|
|
14
26
|
FileArtifactKind,
|
|
15
27
|
FileArtifactMetadata,
|
|
16
28
|
ToolPresentation,
|