pi-agent-browser-native 0.2.12 → 0.2.14

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.
@@ -7,24 +7,34 @@
7
7
  */
8
8
 
9
9
  import { readFile, stat } from "node:fs/promises";
10
- import { resolve } from "node:path";
10
+ import { extname, resolve } from "node:path";
11
11
 
12
- import { parseCommandInfo, type CommandInfo } from "../runtime.js";
12
+ import { isRecord, parsePositiveInteger } from "../parsing.js";
13
+ import { parseCommandInfo, redactSensitiveText, redactSensitiveValue, type CommandInfo } from "../runtime.js";
13
14
  import {
15
+ type PersistentSessionArtifactEviction,
14
16
  type PersistentSessionArtifactStore,
15
17
  writePersistentSessionArtifactFile,
16
18
  writeSecureTempFile,
17
19
  } from "../temp.js";
20
+ import { detectConfirmationRequired, type ConfirmationRequiredPresentation } from "./confirmation.js";
18
21
  import { buildSnapshotPresentation, formatRawSnapshotText, formatSnapshotSummary } from "./snapshot.js";
19
22
  import {
20
23
  type AgentBrowserBatchResult,
21
24
  type AgentBrowserEnvelope,
22
25
  type BatchFailurePresentationDetails,
23
26
  type BatchStepPresentationDetails,
27
+ type ArtifactStorageScope,
28
+ type FileArtifactKind,
29
+ type FileArtifactMetadata,
30
+ type SavedFilePresentationDetails,
31
+ type SessionArtifactManifest,
32
+ type SessionArtifactManifestEntry,
24
33
  type ToolPresentation,
25
- isRecord,
34
+ buildEvictedSessionArtifactEntries,
26
35
  countLines,
27
- parsePositiveInteger,
36
+ formatSessionArtifactRetentionSummary,
37
+ mergeSessionArtifactManifest,
28
38
  stringifyUnknown,
29
39
  truncateText,
30
40
  } from "./shared.js";
@@ -46,6 +56,16 @@ const LARGE_OUTPUT_INLINE_MAX_LINES = 120;
46
56
  const LARGE_OUTPUT_PREVIEW_MAX_CHARS = 2_500;
47
57
  const LARGE_OUTPUT_PREVIEW_MAX_LINES = 40;
48
58
  const LARGE_OUTPUT_FILE_PREFIX = "pi-agent-browser-output";
59
+ const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
60
+ const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
61
+ const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
62
+ const NETWORK_ERROR_PREVIEW_MAX_CHARS = 220;
63
+ const NETWORK_PREVIEW_FIELD_CANDIDATES = {
64
+ request: ["postData"] as const,
65
+ response: ["responseBody"] as const,
66
+ error: ["error", "failureText", "errorText"] as const,
67
+ };
68
+ const AUTH_SHOW_SAFE_FIELDS = ["name", "profile", "url", "username", "createdAt", "updatedAt"] as const;
49
69
 
50
70
  interface NavigationSummary {
51
71
  title?: string;
@@ -53,7 +73,7 @@ interface NavigationSummary {
53
73
  }
54
74
 
55
75
  function getImageMimeType(filePath: string): string | undefined {
56
- const extension = filePath.toLowerCase().slice(filePath.lastIndexOf("."));
76
+ const extension = extname(filePath).toLowerCase();
57
77
  return IMAGE_EXTENSION_TO_MIME_TYPE[extension];
58
78
  }
59
79
 
@@ -75,6 +95,50 @@ function appendPresentationNotice(presentation: ToolPresentation, message: strin
75
95
  };
76
96
  }
77
97
 
