pi-agent-browser-native 0.2.32 → 0.2.33

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +27 -16
  3. package/docs/ARCHITECTURE.md +3 -2
  4. package/docs/COMMAND_REFERENCE.md +18 -10
  5. package/docs/ELECTRON.md +23 -4
  6. package/docs/RELEASE.md +4 -2
  7. package/docs/REQUIREMENTS.md +1 -1
  8. package/docs/SUPPORT_MATRIX.md +28 -16
  9. package/docs/TOOL_CONTRACT.md +29 -24
  10. package/extensions/agent-browser/index.ts +404 -4371
  11. package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
  12. package/extensions/agent-browser/lib/input-modes/job.ts +203 -0
  13. package/extensions/agent-browser/lib/input-modes/lookups.ts +447 -0
  14. package/extensions/agent-browser/lib/input-modes/params.ts +188 -0
  15. package/extensions/agent-browser/lib/input-modes/semantic-action.ts +107 -0
  16. package/extensions/agent-browser/lib/input-modes/shared.ts +46 -0
  17. package/extensions/agent-browser/lib/input-modes/types.ts +221 -0
  18. package/extensions/agent-browser/lib/input-modes.ts +41 -0
  19. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +696 -0
  20. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +450 -0
  21. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +46 -0
  22. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +711 -0
  23. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +386 -0
  24. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
  25. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +476 -0
  26. package/extensions/agent-browser/lib/orchestration/browser-run.ts +1 -0
  27. package/extensions/agent-browser/lib/orchestration/input-plan.ts +338 -0
  28. package/extensions/agent-browser/lib/playbook.ts +12 -11
  29. package/extensions/agent-browser/lib/process.ts +106 -4
  30. package/extensions/agent-browser/lib/results/action-recommendations.ts +269 -0
  31. package/extensions/agent-browser/lib/results/artifact-manifest.ts +114 -0
  32. package/extensions/agent-browser/lib/results/artifact-state.ts +13 -0
  33. package/extensions/agent-browser/lib/results/categories.ts +106 -0
  34. package/extensions/agent-browser/lib/results/contracts.ts +220 -0
  35. package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +72 -0
  36. package/extensions/agent-browser/lib/results/envelope.ts +2 -1
  37. package/extensions/agent-browser/lib/results/network.ts +64 -0
  38. package/extensions/agent-browser/lib/results/next-actions.ts +117 -0
  39. package/extensions/agent-browser/lib/results/presentation/artifacts.ts +506 -0
  40. package/extensions/agent-browser/lib/results/presentation/batch.ts +355 -0
  41. package/extensions/agent-browser/lib/results/presentation/common.ts +53 -0
  42. package/extensions/agent-browser/lib/results/presentation/content.ts +36 -0
  43. package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +730 -0
  44. package/extensions/agent-browser/lib/results/presentation/errors.ts +125 -0
  45. package/extensions/agent-browser/lib/results/presentation/large-output.ts +182 -0
  46. package/extensions/agent-browser/lib/results/presentation/navigation.ts +216 -0
  47. package/extensions/agent-browser/lib/results/presentation/registry.ts +154 -0
  48. package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
  49. package/extensions/agent-browser/lib/results/presentation.ts +87 -2399
  50. package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
  51. package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
  52. package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
  53. package/extensions/agent-browser/lib/results/shared.ts +17 -789
  54. package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
  55. package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
  56. package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
  57. package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
  58. package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
  59. package/extensions/agent-browser/lib/results/text.ts +40 -0
  60. package/extensions/agent-browser/lib/results.ts +16 -5
  61. package/extensions/agent-browser/lib/session-page-state.ts +486 -0
  62. package/package.json +2 -1
@@ -7,158 +7,45 @@
7
7
  */
8
8
 
9
9
  import { isRecord } from "../parsing.js";
