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.
@@ -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
- button: 6,
74
+ searchbox: 6,
73
75
  textbox: 7,
74
76
  combobox: 8,
75
- checkbox: 9,
76
- radio: 10,
77
- tab: 11,
78
- option: 12,
79
- link: 13,
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 omittedOtherRefs = Math.max(0, visibleRankedRefEntries.length - keyRefEntries.length - otherRefEntries.length);
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 (omittedOtherRefs > 0) {
662
- lines.push(`- ... (${omittedOtherRefs} additional refs omitted)`);
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,