pi-agent-browser-native 0.2.24 → 0.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ import { readFile, stat } from "node:fs/promises";
10
10
  import { extname, resolve } from "node:path";
11
11
 
12
12
  import { isRecord, parsePositiveInteger } from "../parsing.js";
13
- import { parseCommandInfo, redactSensitiveText, redactSensitiveValue, type CommandInfo } from "../runtime.js";
13
+ import { extractCommandTokens, parseCommandInfo, redactInvocationArgs, redactSensitiveText, redactSensitiveValue, type CommandInfo } from "../runtime.js";
14
14
  import {
15
15
  type PersistentSessionArtifactEviction,
16
16
  type PersistentSessionArtifactStore,
@@ -22,6 +22,15 @@ import { buildSnapshotPresentation, formatRawSnapshotText, formatSnapshotSummary
22
22
  import {
23
23
  type AgentBrowserBatchResult,
24
24
  type AgentBrowserEnvelope,
25
+ type AgentBrowserNextAction,
26
+ type AgentBrowserPageChangeSummary,
27
+ type ArtifactVerificationEntry,
28
+ type ArtifactVerificationSummary,
29
+ buildAgentBrowserNextActions,
30
+ buildAgentBrowserResultCategoryDetails,
31
+ classifyAgentBrowserFailureCategory,
32
+ classifyAgentBrowserSuccessCategory,
33
+ classifyNetworkRequestFailure,
25
34
  type BatchFailurePresentationDetails,
26
35
  type BatchStepPresentationDetails,
27
36
  type ArtifactStorageScope,
@@ -36,6 +45,7 @@ import {
36
45
  formatSessionArtifactRetentionSummary,
37
46
  mergeSessionArtifactManifest,
38
47
  stringifyUnknown,
48
+ summarizeNetworkFailures,
39
49
  truncateText,
40
50
  } from "./shared.js";
41
51
 
@@ -50,6 +60,32 @@ const IMAGE_EXTENSION_TO_MIME_TYPE: Record<string, string> = {
50
60
  const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
51
61
  const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
52
62
  const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
63
+ const PAGE_CHANGE_SUMMARY_COMMANDS = new Set([
64
+ "back",
65
+ "check",
66
+ "click",
67
+ "dblclick",
68
+ "dialog",
69
+ "download",
70
+ "fill",
71
+ "forward",
72
+ "goto",
73
+ "hover",
74
+ "navigate",
75
+ "open",
76
+ "pdf",
77
+ "press",
78
+ "pushstate",
79
+ "reload",
80
+ "screenshot",
81
+ "scroll",
82
+ "scrollintoview",
83
+ "select",
84
+ "swipe",
85
+ "tap",
86
+ "type",
87
+ "uncheck",
88
+ ]);
53
89
  const NAVIGATION_SUMMARY_FIELD = "navigationSummary";
54
90
  const LARGE_OUTPUT_INLINE_MAX_CHARS = 8_000;
55
91
  const LARGE_OUTPUT_INLINE_MAX_LINES = 120;
@@ -229,13 +265,89 @@ function formatDiagnosticSummary(commandInfo: CommandInfo, data: Record<string,
229
265
  if (profiles) return `Auth profiles: ${profiles.length}`;
230
266
  const name = getStringField(data, "name") ?? getStringField(data, "profile") ?? commandInfo.subcommand;
231
267
  if (name && commandInfo.subcommand === "show") return `Auth profile: ${name}`;
268
+ if (name && ["save", "login", "delete"].includes(commandInfo.subcommand ?? "")) return `Auth ${commandInfo.subcommand}: ${name}`;
232
269
  }
233
270
 
234
- if (commandInfo.command === "network" && commandInfo.subcommand === "requests") {
235
- const requests = getArrayField(data, "requests");
236
- if (requests) return `Network requests: ${requests.length}`;
271
+ if (commandInfo.command === "cookies") {
272
+ const cookies = getArrayField(data, "cookies");
273
+ if (cookies) return `Cookies: ${cookies.length}`;
274
+ const name = getStringField(data, "name");
275
+ if (name) return name;
276
+ if (data.set === true) return "Cookie set";
277
+ if (data.cleared === true || data.clear === true) return "Cookies cleared";
237
278
  }
238
279
 
280
+ if (commandInfo.command === "storage") {
281
+ const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
282
+ if (entries) return `Storage entries: ${entries.length}`;
283
+ const key = getStringField(data, "key");
284
+ if (key && (commandInfo.subcommand === "set" || data.set === true || Object.hasOwn(data, "value"))) return `Storage set: ${key}`;
285
+ if (data.cleared === true || data.clear === true) return "Storage cleared";
286
+ }
287
+
288
+ if (commandInfo.command === "dialog") {
289
+ const open = typeof data.open === "boolean" ? data.open : undefined;
290
+ if (open !== undefined) return open ? "Dialog open" : "No dialog open";
291
+ if (data.accepted === true) return "Dialog accepted";
292
+ if (data.dismissed === true) return "Dialog dismissed";
293
+ }
294
+
295
+ if (commandInfo.command === "frame") {
296
+ const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector") ?? commandInfo.subcommand;
297
+ if (frame) return `Frame: ${frame}`;
298
+ }
299
+
300
+ if (commandInfo.command === "state") {
301
+ const states = getArrayField(data, "states") ?? getArrayField(data, "files");
302
+ if (states) return `States: ${states.length}`;
303
+ if (commandInfo.subcommand === "load") return undefined;
304
+ const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "path") ?? commandInfo.subcommand;
305
+ if (stateName) return `State ${commandInfo.subcommand ?? "result"}: ${stateName}`;
306
+ }
307
+
308
+ if (commandInfo.command === "network") {
309
+ if (commandInfo.subcommand === "requests") {
310
+ const requests = getArrayField(data, "requests");
311
+ if (requests) return `Network requests: ${requests.length}`;
312
+ }
313
+ if (commandInfo.subcommand === "route") {
314
+ const routed = getStringField(data, "routed") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
315
+ return routed ? `Network route: ${redactModelFacingTextIfSensitive(routed)}` : "Network route configured";
316
+ }
317
+ if (commandInfo.subcommand === "unroute") {
318
+ const unrouted = getStringField(data, "unrouted") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
319
+ return unrouted ? `Network unroute: ${redactModelFacingTextIfSensitive(unrouted)}` : "Network route removed";
320
+ }
321
+ if (commandInfo.subcommand === "har") {
322
+ const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
323
+ return `Network HAR: ${state}`;
324
+ }
325
+ }
326
+
327
+ if (commandInfo.command === "diff") {
328
+ if (commandInfo.subcommand === "snapshot") return "Snapshot diff completed";
329
+ if (commandInfo.subcommand === "url") return "URL diff completed";
330
+ }
331
+
332
+ if (["trace", "profiler"].includes(commandInfo.command ?? "")) {
333
+ const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
334
+ if (state) return `${commandInfo.command === "trace" ? "Trace" : "Profiler"}: ${state}`;
335
+ }
336
+
337
+ if (commandInfo.command === "highlight") return "Element highlighted";
338
+ if (commandInfo.command === "inspect") return "DevTools inspect opened";
339
+ if (commandInfo.command === "clipboard") return `Clipboard ${commandInfo.subcommand ?? "completed"}`;
340
+
341
+ if (commandInfo.command === "stream") {
342
+ if (commandInfo.subcommand === "enable") {
343
+ const port = typeof data.port === "number" ? ` on port ${data.port}` : "";
344
+ return `Stream enabled${port}`;
345
+ }
346
+ if (commandInfo.subcommand === "disable") return "Stream disabled";
347
+ }
348
+
349
+ if (commandInfo.command === "chat") return "Chat response";
350
+
239
351
  if (commandInfo.command === "console") {
240
352
  const messages = getArrayField(data, "messages");
241
353
  if (messages) return `Console messages: ${messages.length}`;
@@ -417,6 +529,7 @@ function formatNativeSkillContent(content: string): string {
417
529
 
418
530
  function formatSkillsText(commandInfo: CommandInfo, data: unknown): string | undefined {
419
531
  if (commandInfo.command !== "skills") return undefined;
532
+ if (commandInfo.subcommand === "path") return typeof data === "string" ? redactModelFacingText(data) : undefined;
420
533
  if (commandInfo.subcommand === "list" && Array.isArray(data)) return formatSkillsListText(data);
421
534
  const content = getSkillContent(data);
422
535
  if (content) {
@@ -479,7 +592,9 @@ function formatNetworkRequestLine(item: Record<string, unknown>, index: number):
479
592
  const url = getStringField(item, "url") ?? "(no url)";
480
593
  const requestId = getStringField(item, "requestId") ?? getStringField(item, "id");
481
594
  const idText = requestId ? ` [${redactSensitiveText(requestId)}]` : "";
482
- const lines = [`${index + 1}. ${status} ${method} ${truncateText(redactSensitiveText(url), 180)}${type ? ` (${type})` : ""}${idText}`];
595
+ const failureClassification = classifyNetworkRequestFailure(item);
596
+ const impactText = failureClassification ? ` [${failureClassification.impact}: ${failureClassification.reason}]` : "";
597
+ const lines = [`${index + 1}. ${status} ${method} ${truncateText(redactSensitiveText(url), 180)}${type ? ` (${type})` : ""}${idText}${impactText}`];
483
598
  appendNetworkPreview(lines, "Payload", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.request), NETWORK_BODY_PREVIEW_MAX_CHARS);
484
599
  appendNetworkPreview(lines, "Response", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.response), NETWORK_BODY_PREVIEW_MAX_CHARS);
485
600
  appendNetworkPreview(lines, "Error", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.error), NETWORK_ERROR_PREVIEW_MAX_CHARS);
@@ -490,10 +605,14 @@ function formatNetworkRequestsText(data: Record<string, unknown>): string | unde
490
605
  const requests = getArrayField(data, "requests");
491
606
  if (!requests) return undefined;
492
607
  if (requests.length === 0) return "No network requests captured.";
493
- const shown = requests.slice(0, DIAGNOSTIC_REQUEST_PREVIEW_LIMIT).flatMap((item, index) => {
608
+ const networkFailureSummary = summarizeNetworkFailures(requests);
609
+ const shown = networkFailureSummary.totalCount > 0
610
+ ? [`Network failure summary: ${networkFailureSummary.actionableCount} actionable, ${networkFailureSummary.benignCount} benign low-impact (${networkFailureSummary.totalCount} total).`]
611
+ : [];
612
+ shown.push(...requests.slice(0, DIAGNOSTIC_REQUEST_PREVIEW_LIMIT).flatMap((item, index) => {
494
613
  if (!isRecord(item)) return [`${index + 1}. ${stringifyModelFacing(item)}`];
495
614
  return formatNetworkRequestLine(item, index);
496
- });
615
+ }));
497
616
  if (requests.length > DIAGNOSTIC_REQUEST_PREVIEW_LIMIT) {
498
617
  shown.push(`... (${requests.length - DIAGNOSTIC_REQUEST_PREVIEW_LIMIT} additional requests omitted from preview)`);
499
618
  }
@@ -557,6 +676,15 @@ function formatDashboardText(data: Record<string, unknown>): string | undefined
557
676
  return lines.length > 0 ? lines.join("\n") : undefined;
558
677
  }
559
678
 
679
+ function formatChatText(data: Record<string, unknown>): string | undefined {
680
+ const response = getStringField(data, "response") ?? getStringField(data, "message") ?? getStringField(data, "text") ?? getStringField(data, "result");
681
+ if (response) return redactModelFacingText(response);
682
+ const model = getStringField(data, "model");
683
+ const provider = getStringField(data, "provider");
684
+ const lines = [model ? `Model: ${redactModelFacingText(model)}` : undefined, provider ? `Provider: ${redactModelFacingText(provider)}` : undefined].filter(Boolean);
685
+ return lines.length > 0 ? lines.join("\n") : undefined;
686
+ }
687
+
560
688
  function formatDoctorText(data: Record<string, unknown>): string | undefined {
561
689
  const lines: string[] = [];
562
690
  const status = getStringField(data, "status") ?? getStringField(data, "result");
@@ -568,6 +696,123 @@ function formatDoctorText(data: Record<string, unknown>): string | undefined {
568
696
  return lines.length > 0 ? lines.join("\n") : undefined;
569
697
  }
570
698
 
699
+ function formatCookieRecordText(item: Record<string, unknown>, fallbackName: string): string {
700
+ const name = redactModelFacingText(getStringField(item, "name") ?? fallbackName);
701
+ const domain = getStringField(item, "domain");
702
+ const path = getStringField(item, "path");
703
+ const flags = [item.httpOnly === true ? "httpOnly" : undefined, item.secure === true ? "secure" : undefined].filter(Boolean).join(", ");
704
+ const location = [domain, path].filter(Boolean).join("");
705
+ return [name, location ? `(${redactModelFacingText(location)})` : undefined, flags ? `[${flags}]` : undefined].filter(Boolean).join(" ");
706
+ }
707
+
708
+ function formatCookiesText(data: Record<string, unknown>): string | undefined {
709
+ const cookies = getArrayField(data, "cookies");
710
+ if (cookies) {
711
+ if (cookies.length === 0) return "No cookies.";
712
+ return cookies
713
+ .map((item, index) => (isRecord(item) ? formatCookieRecordText(item, `(cookie ${index + 1})`) : `${index + 1}. [REDACTED]`))
714
+ .join("\n");
715
+ }
716
+ if (getStringField(data, "name") || getStringField(data, "domain") || getStringField(data, "path") || Object.hasOwn(data, "value")) {
717
+ return formatCookieRecordText(data, "cookie");
718
+ }
719
+ if (data.set === true) return "Cookie set.";
720
+ if (data.cleared === true || data.clear === true) return "Cookies cleared.";
721
+ return undefined;
722
+ }
723
+
724
+ function formatStorageText(data: Record<string, unknown>): string | undefined {
725
+ const type = getStringField(data, "type") ?? getStringField(data, "storage") ?? "storage";
726
+ const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
727
+ if (entries) {
728
+ if (entries.length === 0) return `${type}: no entries.`;
729
+ return entries
730
+ .map((item, index) => {
731
+ if (!isRecord(item)) return `${index + 1}. [REDACTED]`;
732
+ const rawKey = getStringField(item, "key") ?? getStringField(item, "name") ?? `(entry ${index + 1})`;
733
+ const key = redactModelFacingText(rawKey);
734
+ return Object.hasOwn(item, "value") ? `${key}: [REDACTED]` : key;
735
+ })
736
+ .join("\n");
737
+ }
738
+ const key = getStringField(data, "key");
739
+ if (key && Object.hasOwn(data, "value")) return `${type} ${redactModelFacingText(key)}: [REDACTED]`;
740
+ if (key && data.set === true) return `${type} set: ${redactModelFacingText(key)}`;
741
+ if (data.cleared === true || data.clear === true) return `${type} cleared.`;
742
+ return undefined;
743
+ }
744
+
745
+ function formatDialogText(data: Record<string, unknown>): string | undefined {
746
+ const lines: string[] = [];
747
+ if (typeof data.open === "boolean") lines.push(data.open ? "Dialog open." : "No dialog open.");
748
+ const type = getStringField(data, "type");
749
+ if (type) lines.push(`Type: ${redactModelFacingText(type)}`);
750
+ const message = getStringField(data, "message");
751
+ if (message) lines.push(`Message: ${/(?:auth|authorization|bearer|cookie|pass(?:word)?|secret|session|token)/i.test(message) ? "[REDACTED]" : redactModelFacingText(message)}`);
752
+ if (data.accepted === true) lines.push("Accepted.");
753
+ if (data.dismissed === true) lines.push("Dismissed.");
754
+ return lines.length > 0 ? lines.join("\n") : undefined;
755
+ }
756
+
757
+ function formatFrameText(data: Record<string, unknown>): string | undefined {
758
+ const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector");
759
+ const url = getStringField(data, "url");
760
+ const title = getStringField(data, "title");
761
+ const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactModelFacingTextIfSensitive(url)}` : undefined].filter(Boolean);
762
+ return lines.length > 0 ? lines.join("\n") : undefined;
763
+ }
764
+
765
+ function formatStateText(data: Record<string, unknown>): string | undefined {
766
+ const states = getArrayField(data, "states") ?? getArrayField(data, "files");
767
+ if (states) {
768
+ if (states.length === 0) return "No saved states.";
769
+ return states
770
+ .map((item, index) => {
771
+ if (!isRecord(item)) return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
772
+ const name = getStringField(item, "name") ?? getStringField(item, "file") ?? getStringField(item, "path") ?? `(state ${index + 1})`;
773
+ const url = getStringField(item, "url");
774
+ return url ? `${index + 1}. ${redactModelFacingText(name)} — ${redactModelFacingTextIfSensitive(url)}` : `${index + 1}. ${redactModelFacingText(name)}`;
775
+ })
776
+ .join("\n");
777
+ }
778
+ if (data.loaded === true) return `State loaded: ${redactModelFacingText(getStringField(data, "path") ?? getStringField(data, "name") ?? "ok")}`;
779
+ if (data.cleared === true || data.clear === true) return "State cleared.";
780
+ return undefined;
781
+ }
782
+
783
+ function isSensitivePresentationField(key: string): boolean {
784
+ return /^(?:access(?:_|-)?token|api(?:_|-)?key|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|secret|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|token|x(?:_|-)?api(?:_|-)?key)$/i.test(key);
785
+ }
786
+
787
+ function redactStructuredPresentationValue(value: unknown): unknown {
788
+ if (typeof value === "string") return redactModelFacingTextIfSensitive(value);
789
+ if (Array.isArray(value)) return value.map((item) => redactStructuredPresentationValue(item));
790
+ if (!isRecord(value)) return value;
791
+ return Object.fromEntries(
792
+ Object.entries(value).map(([key, entryValue]) => [
793
+ key,
794
+ isSensitivePresentationField(key) ? "[REDACTED]" : redactStructuredPresentationValue(entryValue),
795
+ ]),
796
+ );
797
+ }
798
+
799
+ function redactStatefulValues(value: unknown, sensitiveKeys: Set<string>): unknown {
800
+ if (Array.isArray(value)) return value.map((item) => redactStatefulValues(item, sensitiveKeys));
801
+ if (!isRecord(value)) return redactStructuredPresentationValue(value);
802
+ return Object.fromEntries(
803
+ Object.entries(value).map(([key, entryValue]) => [
804
+ key,
805
+ sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : redactStatefulValues(entryValue, sensitiveKeys),
806
+ ]),
807
+ );
808
+ }
809
+
810
+ function redactPresentationData(commandInfo: CommandInfo, data: unknown): unknown {
811
+ if (commandInfo.command === "cookies") return redactStatefulValues(data, new Set(["value"]));
812
+ if (commandInfo.command === "storage") return redactStatefulValues(data, new Set(["value"]));
813
+ return redactStructuredPresentationValue(data);
814
+ }
815
+
571
816
  function formatDiagnosticText(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
572
817
  if (commandInfo.command === "session") return formatSessionText(data);
573
818
  if (commandInfo.command === "profiles") {
@@ -579,8 +824,23 @@ function formatDiagnosticText(commandInfo: CommandInfo, data: Record<string, unk
579
824
  if (profiles) return formatProfilesText(profiles, "auth profiles");
580
825
  if (commandInfo.subcommand === "show") return formatAuthShowText(data);
581
826
  }
827
+ if (commandInfo.command === "cookies") return formatCookiesText(data);
828
+ if (commandInfo.command === "storage") return formatStorageText(data);
829
+ if (commandInfo.command === "dialog") return formatDialogText(data);
830
+ if (commandInfo.command === "frame") return formatFrameText(data);
831
+ if (commandInfo.command === "state") return formatStateText(data);
582
832
  if (commandInfo.command === "network" && commandInfo.subcommand === "requests") return formatNetworkRequestsText(data);
583
833
  if (commandInfo.command === "network" && commandInfo.subcommand === "request") return formatNetworkRequestText(data);
834
+ if (commandInfo.command === "diff") return stringifyModelFacing(data);
835
+ if (commandInfo.command === "clipboard") {
836
+ const text = getStringField(data, "text") ?? getStringField(data, "value") ?? getStringField(data, "result");
837
+ if (text) return redactModelFacingText(text);
838
+ }
839
+ if (commandInfo.command === "stream") {
840
+ const streamSummary = getStreamSummary(data);
841
+ if (streamSummary) return streamSummary;
842
+ }
843
+ if (commandInfo.command === "chat") return formatChatText(data);
584
844
  if (commandInfo.command === "console") return formatConsoleText(data);
585
845
  if (commandInfo.command === "errors") return formatErrorsText(data);
586
846
  if (commandInfo.command === "dashboard") return formatDashboardText(data);
@@ -627,7 +887,10 @@ const PATH_FIELD_CANDIDATES = [
627
887
  "filePath",
628
888
  "outputPath",
629
889
  "downloadPath",
890
+ "diffPath",
630
891
  "harPath",
892
+ "savedPath",
893
+ "statePath",
631
894
  "tracePath",
632
895
  "profilePath",
633
896
  "videoPath",
@@ -647,9 +910,11 @@ const ARTIFACT_EXTENSION_TO_MEDIA_TYPE: Record<string, string> = {
647
910
 
648
911
  function getArtifactKind(commandInfo: CommandInfo): FileArtifactKind | undefined {
649
912
  if (commandInfo.command === "screenshot") return "image";
913
+ if (commandInfo.command === "diff" && commandInfo.subcommand === "screenshot") return "image";
650
914
  if (commandInfo.command === "pdf") return "pdf";
651
915
  if (commandInfo.command === "download") return "download";
652
916
  if (commandInfo.command === "wait" && commandInfo.subcommand === "--download") return "download";
917
+ if (commandInfo.command === "state" && commandInfo.subcommand === "save") return "file";
653
918
  if (commandInfo.command === "trace") return "trace";
654
919
  if (commandInfo.command === "profiler") return "profile";
655
920
  if (commandInfo.command === "record") return "video";
@@ -773,16 +1038,124 @@ function isManifestFileArtifact(artifact: FileArtifactMetadata): boolean {
773
1038
  return !isRecordingStartArtifact(artifact);
774
1039
  }
775
1040
 
1041
+ function getArtifactVerificationEntry(artifact: FileArtifactMetadata): ArtifactVerificationEntry {
1042
+ if (isRecordingStartArtifact(artifact)) {
1043
+ return {
1044
+ absolutePath: artifact.absolutePath,
1045
+ exists: artifact.exists,
1046
+ kind: artifact.kind,
1047
+ limitation: "Recording output is pending until record stop completes.",
1048
+ mediaType: artifact.mediaType,
1049
+ path: artifact.path,
1050
+ requestedPath: artifact.requestedPath,
1051
+ retentionState: undefined,
1052
+ sizeBytes: artifact.sizeBytes,
1053
+ state: "pending",
1054
+ status: artifact.status,
1055
+ storageScope: undefined,
1056
+ };
1057
+ }
1058
+ const state = artifact.exists === true
1059
+ ? "verified"
1060
+ : artifact.exists === false
1061
+ ? "missing"
1062
+ : "unverified";
1063
+ return {
1064
+ absolutePath: artifact.absolutePath,
1065
+ exists: artifact.exists,
1066
+ kind: artifact.kind,
1067
+ limitation: state === "missing"
1068
+ ? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
1069
+ : state === "unverified"
1070
+ ? "The wrapper could not prove local filesystem existence for this artifact."
1071
+ : undefined,
1072
+ mediaType: artifact.mediaType,
1073
+ path: artifact.path,
1074
+ requestedPath: artifact.requestedPath,
1075
+ retentionState: artifact.exists === false ? "missing" : "live",
1076
+ sizeBytes: artifact.sizeBytes,
1077
+ state,
1078
+ status: artifact.status,
1079
+ storageScope: "explicit-path",
1080
+ };
1081
+ }
1082
+
1083
+ function getManifestVerificationEntry(entry: SessionArtifactManifestEntry): ArtifactVerificationEntry | undefined {
1084
+ if (entry.storageScope === "explicit-path") return undefined;
1085
+ const state = entry.retentionState === "live"
1086
+ ? "verified"
1087
+ : entry.retentionState === "missing" || entry.retentionState === "evicted"
1088
+ ? "missing"
1089
+ : "unverified";
1090
+ return {
1091
+ absolutePath: entry.absolutePath,
1092
+ exists: entry.exists,
1093
+ kind: entry.kind,
1094
+ limitation: entry.retentionState === "ephemeral"
1095
+ ? "This spill file is process-temporary and may not survive reload or restart."
1096
+ : entry.retentionState === "evicted"
1097
+ ? "This persisted spill file was evicted from the bounded session artifact store."
1098
+ : undefined,
1099
+ mediaType: entry.mediaType,
1100
+ path: entry.path,
1101
+ requestedPath: entry.requestedPath,
1102
+ retentionState: entry.retentionState,
1103
+ sizeBytes: entry.sizeBytes,
1104
+ state,
1105
+ storageScope: entry.storageScope,
1106
+ };
1107
+ }
1108
+
1109
+ function buildArtifactVerificationSummary(
1110
+ artifacts: FileArtifactMetadata[],
1111
+ manifest?: SessionArtifactManifest,
1112
+ manifestPaths?: ReadonlySet<string>,
1113
+ ): ArtifactVerificationSummary | undefined {
1114
+ const entries = [
1115
+ ...artifacts.map(getArtifactVerificationEntry),
1116
+ ...(manifest?.entries.flatMap((entry) => {
1117
+ if (manifestPaths && !manifestPaths.has(entry.path)) return [];
1118
+ const verificationEntry = getManifestVerificationEntry(entry);
1119
+ return verificationEntry ? [verificationEntry] : [];
1120
+ }) ?? []),
1121
+ ];
1122
+ if (entries.length === 0) return undefined;
1123
+ const verifiedCount = entries.filter((entry) => entry.state === "verified").length;
1124
+ const missingCount = entries.filter((entry) => entry.state === "missing").length;
1125
+ const pendingCount = entries.filter((entry) => entry.state === "pending").length;
1126
+ const unverifiedCount = entries.filter((entry) => entry.state === "unverified").length;
1127
+ return {
1128
+ artifacts: entries,
1129
+ missingCount,
1130
+ pendingCount,
1131
+ unverifiedCount,
1132
+ verified: entries.length > 0 && verifiedCount === entries.length,
1133
+ verifiedCount,
1134
+ };
1135
+ }
1136
+
1137
+ function classifyPresentationSuccessCategory(options: {
1138
+ artifactVerification?: ArtifactVerificationSummary;
1139
+ artifacts?: FileArtifactMetadata[];
1140
+ inspection?: boolean;
1141
+ savedFile?: SavedFilePresentationDetails;
1142
+ }) {
1143
+ if ((options.artifactVerification?.missingCount ?? 0) > 0 || (options.artifactVerification?.unverifiedCount ?? 0) > 0) {
1144
+ return "artifact-unverified" as const;
1145
+ }
1146
+ return classifyAgentBrowserSuccessCategory(options);
1147
+ }
1148
+
776
1149
  function formatArtifactLabel(artifact: FileArtifactMetadata): string {
777
1150
  switch (artifact.kind) {
778
1151
  case "download":
779
1152
  return artifact.command === "wait" && artifact.subcommand === "--download" ? "Download completed" : "Downloaded file";
780
1153
  case "file":
781
- return "Saved file";
1154
+ return artifact.command === "state" ? "State file" : "Saved file";
782
1155
  case "har":
783
1156
  return "Saved HAR";
784
1157
  case "image":
785
- return "Saved image";
1158
+ return artifact.command === "diff" && artifact.subcommand === "screenshot" ? "Saved diff image" : "Saved image";
786
1159
  case "pdf":
787
1160
  return "Saved PDF";
788
1161
  case "profile":
@@ -951,12 +1324,71 @@ function getNavigationSummary(data: Record<string, unknown>): NavigationSummary
951
1324
  return isNavigationSummary(candidate) ? candidate : undefined;
952
1325
  }
953
1326
 
1327
+ function getTopLevelNavigationSummary(data: Record<string, unknown>): NavigationSummary | undefined {
1328
+ return isNavigationSummary(data)
1329
+ ? {
1330
+ title: typeof data.title === "string" ? data.title : undefined,
1331
+ url: typeof data.url === "string" ? data.url : undefined,
1332
+ }
1333
+ : undefined;
1334
+ }
1335
+
1336
+ function getNormalizedNavigationSummary(summary: NavigationSummary | undefined): { title?: string; url?: string } | undefined {
1337
+ const title = typeof summary?.title === "string" && summary.title.trim().length > 0 ? summary.title.trim() : undefined;
1338
+ const url = typeof summary?.url === "string" && summary.url.trim().length > 0 ? summary.url.trim() : undefined;
1339
+ return title || url ? { title, url } : undefined;
1340
+ }
1341
+
954
1342
  function formatNavigationSummary(summary: NavigationSummary): string | undefined {
955
- const title = typeof summary.title === "string" && summary.title.trim().length > 0 ? summary.title.trim() : undefined;
956
- const url = typeof summary.url === "string" && summary.url.trim().length > 0 ? summary.url.trim() : undefined;
957
- if (!title && !url) return undefined;
958
- if (title && url) return `${title}\n${url}`;
959
- return title ?? url;
1343
+ const normalized = getNormalizedNavigationSummary(summary);
1344
+ if (!normalized) return undefined;
1345
+ if (normalized.title && normalized.url) return `${normalized.title}\n${normalized.url}`;
1346
+ return normalized.title ?? normalized.url;
1347
+ }
1348
+
1349
+ function isPageChangeSummaryCommand(command: string | undefined): boolean {
1350
+ return command !== undefined && PAGE_CHANGE_SUMMARY_COMMANDS.has(command);
1351
+ }
1352
+
1353
+ function buildPageChangeSummary(options: {
1354
+ artifacts?: FileArtifactMetadata[];
1355
+ commandInfo: CommandInfo;
1356
+ data: unknown;
1357
+ nextActions?: Array<{ id: string }>;
1358
+ savedFilePath?: string;
1359
+ summary: string;
1360
+ }): AgentBrowserPageChangeSummary | undefined {
1361
+ const { artifacts, commandInfo, data, nextActions, savedFilePath } = options;
1362
+ const artifactCount = artifacts?.length ?? 0;
1363
+ const navigation = isRecord(data)
1364
+ ? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command) ? getTopLevelNavigationSummary(data) : undefined))
1365
+ : undefined;
1366
+ const confirmationRequired = detectConfirmationRequired(data) !== undefined;
1367
+ if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command)) {
1368
+ return undefined;
1369
+ }
1370
+ const changeType: AgentBrowserPageChangeSummary["changeType"] = savedFilePath || artifactCount > 0
1371
+ ? "artifact"
1372
+ : navigation
1373
+ ? "navigation"
1374
+ : confirmationRequired
1375
+ ? "confirmation"
1376
+ : "mutation";
1377
+ const parts = [commandInfo.command ?? "agent-browser", changeType];
1378
+ if (navigation?.title) parts.push(navigation.title);
1379
+ if (navigation?.url) parts.push(navigation.url);
1380
+ if (savedFilePath) parts.push(savedFilePath);
1381
+ else if (artifactCount > 0) parts.push(`${artifactCount} artifact${artifactCount === 1 ? "" : "s"}`);
1382
+ return {
1383
+ ...(artifactCount > 0 ? { artifactCount } : {}),
1384
+ changeType,
1385
+ ...(commandInfo.command ? { command: commandInfo.command } : {}),
1386
+ ...(nextActions ? { nextActionIds: nextActions.map((action) => action.id) } : {}),
1387
+ ...(savedFilePath ? { savedFilePath } : {}),
1388
+ summary: parts.join(" → "),
1389
+ ...(navigation?.title ? { title: navigation.title } : {}),
1390
+ ...(navigation?.url ? { url: navigation.url } : {}),
1391
+ };
960
1392
  }
961
1393
 
962
1394
  function stripNavigationSummary(data: Record<string, unknown>): Record<string, unknown> {
@@ -1055,6 +1487,65 @@ function getSelectorRecoveryHint(errorText: string): string | undefined {
1055
1487
  return undefined;
1056
1488
  }
1057
1489
 
1490
+ interface CommandSuggestion {
1491
+ args?: string[];
1492
+ description: string;
1493
+ id?: string;
1494
+ }
1495
+
1496
+ const UNKNOWN_COMMAND_SUGGESTIONS: Record<string, CommandSuggestion[]> = {
1497
+ attr: [
1498
+ { description: "Use `get attr <selector> <name>` to read an attribute from a selector or current `@ref`." },
1499
+ ],
1500
+ count: [
1501
+ { description: "Use `get count <selector>` to count matching elements." },
1502
+ ],
1503
+ html: [
1504
+ { description: "Use `get html <selector>` to read element HTML, or `get html` for the page when upstream supports it." },
1505
+ ],
1506
+ text: [
1507
+ { description: "Use `get text <selector>` to read text from a selector or current `@ref`; run `snapshot -i` first when you need a safe `@ref`." },
1508
+ ],
1509
+ title: [
1510
+ { args: ["get", "title"], description: "Use `get title` to read the current page title.", id: "use-get-title" },
1511
+ ],
1512
+ url: [
1513
+ { args: ["get", "url"], description: "Use `get url` to read the current page URL.", id: "use-get-url" },
1514
+ ],
1515
+ value: [
1516
+ { description: "Use `get value <selector>` to read form control value from a selector or current `@ref`." },
1517
+ ],
1518
+ };
1519
+
1520
+ function getUnknownCommandSuggestions(command: string | undefined, errorText: string): CommandSuggestion[] {
1521
+ if (!command) return [];
1522
+ const normalizedCommand = command.trim().toLowerCase();
1523
+ if (!/\bunknown\s+command\b|\bunknown\s+subcommand\b|\bunrecognized\s+command\b/i.test(errorText)) return [];
1524
+ return UNKNOWN_COMMAND_SUGGESTIONS[normalizedCommand] ?? [];
1525
+ }
1526
+
1527
+ function formatUnknownCommandSuggestionText(suggestions: CommandSuggestion[]): string | undefined {
1528
+ if (suggestions.length === 0) return undefined;
1529
+ return ["Agent-browser hint: This looks like a getter shortcut, but upstream getter commands are grouped under `get`.", ...suggestions.map((suggestion) => suggestion.description)].join(" ");
1530
+ }
1531
+
1532
+ function withSessionPrefix(sessionName: string | undefined, args: string[]): string[] {
1533
+ return sessionName ? ["--session", sessionName, ...args] : args;
1534
+ }
1535
+
1536
+ function buildUnknownCommandSuggestionActions(suggestions: CommandSuggestion[], sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
1537
+ const actions = suggestions
1538
+ .filter((suggestion): suggestion is CommandSuggestion & { args: string[]; id: string } => suggestion.args !== undefined && suggestion.id !== undefined)
1539
+ .map((suggestion) => ({
1540
+ id: suggestion.id,
1541
+ params: { args: withSessionPrefix(sessionName, suggestion.args) },
1542
+ reason: suggestion.description,
1543
+ safety: "Read-only getter command; safe to retry when you intended to inspect page state.",
1544
+ tool: "agent_browser" as const,
1545
+ }));
1546
+ return actions.length > 0 ? actions : undefined;
1547
+ }
1548
+
1058
1549
  function appendSelectorRecoveryHint(errorText: string): string {
1059
1550
  const hint = getSelectorRecoveryHint(errorText);
1060
1551
  if (!hint || errorText.includes("Agent-browser hint:")) {
@@ -1083,6 +1574,36 @@ function getBatchFailureDetails(steps: Array<{ details: BatchStepPresentationDet
1083
1574
  };
1084
1575
  }
1085
1576
 
1577
+ function hasModelFacingArgRedaction(args: string[] | undefined): boolean {
1578
+ return args?.some((arg) => arg === "[REDACTED]" || arg.includes("%5BREDACTED%5D") || arg.includes("[REDACTED]")) === true;
1579
+ }
1580
+
1581
+ function getStatefulCommandSensitiveValues(command: string[] | undefined): string[] {
1582
+ if (!command) return [];
1583
+ const tokens = extractCommandTokens(command);
1584
+ const values: string[] = [];
1585
+ if (tokens[0] === "cookies" && tokens[1] === "set" && tokens[3]) values.push(tokens[3]);
1586
+ if (tokens[0] === "storage" && ["local", "session"].includes(tokens[1] ?? "") && tokens[2] === "set" && tokens[4]) values.push(tokens[4]);
1587
+ for (let index = 0; index < tokens.length; index += 1) {
1588
+ const token = tokens[index];
1589
+ if (token === "--password" && tokens[index + 1]) values.push(tokens[index + 1]);
1590
+ else if (token?.startsWith("--password=")) values.push(token.slice("--password=".length));
1591
+ }
1592
+ return values.filter((value) => value.length > 0);
1593
+ }
1594
+
1595
+ function redactExactValues(value: unknown, sensitiveValues: string[]): unknown {
1596
+ if (sensitiveValues.length === 0) return redactSensitiveValue(value);
1597
+ if (typeof value === "string") {
1598
+ let redacted = value;
1599
+ for (const sensitiveValue of sensitiveValues) redacted = redacted.split(sensitiveValue).join("[REDACTED]");
1600
+ return redactSensitiveText(redacted);
1601
+ }
1602
+ if (Array.isArray(value)) return value.map((item) => redactExactValues(item, sensitiveValues));
1603
+ if (!isRecord(value)) return value;
1604
+ return redactSensitiveValue(Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactValues(entryValue, sensitiveValues)])));
1605
+ }
1606
+
1086
1607
  async function buildBatchStepPresentation(options: {
1087
1608
  artifactManifest?: SessionArtifactManifest;
1088
1609
  artifactRequest?: ArtifactRequestContext;
@@ -1094,21 +1615,43 @@ async function buildBatchStepPresentation(options: {
1094
1615
  }): Promise<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> {
1095
1616
  const { artifactManifest, artifactRequest, cwd, index, item, persistentArtifactStore, sessionName } = options;
1096
1617
  const command = isStringArray(item.command) ? item.command : undefined;
1097
- const commandText = formatBatchStepCommand(command, index);
1618
+ const redactedCommand = command ? redactInvocationArgs(command) : undefined;
1619
+ const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
1098
1620
 
1099
1621
  if (item.success === false) {
1100
- const errorText = formatBatchStepError(item.error);
1622
+ const redactedErrorData = redactExactValues(item.error, getStatefulCommandSensitiveValues(command));
1623
+ const errorText = formatBatchStepError(redactedErrorData);
1624
+ const failureCategory = classifyAgentBrowserFailureCategory({
1625
+ args: command,
1626
+ command: command?.[0],
1627
+ errorText,
1628
+ });
1629
+ const confirmationRequired = detectConfirmationRequired(item.error);
1630
+ const nextActions = buildAgentBrowserNextActions({
1631
+ args: command,
1632
+ command: command?.[0],
1633
+ confirmationId: confirmationRequired?.id,
1634
+ failureCategory,
1635
+ resultCategory: "failure",
1636
+ });
1101
1637
  const presentation: ToolPresentation = {
1102
1638
  content: [{ type: "text", text: errorText }],
1639
+ failureCategory,
1640
+ nextActions,
1641
+ resultCategory: "failure",
1103
1642
  summary: errorText,
1104
1643
  };
1105
1644
  return {
1106
1645
  details: {
1646
+ artifactVerification: presentation.artifactVerification,
1107
1647
  artifacts: presentation.artifacts,
1108
- command,
1648
+ command: redactedCommand,
1109
1649
  commandText,
1110
- data: item.error,
1650
+ data: redactedErrorData,
1651
+ failureCategory,
1111
1652
  index,
1653
+ nextActions,
1654
+ resultCategory: "failure",
1112
1655
  success: false,
1113
1656
  summary: errorText,
1114
1657
  text: errorText,
@@ -1122,6 +1665,7 @@ async function buildBatchStepPresentation(options: {
1122
1665
  artifactRequest,
1123
1666
  commandInfo: parseCommandInfo(command ?? []),
1124
1667
  cwd,
1668
+ args: command,
1125
1669
  envelope: { data: item.result, success: true },
1126
1670
  persistentArtifactStore,
1127
1671
  sessionName,
@@ -1135,11 +1679,28 @@ async function buildBatchStepPresentation(options: {
1135
1679
  secondaryPaths: presentation.imagePaths,
1136
1680
  });
1137
1681
  const text = getPresentationText(presentation) || presentation.summary;
1682
+ const nextActions = buildAgentBrowserNextActions({
1683
+ artifacts: presentation.artifacts,
1684
+ args: command,
1685
+ command: command?.[0],
1686
+ resultCategory: "success",
1687
+ savedFilePath: presentation.savedFilePath,
1688
+ successCategory: presentation.successCategory,
1689
+ });
1690
+ const pageChangeSummary = buildPageChangeSummary({
1691
+ artifacts: presentation.artifacts,
1692
+ commandInfo: parseCommandInfo(command ?? []),
1693
+ data: presentation.data,
1694
+ nextActions,
1695
+ savedFilePath: presentation.savedFilePath,
1696
+ summary: presentation.summary,
1697
+ });
1138
1698
 
1139
1699
  return {
1140
1700
  details: {
1701
+ artifactVerification: presentation.artifactVerification,
1141
1702
  artifacts: presentation.artifacts,
1142
- command,
1703
+ command: redactedCommand,
1143
1704
  commandText,
1144
1705
  data: presentation.data,
1145
1706
  fullOutputPath: fullOutputPaths[0],
@@ -1147,9 +1708,13 @@ async function buildBatchStepPresentation(options: {
1147
1708
  imagePath: imagePaths[0],
1148
1709
  imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
1149
1710
  index,
1711
+ nextActions,
1712
+ pageChangeSummary,
1713
+ resultCategory: "success",
1150
1714
  savedFile: presentation.savedFile,
1151
1715
  savedFilePath: presentation.savedFilePath,
1152
1716
  success: true,
1717
+ successCategory: classifyPresentationSuccessCategory({ artifactVerification: presentation.artifactVerification, artifacts: presentation.artifacts, savedFile: presentation.savedFile }),
1153
1718
  summary: presentation.summary,
1154
1719
  text,
1155
1720
  },
@@ -1195,6 +1760,7 @@ async function buildBatchPresentation(options: {
1195
1760
  const batchFailure = getBatchFailureDetails(steps);
1196
1761
  const images = steps.flatMap((step) => getPresentationImages(step.presentation));
1197
1762
  const artifacts = steps.flatMap((step) => step.presentation.artifacts ?? []);
1763
+ const artifactVerification = buildArtifactVerificationSummary(artifacts);
1198
1764
  const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
1199
1765
  primaryPath: step.presentation.fullOutputPath,
1200
1766
  secondaryPaths: step.presentation.fullOutputPaths,
@@ -1203,6 +1769,11 @@ async function buildBatchPresentation(options: {
1203
1769
  primaryPath: step.presentation.imagePath,
1204
1770
  secondaryPaths: step.presentation.imagePaths,
1205
1771
  }));
1772
+ const redactedBatchData = steps.map(({ details }) => (
1773
+ details.success
1774
+ ? { command: details.command, result: details.data, success: true }
1775
+ : { command: details.command, error: details.text, success: false }
1776
+ ));
1206
1777
  const stepText =
1207
1778
  steps.length === 0
1208
1779
  ? "(no batch steps)"
@@ -1235,18 +1806,45 @@ async function buildBatchPresentation(options: {
1235
1806
  const artifactRetentionSummary = currentArtifactManifest ? formatSessionArtifactRetentionSummary(currentArtifactManifest) : undefined;
1236
1807
  const contentText = artifactRetentionSummary && manifestHasNewNoticeWorthyEntries(options.artifactManifest, currentArtifactManifest) ? `${text}\n\n${artifactRetentionSummary}` : text;
1237
1808
 
1809
+ const nextActions = batchFailure
1810
+ ? batchFailure.failedStep.nextActions
1811
+ : buildAgentBrowserNextActions({ artifacts, command: "batch", resultCategory: "success" });
1812
+ const changedSteps = steps.map((step) => step.details).filter((details) => details.pageChangeSummary !== undefined);
1813
+ const pageChangeSummary = artifacts.length > 0
1814
+ ? buildPageChangeSummary({
1815
+ artifacts,
1816
+ commandInfo: { command: "batch" },
1817
+ data,
1818
+ nextActions,
1819
+ summary,
1820
+ })
1821
+ : changedSteps.length > 0
1822
+ ? {
1823
+ changeType: "mutation" as const,
1824
+ command: "batch",
1825
+ nextActionIds: nextActions?.map((action) => action.id),
1826
+ summary: `batch → mutation → ${changedSteps.length} changed step${changedSteps.length === 1 ? "" : "s"}`,
1827
+ }
1828
+ : undefined;
1829
+
1238
1830
  return {
1239
1831
  artifactManifest: currentArtifactManifest,
1240
1832
  artifactRetentionSummary,
1833
+ artifactVerification,
1241
1834
  artifacts: artifacts.length > 0 ? artifacts : undefined,
1242
1835
  batchFailure,
1243
1836
  batchSteps: steps.map((step) => step.details),
1244
1837
  content: [{ type: "text", text: contentText }, ...images],
1245
- data,
1838
+ failureCategory: batchFailure?.failedStep.failureCategory,
1839
+ data: redactedBatchData,
1246
1840
  fullOutputPath: fullOutputPaths[0],
1247
1841
  fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
1248
1842
  imagePath: imagePaths[0],
1249
1843
  imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
1844
+ nextActions,
1845
+ pageChangeSummary,
1846
+ resultCategory: batchFailure ? "failure" : "success",
1847
+ successCategory: batchFailure ? undefined : classifyPresentationSuccessCategory({ artifactVerification, artifacts }),
1250
1848
  summary,
1251
1849
  };
1252
1850
  }
@@ -1270,6 +1868,9 @@ function formatSummary(commandInfo: CommandInfo, data: unknown): string {
1270
1868
  if (commandInfo.command === "skills" && commandInfo.subcommand === "get") {
1271
1869
  return "agent-browser skill loaded";
1272
1870
  }
1871
+ if (commandInfo.command === "skills" && commandInfo.subcommand === "path") {
1872
+ return "agent-browser skill path";
1873
+ }
1273
1874
  if (isRecord(data)) {
1274
1875
  const navigationSummary = getNavigationSummary(data);
1275
1876
  if (navigationSummary && isNavigationObservableCommand(commandInfo.command)) {
@@ -1319,6 +1920,10 @@ function formatContentText(commandInfo: CommandInfo, data: unknown): string {
1319
1920
  return formatConfirmationRequiredText(confirmationRequired);
1320
1921
  }
1321
1922
 
1923
+ const skillsText = formatSkillsText(commandInfo, data);
1924
+ if (skillsText) {
1925
+ return skillsText;
1926
+ }
1322
1927
  if (typeof data === "string") {
1323
1928
  return redactModelFacingText(data);
1324
1929
  }
@@ -1328,9 +1933,6 @@ function formatContentText(commandInfo: CommandInfo, data: unknown): string {
1328
1933
  if (Array.isArray(data) && commandInfo.command === "profiles") {
1329
1934
  return formatProfilesText(data, "Chrome profiles");
1330
1935
  }
1331
- if (Array.isArray(data) && commandInfo.command === "skills") {
1332
- return formatSkillsText(commandInfo, data) ?? stringifyModelFacing(data);
1333
- }
1334
1936
  if (!isRecord(data)) {
1335
1937
  return stringifyModelFacing(data);
1336
1938
  }
@@ -1359,10 +1961,6 @@ function formatContentText(commandInfo: CommandInfo, data: unknown): string {
1359
1961
  const screenshotSummary = getScreenshotSummary(data);
1360
1962
  if (screenshotSummary) return screenshotSummary;
1361
1963
  }
1362
- const skillsText = formatSkillsText(commandInfo, data);
1363
- if (skillsText) {
1364
- return skillsText;
1365
- }
1366
1964
  const extractionText = formatExtractionText(commandInfo, data);
1367
1965
  if (extractionText) {
1368
1966
  return extractionText;
@@ -1595,6 +2193,7 @@ async function compactLargePresentationOutput(options: {
1595
2193
 
1596
2194
  export async function buildToolPresentation(options: {
1597
2195
  artifactManifest?: SessionArtifactManifest;
2196
+ args?: string[];
1598
2197
  artifactRequest?: ArtifactRequestContext;
1599
2198
  batchArtifactRequests?: Array<ArtifactRequestContext | undefined>;
1600
2199
  commandInfo: CommandInfo;
@@ -1604,17 +2203,32 @@ export async function buildToolPresentation(options: {
1604
2203
  persistentArtifactStore?: PersistentSessionArtifactStore;
1605
2204
  sessionName?: string;
1606
2205
  }): Promise<ToolPresentation> {
1607
- const { artifactManifest, artifactRequest, commandInfo, cwd, envelope, errorText, persistentArtifactStore, sessionName } = options;
2206
+ const { args, artifactManifest, artifactRequest, commandInfo, cwd, envelope, errorText, persistentArtifactStore, sessionName } = options;
1608
2207
  if (errorText) {
1609
- const hintedErrorText = appendSelectorRecoveryHint(redactModelFacingText(errorText));
2208
+ const safeErrorText = redactModelFacingText(errorText);
2209
+ const selectorHintedErrorText = appendSelectorRecoveryHint(safeErrorText);
2210
+ const unknownCommandSuggestions = getUnknownCommandSuggestions(commandInfo.command, safeErrorText);
2211
+ const unknownCommandSuggestionText = formatUnknownCommandSuggestionText(unknownCommandSuggestions);
2212
+ const hintedErrorText = unknownCommandSuggestionText && !selectorHintedErrorText.includes("Agent-browser hint:")
2213
+ ? `${selectorHintedErrorText}\n\n${unknownCommandSuggestionText}`
2214
+ : selectorHintedErrorText;
2215
+ const categoryDetails = buildAgentBrowserResultCategoryDetails({ args: [commandInfo.command, commandInfo.subcommand].filter((item): item is string => item !== undefined), command: commandInfo.command, errorText: hintedErrorText, succeeded: false });
2216
+ const nextActions = [
2217
+ ...(buildUnknownCommandSuggestionActions(unknownCommandSuggestions, sessionName) ?? []),
2218
+ ...(buildAgentBrowserNextActions({ args, command: commandInfo.command, failureCategory: categoryDetails.failureCategory, resultCategory: "failure" }) ?? []),
2219
+ ];
1610
2220
  return {
2221
+ ...categoryDetails,
1611
2222
  content: [{ type: "text", text: hintedErrorText }],
2223
+ nextActions: nextActions.length > 0 ? nextActions : undefined,
1612
2224
  summary: hintedErrorText,
1613
2225
  };
1614
2226
  }
1615
2227
 
1616
2228
  const data = enrichStreamStatusData(commandInfo, envelope?.data);
2229
+ const presentationData = redactPresentationData(commandInfo, data);
1617
2230
  const artifacts = await extractFileArtifacts({ artifactRequest, commandInfo, cwd, data, sessionName });
2231
+ const artifactVerification = buildArtifactVerificationSummary(artifacts);
1618
2232
  const artifactSummary = formatArtifactSummary(artifacts);
1619
2233
  const summary = artifactSummary ?? formatSummary(commandInfo, data);
1620
2234
  const artifactText = artifacts.length > 0 ? formatArtifactMetadataLines(artifacts).join("\n") : undefined;
@@ -1624,14 +2238,16 @@ export async function buildToolPresentation(options: {
1624
2238
  : commandInfo.command === "snapshot" && isRecord(data)
1625
2239
  ? await buildSnapshotPresentation(data, persistentArtifactStore, artifactManifest)
1626
2240
  : {
2241
+ artifactVerification,
1627
2242
  artifacts: artifacts.length > 0 ? artifacts : undefined,
1628
2243
  content: [{ type: "text" as const, text: artifactText ?? formatContentText(commandInfo, data) }],
1629
- data,
2244
+ data: presentationData,
1630
2245
  summary,
1631
2246
  };
1632
2247
  if (artifacts.length > 0 && !presentation.artifacts) {
1633
2248
  presentation.artifacts = artifacts;
1634
2249
  }
2250
+ presentation.artifactVerification = presentation.artifactVerification ?? artifactVerification;
1635
2251
  if (isRecord(data)) {
1636
2252
  const savedFile = getSavedFileDetails(commandInfo, data);
1637
2253
  if (savedFile) {
@@ -1645,15 +2261,68 @@ export async function buildToolPresentation(options: {
1645
2261
  const compactedPresentation = await compactLargePresentationOutput({
1646
2262
  artifactManifest,
1647
2263
  commandInfo,
1648
- data,
2264
+ data: presentationData,
1649
2265
  persistentArtifactStore,
1650
2266
  presentation: presentationWithImage,
1651
2267
  });
1652
- return sanitizeModelFacingPresentation(
1653
- applyArtifactManifest(
1654
- compactedPresentation,
1655
- compactedPresentation.artifactManifest ?? artifactManifest,
1656
- buildManifestEntriesForFileArtifacts(artifacts.filter(isManifestFileArtifact)),
1657
- ),
2268
+ const presentationWithManifest = applyArtifactManifest(
2269
+ compactedPresentation,
2270
+ compactedPresentation.artifactManifest ?? artifactManifest,
2271
+ buildManifestEntriesForFileArtifacts(artifacts.filter(isManifestFileArtifact)),
1658
2272
  );
2273
+ const currentSpillPaths = new Set(getPresentationPaths({
2274
+ primaryPath: presentationWithManifest.fullOutputPath,
2275
+ secondaryPaths: presentationWithManifest.fullOutputPaths,
2276
+ }));
2277
+ presentationWithManifest.artifactVerification = buildArtifactVerificationSummary(
2278
+ artifacts,
2279
+ presentationWithManifest.artifactManifest,
2280
+ currentSpillPaths,
2281
+ ) ?? presentationWithManifest.artifactVerification;
2282
+ const confirmationRequired = detectConfirmationRequired(data);
2283
+ if (!presentationWithManifest.resultCategory) {
2284
+ const categoryDetails = buildAgentBrowserResultCategoryDetails({
2285
+ artifacts: presentationWithManifest.artifacts,
2286
+ command: commandInfo.command,
2287
+ confirmationRequired: confirmationRequired !== undefined,
2288
+ errorText: envelope?.success === false ? presentationWithManifest.summary : undefined,
2289
+ savedFile: presentationWithManifest.savedFile,
2290
+ succeeded: envelope?.success !== false,
2291
+ });
2292
+ presentationWithManifest.resultCategory = categoryDetails.resultCategory;
2293
+ presentationWithManifest.successCategory = categoryDetails.resultCategory === "success"
2294
+ ? classifyPresentationSuccessCategory({
2295
+ artifactVerification: presentationWithManifest.artifactVerification,
2296
+ artifacts: presentationWithManifest.artifacts,
2297
+ savedFile: presentationWithManifest.savedFile,
2298
+ })
2299
+ : categoryDetails.successCategory;
2300
+ presentationWithManifest.failureCategory = categoryDetails.failureCategory;
2301
+ }
2302
+ if (presentationWithManifest.resultCategory === "success") {
2303
+ presentationWithManifest.successCategory = classifyPresentationSuccessCategory({
2304
+ artifactVerification: presentationWithManifest.artifactVerification,
2305
+ artifacts: presentationWithManifest.artifacts,
2306
+ savedFile: presentationWithManifest.savedFile,
2307
+ });
2308
+ }
2309
+ presentationWithManifest.nextActions = presentationWithManifest.nextActions ?? buildAgentBrowserNextActions({
2310
+ artifacts: presentationWithManifest.artifacts,
2311
+ args,
2312
+ command: commandInfo.command,
2313
+ confirmationId: confirmationRequired?.id,
2314
+ failureCategory: presentationWithManifest.failureCategory,
2315
+ resultCategory: presentationWithManifest.resultCategory ?? "success",
2316
+ savedFilePath: presentationWithManifest.savedFilePath,
2317
+ successCategory: presentationWithManifest.successCategory,
2318
+ });
2319
+ presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
2320
+ artifacts: presentationWithManifest.artifacts,
2321
+ commandInfo,
2322
+ data,
2323
+ nextActions: presentationWithManifest.nextActions,
2324
+ savedFilePath: presentationWithManifest.savedFilePath,
2325
+ summary: presentationWithManifest.summary,
2326
+ });
2327
+ return sanitizeModelFacingPresentation(presentationWithManifest);
1659
2328
  }