10
+ import type { PersistentSessionArtifactStore } from "../temp.js";
11
+ import type {
12
+ SessionArtifactManifest,
13
+ ToolPresentation,
14
+ } from "./contracts.js";
15
+ import { isHighValueControlEntry, selectHighValueControlEntries } from "./snapshot-high-value-controls.js";
10
16
  import {
11
- type PersistentSessionArtifactEviction,
12
- type PersistentSessionArtifactStore,
13
- writePersistentSessionArtifactFile,
14
- writeSecureTempFile,
15
- } from "../temp.js";
17
+ buildFallbackSnapshotOutline,
18
+ buildRefLineOrderMap,
19
+ buildSegmentPreview,
20
+ buildSnapshotSegments,
21
+ canUseStructuredSnapshotPreview,
22
+ chooseAdditionalSegments,
23
+ choosePrimarySegment,
24
+ getMeaningfulSegmentLines,
25
+ getSnapshotRolePriority,
26
+ isChromeSectionName,
27
+ isNoiseName,
28
+ parseSnapshotLines,
29
+ } from "./snapshot-segments.js";
30
+ import { applySnapshotArtifactManifest, writeSnapshotSpillFile, type SnapshotSpillWriteResult } from "./snapshot-spill.js";
16
31
  import {
17
- type SessionArtifactManifest,
18
- type SessionArtifactManifestEntry,
19
- type ToolPresentation,
20
- buildEvictedSessionArtifactEntries,
21
- compareRefIds,
22
- countLines,
23
- formatSessionArtifactRetentionSummary,
24
- mergeSessionArtifactManifest,
25
- normalizeWhitespace,
26
- truncateText,
27
- } from "./shared.js";
32
+ enrichSnapshotRefEntries,
33
+ getSnapshotRefEntries,
34
+ type SnapshotLineRefInfo,
35
+ type SnapshotRefEntry,
36
+ } from "./snapshot-refs.js";
37
+ import { compareRefIds, countLines, truncateText } from "./text.js";
28
38
 
29
39
  const SNAPSHOT_INLINE_MAX_CHARS = 6_000;
30
40
  const SNAPSHOT_INLINE_MAX_LINES = 80;
31
41
  const SNAPSHOT_INLINE_MAX_REFS = 60;
32
42
  const SNAPSHOT_PRIMARY_PREVIEW_LINES = 8;
33
43
  const SNAPSHOT_SECTION_PREVIEW_LINES = 2;
34
- const SNAPSHOT_MAX_ADDITIONAL_SECTIONS = 2;
35
44
  const SNAPSHOT_KEY_REF_MAX_LINES = 8;
36
45
  const SNAPSHOT_OTHER_REF_MAX_LINES = 4;
37
46
  const SNAPSHOT_HIGH_VALUE_REF_MAX_LINES = 10;
38
47
  const SNAPSHOT_ROLE_COUNT_MAX_ENTRIES = 4;
39
- const SNAPSHOT_FALLBACK_PREVIEW_MAX_LINES = 12;
40
48
  const SNAPSHOT_NAME_MAX_CHARS = 96;
