pi-agent-browser-native 0.1.5 → 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/CHANGELOG.md +23 -0
- package/README.md +89 -2
- package/docs/ARCHITECTURE.md +7 -3
- package/docs/RELEASE.md +1 -1
- package/docs/TOOL_CONTRACT.md +18 -10
- package/extensions/agent-browser/index.ts +185 -62
- package/extensions/agent-browser/lib/process.ts +91 -6
- package/extensions/agent-browser/lib/results/envelope.ts +102 -0
- package/extensions/agent-browser/lib/results/presentation.ts +461 -0
- package/extensions/agent-browser/lib/results/shared.ts +91 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +648 -0
- package/extensions/agent-browser/lib/results.ts +8 -934
- package/extensions/agent-browser/lib/runtime.ts +66 -24
- package/extensions/agent-browser/lib/temp.ts +159 -16
- package/package.json +1 -1
|
@@ -1,937 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Purpose:
|
|
3
|
-
* Responsibilities:
|
|
4
|
-
* Scope:
|
|
5
|
-
* Usage: Imported by the extension entrypoint after
|
|
6
|
-
* Invariants/Assumptions:
|
|
2
|
+
* Purpose: Provide the public result-rendering facade for the pi-agent-browser extension.
|
|
3
|
+
* Responsibilities: Re-export focused envelope parsing, snapshot rendering, and presentation helpers from smaller modules while preserving the stable import surface used by the extension entrypoint and tests.
|
|
4
|
+
* Scope: Facade only; implementation details live in `lib/results/*` modules.
|
|
5
|
+
* Usage: Imported by the extension entrypoint after upstream `agent-browser` execution completes.
|
|
6
|
+
* Invariants/Assumptions: Public exports remain stable even as result-rendering internals are split into narrower modules.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import type { CommandInfo } from "./runtime.js";
|
|
13
|
-
import { getImageMimeType } from "./runtime.js";
|
|
14
|
-
import { writeSecureTempFile } from "./temp.js";
|
|
15
|
-
|
|
16
|
-
const SNAPSHOT_INLINE_MAX_CHARS = 6_000;
|
|
17
|
-
const SNAPSHOT_INLINE_MAX_LINES = 80;
|
|
18
|
-
const SNAPSHOT_INLINE_MAX_REFS = 60;
|
|
19
|
-
const SNAPSHOT_PRIMARY_PREVIEW_LINES = 10;
|
|
20
|
-
const SNAPSHOT_SECTION_PREVIEW_LINES = 4;
|
|
21
|
-
const SNAPSHOT_MAX_ADDITIONAL_SECTIONS = 5;
|
|
22
|
-
const SNAPSHOT_KEY_REF_MAX_LINES = 20;
|
|
23
|
-
const SNAPSHOT_OTHER_REF_MAX_LINES = 12;
|
|
24
|
-
const SNAPSHOT_NAME_MAX_CHARS = 96;
|
|
25
|
-
const SNAPSHOT_LINE_MAX_CHARS = 140;
|
|
26
|
-
const SNAPSHOT_SPILL_FILE_PREFIX = "pi-agent-browser-snapshot";
|
|
27
|
-
const SNAPSHOT_SIGNAL_ROLES = new Set([
|
|
28
|
-
"article",
|
|
29
|
-
"banner",
|
|
30
|
-
"button",
|
|
31
|
-
"checkbox",
|
|
32
|
-
"combobox",
|
|
33
|
-
"dialog",
|
|
34
|
-
"gridcell",
|
|
35
|
-
"heading",
|
|
36
|
-
"link",
|
|
37
|
-
"listitem",
|
|
38
|
-
"main",
|
|
39
|
-
"menu",
|
|
40
|
-
"menuitem",
|
|
41
|
-
"navigation",
|
|
42
|
-
"option",
|
|
43
|
-
"radio",
|
|
44
|
-
"region",
|
|
45
|
-
"row",
|
|
46
|
-
"tab",
|
|
47
|
-
"textbox",
|
|
48
|
-
]);
|
|
49
|
-
const SNAPSHOT_SEGMENT_ROOT_ROLES = new Set(["article", "dialog", "heading", "main", "menu", "region"]);
|
|
50
|
-
const SNAPSHOT_ROLE_PRIORITY: Record<string, number> = {
|
|
51
|
-
article: 0,
|
|
52
|
-
main: 1,
|
|
53
|
-
dialog: 2,
|
|
54
|
-
menu: 3,
|
|
55
|
-
region: 4,
|
|
56
|
-
heading: 5,
|
|
57
|
-
button: 6,
|
|
58
|
-
textbox: 7,
|
|
59
|
-
combobox: 8,
|
|
60
|
-
checkbox: 9,
|
|
61
|
-
radio: 10,
|
|
62
|
-
tab: 11,
|
|
63
|
-
option: 12,
|
|
64
|
-
link: 13,
|
|
65
|
-
listitem: 14,
|
|
66
|
-
row: 15,
|
|
67
|
-
gridcell: 16,
|
|
68
|
-
navigation: 17,
|
|
69
|
-
generic: 99,
|
|
70
|
-
unknown: 100,
|
|
71
|
-
};
|
|
72
|
-
const SNAPSHOT_NOISE_NAME_PATTERNS = [
|
|
73
|
-
/^skip to /i,
|
|
74
|
-
/^ad$/i,
|
|
75
|
-
/^don't want to see ads\??$/i,
|
|
76
|
-
/keyboard shortcuts/i,
|
|
77
|
-
/\bpromoted\b/i,
|
|
78
|
-
/\bsponsored\b/i,
|
|
79
|
-
];
|
|
80
|
-
const SNAPSHOT_CHROME_SECTION_PATTERNS = [
|
|
81
|
-
/^primary$/i,
|
|
82
|
-
/^footer$/i,
|
|
83
|
-
/^navigation$/i,
|
|
84
|
-
/\bwhat['’]?s happening\b/i,
|
|
85
|
-
/\brelevant people\b/i,
|
|
86
|
-
/\btrending\b/i,
|
|
87
|
-
/\brelated\b/i,
|
|
88
|
-
/\brecommended\b/i,
|
|
89
|
-
/\bsuggested\b/i,
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
export interface AgentBrowserEnvelope {
|
|
93
|
-
data?: unknown;
|
|
94
|
-
error?: unknown;
|
|
95
|
-
success?: boolean;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface AgentBrowserBatchResult {
|
|
99
|
-
command?: string[];
|
|
100
|
-
error?: unknown;
|
|
101
|
-
result?: unknown;
|
|
102
|
-
success?: boolean;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export interface ToolPresentation {
|
|
106
|
-
content: Array<{ text: string; type: "text" } | { data: string; mimeType: string; type: "image" }>;
|
|
107
|
-
data?: unknown;
|
|
108
|
-
fullOutputPath?: string;
|
|
109
|
-
imagePath?: string;
|
|
110
|
-
summary: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
interface SnapshotRefEntry {
|
|
114
|
-
id: string;
|
|
115
|
-
name: string;
|
|
116
|
-
role: string;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
interface SnapshotLine {
|
|
120
|
-
depth: number;
|
|
121
|
-
headingLevel?: number;
|
|
122
|
-
index: number;
|
|
123
|
-
name: string;
|
|
124
|
-
raw: string;
|
|
125
|
-
ref?: string;
|
|
126
|
-
role: string;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
interface SnapshotSegment {
|
|
130
|
-
endIndexExclusive: number;
|
|
131
|
-
lines: SnapshotLine[];
|
|
132
|
-
root: SnapshotLine;
|
|
133
|
-
score: number;
|
|
134
|
-
startIndex: number;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
interface SnapshotPreview {
|
|
138
|
-
omittedCount: number;
|
|
139
|
-
refIds: string[];
|
|
140
|
-
lines: string[];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
144
|
-
return typeof value === "object" && value !== null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function stringifyUnknown(value: unknown): string {
|
|
148
|
-
if (typeof value === "string") return value;
|
|
149
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
150
|
-
if (value === null || value === undefined) return "";
|
|
151
|
-
try {
|
|
152
|
-
return JSON.stringify(value, null, 2);
|
|
153
|
-
} catch {
|
|
154
|
-
return String(value);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function getSnapshotText(data: Record<string, unknown>): string | undefined {
|
|
159
|
-
return typeof data.snapshot === "string" ? data.snapshot : undefined;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function getTabSummary(data: Record<string, unknown>): string | undefined {
|
|
163
|
-
const tabs = Array.isArray(data.tabs) ? data.tabs : undefined;
|
|
164
|
-
if (!tabs) return undefined;
|
|
165
|
-
|
|
166
|
-
const lines = tabs.map((tab, index) => {
|
|
167
|
-
if (!isRecord(tab)) return `${index}: <invalid tab>`;
|
|
168
|
-
const marker = tab.active === true ? "*" : "-";
|
|
169
|
-
const title = typeof tab.title === "string" ? tab.title : "(untitled)";
|
|
170
|
-
const url = typeof tab.url === "string" ? tab.url : "(no url)";
|
|
171
|
-
const tabIndex = typeof tab.index === "number" ? tab.index : index;
|
|
172
|
-
return `${marker} [${tabIndex}] ${title} — ${url}`;
|
|
173
|
-
});
|
|
174
|
-
return lines.join("\n");
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function getStreamSummary(data: Record<string, unknown>): string | undefined {
|
|
178
|
-
if (typeof data.enabled !== "boolean" || typeof data.connected !== "boolean") {
|
|
179
|
-
return undefined;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const lines = [
|
|
183
|
-
`Enabled: ${data.enabled}`,
|
|
184
|
-
`Connected: ${data.connected}`,
|
|
185
|
-
`Screencasting: ${data.screencasting === true}`,
|
|
186
|
-
];
|
|
187
|
-
if (typeof data.port === "number") {
|
|
188
|
-
lines.push(`Port: ${data.port}`);
|
|
189
|
-
}
|
|
190
|
-
return lines.join("\n");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function getPageSummary(data: Record<string, unknown>): string | undefined {
|
|
194
|
-
const title = typeof data.title === "string" ? data.title : undefined;
|
|
195
|
-
const url = typeof data.url === "string" ? data.url : undefined;
|
|
196
|
-
if (!title && !url) return undefined;
|
|
197
|
-
if (title && url) return `${title}\n${url}`;
|
|
198
|
-
return title ?? url;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function getScreenshotSummary(data: Record<string, unknown>): string | undefined {
|
|
202
|
-
return typeof data.path === "string" ? `Saved image: ${data.path}` : undefined;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function formatBatchContent(data: AgentBrowserBatchResult[]): string {
|
|
206
|
-
return data
|
|
207
|
-
.map((item, index) => {
|
|
208
|
-
const command = Array.isArray(item.command) ? item.command.join(" ") : `step-${index + 1}`;
|
|
209
|
-
if (item.success === false) {
|
|
210
|
-
return `${command}\nError: ${stringifyUnknown(item.error)}`;
|
|
211
|
-
}
|
|
212
|
-
return `${command}\n${stringifyUnknown(item.result)}`;
|
|
213
|
-
})
|
|
214
|
-
.join("\n\n");
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function formatSummary(commandInfo: CommandInfo, data: unknown): string {
|
|
218
|
-
if (Array.isArray(data) && commandInfo.command === "batch") {
|
|
219
|
-
const successCount = data.filter((item) => isRecord(item) && item.success !== false).length;
|
|
220
|
-
return `Batch: ${successCount}/${data.length} succeeded`;
|
|
221
|
-
}
|
|
222
|
-
if (isRecord(data)) {
|
|
223
|
-
if (commandInfo.command === "snapshot") {
|
|
224
|
-
const origin = typeof data.origin === "string" ? data.origin : "page";
|
|
225
|
-
const refs = isRecord(data.refs) ? Object.keys(data.refs).length : 0;
|
|
226
|
-
return `Snapshot: ${refs} refs on ${origin}`;
|
|
227
|
-
}
|
|
228
|
-
if (commandInfo.command === "tab" && Array.isArray(data.tabs)) {
|
|
229
|
-
return `Tabs: ${data.tabs.length}`;
|
|
230
|
-
}
|
|
231
|
-
if (commandInfo.command === "stream" && commandInfo.subcommand === "status") {
|
|
232
|
-
const port = typeof data.port === "number" ? ` on port ${data.port}` : "";
|
|
233
|
-
return `Stream ${data.enabled === true ? "enabled" : "disabled"}${port}`;
|
|
234
|
-
}
|
|
235
|
-
if (commandInfo.command === "screenshot" && typeof data.path === "string") {
|
|
236
|
-
return `Screenshot saved: ${data.path}`;
|
|
237
|
-
}
|
|
238
|
-
const pageSummary = getPageSummary(data);
|
|
239
|
-
if (pageSummary) {
|
|
240
|
-
return pageSummary.split("\n", 1)[0] ?? "agent-browser result";
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (typeof data === "string" && data.length > 0) {
|
|
245
|
-
return data.split("\n", 1)[0] ?? data;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const primaryCommand = commandInfo.command ?? "agent-browser";
|
|
249
|
-
return `${primaryCommand} completed`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getSnapshotOrigin(data: Record<string, unknown>): string {
|
|
253
|
-
return typeof data.origin === "string" ? data.origin : "(unknown origin)";
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function formatRawSnapshotText(data: Record<string, unknown>): string {
|
|
257
|
-
const origin = getSnapshotOrigin(data);
|
|
258
|
-
const refs = isRecord(data.refs) ? Object.keys(data.refs).length : 0;
|
|
259
|
-
const snapshot = getSnapshotText(data);
|
|
260
|
-
if (!snapshot) {
|
|
261
|
-
return `Origin: ${origin}\nRefs: ${refs}\n\n(no interactive elements)`;
|
|
262
|
-
}
|
|
263
|
-
return `Origin: ${origin}\nRefs: ${refs}\n\n${snapshot}`;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function countLines(text: string): number {
|
|
267
|
-
return text.length === 0 ? 0 : text.split("\n").length;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function normalizeWhitespace(text: string): string {
|
|
271
|
-
return text.replace(/\s+/g, " ").trim();
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function truncateText(text: string, maxChars: number): string {
|
|
275
|
-
if (text.length <= maxChars) return text;
|
|
276
|
-
return `${text.slice(0, Math.max(1, maxChars - 1))}…`;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function formatPreviewLine(line: SnapshotLine, baseDepth: number): string {
|
|
280
|
-
const leadingWhitespace = (line.raw.match(/^\s*/) ?? [""])[0].length;
|
|
281
|
-
const stripChars = Math.min(leadingWhitespace, Math.max(0, baseDepth) * 2);
|
|
282
|
-
return truncateText(line.raw.slice(stripChars), SNAPSHOT_LINE_MAX_CHARS);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function compareRefIds(left: string, right: string): number {
|
|
286
|
-
const leftMatch = left.match(/^(?:[a-zA-Z]+)?(\d+)$/);
|
|
287
|
-
const rightMatch = right.match(/^(?:[a-zA-Z]+)?(\d+)$/);
|
|
288
|
-
if (leftMatch && rightMatch) {
|
|
289
|
-
return Number(leftMatch[1]) - Number(rightMatch[1]);
|
|
290
|
-
}
|
|
291
|
-
return left.localeCompare(right);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function getRolePriority(role: string): number {
|
|
295
|
-
return SNAPSHOT_ROLE_PRIORITY[role] ?? 50;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function getSnapshotRefEntries(data: Record<string, unknown>): SnapshotRefEntry[] {
|
|
299
|
-
const refs = isRecord(data.refs) ? data.refs : undefined;
|
|
300
|
-
if (!refs) return [];
|
|
301
|
-
|
|
302
|
-
return Object.entries(refs)
|
|
303
|
-
.map(([id, value]) => {
|
|
304
|
-
if (!isRecord(value)) {
|
|
305
|
-
return { id, name: "", role: "unknown" } satisfies SnapshotRefEntry;
|
|
306
|
-
}
|
|
307
|
-
const name = typeof value.name === "string" ? normalizeWhitespace(value.name) : "";
|
|
308
|
-
const role = typeof value.role === "string" && value.role.length > 0 ? value.role : "unknown";
|
|
309
|
-
return { id, name, role } satisfies SnapshotRefEntry;
|
|
310
|
-
})
|
|
311
|
-
.sort((a, b) => compareRefIds(a.id, b.id));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function getSnapshotRoleCounts(refEntries: SnapshotRefEntry[]): Record<string, number> {
|
|
315
|
-
const counts: Record<string, number> = {};
|
|
316
|
-
for (const entry of refEntries) {
|
|
317
|
-
counts[entry.role] = (counts[entry.role] ?? 0) + 1;
|
|
318
|
-
}
|
|
319
|
-
return counts;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function formatRoleCounts(roleCounts: Record<string, number>): string | undefined {
|
|
323
|
-
const entries = Object.entries(roleCounts);
|
|
324
|
-
if (entries.length === 0) return undefined;
|
|
325
|
-
|
|
326
|
-
const ordered = entries.sort((left, right) => {
|
|
327
|
-
if (right[1] !== left[1]) return right[1] - left[1];
|
|
328
|
-
return getRolePriority(left[0]) - getRolePriority(right[0]);
|
|
329
|
-
});
|
|
330
|
-
return ordered.map(([role, count]) => `${role} ${count}`).join(", ");
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function parseSnapshotLines(snapshot: string): SnapshotLine[] {
|
|
334
|
-
return snapshot
|
|
335
|
-
.split("\n")
|
|
336
|
-
.filter((line) => line.length > 0)
|
|
337
|
-
.map((raw, index) => {
|
|
338
|
-
const trimmed = raw.trimStart();
|
|
339
|
-
const depth = Math.floor(((raw.match(/^\s*/) ?? [""])[0].length ?? 0) / 2);
|
|
340
|
-
const role = trimmed.match(/^[-*]\s+([^\s"]+)/)?.[1] ?? "unknown";
|
|
341
|
-
const name = normalizeWhitespace(trimmed.match(/"([^"]*)"/)?.[1] ?? "");
|
|
342
|
-
const ref = trimmed.match(/\bref=([^,\]\s]+)/)?.[1];
|
|
343
|
-
const headingLevel = trimmed.match(/\blevel=(\d+)/)?.[1];
|
|
344
|
-
return {
|
|
345
|
-
depth,
|
|
346
|
-
headingLevel: headingLevel ? Number(headingLevel) : undefined,
|
|
347
|
-
index,
|
|
348
|
-
name,
|
|
349
|
-
raw,
|
|
350
|
-
ref,
|
|
351
|
-
role,
|
|
352
|
-
} satisfies SnapshotLine;
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function isNoiseName(name: string): boolean {
|
|
357
|
-
return SNAPSHOT_NOISE_NAME_PATTERNS.some((pattern) => pattern.test(name));
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function isChromeSectionName(name: string): boolean {
|
|
361
|
-
return SNAPSHOT_CHROME_SECTION_PATTERNS.some((pattern) => pattern.test(name));
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function isNoiseSnapshotLine(line: SnapshotLine): boolean {
|
|
365
|
-
if (line.name.length > 0 && isNoiseName(line.name)) return true;
|
|
366
|
-
const loweredRaw = line.raw.toLowerCase();
|
|
367
|
-
return loweredRaw.includes("promoted") || loweredRaw.includes("sponsored");
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function isPotentialSegmentRootLine(line: SnapshotLine): boolean {
|
|
371
|
-
if (!SNAPSHOT_SEGMENT_ROOT_ROLES.has(line.role)) return false;
|
|
372
|
-
if (isNoiseSnapshotLine(line)) return false;
|
|
373
|
-
if (line.role === "heading") {
|
|
374
|
-
return line.name.length > 0 && (line.headingLevel ?? 99) <= 3;
|
|
375
|
-
}
|
|
376
|
-
if (line.role === "region") {
|
|
377
|
-
return line.name.length > 0;
|
|
378
|
-
}
|
|
379
|
-
return true;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function scoreSegment(segment: SnapshotSegment): number {
|
|
383
|
-
const { root } = segment;
|
|
384
|
-
const distinctRefs = new Set(segment.lines.flatMap((line) => (line.ref ? [line.ref] : []))).size;
|
|
385
|
-
let score = 0;
|
|
386
|
-
|
|
387
|
-
score += 120 - getRolePriority(root.role) * 8;
|
|
388
|
-
score += Math.min(distinctRefs, 16);
|
|
389
|
-
score += Math.min(segment.lines.length, 12);
|
|
390
|
-
score -= Math.min(root.index, 60) / 3;
|
|
391
|
-
score -= root.depth * 6;
|
|
392
|
-
|
|
393
|
-
if (root.role === "heading") {
|
|
394
|
-
if (root.headingLevel === 1) score += 40;
|
|
395
|
-
else if (root.headingLevel === 2) score += 22;
|
|
396
|
-
else if (root.headingLevel === 3) score += 12;
|
|
397
|
-
}
|
|
398
|
-
if (root.name.length > 0) score += 10;
|
|
399
|
-
if (root.name.length <= 2) score -= 18;
|
|
400
|
-
if (isChromeSectionName(root.name)) score -= 45;
|
|
401
|
-
if (isNoiseName(root.name)) score -= 1000;
|
|
402
|
-
return score;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function buildSnapshotSegments(snapshotLines: SnapshotLine[]): SnapshotSegment[] {
|
|
406
|
-
const roots: SnapshotLine[] = [];
|
|
407
|
-
const stack: SnapshotLine[] = [];
|
|
408
|
-
|
|
409
|
-
for (const line of snapshotLines) {
|
|
410
|
-
stack.length = line.depth;
|
|
411
|
-
if (isPotentialSegmentRootLine(line)) {
|
|
412
|
-
const normalizedName = normalizeWhitespace(line.name.toLowerCase());
|
|
413
|
-
let duplicateAncestor: SnapshotLine | undefined;
|
|
414
|
-
for (let index = stack.length - 1; index >= 0; index -= 1) {
|
|
415
|
-
const ancestor = stack[index];
|
|
416
|
-
if (
|
|
417
|
-
normalizedName.length > 0 &&
|
|
418
|
-
normalizeWhitespace(ancestor.name.toLowerCase()) === normalizedName &&
|
|
419
|
-
SNAPSHOT_SEGMENT_ROOT_ROLES.has(ancestor.role)
|
|
420
|
-
) {
|
|
421
|
-
duplicateAncestor = ancestor;
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
if (!duplicateAncestor) {
|
|
426
|
-
roots.push(line);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
stack[line.depth] = line;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return roots.map((root, index) => {
|
|
433
|
-
let endIndexExclusive = snapshotLines.length;
|
|
434
|
-
for (let nextIndex = index + 1; nextIndex < roots.length; nextIndex += 1) {
|
|
435
|
-
const candidate = roots[nextIndex];
|
|
436
|
-
if (candidate.depth <= root.depth) {
|
|
437
|
-
endIndexExclusive = candidate.index;
|
|
438
|
-
break;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
const lines = snapshotLines.slice(root.index, endIndexExclusive);
|
|
442
|
-
const segment: SnapshotSegment = {
|
|
443
|
-
endIndexExclusive,
|
|
444
|
-
lines,
|
|
445
|
-
root,
|
|
446
|
-
score: 0,
|
|
447
|
-
startIndex: root.index,
|
|
448
|
-
};
|
|
449
|
-
segment.score = scoreSegment(segment);
|
|
450
|
-
return segment;
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function choosePrimarySegment(segments: SnapshotSegment[]): SnapshotSegment | undefined {
|
|
455
|
-
if (segments.length === 0) return undefined;
|
|
456
|
-
return (
|
|
457
|
-
segments.find((segment) => segment.root.role === "main" || segment.root.role === "article") ??
|
|
458
|
-
segments.find((segment) => segment.root.role === "heading" && segment.root.headingLevel === 1) ??
|
|
459
|
-
segments.find((segment) => segment.score >= 90) ??
|
|
460
|
-
[...segments].sort((left, right) => right.score - left.score || left.startIndex - right.startIndex)[0]
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function chooseAdditionalSegments(segments: SnapshotSegment[], primary: SnapshotSegment | undefined): SnapshotSegment[] {
|
|
465
|
-
if (!primary) return [];
|
|
466
|
-
|
|
467
|
-
const seenNames = new Set<string>([normalizeWhitespace(primary.root.name.toLowerCase())]);
|
|
468
|
-
const rankedCandidates = segments
|
|
469
|
-
.filter((segment) => segment !== primary && segment.score >= 45)
|
|
470
|
-
.sort((left, right) => {
|
|
471
|
-
const leftDistance = Math.abs(left.startIndex - primary.startIndex);
|
|
472
|
-
const rightDistance = Math.abs(right.startIndex - primary.startIndex);
|
|
473
|
-
if (leftDistance !== rightDistance) return leftDistance - rightDistance;
|
|
474
|
-
if (right.score !== left.score) return right.score - left.score;
|
|
475
|
-
return left.startIndex - right.startIndex;
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
const chosen: SnapshotSegment[] = [];
|
|
479
|
-
for (const segment of rankedCandidates) {
|
|
480
|
-
if (chosen.length >= SNAPSHOT_MAX_ADDITIONAL_SECTIONS) break;
|
|
481
|
-
if (isChromeSectionName(segment.root.name)) continue;
|
|
482
|
-
if (segment.root.role === "heading" && segment.root.name.length <= 2) continue;
|
|
483
|
-
const nameKey = normalizeWhitespace(segment.root.name.toLowerCase());
|
|
484
|
-
if (nameKey && seenNames.has(nameKey)) continue;
|
|
485
|
-
chosen.push(segment);
|
|
486
|
-
if (nameKey) seenNames.add(nameKey);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return chosen.sort((left, right) => left.startIndex - right.startIndex);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function getMeaningfulSegmentLines(segment: SnapshotSegment): SnapshotLine[] {
|
|
493
|
-
return segment.lines.filter((line) => {
|
|
494
|
-
if (isNoiseSnapshotLine(line)) return false;
|
|
495
|
-
if (line.role === "generic" && !line.ref && line.name.length === 0) return false;
|
|
496
|
-
if (line.role === "link" && line.name.length === 0) return false;
|
|
497
|
-
return true;
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function buildSegmentPreview(segment: SnapshotSegment, maxLines: number): SnapshotPreview {
|
|
502
|
-
const meaningfulLines = getMeaningfulSegmentLines(segment);
|
|
503
|
-
if (meaningfulLines.length === 0) {
|
|
504
|
-
return { omittedCount: 0, refIds: [], lines: [] };
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const previewLines: SnapshotLine[] = [];
|
|
508
|
-
const previewRefIds = new Set<string>();
|
|
509
|
-
const seenPreviewKeys = new Set<string>();
|
|
510
|
-
const rootDepth = segment.root.depth;
|
|
511
|
-
|
|
512
|
-
for (const line of meaningfulLines) {
|
|
513
|
-
if (previewLines.length >= maxLines) break;
|
|
514
|
-
if (line !== segment.root) {
|
|
515
|
-
const relativeDepth = line.depth - rootDepth;
|
|
516
|
-
if (segment.root.role !== "heading" && relativeDepth > 2) continue;
|
|
517
|
-
if (segment.root.name.length > 0 && line.name === segment.root.name && (line.role === "heading" || line.role === "link")) {
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const key = `${line.role}:${line.name}:${line.ref ?? ""}:${line.depth}`;
|
|
523
|
-
if (seenPreviewKeys.has(key)) continue;
|
|
524
|
-
seenPreviewKeys.add(key);
|
|
525
|
-
previewLines.push(line);
|
|
526
|
-
if (line.ref) previewRefIds.add(line.ref);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return {
|
|
530
|
-
omittedCount: Math.max(0, meaningfulLines.length - previewLines.length),
|
|
531
|
-
refIds: [...previewRefIds],
|
|
532
|
-
lines: previewLines.map((line) => formatPreviewLine(line, rootDepth)),
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function buildFallbackSnapshotOutline(snapshotLines: SnapshotLine[]): SnapshotPreview {
|
|
537
|
-
const selected = new Set<number>();
|
|
538
|
-
for (let index = 0; index < snapshotLines.length && selected.size < 6; index += 1) {
|
|
539
|
-
if (!isNoiseSnapshotLine(snapshotLines[index])) selected.add(index);
|
|
540
|
-
}
|
|
541
|
-
for (let index = 0; index < snapshotLines.length && selected.size < 18; index += 1) {
|
|
542
|
-
const line = snapshotLines[index];
|
|
543
|
-
if (isNoiseSnapshotLine(line)) continue;
|
|
544
|
-
if (SNAPSHOT_SIGNAL_ROLES.has(line.role)) {
|
|
545
|
-
selected.add(index);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
const chosenLines = [...selected]
|
|
549
|
-
.sort((left, right) => left - right)
|
|
550
|
-
.slice(0, 18)
|
|
551
|
-
.map((index) => snapshotLines[index]);
|
|
552
|
-
return {
|
|
553
|
-
omittedCount: Math.max(0, snapshotLines.length - chosenLines.length),
|
|
554
|
-
refIds: chosenLines.flatMap((line) => (line.ref ? [line.ref] : [])),
|
|
555
|
-
lines: chosenLines.map((line) => truncateText(line.raw, SNAPSHOT_LINE_MAX_CHARS)),
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function buildRefLineOrderMap(snapshotLines: SnapshotLine[]): Map<string, number> {
|
|
560
|
-
const map = new Map<string, number>();
|
|
561
|
-
for (const line of snapshotLines) {
|
|
562
|
-
if (!line.ref || map.has(line.ref)) continue;
|
|
563
|
-
map.set(line.ref, line.index);
|
|
564
|
-
}
|
|
565
|
-
return map;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function rankRefEntries(
|
|
569
|
-
refEntries: SnapshotRefEntry[],
|
|
570
|
-
previewRefIds: Set<string>,
|
|
571
|
-
focusRefIds: Set<string>,
|
|
572
|
-
lineOrderByRef: Map<string, number>,
|
|
573
|
-
): SnapshotRefEntry[] {
|
|
574
|
-
return [...refEntries].sort((left, right) => {
|
|
575
|
-
const leftBucket = previewRefIds.has(left.id) ? 0 : focusRefIds.has(left.id) ? 1 : 2;
|
|
576
|
-
const rightBucket = previewRefIds.has(right.id) ? 0 : focusRefIds.has(right.id) ? 1 : 2;
|
|
577
|
-
if (leftBucket !== rightBucket) return leftBucket - rightBucket;
|
|
578
|
-
|
|
579
|
-
const rolePriority = getRolePriority(left.role) - getRolePriority(right.role);
|
|
580
|
-
if (rolePriority !== 0) return rolePriority;
|
|
581
|
-
|
|
582
|
-
const leftHasName = left.name.length > 0 ? 0 : 1;
|
|
583
|
-
const rightHasName = right.name.length > 0 ? 0 : 1;
|
|
584
|
-
if (leftHasName !== rightHasName) return leftHasName - rightHasName;
|
|
585
|
-
|
|
586
|
-
const leftLineOrder = lineOrderByRef.get(left.id) ?? Number.MAX_SAFE_INTEGER;
|
|
587
|
-
const rightLineOrder = lineOrderByRef.get(right.id) ?? Number.MAX_SAFE_INTEGER;
|
|
588
|
-
if (leftLineOrder !== rightLineOrder) return leftLineOrder - rightLineOrder;
|
|
589
|
-
|
|
590
|
-
return compareRefIds(left.id, right.id);
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function formatCompactRef(entry: SnapshotRefEntry): string {
|
|
595
|
-
const suffix = entry.name.length > 0 ? ` "${truncateText(entry.name, SNAPSHOT_NAME_MAX_CHARS)}"` : "";
|
|
596
|
-
return `- ${entry.id} ${entry.role}${suffix}`;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
function shouldCompactSnapshot(rawText: string, data: Record<string, unknown>): boolean {
|
|
600
|
-
const snapshot = getSnapshotText(data) ?? "";
|
|
601
|
-
const refEntries = getSnapshotRefEntries(data);
|
|
602
|
-
return (
|
|
603
|
-
rawText.length > SNAPSHOT_INLINE_MAX_CHARS ||
|
|
604
|
-
countLines(snapshot) > SNAPSHOT_INLINE_MAX_LINES ||
|
|
605
|
-
refEntries.length > SNAPSHOT_INLINE_MAX_REFS
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
async function writeSnapshotSpillFile(data: Record<string, unknown>): Promise<string> {
|
|
610
|
-
return await writeSecureTempFile({
|
|
611
|
-
content: JSON.stringify(data, null, 2),
|
|
612
|
-
prefix: SNAPSHOT_SPILL_FILE_PREFIX,
|
|
613
|
-
suffix: ".json",
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async function buildSnapshotPresentation(data: Record<string, unknown>): Promise<ToolPresentation> {
|
|
618
|
-
const summary = formatSummary({ command: "snapshot" }, data);
|
|
619
|
-
const rawText = formatRawSnapshotText(data);
|
|
620
|
-
if (!shouldCompactSnapshot(rawText, data)) {
|
|
621
|
-
return {
|
|
622
|
-
content: [{ type: "text", text: rawText }],
|
|
623
|
-
data,
|
|
624
|
-
summary,
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const fullOutputPath = await writeSnapshotSpillFile(data);
|
|
629
|
-
const refEntries = getSnapshotRefEntries(data);
|
|
630
|
-
const roleCounts = getSnapshotRoleCounts(refEntries);
|
|
631
|
-
const roleCountsText = formatRoleCounts(roleCounts);
|
|
632
|
-
const snapshot = getSnapshotText(data) ?? "(no interactive elements)";
|
|
633
|
-
const snapshotLines = parseSnapshotLines(snapshot);
|
|
634
|
-
const snapshotSegments = buildSnapshotSegments(snapshotLines);
|
|
635
|
-
const primarySegment = choosePrimarySegment(snapshotSegments);
|
|
636
|
-
const additionalSegments = chooseAdditionalSegments(snapshotSegments, primarySegment);
|
|
637
|
-
const primaryPreview = primarySegment ? buildSegmentPreview(primarySegment, SNAPSHOT_PRIMARY_PREVIEW_LINES) : undefined;
|
|
638
|
-
const additionalPreviews = additionalSegments
|
|
639
|
-
.map((segment) => ({
|
|
640
|
-
preview: buildSegmentPreview(segment, SNAPSHOT_SECTION_PREVIEW_LINES),
|
|
641
|
-
segment,
|
|
642
|
-
}))
|
|
643
|
-
.filter(({ preview }) => preview.lines.length > 0);
|
|
644
|
-
const fallbackPreview =
|
|
645
|
-
!primaryPreview || primaryPreview.lines.length === 0 ? buildFallbackSnapshotOutline(snapshotLines) : undefined;
|
|
646
|
-
|
|
647
|
-
const previewRefIds = new Set<string>([
|
|
648
|
-
...(primaryPreview?.refIds ?? []),
|
|
649
|
-
...additionalPreviews.flatMap(({ preview }) => preview.refIds),
|
|
650
|
-
...(fallbackPreview?.refIds ?? []),
|
|
651
|
-
]);
|
|
652
|
-
const focusRefIds = new Set<string>([
|
|
653
|
-
...(primarySegment ? getMeaningfulSegmentLines(primarySegment).flatMap((line) => (line.ref ? [line.ref] : [])) : []),
|
|
654
|
-
...additionalSegments.flatMap((segment) => getMeaningfulSegmentLines(segment).flatMap((line) => (line.ref ? [line.ref] : []))),
|
|
655
|
-
...(fallbackPreview?.refIds ?? []),
|
|
656
|
-
]);
|
|
657
|
-
const lineOrderByRef = buildRefLineOrderMap(snapshotLines);
|
|
658
|
-
const rankedRefEntries = rankRefEntries(refEntries, previewRefIds, focusRefIds, lineOrderByRef);
|
|
659
|
-
const visibleRankedRefEntries = rankedRefEntries.filter(
|
|
660
|
-
(entry) => !isNoiseName(entry.name) && !isChromeSectionName(entry.name) && !(entry.role === "heading" && entry.name.length <= 2),
|
|
661
|
-
);
|
|
662
|
-
const keyRefEntries = visibleRankedRefEntries.slice(0, SNAPSHOT_KEY_REF_MAX_LINES);
|
|
663
|
-
const keyRefIdSet = new Set(keyRefEntries.map((entry) => entry.id));
|
|
664
|
-
const otherRefEntries = visibleRankedRefEntries
|
|
665
|
-
.filter((entry) => !keyRefIdSet.has(entry.id))
|
|
666
|
-
.slice(0, SNAPSHOT_OTHER_REF_MAX_LINES);
|
|
667
|
-
const omittedOtherRefs = Math.max(0, visibleRankedRefEntries.length - keyRefEntries.length - otherRefEntries.length);
|
|
668
|
-
const origin = getSnapshotOrigin(data);
|
|
669
|
-
|
|
670
|
-
const lines: string[] = [
|
|
671
|
-
`Origin: ${origin}`,
|
|
672
|
-
`Refs: ${refEntries.length}`,
|
|
673
|
-
...(roleCountsText ? [`Roles: ${roleCountsText}`] : []),
|
|
674
|
-
"",
|
|
675
|
-
`Compact snapshot view. Full raw snapshot: ${fullOutputPath}`,
|
|
676
|
-
];
|
|
677
|
-
|
|
678
|
-
if (fallbackPreview) {
|
|
679
|
-
lines.push(
|
|
680
|
-
"",
|
|
681
|
-
"Compact outline:",
|
|
682
|
-
...(fallbackPreview.lines.length > 0 ? fallbackPreview.lines : ["(no interactive elements)"]),
|
|
683
|
-
);
|
|
684
|
-
if (fallbackPreview.omittedCount > 0) {
|
|
685
|
-
lines.push(`- ... (${fallbackPreview.omittedCount} additional snapshot lines omitted; use the spill file for everything)`);
|
|
686
|
-
}
|
|
687
|
-
} else {
|
|
688
|
-
lines.push("", "Primary content:", ...(primaryPreview?.lines ?? ["(no interactive elements)"]));
|
|
689
|
-
if ((primaryPreview?.omittedCount ?? 0) > 0) {
|
|
690
|
-
lines.push(`- ... (${primaryPreview?.omittedCount} more lines in this section)`);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if (additionalPreviews.length > 0) {
|
|
694
|
-
lines.push("", "Additional sections:");
|
|
695
|
-
additionalPreviews.forEach(({ preview }, index) => {
|
|
696
|
-
if (index > 0) lines.push("");
|
|
697
|
-
lines.push(...preview.lines);
|
|
698
|
-
if (preview.omittedCount > 0) {
|
|
699
|
-
lines.push(`- ... (${preview.omittedCount} more lines in this section)`);
|
|
700
|
-
}
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
lines.push("", "Key refs:", ...(keyRefEntries.length > 0 ? keyRefEntries.map(formatCompactRef) : ["(no refs)"]));
|
|
706
|
-
if (otherRefEntries.length > 0) {
|
|
707
|
-
lines.push("", "Other refs:", ...otherRefEntries.map(formatCompactRef));
|
|
708
|
-
}
|
|
709
|
-
if (omittedOtherRefs > 0) {
|
|
710
|
-
lines.push(`- ... (${omittedOtherRefs} additional refs in the full snapshot file)`);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return {
|
|
714
|
-
content: [{ type: "text", text: lines.join("\n") }],
|
|
715
|
-
data: {
|
|
716
|
-
compacted: true,
|
|
717
|
-
fullOutputPath,
|
|
718
|
-
origin,
|
|
719
|
-
previewRefIds: [...previewRefIds],
|
|
720
|
-
previewSections: [
|
|
721
|
-
...(primarySegment
|
|
722
|
-
? [
|
|
723
|
-
{
|
|
724
|
-
linesShown: primaryPreview?.lines.length ?? 0,
|
|
725
|
-
omittedLines: primaryPreview?.omittedCount ?? 0,
|
|
726
|
-
role: primarySegment.root.role,
|
|
727
|
-
title: primarySegment.root.name,
|
|
728
|
-
},
|
|
729
|
-
]
|
|
730
|
-
: []),
|
|
731
|
-
...additionalPreviews.map(({ preview, segment }) => ({
|
|
732
|
-
linesShown: preview.lines.length,
|
|
733
|
-
omittedLines: preview.omittedCount,
|
|
734
|
-
role: segment.root.role,
|
|
735
|
-
title: segment.root.name,
|
|
736
|
-
})),
|
|
737
|
-
],
|
|
738
|
-
refCount: refEntries.length,
|
|
739
|
-
roleCounts,
|
|
740
|
-
snapshotLineCount: countLines(snapshot),
|
|
741
|
-
},
|
|
742
|
-
fullOutputPath,
|
|
743
|
-
summary: `${summary} (compact)`,
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function formatContentText(commandInfo: CommandInfo, data: unknown): string {
|
|
748
|
-
if (Array.isArray(data) && commandInfo.command === "batch") {
|
|
749
|
-
return formatBatchContent(data as AgentBrowserBatchResult[]);
|
|
750
|
-
}
|
|
751
|
-
if (typeof data === "string") {
|
|
752
|
-
return data;
|
|
753
|
-
}
|
|
754
|
-
if (typeof data === "number" || typeof data === "boolean") {
|
|
755
|
-
return String(data);
|
|
756
|
-
}
|
|
757
|
-
if (!isRecord(data)) {
|
|
758
|
-
return stringifyUnknown(data);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
if (commandInfo.command === "snapshot") {
|
|
762
|
-
return formatRawSnapshotText(data);
|
|
763
|
-
}
|
|
764
|
-
if (commandInfo.command === "tab") {
|
|
765
|
-
const tabSummary = getTabSummary(data);
|
|
766
|
-
if (tabSummary) return tabSummary;
|
|
767
|
-
}
|
|
768
|
-
if (commandInfo.command === "stream" && commandInfo.subcommand === "status") {
|
|
769
|
-
const streamSummary = getStreamSummary(data);
|
|
770
|
-
if (streamSummary) return streamSummary;
|
|
771
|
-
}
|
|
772
|
-
if (commandInfo.command === "screenshot") {
|
|
773
|
-
const screenshotSummary = getScreenshotSummary(data);
|
|
774
|
-
if (screenshotSummary) return screenshotSummary;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const pageSummary = getPageSummary(data);
|
|
778
|
-
if (pageSummary) {
|
|
779
|
-
return pageSummary;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
return stringifyUnknown(data);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
async function readEnvelopeSource(options: { stdout: string; stdoutPath?: string }): Promise<string> {
|
|
786
|
-
if (!options.stdoutPath) {
|
|
787
|
-
return options.stdout;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
try {
|
|
791
|
-
return await readFile(options.stdoutPath, "utf8");
|
|
792
|
-
} catch (error) {
|
|
793
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
794
|
-
throw new Error(`agent-browser output spill file could not be read: ${message}`);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
function extractEnvelopeErrorText(error: unknown): string | undefined {
|
|
799
|
-
if (typeof error === "string") {
|
|
800
|
-
return error.trim() || undefined;
|
|
801
|
-
}
|
|
802
|
-
if (typeof error === "number" || typeof error === "boolean") {
|
|
803
|
-
return String(error);
|
|
804
|
-
}
|
|
805
|
-
if (Array.isArray(error)) {
|
|
806
|
-
const parts = error.map((item) => extractEnvelopeErrorText(item) ?? stringifyUnknown(item)).filter((item) => item.length > 0);
|
|
807
|
-
return parts.length > 0 ? parts.join("\n") : undefined;
|
|
808
|
-
}
|
|
809
|
-
if (!isRecord(error)) {
|
|
810
|
-
return error == null ? undefined : stringifyUnknown(error);
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
for (const key of ["message", "error", "details", "cause", "stderr"] as const) {
|
|
814
|
-
const value = extractEnvelopeErrorText(error[key]);
|
|
815
|
-
if (value) return value;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const fallback = stringifyUnknown(error).trim();
|
|
819
|
-
return fallback.length > 0 && fallback !== "{}" ? fallback : undefined;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
export async function parseAgentBrowserEnvelope(options: string | { stdout: string; stdoutPath?: string }): Promise<{
|
|
823
|
-
envelope?: AgentBrowserEnvelope;
|
|
824
|
-
parseError?: string;
|
|
825
|
-
}> {
|
|
826
|
-
let stdout: string;
|
|
827
|
-
try {
|
|
828
|
-
stdout = typeof options === "string" ? options : await readEnvelopeSource(options);
|
|
829
|
-
} catch (error) {
|
|
830
|
-
return { parseError: error instanceof Error ? error.message : String(error) };
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const trimmed = stdout.trim();
|
|
834
|
-
if (trimmed.length === 0) {
|
|
835
|
-
return { parseError: "agent-browser returned no JSON output." };
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
try {
|
|
839
|
-
const parsed = JSON.parse(trimmed) as AgentBrowserEnvelope | AgentBrowserBatchResult[];
|
|
840
|
-
if (Array.isArray(parsed)) {
|
|
841
|
-
return { envelope: { success: parsed.every((item) => !isRecord(item) || item.success !== false), data: parsed } };
|
|
842
|
-
}
|
|
843
|
-
if (!isRecord(parsed)) {
|
|
844
|
-
return { parseError: "agent-browser returned JSON, but it was not an object envelope." };
|
|
845
|
-
}
|
|
846
|
-
return { envelope: parsed };
|
|
847
|
-
} catch (error) {
|
|
848
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
849
|
-
return { parseError: `agent-browser returned invalid JSON: ${message}` };
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
export function getAgentBrowserErrorText(options: {
|
|
854
|
-
aborted: boolean;
|
|
855
|
-
envelope?: AgentBrowserEnvelope;
|
|
856
|
-
exitCode: number;
|
|
857
|
-
parseError?: string;
|
|
858
|
-
plainTextInspection: boolean;
|
|
859
|
-
spawnError?: Error;
|
|
860
|
-
stderr: string;
|
|
861
|
-
}): string | undefined {
|
|
862
|
-
const { aborted, envelope, exitCode, parseError, plainTextInspection, spawnError, stderr } = options;
|
|
863
|
-
if (plainTextInspection) return undefined;
|
|
864
|
-
if (parseError) return parseError;
|
|
865
|
-
if (aborted) return "agent-browser was aborted.";
|
|
866
|
-
if (spawnError) return spawnError.message;
|
|
867
|
-
if (envelope?.success === false) {
|
|
868
|
-
return extractEnvelopeErrorText(envelope.error) ?? (stderr.trim() || `agent-browser reported failure${exitCode !== 0 ? ` (exit code ${exitCode})` : "."}`);
|
|
869
|
-
}
|
|
870
|
-
if (exitCode !== 0) {
|
|
871
|
-
return stderr.trim() || `agent-browser exited with code ${exitCode}.`;
|
|
872
|
-
}
|
|
873
|
-
return undefined;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
export async function buildToolPresentation(options: {
|
|
877
|
-
commandInfo: CommandInfo;
|
|
878
|
-
cwd: string;
|
|
879
|
-
envelope?: AgentBrowserEnvelope;
|
|
880
|
-
errorText?: string;
|
|
881
|
-
}): Promise<ToolPresentation> {
|
|
882
|
-
const { commandInfo, cwd, envelope, errorText } = options;
|
|
883
|
-
if (errorText) {
|
|
884
|
-
return {
|
|
885
|
-
content: [{ type: "text", text: errorText }],
|
|
886
|
-
summary: errorText,
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
const data = envelope?.data;
|
|
891
|
-
const summary = formatSummary(commandInfo, data);
|
|
892
|
-
const presentation =
|
|
893
|
-
commandInfo.command === "snapshot" && isRecord(data)
|
|
894
|
-
? await buildSnapshotPresentation(data)
|
|
895
|
-
: {
|
|
896
|
-
content: [{ type: "text" as const, text: formatContentText(commandInfo, data) }],
|
|
897
|
-
data,
|
|
898
|
-
summary,
|
|
899
|
-
};
|
|
900
|
-
|
|
901
|
-
const imagePath = extractImagePath(cwd, data);
|
|
902
|
-
if (!imagePath) {
|
|
903
|
-
return presentation;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
const mimeType = getImageMimeType(imagePath);
|
|
907
|
-
if (!mimeType) {
|
|
908
|
-
return presentation;
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
try {
|
|
912
|
-
const file = await readFile(imagePath);
|
|
913
|
-
presentation.content.push({ type: "image", data: file.toString("base64"), mimeType });
|
|
914
|
-
presentation.imagePath = imagePath;
|
|
915
|
-
return presentation;
|
|
916
|
-
} catch (error) {
|
|
917
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
918
|
-
presentation.content[0] = {
|
|
919
|
-
type: "text",
|
|
920
|
-
text: `${presentation.content[0]?.type === "text" ? presentation.content[0].text : ""}\n\nImage attachment failed: ${message}`,
|
|
921
|
-
};
|
|
922
|
-
presentation.imagePath = imagePath;
|
|
923
|
-
return presentation;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function extractImagePath(cwd: string, data: unknown): string | undefined {
|
|
928
|
-
if (typeof data === "string") {
|
|
929
|
-
const mimeType = getImageMimeType(data);
|
|
930
|
-
return mimeType ? resolve(cwd, data) : undefined;
|
|
931
|
-
}
|
|
932
|
-
if (!isRecord(data) || typeof data.path !== "string") {
|
|
933
|
-
return undefined;
|
|
934
|
-
}
|
|
935
|
-
const mimeType = getImageMimeType(data.path);
|
|
936
|
-
return mimeType ? resolve(cwd, data.path) : undefined;
|
|
937
|
-
}
|
|
9
|
+
export { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "./results/envelope.js";
|
|
10
|
+
export { buildToolPresentation } from "./results/presentation.js";
|
|
11
|
+
export type { AgentBrowserBatchResult, AgentBrowserEnvelope, ToolPresentation } from "./results/shared.js";
|