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.
- package/CHANGELOG.md +48 -1
- package/README.md +137 -13
- package/docs/ARCHITECTURE.md +54 -7
- package/docs/COMMAND_REFERENCE.md +586 -42
- package/docs/RELEASE.md +61 -7
- package/docs/REQUIREMENTS.md +14 -1
- package/docs/SUPPORT_MATRIX.md +85 -0
- package/docs/TOOL_CONTRACT.md +301 -24
- package/extensions/agent-browser/index.ts +1983 -38
- package/extensions/agent-browser/lib/playbook.ts +23 -12
- package/extensions/agent-browser/lib/results/presentation.ts +706 -37
- package/extensions/agent-browser/lib/results/shared.ts +437 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +69 -9
- package/extensions/agent-browser/lib/results.ts +12 -0
- package/extensions/agent-browser/lib/runtime.ts +82 -10
- package/package.json +4 -2
- package/scripts/agent-browser-capability-baseline.mjs +499 -110
- package/scripts/doctor.mjs +1 -1
|
@@ -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 === "
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
956
|
-
|
|
957
|
-
if (
|
|
958
|
-
|
|
959
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
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
|
}
|