41
- const SNAPSHOT_LINE_MAX_CHARS = 140;
42
- const SNAPSHOT_SPILL_FILE_PREFIX = "pi-agent-browser-snapshot";
43
- const SNAPSHOT_SIGNAL_ROLES = new Set([
44
- "article",
45
- "banner",
46
- "button",
47
- "checkbox",
48
- "combobox",
49
- "dialog",
50
- "gridcell",
51
- "heading",
52
- "link",
53
- "listitem",
54
- "main",
55
- "menu",
56
- "menuitem",
57
- "navigation",
58
- "option",
59
- "radio",
60
- "region",
61
- "row",
62
- "searchbox",
63
- "tab",
64
- "textbox",
65
- ]);
66
- const SNAPSHOT_SEGMENT_ROOT_ROLES = new Set(["article", "dialog", "heading", "main", "menu", "region"]);
67
- const SNAPSHOT_ROLE_PRIORITY: Record<string, number> = {
68
- article: 0,
69
- main: 1,
70
- dialog: 2,
71
- menu: 3,
72
- region: 4,
73
- heading: 5,
74
- searchbox: 6,
75
- textbox: 7,
76
- combobox: 8,
77
- button: 9,
78
- checkbox: 10,
79
- radio: 11,
80
- tab: 12,
81
- option: 13,
82
- link: 14,
83
- listitem: 14,
84
- row: 15,
85
- gridcell: 16,
86
- navigation: 17,
87
- generic: 99,
88
- unknown: 100,
89
- };
90
- const SNAPSHOT_NOISE_NAME_PATTERNS = [
91
- /^skip to /i,
92
- /^ad$/i,
93
- /^don't want to see ads\??$/i,
94
- /keyboard shortcuts/i,
95
- /\bpromoted\b/i,
96
- /\bsponsored\b/i,
97
- ];
98
- const SNAPSHOT_CHROME_SECTION_PATTERNS = [
99
- /^primary$/i,
100
- /^footer$/i,
101
- /^navigation$/i,
102
- /\bwhat['’]?s happening\b/i,
103
- /\brelevant people\b/i,
104
- /\btrending\b/i,
105
- /\brelated\b/i,
106
- /\brecommended\b/i,
107
- /\bsuggested\b/i,
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
- };
131
-
132
- interface SnapshotRefEntry {
133
- id: string;
134
- name: string;
135
- role: string;
136
- }
137
-
138
- interface SnapshotLine {
139
- depth: number;
140
- headingLevel?: number;
141
- index: number;
142
- name: string;
143
- raw: string;
144
- ref?: string;
145
- role: string;
146
- }
147
-
148
- interface SnapshotSegment {
149
- endIndexExclusive: number;
150
- lines: SnapshotLine[];
151
- root: SnapshotLine;
152
- score: number;
153
- startIndex: number;
154
- }
155
-
156
- interface SnapshotPreview {
157
- omittedCount: number;
158
- refIds: string[];
159
- lines: string[];
160
- }
161
-
162
49
  function getSnapshotText(data: Record<string, unknown>): string | undefined {
163
50
  return typeof data.snapshot === "string" ? data.snapshot : undefined;
164
51
  }
@@ -167,32 +54,6 @@ function getSnapshotOrigin(data: Record<string, unknown>): string {
167
54
  return typeof data.origin === "string" ? data.origin : "(unknown origin)";
168
55
  }
169
56
 
170
- function formatPreviewLine(line: SnapshotLine, baseDepth: number): string {
171
- const leadingWhitespace = (line.raw.match(/^\s*/) ?? [""])[0].length;
172
- const stripChars = Math.min(leadingWhitespace, Math.max(0, baseDepth) * 2);
173
- return truncateText(line.raw.slice(stripChars), SNAPSHOT_LINE_MAX_CHARS);
174
- }
175
-
176
- function getRolePriority(role: string): number {
177
- return SNAPSHOT_ROLE_PRIORITY[role] ?? 50;
178
- }
179
-
180
- function getSnapshotRefEntries(data: Record<string, unknown>): SnapshotRefEntry[] {
181
- const refs = isRecord(data.refs) ? data.refs : undefined;
182
- if (!refs) return [];
183
-
184
- return Object.entries(refs)
185
- .map(([id, value]) => {
186
- if (!isRecord(value)) {
187
- return { id, name: "", role: "unknown" } satisfies SnapshotRefEntry;
188
- }
189
- const name = typeof value.name === "string" ? normalizeWhitespace(value.name) : "";
190
- const role = typeof value.role === "string" && value.role.length > 0 ? value.role : "unknown";
191
- return { id, name, role } satisfies SnapshotRefEntry;
192
- })
193
- .sort((a, b) => compareRefIds(a.id, b.id));
194
- }
195
-
196
57
  function getSnapshotRoleCounts(refEntries: SnapshotRefEntry[]): Record<string, number> {
197
58
  const counts: Record<string, number> = {};
198
59
  for (const entry of refEntries) {
@@ -207,7 +68,7 @@ function formatRoleCounts(roleCounts: Record<string, number>): string | undefine
207
68
 
208
69
  const ordered = entries.sort((left, right) => {
209
70
  if (right[1] !== left[1]) return right[1] - left[1];
210
- return getRolePriority(left[0]) - getRolePriority(right[0]);
71
+ return getSnapshotRolePriority(left[0]) - getSnapshotRolePriority(right[0]);
211
72
  });
212
73
  const visibleEntries = ordered.slice(0, SNAPSHOT_ROLE_COUNT_MAX_ENTRIES).map(([role, count]) => `${role} ${count}`);
213
74
  const omittedEntries = Math.max(0, ordered.length - visibleEntries.length);
@@ -217,241 +78,6 @@ function formatRoleCounts(roleCounts: Record<string, number>): string | undefine
217
78
  return visibleEntries.join(", ");
218
79
  }
219
80
 
220
- function parseSnapshotLines(snapshot: string): SnapshotLine[] {
221
- return snapshot
222
- .split("\n")
223
- .filter((line) => line.length > 0)
224
- .map((raw, index) => {
225
- const trimmed = raw.trimStart();
226
- const depth = Math.floor(((raw.match(/^\s*/) ?? [""])[0].length ?? 0) / 2);
227
- const role = trimmed.match(/^[-*]\s+([^\s"]+)/)?.[1] ?? "unknown";
228
- const name = normalizeWhitespace(trimmed.match(/"([^"]*)"/)?.[1] ?? "");
229
- const ref = trimmed.match(/\bref=([^,\]\s]+)/)?.[1];
230
- const headingLevel = trimmed.match(/\blevel=(\d+)/)?.[1];
231
- return {
232
- depth,
233
- headingLevel: headingLevel ? Number(headingLevel) : undefined,
234
- index,
235
- name,
236
- raw,
237
- ref,
238
- role,
239
- } satisfies SnapshotLine;
240
- });
241
- }
242
-
243
- function isNoiseName(name: string): boolean {
244
- return SNAPSHOT_NOISE_NAME_PATTERNS.some((pattern) => pattern.test(name));
245
- }
246
-
247
- function isChromeSectionName(name: string): boolean {
248
- return SNAPSHOT_CHROME_SECTION_PATTERNS.some((pattern) => pattern.test(name));
249
- }
250
-
251
- function isNoiseSnapshotLine(line: SnapshotLine): boolean {
252
- if (line.name.length > 0 && isNoiseName(line.name)) return true;
253
- const loweredRaw = line.raw.toLowerCase();
254
- return loweredRaw.includes("promoted") || loweredRaw.includes("sponsored");
255
- }
256
-
257
- function isPotentialSegmentRootLine(line: SnapshotLine): boolean {
258
- if (!SNAPSHOT_SEGMENT_ROOT_ROLES.has(line.role)) return false;
259
- if (isNoiseSnapshotLine(line)) return false;
260
- if (line.role === "heading") {
261
- return line.name.length > 0 && (line.headingLevel ?? 99) <= 3;
262
- }
263
- if (line.role === "region") {
264
- return line.name.length > 0;
265
- }
266
- return true;
267
- }
268
-
269
- function scoreSegment(segment: SnapshotSegment): number {
270
- const { root } = segment;
271
- const distinctRefs = new Set(segment.lines.flatMap((line) => (line.ref ? [line.ref] : []))).size;
272
- let score = 0;
273
-
274
- score += 120 - getRolePriority(root.role) * 8;
275
- score += Math.min(distinctRefs, 16);
276
- score += Math.min(segment.lines.length, 12);
277
- score -= Math.min(root.index, 60) / 3;
278
- score -= root.depth * 6;
279
-
280
- if (root.role === "heading") {
281
- if (root.headingLevel === 1) score += 40;
282
- else if (root.headingLevel === 2) score += 22;
283
- else if (root.headingLevel === 3) score += 12;
284
- }
285
- if (root.name.length > 0) score += 10;
286
- if (root.name.length <= 2) score -= 18;
287
- if (isChromeSectionName(root.name)) score -= 45;
288
- if (isNoiseName(root.name)) score -= 1000;
289
- return score;
290
- }
291
-
292
- function buildSnapshotSegments(snapshotLines: SnapshotLine[]): SnapshotSegment[] {
293
- const roots: SnapshotLine[] = [];
294
- const stack: SnapshotLine[] = [];
295
-
296
- for (const line of snapshotLines) {
297
- stack.length = line.depth;
298
- if (isPotentialSegmentRootLine(line)) {
299
- const normalizedName = normalizeWhitespace(line.name.toLowerCase());
300
- let duplicateAncestor: SnapshotLine | undefined;
301
- for (let index = stack.length - 1; index >= 0; index -= 1) {
302
- const ancestor = stack[index];
303
- if (
304
- normalizedName.length > 0 &&
305
- normalizeWhitespace(ancestor.name.toLowerCase()) === normalizedName &&
306
- SNAPSHOT_SEGMENT_ROOT_ROLES.has(ancestor.role)
307
- ) {
308
- duplicateAncestor = ancestor;
309
- break;
310
- }
311
- }
312
- if (!duplicateAncestor) {
313
- roots.push(line);
314
- }
315
- }
316
- stack[line.depth] = line;
317
- }
318
-
319
- return roots.map((root, index) => {
320
- let endIndexExclusive = snapshotLines.length;
321
- for (let nextIndex = index + 1; nextIndex < roots.length; nextIndex += 1) {
322
- const candidate = roots[nextIndex];
323
- if (candidate.depth <= root.depth) {
324
- endIndexExclusive = candidate.index;
325
- break;
326
- }
327
- }
328
- const lines = snapshotLines.slice(root.index, endIndexExclusive);
329
- const segment: SnapshotSegment = {
330
- endIndexExclusive,
331
- lines,
332
- root,
333
- score: 0,
334
- startIndex: root.index,
335
- };
336
- segment.score = scoreSegment(segment);
337
- return segment;
338
- });
339
- }
340
-
341
- function choosePrimarySegment(segments: SnapshotSegment[]): SnapshotSegment | undefined {
342
- if (segments.length === 0) return undefined;
343
- return (
344
- segments.find((segment) => segment.root.role === "main" || segment.root.role === "article") ??
345
- segments.find((segment) => segment.root.role === "heading" && segment.root.headingLevel === 1) ??
346
- segments.find((segment) => segment.score >= 90) ??
347
- [...segments].sort((left, right) => right.score - left.score || left.startIndex - right.startIndex)[0]
348
- );
349
- }
350
-
351
- function chooseAdditionalSegments(segments: SnapshotSegment[], primary: SnapshotSegment | undefined): SnapshotSegment[] {
352
- if (!primary) return [];
353
-
354
- const seenNames = new Set<string>([normalizeWhitespace(primary.root.name.toLowerCase())]);
355
- const rankedCandidates = segments
356
- .filter((segment) => segment !== primary && segment.score >= 45)
357
- .sort((left, right) => {
358
- const leftDistance = Math.abs(left.startIndex - primary.startIndex);
359
- const rightDistance = Math.abs(right.startIndex - primary.startIndex);
360
- if (leftDistance !== rightDistance) return leftDistance - rightDistance;
361
- if (right.score !== left.score) return right.score - left.score;
362
- return left.startIndex - right.startIndex;
363
- });
364
-
365
- const chosen: SnapshotSegment[] = [];
366
- for (const segment of rankedCandidates) {
367
- if (chosen.length >= SNAPSHOT_MAX_ADDITIONAL_SECTIONS) break;
368
- if (isChromeSectionName(segment.root.name)) continue;
369
- if (segment.root.role === "heading" && segment.root.name.length <= 2) continue;
370
- const nameKey = normalizeWhitespace(segment.root.name.toLowerCase());
371
- if (nameKey && seenNames.has(nameKey)) continue;
372
- chosen.push(segment);
373
- if (nameKey) seenNames.add(nameKey);
374
- }
375
-
376
- return chosen.sort((left, right) => left.startIndex - right.startIndex);
377
- }
378
-
379
- function getMeaningfulSegmentLines(segment: SnapshotSegment): SnapshotLine[] {
380
- return segment.lines.filter((line) => {
381
- if (isNoiseSnapshotLine(line)) return false;
382
- if (line.role === "generic" && !line.ref && line.name.length === 0) return false;
383
- if (line.role === "link" && line.name.length === 0) return false;
384
- return true;
385
- });
386
- }
387
-
388
- function buildSegmentPreview(segment: SnapshotSegment, maxLines: number): SnapshotPreview {
389
- const meaningfulLines = getMeaningfulSegmentLines(segment);
390
- if (meaningfulLines.length === 0) {
391
- return { omittedCount: 0, refIds: [], lines: [] };
392
- }
393
-
394
- const previewLines: SnapshotLine[] = [];
395
- const previewRefIds = new Set<string>();
396
- const seenPreviewKeys = new Set<string>();
397
- const rootDepth = segment.root.depth;
398
-
399
- for (const line of meaningfulLines) {
400
- if (previewLines.length >= maxLines) break;
401
- if (line !== segment.root) {
402
- const relativeDepth = line.depth - rootDepth;
403
- if (segment.root.role !== "heading" && relativeDepth > 2) continue;
404
- if (segment.root.name.length > 0 && line.name === segment.root.name && (line.role === "heading" || line.role === "link")) {
405
- continue;
406
- }
407
- }
408
-
409
- const key = `${line.role}:${line.name}:${line.ref ?? ""}:${line.depth}`;
410
- if (seenPreviewKeys.has(key)) continue;
411
- seenPreviewKeys.add(key);
412
- previewLines.push(line);
413
- if (line.ref) previewRefIds.add(line.ref);
414
- }
415
-
416
- return {
417
- omittedCount: Math.max(0, meaningfulLines.length - previewLines.length),
418
- refIds: [...previewRefIds],
419
- lines: previewLines.map((line) => formatPreviewLine(line, rootDepth)),
420
- };
421
- }
422
-
423
- function buildFallbackSnapshotOutline(snapshotLines: SnapshotLine[]): SnapshotPreview {
424
- const selected = new Set<number>();
425
- for (let index = 0; index < snapshotLines.length && selected.size < 4; index += 1) {
426
- if (!isNoiseSnapshotLine(snapshotLines[index])) selected.add(index);
427
- }
428
- for (let index = 0; index < snapshotLines.length && selected.size < SNAPSHOT_FALLBACK_PREVIEW_MAX_LINES; index += 1) {
429
- const line = snapshotLines[index];
430
- if (isNoiseSnapshotLine(line)) continue;
431
- if (SNAPSHOT_SIGNAL_ROLES.has(line.role) || line.ref || line.name.length > 0) {
432
- selected.add(index);
433
- }
434
- }
435
- const chosenLines = [...selected]
436
- .sort((left, right) => left - right)
437
- .slice(0, SNAPSHOT_FALLBACK_PREVIEW_MAX_LINES)
438
- .map((index) => snapshotLines[index]);
439
- return {
440
- omittedCount: Math.max(0, snapshotLines.length - chosenLines.length),
441
- refIds: chosenLines.flatMap((line) => (line.ref ? [line.ref] : [])),
442
- lines: chosenLines.map((line) => truncateText(line.raw, SNAPSHOT_LINE_MAX_CHARS)),
443
- };
444
- }
445
-
446
- function buildRefLineOrderMap(snapshotLines: SnapshotLine[]): Map<string, number> {
447
- const map = new Map<string, number>();
448
- for (const line of snapshotLines) {
449
- if (!line.ref || map.has(line.ref)) continue;
450
- map.set(line.ref, line.index);
451
- }
452
- return map;
453
- }
454
-
455
81
  function rankRefEntries(
456
82
  refEntries: SnapshotRefEntry[],
457
83
  previewRefIds: Set<string>,
@@ -463,7 +89,7 @@ function rankRefEntries(
463
89
  const rightBucket = previewRefIds.has(right.id) ? 0 : focusRefIds.has(right.id) ? 1 : 2;
464
90
  if (leftBucket !== rightBucket) return leftBucket - rightBucket;
465
91
 
466
- const rolePriority = getRolePriority(left.role) - getRolePriority(right.role);
92
+ const rolePriority = getSnapshotRolePriority(left.role) - getSnapshotRolePriority(right.role);
467
93
  if (rolePriority !== 0) return rolePriority;
468
94
 
469
95
  const leftHasName = left.name.length > 0 ? 0 : 1;
@@ -483,25 +109,6 @@ function formatCompactRef(entry: SnapshotRefEntry): string {
483
109
  return `- ${entry.id} ${entry.role}${suffix}`;
484
110
  }
485
111
 
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
-
505
112
  function shouldCompactSnapshot(rawText: string, data: Record<string, unknown>): boolean {
506
113
  const snapshot = getSnapshotText(data) ?? "";
507
114
  const refEntries = getSnapshotRefEntries(data);
@@ -512,63 +119,6 @@ function shouldCompactSnapshot(rawText: string, data: Record<string, unknown>):
512
119
  );
513
120
  }
514
121
 
515
- function canUseStructuredSnapshotPreview(snapshotLines: SnapshotLine[], refEntries: SnapshotRefEntry[]): boolean {
516
- if (snapshotLines.length === 0) return false;
517
- const linesWithRecognizedRoles = snapshotLines.filter((line) => line.role !== "unknown").length;
518
- const linesWithNames = snapshotLines.filter((line) => line.name.length > 0).length;
519
- const parsedRefIds = new Set(snapshotLines.flatMap((line) => (line.ref ? [line.ref] : [])));
520
- return (
521
- linesWithRecognizedRoles >= Math.min(snapshotLines.length, 3) ||
522
- linesWithNames >= Math.min(snapshotLines.length, 3) ||
523
- parsedRefIds.size >= Math.min(refEntries.length, 3)
524
- );
525
- }
526
-
527
- interface SnapshotSpillWriteResult {
528
- evictedArtifacts: PersistentSessionArtifactEviction[];
529
- path: string;
530
- storageScope: "persistent-session" | "process-temp";
531
- }
532
-
533
- async function writeSnapshotSpillFile(
534
- data: Record<string, unknown>,
535
- persistentArtifactStore: PersistentSessionArtifactStore | undefined,
536
- ): Promise<SnapshotSpillWriteResult> {
537
- const options = {
538
- content: JSON.stringify(data, null, 2),
539
- prefix: SNAPSHOT_SPILL_FILE_PREFIX,
540
- suffix: ".json",
541
- };
542
- if (persistentArtifactStore) {
543
- const result = await writePersistentSessionArtifactFile({ ...options, store: persistentArtifactStore });
544
- return { ...result, storageScope: "persistent-session" };
545
- }
546
- return { evictedArtifacts: [], path: await writeSecureTempFile(options), storageScope: "process-temp" };
547
- }
548
-
549
- function applySnapshotArtifactManifest(options: {
550
- baseManifest?: SessionArtifactManifest;
551
- command?: string;
552
- fullOutputPath?: string;
553
- spill?: SnapshotSpillWriteResult;
554
- }): { artifactManifest?: SessionArtifactManifest; artifactRetentionSummary?: string } {
555
- if (!options.fullOutputPath || !options.spill) return {};
556
- const nowMs = Date.now();
557
- const entries: SessionArtifactManifestEntry[] = [
558
- {
559
- command: options.command,
560
- createdAtMs: nowMs,
561
- kind: "spill",
562
- path: options.fullOutputPath,
563
- retentionState: options.spill.storageScope === "persistent-session" ? "live" : "ephemeral",
564
- storageScope: options.spill.storageScope,
565
- },
566
- ...buildEvictedSessionArtifactEntries(options.spill.evictedArtifacts, nowMs),
567
- ];
568
- const artifactManifest = mergeSessionArtifactManifest({ base: options.baseManifest, entries, nowMs });
569
- return artifactManifest ? { artifactManifest, artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest) } : {};
570
- }
571
-
572
122
  export function formatSnapshotSummary(data: Record<string, unknown>): string {
573
123
  const origin = typeof data.origin === "string" ? data.origin : "page";
574
124
  const refs = isRecord(data.refs) ? Object.keys(data.refs).length : 0;
@@ -610,11 +160,11 @@ export async function buildSnapshotPresentation(
610
160
  spillErrorText = error instanceof Error ? error.message : String(error);
611
161
  }
612
162
 
613
- const refEntries = getSnapshotRefEntries(data);
614
- const roleCounts = getSnapshotRoleCounts(refEntries);
615
- const roleCountsText = formatRoleCounts(roleCounts);
616
163
  const snapshot = getSnapshotText(data) ?? "(no interactive elements)";
617
164
  const snapshotLines = parseSnapshotLines(snapshot);
165
+ const refEntries = enrichSnapshotRefEntries(getSnapshotRefEntries(data), snapshotLines);
166
+ const roleCounts = getSnapshotRoleCounts(refEntries);
167
+ const roleCountsText = formatRoleCounts(roleCounts);
618
168
  const useStructuredPreview = canUseStructuredSnapshotPreview(snapshotLines, refEntries);
619
169
  const snapshotSegments = useStructuredPreview ? buildSnapshotSegments(snapshotLines) : [];
620
170
  const primarySegment = useStructuredPreview ? choosePrimarySegment(snapshotSegments) : undefined;
@@ -656,15 +206,13 @@ export async function buildSnapshotPresentation(
656
206
  .filter((entry) => !keyRefIdSet.has(entry.id))
657
207
  .slice(0, SNAPSHOT_OTHER_REF_MAX_LINES);
658
208
  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,
209
+ const omittedRefEntries = visibleRankedRefEntries.filter((entry) => !displayedRefIdSet.has(entry.id));
210
+ const highValueControlEntries = omittedRefEntries.filter(
211
+ (entry) => isHighValueControlEntry(entry) && !isNoiseName(entry.name) && !isChromeSectionName(entry.name),
667
212
  );
213
+ const visibleHighValueControlEntries = selectHighValueControlEntries(highValueControlEntries, SNAPSHOT_HIGH_VALUE_REF_MAX_LINES);
214
+ const omittedHighValueControls = Math.max(0, highValueControlEntries.length - visibleHighValueControlEntries.length);
215
+ const omittedNonHighlightedRefs = Math.max(0, omittedRefEntries.length - highValueControlEntries.length);
668
216
  const origin = getSnapshotOrigin(data);
669
217
 
670
218
  const lines: string[] = [
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Purpose: Hold tiny text and ref-id helpers shared by result renderers.
3
+ * Responsibilities: Convert unknown values to text, count lines, normalize whitespace, truncate labels, and sort ref ids naturally.
4
+ * Scope: Generic pure utilities only; no agent-browser command policy belongs here.
5
+ * Usage: Imported by envelope, presentation, snapshot, and diagnostic helpers.
6
+ * Invariants/Assumptions: Helpers are deterministic and side-effect free.
7
+ */
8
+
9
+ export function stringifyUnknown(value: unknown): string {
10
+ if (typeof value === "string") return value;
11
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
12
+ if (value === null || value === undefined) return "";
13
+ try {
14
+ return JSON.stringify(value, null, 2);
15
+ } catch {
16
+ return String(value);
17
+ }
18
+ }
19
+
20
+ export function countLines(text: string): number {
21
+ return text.length === 0 ? 0 : text.split("\n").length;
22
+ }
23
+
24
+ export function normalizeWhitespace(text: string): string {
25
+ return text.replace(/\s+/g, " ").trim();
26
+ }
27
+
28
+ export function truncateText(text: string, maxChars: number): string {
29
+ if (text.length <= maxChars) return text;
30
+ return `${text.slice(0, Math.max(1, maxChars - 1))}…`;
31
+ }
32
+
33
+ export function compareRefIds(left: string, right: string): number {
34
+ const leftMatch = left.match(/^(?:[a-zA-Z]+)?(\d+)$/);
35
+ const rightMatch = right.match(/^(?:[a-zA-Z]+)?(\d+)$/);
36
+ if (leftMatch && rightMatch) {
37
+ return Number(leftMatch[1]) - Number(rightMatch[1]);
38
+ }
39
+ return left.localeCompare(right);
40
+ }
@@ -9,22 +9,33 @@
9
9
  export { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "./results/envelope.js";
10
10
  export { buildToolPresentation } from "./results/presentation.js";
11
11
  export {
12
- buildAgentBrowserNextActions,
13
12
  buildAgentBrowserResultCategoryDetails,
14
13
  classifyAgentBrowserFailureCategory,
15
14
  classifyAgentBrowserSuccessCategory,
16
- compareRefIds,
17
- } from "./results/shared.js";
15
+ } from "./results/categories.js";
16
+ export { buildAgentBrowserNextActions } from "./results/action-recommendations.js";
17
+ export { compareRefIds } from "./results/text.js";
18
+ export {
19
+ AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS,
20
+ AGENT_BROWSER_RICH_INPUT_RECOVERY_NEXT_ACTION_IDS,
21
+ getAgentBrowserRichInputRecoveryNextActionId,
22
+ getAgentBrowserRichInputRecoveryNextActionIds,
23
+ } from "./results/recovery-actions.js";
18
24
  export type {
19
25
  AgentBrowserBatchResult,
20
26
  AgentBrowserEnvelope,
21
27
  AgentBrowserFailureCategory,
22
- AgentBrowserResultCategory,
23
28
  AgentBrowserNextAction,
24
29
  AgentBrowserPageChangeSummary,
30
+ AgentBrowserResultCategory,
25
31
  AgentBrowserResultCategoryDetails,
26
32
  AgentBrowserSuccessCategory,
27
33
  FileArtifactKind,
28
34
  FileArtifactMetadata,
29
35
  ToolPresentation,
30
- } from "./results/shared.js";
36
+ } from "./results/contracts.js";
37
+ export type {
38
+ AgentBrowserRecoveryContext,
39
+ AgentBrowserRecoveryKind,
40
+ AgentBrowserRichInputRecoveryNextActionKind,
41
+ } from "./results/recovery-actions.js";