pi-agent-browser-native 0.2.24 → 0.2.25
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 +26 -0
- package/README.md +122 -9
- package/docs/ARCHITECTURE.md +48 -7
- package/docs/COMMAND_REFERENCE.md +567 -40
- package/docs/RELEASE.md +57 -7
- package/docs/REQUIREMENTS.md +13 -1
- package/docs/SUPPORT_MATRIX.md +65 -0
- package/docs/TOOL_CONTRACT.md +265 -19
- package/extensions/agent-browser/index.ts +986 -25
- package/extensions/agent-browser/lib/playbook.ts +20 -10
- package/extensions/agent-browser/lib/results/presentation.ts +624 -33
- package/extensions/agent-browser/lib/results/shared.ts +365 -0
- package/extensions/agent-browser/lib/results.ts +12 -0
- package/extensions/agent-browser/lib/runtime.ts +75 -8
- 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,13 @@ import { buildSnapshotPresentation, formatRawSnapshotText, formatSnapshotSummary
|
|
|
22
22
|
import {
|
|
23
23
|
type AgentBrowserBatchResult,
|
|
24
24
|
type AgentBrowserEnvelope,
|
|
25
|
+
type AgentBrowserPageChangeSummary,
|
|
26
|
+
type ArtifactVerificationEntry,
|
|
27
|
+
type ArtifactVerificationSummary,
|
|
28
|
+
buildAgentBrowserNextActions,
|
|
29
|
+
buildAgentBrowserResultCategoryDetails,
|
|
30
|
+
classifyAgentBrowserFailureCategory,
|
|
31
|
+
classifyAgentBrowserSuccessCategory,
|
|
25
32
|
type BatchFailurePresentationDetails,
|
|
26
33
|
type BatchStepPresentationDetails,
|
|
27
34
|
type ArtifactStorageScope,
|
|
@@ -50,6 +57,32 @@ const IMAGE_EXTENSION_TO_MIME_TYPE: Record<string, string> = {
|
|
|
50
57
|
const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
|
|
51
58
|
const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
|
|
52
59
|
const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
|
|
60
|
+
const PAGE_CHANGE_SUMMARY_COMMANDS = new Set([
|
|
61
|
+
"back",
|
|
62
|
+
"check",
|
|
63
|
+
"click",
|
|
64
|
+
"dblclick",
|
|
65
|
+
"dialog",
|
|
66
|
+
"download",
|
|
67
|
+
"fill",
|
|
68
|
+
"forward",
|
|
69
|
+
"goto",
|
|
70
|
+
"hover",
|
|
71
|
+
"navigate",
|
|
72
|
+
"open",
|
|
73
|
+
"pdf",
|
|
74
|
+
"press",
|
|
75
|
+
"pushstate",
|
|
76
|
+
"reload",
|
|
77
|
+
"screenshot",
|
|
78
|
+
"scroll",
|
|
79
|
+
"scrollintoview",
|
|
80
|
+
"select",
|
|
81
|
+
"swipe",
|
|
82
|
+
"tap",
|
|
83
|
+
"type",
|
|
84
|
+
"uncheck",
|
|
85
|
+
]);
|
|
53
86
|
const NAVIGATION_SUMMARY_FIELD = "navigationSummary";
|
|
54
87
|
const LARGE_OUTPUT_INLINE_MAX_CHARS = 8_000;
|
|
55
88
|
const LARGE_OUTPUT_INLINE_MAX_LINES = 120;
|
|
@@ -229,13 +262,89 @@ function formatDiagnosticSummary(commandInfo: CommandInfo, data: Record<string,
|
|
|
229
262
|
if (profiles) return `Auth profiles: ${profiles.length}`;
|
|
230
263
|
const name = getStringField(data, "name") ?? getStringField(data, "profile") ?? commandInfo.subcommand;
|
|
231
264
|
if (name && commandInfo.subcommand === "show") return `Auth profile: ${name}`;
|
|
265
|
+
if (name && ["save", "login", "delete"].includes(commandInfo.subcommand ?? "")) return `Auth ${commandInfo.subcommand}: ${name}`;
|
|
232
266
|
}
|
|
233
267
|
|
|
234
|
-
if (commandInfo.command === "
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
268
|
+
if (commandInfo.command === "cookies") {
|
|
269
|
+
const cookies = getArrayField(data, "cookies");
|
|
270
|
+
if (cookies) return `Cookies: ${cookies.length}`;
|
|
271
|
+
const name = getStringField(data, "name");
|
|
272
|
+
if (name) return name;
|
|
273
|
+
if (data.set === true) return "Cookie set";
|
|
274
|
+
if (data.cleared === true || data.clear === true) return "Cookies cleared";
|
|
237
275
|
}
|
|
238
276
|
|
|
277
|
+
if (commandInfo.command === "storage") {
|
|
278
|
+
const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
|
|
279
|
+
if (entries) return `Storage entries: ${entries.length}`;
|
|
280
|
+
const key = getStringField(data, "key");
|
|
281
|
+
if (key && (commandInfo.subcommand === "set" || data.set === true || Object.hasOwn(data, "value"))) return `Storage set: ${key}`;
|
|
282
|
+
if (data.cleared === true || data.clear === true) return "Storage cleared";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (commandInfo.command === "dialog") {
|
|
286
|
+
const open = typeof data.open === "boolean" ? data.open : undefined;
|
|
287
|
+
if (open !== undefined) return open ? "Dialog open" : "No dialog open";
|
|
288
|
+
if (data.accepted === true) return "Dialog accepted";
|
|
289
|
+
if (data.dismissed === true) return "Dialog dismissed";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (commandInfo.command === "frame") {
|
|
293
|
+
const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector") ?? commandInfo.subcommand;
|
|
294
|
+
if (frame) return `Frame: ${frame}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (commandInfo.command === "state") {
|
|
298
|
+
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
299
|
+
if (states) return `States: ${states.length}`;
|
|
300
|
+
if (commandInfo.subcommand === "load") return undefined;
|
|
301
|
+
const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "path") ?? commandInfo.subcommand;
|
|
302
|
+
if (stateName) return `State ${commandInfo.subcommand ?? "result"}: ${stateName}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (commandInfo.command === "network") {
|
|
306
|
+
if (commandInfo.subcommand === "requests") {
|
|
307
|
+
const requests = getArrayField(data, "requests");
|
|
308
|
+
if (requests) return `Network requests: ${requests.length}`;
|
|
309
|
+
}
|
|
310
|
+
if (commandInfo.subcommand === "route") {
|
|
311
|
+
const routed = getStringField(data, "routed") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
|
|
312
|
+
return routed ? `Network route: ${redactModelFacingTextIfSensitive(routed)}` : "Network route configured";
|
|
313
|
+
}
|
|
314
|
+
if (commandInfo.subcommand === "unroute") {
|
|
315
|
+
const unrouted = getStringField(data, "unrouted") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
|
|
316
|
+
return unrouted ? `Network unroute: ${redactModelFacingTextIfSensitive(unrouted)}` : "Network route removed";
|
|
317
|
+
}
|
|
318
|
+
if (commandInfo.subcommand === "har") {
|
|
319
|
+
const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
|
|
320
|
+
return `Network HAR: ${state}`;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (commandInfo.command === "diff") {
|
|
325
|
+
if (commandInfo.subcommand === "snapshot") return "Snapshot diff completed";
|
|
326
|
+
if (commandInfo.subcommand === "url") return "URL diff completed";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (["trace", "profiler"].includes(commandInfo.command ?? "")) {
|
|
330
|
+
const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
|
|
331
|
+
if (state) return `${commandInfo.command === "trace" ? "Trace" : "Profiler"}: ${state}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (commandInfo.command === "highlight") return "Element highlighted";
|
|
335
|
+
if (commandInfo.command === "inspect") return "DevTools inspect opened";
|
|
336
|
+
if (commandInfo.command === "clipboard") return `Clipboard ${commandInfo.subcommand ?? "completed"}`;
|
|
337
|
+
|
|
338
|
+
if (commandInfo.command === "stream") {
|
|
339
|
+
if (commandInfo.subcommand === "enable") {
|
|
340
|
+
const port = typeof data.port === "number" ? ` on port ${data.port}` : "";
|
|
341
|
+
return `Stream enabled${port}`;
|
|
342
|
+
}
|
|
343
|
+
if (commandInfo.subcommand === "disable") return "Stream disabled";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (commandInfo.command === "chat") return "Chat response";
|
|
347
|
+
|
|
239
348
|
if (commandInfo.command === "console") {
|
|
240
349
|
const messages = getArrayField(data, "messages");
|
|
241
350
|
if (messages) return `Console messages: ${messages.length}`;
|
|
@@ -417,6 +526,7 @@ function formatNativeSkillContent(content: string): string {
|
|
|
417
526
|
|
|
418
527
|
function formatSkillsText(commandInfo: CommandInfo, data: unknown): string | undefined {
|
|
419
528
|
if (commandInfo.command !== "skills") return undefined;
|
|
529
|
+
if (commandInfo.subcommand === "path") return typeof data === "string" ? redactModelFacingText(data) : undefined;
|
|
420
530
|
if (commandInfo.subcommand === "list" && Array.isArray(data)) return formatSkillsListText(data);
|
|
421
531
|
const content = getSkillContent(data);
|
|
422
532
|
if (content) {
|
|
@@ -557,6 +667,15 @@ function formatDashboardText(data: Record<string, unknown>): string | undefined
|
|
|
557
667
|
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
558
668
|
}
|
|
559
669
|
|
|
670
|
+
function formatChatText(data: Record<string, unknown>): string | undefined {
|
|
671
|
+
const response = getStringField(data, "response") ?? getStringField(data, "message") ?? getStringField(data, "text") ?? getStringField(data, "result");
|
|
672
|
+
if (response) return redactModelFacingText(response);
|
|
673
|
+
const model = getStringField(data, "model");
|
|
674
|
+
const provider = getStringField(data, "provider");
|
|
675
|
+
const lines = [model ? `Model: ${redactModelFacingText(model)}` : undefined, provider ? `Provider: ${redactModelFacingText(provider)}` : undefined].filter(Boolean);
|
|
676
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
677
|
+
}
|
|
678
|
+
|
|
560
679
|
function formatDoctorText(data: Record<string, unknown>): string | undefined {
|
|
561
680
|
const lines: string[] = [];
|
|
562
681
|
const status = getStringField(data, "status") ?? getStringField(data, "result");
|
|
@@ -568,6 +687,123 @@ function formatDoctorText(data: Record<string, unknown>): string | undefined {
|
|
|
568
687
|
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
569
688
|
}
|
|
570
689
|
|
|
690
|
+
function formatCookieRecordText(item: Record<string, unknown>, fallbackName: string): string {
|
|
691
|
+
const name = redactModelFacingText(getStringField(item, "name") ?? fallbackName);
|
|
692
|
+
const domain = getStringField(item, "domain");
|
|
693
|
+
const path = getStringField(item, "path");
|
|
694
|
+
const flags = [item.httpOnly === true ? "httpOnly" : undefined, item.secure === true ? "secure" : undefined].filter(Boolean).join(", ");
|
|
695
|
+
const location = [domain, path].filter(Boolean).join("");
|
|
696
|
+
return [name, location ? `(${redactModelFacingText(location)})` : undefined, flags ? `[${flags}]` : undefined].filter(Boolean).join(" ");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function formatCookiesText(data: Record<string, unknown>): string | undefined {
|
|
700
|
+
const cookies = getArrayField(data, "cookies");
|
|
701
|
+
if (cookies) {
|
|
702
|
+
if (cookies.length === 0) return "No cookies.";
|
|
703
|
+
return cookies
|
|
704
|
+
.map((item, index) => (isRecord(item) ? formatCookieRecordText(item, `(cookie ${index + 1})`) : `${index + 1}. [REDACTED]`))
|
|
705
|
+
.join("\n");
|
|
706
|
+
}
|
|
707
|
+
if (getStringField(data, "name") || getStringField(data, "domain") || getStringField(data, "path") || Object.hasOwn(data, "value")) {
|
|
708
|
+
return formatCookieRecordText(data, "cookie");
|
|
709
|
+
}
|
|
710
|
+
if (data.set === true) return "Cookie set.";
|
|
711
|
+
if (data.cleared === true || data.clear === true) return "Cookies cleared.";
|
|
712
|
+
return undefined;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function formatStorageText(data: Record<string, unknown>): string | undefined {
|
|
716
|
+
const type = getStringField(data, "type") ?? getStringField(data, "storage") ?? "storage";
|
|
717
|
+
const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
|
|
718
|
+
if (entries) {
|
|
719
|
+
if (entries.length === 0) return `${type}: no entries.`;
|
|
720
|
+
return entries
|
|
721
|
+
.map((item, index) => {
|
|
722
|
+
if (!isRecord(item)) return `${index + 1}. [REDACTED]`;
|
|
723
|
+
const rawKey = getStringField(item, "key") ?? getStringField(item, "name") ?? `(entry ${index + 1})`;
|
|
724
|
+
const key = redactModelFacingText(rawKey);
|
|
725
|
+
return Object.hasOwn(item, "value") ? `${key}: [REDACTED]` : key;
|
|
726
|
+
})
|
|
727
|
+
.join("\n");
|
|
728
|
+
}
|
|
729
|
+
const key = getStringField(data, "key");
|
|
730
|
+
if (key && Object.hasOwn(data, "value")) return `${type} ${redactModelFacingText(key)}: [REDACTED]`;
|
|
731
|
+
if (key && data.set === true) return `${type} set: ${redactModelFacingText(key)}`;
|
|
732
|
+
if (data.cleared === true || data.clear === true) return `${type} cleared.`;
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function formatDialogText(data: Record<string, unknown>): string | undefined {
|
|
737
|
+
const lines: string[] = [];
|
|
738
|
+
if (typeof data.open === "boolean") lines.push(data.open ? "Dialog open." : "No dialog open.");
|
|
739
|
+
const type = getStringField(data, "type");
|
|
740
|
+
if (type) lines.push(`Type: ${redactModelFacingText(type)}`);
|
|
741
|
+
const message = getStringField(data, "message");
|
|
742
|
+
if (message) lines.push(`Message: ${/(?:auth|authorization|bearer|cookie|pass(?:word)?|secret|session|token)/i.test(message) ? "[REDACTED]" : redactModelFacingText(message)}`);
|
|
743
|
+
if (data.accepted === true) lines.push("Accepted.");
|
|
744
|
+
if (data.dismissed === true) lines.push("Dismissed.");
|
|
745
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function formatFrameText(data: Record<string, unknown>): string | undefined {
|
|
749
|
+
const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector");
|
|
750
|
+
const url = getStringField(data, "url");
|
|
751
|
+
const title = getStringField(data, "title");
|
|
752
|
+
const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactModelFacingTextIfSensitive(url)}` : undefined].filter(Boolean);
|
|
753
|
+
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function formatStateText(data: Record<string, unknown>): string | undefined {
|
|
757
|
+
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
758
|
+
if (states) {
|
|
759
|
+
if (states.length === 0) return "No saved states.";
|
|
760
|
+
return states
|
|
761
|
+
.map((item, index) => {
|
|
762
|
+
if (!isRecord(item)) return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
|
|
763
|
+
const name = getStringField(item, "name") ?? getStringField(item, "file") ?? getStringField(item, "path") ?? `(state ${index + 1})`;
|
|
764
|
+
const url = getStringField(item, "url");
|
|
765
|
+
return url ? `${index + 1}. ${redactModelFacingText(name)} — ${redactModelFacingTextIfSensitive(url)}` : `${index + 1}. ${redactModelFacingText(name)}`;
|
|
766
|
+
})
|
|
767
|
+
.join("\n");
|
|
768
|
+
}
|
|
769
|
+
if (data.loaded === true) return `State loaded: ${redactModelFacingText(getStringField(data, "path") ?? getStringField(data, "name") ?? "ok")}`;
|
|
770
|
+
if (data.cleared === true || data.clear === true) return "State cleared.";
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function isSensitivePresentationField(key: string): boolean {
|
|
775
|
+
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);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function redactStructuredPresentationValue(value: unknown): unknown {
|
|
779
|
+
if (typeof value === "string") return redactModelFacingTextIfSensitive(value);
|
|
780
|
+
if (Array.isArray(value)) return value.map((item) => redactStructuredPresentationValue(item));
|
|
781
|
+
if (!isRecord(value)) return value;
|
|
782
|
+
return Object.fromEntries(
|
|
783
|
+
Object.entries(value).map(([key, entryValue]) => [
|
|
784
|
+
key,
|
|
785
|
+
isSensitivePresentationField(key) ? "[REDACTED]" : redactStructuredPresentationValue(entryValue),
|
|
786
|
+
]),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function redactStatefulValues(value: unknown, sensitiveKeys: Set<string>): unknown {
|
|
791
|
+
if (Array.isArray(value)) return value.map((item) => redactStatefulValues(item, sensitiveKeys));
|
|
792
|
+
if (!isRecord(value)) return redactStructuredPresentationValue(value);
|
|
793
|
+
return Object.fromEntries(
|
|
794
|
+
Object.entries(value).map(([key, entryValue]) => [
|
|
795
|
+
key,
|
|
796
|
+
sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : redactStatefulValues(entryValue, sensitiveKeys),
|
|
797
|
+
]),
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function redactPresentationData(commandInfo: CommandInfo, data: unknown): unknown {
|
|
802
|
+
if (commandInfo.command === "cookies") return redactStatefulValues(data, new Set(["value"]));
|
|
803
|
+
if (commandInfo.command === "storage") return redactStatefulValues(data, new Set(["value"]));
|
|
804
|
+
return redactStructuredPresentationValue(data);
|
|
805
|
+
}
|
|
806
|
+
|
|
571
807
|
function formatDiagnosticText(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
|
|
572
808
|
if (commandInfo.command === "session") return formatSessionText(data);
|
|
573
809
|
if (commandInfo.command === "profiles") {
|
|
@@ -579,8 +815,23 @@ function formatDiagnosticText(commandInfo: CommandInfo, data: Record<string, unk
|
|
|
579
815
|
if (profiles) return formatProfilesText(profiles, "auth profiles");
|
|
580
816
|
if (commandInfo.subcommand === "show") return formatAuthShowText(data);
|
|
581
817
|
}
|
|
818
|
+
if (commandInfo.command === "cookies") return formatCookiesText(data);
|
|
819
|
+
if (commandInfo.command === "storage") return formatStorageText(data);
|
|
820
|
+
if (commandInfo.command === "dialog") return formatDialogText(data);
|
|
821
|
+
if (commandInfo.command === "frame") return formatFrameText(data);
|
|
822
|
+
if (commandInfo.command === "state") return formatStateText(data);
|
|
582
823
|
if (commandInfo.command === "network" && commandInfo.subcommand === "requests") return formatNetworkRequestsText(data);
|
|
583
824
|
if (commandInfo.command === "network" && commandInfo.subcommand === "request") return formatNetworkRequestText(data);
|
|
825
|
+
if (commandInfo.command === "diff") return stringifyModelFacing(data);
|
|
826
|
+
if (commandInfo.command === "clipboard") {
|
|
827
|
+
const text = getStringField(data, "text") ?? getStringField(data, "value") ?? getStringField(data, "result");
|
|
828
|
+
if (text) return redactModelFacingText(text);
|
|
829
|
+
}
|
|
830
|
+
if (commandInfo.command === "stream") {
|
|
831
|
+
const streamSummary = getStreamSummary(data);
|
|
832
|
+
if (streamSummary) return streamSummary;
|
|
833
|
+
}
|
|
834
|
+
if (commandInfo.command === "chat") return formatChatText(data);
|
|
584
835
|
if (commandInfo.command === "console") return formatConsoleText(data);
|
|
585
836
|
if (commandInfo.command === "errors") return formatErrorsText(data);
|
|
586
837
|
if (commandInfo.command === "dashboard") return formatDashboardText(data);
|
|
@@ -627,7 +878,10 @@ const PATH_FIELD_CANDIDATES = [
|
|
|
627
878
|
"filePath",
|
|
628
879
|
"outputPath",
|
|
629
880
|
"downloadPath",
|
|
881
|
+
"diffPath",
|
|
630
882
|
"harPath",
|
|
883
|
+
"savedPath",
|
|
884
|
+
"statePath",
|
|
631
885
|
"tracePath",
|
|
632
886
|
"profilePath",
|
|
633
887
|
"videoPath",
|
|
@@ -647,9 +901,11 @@ const ARTIFACT_EXTENSION_TO_MEDIA_TYPE: Record<string, string> = {
|
|
|
647
901
|
|
|
648
902
|
function getArtifactKind(commandInfo: CommandInfo): FileArtifactKind | undefined {
|
|
649
903
|
if (commandInfo.command === "screenshot") return "image";
|
|
904
|
+
if (commandInfo.command === "diff" && commandInfo.subcommand === "screenshot") return "image";
|
|
650
905
|
if (commandInfo.command === "pdf") return "pdf";
|
|
651
906
|
if (commandInfo.command === "download") return "download";
|
|
652
907
|
if (commandInfo.command === "wait" && commandInfo.subcommand === "--download") return "download";
|
|
908
|
+
if (commandInfo.command === "state" && commandInfo.subcommand === "save") return "file";
|
|
653
909
|
if (commandInfo.command === "trace") return "trace";
|
|
654
910
|
if (commandInfo.command === "profiler") return "profile";
|
|
655
911
|
if (commandInfo.command === "record") return "video";
|
|
@@ -773,16 +1029,124 @@ function isManifestFileArtifact(artifact: FileArtifactMetadata): boolean {
|
|
|
773
1029
|
return !isRecordingStartArtifact(artifact);
|
|
774
1030
|
}
|
|
775
1031
|
|
|
1032
|
+
function getArtifactVerificationEntry(artifact: FileArtifactMetadata): ArtifactVerificationEntry {
|
|
1033
|
+
if (isRecordingStartArtifact(artifact)) {
|
|
1034
|
+
return {
|
|
1035
|
+
absolutePath: artifact.absolutePath,
|
|
1036
|
+
exists: artifact.exists,
|
|
1037
|
+
kind: artifact.kind,
|
|
1038
|
+
limitation: "Recording output is pending until record stop completes.",
|
|
1039
|
+
mediaType: artifact.mediaType,
|
|
1040
|
+
path: artifact.path,
|
|
1041
|
+
requestedPath: artifact.requestedPath,
|
|
1042
|
+
retentionState: undefined,
|
|
1043
|
+
sizeBytes: artifact.sizeBytes,
|
|
1044
|
+
state: "pending",
|
|
1045
|
+
status: artifact.status,
|
|
1046
|
+
storageScope: undefined,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
const state = artifact.exists === true
|
|
1050
|
+
? "verified"
|
|
1051
|
+
: artifact.exists === false
|
|
1052
|
+
? "missing"
|
|
1053
|
+
: "unverified";
|
|
1054
|
+
return {
|
|
1055
|
+
absolutePath: artifact.absolutePath,
|
|
1056
|
+
exists: artifact.exists,
|
|
1057
|
+
kind: artifact.kind,
|
|
1058
|
+
limitation: state === "missing"
|
|
1059
|
+
? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
|
|
1060
|
+
: state === "unverified"
|
|
1061
|
+
? "The wrapper could not prove local filesystem existence for this artifact."
|
|
1062
|
+
: undefined,
|
|
1063
|
+
mediaType: artifact.mediaType,
|
|
1064
|
+
path: artifact.path,
|
|
1065
|
+
requestedPath: artifact.requestedPath,
|
|
1066
|
+
retentionState: artifact.exists === false ? "missing" : "live",
|
|
1067
|
+
sizeBytes: artifact.sizeBytes,
|
|
1068
|
+
state,
|
|
1069
|
+
status: artifact.status,
|
|
1070
|
+
storageScope: "explicit-path",
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function getManifestVerificationEntry(entry: SessionArtifactManifestEntry): ArtifactVerificationEntry | undefined {
|
|
1075
|
+
if (entry.storageScope === "explicit-path") return undefined;
|
|
1076
|
+
const state = entry.retentionState === "live"
|
|
1077
|
+
? "verified"
|
|
1078
|
+
: entry.retentionState === "missing" || entry.retentionState === "evicted"
|
|
1079
|
+
? "missing"
|
|
1080
|
+
: "unverified";
|
|
1081
|
+
return {
|
|
1082
|
+
absolutePath: entry.absolutePath,
|
|
1083
|
+
exists: entry.exists,
|
|
1084
|
+
kind: entry.kind,
|
|
1085
|
+
limitation: entry.retentionState === "ephemeral"
|
|
1086
|
+
? "This spill file is process-temporary and may not survive reload or restart."
|
|
1087
|
+
: entry.retentionState === "evicted"
|
|
1088
|
+
? "This persisted spill file was evicted from the bounded session artifact store."
|
|
1089
|
+
: undefined,
|
|
1090
|
+
mediaType: entry.mediaType,
|
|
1091
|
+
path: entry.path,
|
|
1092
|
+
requestedPath: entry.requestedPath,
|
|
1093
|
+
retentionState: entry.retentionState,
|
|
1094
|
+
sizeBytes: entry.sizeBytes,
|
|
1095
|
+
state,
|
|
1096
|
+
storageScope: entry.storageScope,
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function buildArtifactVerificationSummary(
|
|
1101
|
+
artifacts: FileArtifactMetadata[],
|
|
1102
|
+
manifest?: SessionArtifactManifest,
|
|
1103
|
+
manifestPaths?: ReadonlySet<string>,
|
|
1104
|
+
): ArtifactVerificationSummary | undefined {
|
|
1105
|
+
const entries = [
|
|
1106
|
+
...artifacts.map(getArtifactVerificationEntry),
|
|
1107
|
+
...(manifest?.entries.flatMap((entry) => {
|
|
1108
|
+
if (manifestPaths && !manifestPaths.has(entry.path)) return [];
|
|
1109
|
+
const verificationEntry = getManifestVerificationEntry(entry);
|
|
1110
|
+
return verificationEntry ? [verificationEntry] : [];
|
|
1111
|
+
}) ?? []),
|
|
1112
|
+
];
|
|
1113
|
+
if (entries.length === 0) return undefined;
|
|
1114
|
+
const verifiedCount = entries.filter((entry) => entry.state === "verified").length;
|
|
1115
|
+
const missingCount = entries.filter((entry) => entry.state === "missing").length;
|
|
1116
|
+
const pendingCount = entries.filter((entry) => entry.state === "pending").length;
|
|
1117
|
+
const unverifiedCount = entries.filter((entry) => entry.state === "unverified").length;
|
|
1118
|
+
return {
|
|
1119
|
+
artifacts: entries,
|
|
1120
|
+
missingCount,
|
|
1121
|
+
pendingCount,
|
|
1122
|
+
unverifiedCount,
|
|
1123
|
+
verified: entries.length > 0 && verifiedCount === entries.length,
|
|
1124
|
+
verifiedCount,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function classifyPresentationSuccessCategory(options: {
|
|
1129
|
+
artifactVerification?: ArtifactVerificationSummary;
|
|
1130
|
+
artifacts?: FileArtifactMetadata[];
|
|
1131
|
+
inspection?: boolean;
|
|
1132
|
+
savedFile?: SavedFilePresentationDetails;
|
|
1133
|
+
}) {
|
|
1134
|
+
if ((options.artifactVerification?.missingCount ?? 0) > 0 || (options.artifactVerification?.unverifiedCount ?? 0) > 0) {
|
|
1135
|
+
return "artifact-unverified" as const;
|
|
1136
|
+
}
|
|
1137
|
+
return classifyAgentBrowserSuccessCategory(options);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
776
1140
|
function formatArtifactLabel(artifact: FileArtifactMetadata): string {
|
|
777
1141
|
switch (artifact.kind) {
|
|
778
1142
|
case "download":
|
|
779
1143
|
return artifact.command === "wait" && artifact.subcommand === "--download" ? "Download completed" : "Downloaded file";
|
|
780
1144
|
case "file":
|
|
781
|
-
return "Saved file";
|
|
1145
|
+
return artifact.command === "state" ? "State file" : "Saved file";
|
|
782
1146
|
case "har":
|
|
783
1147
|
return "Saved HAR";
|
|
784
1148
|
case "image":
|
|
785
|
-
return "Saved image";
|
|
1149
|
+
return artifact.command === "diff" && artifact.subcommand === "screenshot" ? "Saved diff image" : "Saved image";
|
|
786
1150
|
case "pdf":
|
|
787
1151
|
return "Saved PDF";
|
|
788
1152
|
case "profile":
|
|
@@ -951,12 +1315,71 @@ function getNavigationSummary(data: Record<string, unknown>): NavigationSummary
|
|
|
951
1315
|
return isNavigationSummary(candidate) ? candidate : undefined;
|
|
952
1316
|
}
|
|
953
1317
|
|
|
1318
|
+
function getTopLevelNavigationSummary(data: Record<string, unknown>): NavigationSummary | undefined {
|
|
1319
|
+
return isNavigationSummary(data)
|
|
1320
|
+
? {
|
|
1321
|
+
title: typeof data.title === "string" ? data.title : undefined,
|
|
1322
|
+
url: typeof data.url === "string" ? data.url : undefined,
|
|
1323
|
+
}
|
|
1324
|
+
: undefined;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function getNormalizedNavigationSummary(summary: NavigationSummary | undefined): { title?: string; url?: string } | undefined {
|
|
1328
|
+
const title = typeof summary?.title === "string" && summary.title.trim().length > 0 ? summary.title.trim() : undefined;
|
|
1329
|
+
const url = typeof summary?.url === "string" && summary.url.trim().length > 0 ? summary.url.trim() : undefined;
|
|
1330
|
+
return title || url ? { title, url } : undefined;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
954
1333
|
function formatNavigationSummary(summary: NavigationSummary): string | undefined {
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
if (
|
|
958
|
-
|
|
959
|
-
|
|
1334
|
+
const normalized = getNormalizedNavigationSummary(summary);
|
|
1335
|
+
if (!normalized) return undefined;
|
|
1336
|
+
if (normalized.title && normalized.url) return `${normalized.title}\n${normalized.url}`;
|
|
1337
|
+
return normalized.title ?? normalized.url;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function isPageChangeSummaryCommand(command: string | undefined): boolean {
|
|
1341
|
+
return command !== undefined && PAGE_CHANGE_SUMMARY_COMMANDS.has(command);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function buildPageChangeSummary(options: {
|
|
1345
|
+
artifacts?: FileArtifactMetadata[];
|
|
1346
|
+
commandInfo: CommandInfo;
|
|
1347
|
+
data: unknown;
|
|
1348
|
+
nextActions?: Array<{ id: string }>;
|
|
1349
|
+
savedFilePath?: string;
|
|
1350
|
+
summary: string;
|
|
1351
|
+
}): AgentBrowserPageChangeSummary | undefined {
|
|
1352
|
+
const { artifacts, commandInfo, data, nextActions, savedFilePath } = options;
|
|
1353
|
+
const artifactCount = artifacts?.length ?? 0;
|
|
1354
|
+
const navigation = isRecord(data)
|
|
1355
|
+
? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command) ? getTopLevelNavigationSummary(data) : undefined))
|
|
1356
|
+
: undefined;
|
|
1357
|
+
const confirmationRequired = detectConfirmationRequired(data) !== undefined;
|
|
1358
|
+
if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command)) {
|
|
1359
|
+
return undefined;
|
|
1360
|
+
}
|
|
1361
|
+
const changeType: AgentBrowserPageChangeSummary["changeType"] = savedFilePath || artifactCount > 0
|
|
1362
|
+
? "artifact"
|
|
1363
|
+
: navigation
|
|
1364
|
+
? "navigation"
|
|
1365
|
+
: confirmationRequired
|
|
1366
|
+
? "confirmation"
|
|
1367
|
+
: "mutation";
|
|
1368
|
+
const parts = [commandInfo.command ?? "agent-browser", changeType];
|
|
1369
|
+
if (navigation?.title) parts.push(navigation.title);
|
|
1370
|
+
if (navigation?.url) parts.push(navigation.url);
|
|
1371
|
+
if (savedFilePath) parts.push(savedFilePath);
|
|
1372
|
+
else if (artifactCount > 0) parts.push(`${artifactCount} artifact${artifactCount === 1 ? "" : "s"}`);
|
|
1373
|
+
return {
|
|
1374
|
+
...(artifactCount > 0 ? { artifactCount } : {}),
|
|
1375
|
+
changeType,
|
|
1376
|
+
...(commandInfo.command ? { command: commandInfo.command } : {}),
|
|
1377
|
+
...(nextActions ? { nextActionIds: nextActions.map((action) => action.id) } : {}),
|
|
1378
|
+
...(savedFilePath ? { savedFilePath } : {}),
|
|
1379
|
+
summary: parts.join(" → "),
|
|
1380
|
+
...(navigation?.title ? { title: navigation.title } : {}),
|
|
1381
|
+
...(navigation?.url ? { url: navigation.url } : {}),
|
|
1382
|
+
};
|
|
960
1383
|
}
|
|
961
1384
|
|
|
962
1385
|
function stripNavigationSummary(data: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -1083,6 +1506,36 @@ function getBatchFailureDetails(steps: Array<{ details: BatchStepPresentationDet
|
|
|
1083
1506
|
};
|
|
1084
1507
|
}
|
|
1085
1508
|
|
|
1509
|
+
function hasModelFacingArgRedaction(args: string[] | undefined): boolean {
|
|
1510
|
+
return args?.some((arg) => arg === "[REDACTED]" || arg.includes("%5BREDACTED%5D") || arg.includes("[REDACTED]")) === true;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function getStatefulCommandSensitiveValues(command: string[] | undefined): string[] {
|
|
1514
|
+
if (!command) return [];
|
|
1515
|
+
const tokens = extractCommandTokens(command);
|
|
1516
|
+
const values: string[] = [];
|
|
1517
|
+
if (tokens[0] === "cookies" && tokens[1] === "set" && tokens[3]) values.push(tokens[3]);
|
|
1518
|
+
if (tokens[0] === "storage" && ["local", "session"].includes(tokens[1] ?? "") && tokens[2] === "set" && tokens[4]) values.push(tokens[4]);
|
|
1519
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1520
|
+
const token = tokens[index];
|
|
1521
|
+
if (token === "--password" && tokens[index + 1]) values.push(tokens[index + 1]);
|
|
1522
|
+
else if (token?.startsWith("--password=")) values.push(token.slice("--password=".length));
|
|
1523
|
+
}
|
|
1524
|
+
return values.filter((value) => value.length > 0);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function redactExactValues(value: unknown, sensitiveValues: string[]): unknown {
|
|
1528
|
+
if (sensitiveValues.length === 0) return redactSensitiveValue(value);
|
|
1529
|
+
if (typeof value === "string") {
|
|
1530
|
+
let redacted = value;
|
|
1531
|
+
for (const sensitiveValue of sensitiveValues) redacted = redacted.split(sensitiveValue).join("[REDACTED]");
|
|
1532
|
+
return redactSensitiveText(redacted);
|
|
1533
|
+
}
|
|
1534
|
+
if (Array.isArray(value)) return value.map((item) => redactExactValues(item, sensitiveValues));
|
|
1535
|
+
if (!isRecord(value)) return value;
|
|
1536
|
+
return redactSensitiveValue(Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactValues(entryValue, sensitiveValues)])));
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1086
1539
|
async function buildBatchStepPresentation(options: {
|
|
1087
1540
|
artifactManifest?: SessionArtifactManifest;
|
|
1088
1541
|
artifactRequest?: ArtifactRequestContext;
|
|
@@ -1094,21 +1547,43 @@ async function buildBatchStepPresentation(options: {
|
|
|
1094
1547
|
}): Promise<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> {
|
|
1095
1548
|
const { artifactManifest, artifactRequest, cwd, index, item, persistentArtifactStore, sessionName } = options;
|
|
1096
1549
|
const command = isStringArray(item.command) ? item.command : undefined;
|
|
1097
|
-
const
|
|
1550
|
+
const redactedCommand = command ? redactInvocationArgs(command) : undefined;
|
|
1551
|
+
const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
|
|
1098
1552
|
|
|
1099
1553
|
if (item.success === false) {
|
|
1100
|
-
const
|
|
1554
|
+
const redactedErrorData = redactExactValues(item.error, getStatefulCommandSensitiveValues(command));
|
|
1555
|
+
const errorText = formatBatchStepError(redactedErrorData);
|
|
1556
|
+
const failureCategory = classifyAgentBrowserFailureCategory({
|
|
1557
|
+
args: command,
|
|
1558
|
+
command: command?.[0],
|
|
1559
|
+
errorText,
|
|
1560
|
+
});
|
|
1561
|
+
const confirmationRequired = detectConfirmationRequired(item.error);
|
|
1562
|
+
const nextActions = buildAgentBrowserNextActions({
|
|
1563
|
+
args: command,
|
|
1564
|
+
command: command?.[0],
|
|
1565
|
+
confirmationId: confirmationRequired?.id,
|
|
1566
|
+
failureCategory,
|
|
1567
|
+
resultCategory: "failure",
|
|
1568
|
+
});
|
|
1101
1569
|
const presentation: ToolPresentation = {
|
|
1102
1570
|
content: [{ type: "text", text: errorText }],
|
|
1571
|
+
failureCategory,
|
|
1572
|
+
nextActions,
|
|
1573
|
+
resultCategory: "failure",
|
|
1103
1574
|
summary: errorText,
|
|
1104
1575
|
};
|
|
1105
1576
|
return {
|
|
1106
1577
|
details: {
|
|
1578
|
+
artifactVerification: presentation.artifactVerification,
|
|
1107
1579
|
artifacts: presentation.artifacts,
|
|
1108
|
-
command,
|
|
1580
|
+
command: redactedCommand,
|
|
1109
1581
|
commandText,
|
|
1110
|
-
data:
|
|
1582
|
+
data: redactedErrorData,
|
|
1583
|
+
failureCategory,
|
|
1111
1584
|
index,
|
|
1585
|
+
nextActions,
|
|
1586
|
+
resultCategory: "failure",
|
|
1112
1587
|
success: false,
|
|
1113
1588
|
summary: errorText,
|
|
1114
1589
|
text: errorText,
|
|
@@ -1122,6 +1597,7 @@ async function buildBatchStepPresentation(options: {
|
|
|
1122
1597
|
artifactRequest,
|
|
1123
1598
|
commandInfo: parseCommandInfo(command ?? []),
|
|
1124
1599
|
cwd,
|
|
1600
|
+
args: command,
|
|
1125
1601
|
envelope: { data: item.result, success: true },
|
|
1126
1602
|
persistentArtifactStore,
|
|
1127
1603
|
sessionName,
|
|
@@ -1135,11 +1611,28 @@ async function buildBatchStepPresentation(options: {
|
|
|
1135
1611
|
secondaryPaths: presentation.imagePaths,
|
|
1136
1612
|
});
|
|
1137
1613
|
const text = getPresentationText(presentation) || presentation.summary;
|
|
1614
|
+
const nextActions = buildAgentBrowserNextActions({
|
|
1615
|
+
artifacts: presentation.artifacts,
|
|
1616
|
+
args: command,
|
|
1617
|
+
command: command?.[0],
|
|
1618
|
+
resultCategory: "success",
|
|
1619
|
+
savedFilePath: presentation.savedFilePath,
|
|
1620
|
+
successCategory: presentation.successCategory,
|
|
1621
|
+
});
|
|
1622
|
+
const pageChangeSummary = buildPageChangeSummary({
|
|
1623
|
+
artifacts: presentation.artifacts,
|
|
1624
|
+
commandInfo: parseCommandInfo(command ?? []),
|
|
1625
|
+
data: presentation.data,
|
|
1626
|
+
nextActions,
|
|
1627
|
+
savedFilePath: presentation.savedFilePath,
|
|
1628
|
+
summary: presentation.summary,
|
|
1629
|
+
});
|
|
1138
1630
|
|
|
1139
1631
|
return {
|
|
1140
1632
|
details: {
|
|
1633
|
+
artifactVerification: presentation.artifactVerification,
|
|
1141
1634
|
artifacts: presentation.artifacts,
|
|
1142
|
-
command,
|
|
1635
|
+
command: redactedCommand,
|
|
1143
1636
|
commandText,
|
|
1144
1637
|
data: presentation.data,
|
|
1145
1638
|
fullOutputPath: fullOutputPaths[0],
|
|
@@ -1147,9 +1640,13 @@ async function buildBatchStepPresentation(options: {
|
|
|
1147
1640
|
imagePath: imagePaths[0],
|
|
1148
1641
|
imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
|
|
1149
1642
|
index,
|
|
1643
|
+
nextActions,
|
|
1644
|
+
pageChangeSummary,
|
|
1645
|
+
resultCategory: "success",
|
|
1150
1646
|
savedFile: presentation.savedFile,
|
|
1151
1647
|
savedFilePath: presentation.savedFilePath,
|
|
1152
1648
|
success: true,
|
|
1649
|
+
successCategory: classifyPresentationSuccessCategory({ artifactVerification: presentation.artifactVerification, artifacts: presentation.artifacts, savedFile: presentation.savedFile }),
|
|
1153
1650
|
summary: presentation.summary,
|
|
1154
1651
|
text,
|
|
1155
1652
|
},
|
|
@@ -1195,6 +1692,7 @@ async function buildBatchPresentation(options: {
|
|
|
1195
1692
|
const batchFailure = getBatchFailureDetails(steps);
|
|
1196
1693
|
const images = steps.flatMap((step) => getPresentationImages(step.presentation));
|
|
1197
1694
|
const artifacts = steps.flatMap((step) => step.presentation.artifacts ?? []);
|
|
1695
|
+
const artifactVerification = buildArtifactVerificationSummary(artifacts);
|
|
1198
1696
|
const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
|
|
1199
1697
|
primaryPath: step.presentation.fullOutputPath,
|
|
1200
1698
|
secondaryPaths: step.presentation.fullOutputPaths,
|
|
@@ -1203,6 +1701,11 @@ async function buildBatchPresentation(options: {
|
|
|
1203
1701
|
primaryPath: step.presentation.imagePath,
|
|
1204
1702
|
secondaryPaths: step.presentation.imagePaths,
|
|
1205
1703
|
}));
|
|
1704
|
+
const redactedBatchData = steps.map(({ details }) => (
|
|
1705
|
+
details.success
|
|
1706
|
+
? { command: details.command, result: details.data, success: true }
|
|
1707
|
+
: { command: details.command, error: details.text, success: false }
|
|
1708
|
+
));
|
|
1206
1709
|
const stepText =
|
|
1207
1710
|
steps.length === 0
|
|
1208
1711
|
? "(no batch steps)"
|
|
@@ -1235,18 +1738,45 @@ async function buildBatchPresentation(options: {
|
|
|
1235
1738
|
const artifactRetentionSummary = currentArtifactManifest ? formatSessionArtifactRetentionSummary(currentArtifactManifest) : undefined;
|
|
1236
1739
|
const contentText = artifactRetentionSummary && manifestHasNewNoticeWorthyEntries(options.artifactManifest, currentArtifactManifest) ? `${text}\n\n${artifactRetentionSummary}` : text;
|
|
1237
1740
|
|
|
1741
|
+
const nextActions = batchFailure
|
|
1742
|
+
? batchFailure.failedStep.nextActions
|
|
1743
|
+
: buildAgentBrowserNextActions({ artifacts, command: "batch", resultCategory: "success" });
|
|
1744
|
+
const changedSteps = steps.map((step) => step.details).filter((details) => details.pageChangeSummary !== undefined);
|
|
1745
|
+
const pageChangeSummary = artifacts.length > 0
|
|
1746
|
+
? buildPageChangeSummary({
|
|
1747
|
+
artifacts,
|
|
1748
|
+
commandInfo: { command: "batch" },
|
|
1749
|
+
data,
|
|
1750
|
+
nextActions,
|
|
1751
|
+
summary,
|
|
1752
|
+
})
|
|
1753
|
+
: changedSteps.length > 0
|
|
1754
|
+
? {
|
|
1755
|
+
changeType: "mutation" as const,
|
|
1756
|
+
command: "batch",
|
|
1757
|
+
nextActionIds: nextActions?.map((action) => action.id),
|
|
1758
|
+
summary: `batch → mutation → ${changedSteps.length} changed step${changedSteps.length === 1 ? "" : "s"}`,
|
|
1759
|
+
}
|
|
1760
|
+
: undefined;
|
|
1761
|
+
|
|
1238
1762
|
return {
|
|
1239
1763
|
artifactManifest: currentArtifactManifest,
|
|
1240
1764
|
artifactRetentionSummary,
|
|
1765
|
+
artifactVerification,
|
|
1241
1766
|
artifacts: artifacts.length > 0 ? artifacts : undefined,
|
|
1242
1767
|
batchFailure,
|
|
1243
1768
|
batchSteps: steps.map((step) => step.details),
|
|
1244
1769
|
content: [{ type: "text", text: contentText }, ...images],
|
|
1245
|
-
|
|
1770
|
+
failureCategory: batchFailure?.failedStep.failureCategory,
|
|
1771
|
+
data: redactedBatchData,
|
|
1246
1772
|
fullOutputPath: fullOutputPaths[0],
|
|
1247
1773
|
fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
|
|
1248
1774
|
imagePath: imagePaths[0],
|
|
1249
1775
|
imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
|
|
1776
|
+
nextActions,
|
|
1777
|
+
pageChangeSummary,
|
|
1778
|
+
resultCategory: batchFailure ? "failure" : "success",
|
|
1779
|
+
successCategory: batchFailure ? undefined : classifyPresentationSuccessCategory({ artifactVerification, artifacts }),
|
|
1250
1780
|
summary,
|
|
1251
1781
|
};
|
|
1252
1782
|
}
|
|
@@ -1270,6 +1800,9 @@ function formatSummary(commandInfo: CommandInfo, data: unknown): string {
|
|
|
1270
1800
|
if (commandInfo.command === "skills" && commandInfo.subcommand === "get") {
|
|
1271
1801
|
return "agent-browser skill loaded";
|
|
1272
1802
|
}
|
|
1803
|
+
if (commandInfo.command === "skills" && commandInfo.subcommand === "path") {
|
|
1804
|
+
return "agent-browser skill path";
|
|
1805
|
+
}
|
|
1273
1806
|
if (isRecord(data)) {
|
|
1274
1807
|
const navigationSummary = getNavigationSummary(data);
|
|
1275
1808
|
if (navigationSummary && isNavigationObservableCommand(commandInfo.command)) {
|
|
@@ -1319,6 +1852,10 @@ function formatContentText(commandInfo: CommandInfo, data: unknown): string {
|
|
|
1319
1852
|
return formatConfirmationRequiredText(confirmationRequired);
|
|
1320
1853
|
}
|
|
1321
1854
|
|
|
1855
|
+
const skillsText = formatSkillsText(commandInfo, data);
|
|
1856
|
+
if (skillsText) {
|
|
1857
|
+
return skillsText;
|
|
1858
|
+
}
|
|
1322
1859
|
if (typeof data === "string") {
|
|
1323
1860
|
return redactModelFacingText(data);
|
|
1324
1861
|
}
|
|
@@ -1328,9 +1865,6 @@ function formatContentText(commandInfo: CommandInfo, data: unknown): string {
|
|
|
1328
1865
|
if (Array.isArray(data) && commandInfo.command === "profiles") {
|
|
1329
1866
|
return formatProfilesText(data, "Chrome profiles");
|
|
1330
1867
|
}
|
|
1331
|
-
if (Array.isArray(data) && commandInfo.command === "skills") {
|
|
1332
|
-
return formatSkillsText(commandInfo, data) ?? stringifyModelFacing(data);
|
|
1333
|
-
}
|
|
1334
1868
|
if (!isRecord(data)) {
|
|
1335
1869
|
return stringifyModelFacing(data);
|
|
1336
1870
|
}
|
|
@@ -1359,10 +1893,6 @@ function formatContentText(commandInfo: CommandInfo, data: unknown): string {
|
|
|
1359
1893
|
const screenshotSummary = getScreenshotSummary(data);
|
|
1360
1894
|
if (screenshotSummary) return screenshotSummary;
|
|
1361
1895
|
}
|
|
1362
|
-
const skillsText = formatSkillsText(commandInfo, data);
|
|
1363
|
-
if (skillsText) {
|
|
1364
|
-
return skillsText;
|
|
1365
|
-
}
|
|
1366
1896
|
const extractionText = formatExtractionText(commandInfo, data);
|
|
1367
1897
|
if (extractionText) {
|
|
1368
1898
|
return extractionText;
|
|
@@ -1595,6 +2125,7 @@ async function compactLargePresentationOutput(options: {
|
|
|
1595
2125
|
|
|
1596
2126
|
export async function buildToolPresentation(options: {
|
|
1597
2127
|
artifactManifest?: SessionArtifactManifest;
|
|
2128
|
+
args?: string[];
|
|
1598
2129
|
artifactRequest?: ArtifactRequestContext;
|
|
1599
2130
|
batchArtifactRequests?: Array<ArtifactRequestContext | undefined>;
|
|
1600
2131
|
commandInfo: CommandInfo;
|
|
@@ -1604,17 +2135,22 @@ export async function buildToolPresentation(options: {
|
|
|
1604
2135
|
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
1605
2136
|
sessionName?: string;
|
|
1606
2137
|
}): Promise<ToolPresentation> {
|
|
1607
|
-
const { artifactManifest, artifactRequest, commandInfo, cwd, envelope, errorText, persistentArtifactStore, sessionName } = options;
|
|
2138
|
+
const { args, artifactManifest, artifactRequest, commandInfo, cwd, envelope, errorText, persistentArtifactStore, sessionName } = options;
|
|
1608
2139
|
if (errorText) {
|
|
1609
2140
|
const hintedErrorText = appendSelectorRecoveryHint(redactModelFacingText(errorText));
|
|
2141
|
+
const categoryDetails = buildAgentBrowserResultCategoryDetails({ args: [commandInfo.command, commandInfo.subcommand].filter((item): item is string => item !== undefined), command: commandInfo.command, errorText: hintedErrorText, succeeded: false });
|
|
1610
2142
|
return {
|
|
2143
|
+
...categoryDetails,
|
|
1611
2144
|
content: [{ type: "text", text: hintedErrorText }],
|
|
2145
|
+
nextActions: buildAgentBrowserNextActions({ args, command: commandInfo.command, failureCategory: categoryDetails.failureCategory, resultCategory: "failure" }),
|
|
1612
2146
|
summary: hintedErrorText,
|
|
1613
2147
|
};
|
|
1614
2148
|
}
|
|
1615
2149
|
|
|
1616
2150
|
const data = enrichStreamStatusData(commandInfo, envelope?.data);
|
|
2151
|
+
const presentationData = redactPresentationData(commandInfo, data);
|
|
1617
2152
|
const artifacts = await extractFileArtifacts({ artifactRequest, commandInfo, cwd, data, sessionName });
|
|
2153
|
+
const artifactVerification = buildArtifactVerificationSummary(artifacts);
|
|
1618
2154
|
const artifactSummary = formatArtifactSummary(artifacts);
|
|
1619
2155
|
const summary = artifactSummary ?? formatSummary(commandInfo, data);
|
|
1620
2156
|
const artifactText = artifacts.length > 0 ? formatArtifactMetadataLines(artifacts).join("\n") : undefined;
|
|
@@ -1624,14 +2160,16 @@ export async function buildToolPresentation(options: {
|
|
|
1624
2160
|
: commandInfo.command === "snapshot" && isRecord(data)
|
|
1625
2161
|
? await buildSnapshotPresentation(data, persistentArtifactStore, artifactManifest)
|
|
1626
2162
|
: {
|
|
2163
|
+
artifactVerification,
|
|
1627
2164
|
artifacts: artifacts.length > 0 ? artifacts : undefined,
|
|
1628
2165
|
content: [{ type: "text" as const, text: artifactText ?? formatContentText(commandInfo, data) }],
|
|
1629
|
-
data,
|
|
2166
|
+
data: presentationData,
|
|
1630
2167
|
summary,
|
|
1631
2168
|
};
|
|
1632
2169
|
if (artifacts.length > 0 && !presentation.artifacts) {
|
|
1633
2170
|
presentation.artifacts = artifacts;
|
|
1634
2171
|
}
|
|
2172
|
+
presentation.artifactVerification = presentation.artifactVerification ?? artifactVerification;
|
|
1635
2173
|
if (isRecord(data)) {
|
|
1636
2174
|
const savedFile = getSavedFileDetails(commandInfo, data);
|
|
1637
2175
|
if (savedFile) {
|
|
@@ -1645,15 +2183,68 @@ export async function buildToolPresentation(options: {
|
|
|
1645
2183
|
const compactedPresentation = await compactLargePresentationOutput({
|
|
1646
2184
|
artifactManifest,
|
|
1647
2185
|
commandInfo,
|
|
1648
|
-
data,
|
|
2186
|
+
data: presentationData,
|
|
1649
2187
|
persistentArtifactStore,
|
|
1650
2188
|
presentation: presentationWithImage,
|
|
1651
2189
|
});
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
buildManifestEntriesForFileArtifacts(artifacts.filter(isManifestFileArtifact)),
|
|
1657
|
-
),
|
|
2190
|
+
const presentationWithManifest = applyArtifactManifest(
|
|
2191
|
+
compactedPresentation,
|
|
2192
|
+
compactedPresentation.artifactManifest ?? artifactManifest,
|
|
2193
|
+
buildManifestEntriesForFileArtifacts(artifacts.filter(isManifestFileArtifact)),
|
|
1658
2194
|
);
|
|
2195
|
+
const currentSpillPaths = new Set(getPresentationPaths({
|
|
2196
|
+
primaryPath: presentationWithManifest.fullOutputPath,
|
|
2197
|
+
secondaryPaths: presentationWithManifest.fullOutputPaths,
|
|
2198
|
+
}));
|
|
2199
|
+
presentationWithManifest.artifactVerification = buildArtifactVerificationSummary(
|
|
2200
|
+
artifacts,
|
|
2201
|
+
presentationWithManifest.artifactManifest,
|
|
2202
|
+
currentSpillPaths,
|
|
2203
|
+
) ?? presentationWithManifest.artifactVerification;
|
|
2204
|
+
const confirmationRequired = detectConfirmationRequired(data);
|
|
2205
|
+
if (!presentationWithManifest.resultCategory) {
|
|
2206
|
+
const categoryDetails = buildAgentBrowserResultCategoryDetails({
|
|
2207
|
+
artifacts: presentationWithManifest.artifacts,
|
|
2208
|
+
command: commandInfo.command,
|
|
2209
|
+
confirmationRequired: confirmationRequired !== undefined,
|
|
2210
|
+
errorText: envelope?.success === false ? presentationWithManifest.summary : undefined,
|
|
2211
|
+
savedFile: presentationWithManifest.savedFile,
|
|
2212
|
+
succeeded: envelope?.success !== false,
|
|
2213
|
+
});
|
|
2214
|
+
presentationWithManifest.resultCategory = categoryDetails.resultCategory;
|
|
2215
|
+
presentationWithManifest.successCategory = categoryDetails.resultCategory === "success"
|
|
2216
|
+
? classifyPresentationSuccessCategory({
|
|
2217
|
+
artifactVerification: presentationWithManifest.artifactVerification,
|
|
2218
|
+
artifacts: presentationWithManifest.artifacts,
|
|
2219
|
+
savedFile: presentationWithManifest.savedFile,
|
|
2220
|
+
})
|
|
2221
|
+
: categoryDetails.successCategory;
|
|
2222
|
+
presentationWithManifest.failureCategory = categoryDetails.failureCategory;
|
|
2223
|
+
}
|
|
2224
|
+
if (presentationWithManifest.resultCategory === "success") {
|
|
2225
|
+
presentationWithManifest.successCategory = classifyPresentationSuccessCategory({
|
|
2226
|
+
artifactVerification: presentationWithManifest.artifactVerification,
|
|
2227
|
+
artifacts: presentationWithManifest.artifacts,
|
|
2228
|
+
savedFile: presentationWithManifest.savedFile,
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
presentationWithManifest.nextActions = presentationWithManifest.nextActions ?? buildAgentBrowserNextActions({
|
|
2232
|
+
artifacts: presentationWithManifest.artifacts,
|
|
2233
|
+
args,
|
|
2234
|
+
command: commandInfo.command,
|
|
2235
|
+
confirmationId: confirmationRequired?.id,
|
|
2236
|
+
failureCategory: presentationWithManifest.failureCategory,
|
|
2237
|
+
resultCategory: presentationWithManifest.resultCategory ?? "success",
|
|
2238
|
+
savedFilePath: presentationWithManifest.savedFilePath,
|
|
2239
|
+
successCategory: presentationWithManifest.successCategory,
|
|
2240
|
+
});
|
|
2241
|
+
presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
|
|
2242
|
+
artifacts: presentationWithManifest.artifacts,
|
|
2243
|
+
commandInfo,
|
|
2244
|
+
data,
|
|
2245
|
+
nextActions: presentationWithManifest.nextActions,
|
|
2246
|
+
savedFilePath: presentationWithManifest.savedFilePath,
|
|
2247
|
+
summary: presentationWithManifest.summary,
|
|
2248
|
+
});
|
|
2249
|
+
return sanitizeModelFacingPresentation(presentationWithManifest);
|
|
1659
2250
|
}
|