98
+ function shouldAppendArtifactRetentionNotice(entries: SessionArtifactManifestEntry[]): boolean {
99
+ return entries.some((entry) => entry.retentionState === "evicted" || entry.storageScope !== "explicit-path");
100
+ }
101
+
102
+ function getManifestEntryKey(entry: SessionArtifactManifestEntry): string {
103
+ return entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
104
+ }
105
+
106
+ function manifestHasNewNoticeWorthyEntries(base: SessionArtifactManifest | undefined, current: SessionArtifactManifest | undefined): boolean {
107
+ if (!current) return false;
108
+ const baseKeys = new Set((base?.entries ?? []).map(getManifestEntryKey));
109
+ return current.entries.some((entry) => !baseKeys.has(getManifestEntryKey(entry)) && (entry.retentionState === "evicted" || entry.storageScope !== "explicit-path"));
110
+ }
111
+
112
+ function applyArtifactManifest(presentation: ToolPresentation, baseManifest: SessionArtifactManifest | undefined, entries: SessionArtifactManifestEntry[]): ToolPresentation {
113
+ if (entries.length === 0) return presentation;
114
+ const artifactManifest = mergeSessionArtifactManifest({ base: baseManifest, entries });
115
+ if (!artifactManifest) return presentation;
116
+ presentation.artifactManifest = artifactManifest;
117
+ presentation.artifactRetentionSummary = formatSessionArtifactRetentionSummary(artifactManifest);
118
+ if (shouldAppendArtifactRetentionNotice(entries)) {
119
+ appendPresentationNotice(presentation, presentation.artifactRetentionSummary);
120
+ }
121
+ return presentation;
122
+ }
123
+
124
+ function stringifyModelFacing(value: unknown): string {
125
+ return stringifyUnknown(redactSensitiveValue(value));
126
+ }
127
+
128
+ function redactModelFacingText(text: string): string {
129
+ const parsed = parseJsonPreviewString(text);
130
+ if (parsed !== text) {
131
+ return stringifyModelFacing(parsed);
132
+ }
133
+ return redactSensitiveText(text);
134
+ }
135
+
136
+ function redactModelFacingTextIfSensitive(text: string): string {
137
+ return /(?:@|\b(?:api[_-]?key|auth|authorization|basic|bearer|cookie|pass(?:word)?|secret|session[_-]?id|token)\b)/i.test(text)
138
+ ? redactModelFacingText(text)
139
+ : text;
140
+ }
141
+
78
142
  function getTabSummary(data: Record<string, unknown>): string | undefined {
79
143
  const tabs = Array.isArray(data.tabs) ? data.tabs : undefined;
80
144
  if (!tabs) return undefined;
@@ -113,6 +177,397 @@ function getStreamSummary(data: Record<string, unknown>): string | undefined {
113
177
  return lines.join("\n");
114
178
  }
115
179
 
180
+ function getArrayField(data: Record<string, unknown>, key: string): unknown[] | undefined {
181
+ return Array.isArray(data[key]) ? data[key] : undefined;
182
+ }
183
+
184
+ function getStringField(data: Record<string, unknown>, key: string): string | undefined {
185
+ const value = data[key];
186
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
187
+ }
188
+
189
+ function formatCount(count: number, singular: string, plural = `${singular}s`): string {
190
+ return `${count} ${count === 1 ? singular : plural}`;
191
+ }
192
+
193
+ function firstLine(value: string, maxChars = 160): string {
194
+ return truncateText(value.split("\n", 1)[0] ?? value, maxChars);
195
+ }
196
+
197
+ function formatDiagnosticSummary(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
198
+ if (commandInfo.command === "session") {
199
+ const sessions = getArrayField(data, "sessions");
200
+ if (sessions) return `Sessions: ${sessions.length}`;
201
+ const session = getStringField(data, "session");
202
+ if (session) return `Session: ${session}`;
203
+ }
204
+
205
+ if (commandInfo.command === "profiles") {
206
+ const profiles = getArrayField(data, "profiles");
207
+ if (profiles) return `Chrome profiles: ${profiles.length}`;
208
+ }
209
+
210
+ if (commandInfo.command === "auth") {
211
+ const profiles = getArrayField(data, "profiles");
212
+ if (profiles) return `Auth profiles: ${profiles.length}`;
213
+ const name = getStringField(data, "name") ?? getStringField(data, "profile") ?? commandInfo.subcommand;
214
+ if (name && commandInfo.subcommand === "show") return `Auth profile: ${name}`;
215
+ }
216
+
217
+ if (commandInfo.command === "network" && commandInfo.subcommand === "requests") {
218
+ const requests = getArrayField(data, "requests");
219
+ if (requests) return `Network requests: ${requests.length}`;
220
+ }
221
+
222
+ if (commandInfo.command === "console") {
223
+ const messages = getArrayField(data, "messages");
224
+ if (messages) return `Console messages: ${messages.length}`;
225
+ }
226
+
227
+ if (commandInfo.command === "errors") {
228
+ const errors = getArrayField(data, "errors");
229
+ if (errors) return `Page errors: ${errors.length}`;
230
+ }
231
+
232
+ if (commandInfo.command === "dashboard") {
233
+ if (typeof data.port === "number") return `Dashboard running on port ${data.port}`;
234
+ if (data.stopped === true) return "Dashboard stopped";
235
+ if (data.stopped === false) {
236
+ const reason = getStringField(data, "reason");
237
+ return reason ? `Dashboard not stopped: ${reason}` : "Dashboard not stopped";
238
+ }
239
+ }
240
+
241
+ if (commandInfo.command === "doctor") {
242
+ const status = getStringField(data, "status") ?? getStringField(data, "result");
243
+ if (status) return `Doctor: ${status}`;
244
+ const checks = getArrayField(data, "checks") ?? getArrayField(data, "issues") ?? getArrayField(data, "problems");
245
+ if (checks) return `Doctor: ${formatCount(checks.length, "item")}`;
246
+ }
247
+
248
+ return undefined;
249
+ }
250
+
251
+ function formatSessionText(data: Record<string, unknown>): string | undefined {
252
+ const sessions = getArrayField(data, "sessions");
253
+ if (sessions) {
254
+ if (sessions.length === 0) return "No active sessions.";
255
+ return sessions
256
+ .map((item, index) => {
257
+ if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
258
+ const name = redactModelFacingText(getStringField(item, "name") ?? getStringField(item, "session") ?? getStringField(item, "id") ?? `(session ${index + 1})`);
259
+ const active = item.active === true ? " *active*" : "";
260
+ const details = [getStringField(item, "url"), getStringField(item, "title")]
261
+ .flatMap((detail) => (detail ? [redactModelFacingTextIfSensitive(detail)] : []))
262
+ .join(" — ");
263
+ return details ? `${index + 1}. ${name}${active} — ${details}` : `${index + 1}. ${name}${active}`;
264
+ })
265
+ .join("\n");
266
+ }
267
+ const session = getStringField(data, "session");
268
+ return session ? `Current session: ${redactModelFacingText(session)}` : undefined;
269
+ }
270
+
271
+ function formatProfilesText(profiles: unknown[], label: string): string {
272
+ if (profiles.length === 0) return `No ${label}.`;
273
+ return profiles
274
+ .map((item, index) => {
275
+ if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
276
+ const name = redactModelFacingText(getStringField(item, "name") ?? getStringField(item, "profile") ?? `(unnamed ${index + 1})`);
277
+ const directory = getStringField(item, "directory") ?? getStringField(item, "path");
278
+ return directory ? `${index + 1}. ${name} (${redactModelFacingText(directory)})` : `${index + 1}. ${name}`;
279
+ })
280
+ .join("\n");
281
+ }
282
+
283
+ function formatSkillsListText(skills: unknown[]): string {
284
+ if (skills.length === 0) return "No agent-browser skills found.";
285
+ return skills
286
+ .map((item, index) => {
287
+ if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
288
+ const name = redactModelFacingText(getStringField(item, "name") ?? `(skill ${index + 1})`);
289
+ const description = getStringField(item, "description");
290
+ return description ? `${index + 1}. ${name} — ${redactModelFacingText(description)}` : `${index + 1}. ${name}`;
291
+ })
292
+ .join("\n");
293
+ }
294
+
295
+ function getSkillContent(data: unknown): string | undefined {
296
+ if (typeof data === "string") return data;
297
+ if (isRecord(data) && typeof data.content === "string") return data.content;
298
+ if (!Array.isArray(data)) return undefined;
299
+ const content = data.flatMap((item) => (isRecord(item) && typeof item.content === "string" ? [item.content] : []));
300
+ return content.length > 0 ? content.join("\n\n") : undefined;
301
+ }
302
+
303
+ function splitShellWords(input: string): string[] | undefined {
304
+ const words: string[] = [];
305
+ let current = "";
306
+ let quote: 'single' | 'double' | undefined;
307
+ for (let index = 0; index < input.length; index += 1) {
308
+ const char = input[index];
309
+ if (quote === "single") {
310
+ if (char === "'") quote = undefined;
311
+ else current += char;
312
+ continue;
313
+ }
314
+ if (quote === "double") {
315
+ if (char === '"') quote = undefined;
316
+ else if (char === "\\" && index + 1 < input.length) {
317
+ index += 1;
318
+ current += input[index];
319
+ } else current += char;
320
+ continue;
321
+ }
322
+ if (char === "'") {
323
+ quote = "single";
324
+ continue;
325
+ }
326
+ if (char === '"') {
327
+ quote = "double";
328
+ continue;
329
+ }
330
+ if (char === "\\" && index + 1 < input.length) {
331
+ index += 1;
332
+ current += input[index];
333
+ continue;
334
+ }
335
+ if (/\s/.test(char)) {
336
+ if (current.length > 0) {
337
+ words.push(current);
338
+ current = "";
339
+ }
340
+ continue;
341
+ }
342
+ current += char;
343
+ }
344
+ if (quote) return undefined;
345
+ if (current.length > 0) words.push(current);
346
+ return words;
347
+ }
348
+
349
+ function formatNativeAgentBrowserCall(args: string[], stdin?: string): string {
350
+ return stdin === undefined
351
+ ? `agent_browser { "args": ${JSON.stringify(args)} }`
352
+ : `agent_browser { "args": ${JSON.stringify(args)}, "stdin": ${JSON.stringify(stdin)} }`;
353
+ }
354
+
355
+ function formatNativeSkillContent(content: string): string {
356
+ const lines = content.replace(/^allowed-tools:.*agent-browser.*\n?/gim, "").replace(/^```bash\s*$/gim, "```text").split("\n");
357
+ const output: string[] = [];
358
+ for (let index = 0; index < lines.length; index += 1) {
359
+ const line = lines[index];
360
+ const commandMatch = /^(\s*)agent-browser\s+(.+?)\s*$/.exec(line);
361
+ if (!commandMatch) {
362
+ output.push(line);
363
+ continue;
364
+ }
365
+ const indent = commandMatch[1];
366
+ const rawArgsText = commandMatch[2];
367
+ const heredocMatch = /^(.*?)\s+(<<-?)['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?\s*$/.exec(rawArgsText);
368
+ const argsText = heredocMatch?.[1] ?? rawArgsText;
369
+ const args = splitShellWords(argsText);
370
+ if (!args) {
371
+ output.push(line);
372
+ continue;
373
+ }
374
+ if (!heredocMatch) {
375
+ output.push(`${indent}${formatNativeAgentBrowserCall(args)}`);
376
+ continue;
377
+ }
378
+ const stripsLeadingTabs = heredocMatch[2] === "<<-";
379
+ const delimiter = heredocMatch[3];
380
+ const stdinLines: string[] = [];
381
+ let cursor = index + 1;
382
+ while (cursor < lines.length) {
383
+ const candidate = stripsLeadingTabs ? lines[cursor].replace(/^\t+/, "") : lines[cursor];
384
+ if (candidate === delimiter) break;
385
+ stdinLines.push(candidate);
386
+ cursor += 1;
387
+ }
388
+ if (cursor >= lines.length) {
389
+ output.push(line);
390
+ continue;
391
+ }
392
+ output.push(`${indent}${formatNativeAgentBrowserCall(args, stdinLines.join("\n"))}`);
393
+ index = cursor;
394
+ }
395
+ return output.join("\n");
396
+ }
397
+
398
+ function formatSkillsText(commandInfo: CommandInfo, data: unknown): string | undefined {
399
+ if (commandInfo.command !== "skills") return undefined;
400
+ if (commandInfo.subcommand === "list" && Array.isArray(data)) return formatSkillsListText(data);
401
+ const content = getSkillContent(data);
402
+ if (content) {
403
+ const note = [
404
+ "Pi native-tool note: upstream skill text was adapted for this native tool.",
405
+ "Use args for CLI tokens and stdin only for batch or eval --stdin; do not pipe heredocs through bash unless the user explicitly asks for a bash workflow.",
406
+ ].join("\n");
407
+ return `${note}\n\n${redactModelFacingText(formatNativeSkillContent(content))}`;
408
+ }
409
+ if (typeof data === "string") return redactModelFacingText(formatNativeSkillContent(data));
410
+ return undefined;
411
+ }
412
+
413
+ function formatAuthShowText(data: Record<string, unknown>): string | undefined {
414
+ const lines = AUTH_SHOW_SAFE_FIELDS.flatMap((key) => {
415
+ const value = data[key];
416
+ return typeof value === "string" && value.trim().length > 0 ? [`${key}: ${redactModelFacingText(value.trim())}`] : [];
417
+ });
418
+ return lines.length > 0 ? lines.join("\n") : undefined;
419
+ }
420
+
421
+ function getPreviewCandidate(item: Record<string, unknown>, keys: readonly string[]): unknown {
422
+ for (const key of keys) {
423
+ const value = item[key];
424
+ if (value !== undefined && value !== null && value !== "") return value;
425
+ }
426
+ return undefined;
427
+ }
428
+
429
+ function parseJsonPreviewString(value: string): unknown {
430
+ const trimmed = value.trim();
431
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
432
+ try {
433
+ return JSON.parse(trimmed) as unknown;
434
+ } catch {
435
+ return value;
436
+ }
437
+ }
438
+
439
+ function formatNetworkPreviewValue(value: unknown, maxChars: number): string | undefined {
440
+ if (value === undefined || value === null) return undefined;
441
+ const previewValue = typeof value === "string" ? parseJsonPreviewString(value) : value;
442
+ const redacted = redactSensitiveValue(previewValue);
443
+ const raw = typeof redacted === "string" ? redacted : stringifyUnknown(redacted);
444
+ const normalized = raw.replace(/\s+/g, " ").trim();
445
+ if (normalized.length === 0) return undefined;
446
+ return truncateText(redactSensitiveText(normalized), maxChars);
447
+ }
448
+
449
+ function appendNetworkPreview(lines: string[], label: string, value: unknown, maxChars: number): void {
450
+ const preview = formatNetworkPreviewValue(value, maxChars);
451
+ if (!preview) return;
452
+ lines.push(` ${label}: ${preview}`);
453
+ }
454
+
455
+ function formatNetworkRequestLine(item: Record<string, unknown>, index: number): string[] {
456
+ const method = getStringField(item, "method") ?? "GET";
457
+ const status = typeof item.status === "number" ? String(item.status) : "pending";
458
+ const type = getStringField(item, "resourceType") ?? getStringField(item, "mimeType");
459
+ const url = getStringField(item, "url") ?? "(no url)";
460
+ const requestId = getStringField(item, "requestId") ?? getStringField(item, "id");
461
+ const idText = requestId ? ` [${redactSensitiveText(requestId)}]` : "";
462
+ const lines = [`${index + 1}. ${status} ${method} ${truncateText(redactSensitiveText(url), 180)}${type ? ` (${type})` : ""}${idText}`];
463
+ appendNetworkPreview(lines, "Payload", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.request), NETWORK_BODY_PREVIEW_MAX_CHARS);
464
+ appendNetworkPreview(lines, "Response", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.response), NETWORK_BODY_PREVIEW_MAX_CHARS);
465
+ appendNetworkPreview(lines, "Error", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.error), NETWORK_ERROR_PREVIEW_MAX_CHARS);
466
+ return lines;
467
+ }
468
+
469
+ function formatNetworkRequestsText(data: Record<string, unknown>): string | undefined {
470
+ const requests = getArrayField(data, "requests");
471
+ if (!requests) return undefined;
472
+ if (requests.length === 0) return "No network requests captured.";
473
+ const shown = requests.slice(0, DIAGNOSTIC_REQUEST_PREVIEW_LIMIT).flatMap((item, index) => {
474
+ if (!isRecord(item)) return [`${index + 1}. ${stringifyModelFacing(item)}`];
475
+ return formatNetworkRequestLine(item, index);
476
+ });
477
+ if (requests.length > DIAGNOSTIC_REQUEST_PREVIEW_LIMIT) {
478
+ shown.push(`... (${requests.length - DIAGNOSTIC_REQUEST_PREVIEW_LIMIT} additional requests omitted from preview)`);
479
+ }
480
+ return shown.join("\n");
481
+ }
482
+
483
+ function formatNetworkRequestText(data: Record<string, unknown>): string | undefined {
484
+ if (!getStringField(data, "url") && !getStringField(data, "requestId") && !getStringField(data, "id")) {
485
+ return undefined;
486
+ }
487
+ return formatNetworkRequestLine(data, 0).join("\n");
488
+ }
489
+
490
+ function formatConsoleText(data: Record<string, unknown>): string | undefined {
491
+ const messages = getArrayField(data, "messages");
492
+ if (!messages) return undefined;
493
+ if (messages.length === 0) return "No console messages.";
494
+ const shown = messages.slice(0, DIAGNOSTIC_LOG_PREVIEW_LIMIT).map((item, index) => {
495
+ if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
496
+ const type = redactModelFacingText(getStringField(item, "type") ?? "message");
497
+ const text = getStringField(item, "text") ?? stringifyModelFacing(item);
498
+ return `${index + 1}. [${type}] ${firstLine(redactModelFacingText(text).replace(/\s+/g, " ").trim(), 220)}`;
499
+ });
500
+ if (messages.length > shown.length) {
501
+ shown.push(`... (${messages.length - shown.length} additional console messages omitted from preview)`);
502
+ }
503
+ return shown.join("\n");
504
+ }
505
+
506
+ function formatErrorsText(data: Record<string, unknown>): string | undefined {
507
+ const errors = getArrayField(data, "errors");
508
+ if (!errors) return undefined;
509
+ if (errors.length === 0) return "No page errors.";
510
+ const shown = errors.slice(0, DIAGNOSTIC_LOG_PREVIEW_LIMIT).map((item, index) => {
511
+ if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
512
+ const text = getStringField(item, "text") ?? stringifyModelFacing(item);
513
+ const location = [
514
+ getStringField(item, "url"),
515
+ typeof item.line === "number" ? `line ${item.line}` : undefined,
516
+ typeof item.column === "number" ? `column ${item.column}` : undefined,
517
+ ]
518
+ .filter(Boolean)
519
+ .map((item) => redactModelFacingText(String(item)))
520
+ .join(":");
521
+ const safeText = firstLine(redactModelFacingText(text), 220);
522
+ return location ? `${index + 1}. ${safeText} (${location})` : `${index + 1}. ${safeText}`;
523
+ });
524
+ if (errors.length > shown.length) {
525
+ shown.push(`... (${errors.length - shown.length} additional errors omitted from preview)`);
526
+ }
527
+ return shown.join("\n");
528
+ }
529
+
530
+ function formatDashboardText(data: Record<string, unknown>): string | undefined {
531
+ const lines: string[] = [];
532
+ if (typeof data.port === "number") lines.push(`Port: ${data.port}`);
533
+ if (typeof data.pid === "number") lines.push(`PID: ${data.pid}`);
534
+ if (typeof data.stopped === "boolean") lines.push(`Stopped: ${data.stopped}`);
535
+ const reason = getStringField(data, "reason");
536
+ if (reason) lines.push(`Reason: ${redactModelFacingText(reason)}`);
537
+ return lines.length > 0 ? lines.join("\n") : undefined;
538
+ }
539
+
540
+ function formatDoctorText(data: Record<string, unknown>): string | undefined {
541
+ const lines: string[] = [];
542
+ const status = getStringField(data, "status") ?? getStringField(data, "result");
543
+ if (status) lines.push(`Status: ${redactModelFacingText(status)}`);
544
+ for (const key of ["checks", "issues", "problems"] as const) {
545
+ const items = getArrayField(data, key);
546
+ if (items) lines.push(`${key}: ${items.length}`);
547
+ }
548
+ return lines.length > 0 ? lines.join("\n") : undefined;
549
+ }
550
+
551
+ function formatDiagnosticText(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
552
+ if (commandInfo.command === "session") return formatSessionText(data);
553
+ if (commandInfo.command === "profiles") {
554
+ const profiles = getArrayField(data, "profiles");
555
+ if (profiles) return formatProfilesText(profiles, "Chrome profiles");
556
+ }
557
+ if (commandInfo.command === "auth") {
558
+ const profiles = getArrayField(data, "profiles");
559
+ if (profiles) return formatProfilesText(profiles, "auth profiles");
560
+ if (commandInfo.subcommand === "show") return formatAuthShowText(data);
561
+ }
562
+ if (commandInfo.command === "network" && commandInfo.subcommand === "requests") return formatNetworkRequestsText(data);
563
+ if (commandInfo.command === "network" && commandInfo.subcommand === "request") return formatNetworkRequestText(data);
564
+ if (commandInfo.command === "console") return formatConsoleText(data);
565
+ if (commandInfo.command === "errors") return formatErrorsText(data);
566
+ if (commandInfo.command === "dashboard") return formatDashboardText(data);
567
+ if (commandInfo.command === "doctor") return formatDoctorText(data);
568
+ return undefined;
569
+ }
570
+
116
571
  function getPageSummary(data: Record<string, unknown>): string | undefined {
117
572
  const title = typeof data.title === "string" ? data.title : undefined;
118
573
  const url = typeof data.url === "string" ? data.url : undefined;
@@ -121,21 +576,240 @@ function getPageSummary(data: Record<string, unknown>): string | undefined {
121
576
  return title ?? url;
122
577
  }
123
578
 
579
+ function formatConfirmationRequiredSummary(confirmation: ConfirmationRequiredPresentation): string {
580
+ return `Confirmation required: ${confirmation.id}`;
581
+ }
582
+
583
+ function formatConfirmationRequiredText(confirmation: ConfirmationRequiredPresentation): string {
584
+ const lines = [
585
+ "Confirmation required.",
586
+ `Pending confirmation id: ${confirmation.id}`,
587
+ ];
588
+ if (confirmation.actionText) {
589
+ lines.push(`Action: ${confirmation.actionText}`);
590
+ }
591
+ lines.push(
592
+ "",
593
+ "Next steps:",
594
+ `- Approve: { "args": ["confirm", "${confirmation.id}"] }`,
595
+ `- Deny: { "args": ["deny", "${confirmation.id}"] }`,
596
+ );
597
+ return lines.join("\n");
598
+ }
599
+
124
600
  function getScreenshotSummary(data: Record<string, unknown>): string | undefined {
125
601
  return typeof data.path === "string" ? `Saved image: ${data.path}` : undefined;
126
602
  }
127
603
 
128
- function getSavedFileSummary(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
129
- if (typeof data.path !== "string") {
604
+ const PATH_FIELD_CANDIDATES = [
605
+ "path",
606
+ "file",
607
+ "filePath",
608
+ "outputPath",
609
+ "downloadPath",
610
+ "harPath",
611
+ "tracePath",
612
+ "profilePath",
613
+ "videoPath",
614
+ ] as const;
615
+
616
+ const ARTIFACT_EXTENSION_TO_MEDIA_TYPE: Record<string, string> = {
617
+ ".cpuprofile": "application/json",
618
+ ".har": "application/json",
619
+ ".html": "text/html",
620
+ ".json": "application/json",
621
+ ".pdf": "application/pdf",
622
+ ".txt": "text/plain",
623
+ ".webm": "video/webm",
624
+ ".zip": "application/zip",
625
+ ...IMAGE_EXTENSION_TO_MIME_TYPE,
626
+ };
627
+
628
+ function getArtifactKind(commandInfo: CommandInfo): FileArtifactKind | undefined {
629
+ if (commandInfo.command === "screenshot") return "image";
630
+ if (commandInfo.command === "pdf") return "pdf";
631
+ if (commandInfo.command === "download") return "download";
632
+ if (commandInfo.command === "wait" && commandInfo.subcommand === "--download") return "download";
633
+ if (commandInfo.command === "trace") return "trace";
634
+ if (commandInfo.command === "profiler") return "profile";
635
+ if (commandInfo.command === "record") return "video";
636
+ if (commandInfo.command === "network" && commandInfo.subcommand === "har") return "har";
637
+ return undefined;
638
+ }
639
+
640
+ function extractPathStrings(data: unknown): string[] {
641
+ if (typeof data === "string") {
642
+ return data.trim().length > 0 ? [data] : [];
643
+ }
644
+ if (!isRecord(data)) {
645
+ return [];
646
+ }
647
+
648
+ const paths: string[] = [];
649
+ for (const key of PATH_FIELD_CANDIDATES) {
650
+ const value = data[key];
651
+ if (typeof value === "string" && value.trim().length > 0) {
652
+ paths.push(value);
653
+ }
654
+ if (Array.isArray(value)) {
655
+ for (const item of value) {
656
+ if (typeof item === "string" && item.trim().length > 0) {
657
+ paths.push(item);
658
+ }
659
+ }
660
+ }
661
+ }
662
+ return [...new Set(paths)];
663
+ }
664
+
665
+ async function buildFileArtifactMetadata(options: {
666
+ commandInfo: CommandInfo;
667
+ cwd: string;
668
+ path: string;
669
+ }): Promise<FileArtifactMetadata | undefined> {
670
+ const kind = getArtifactKind(options.commandInfo);
671
+ if (!kind) {
130
672
  return undefined;
131
673
  }
132
- if (commandInfo.command === "download") {
133
- return `Downloaded file: ${data.path}`;
674
+
675
+ const absolutePath = resolve(options.cwd, options.path);
676
+ const extension = extname(options.path).toLowerCase() || undefined;
677
+ let exists: boolean | undefined;
678
+ let sizeBytes: number | undefined;
679
+ try {
680
+ const fileStats = await stat(absolutePath);
681
+ exists = true;
682
+ sizeBytes = fileStats.size;
683
+ } catch {
684
+ exists = false;
685
+ }
686
+
687
+ return {
688
+ absolutePath,
689
+ command: options.commandInfo.command,
690
+ exists,
691
+ extension,
692
+ kind,
693
+ mediaType: extension ? ARTIFACT_EXTENSION_TO_MEDIA_TYPE[extension] : undefined,
694
+ path: options.path,
695
+ sizeBytes,
696
+ subcommand: options.commandInfo.subcommand,
697
+ };
698
+ }
699
+
700
+ async function extractFileArtifacts(commandInfo: CommandInfo, cwd: string, data: unknown): Promise<FileArtifactMetadata[]> {
701
+ const candidates = extractPathStrings(data);
702
+ const artifacts = await Promise.all(candidates.map((path) => buildFileArtifactMetadata({ commandInfo, cwd, path })));
703
+ return artifacts.filter((artifact): artifact is FileArtifactMetadata => artifact !== undefined);
704
+ }
705
+
706
+ function buildManifestEntriesForFileArtifacts(artifacts: FileArtifactMetadata[], nowMs = Date.now()): SessionArtifactManifestEntry[] {
707
+ return artifacts.map((artifact) => ({
708
+ absolutePath: artifact.absolutePath,
709
+ command: artifact.command,
710
+ createdAtMs: nowMs,
711
+ exists: artifact.exists,
712
+ extension: artifact.extension,
713
+ kind: artifact.kind,
714
+ mediaType: artifact.mediaType,
715
+ path: artifact.path,
716
+ retentionState: artifact.exists === false ? "missing" : "live",
717
+ sizeBytes: artifact.sizeBytes,
718
+ storageScope: "explicit-path",
719
+ subcommand: artifact.subcommand,
720
+ }));
721
+ }
722
+
723
+ function isRecordingStartArtifact(artifact: FileArtifactMetadata): boolean {
724
+ return artifact.command === "record" && artifact.subcommand === "start" && artifact.kind === "video";
725
+ }
726
+
727
+ function isManifestFileArtifact(artifact: FileArtifactMetadata): boolean {
728
+ return !isRecordingStartArtifact(artifact);
729
+ }
730
+
731
+ function formatArtifactLabel(artifact: FileArtifactMetadata): string {
732
+ switch (artifact.kind) {
733
+ case "download":
734
+ return artifact.command === "wait" && artifact.subcommand === "--download" ? "Download completed" : "Downloaded file";
735
+ case "file":
736
+ return "Saved file";
737
+ case "har":
738
+ return "Saved HAR";
739
+ case "image":
740
+ return "Saved image";
741
+ case "pdf":
742
+ return "Saved PDF";
743
+ case "profile":
744
+ return "Saved profile";
745
+ case "trace":
746
+ return "Saved trace";
747
+ case "video":
748
+ return isRecordingStartArtifact(artifact) ? "Recording started; output will be written on stop" : "Saved recording";
134
749
  }
135
- if (commandInfo.command === "pdf") {
136
- return `Saved PDF: ${data.path}`;
750
+ }
751
+
752
+ function formatArtifactSummary(artifacts: FileArtifactMetadata[]): string | undefined {
753
+ if (artifacts.length === 0) {
754
+ return undefined;
137
755
  }
138
- return undefined;
756
+ if (artifacts.length === 1) {
757
+ const artifact = artifacts[0];
758
+ return `${formatArtifactLabel(artifact)}: ${artifact.path}`;
759
+ }
760
+ return `Saved ${artifacts.length} artifacts: ${artifacts.map((artifact) => `${artifact.kind} ${artifact.path}`).join(", ")}`;
761
+ }
762
+
763
+ function formatArtifactMetadataLines(artifacts: FileArtifactMetadata[]): string[] {
764
+ return artifacts.map((artifact) => {
765
+ if (isRecordingStartArtifact(artifact)) {
766
+ return `${formatArtifactLabel(artifact)}: ${artifact.path}`;
767
+ }
768
+
769
+ const suffix = [
770
+ artifact.mediaType,
771
+ typeof artifact.sizeBytes === "number" ? formatByteCount(artifact.sizeBytes) : undefined,
772
+ artifact.exists === false ? "not found on disk" : undefined,
773
+ ].filter((item): item is string => item !== undefined).join(", ");
774
+ return suffix ? `${formatArtifactLabel(artifact)}: ${artifact.path} (${suffix})` : `${formatArtifactLabel(artifact)}: ${artifact.path}`;
775
+ });
776
+ }
777
+
778
+ function isDownloadWaitCommand(commandInfo: CommandInfo): boolean {
779
+ return commandInfo.command === "wait" && commandInfo.subcommand === "--download";
780
+ }
781
+
782
+ function extractSavedFilePath(data: Record<string, unknown>): string | undefined {
783
+ return typeof data.path === "string" && data.path.trim().length > 0 ? data.path : undefined;
784
+ }
785
+
786
+ function getSavedFileDetails(commandInfo: CommandInfo, data: Record<string, unknown>): SavedFilePresentationDetails | undefined {
787
+ const path = extractSavedFilePath(data);
788
+ if (!path) {
789
+ return undefined;
790
+ }
791
+ const savedFileCommand = isDownloadWaitCommand(commandInfo)
792
+ ? "wait"
793
+ : commandInfo.command === "download" || commandInfo.command === "pdf"
794
+ ? commandInfo.command
795
+ : undefined;
796
+ if (!savedFileCommand) {
797
+ return undefined;
798
+ }
799
+
800
+ const { path: _path, ...metadata } = data;
801
+ const details: SavedFilePresentationDetails = {
802
+ command: savedFileCommand,
803
+ kind: savedFileCommand === "pdf" ? "pdf" : "download",
804
+ path,
805
+ };
806
+ if (Object.keys(metadata).length > 0) {
807
+ details.metadata = metadata;
808
+ }
809
+ if (commandInfo.subcommand) {
810
+ details.subcommand = commandInfo.subcommand;
811
+ }
812
+ return details;
139
813
  }
140
814
 
141
815
  function getScalarExtractionResult(data: Record<string, unknown>): string | undefined {
@@ -174,11 +848,13 @@ function formatExtractionSummary(commandInfo: CommandInfo, data: Record<string,
174
848
  if (!scalarResult) {
175
849
  return undefined;
176
850
  }
851
+ const safeScalarResult = redactModelFacingText(scalarResult);
852
+ const firstResultLine = safeScalarResult.split("\n", 1)[0] ?? safeScalarResult;
177
853
  if (commandInfo.command === "get") {
178
- return `${formatGetSummaryLabel(commandInfo.subcommand)}: ${scalarResult.split("\n", 1)[0] ?? scalarResult}`;
854
+ return `${formatGetSummaryLabel(commandInfo.subcommand)}: ${firstResultLine}`;
179
855
  }
180
856
  if (commandInfo.command === "eval") {
181
- return `Eval result: ${scalarResult.split("\n", 1)[0] ?? scalarResult}`;
857
+ return `Eval result: ${firstResultLine}`;
182
858
  }
183
859
  return undefined;
184
860
  }
@@ -192,7 +868,9 @@ function formatExtractionText(commandInfo: CommandInfo, data: Record<string, unk
192
868
  return undefined;
193
869
  }
194
870
  const origin = getExtractionOrigin(data);
195
- return origin && origin !== scalarResult ? `${scalarResult}\n\nOrigin: ${origin}` : scalarResult;
871
+ const safeScalarResult = redactModelFacingText(scalarResult);
872
+ const safeOrigin = origin ? redactModelFacingText(origin) : undefined;
873
+ return safeOrigin && safeOrigin !== safeScalarResult ? `${safeScalarResult}\n\nOrigin: ${safeOrigin}` : safeScalarResult;
196
874
  }
197
875
 
198
876
  function isNavigationObservableCommand(command: string | undefined): boolean {
@@ -228,7 +906,7 @@ function formatNavigationActionResult(data: Record<string, unknown>): string | u
228
906
  lines.push(`Clicked: ${String(actionData.clicked)}`);
229
907
  }
230
908
  if (typeof actionData.href === "string") {
231
- lines.push(`Href: ${actionData.href}`);
909
+ lines.push(`Href: ${redactModelFacingText(actionData.href)}`);
232
910
  }
233
911
  if (typeof actionData.navigated === "boolean") {
234
912
  lines.push(`Navigated: ${actionData.navigated}`);
@@ -237,7 +915,7 @@ function formatNavigationActionResult(data: Record<string, unknown>): string | u
237
915
  return lines.join("\n");
238
916
  }
239
917
 
240
- const actionText = stringifyUnknown(actionData).trim();
918
+ const actionText = stringifyModelFacing(actionData).trim();
241
919
  if (actionText.length === 0 || actionText === "{}") {
242
920
  return undefined;
243
921
  }
@@ -273,9 +951,57 @@ function formatBatchStepCommand(command: string[] | undefined, index: number): s
273
951
  return command && command.length > 0 ? command.join(" ") : `step-${index + 1}`;
274
952
  }
275
953
 
954
+ const STALE_REF_ERROR_HINT = [
955
+ "Agent-browser hint: This ref may be stale after navigation, scrolling, or re-rendering.",
956
+ "Run `snapshot -i` again and retry with a current `@e…` ref; for less ref churn, use `find role|text|label|placeholder|alt|title|testid ...` or `scrollintoview` before interacting with off-screen elements.",
957
+ ].join(" ");
958
+
959
+ const SELECTOR_DIALECT_ERROR_HINT = [
960
+ "Agent-browser hint: This selector may use an unsupported selector dialect.",
961
+ "Prefer refs from `snapshot -i`, or use supported `find role|text|label|placeholder|alt|title|testid ...` locators; use `scrollintoview` before interacting with off-screen elements.",
962
+ ].join(" ");
963
+
964
+ function getSelectorRecoveryHint(errorText: string): string | undefined {
965
+ const normalized = errorText.trim();
966
+ if (normalized.length === 0) {
967
+ return undefined;
968
+ }
969
+
970
+ if (/\bUnknown ref\b|\bstale ref\b|\bref\b.*\b(?:not found|missing|expired)\b/i.test(normalized)) {
971
+ return STALE_REF_ERROR_HINT;
972
+ }
973
+
974
+ const mentionsPlaywrightSelectorDialect = /(?:\btext=|:has-text\(|\bgetByRole\b|\bgetByText\b)/i.test(normalized);
975
+ const reportsSelectorMatchFailure =
976
+ /\b(?:no elements? found|failed to find|could not find|unable to find)\b.*\b(?:selector|locator)\b/i.test(normalized) ||
977
+ /\b(?:selector|locator)\b.*\b(?:no elements? found|not found|missing|failed to find|could not find|unable to find)\b/i.test(
978
+ normalized,
979
+ );
980
+
981
+ if (
982
+ /\b(?:unsupported|unknown|invalid)\s+(?:selector|locator)\b/i.test(normalized) ||
983
+ /\bfailed to parse selector\b/i.test(normalized) ||
984
+ /\bselector\b.*\b(?:parse|syntax|unsupported|invalid)\b/i.test(normalized) ||
985
+ (mentionsPlaywrightSelectorDialect && reportsSelectorMatchFailure)
986
+ ) {
987
+ return SELECTOR_DIALECT_ERROR_HINT;
988
+ }
989
+
990
+ return undefined;
991
+ }
992
+
993
+ function appendSelectorRecoveryHint(errorText: string): string {
994
+ const hint = getSelectorRecoveryHint(errorText);
995
+ if (!hint || errorText.includes("Agent-browser hint:")) {
996
+ return errorText;
997
+ }
998
+ return `${errorText}\n\n${hint}`;
999
+ }
1000
+
276
1001
  function formatBatchStepError(error: unknown): string {
277
- const errorText = stringifyUnknown(error).trim();
278
- return errorText.length > 0 ? `Error: ${errorText}` : "Error: batch step failed.";
1002
+ const errorText = stringifyModelFacing(error).trim();
1003
+ const formattedErrorText = errorText.length > 0 ? `Error: ${errorText}` : "Error: batch step failed.";
1004
+ return appendSelectorRecoveryHint(formattedErrorText);
279
1005
  }
280
1006
 
281
1007
  function getBatchFailureDetails(steps: Array<{ details: BatchStepPresentationDetails }>): BatchFailurePresentationDetails | undefined {
@@ -293,12 +1019,13 @@ function getBatchFailureDetails(steps: Array<{ details: BatchStepPresentationDet
293
1019
  }
294
1020
 
295
1021
  async function buildBatchStepPresentation(options: {
1022
+ artifactManifest?: SessionArtifactManifest;
296
1023
  cwd: string;
297
1024
  index: number;
298
1025
  item: AgentBrowserBatchResult;
299
1026
  persistentArtifactStore?: PersistentSessionArtifactStore;
300
1027
  }): Promise<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> {
301
- const { cwd, index, item, persistentArtifactStore } = options;
1028
+ const { artifactManifest, cwd, index, item, persistentArtifactStore } = options;
302
1029
  const command = isStringArray(item.command) ? item.command : undefined;
303
1030
  const commandText = formatBatchStepCommand(command, index);
304
1031
 
@@ -310,6 +1037,7 @@ async function buildBatchStepPresentation(options: {
310
1037
  };
311
1038
  return {
312
1039
  details: {
1040
+ artifacts: presentation.artifacts,
313
1041
  command,
314
1042
  commandText,
315
1043
  data: item.error,
@@ -323,6 +1051,7 @@ async function buildBatchStepPresentation(options: {
323
1051
  }
324
1052
 
325
1053
  const presentation = await buildToolPresentation({
1054
+ artifactManifest,
326
1055
  commandInfo: parseCommandInfo(command ?? []),
327
1056
  cwd,
328
1057
  envelope: { data: item.result, success: true },
@@ -340,6 +1069,7 @@ async function buildBatchStepPresentation(options: {
340
1069
 
341
1070
  return {
342
1071
  details: {
1072
+ artifacts: presentation.artifacts,
343
1073
  command,
344
1074
  commandText,
345
1075
  data: presentation.data,
@@ -348,6 +1078,8 @@ async function buildBatchStepPresentation(options: {
348
1078
  imagePath: imagePaths[0],
349
1079
  imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
350
1080
  index,
1081
+ savedFile: presentation.savedFile,
1082
+ savedFilePath: presentation.savedFilePath,
351
1083
  success: true,
352
1084
  summary: presentation.summary,
353
1085
  text,
@@ -357,6 +1089,7 @@ async function buildBatchStepPresentation(options: {
357
1089
  }
358
1090
 
359
1091
  async function buildBatchPresentation(options: {
1092
+ artifactManifest?: SessionArtifactManifest;
360
1093
  cwd: string;
361
1094
  data: AgentBrowserBatchResult[];
362
1095
  persistentArtifactStore?: PersistentSessionArtifactStore;
@@ -365,8 +1098,10 @@ async function buildBatchPresentation(options: {
365
1098
  const { cwd, data, persistentArtifactStore, summary } = options;
366
1099
  const steps: Array<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> = [];
367
1100
  const protectedPersistentPaths: string[] = [];
1101
+ let currentArtifactManifest = options.artifactManifest;
368
1102
  for (const [index, item] of data.entries()) {
369
1103
  const step = await buildBatchStepPresentation({
1104
+ artifactManifest: currentArtifactManifest,
370
1105
  cwd,
371
1106
  index,
372
1107
  item,
@@ -375,6 +1110,7 @@ async function buildBatchPresentation(options: {
375
1110
  : undefined,
376
1111
  });
377
1112
  steps.push(step);
1113
+ currentArtifactManifest = step.presentation.artifactManifest ?? currentArtifactManifest;
378
1114
  protectedPersistentPaths.push(
379
1115
  ...getPresentationPaths({
380
1116
  primaryPath: step.presentation.fullOutputPath,
@@ -385,6 +1121,7 @@ async function buildBatchPresentation(options: {
385
1121
 
386
1122
  const batchFailure = getBatchFailureDetails(steps);
387
1123
  const images = steps.flatMap((step) => getPresentationImages(step.presentation));
1124
+ const artifacts = steps.flatMap((step) => step.presentation.artifacts ?? []);
388
1125
  const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
389
1126
  primaryPath: step.presentation.fullOutputPath,
390
1127
  secondaryPaths: step.presentation.fullOutputPaths,
@@ -422,10 +1159,16 @@ async function buildBatchPresentation(options: {
422
1159
  ].join("\n");
423
1160
  const text = failureHeader ? `${failureHeader}\n\n${stepText}` : stepText;
424
1161
 
1162
+ const artifactRetentionSummary = currentArtifactManifest ? formatSessionArtifactRetentionSummary(currentArtifactManifest) : undefined;
1163
+ const contentText = artifactRetentionSummary && manifestHasNewNoticeWorthyEntries(options.artifactManifest, currentArtifactManifest) ? `${text}\n\n${artifactRetentionSummary}` : text;
1164
+
425
1165
  return {
1166
+ artifactManifest: currentArtifactManifest,
1167
+ artifactRetentionSummary,
1168
+ artifacts: artifacts.length > 0 ? artifacts : undefined,
426
1169
  batchFailure,
427
1170
  batchSteps: steps.map((step) => step.details),
428
- content: [{ type: "text", text }, ...images],
1171
+ content: [{ type: "text", text: contentText }, ...images],
429
1172
  data,
430
1173
  fullOutputPath: fullOutputPaths[0],
431
1174
  fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
@@ -436,10 +1179,24 @@ async function buildBatchPresentation(options: {
436
1179
  }
437
1180
 
438
1181
  function formatSummary(commandInfo: CommandInfo, data: unknown): string {
1182
+ const confirmationRequired = detectConfirmationRequired(data);
1183
+ if (confirmationRequired) {
1184
+ return formatConfirmationRequiredSummary(confirmationRequired);
1185
+ }
1186
+
439
1187
  if (Array.isArray(data) && commandInfo.command === "batch") {
440
1188
  const successCount = data.filter((item) => isRecord(item) && item.success !== false).length;
441
1189
  return successCount === data.length ? `Batch: ${successCount}/${data.length} succeeded` : `Batch failed: ${successCount}/${data.length} succeeded`;
442
1190
  }
1191
+ if (Array.isArray(data) && commandInfo.command === "profiles") {
1192
+ return `Chrome profiles: ${data.length}`;
1193
+ }
1194
+ if (Array.isArray(data) && commandInfo.command === "skills" && commandInfo.subcommand === "list") {
1195
+ return `agent-browser skills: ${data.length}`;
1196
+ }
1197
+ if (commandInfo.command === "skills" && commandInfo.subcommand === "get") {
1198
+ return "agent-browser skill loaded";
1199
+ }
443
1200
  if (isRecord(data)) {
444
1201
  const navigationSummary = getNavigationSummary(data);
445
1202
  if (navigationSummary && isNavigationObservableCommand(commandInfo.command)) {
@@ -461,9 +1218,9 @@ function formatSummary(commandInfo: CommandInfo, data: unknown): string {
461
1218
  if (commandInfo.command === "screenshot" && typeof data.path === "string") {
462
1219
  return `Screenshot saved: ${data.path}`;
463
1220
  }
464
- const savedFileSummary = getSavedFileSummary(commandInfo, data);
465
- if (savedFileSummary) {
466
- return savedFileSummary;
1221
+ const diagnosticSummary = formatDiagnosticSummary(commandInfo, data);
1222
+ if (diagnosticSummary) {
1223
+ return diagnosticSummary;
467
1224
  }
468
1225
  const extractionSummary = formatExtractionSummary(commandInfo, data);
469
1226
  if (extractionSummary) {
@@ -484,14 +1241,25 @@ function formatSummary(commandInfo: CommandInfo, data: unknown): string {
484
1241
  }
485
1242
 
486
1243
  function formatContentText(commandInfo: CommandInfo, data: unknown): string {
1244
+ const confirmationRequired = detectConfirmationRequired(data);
1245
+ if (confirmationRequired) {
1246
+ return formatConfirmationRequiredText(confirmationRequired);
1247
+ }
1248
+
487
1249
  if (typeof data === "string") {
488
- return data;
1250
+ return redactModelFacingText(data);
489
1251
  }
490
1252
  if (typeof data === "number" || typeof data === "boolean") {
491
1253
  return String(data);
492
1254
  }
1255
+ if (Array.isArray(data) && commandInfo.command === "profiles") {
1256
+ return formatProfilesText(data, "Chrome profiles");
1257
+ }
1258
+ if (Array.isArray(data) && commandInfo.command === "skills") {
1259
+ return formatSkillsText(commandInfo, data) ?? stringifyModelFacing(data);
1260
+ }
493
1261
  if (!isRecord(data)) {
494
- return stringifyUnknown(data);
1262
+ return stringifyModelFacing(data);
495
1263
  }
496
1264
 
497
1265
  const navigationSummary = getNavigationSummary(data);
@@ -518,25 +1286,36 @@ function formatContentText(commandInfo: CommandInfo, data: unknown): string {
518
1286
  const screenshotSummary = getScreenshotSummary(data);
519
1287
  if (screenshotSummary) return screenshotSummary;
520
1288
  }
521
- const savedFileSummary = getSavedFileSummary(commandInfo, data);
522
- if (savedFileSummary) {
523
- return savedFileSummary;
1289
+ const skillsText = formatSkillsText(commandInfo, data);
1290
+ if (skillsText) {
1291
+ return skillsText;
524
1292
  }
525
-
526
1293
  const extractionText = formatExtractionText(commandInfo, data);
527
1294
  if (extractionText) {
528
1295
  return extractionText;
529
1296
  }
530
1297
 
1298
+ const diagnosticText = formatDiagnosticText(commandInfo, data);
1299
+ if (diagnosticText) {
1300
+ return diagnosticText;
1301
+ }
1302
+
531
1303
  const pageSummary = getPageSummary(data);
532
1304
  if (pageSummary) {
533
- return pageSummary;
1305
+ return redactModelFacingText(pageSummary);
534
1306
  }
535
1307
 
536
- return stringifyUnknown(data);
1308
+ return stringifyModelFacing(data);
1309
+ }
1310
+
1311
+ function isTrustedScreenshotOutput(commandInfo: CommandInfo): boolean {
1312
+ return commandInfo.command === "screenshot";
537
1313
  }
538
1314
 
539
- function extractImagePath(cwd: string, data: unknown): string | undefined {
1315
+ function extractImagePath(commandInfo: CommandInfo, cwd: string, data: unknown): string | undefined {
1316
+ if (!isTrustedScreenshotOutput(commandInfo)) {
1317
+ return undefined;
1318
+ }
540
1319
  if (typeof data === "string") {
541
1320
  const mimeType = getImageMimeType(data);
542
1321
  return mimeType ? resolve(cwd, data) : undefined;
@@ -548,6 +1327,16 @@ function extractImagePath(cwd: string, data: unknown): string | undefined {
548
1327
  return mimeType ? resolve(cwd, data.path) : undefined;
549
1328
  }
550
1329
 
1330
+ function sanitizeModelFacingPresentation(presentation: ToolPresentation): ToolPresentation {
1331
+ presentation.content = presentation.content.map((item) => {
1332
+ if (item.type !== "text") return item;
1333
+ const parsed = parseJsonPreviewString(item.text);
1334
+ return parsed === item.text ? item : { ...item, text: stringifyModelFacing(parsed) };
1335
+ });
1336
+ presentation.summary = redactModelFacingText(presentation.summary);
1337
+ return presentation;
1338
+ }
1339
+
551
1340
  async function attachInlineImage(presentation: ToolPresentation, imagePath: string): Promise<ToolPresentation> {
552
1341
  const mimeType = getImageMimeType(imagePath);
553
1342
  if (!mimeType) {
@@ -601,31 +1390,61 @@ function buildLargeOutputPreview(text: string): { omittedLineCount: number; prev
601
1390
  };
602
1391
  }
603
1392
 
1393
+ interface LargeOutputSpillWriteResult {
1394
+ evictedArtifacts: PersistentSessionArtifactEviction[];
1395
+ path: string;
1396
+ storageScope: ArtifactStorageScope;
1397
+ }
1398
+
604
1399
  async function writeLargeOutputSpillFile(options: {
605
1400
  data: unknown;
606
1401
  persistentArtifactStore?: PersistentSessionArtifactStore;
607
1402
  text: string;
608
- }): Promise<string> {
1403
+ }): Promise<LargeOutputSpillWriteResult> {
609
1404
  const payload =
610
1405
  typeof options.data === "string"
611
- ? options.data
1406
+ ? redactModelFacingText(options.data)
612
1407
  : typeof options.data === "number" || typeof options.data === "boolean"
613
1408
  ? String(options.data)
614
1409
  : options.data === undefined
615
- ? options.text
616
- : stringifyUnknown(options.data);
1410
+ ? redactModelFacingText(options.text)
1411
+ : stringifyModelFacing(options.data);
617
1412
  const isStructuredPayload = typeof options.data !== "string" && typeof options.data !== "number" && typeof options.data !== "boolean";
618
1413
  const fileOptions = {
619
1414
  content: payload,
620
1415
  prefix: LARGE_OUTPUT_FILE_PREFIX,
621
1416
  suffix: isStructuredPayload ? ".json" : ".txt",
622
1417
  };
623
- return options.persistentArtifactStore
624
- ? await writePersistentSessionArtifactFile({ ...fileOptions, store: options.persistentArtifactStore })
625
- : await writeSecureTempFile(fileOptions);
1418
+ if (options.persistentArtifactStore) {
1419
+ const result = await writePersistentSessionArtifactFile({ ...fileOptions, store: options.persistentArtifactStore });
1420
+ return { ...result, storageScope: "persistent-session" };
1421
+ }
1422
+ return { evictedArtifacts: [], path: await writeSecureTempFile(fileOptions), storageScope: "process-temp" };
1423
+ }
1424
+
1425
+ function buildSpillArtifactEntries(options: {
1426
+ commandInfo: CommandInfo;
1427
+ evictedArtifacts: PersistentSessionArtifactEviction[];
1428
+ path: string;
1429
+ storageScope: ArtifactStorageScope;
1430
+ }): SessionArtifactManifestEntry[] {
1431
+ const nowMs = Date.now();
1432
+ return [
1433
+ {
1434
+ command: options.commandInfo.command,
1435
+ createdAtMs: nowMs,
1436
+ kind: "spill",
1437
+ path: options.path,
1438
+ retentionState: options.storageScope === "persistent-session" ? "live" : "ephemeral",
1439
+ storageScope: options.storageScope,
1440
+ subcommand: options.commandInfo.subcommand,
1441
+ },
1442
+ ...buildEvictedSessionArtifactEntries(options.evictedArtifacts, nowMs),
1443
+ ];
626
1444
  }
627
1445
 
628
1446
  async function compactLargePresentationOutput(options: {
1447
+ artifactManifest?: SessionArtifactManifest;
629
1448
  commandInfo: CommandInfo;
630
1449
  data: unknown;
631
1450
  persistentArtifactStore?: PersistentSessionArtifactStore;
@@ -637,13 +1456,15 @@ async function compactLargePresentationOutput(options: {
637
1456
  }
638
1457
 
639
1458
  let fullOutputPath: string | undefined;
1459
+ let spill: LargeOutputSpillWriteResult | undefined;
640
1460
  let spillErrorText: string | undefined;
641
1461
  try {
642
- fullOutputPath = await writeLargeOutputSpillFile({
1462
+ spill = await writeLargeOutputSpillFile({
643
1463
  data: options.data,
644
1464
  persistentArtifactStore: options.persistentArtifactStore,
645
1465
  text,
646
1466
  });
1467
+ fullOutputPath = spill.path;
647
1468
  } catch (error) {
648
1469
  spillErrorText = error instanceof Error ? error.message : String(error);
649
1470
  }
@@ -684,43 +1505,79 @@ async function compactLargePresentationOutput(options: {
684
1505
  };
685
1506
  options.presentation.fullOutputPath = fullOutputPath;
686
1507
  options.presentation.summary = `${options.presentation.summary} (compact)`;
1508
+ if (fullOutputPath && spill) {
1509
+ return applyArtifactManifest(
1510
+ options.presentation,
1511
+ options.presentation.artifactManifest ?? options.artifactManifest,
1512
+ buildSpillArtifactEntries({
1513
+ commandInfo: options.commandInfo,
1514
+ evictedArtifacts: spill.evictedArtifacts,
1515
+ path: fullOutputPath,
1516
+ storageScope: spill.storageScope,
1517
+ }),
1518
+ );
1519
+ }
687
1520
  return options.presentation;
688
1521
  }
689
1522
 
690
1523
  export async function buildToolPresentation(options: {
1524
+ artifactManifest?: SessionArtifactManifest;
691
1525
  commandInfo: CommandInfo;
692
1526
  cwd: string;
693
1527
  envelope?: AgentBrowserEnvelope;
694
1528
  errorText?: string;
695
1529
  persistentArtifactStore?: PersistentSessionArtifactStore;
696
1530
  }): Promise<ToolPresentation> {
697
- const { commandInfo, cwd, envelope, errorText, persistentArtifactStore } = options;
1531
+ const { artifactManifest, commandInfo, cwd, envelope, errorText, persistentArtifactStore } = options;
698
1532
  if (errorText) {
1533
+ const hintedErrorText = appendSelectorRecoveryHint(redactModelFacingText(errorText));
699
1534
  return {
700
- content: [{ type: "text", text: errorText }],
701
- summary: errorText,
1535
+ content: [{ type: "text", text: hintedErrorText }],
1536
+ summary: hintedErrorText,
702
1537
  };
703
1538
  }
704
1539
 
705
1540
  const data = envelope?.data;
706
- const summary = formatSummary(commandInfo, data);
1541
+ const artifacts = await extractFileArtifacts(commandInfo, cwd, data);
1542
+ const artifactSummary = formatArtifactSummary(artifacts);
1543
+ const summary = artifactSummary ?? formatSummary(commandInfo, data);
1544
+ const artifactText = artifacts.length > 0 ? formatArtifactMetadataLines(artifacts).join("\n") : undefined;
707
1545
  const presentation =
708
1546
  commandInfo.command === "batch" && Array.isArray(data)
709
- ? await buildBatchPresentation({ cwd, data: data as AgentBrowserBatchResult[], persistentArtifactStore, summary })
1547
+ ? await buildBatchPresentation({ artifactManifest, cwd, data: data as AgentBrowserBatchResult[], persistentArtifactStore, summary })
710
1548
  : commandInfo.command === "snapshot" && isRecord(data)
711
- ? await buildSnapshotPresentation(data, persistentArtifactStore)
1549
+ ? await buildSnapshotPresentation(data, persistentArtifactStore, artifactManifest)
712
1550
  : {
713
- content: [{ type: "text" as const, text: formatContentText(commandInfo, data) }],
1551
+ artifacts: artifacts.length > 0 ? artifacts : undefined,
1552
+ content: [{ type: "text" as const, text: artifactText ?? formatContentText(commandInfo, data) }],
714
1553
  data,
715
1554
  summary,
716
1555
  };
1556
+ if (artifacts.length > 0 && !presentation.artifacts) {
1557
+ presentation.artifacts = artifacts;
1558
+ }
1559
+ if (isRecord(data)) {
1560
+ const savedFile = getSavedFileDetails(commandInfo, data);
1561
+ if (savedFile) {
1562
+ presentation.savedFile = savedFile;
1563
+ presentation.savedFilePath = savedFile.path;
1564
+ }
1565
+ }
717
1566
 
718
- const imagePath = extractImagePath(cwd, data);
1567
+ const imagePath = extractImagePath(commandInfo, cwd, data);
719
1568
  const presentationWithImage = imagePath ? await attachInlineImage(presentation, imagePath) : presentation;
720
- return await compactLargePresentationOutput({
1569
+ const compactedPresentation = await compactLargePresentationOutput({
1570
+ artifactManifest,
721
1571
  commandInfo,
722
1572
  data,
723
1573
  persistentArtifactStore,
724
1574
  presentation: presentationWithImage,
725
1575
  });
1576
+ return sanitizeModelFacingPresentation(
1577
+ applyArtifactManifest(
1578
+ compactedPresentation,
1579
+ compactedPresentation.artifactManifest ?? artifactManifest,
1580
+ buildManifestEntriesForFileArtifacts(artifacts.filter(isManifestFileArtifact)),
1581
+ ),
1582
+ );
726
1583
  }