pi-agent-browser-native 0.2.31 → 0.2.33
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 +35 -0
- package/README.md +64 -18
- package/docs/ARCHITECTURE.md +13 -10
- package/docs/COMMAND_REFERENCE.md +71 -16
- package/docs/ELECTRON.md +387 -0
- package/docs/RELEASE.md +34 -4
- package/docs/REQUIREMENTS.md +5 -3
- package/docs/SUPPORT_MATRIX.md +36 -21
- package/docs/TOOL_CONTRACT.md +198 -40
- package/extensions/agent-browser/index.ts +1585 -3486
- package/extensions/agent-browser/lib/electron/cleanup.ts +287 -0
- package/extensions/agent-browser/lib/electron/discovery.ts +717 -0
- package/extensions/agent-browser/lib/electron/launch.ts +553 -0
- package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +203 -0
- package/extensions/agent-browser/lib/input-modes/lookups.ts +447 -0
- package/extensions/agent-browser/lib/input-modes/params.ts +188 -0
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +107 -0
- package/extensions/agent-browser/lib/input-modes/shared.ts +46 -0
- package/extensions/agent-browser/lib/input-modes/types.ts +221 -0
- package/extensions/agent-browser/lib/input-modes.ts +41 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +696 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +450 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +46 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +711 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +386 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +476 -0
- package/extensions/agent-browser/lib/orchestration/browser-run.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +338 -0
- package/extensions/agent-browser/lib/playbook.ts +15 -13
- package/extensions/agent-browser/lib/process.ts +106 -4
- package/extensions/agent-browser/lib/results/action-recommendations.ts +269 -0
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +114 -0
- package/extensions/agent-browser/lib/results/artifact-state.ts +13 -0
- package/extensions/agent-browser/lib/results/categories.ts +106 -0
- package/extensions/agent-browser/lib/results/contracts.ts +220 -0
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +72 -0
- package/extensions/agent-browser/lib/results/envelope.ts +2 -1
- package/extensions/agent-browser/lib/results/network.ts +64 -0
- package/extensions/agent-browser/lib/results/next-actions.ts +117 -0
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +506 -0
- package/extensions/agent-browser/lib/results/presentation/batch.ts +355 -0
- package/extensions/agent-browser/lib/results/presentation/common.ts +53 -0
- package/extensions/agent-browser/lib/results/presentation/content.ts +36 -0
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +730 -0
- package/extensions/agent-browser/lib/results/presentation/errors.ts +125 -0
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +182 -0
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +216 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +154 -0
- package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
- package/extensions/agent-browser/lib/results/presentation.ts +87 -2369
- package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
- package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
- package/extensions/agent-browser/lib/results/shared.ts +17 -701
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
- package/extensions/agent-browser/lib/results/text.ts +40 -0
- package/extensions/agent-browser/lib/results.ts +16 -5
- package/extensions/agent-browser/lib/session-page-state.ts +486 -0
- package/extensions/agent-browser/lib/temp.ts +26 -0
- package/package.json +6 -4
|
@@ -1,2160 +1,48 @@
|
|
|
1
|
-
|
|
1
|
+
/*
|
|
2
2
|
* Purpose: Render parsed agent-browser results into concise pi-facing summaries, text content, and optional inline image attachments.
|
|
3
|
-
* Responsibilities:
|
|
3
|
+
* Responsibilities: Orchestrate specialized presentation modules, attach inline images within size limits, and keep generic record formatting distinct from envelope parsing.
|
|
4
4
|
* Scope: Presentation shaping only; upstream stdout parsing and snapshot compaction internals live in separate modules.
|
|
5
|
-
* Usage: Imported by the public `lib/results.ts` facade and consumed by the extension entrypoint after envelope parsing.
|
|
6
|
-
* Invariants/Assumptions: Presentation logic should stay close to upstream data while remaining small enough to reason about without mixing in snapshot-parser or envelope-parser internals.
|
|
7
5
|
*/
|
|
8
6
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import { buildSnapshotPresentation
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
type
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
summarizeNetworkFailures,
|
|
49
|
-
truncateText,
|
|
50
|
-
} from "./shared.js";
|
|
51
|
-
|
|
52
|
-
const IMAGE_EXTENSION_TO_MIME_TYPE: Record<string, string> = {
|
|
53
|
-
".gif": "image/gif",
|
|
54
|
-
".jpeg": "image/jpeg",
|
|
55
|
-
".jpg": "image/jpeg",
|
|
56
|
-
".png": "image/png",
|
|
57
|
-
".webp": "image/webp",
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
|
|
61
|
-
const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
|
|
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
|
-
]);
|
|
89
|
-
const NAVIGATION_SUMMARY_FIELD = "navigationSummary";
|
|
90
|
-
const LARGE_OUTPUT_INLINE_MAX_CHARS = 8_000;
|
|
91
|
-
const LARGE_OUTPUT_INLINE_MAX_LINES = 120;
|
|
92
|
-
const LARGE_OUTPUT_PREVIEW_MAX_CHARS = 2_500;
|
|
93
|
-
const LARGE_OUTPUT_PREVIEW_MAX_LINES = 40;
|
|
94
|
-
const LARGE_OUTPUT_FILE_PREFIX = "pi-agent-browser-output";
|
|
95
|
-
const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
|
|
96
|
-
const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
|
|
97
|
-
const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
|
|
98
|
-
const NETWORK_ERROR_PREVIEW_MAX_CHARS = 220;
|
|
99
|
-
const NETWORK_NEXT_ACTION_LIMIT = 4;
|
|
100
|
-
const NETWORK_FILTER_MAX_CHARS = 160;
|
|
101
|
-
const NETWORK_FILTER_SENSITIVE_SEGMENT_TERMS = ["apikey", "api-key", "api_key", "authentication", "authorization", "bearer", "credential", "credentials", "jwt", "passwd", "password", "reset", "secret", "session", "token"] as const;
|
|
102
|
-
const NETWORK_FILTER_OPAQUE_SEGMENT_PATTERN = /^(?:[A-Fa-f0-9]{16,}|(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9_-]{16,})$/;
|
|
103
|
-
const NETWORK_PREVIEW_FIELD_CANDIDATES = {
|
|
104
|
-
request: ["postData"] as const,
|
|
105
|
-
response: ["responseBody"] as const,
|
|
106
|
-
error: ["error", "failureText", "errorText"] as const,
|
|
107
|
-
};
|
|
108
|
-
const AUTH_SHOW_SAFE_FIELDS = ["name", "profile", "url", "username", "createdAt", "updatedAt"] as const;
|
|
109
|
-
|
|
110
|
-
interface NavigationSummary {
|
|
111
|
-
title?: string;
|
|
112
|
-
url?: string;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function getImageMimeType(filePath: string): string | undefined {
|
|
116
|
-
const extension = extname(filePath).toLowerCase();
|
|
117
|
-
return IMAGE_EXTENSION_TO_MIME_TYPE[extension];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getInlineImageMaxBytes(env: NodeJS.ProcessEnv = process.env): number {
|
|
121
|
-
return parsePositiveInteger(env[INLINE_IMAGE_MAX_BYTES_ENV]) ?? DEFAULT_INLINE_IMAGE_MAX_BYTES;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function formatByteCount(bytes: number): string {
|
|
125
|
-
if (bytes < 1_024) return `${bytes} B`;
|
|
126
|
-
if (bytes < 1_024 * 1_024) return `${(bytes / 1_024).toFixed(1)} KiB`;
|
|
127
|
-
return `${(bytes / (1_024 * 1_024)).toFixed(1)} MiB`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function appendPresentationNotice(presentation: ToolPresentation, message: string): void {
|
|
131
|
-
const existingText = presentation.content[0]?.type === "text" ? presentation.content[0].text : "";
|
|
132
|
-
presentation.content[0] = {
|
|
133
|
-
type: "text",
|
|
134
|
-
text: existingText.length > 0 ? `${existingText}\n\n${message}` : message,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function shouldAppendArtifactRetentionNotice(entries: SessionArtifactManifestEntry[]): boolean {
|
|
139
|
-
return entries.some((entry) => entry.retentionState === "evicted" || entry.storageScope !== "explicit-path");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function getManifestEntryKey(entry: SessionArtifactManifestEntry): string {
|
|
143
|
-
return entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function manifestHasNewNoticeWorthyEntries(base: SessionArtifactManifest | undefined, current: SessionArtifactManifest | undefined): boolean {
|
|
147
|
-
if (!current) return false;
|
|
148
|
-
const baseKeys = new Set((base?.entries ?? []).map(getManifestEntryKey));
|
|
149
|
-
return current.entries.some((entry) => !baseKeys.has(getManifestEntryKey(entry)) && (entry.retentionState === "evicted" || entry.storageScope !== "explicit-path"));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function applyArtifactManifest(presentation: ToolPresentation, baseManifest: SessionArtifactManifest | undefined, entries: SessionArtifactManifestEntry[]): ToolPresentation {
|
|
153
|
-
if (entries.length === 0) return presentation;
|
|
154
|
-
const artifactManifest = mergeSessionArtifactManifest({ base: baseManifest, entries });
|
|
155
|
-
if (!artifactManifest) return presentation;
|
|
156
|
-
presentation.artifactManifest = artifactManifest;
|
|
157
|
-
presentation.artifactRetentionSummary = formatSessionArtifactRetentionSummary(artifactManifest);
|
|
158
|
-
if (shouldAppendArtifactRetentionNotice(entries)) {
|
|
159
|
-
appendPresentationNotice(presentation, presentation.artifactRetentionSummary);
|
|
160
|
-
}
|
|
161
|
-
return presentation;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function stringifyModelFacing(value: unknown): string {
|
|
165
|
-
return stringifyUnknown(redactSensitiveValue(value));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function redactModelFacingText(text: string): string {
|
|
169
|
-
const parsed = parseJsonPreviewString(text);
|
|
170
|
-
if (parsed !== text) {
|
|
171
|
-
return stringifyModelFacing(parsed);
|
|
172
|
-
}
|
|
173
|
-
return redactSensitiveText(text);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function redactModelFacingTextIfSensitive(text: string): string {
|
|
177
|
-
return /(?:@|\b(?:api[_-]?key|auth|authorization|basic|bearer|cookie|pass(?:word)?|secret|session[_-]?id|token)\b)/i.test(text)
|
|
178
|
-
? redactModelFacingText(text)
|
|
179
|
-
: text;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function getTabSummary(data: Record<string, unknown>): string | undefined {
|
|
183
|
-
const tabs = Array.isArray(data.tabs) ? data.tabs : undefined;
|
|
184
|
-
if (!tabs) return undefined;
|
|
185
|
-
|
|
186
|
-
const lines = tabs.map((tab, index) => {
|
|
187
|
-
if (!isRecord(tab)) return `${index}: <invalid tab>`;
|
|
188
|
-
const marker = tab.active === true ? "*" : "-";
|
|
189
|
-
const title = typeof tab.title === "string" ? tab.title : "(untitled)";
|
|
190
|
-
const url = typeof tab.url === "string" ? tab.url : "(no url)";
|
|
191
|
-
const tabSelector =
|
|
192
|
-
typeof tab.tabId === "string" && tab.tabId.trim().length > 0
|
|
193
|
-
? tab.tabId.trim()
|
|
194
|
-
: typeof tab.label === "string" && tab.label.trim().length > 0
|
|
195
|
-
? tab.label.trim()
|
|
196
|
-
: typeof tab.index === "number"
|
|
197
|
-
? String(tab.index)
|
|
198
|
-
: String(index);
|
|
199
|
-
return `${marker} [${tabSelector}] ${title} — ${url}`;
|
|
200
|
-
});
|
|
201
|
-
return lines.join("\n");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function getStreamSummary(data: Record<string, unknown>): string | undefined {
|
|
205
|
-
if (typeof data.enabled !== "boolean" || typeof data.connected !== "boolean") {
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const lines = [
|
|
210
|
-
`Enabled: ${data.enabled}`,
|
|
211
|
-
`Connected: ${data.connected}`,
|
|
212
|
-
`Screencasting: ${data.screencasting === true}`,
|
|
213
|
-
];
|
|
214
|
-
if (typeof data.port === "number") {
|
|
215
|
-
lines.push(`Port: ${data.port}`);
|
|
216
|
-
lines.push(`WebSocket URL: ${getStreamWebSocketUrl(data.port)}`);
|
|
217
|
-
lines.push(`Frame format: JSON messages with base64 JPEG frame data`);
|
|
218
|
-
}
|
|
219
|
-
return lines.join("\n");
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function getStreamWebSocketUrl(port: number): string {
|
|
223
|
-
return `ws://127.0.0.1:${port}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function enrichStreamStatusData(commandInfo: CommandInfo, data: unknown): unknown {
|
|
227
|
-
if (commandInfo.command !== "stream" || commandInfo.subcommand !== "status" || !isRecord(data) || typeof data.port !== "number") {
|
|
228
|
-
return data;
|
|
229
|
-
}
|
|
230
|
-
return {
|
|
231
|
-
...data,
|
|
232
|
-
frameFormat: "JSON messages with base64 JPEG frame data",
|
|
233
|
-
wsUrl: getStreamWebSocketUrl(data.port),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function getArrayField(data: Record<string, unknown>, key: string): unknown[] | undefined {
|
|
238
|
-
return Array.isArray(data[key]) ? data[key] : undefined;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function getStringField(data: Record<string, unknown>, key: string): string | undefined {
|
|
242
|
-
const value = data[key];
|
|
243
|
-
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function formatCount(count: number, singular: string, plural = `${singular}s`): string {
|
|
247
|
-
return `${count} ${count === 1 ? singular : plural}`;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function firstLine(value: string, maxChars = 160): string {
|
|
251
|
-
return truncateText(value.split("\n", 1)[0] ?? value, maxChars);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function formatDiagnosticSummary(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
|
|
255
|
-
if (commandInfo.command === "session") {
|
|
256
|
-
const sessions = getArrayField(data, "sessions");
|
|
257
|
-
if (sessions) return `Sessions: ${sessions.length}`;
|
|
258
|
-
const session = getStringField(data, "session");
|
|
259
|
-
if (session) return `Session: ${session}`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (commandInfo.command === "profiles") {
|
|
263
|
-
const profiles = getArrayField(data, "profiles");
|
|
264
|
-
if (profiles) return `Chrome profiles: ${profiles.length}`;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (commandInfo.command === "auth") {
|
|
268
|
-
const profiles = getArrayField(data, "profiles");
|
|
269
|
-
if (profiles) return `Auth profiles: ${profiles.length}`;
|
|
270
|
-
const name = getStringField(data, "name") ?? getStringField(data, "profile") ?? commandInfo.subcommand;
|
|
271
|
-
if (name && commandInfo.subcommand === "show") return `Auth profile: ${name}`;
|
|
272
|
-
if (name && ["save", "login", "delete"].includes(commandInfo.subcommand ?? "")) return `Auth ${commandInfo.subcommand}: ${name}`;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (commandInfo.command === "cookies") {
|
|
276
|
-
const cookies = getArrayField(data, "cookies");
|
|
277
|
-
if (cookies) return `Cookies: ${cookies.length}`;
|
|
278
|
-
const name = getStringField(data, "name");
|
|
279
|
-
if (name) return name;
|
|
280
|
-
if (data.set === true) return "Cookie set";
|
|
281
|
-
if (data.cleared === true || data.clear === true) return "Cookies cleared";
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (commandInfo.command === "storage") {
|
|
285
|
-
const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
|
|
286
|
-
if (entries) return `Storage entries: ${entries.length}`;
|
|
287
|
-
const key = getStringField(data, "key");
|
|
288
|
-
if (key && (commandInfo.subcommand === "set" || data.set === true || Object.hasOwn(data, "value"))) return `Storage set: ${key}`;
|
|
289
|
-
if (data.cleared === true || data.clear === true) return "Storage cleared";
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (commandInfo.command === "dialog") {
|
|
293
|
-
const open = typeof data.open === "boolean" ? data.open : undefined;
|
|
294
|
-
if (open !== undefined) return open ? "Dialog open" : "No dialog open";
|
|
295
|
-
if (data.accepted === true) return "Dialog accepted";
|
|
296
|
-
if (data.dismissed === true) return "Dialog dismissed";
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (commandInfo.command === "frame") {
|
|
300
|
-
const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector") ?? commandInfo.subcommand;
|
|
301
|
-
if (frame) return `Frame: ${frame}`;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
if (commandInfo.command === "state") {
|
|
305
|
-
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
306
|
-
if (states) return `States: ${states.length}`;
|
|
307
|
-
if (commandInfo.subcommand === "load") return undefined;
|
|
308
|
-
const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "path") ?? commandInfo.subcommand;
|
|
309
|
-
if (stateName) return `State ${commandInfo.subcommand ?? "result"}: ${stateName}`;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (commandInfo.command === "network") {
|
|
313
|
-
if (commandInfo.subcommand === "requests") {
|
|
314
|
-
const requests = getArrayField(data, "requests");
|
|
315
|
-
if (requests) return `Network requests: ${requests.length}`;
|
|
316
|
-
}
|
|
317
|
-
if (commandInfo.subcommand === "route") {
|
|
318
|
-
const routed = getStringField(data, "routed") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
|
|
319
|
-
return routed ? `Network route: ${redactModelFacingTextIfSensitive(routed)}` : "Network route configured";
|
|
320
|
-
}
|
|
321
|
-
if (commandInfo.subcommand === "unroute") {
|
|
322
|
-
const unrouted = getStringField(data, "unrouted") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
|
|
323
|
-
return unrouted ? `Network unroute: ${redactModelFacingTextIfSensitive(unrouted)}` : "Network route removed";
|
|
324
|
-
}
|
|
325
|
-
if (commandInfo.subcommand === "har") {
|
|
326
|
-
const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
|
|
327
|
-
return `Network HAR: ${state}`;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
if (commandInfo.command === "diff") {
|
|
332
|
-
if (commandInfo.subcommand === "snapshot") return "Snapshot diff completed";
|
|
333
|
-
if (commandInfo.subcommand === "url") return "URL diff completed";
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (["trace", "profiler"].includes(commandInfo.command ?? "")) {
|
|
337
|
-
const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
|
|
338
|
-
if (state) return `${commandInfo.command === "trace" ? "Trace" : "Profiler"}: ${state}`;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (commandInfo.command === "highlight") return "Element highlighted";
|
|
342
|
-
if (commandInfo.command === "inspect") return "DevTools inspect opened";
|
|
343
|
-
if (commandInfo.command === "clipboard") return `Clipboard ${commandInfo.subcommand ?? "completed"}`;
|
|
344
|
-
|
|
345
|
-
if (commandInfo.command === "stream") {
|
|
346
|
-
if (commandInfo.subcommand === "enable") {
|
|
347
|
-
const port = typeof data.port === "number" ? ` on port ${data.port}` : "";
|
|
348
|
-
return `Stream enabled${port}`;
|
|
349
|
-
}
|
|
350
|
-
if (commandInfo.subcommand === "disable") return "Stream disabled";
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (commandInfo.command === "chat") return "Chat response";
|
|
354
|
-
|
|
355
|
-
if (commandInfo.command === "console") {
|
|
356
|
-
const messages = getArrayField(data, "messages");
|
|
357
|
-
if (messages) return `Console messages: ${messages.length}`;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (commandInfo.command === "errors") {
|
|
361
|
-
const errors = getArrayField(data, "errors");
|
|
362
|
-
if (errors) return `Page errors: ${errors.length}`;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (commandInfo.command === "dashboard") {
|
|
366
|
-
if (typeof data.port === "number") return `Dashboard running on port ${data.port}`;
|
|
367
|
-
if (data.stopped === true) return "Dashboard stopped";
|
|
368
|
-
if (data.stopped === false) {
|
|
369
|
-
const reason = getStringField(data, "reason");
|
|
370
|
-
return reason ? `Dashboard not stopped: ${reason}` : "Dashboard not stopped";
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (commandInfo.command === "doctor") {
|
|
375
|
-
const status = getStringField(data, "status") ?? getStringField(data, "result");
|
|
376
|
-
if (status) return `Doctor: ${status}`;
|
|
377
|
-
const checks = getArrayField(data, "checks") ?? getArrayField(data, "issues") ?? getArrayField(data, "problems");
|
|
378
|
-
if (checks) return `Doctor: ${formatCount(checks.length, "item")}`;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
return undefined;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function formatSessionText(data: Record<string, unknown>): string | undefined {
|
|
385
|
-
const sessions = getArrayField(data, "sessions");
|
|
386
|
-
if (sessions) {
|
|
387
|
-
if (sessions.length === 0) return "No active sessions.";
|
|
388
|
-
return sessions
|
|
389
|
-
.map((item, index) => {
|
|
390
|
-
if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
391
|
-
const name = redactModelFacingText(getStringField(item, "name") ?? getStringField(item, "session") ?? getStringField(item, "id") ?? `(session ${index + 1})`);
|
|
392
|
-
const active = item.active === true ? " *active*" : "";
|
|
393
|
-
const details = [getStringField(item, "url"), getStringField(item, "title")]
|
|
394
|
-
.flatMap((detail) => (detail ? [redactModelFacingTextIfSensitive(detail)] : []))
|
|
395
|
-
.join(" — ");
|
|
396
|
-
return details ? `${index + 1}. ${name}${active} — ${details}` : `${index + 1}. ${name}${active}`;
|
|
397
|
-
})
|
|
398
|
-
.join("\n");
|
|
399
|
-
}
|
|
400
|
-
const session = getStringField(data, "session");
|
|
401
|
-
return session ? `Current session: ${redactModelFacingText(session)}` : undefined;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function formatProfilesText(profiles: unknown[], label: string): string {
|
|
405
|
-
if (profiles.length === 0) return `No ${label}.`;
|
|
406
|
-
return profiles
|
|
407
|
-
.map((item, index) => {
|
|
408
|
-
if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
409
|
-
const name = redactModelFacingText(getStringField(item, "name") ?? getStringField(item, "profile") ?? `(unnamed ${index + 1})`);
|
|
410
|
-
const directory = getStringField(item, "directory") ?? getStringField(item, "path");
|
|
411
|
-
return directory ? `${index + 1}. ${name} (${redactModelFacingText(directory)})` : `${index + 1}. ${name}`;
|
|
412
|
-
})
|
|
413
|
-
.join("\n");
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function formatSkillsListText(skills: unknown[]): string {
|
|
417
|
-
if (skills.length === 0) return "No agent-browser skills found.";
|
|
418
|
-
return skills
|
|
419
|
-
.map((item, index) => {
|
|
420
|
-
if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
421
|
-
const name = redactModelFacingText(getStringField(item, "name") ?? `(skill ${index + 1})`);
|
|
422
|
-
const description = getStringField(item, "description");
|
|
423
|
-
return description ? `${index + 1}. ${name} — ${redactModelFacingText(description)}` : `${index + 1}. ${name}`;
|
|
424
|
-
})
|
|
425
|
-
.join("\n");
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function getSkillContent(data: unknown): string | undefined {
|
|
429
|
-
if (typeof data === "string") return data;
|
|
430
|
-
if (isRecord(data) && typeof data.content === "string") return data.content;
|
|
431
|
-
if (!Array.isArray(data)) return undefined;
|
|
432
|
-
const content = data.flatMap((item) => (isRecord(item) && typeof item.content === "string" ? [item.content] : []));
|
|
433
|
-
return content.length > 0 ? content.join("\n\n") : undefined;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function splitShellWords(input: string): string[] | undefined {
|
|
437
|
-
const words: string[] = [];
|
|
438
|
-
let current = "";
|
|
439
|
-
let quote: 'single' | 'double' | undefined;
|
|
440
|
-
for (let index = 0; index < input.length; index += 1) {
|
|
441
|
-
const char = input[index];
|
|
442
|
-
if (quote === "single") {
|
|
443
|
-
if (char === "'") quote = undefined;
|
|
444
|
-
else current += char;
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
if (quote === "double") {
|
|
448
|
-
if (char === '"') quote = undefined;
|
|
449
|
-
else if (char === "\\" && index + 1 < input.length) {
|
|
450
|
-
index += 1;
|
|
451
|
-
current += input[index];
|
|
452
|
-
} else current += char;
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
if (char === "'") {
|
|
456
|
-
quote = "single";
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
if (char === '"') {
|
|
460
|
-
quote = "double";
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
if (char === "\\" && index + 1 < input.length) {
|
|
464
|
-
index += 1;
|
|
465
|
-
current += input[index];
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
if (char === "#" && current.length === 0) {
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
471
|
-
if (/\s/.test(char)) {
|
|
472
|
-
if (current.length > 0) {
|
|
473
|
-
words.push(current);
|
|
474
|
-
current = "";
|
|
475
|
-
}
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
current += char;
|
|
479
|
-
}
|
|
480
|
-
if (quote) return undefined;
|
|
481
|
-
if (current.length > 0) words.push(current);
|
|
482
|
-
return words;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function formatNativeAgentBrowserCall(args: string[], stdin?: string): string {
|
|
486
|
-
return stdin === undefined
|
|
487
|
-
? `agent_browser { "args": ${JSON.stringify(args)} }`
|
|
488
|
-
: `agent_browser { "args": ${JSON.stringify(args)}, "stdin": ${JSON.stringify(stdin)} }`;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function formatNativeSkillContent(content: string): string {
|
|
492
|
-
const lines = content.replace(/^allowed-tools:.*agent-browser.*\n?/gim, "").replace(/^```bash\s*$/gim, "```text").split("\n");
|
|
493
|
-
const output: string[] = [];
|
|
494
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
495
|
-
const line = lines[index];
|
|
496
|
-
const commandMatch = /^(\s*)agent-browser\s+(.+?)\s*$/.exec(line);
|
|
497
|
-
if (!commandMatch) {
|
|
498
|
-
output.push(line);
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
const indent = commandMatch[1];
|
|
502
|
-
const rawArgsText = commandMatch[2];
|
|
503
|
-
const heredocMatch = /^(.*?)\s+(<<-?)['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?\s*$/.exec(rawArgsText);
|
|
504
|
-
const argsText = heredocMatch?.[1] ?? rawArgsText;
|
|
505
|
-
const args = splitShellWords(argsText);
|
|
506
|
-
if (!args || args.length === 0) {
|
|
507
|
-
output.push(line);
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
if (!heredocMatch) {
|
|
511
|
-
output.push(`${indent}${formatNativeAgentBrowserCall(args)}`);
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
const stripsLeadingTabs = heredocMatch[2] === "<<-";
|
|
515
|
-
const delimiter = heredocMatch[3];
|
|
516
|
-
const stdinLines: string[] = [];
|
|
517
|
-
let cursor = index + 1;
|
|
518
|
-
while (cursor < lines.length) {
|
|
519
|
-
const candidate = stripsLeadingTabs ? lines[cursor].replace(/^\t+/, "") : lines[cursor];
|
|
520
|
-
if (candidate === delimiter) break;
|
|
521
|
-
stdinLines.push(candidate);
|
|
522
|
-
cursor += 1;
|
|
523
|
-
}
|
|
524
|
-
if (cursor >= lines.length) {
|
|
525
|
-
output.push(line);
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
output.push(`${indent}${formatNativeAgentBrowserCall(args, stdinLines.join("\n"))}`);
|
|
529
|
-
index = cursor;
|
|
530
|
-
}
|
|
531
|
-
return output.join("\n");
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function formatSkillsText(commandInfo: CommandInfo, data: unknown): string | undefined {
|
|
535
|
-
if (commandInfo.command !== "skills") return undefined;
|
|
536
|
-
if (commandInfo.subcommand === "path") return typeof data === "string" ? redactModelFacingText(data) : undefined;
|
|
537
|
-
if (commandInfo.subcommand === "list" && Array.isArray(data)) return formatSkillsListText(data);
|
|
538
|
-
const content = getSkillContent(data);
|
|
539
|
-
if (content) {
|
|
540
|
-
const note = [
|
|
541
|
-
"Pi native-tool note: upstream skill text was adapted for this native tool.",
|
|
542
|
-
"Use args for CLI tokens and stdin only for batch, eval --stdin, or auth save --password-stdin; do not pipe heredocs through bash unless the user explicitly asks for a bash workflow.",
|
|
543
|
-
].join("\n");
|
|
544
|
-
return `${note}\n\n${redactModelFacingText(formatNativeSkillContent(content))}`;
|
|
545
|
-
}
|
|
546
|
-
if (typeof data === "string") return redactModelFacingText(formatNativeSkillContent(data));
|
|
547
|
-
return undefined;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function formatAuthShowText(data: Record<string, unknown>): string | undefined {
|
|
551
|
-
const lines = AUTH_SHOW_SAFE_FIELDS.flatMap((key) => {
|
|
552
|
-
const value = data[key];
|
|
553
|
-
return typeof value === "string" && value.trim().length > 0 ? [`${key}: ${redactModelFacingText(value.trim())}`] : [];
|
|
554
|
-
});
|
|
555
|
-
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function getPreviewCandidate(item: Record<string, unknown>, keys: readonly string[]): unknown {
|
|
559
|
-
for (const key of keys) {
|
|
560
|
-
const value = item[key];
|
|
561
|
-
if (value !== undefined && value !== null && value !== "") return value;
|
|
562
|
-
}
|
|
563
|
-
return undefined;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function parseJsonPreviewString(value: string): unknown {
|
|
567
|
-
const trimmed = value.trim();
|
|
568
|
-
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
|
|
569
|
-
try {
|
|
570
|
-
return JSON.parse(trimmed) as unknown;
|
|
571
|
-
} catch {
|
|
572
|
-
return value;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function formatNetworkPreviewValue(value: unknown, maxChars: number): string | undefined {
|
|
577
|
-
if (value === undefined || value === null) return undefined;
|
|
578
|
-
const previewValue = typeof value === "string" ? parseJsonPreviewString(value) : value;
|
|
579
|
-
const redacted = redactSensitiveValue(previewValue);
|
|
580
|
-
const raw = typeof redacted === "string" ? redacted : stringifyUnknown(redacted);
|
|
581
|
-
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
582
|
-
if (normalized.length === 0) return undefined;
|
|
583
|
-
return truncateText(redactSensitiveText(normalized), maxChars);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
function appendNetworkPreview(lines: string[], label: string, value: unknown, maxChars: number): void {
|
|
587
|
-
const preview = formatNetworkPreviewValue(value, maxChars);
|
|
588
|
-
if (!preview) return;
|
|
589
|
-
lines.push(` ${label}: ${preview}`);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function formatNetworkRequestLine(item: Record<string, unknown>, index: number): string[] {
|
|
593
|
-
const method = getStringField(item, "method") ?? "GET";
|
|
594
|
-
const status = typeof item.status === "number" ? String(item.status) : "pending";
|
|
595
|
-
const type = getStringField(item, "resourceType") ?? getStringField(item, "mimeType");
|
|
596
|
-
const url = getStringField(item, "url") ?? "(no url)";
|
|
597
|
-
const requestId = getStringField(item, "requestId") ?? getStringField(item, "id");
|
|
598
|
-
const idText = requestId ? ` [${redactSensitiveText(requestId)}]` : "";
|
|
599
|
-
const failureClassification = classifyNetworkRequestFailure(item);
|
|
600
|
-
const impactText = failureClassification ? ` [${failureClassification.impact}: ${failureClassification.reason}]` : "";
|
|
601
|
-
const lines = [`${index + 1}. ${status} ${method} ${truncateText(redactSensitiveText(url), 180)}${type ? ` (${type})` : ""}${idText}${impactText}`];
|
|
602
|
-
appendNetworkPreview(lines, "Payload", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.request), NETWORK_BODY_PREVIEW_MAX_CHARS);
|
|
603
|
-
appendNetworkPreview(lines, "Response", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.response), NETWORK_BODY_PREVIEW_MAX_CHARS);
|
|
604
|
-
appendNetworkPreview(lines, "Error", getPreviewCandidate(item, NETWORK_PREVIEW_FIELD_CANDIDATES.error), NETWORK_ERROR_PREVIEW_MAX_CHARS);
|
|
605
|
-
return lines;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function formatNetworkRequestsText(data: Record<string, unknown>): string | undefined {
|
|
609
|
-
const requests = getArrayField(data, "requests");
|
|
610
|
-
if (!requests) return undefined;
|
|
611
|
-
if (requests.length === 0) return "No network requests captured.";
|
|
612
|
-
const networkFailureSummary = summarizeNetworkFailures(requests);
|
|
613
|
-
const shown = networkFailureSummary.totalCount > 0
|
|
614
|
-
? [`Network failure summary: ${networkFailureSummary.actionableCount} actionable, ${networkFailureSummary.benignCount} benign low-impact (${networkFailureSummary.totalCount} total).`]
|
|
615
|
-
: [];
|
|
616
|
-
const indexedRequests = requests.map((item, index) => ({ index, item }));
|
|
617
|
-
const failedRequests: typeof indexedRequests = [];
|
|
618
|
-
const normalRequests: typeof indexedRequests = [];
|
|
619
|
-
for (const indexed of indexedRequests) {
|
|
620
|
-
if (isRecord(indexed.item) && classifyNetworkRequestFailure(indexed.item)) failedRequests.push(indexed);
|
|
621
|
-
else normalRequests.push(indexed);
|
|
622
|
-
}
|
|
623
|
-
failedRequests.sort((left, right) => {
|
|
624
|
-
const leftClassification = isRecord(left.item) ? classifyNetworkRequestFailure(left.item) : undefined;
|
|
625
|
-
const rightClassification = isRecord(right.item) ? classifyNetworkRequestFailure(right.item) : undefined;
|
|
626
|
-
const leftRank = leftClassification?.impact === "actionable" ? 0 : 1;
|
|
627
|
-
const rightRank = rightClassification?.impact === "actionable" ? 0 : 1;
|
|
628
|
-
return leftRank - rightRank || left.index - right.index;
|
|
629
|
-
});
|
|
630
|
-
const prioritizedRequests = [...failedRequests, ...normalRequests];
|
|
631
|
-
shown.push(...prioritizedRequests.slice(0, DIAGNOSTIC_REQUEST_PREVIEW_LIMIT).flatMap(({ item, index }) => {
|
|
632
|
-
if (!isRecord(item)) return [`${index + 1}. ${stringifyModelFacing(item)}`];
|
|
633
|
-
return formatNetworkRequestLine(item, index);
|
|
634
|
-
}));
|
|
635
|
-
if (requests.length > DIAGNOSTIC_REQUEST_PREVIEW_LIMIT) {
|
|
636
|
-
shown.push(`... (${requests.length - DIAGNOSTIC_REQUEST_PREVIEW_LIMIT} additional requests omitted from preview; failed requests are shown first when present)`);
|
|
637
|
-
}
|
|
638
|
-
return shown.join("\n");
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function formatNetworkRequestText(data: Record<string, unknown>): string | undefined {
|
|
642
|
-
if (!getStringField(data, "url") && !getStringField(data, "requestId") && !getStringField(data, "id")) {
|
|
643
|
-
return undefined;
|
|
644
|
-
}
|
|
645
|
-
return formatNetworkRequestLine(data, 0).join("\n");
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
interface NetworkRequestActionCandidate {
|
|
649
|
-
filter?: string;
|
|
650
|
-
item: Record<string, unknown>;
|
|
651
|
-
kind: "actionable" | "api" | "benign" | "request";
|
|
652
|
-
requestId: string;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function getSafeNetworkActionValue(value: string | undefined): string | undefined {
|
|
656
|
-
if (!value) return undefined;
|
|
657
|
-
const trimmed = value.trim();
|
|
658
|
-
if (trimmed.length === 0 || redactSensitiveText(trimmed) !== trimmed) return undefined;
|
|
659
|
-
return trimmed;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function getNetworkRequestId(item: Record<string, unknown>): string | undefined {
|
|
663
|
-
return getSafeNetworkActionValue(getStringField(item, "requestId") ?? getStringField(item, "id"));
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function isSensitiveNetworkPathSegment(segment: string): boolean {
|
|
667
|
-
const normalized = segment.toLowerCase();
|
|
668
|
-
return normalized === "auth" || NETWORK_FILTER_SENSITIVE_SEGMENT_TERMS.some((term) => normalized.includes(term));
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function pathFilterMayExposeSensitiveSegment(filter: string): boolean {
|
|
672
|
-
const decoded = (() => {
|
|
673
|
-
try {
|
|
674
|
-
return decodeURIComponent(filter);
|
|
675
|
-
} catch {
|
|
676
|
-
return filter;
|
|
677
|
-
}
|
|
678
|
-
})();
|
|
679
|
-
return decoded.split("/").some((segment) => isSensitiveNetworkPathSegment(segment) || NETWORK_FILTER_OPAQUE_SEGMENT_PATTERN.test(segment));
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function getNetworkRequestPathFilter(item: Record<string, unknown>): string | undefined {
|
|
683
|
-
const url = getStringField(item, "url");
|
|
684
|
-
if (!url) return undefined;
|
|
685
|
-
let filter: string | undefined;
|
|
686
|
-
try {
|
|
687
|
-
filter = new URL(url).pathname;
|
|
688
|
-
} catch {
|
|
689
|
-
filter = url.split(/[?#]/, 1)[0];
|
|
690
|
-
}
|
|
691
|
-
filter = filter?.trim();
|
|
692
|
-
if (!filter || filter === "/" || filter.length > NETWORK_FILTER_MAX_CHARS || pathFilterMayExposeSensitiveSegment(filter)) return undefined;
|
|
693
|
-
return getSafeNetworkActionValue(filter);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function isApiLikeNetworkRequest(item: Record<string, unknown>): boolean {
|
|
697
|
-
const method = (getStringField(item, "method") ?? "GET").toUpperCase();
|
|
698
|
-
const resourceType = (getStringField(item, "resourceType") ?? "").toLowerCase();
|
|
699
|
-
const mimeType = (getStringField(item, "mimeType") ?? "").toLowerCase();
|
|
700
|
-
const filter = getNetworkRequestPathFilter(item) ?? "";
|
|
701
|
-
return resourceType === "fetch" || resourceType === "xhr" || mimeType.includes("json") || /\/(?:api|graphql|rpc)(?:\/|$)/i.test(filter) || !["GET", "HEAD"].includes(method);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function getNetworkRequestActionCandidate(item: Record<string, unknown>): NetworkRequestActionCandidate | undefined {
|
|
705
|
-
const requestId = getNetworkRequestId(item);
|
|
706
|
-
if (!requestId) return undefined;
|
|
707
|
-
const classification = classifyNetworkRequestFailure(item);
|
|
708
|
-
const kind: NetworkRequestActionCandidate["kind"] = classification?.impact === "actionable"
|
|
709
|
-
? "actionable"
|
|
710
|
-
: classification?.impact === "benign"
|
|
711
|
-
? "benign"
|
|
712
|
-
: isApiLikeNetworkRequest(item)
|
|
713
|
-
? "api"
|
|
714
|
-
: "request";
|
|
715
|
-
return { filter: getNetworkRequestPathFilter(item), item, kind, requestId };
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function chooseNetworkRequestActionCandidate(candidates: NetworkRequestActionCandidate[]): NetworkRequestActionCandidate | undefined {
|
|
719
|
-
return candidates.find((candidate) => candidate.kind === "actionable")
|
|
720
|
-
?? candidates.find((candidate) => candidate.kind === "api")
|
|
721
|
-
?? candidates.find((candidate) => candidate.kind === "benign")
|
|
722
|
-
?? candidates[0];
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function formatNetworkRequestActionDescriptor(candidate: NetworkRequestActionCandidate): string {
|
|
726
|
-
const method = getStringField(candidate.item, "method") ?? "GET";
|
|
727
|
-
const status = typeof candidate.item.status === "number" ? String(candidate.item.status) : "pending";
|
|
728
|
-
const target = candidate.filter ? ` ${candidate.filter}` : "";
|
|
729
|
-
return `${status} ${method}${target} [${candidate.requestId}]`;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function getNetworkRequestDetailActionId(candidate: NetworkRequestActionCandidate): string {
|
|
733
|
-
if (candidate.kind === "actionable") return "inspect-actionable-network-request";
|
|
734
|
-
if (candidate.kind === "benign") return "inspect-benign-network-request";
|
|
735
|
-
return "inspect-network-request";
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function buildNetworkRequestsNextActions(data: unknown, sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
|
|
739
|
-
if (!isRecord(data)) return undefined;
|
|
740
|
-
const requests = getArrayField(data, "requests");
|
|
741
|
-
if (!requests) return undefined;
|
|
742
|
-
const candidates = requests.flatMap((item) => {
|
|
743
|
-
if (!isRecord(item)) return [];
|
|
744
|
-
const candidate = getNetworkRequestActionCandidate(item);
|
|
745
|
-
return candidate ? [candidate] : [];
|
|
746
|
-
});
|
|
747
|
-
const selected = chooseNetworkRequestActionCandidate(candidates);
|
|
748
|
-
if (!selected) return undefined;
|
|
749
|
-
const descriptor = formatNetworkRequestActionDescriptor(selected);
|
|
750
|
-
const actions: AgentBrowserNextAction[] = [
|
|
751
|
-
{
|
|
752
|
-
id: getNetworkRequestDetailActionId(selected),
|
|
753
|
-
params: { args: withSessionPrefix(sessionName, ["network", "request", selected.requestId]) },
|
|
754
|
-
reason: `Inspect full request details for ${descriptor}.`,
|
|
755
|
-
safety: "Read-only network diagnostic; request inspection must not replace the active page/ref context.",
|
|
756
|
-
tool: "agent_browser",
|
|
757
|
-
},
|
|
758
|
-
];
|
|
759
|
-
if (selected.kind === "actionable") {
|
|
760
|
-
actions.push({
|
|
761
|
-
id: "trace-actionable-network-source",
|
|
762
|
-
params: { networkSourceLookup: { requestId: selected.requestId, ...(sessionName ? { session: sessionName } : {}) } },
|
|
763
|
-
reason: `Look for local source candidates related to ${descriptor}.`,
|
|
764
|
-
safety: "Read-only experimental helper; it reports bounded candidates and may miss bundled or dynamic call sites.",
|
|
765
|
-
tool: "agent_browser",
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
if (selected.filter) {
|
|
769
|
-
actions.push({
|
|
770
|
-
id: "filter-network-requests-by-path",
|
|
771
|
-
params: { args: withSessionPrefix(sessionName, ["network", "requests", "--filter", selected.filter]) },
|
|
772
|
-
reason: `List captured requests matching ${selected.filter}.`,
|
|
773
|
-
safety: "Read-only request-list filter; absence from a compact preview is not proof the request did not happen.",
|
|
774
|
-
tool: "agent_browser",
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
actions.push({
|
|
778
|
-
id: "start-network-har-capture",
|
|
779
|
-
params: { args: withSessionPrefix(sessionName, ["network", "har", "start"]) },
|
|
780
|
-
reason: "Start HAR capture before reproducing the network behavior again.",
|
|
781
|
-
safety: "HARs can contain URLs and headers; stop to an explicit path, inspect metadata, and avoid sharing sensitive captures.",
|
|
782
|
-
tool: "agent_browser",
|
|
783
|
-
});
|
|
784
|
-
return actions.slice(0, NETWORK_NEXT_ACTION_LIMIT);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function formatConsoleText(data: Record<string, unknown>): string | undefined {
|
|
788
|
-
const messages = getArrayField(data, "messages");
|
|
789
|
-
if (!messages) return undefined;
|
|
790
|
-
if (messages.length === 0) return "No console messages.";
|
|
791
|
-
const shown = messages.slice(0, DIAGNOSTIC_LOG_PREVIEW_LIMIT).map((item, index) => {
|
|
792
|
-
if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
793
|
-
const type = redactModelFacingText(getStringField(item, "type") ?? "message");
|
|
794
|
-
const text = getStringField(item, "text") ?? stringifyModelFacing(item);
|
|
795
|
-
return `${index + 1}. [${type}] ${firstLine(redactModelFacingText(text).replace(/\s+/g, " ").trim(), 220)}`;
|
|
796
|
-
});
|
|
797
|
-
if (messages.length > shown.length) {
|
|
798
|
-
shown.push(`... (${messages.length - shown.length} additional console messages omitted from preview)`);
|
|
799
|
-
}
|
|
800
|
-
return shown.join("\n");
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
function formatErrorsText(data: Record<string, unknown>): string | undefined {
|
|
804
|
-
const errors = getArrayField(data, "errors");
|
|
805
|
-
if (!errors) return undefined;
|
|
806
|
-
if (errors.length === 0) return "No page errors.";
|
|
807
|
-
const shown = errors.slice(0, DIAGNOSTIC_LOG_PREVIEW_LIMIT).map((item, index) => {
|
|
808
|
-
if (!isRecord(item)) return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
809
|
-
const text = getStringField(item, "text") ?? stringifyModelFacing(item);
|
|
810
|
-
const location = [
|
|
811
|
-
getStringField(item, "url"),
|
|
812
|
-
typeof item.line === "number" ? `line ${item.line}` : undefined,
|
|
813
|
-
typeof item.column === "number" ? `column ${item.column}` : undefined,
|
|
814
|
-
]
|
|
815
|
-
.filter(Boolean)
|
|
816
|
-
.map((item) => redactModelFacingText(String(item)))
|
|
817
|
-
.join(":");
|
|
818
|
-
const safeText = firstLine(redactModelFacingText(text), 220);
|
|
819
|
-
return location ? `${index + 1}. ${safeText} (${location})` : `${index + 1}. ${safeText}`;
|
|
820
|
-
});
|
|
821
|
-
if (errors.length > shown.length) {
|
|
822
|
-
shown.push(`... (${errors.length - shown.length} additional errors omitted from preview)`);
|
|
823
|
-
}
|
|
824
|
-
return shown.join("\n");
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
function formatDashboardText(data: Record<string, unknown>): string | undefined {
|
|
828
|
-
const lines: string[] = [];
|
|
829
|
-
if (typeof data.port === "number") lines.push(`Port: ${data.port}`);
|
|
830
|
-
if (typeof data.pid === "number") lines.push(`PID: ${data.pid}`);
|
|
831
|
-
if (typeof data.stopped === "boolean") lines.push(`Stopped: ${data.stopped}`);
|
|
832
|
-
const reason = getStringField(data, "reason");
|
|
833
|
-
if (reason) lines.push(`Reason: ${redactModelFacingText(reason)}`);
|
|
834
|
-
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function formatChatText(data: Record<string, unknown>): string | undefined {
|
|
838
|
-
const response = getStringField(data, "response") ?? getStringField(data, "message") ?? getStringField(data, "text") ?? getStringField(data, "result");
|
|
839
|
-
if (response) return redactModelFacingText(response);
|
|
840
|
-
const model = getStringField(data, "model");
|
|
841
|
-
const provider = getStringField(data, "provider");
|
|
842
|
-
const lines = [model ? `Model: ${redactModelFacingText(model)}` : undefined, provider ? `Provider: ${redactModelFacingText(provider)}` : undefined].filter(Boolean);
|
|
843
|
-
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
function formatDoctorText(data: Record<string, unknown>): string | undefined {
|
|
847
|
-
const lines: string[] = [];
|
|
848
|
-
const status = getStringField(data, "status") ?? getStringField(data, "result");
|
|
849
|
-
if (status) lines.push(`Status: ${redactModelFacingText(status)}`);
|
|
850
|
-
for (const key of ["checks", "issues", "problems"] as const) {
|
|
851
|
-
const items = getArrayField(data, key);
|
|
852
|
-
if (items) lines.push(`${key}: ${items.length}`);
|
|
853
|
-
}
|
|
854
|
-
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
function formatCookieRecordText(item: Record<string, unknown>, fallbackName: string): string {
|
|
858
|
-
const name = redactModelFacingText(getStringField(item, "name") ?? fallbackName);
|
|
859
|
-
const domain = getStringField(item, "domain");
|
|
860
|
-
const path = getStringField(item, "path");
|
|
861
|
-
const flags = [item.httpOnly === true ? "httpOnly" : undefined, item.secure === true ? "secure" : undefined].filter(Boolean).join(", ");
|
|
862
|
-
const location = [domain, path].filter(Boolean).join("");
|
|
863
|
-
return [name, location ? `(${redactModelFacingText(location)})` : undefined, flags ? `[${flags}]` : undefined].filter(Boolean).join(" ");
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function formatCookiesText(data: Record<string, unknown>): string | undefined {
|
|
867
|
-
const cookies = getArrayField(data, "cookies");
|
|
868
|
-
if (cookies) {
|
|
869
|
-
if (cookies.length === 0) return "No cookies.";
|
|
870
|
-
return cookies
|
|
871
|
-
.map((item, index) => (isRecord(item) ? formatCookieRecordText(item, `(cookie ${index + 1})`) : `${index + 1}. [REDACTED]`))
|
|
872
|
-
.join("\n");
|
|
873
|
-
}
|
|
874
|
-
if (getStringField(data, "name") || getStringField(data, "domain") || getStringField(data, "path") || Object.hasOwn(data, "value")) {
|
|
875
|
-
return formatCookieRecordText(data, "cookie");
|
|
876
|
-
}
|
|
877
|
-
if (data.set === true) return "Cookie set.";
|
|
878
|
-
if (data.cleared === true || data.clear === true) return "Cookies cleared.";
|
|
879
|
-
return undefined;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
function formatStorageText(data: Record<string, unknown>): string | undefined {
|
|
883
|
-
const type = getStringField(data, "type") ?? getStringField(data, "storage") ?? "storage";
|
|
884
|
-
const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
|
|
885
|
-
if (entries) {
|
|
886
|
-
if (entries.length === 0) return `${type}: no entries.`;
|
|
887
|
-
return entries
|
|
888
|
-
.map((item, index) => {
|
|
889
|
-
if (!isRecord(item)) return `${index + 1}. [REDACTED]`;
|
|
890
|
-
const rawKey = getStringField(item, "key") ?? getStringField(item, "name") ?? `(entry ${index + 1})`;
|
|
891
|
-
const key = redactModelFacingText(rawKey);
|
|
892
|
-
return Object.hasOwn(item, "value") ? `${key}: [REDACTED]` : key;
|
|
893
|
-
})
|
|
894
|
-
.join("\n");
|
|
895
|
-
}
|
|
896
|
-
const key = getStringField(data, "key");
|
|
897
|
-
if (key && Object.hasOwn(data, "value")) return `${type} ${redactModelFacingText(key)}: [REDACTED]`;
|
|
898
|
-
if (key && data.set === true) return `${type} set: ${redactModelFacingText(key)}`;
|
|
899
|
-
if (data.cleared === true || data.clear === true) return `${type} cleared.`;
|
|
900
|
-
return undefined;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function formatDialogText(data: Record<string, unknown>): string | undefined {
|
|
904
|
-
const lines: string[] = [];
|
|
905
|
-
if (typeof data.open === "boolean") lines.push(data.open ? "Dialog open." : "No dialog open.");
|
|
906
|
-
const type = getStringField(data, "type");
|
|
907
|
-
if (type) lines.push(`Type: ${redactModelFacingText(type)}`);
|
|
908
|
-
const message = getStringField(data, "message");
|
|
909
|
-
if (message) lines.push(`Message: ${/(?:auth|authorization|bearer|cookie|pass(?:word)?|secret|session|token)/i.test(message) ? "[REDACTED]" : redactModelFacingText(message)}`);
|
|
910
|
-
if (data.accepted === true) lines.push("Accepted.");
|
|
911
|
-
if (data.dismissed === true) lines.push("Dismissed.");
|
|
912
|
-
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
function formatFrameText(data: Record<string, unknown>): string | undefined {
|
|
916
|
-
const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector");
|
|
917
|
-
const url = getStringField(data, "url");
|
|
918
|
-
const title = getStringField(data, "title");
|
|
919
|
-
const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactModelFacingTextIfSensitive(url)}` : undefined].filter(Boolean);
|
|
920
|
-
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
function formatStateText(data: Record<string, unknown>): string | undefined {
|
|
924
|
-
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
925
|
-
if (states) {
|
|
926
|
-
if (states.length === 0) return "No saved states.";
|
|
927
|
-
return states
|
|
928
|
-
.map((item, index) => {
|
|
929
|
-
if (!isRecord(item)) return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
|
|
930
|
-
const name = getStringField(item, "name") ?? getStringField(item, "file") ?? getStringField(item, "path") ?? `(state ${index + 1})`;
|
|
931
|
-
const url = getStringField(item, "url");
|
|
932
|
-
return url ? `${index + 1}. ${redactModelFacingText(name)} — ${redactModelFacingTextIfSensitive(url)}` : `${index + 1}. ${redactModelFacingText(name)}`;
|
|
933
|
-
})
|
|
934
|
-
.join("\n");
|
|
935
|
-
}
|
|
936
|
-
if (data.loaded === true) return `State loaded: ${redactModelFacingText(getStringField(data, "path") ?? getStringField(data, "name") ?? "ok")}`;
|
|
937
|
-
if (data.cleared === true || data.clear === true) return "State cleared.";
|
|
938
|
-
return undefined;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function isSensitivePresentationField(key: string): boolean {
|
|
942
|
-
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);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
function redactStructuredPresentationValue(value: unknown): unknown {
|
|
946
|
-
if (typeof value === "string") return redactModelFacingTextIfSensitive(value);
|
|
947
|
-
if (Array.isArray(value)) return value.map((item) => redactStructuredPresentationValue(item));
|
|
948
|
-
if (!isRecord(value)) return value;
|
|
949
|
-
return Object.fromEntries(
|
|
950
|
-
Object.entries(value).map(([key, entryValue]) => [
|
|
951
|
-
key,
|
|
952
|
-
isSensitivePresentationField(key) ? "[REDACTED]" : redactStructuredPresentationValue(entryValue),
|
|
953
|
-
]),
|
|
954
|
-
);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
function redactStatefulValues(value: unknown, sensitiveKeys: Set<string>): unknown {
|
|
958
|
-
if (Array.isArray(value)) return value.map((item) => redactStatefulValues(item, sensitiveKeys));
|
|
959
|
-
if (!isRecord(value)) return redactStructuredPresentationValue(value);
|
|
960
|
-
return Object.fromEntries(
|
|
961
|
-
Object.entries(value).map(([key, entryValue]) => [
|
|
962
|
-
key,
|
|
963
|
-
sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : redactStatefulValues(entryValue, sensitiveKeys),
|
|
964
|
-
]),
|
|
965
|
-
);
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
function redactPresentationData(commandInfo: CommandInfo, data: unknown): unknown {
|
|
969
|
-
if (commandInfo.command === "cookies") return redactStatefulValues(data, new Set(["value"]));
|
|
970
|
-
if (commandInfo.command === "storage") return redactStatefulValues(data, new Set(["value"]));
|
|
971
|
-
return redactStructuredPresentationValue(data);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
function formatDiagnosticText(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
|
|
975
|
-
if (commandInfo.command === "session") return formatSessionText(data);
|
|
976
|
-
if (commandInfo.command === "profiles") {
|
|
977
|
-
const profiles = getArrayField(data, "profiles");
|
|
978
|
-
if (profiles) return formatProfilesText(profiles, "Chrome profiles");
|
|
979
|
-
}
|
|
980
|
-
if (commandInfo.command === "auth") {
|
|
981
|
-
const profiles = getArrayField(data, "profiles");
|
|
982
|
-
if (profiles) return formatProfilesText(profiles, "auth profiles");
|
|
983
|
-
if (commandInfo.subcommand === "show") return formatAuthShowText(data);
|
|
984
|
-
}
|
|
985
|
-
if (commandInfo.command === "cookies") return formatCookiesText(data);
|
|
986
|
-
if (commandInfo.command === "storage") return formatStorageText(data);
|
|
987
|
-
if (commandInfo.command === "dialog") return formatDialogText(data);
|
|
988
|
-
if (commandInfo.command === "frame") return formatFrameText(data);
|
|
989
|
-
if (commandInfo.command === "state") return formatStateText(data);
|
|
990
|
-
if (commandInfo.command === "network" && commandInfo.subcommand === "requests") return formatNetworkRequestsText(data);
|
|
991
|
-
if (commandInfo.command === "network" && commandInfo.subcommand === "request") return formatNetworkRequestText(data);
|
|
992
|
-
if (commandInfo.command === "diff") return stringifyModelFacing(data);
|
|
993
|
-
if (commandInfo.command === "clipboard") {
|
|
994
|
-
const text = getStringField(data, "text") ?? getStringField(data, "value") ?? getStringField(data, "result");
|
|
995
|
-
if (text) return redactModelFacingText(text);
|
|
996
|
-
}
|
|
997
|
-
if (commandInfo.command === "stream") {
|
|
998
|
-
const streamSummary = getStreamSummary(data);
|
|
999
|
-
if (streamSummary) return streamSummary;
|
|
1000
|
-
}
|
|
1001
|
-
if (commandInfo.command === "chat") return formatChatText(data);
|
|
1002
|
-
if (commandInfo.command === "console") return formatConsoleText(data);
|
|
1003
|
-
if (commandInfo.command === "errors") return formatErrorsText(data);
|
|
1004
|
-
if (commandInfo.command === "dashboard") return formatDashboardText(data);
|
|
1005
|
-
if (commandInfo.command === "doctor") return formatDoctorText(data);
|
|
1006
|
-
return undefined;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
function getPageSummary(data: Record<string, unknown>): string | undefined {
|
|
1010
|
-
const title = typeof data.title === "string" ? data.title : undefined;
|
|
1011
|
-
const url = typeof data.url === "string" ? data.url : undefined;
|
|
1012
|
-
if (!title && !url) return undefined;
|
|
1013
|
-
if (title && url) return `${title}\n${url}`;
|
|
1014
|
-
return title ?? url;
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function formatConfirmationRequiredSummary(confirmation: ConfirmationRequiredPresentation): string {
|
|
1018
|
-
return `Confirmation required: ${confirmation.id}`;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function formatConfirmationRequiredText(confirmation: ConfirmationRequiredPresentation): string {
|
|
1022
|
-
const lines = [
|
|
1023
|
-
"Confirmation required.",
|
|
1024
|
-
`Pending confirmation id: ${confirmation.id}`,
|
|
1025
|
-
];
|
|
1026
|
-
if (confirmation.actionText) {
|
|
1027
|
-
lines.push(`Action: ${confirmation.actionText}`);
|
|
1028
|
-
}
|
|
1029
|
-
lines.push(
|
|
1030
|
-
"",
|
|
1031
|
-
"Next steps:",
|
|
1032
|
-
`- Approve: { "args": ["confirm", "${confirmation.id}"] }`,
|
|
1033
|
-
`- Deny: { "args": ["deny", "${confirmation.id}"] }`,
|
|
1034
|
-
);
|
|
1035
|
-
return lines.join("\n");
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
function getScreenshotSummary(data: Record<string, unknown>): string | undefined {
|
|
1039
|
-
return typeof data.path === "string" ? `Saved image: ${data.path}` : undefined;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const PATH_FIELD_CANDIDATES = [
|
|
1043
|
-
"path",
|
|
1044
|
-
"file",
|
|
1045
|
-
"filePath",
|
|
1046
|
-
"outputPath",
|
|
1047
|
-
"downloadPath",
|
|
1048
|
-
"diffPath",
|
|
1049
|
-
"harPath",
|
|
1050
|
-
"savedPath",
|
|
1051
|
-
"statePath",
|
|
1052
|
-
"tracePath",
|
|
1053
|
-
"profilePath",
|
|
1054
|
-
"videoPath",
|
|
1055
|
-
] as const;
|
|
1056
|
-
|
|
1057
|
-
const ARTIFACT_EXTENSION_TO_MEDIA_TYPE: Record<string, string> = {
|
|
1058
|
-
".cpuprofile": "application/json",
|
|
1059
|
-
".har": "application/json",
|
|
1060
|
-
".html": "text/html",
|
|
1061
|
-
".json": "application/json",
|
|
1062
|
-
".pdf": "application/pdf",
|
|
1063
|
-
".txt": "text/plain",
|
|
1064
|
-
".webm": "video/webm",
|
|
1065
|
-
".zip": "application/zip",
|
|
1066
|
-
...IMAGE_EXTENSION_TO_MIME_TYPE,
|
|
1067
|
-
};
|
|
1068
|
-
|
|
1069
|
-
function getArtifactKind(commandInfo: CommandInfo): FileArtifactKind | undefined {
|
|
1070
|
-
if (commandInfo.command === "screenshot") return "image";
|
|
1071
|
-
if (commandInfo.command === "diff" && commandInfo.subcommand === "screenshot") return "image";
|
|
1072
|
-
if (commandInfo.command === "pdf") return "pdf";
|
|
1073
|
-
if (commandInfo.command === "download") return "download";
|
|
1074
|
-
if (commandInfo.command === "wait" && commandInfo.subcommand === "--download") return "download";
|
|
1075
|
-
if (commandInfo.command === "state" && commandInfo.subcommand === "save") return "file";
|
|
1076
|
-
if (commandInfo.command === "trace") return "trace";
|
|
1077
|
-
if (commandInfo.command === "profiler") return "profile";
|
|
1078
|
-
if (commandInfo.command === "record") return "video";
|
|
1079
|
-
if (commandInfo.command === "network" && commandInfo.subcommand === "har") return "har";
|
|
1080
|
-
return undefined;
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function extractPathStrings(data: unknown): string[] {
|
|
1084
|
-
if (typeof data === "string") {
|
|
1085
|
-
return data.trim().length > 0 ? [data] : [];
|
|
1086
|
-
}
|
|
1087
|
-
if (!isRecord(data)) {
|
|
1088
|
-
return [];
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
const paths: string[] = [];
|
|
1092
|
-
for (const key of PATH_FIELD_CANDIDATES) {
|
|
1093
|
-
const value = data[key];
|
|
1094
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
1095
|
-
paths.push(value);
|
|
1096
|
-
}
|
|
1097
|
-
if (Array.isArray(value)) {
|
|
1098
|
-
for (const item of value) {
|
|
1099
|
-
if (typeof item === "string" && item.trim().length > 0) {
|
|
1100
|
-
paths.push(item);
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
return [...new Set(paths)];
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
interface ArtifactRequestContext {
|
|
1109
|
-
absolutePath: string;
|
|
1110
|
-
path: string;
|
|
1111
|
-
status?: FileArtifactMetadata["status"];
|
|
1112
|
-
tempPath?: string;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
async function buildFileArtifactMetadata(options: {
|
|
1116
|
-
artifactRequest?: ArtifactRequestContext;
|
|
1117
|
-
commandInfo: CommandInfo;
|
|
1118
|
-
cwd: string;
|
|
1119
|
-
path: string;
|
|
1120
|
-
sessionName?: string;
|
|
1121
|
-
}): Promise<FileArtifactMetadata | undefined> {
|
|
1122
|
-
const kind = getArtifactKind(options.commandInfo);
|
|
1123
|
-
if (!kind) {
|
|
1124
|
-
return undefined;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
const absolutePath = options.artifactRequest?.absolutePath ?? resolve(options.cwd, options.path);
|
|
1128
|
-
const displayPath = options.artifactRequest?.path ?? options.path;
|
|
1129
|
-
const extension = extname(absolutePath || options.path).toLowerCase() || undefined;
|
|
1130
|
-
let exists: boolean | undefined;
|
|
1131
|
-
let sizeBytes: number | undefined;
|
|
1132
|
-
try {
|
|
1133
|
-
const fileStats = await stat(absolutePath);
|
|
1134
|
-
exists = true;
|
|
1135
|
-
sizeBytes = fileStats.size;
|
|
1136
|
-
} catch {
|
|
1137
|
-
exists = false;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
return {
|
|
1141
|
-
absolutePath,
|
|
1142
|
-
artifactType: kind,
|
|
1143
|
-
command: options.commandInfo.command,
|
|
1144
|
-
cwd: options.cwd,
|
|
1145
|
-
exists,
|
|
1146
|
-
extension,
|
|
1147
|
-
kind,
|
|
1148
|
-
mediaType: extension ? ARTIFACT_EXTENSION_TO_MEDIA_TYPE[extension] : undefined,
|
|
1149
|
-
path: displayPath,
|
|
1150
|
-
requestedPath: options.artifactRequest?.path,
|
|
1151
|
-
session: options.sessionName,
|
|
1152
|
-
sizeBytes,
|
|
1153
|
-
status: options.artifactRequest?.status ?? (exists === false ? "missing" : "saved"),
|
|
1154
|
-
subcommand: options.commandInfo.subcommand,
|
|
1155
|
-
tempPath: options.artifactRequest?.tempPath,
|
|
1156
|
-
};
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
async function extractFileArtifacts(options: {
|
|
1160
|
-
artifactRequest?: ArtifactRequestContext;
|
|
1161
|
-
commandInfo: CommandInfo;
|
|
1162
|
-
cwd: string;
|
|
1163
|
-
data: unknown;
|
|
1164
|
-
sessionName?: string;
|
|
1165
|
-
}): Promise<FileArtifactMetadata[]> {
|
|
1166
|
-
const candidates = extractPathStrings(options.data);
|
|
1167
|
-
const artifacts = await Promise.all(candidates.map((path) => buildFileArtifactMetadata({ ...options, path })));
|
|
1168
|
-
return artifacts.filter((artifact): artifact is FileArtifactMetadata => artifact !== undefined);
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
function buildManifestEntriesForFileArtifacts(artifacts: FileArtifactMetadata[], nowMs = Date.now()): SessionArtifactManifestEntry[] {
|
|
1172
|
-
return artifacts.map((artifact) => ({
|
|
1173
|
-
absolutePath: artifact.absolutePath,
|
|
1174
|
-
command: artifact.command,
|
|
1175
|
-
createdAtMs: nowMs,
|
|
1176
|
-
cwd: artifact.cwd,
|
|
1177
|
-
exists: artifact.exists,
|
|
1178
|
-
extension: artifact.extension,
|
|
1179
|
-
kind: artifact.kind,
|
|
1180
|
-
mediaType: artifact.mediaType,
|
|
1181
|
-
path: artifact.path,
|
|
1182
|
-
requestedPath: artifact.requestedPath,
|
|
1183
|
-
retentionState: artifact.exists === false ? "missing" : "live",
|
|
1184
|
-
session: artifact.session,
|
|
1185
|
-
sizeBytes: artifact.sizeBytes,
|
|
1186
|
-
storageScope: "explicit-path",
|
|
1187
|
-
subcommand: artifact.subcommand,
|
|
1188
|
-
}));
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function isRecordingStartArtifact(artifact: FileArtifactMetadata): boolean {
|
|
1192
|
-
return artifact.command === "record" && artifact.subcommand === "start" && artifact.kind === "video";
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function isManifestFileArtifact(artifact: FileArtifactMetadata): boolean {
|
|
1196
|
-
return !isRecordingStartArtifact(artifact);
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
function getArtifactVerificationEntry(artifact: FileArtifactMetadata): ArtifactVerificationEntry {
|
|
1200
|
-
if (isRecordingStartArtifact(artifact)) {
|
|
1201
|
-
return {
|
|
1202
|
-
absolutePath: artifact.absolutePath,
|
|
1203
|
-
exists: artifact.exists,
|
|
1204
|
-
kind: artifact.kind,
|
|
1205
|
-
limitation: "Recording output is pending until record stop completes.",
|
|
1206
|
-
mediaType: artifact.mediaType,
|
|
1207
|
-
path: artifact.path,
|
|
1208
|
-
requestedPath: artifact.requestedPath,
|
|
1209
|
-
retentionState: undefined,
|
|
1210
|
-
sizeBytes: artifact.sizeBytes,
|
|
1211
|
-
state: "pending",
|
|
1212
|
-
status: artifact.status,
|
|
1213
|
-
storageScope: undefined,
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
const state = artifact.exists === true
|
|
1217
|
-
? "verified"
|
|
1218
|
-
: artifact.exists === false
|
|
1219
|
-
? "missing"
|
|
1220
|
-
: "unverified";
|
|
1221
|
-
return {
|
|
1222
|
-
absolutePath: artifact.absolutePath,
|
|
1223
|
-
exists: artifact.exists,
|
|
1224
|
-
kind: artifact.kind,
|
|
1225
|
-
limitation: state === "missing"
|
|
1226
|
-
? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
|
|
1227
|
-
: state === "unverified"
|
|
1228
|
-
? "The wrapper could not prove local filesystem existence for this artifact."
|
|
1229
|
-
: undefined,
|
|
1230
|
-
mediaType: artifact.mediaType,
|
|
1231
|
-
path: artifact.path,
|
|
1232
|
-
requestedPath: artifact.requestedPath,
|
|
1233
|
-
retentionState: artifact.exists === false ? "missing" : "live",
|
|
1234
|
-
sizeBytes: artifact.sizeBytes,
|
|
1235
|
-
state,
|
|
1236
|
-
status: artifact.status,
|
|
1237
|
-
storageScope: "explicit-path",
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
function getManifestVerificationEntry(entry: SessionArtifactManifestEntry): ArtifactVerificationEntry | undefined {
|
|
1242
|
-
if (entry.storageScope === "explicit-path") return undefined;
|
|
1243
|
-
const state = entry.retentionState === "live"
|
|
1244
|
-
? "verified"
|
|
1245
|
-
: entry.retentionState === "missing" || entry.retentionState === "evicted"
|
|
1246
|
-
? "missing"
|
|
1247
|
-
: "unverified";
|
|
1248
|
-
return {
|
|
1249
|
-
absolutePath: entry.absolutePath,
|
|
1250
|
-
exists: entry.exists,
|
|
1251
|
-
kind: entry.kind,
|
|
1252
|
-
limitation: entry.retentionState === "ephemeral"
|
|
1253
|
-
? "This spill file is process-temporary and may not survive reload or restart."
|
|
1254
|
-
: entry.retentionState === "evicted"
|
|
1255
|
-
? "This persisted spill file was evicted from the bounded session artifact store."
|
|
1256
|
-
: undefined,
|
|
1257
|
-
mediaType: entry.mediaType,
|
|
1258
|
-
path: entry.path,
|
|
1259
|
-
requestedPath: entry.requestedPath,
|
|
1260
|
-
retentionState: entry.retentionState,
|
|
1261
|
-
sizeBytes: entry.sizeBytes,
|
|
1262
|
-
state,
|
|
1263
|
-
storageScope: entry.storageScope,
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
function buildArtifactVerificationSummary(
|
|
1268
|
-
artifacts: FileArtifactMetadata[],
|
|
1269
|
-
manifest?: SessionArtifactManifest,
|
|
1270
|
-
manifestPaths?: ReadonlySet<string>,
|
|
1271
|
-
): ArtifactVerificationSummary | undefined {
|
|
1272
|
-
const entries = [
|
|
1273
|
-
...artifacts.map(getArtifactVerificationEntry),
|
|
1274
|
-
...(manifest?.entries.flatMap((entry) => {
|
|
1275
|
-
if (manifestPaths && !manifestPaths.has(entry.path)) return [];
|
|
1276
|
-
const verificationEntry = getManifestVerificationEntry(entry);
|
|
1277
|
-
return verificationEntry ? [verificationEntry] : [];
|
|
1278
|
-
}) ?? []),
|
|
1279
|
-
];
|
|
1280
|
-
if (entries.length === 0) return undefined;
|
|
1281
|
-
const verifiedCount = entries.filter((entry) => entry.state === "verified").length;
|
|
1282
|
-
const missingCount = entries.filter((entry) => entry.state === "missing").length;
|
|
1283
|
-
const pendingCount = entries.filter((entry) => entry.state === "pending").length;
|
|
1284
|
-
const unverifiedCount = entries.filter((entry) => entry.state === "unverified").length;
|
|
1285
|
-
return {
|
|
1286
|
-
artifacts: entries,
|
|
1287
|
-
missingCount,
|
|
1288
|
-
pendingCount,
|
|
1289
|
-
unverifiedCount,
|
|
1290
|
-
verified: entries.length > 0 && verifiedCount === entries.length,
|
|
1291
|
-
verifiedCount,
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
function classifyPresentationSuccessCategory(options: {
|
|
1296
|
-
artifactVerification?: ArtifactVerificationSummary;
|
|
1297
|
-
artifacts?: FileArtifactMetadata[];
|
|
1298
|
-
inspection?: boolean;
|
|
1299
|
-
savedFile?: SavedFilePresentationDetails;
|
|
1300
|
-
}) {
|
|
1301
|
-
if ((options.artifactVerification?.missingCount ?? 0) > 0 || (options.artifactVerification?.unverifiedCount ?? 0) > 0) {
|
|
1302
|
-
return "artifact-unverified" as const;
|
|
1303
|
-
}
|
|
1304
|
-
return classifyAgentBrowserSuccessCategory(options);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
function formatArtifactLabel(artifact: FileArtifactMetadata): string {
|
|
1308
|
-
switch (artifact.kind) {
|
|
1309
|
-
case "download":
|
|
1310
|
-
return artifact.command === "wait" && artifact.subcommand === "--download" ? "Download completed" : "Downloaded file";
|
|
1311
|
-
case "file":
|
|
1312
|
-
return artifact.command === "state" ? "State file" : "Saved file";
|
|
1313
|
-
case "har":
|
|
1314
|
-
return "Saved HAR";
|
|
1315
|
-
case "image":
|
|
1316
|
-
return artifact.command === "diff" && artifact.subcommand === "screenshot" ? "Saved diff image" : "Saved image";
|
|
1317
|
-
case "pdf":
|
|
1318
|
-
return "Saved PDF";
|
|
1319
|
-
case "profile":
|
|
1320
|
-
return "Saved profile";
|
|
1321
|
-
case "trace":
|
|
1322
|
-
return "Saved trace";
|
|
1323
|
-
case "video":
|
|
1324
|
-
return isRecordingStartArtifact(artifact) ? "Recording started; output will be written on stop" : "Saved recording";
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
function formatArtifactSummary(artifacts: FileArtifactMetadata[]): string | undefined {
|
|
1329
|
-
if (artifacts.length === 0) {
|
|
1330
|
-
return undefined;
|
|
1331
|
-
}
|
|
1332
|
-
if (artifacts.length === 1) {
|
|
1333
|
-
const artifact = artifacts[0];
|
|
1334
|
-
return `${formatArtifactLabel(artifact)}: ${artifact.path}`;
|
|
1335
|
-
}
|
|
1336
|
-
return `Saved ${artifacts.length} artifacts: ${artifacts.map((artifact) => `${artifact.kind} ${artifact.path}`).join(", ")}`;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
function formatArtifactMetadataLines(artifacts: FileArtifactMetadata[]): string[] {
|
|
1340
|
-
return artifacts.map((artifact, index) => {
|
|
1341
|
-
if (isRecordingStartArtifact(artifact)) {
|
|
1342
|
-
return [
|
|
1343
|
-
`${formatArtifactLabel(artifact)}: ${artifact.path}`,
|
|
1344
|
-
`Artifact type: ${artifact.kind}`,
|
|
1345
|
-
`Requested path: ${artifact.requestedPath ?? artifact.path}`,
|
|
1346
|
-
`Absolute path: ${artifact.absolutePath}`,
|
|
1347
|
-
`Exists: ${artifact.exists === true}`,
|
|
1348
|
-
`Status: ${artifact.status ?? (artifact.exists === false ? "missing" : "saved")}`,
|
|
1349
|
-
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
1350
|
-
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
1351
|
-
`Machine data: details.artifacts[${index}]`,
|
|
1352
|
-
].filter((item): item is string => item !== undefined).join("\n");
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
return [
|
|
1356
|
-
`${formatArtifactLabel(artifact)}: ${artifact.path}`,
|
|
1357
|
-
`Artifact type: ${artifact.kind}`,
|
|
1358
|
-
`Requested path: ${artifact.requestedPath ?? artifact.path}`,
|
|
1359
|
-
`Absolute path: ${artifact.absolutePath}`,
|
|
1360
|
-
`Exists: ${artifact.exists === true}`,
|
|
1361
|
-
artifact.exists === false ? "not found on disk" : undefined,
|
|
1362
|
-
typeof artifact.sizeBytes === "number" ? `Size: ${formatByteCount(artifact.sizeBytes)}` : undefined,
|
|
1363
|
-
typeof artifact.sizeBytes === "number" ? `Size bytes: ${artifact.sizeBytes}` : undefined,
|
|
1364
|
-
`Status: ${artifact.status ?? (artifact.exists === false ? "missing" : "saved")}`,
|
|
1365
|
-
artifact.tempPath ? `Temp path: ${artifact.tempPath}` : undefined,
|
|
1366
|
-
artifact.mediaType ? `Media type: ${artifact.mediaType}` : undefined,
|
|
1367
|
-
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
1368
|
-
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
1369
|
-
`Machine data: details.artifacts[${index}]`,
|
|
1370
|
-
].filter((item): item is string => item !== undefined).join("\n");
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
function isDownloadWaitCommand(commandInfo: CommandInfo): boolean {
|
|
1375
|
-
return commandInfo.command === "wait" && commandInfo.subcommand === "--download";
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
function extractSavedFilePath(data: Record<string, unknown>): string | undefined {
|
|
1379
|
-
return typeof data.path === "string" && data.path.trim().length > 0 ? data.path : undefined;
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
function getSavedFileDetails(commandInfo: CommandInfo, data: Record<string, unknown>): SavedFilePresentationDetails | undefined {
|
|
1383
|
-
const path = extractSavedFilePath(data);
|
|
1384
|
-
if (!path) {
|
|
1385
|
-
return undefined;
|
|
1386
|
-
}
|
|
1387
|
-
const savedFileCommand = isDownloadWaitCommand(commandInfo)
|
|
1388
|
-
? "wait"
|
|
1389
|
-
: commandInfo.command === "download" || commandInfo.command === "pdf"
|
|
1390
|
-
? commandInfo.command
|
|
1391
|
-
: undefined;
|
|
1392
|
-
if (!savedFileCommand) {
|
|
1393
|
-
return undefined;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
const { path: _path, ...metadata } = data;
|
|
1397
|
-
const details: SavedFilePresentationDetails = {
|
|
1398
|
-
command: savedFileCommand,
|
|
1399
|
-
kind: savedFileCommand === "pdf" ? "pdf" : "download",
|
|
1400
|
-
path,
|
|
1401
|
-
};
|
|
1402
|
-
if (Object.keys(metadata).length > 0) {
|
|
1403
|
-
details.metadata = metadata;
|
|
1404
|
-
}
|
|
1405
|
-
if (commandInfo.subcommand) {
|
|
1406
|
-
details.subcommand = commandInfo.subcommand;
|
|
1407
|
-
}
|
|
1408
|
-
return details;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
function getScalarExtractionResult(data: Record<string, unknown>): string | undefined {
|
|
1412
|
-
const { result } = data;
|
|
1413
|
-
if (typeof result === "string") {
|
|
1414
|
-
return result.trim().length > 0 ? result : undefined;
|
|
1415
|
-
}
|
|
1416
|
-
if (typeof result === "number" || typeof result === "boolean") {
|
|
1417
|
-
return String(result);
|
|
1418
|
-
}
|
|
1419
|
-
return undefined;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
function getExtractionOrigin(data: Record<string, unknown>): string | undefined {
|
|
1423
|
-
if (typeof data.origin === "string" && data.origin.trim().length > 0) {
|
|
1424
|
-
return data.origin.trim();
|
|
1425
|
-
}
|
|
1426
|
-
if (typeof data.url === "string" && data.url.trim().length > 0) {
|
|
1427
|
-
return data.url.trim();
|
|
1428
|
-
}
|
|
1429
|
-
return undefined;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
function formatGetSummaryLabel(subcommand: string | undefined): string {
|
|
1433
|
-
if (!subcommand) {
|
|
1434
|
-
return "Get result";
|
|
1435
|
-
}
|
|
1436
|
-
if (subcommand.toLowerCase() === "url") {
|
|
1437
|
-
return "URL";
|
|
1438
|
-
}
|
|
1439
|
-
return `${subcommand.slice(0, 1).toUpperCase()}${subcommand.slice(1)}`;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
function formatExtractionSummary(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
|
|
1443
|
-
const scalarResult = getScalarExtractionResult(data);
|
|
1444
|
-
if (!scalarResult) {
|
|
1445
|
-
return undefined;
|
|
1446
|
-
}
|
|
1447
|
-
const safeScalarResult = redactModelFacingText(scalarResult);
|
|
1448
|
-
const firstResultLine = safeScalarResult.split("\n", 1)[0] ?? safeScalarResult;
|
|
1449
|
-
if (commandInfo.command === "get") {
|
|
1450
|
-
return `${formatGetSummaryLabel(commandInfo.subcommand)}: ${firstResultLine}`;
|
|
1451
|
-
}
|
|
1452
|
-
if (commandInfo.command === "eval") {
|
|
1453
|
-
return `Eval result: ${firstResultLine}`;
|
|
1454
|
-
}
|
|
1455
|
-
return undefined;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
function formatExtractionText(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
|
|
1459
|
-
if (commandInfo.command !== "get" && commandInfo.command !== "eval") {
|
|
1460
|
-
return undefined;
|
|
1461
|
-
}
|
|
1462
|
-
const scalarResult = getScalarExtractionResult(data);
|
|
1463
|
-
if (!scalarResult) {
|
|
1464
|
-
return undefined;
|
|
1465
|
-
}
|
|
1466
|
-
const origin = getExtractionOrigin(data);
|
|
1467
|
-
const safeScalarResult = redactModelFacingText(scalarResult);
|
|
1468
|
-
const safeOrigin = origin ? redactModelFacingText(origin) : undefined;
|
|
1469
|
-
return safeOrigin && safeOrigin !== safeScalarResult ? `${safeScalarResult}\n\nOrigin: ${safeOrigin}` : safeScalarResult;
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
function isNavigationObservableCommand(command: string | undefined): boolean {
|
|
1473
|
-
return command !== undefined && NAVIGATION_SUMMARY_COMMANDS.has(command);
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
function isNavigationSummary(value: unknown): value is NavigationSummary {
|
|
1477
|
-
return isRecord(value) && (typeof value.title === "string" || typeof value.url === "string");
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
function getNavigationSummary(data: Record<string, unknown>): NavigationSummary | undefined {
|
|
1481
|
-
const candidate = data[NAVIGATION_SUMMARY_FIELD];
|
|
1482
|
-
return isNavigationSummary(candidate) ? candidate : undefined;
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
function getTopLevelNavigationSummary(data: Record<string, unknown>): NavigationSummary | undefined {
|
|
1486
|
-
return isNavigationSummary(data)
|
|
1487
|
-
? {
|
|
1488
|
-
title: typeof data.title === "string" ? data.title : undefined,
|
|
1489
|
-
url: typeof data.url === "string" ? data.url : undefined,
|
|
1490
|
-
}
|
|
1491
|
-
: undefined;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
function getNormalizedNavigationSummary(summary: NavigationSummary | undefined): { title?: string; url?: string } | undefined {
|
|
1495
|
-
const title = typeof summary?.title === "string" && summary.title.trim().length > 0 ? summary.title.trim() : undefined;
|
|
1496
|
-
const url = typeof summary?.url === "string" && summary.url.trim().length > 0 ? summary.url.trim() : undefined;
|
|
1497
|
-
return title || url ? { title, url } : undefined;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
function formatNavigationSummary(summary: NavigationSummary): string | undefined {
|
|
1501
|
-
const normalized = getNormalizedNavigationSummary(summary);
|
|
1502
|
-
if (!normalized) return undefined;
|
|
1503
|
-
if (normalized.title && normalized.url) return `${normalized.title}\n${normalized.url}`;
|
|
1504
|
-
return normalized.title ?? normalized.url;
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
function isPageChangeSummaryCommand(command: string | undefined): boolean {
|
|
1508
|
-
return command !== undefined && PAGE_CHANGE_SUMMARY_COMMANDS.has(command);
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
function buildPageChangeSummary(options: {
|
|
1512
|
-
artifacts?: FileArtifactMetadata[];
|
|
1513
|
-
commandInfo: CommandInfo;
|
|
1514
|
-
data: unknown;
|
|
1515
|
-
nextActions?: Array<{ id: string }>;
|
|
1516
|
-
savedFilePath?: string;
|
|
1517
|
-
summary: string;
|
|
1518
|
-
}): AgentBrowserPageChangeSummary | undefined {
|
|
1519
|
-
const { artifacts, commandInfo, data, nextActions, savedFilePath } = options;
|
|
1520
|
-
const artifactCount = artifacts?.length ?? 0;
|
|
1521
|
-
const navigation = isRecord(data)
|
|
1522
|
-
? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command) ? getTopLevelNavigationSummary(data) : undefined))
|
|
1523
|
-
: undefined;
|
|
1524
|
-
const confirmationRequired = detectConfirmationRequired(data) !== undefined;
|
|
1525
|
-
if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command)) {
|
|
1526
|
-
return undefined;
|
|
1527
|
-
}
|
|
1528
|
-
const changeType: AgentBrowserPageChangeSummary["changeType"] = savedFilePath || artifactCount > 0
|
|
1529
|
-
? "artifact"
|
|
1530
|
-
: navigation
|
|
1531
|
-
? "navigation"
|
|
1532
|
-
: confirmationRequired
|
|
1533
|
-
? "confirmation"
|
|
1534
|
-
: "mutation";
|
|
1535
|
-
const parts = [commandInfo.command ?? "agent-browser", changeType];
|
|
1536
|
-
if (navigation?.title) parts.push(navigation.title);
|
|
1537
|
-
if (navigation?.url) parts.push(navigation.url);
|
|
1538
|
-
if (savedFilePath) parts.push(savedFilePath);
|
|
1539
|
-
else if (artifactCount > 0) parts.push(`${artifactCount} artifact${artifactCount === 1 ? "" : "s"}`);
|
|
1540
|
-
return {
|
|
1541
|
-
...(artifactCount > 0 ? { artifactCount } : {}),
|
|
1542
|
-
changeType,
|
|
1543
|
-
...(commandInfo.command ? { command: commandInfo.command } : {}),
|
|
1544
|
-
...(nextActions ? { nextActionIds: nextActions.map((action) => action.id) } : {}),
|
|
1545
|
-
...(savedFilePath ? { savedFilePath } : {}),
|
|
1546
|
-
summary: parts.join(" → "),
|
|
1547
|
-
...(navigation?.title ? { title: navigation.title } : {}),
|
|
1548
|
-
...(navigation?.url ? { url: navigation.url } : {}),
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
function stripNavigationSummary(data: Record<string, unknown>): Record<string, unknown> {
|
|
1553
|
-
const { [NAVIGATION_SUMMARY_FIELD]: _navigationSummary, ...rest } = data;
|
|
1554
|
-
return rest;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
function formatNavigationActionResult(data: Record<string, unknown>): string | undefined {
|
|
1558
|
-
const actionData = stripNavigationSummary(data);
|
|
1559
|
-
const lines: string[] = [];
|
|
1560
|
-
if (typeof actionData.clicked === "string" || typeof actionData.clicked === "boolean") {
|
|
1561
|
-
lines.push(`Clicked: ${String(actionData.clicked)}`);
|
|
1562
|
-
}
|
|
1563
|
-
if (typeof actionData.href === "string") {
|
|
1564
|
-
lines.push(`Href: ${redactModelFacingText(actionData.href)}`);
|
|
1565
|
-
}
|
|
1566
|
-
if (typeof actionData.navigated === "boolean") {
|
|
1567
|
-
lines.push(`Navigated: ${actionData.navigated}`);
|
|
1568
|
-
}
|
|
1569
|
-
if (lines.length > 0) {
|
|
1570
|
-
return lines.join("\n");
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
const actionText = stringifyModelFacing(actionData).trim();
|
|
1574
|
-
if (actionText.length === 0 || actionText === "{}") {
|
|
1575
|
-
return undefined;
|
|
1576
|
-
}
|
|
1577
|
-
return actionText;
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
function isStringArray(value: unknown): value is string[] {
|
|
1581
|
-
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
function getPresentationText(presentation: ToolPresentation): string {
|
|
1585
|
-
return presentation.content
|
|
1586
|
-
.filter((part): part is Extract<ToolPresentation["content"][number], { type: "text" }> => part.type === "text")
|
|
1587
|
-
.map((part) => part.text.trim())
|
|
1588
|
-
.filter((text) => text.length > 0)
|
|
1589
|
-
.join("\n\n");
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
function getPresentationImages(presentation: ToolPresentation): Array<Extract<ToolPresentation["content"][number], { type: "image" }>> {
|
|
1593
|
-
return presentation.content.filter(
|
|
1594
|
-
(part): part is Extract<ToolPresentation["content"][number], { type: "image" }> => part.type === "image",
|
|
1595
|
-
);
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
function getPresentationPaths(options: {
|
|
1599
|
-
primaryPath?: string;
|
|
1600
|
-
secondaryPaths?: string[];
|
|
1601
|
-
}): string[] {
|
|
1602
|
-
return options.secondaryPaths ?? (options.primaryPath ? [options.primaryPath] : []);
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
function formatBatchStepCommand(command: string[] | undefined, index: number): string {
|
|
1606
|
-
return command && command.length > 0 ? command.join(" ") : `step-${index + 1}`;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
const STALE_REF_ERROR_HINT = [
|
|
1610
|
-
"Agent-browser hint: This ref may be stale after navigation, scrolling, or re-rendering.",
|
|
1611
|
-
"Run `snapshot -i` again and retry with a current `@e…` ref; for less ref churn, use `find role|text|label|placeholder|alt|title|testid ...` or `scrollintoview` before interacting with off-screen elements.",
|
|
1612
|
-
].join(" ");
|
|
1613
|
-
|
|
1614
|
-
const SELECTOR_DIALECT_ERROR_HINT = [
|
|
1615
|
-
"Agent-browser hint: This selector may use an unsupported selector dialect.",
|
|
1616
|
-
"Prefer refs from `snapshot -i`, or use supported `find role|text|label|placeholder|alt|title|testid ...` locators; use `scrollintoview` before interacting with off-screen elements.",
|
|
1617
|
-
].join(" ");
|
|
1618
|
-
|
|
1619
|
-
function getSelectorRecoveryHint(errorText: string): string | undefined {
|
|
1620
|
-
const normalized = errorText.trim();
|
|
1621
|
-
if (normalized.length === 0) {
|
|
1622
|
-
return undefined;
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
if (/\bUnknown ref\b|\bstale ref\b|\bref\b.*\b(?:not found|missing|expired)\b/i.test(normalized)) {
|
|
1626
|
-
return STALE_REF_ERROR_HINT;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
const mentionsPlaywrightSelectorDialect = /(?:\btext=|:has-text\(|\bgetByRole\b|\bgetByText\b)/i.test(normalized);
|
|
1630
|
-
const reportsSelectorMatchFailure =
|
|
1631
|
-
/\b(?:no elements? found|failed to find|could not find|unable to find)\b.*\b(?:selector|locator)\b/i.test(normalized) ||
|
|
1632
|
-
/\b(?:selector|locator)\b.*\b(?:no elements? found|not found|missing|failed to find|could not find|unable to find)\b/i.test(
|
|
1633
|
-
normalized,
|
|
1634
|
-
);
|
|
1635
|
-
|
|
1636
|
-
if (
|
|
1637
|
-
/\b(?:unsupported|unknown|invalid)\s+(?:selector|locator)\b/i.test(normalized) ||
|
|
1638
|
-
/\bfailed to parse selector\b/i.test(normalized) ||
|
|
1639
|
-
/\bselector\b.*\b(?:parse|syntax|unsupported|invalid)\b/i.test(normalized) ||
|
|
1640
|
-
(mentionsPlaywrightSelectorDialect && reportsSelectorMatchFailure)
|
|
1641
|
-
) {
|
|
1642
|
-
return SELECTOR_DIALECT_ERROR_HINT;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
return undefined;
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
interface CommandSuggestion {
|
|
1649
|
-
args?: string[];
|
|
1650
|
-
description: string;
|
|
1651
|
-
id?: string;
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
const UNKNOWN_COMMAND_SUGGESTIONS: Record<string, CommandSuggestion[]> = {
|
|
1655
|
-
attr: [
|
|
1656
|
-
{ description: "Use `get attr <selector> <name>` to read an attribute from a selector or current `@ref`." },
|
|
1657
|
-
],
|
|
1658
|
-
count: [
|
|
1659
|
-
{ description: "Use `get count <selector>` to count matching elements." },
|
|
1660
|
-
],
|
|
1661
|
-
html: [
|
|
1662
|
-
{ description: "Use `get html <selector>` to read element HTML, or `get html` for the page when upstream supports it." },
|
|
1663
|
-
],
|
|
1664
|
-
text: [
|
|
1665
|
-
{ description: "Use `get text <selector>` to read text from a selector or current `@ref`; run `snapshot -i` first when you need a safe `@ref`." },
|
|
1666
|
-
],
|
|
1667
|
-
title: [
|
|
1668
|
-
{ args: ["get", "title"], description: "Use `get title` to read the current page title.", id: "use-get-title" },
|
|
1669
|
-
],
|
|
1670
|
-
url: [
|
|
1671
|
-
{ args: ["get", "url"], description: "Use `get url` to read the current page URL.", id: "use-get-url" },
|
|
1672
|
-
],
|
|
1673
|
-
value: [
|
|
1674
|
-
{ description: "Use `get value <selector>` to read form control value from a selector or current `@ref`." },
|
|
1675
|
-
],
|
|
1676
|
-
};
|
|
1677
|
-
|
|
1678
|
-
function getUnknownCommandSuggestions(command: string | undefined, errorText: string): CommandSuggestion[] {
|
|
1679
|
-
if (!command) return [];
|
|
1680
|
-
const normalizedCommand = command.trim().toLowerCase();
|
|
1681
|
-
if (!/\bunknown\s+command\b|\bunknown\s+subcommand\b|\bunrecognized\s+command\b/i.test(errorText)) return [];
|
|
1682
|
-
return UNKNOWN_COMMAND_SUGGESTIONS[normalizedCommand] ?? [];
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
function formatUnknownCommandSuggestionText(suggestions: CommandSuggestion[]): string | undefined {
|
|
1686
|
-
if (suggestions.length === 0) return undefined;
|
|
1687
|
-
return ["Agent-browser hint: This looks like a getter shortcut, but upstream getter commands are grouped under `get`.", ...suggestions.map((suggestion) => suggestion.description)].join(" ");
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
function withSessionPrefix(sessionName: string | undefined, args: string[]): string[] {
|
|
1691
|
-
return sessionName ? ["--session", sessionName, ...args] : args;
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
function buildUnknownCommandSuggestionActions(suggestions: CommandSuggestion[], sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
|
|
1695
|
-
const actions = suggestions
|
|
1696
|
-
.filter((suggestion): suggestion is CommandSuggestion & { args: string[]; id: string } => suggestion.args !== undefined && suggestion.id !== undefined)
|
|
1697
|
-
.map((suggestion) => ({
|
|
1698
|
-
id: suggestion.id,
|
|
1699
|
-
params: { args: withSessionPrefix(sessionName, suggestion.args) },
|
|
1700
|
-
reason: suggestion.description,
|
|
1701
|
-
safety: "Read-only getter command; safe to retry when you intended to inspect page state.",
|
|
1702
|
-
tool: "agent_browser" as const,
|
|
1703
|
-
}));
|
|
1704
|
-
return actions.length > 0 ? actions : undefined;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
function appendSelectorRecoveryHint(errorText: string): string {
|
|
1708
|
-
const hint = getSelectorRecoveryHint(errorText);
|
|
1709
|
-
if (!hint || errorText.includes("Agent-browser hint:")) {
|
|
1710
|
-
return errorText;
|
|
1711
|
-
}
|
|
1712
|
-
return `${errorText}\n\n${hint}`;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
function formatBatchStepError(error: unknown): string {
|
|
1716
|
-
const errorText = stringifyModelFacing(error).trim();
|
|
1717
|
-
const formattedErrorText = errorText.length > 0 ? `Error: ${errorText}` : "Error: batch step failed.";
|
|
1718
|
-
return appendSelectorRecoveryHint(formattedErrorText);
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
function getBatchFailureDetails(steps: Array<{ details: BatchStepPresentationDetails }>): BatchFailurePresentationDetails | undefined {
|
|
1722
|
-
const failedSteps = steps.filter((step) => step.details.success === false);
|
|
1723
|
-
if (failedSteps.length === 0) {
|
|
1724
|
-
return undefined;
|
|
1725
|
-
}
|
|
1726
|
-
const successCount = steps.length - failedSteps.length;
|
|
1727
|
-
return {
|
|
1728
|
-
failedStep: failedSteps[0].details,
|
|
1729
|
-
failureCount: failedSteps.length,
|
|
1730
|
-
successCount,
|
|
1731
|
-
totalCount: steps.length,
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
function hasModelFacingArgRedaction(args: string[] | undefined): boolean {
|
|
1736
|
-
return args?.some((arg) => arg === "[REDACTED]" || arg.includes("%5BREDACTED%5D") || arg.includes("[REDACTED]")) === true;
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
function getStatefulCommandSensitiveValues(command: string[] | undefined): string[] {
|
|
1740
|
-
if (!command) return [];
|
|
1741
|
-
const tokens = extractCommandTokens(command);
|
|
1742
|
-
const values: string[] = [];
|
|
1743
|
-
if (tokens[0] === "cookies" && tokens[1] === "set" && tokens[3]) values.push(tokens[3]);
|
|
1744
|
-
if (tokens[0] === "storage" && ["local", "session"].includes(tokens[1] ?? "") && tokens[2] === "set" && tokens[4]) values.push(tokens[4]);
|
|
1745
|
-
for (let index = 0; index < tokens.length; index += 1) {
|
|
1746
|
-
const token = tokens[index];
|
|
1747
|
-
if (token === "--password" && tokens[index + 1]) values.push(tokens[index + 1]);
|
|
1748
|
-
else if (token?.startsWith("--password=")) values.push(token.slice("--password=".length));
|
|
1749
|
-
}
|
|
1750
|
-
return values.filter((value) => value.length > 0);
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
function redactExactValues(value: unknown, sensitiveValues: string[]): unknown {
|
|
1754
|
-
if (sensitiveValues.length === 0) return redactSensitiveValue(value);
|
|
1755
|
-
if (typeof value === "string") {
|
|
1756
|
-
let redacted = value;
|
|
1757
|
-
for (const sensitiveValue of sensitiveValues) redacted = redacted.split(sensitiveValue).join("[REDACTED]");
|
|
1758
|
-
return redactSensitiveText(redacted);
|
|
1759
|
-
}
|
|
1760
|
-
if (Array.isArray(value)) return value.map((item) => redactExactValues(item, sensitiveValues));
|
|
1761
|
-
if (!isRecord(value)) return value;
|
|
1762
|
-
return redactSensitiveValue(Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactValues(entryValue, sensitiveValues)])));
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
async function buildBatchStepPresentation(options: {
|
|
1766
|
-
artifactManifest?: SessionArtifactManifest;
|
|
1767
|
-
artifactRequest?: ArtifactRequestContext;
|
|
1768
|
-
cwd: string;
|
|
1769
|
-
index: number;
|
|
1770
|
-
item: AgentBrowserBatchResult;
|
|
1771
|
-
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
1772
|
-
sessionName?: string;
|
|
1773
|
-
}): Promise<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> {
|
|
1774
|
-
const { artifactManifest, artifactRequest, cwd, index, item, persistentArtifactStore, sessionName } = options;
|
|
1775
|
-
const command = isStringArray(item.command) ? item.command : undefined;
|
|
1776
|
-
const redactedCommand = command ? redactInvocationArgs(command) : undefined;
|
|
1777
|
-
const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
|
|
1778
|
-
|
|
1779
|
-
if (item.success === false) {
|
|
1780
|
-
const redactedErrorData = redactExactValues(item.error, getStatefulCommandSensitiveValues(command));
|
|
1781
|
-
const errorText = formatBatchStepError(redactedErrorData);
|
|
1782
|
-
const failureCategory = classifyAgentBrowserFailureCategory({
|
|
1783
|
-
args: command,
|
|
1784
|
-
command: command?.[0],
|
|
1785
|
-
errorText,
|
|
1786
|
-
});
|
|
1787
|
-
const confirmationRequired = detectConfirmationRequired(item.error);
|
|
1788
|
-
const nextActions = buildAgentBrowserNextActions({
|
|
1789
|
-
args: command,
|
|
1790
|
-
command: command?.[0],
|
|
1791
|
-
confirmationId: confirmationRequired?.id,
|
|
1792
|
-
failureCategory,
|
|
1793
|
-
resultCategory: "failure",
|
|
1794
|
-
});
|
|
1795
|
-
const presentation: ToolPresentation = {
|
|
1796
|
-
content: [{ type: "text", text: errorText }],
|
|
1797
|
-
failureCategory,
|
|
1798
|
-
nextActions,
|
|
1799
|
-
resultCategory: "failure",
|
|
1800
|
-
summary: errorText,
|
|
1801
|
-
};
|
|
1802
|
-
return {
|
|
1803
|
-
details: {
|
|
1804
|
-
artifactVerification: presentation.artifactVerification,
|
|
1805
|
-
artifacts: presentation.artifacts,
|
|
1806
|
-
command: redactedCommand,
|
|
1807
|
-
commandText,
|
|
1808
|
-
data: redactedErrorData,
|
|
1809
|
-
failureCategory,
|
|
1810
|
-
index,
|
|
1811
|
-
nextActions,
|
|
1812
|
-
resultCategory: "failure",
|
|
1813
|
-
success: false,
|
|
1814
|
-
summary: errorText,
|
|
1815
|
-
text: errorText,
|
|
1816
|
-
},
|
|
1817
|
-
presentation,
|
|
1818
|
-
};
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
const presentation = await buildToolPresentation({
|
|
1822
|
-
artifactManifest,
|
|
1823
|
-
artifactRequest,
|
|
1824
|
-
commandInfo: parseCommandInfo(command ?? []),
|
|
1825
|
-
cwd,
|
|
1826
|
-
args: command,
|
|
1827
|
-
envelope: { data: item.result, success: true },
|
|
1828
|
-
persistentArtifactStore,
|
|
1829
|
-
sessionName,
|
|
1830
|
-
});
|
|
1831
|
-
const fullOutputPaths = getPresentationPaths({
|
|
1832
|
-
primaryPath: presentation.fullOutputPath,
|
|
1833
|
-
secondaryPaths: presentation.fullOutputPaths,
|
|
1834
|
-
});
|
|
1835
|
-
const imagePaths = getPresentationPaths({
|
|
1836
|
-
primaryPath: presentation.imagePath,
|
|
1837
|
-
secondaryPaths: presentation.imagePaths,
|
|
1838
|
-
});
|
|
1839
|
-
const text = getPresentationText(presentation) || presentation.summary;
|
|
1840
|
-
const nextActions = presentation.nextActions ?? buildAgentBrowserNextActions({
|
|
1841
|
-
artifacts: presentation.artifacts,
|
|
1842
|
-
args: command,
|
|
1843
|
-
command: command?.[0],
|
|
1844
|
-
resultCategory: "success",
|
|
1845
|
-
savedFilePath: presentation.savedFilePath,
|
|
1846
|
-
successCategory: presentation.successCategory,
|
|
1847
|
-
});
|
|
1848
|
-
const pageChangeSummary = buildPageChangeSummary({
|
|
1849
|
-
artifacts: presentation.artifacts,
|
|
1850
|
-
commandInfo: parseCommandInfo(command ?? []),
|
|
1851
|
-
data: presentation.data,
|
|
1852
|
-
nextActions,
|
|
1853
|
-
savedFilePath: presentation.savedFilePath,
|
|
1854
|
-
summary: presentation.summary,
|
|
1855
|
-
});
|
|
1856
|
-
|
|
1857
|
-
return {
|
|
1858
|
-
details: {
|
|
1859
|
-
artifactVerification: presentation.artifactVerification,
|
|
1860
|
-
artifacts: presentation.artifacts,
|
|
1861
|
-
command: redactedCommand,
|
|
1862
|
-
commandText,
|
|
1863
|
-
data: presentation.data,
|
|
1864
|
-
fullOutputPath: fullOutputPaths[0],
|
|
1865
|
-
fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
|
|
1866
|
-
imagePath: imagePaths[0],
|
|
1867
|
-
imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
|
|
1868
|
-
index,
|
|
1869
|
-
nextActions,
|
|
1870
|
-
pageChangeSummary,
|
|
1871
|
-
resultCategory: "success",
|
|
1872
|
-
savedFile: presentation.savedFile,
|
|
1873
|
-
savedFilePath: presentation.savedFilePath,
|
|
1874
|
-
success: true,
|
|
1875
|
-
successCategory: classifyPresentationSuccessCategory({ artifactVerification: presentation.artifactVerification, artifacts: presentation.artifacts, savedFile: presentation.savedFile }),
|
|
1876
|
-
summary: presentation.summary,
|
|
1877
|
-
text,
|
|
1878
|
-
},
|
|
1879
|
-
presentation,
|
|
1880
|
-
};
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
async function buildBatchPresentation(options: {
|
|
1884
|
-
artifactManifest?: SessionArtifactManifest;
|
|
1885
|
-
artifactRequests?: Array<ArtifactRequestContext | undefined>;
|
|
1886
|
-
cwd: string;
|
|
1887
|
-
data: AgentBrowserBatchResult[];
|
|
1888
|
-
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
1889
|
-
sessionName?: string;
|
|
1890
|
-
summary: string;
|
|
1891
|
-
}): Promise<ToolPresentation> {
|
|
1892
|
-
const { artifactRequests, cwd, data, persistentArtifactStore, sessionName, summary } = options;
|
|
1893
|
-
const steps: Array<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> = [];
|
|
1894
|
-
const protectedPersistentPaths: string[] = [];
|
|
1895
|
-
let currentArtifactManifest = options.artifactManifest;
|
|
1896
|
-
for (const [index, item] of data.entries()) {
|
|
1897
|
-
const step = await buildBatchStepPresentation({
|
|
1898
|
-
artifactManifest: currentArtifactManifest,
|
|
1899
|
-
artifactRequest: artifactRequests?.[index],
|
|
1900
|
-
cwd,
|
|
1901
|
-
index,
|
|
1902
|
-
item,
|
|
1903
|
-
persistentArtifactStore: persistentArtifactStore
|
|
1904
|
-
? { ...persistentArtifactStore, protectedPaths: protectedPersistentPaths }
|
|
1905
|
-
: undefined,
|
|
1906
|
-
sessionName,
|
|
1907
|
-
});
|
|
1908
|
-
steps.push(step);
|
|
1909
|
-
currentArtifactManifest = step.presentation.artifactManifest ?? currentArtifactManifest;
|
|
1910
|
-
protectedPersistentPaths.push(
|
|
1911
|
-
...getPresentationPaths({
|
|
1912
|
-
primaryPath: step.presentation.fullOutputPath,
|
|
1913
|
-
secondaryPaths: step.presentation.fullOutputPaths,
|
|
1914
|
-
}),
|
|
1915
|
-
);
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
const batchFailure = getBatchFailureDetails(steps);
|
|
1919
|
-
const images = steps.flatMap((step) => getPresentationImages(step.presentation));
|
|
1920
|
-
const artifacts = steps.flatMap((step) => step.presentation.artifacts ?? []);
|
|
1921
|
-
const artifactVerification = buildArtifactVerificationSummary(artifacts);
|
|
1922
|
-
const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
|
|
1923
|
-
primaryPath: step.presentation.fullOutputPath,
|
|
1924
|
-
secondaryPaths: step.presentation.fullOutputPaths,
|
|
1925
|
-
}));
|
|
1926
|
-
const imagePaths = steps.flatMap((step) => getPresentationPaths({
|
|
1927
|
-
primaryPath: step.presentation.imagePath,
|
|
1928
|
-
secondaryPaths: step.presentation.imagePaths,
|
|
1929
|
-
}));
|
|
1930
|
-
const redactedBatchData = steps.map(({ details }) => (
|
|
1931
|
-
details.success
|
|
1932
|
-
? { command: details.command, result: details.data, success: true }
|
|
1933
|
-
: { command: details.command, error: details.text, success: false }
|
|
1934
|
-
));
|
|
1935
|
-
const stepText =
|
|
1936
|
-
steps.length === 0
|
|
1937
|
-
? "(no batch steps)"
|
|
1938
|
-
: steps
|
|
1939
|
-
.map(({ details, presentation }) => {
|
|
1940
|
-
const inlineImageCount = getPresentationImages(presentation).length;
|
|
1941
|
-
const status = details.success ? "succeeded" : "failed";
|
|
1942
|
-
const lines = [`Step ${details.index + 1} — ${details.commandText} (${status})`];
|
|
1943
|
-
if (details.text.length > 0) {
|
|
1944
|
-
lines.push(details.text);
|
|
1945
|
-
}
|
|
1946
|
-
if (inlineImageCount > 0) {
|
|
1947
|
-
lines.push(`(${inlineImageCount} inline image attachment${inlineImageCount === 1 ? "" : "s"} below)`);
|
|
1948
|
-
}
|
|
1949
|
-
return lines.join("\n");
|
|
1950
|
-
})
|
|
1951
|
-
.join("\n\n");
|
|
1952
|
-
const failureHeader =
|
|
1953
|
-
batchFailure === undefined
|
|
1954
|
-
? undefined
|
|
1955
|
-
: [
|
|
1956
|
-
summary,
|
|
1957
|
-
`First failing step: ${batchFailure.failedStep.index + 1} — ${batchFailure.failedStep.commandText}`,
|
|
1958
|
-
batchFailure.failureCount > 1
|
|
1959
|
-
? `${batchFailure.failureCount} steps failed. See the per-step results below.`
|
|
1960
|
-
: "See the per-step results below.",
|
|
1961
|
-
].join("\n");
|
|
1962
|
-
const text = failureHeader ? `${failureHeader}\n\n${stepText}` : stepText;
|
|
1963
|
-
|
|
1964
|
-
const artifactRetentionSummary = currentArtifactManifest ? formatSessionArtifactRetentionSummary(currentArtifactManifest) : undefined;
|
|
1965
|
-
const contentText = artifactRetentionSummary && manifestHasNewNoticeWorthyEntries(options.artifactManifest, currentArtifactManifest) ? `${text}\n\n${artifactRetentionSummary}` : text;
|
|
1966
|
-
|
|
1967
|
-
const nextActions = batchFailure
|
|
1968
|
-
? batchFailure.failedStep.nextActions
|
|
1969
|
-
: buildAgentBrowserNextActions({ artifacts, command: "batch", resultCategory: "success" });
|
|
1970
|
-
const changedSteps = steps.map((step) => step.details).filter((details) => details.pageChangeSummary !== undefined);
|
|
1971
|
-
const pageChangeSummary = artifacts.length > 0
|
|
1972
|
-
? buildPageChangeSummary({
|
|
1973
|
-
artifacts,
|
|
1974
|
-
commandInfo: { command: "batch" },
|
|
1975
|
-
data,
|
|
1976
|
-
nextActions,
|
|
1977
|
-
summary,
|
|
1978
|
-
})
|
|
1979
|
-
: changedSteps.length > 0
|
|
1980
|
-
? {
|
|
1981
|
-
changeType: "mutation" as const,
|
|
1982
|
-
command: "batch",
|
|
1983
|
-
nextActionIds: nextActions?.map((action) => action.id),
|
|
1984
|
-
summary: `batch → mutation → ${changedSteps.length} changed step${changedSteps.length === 1 ? "" : "s"}`,
|
|
1985
|
-
}
|
|
1986
|
-
: undefined;
|
|
1987
|
-
|
|
1988
|
-
return {
|
|
1989
|
-
artifactManifest: currentArtifactManifest,
|
|
1990
|
-
artifactRetentionSummary,
|
|
1991
|
-
artifactVerification,
|
|
1992
|
-
artifacts: artifacts.length > 0 ? artifacts : undefined,
|
|
1993
|
-
batchFailure,
|
|
1994
|
-
batchSteps: steps.map((step) => step.details),
|
|
1995
|
-
content: [{ type: "text", text: contentText }, ...images],
|
|
1996
|
-
failureCategory: batchFailure?.failedStep.failureCategory,
|
|
1997
|
-
data: redactedBatchData,
|
|
1998
|
-
fullOutputPath: fullOutputPaths[0],
|
|
1999
|
-
fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
|
|
2000
|
-
imagePath: imagePaths[0],
|
|
2001
|
-
imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
|
|
2002
|
-
nextActions,
|
|
2003
|
-
pageChangeSummary,
|
|
2004
|
-
resultCategory: batchFailure ? "failure" : "success",
|
|
2005
|
-
successCategory: batchFailure ? undefined : classifyPresentationSuccessCategory({ artifactVerification, artifacts }),
|
|
2006
|
-
summary,
|
|
2007
|
-
};
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
function formatSummary(commandInfo: CommandInfo, data: unknown): string {
|
|
2011
|
-
const confirmationRequired = detectConfirmationRequired(data);
|
|
2012
|
-
if (confirmationRequired) {
|
|
2013
|
-
return formatConfirmationRequiredSummary(confirmationRequired);
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
if (Array.isArray(data) && commandInfo.command === "batch") {
|
|
2017
|
-
const successCount = data.filter((item) => isRecord(item) && item.success !== false).length;
|
|
2018
|
-
return successCount === data.length ? `Batch: ${successCount}/${data.length} succeeded` : `Batch failed: ${successCount}/${data.length} succeeded`;
|
|
2019
|
-
}
|
|
2020
|
-
if (Array.isArray(data) && commandInfo.command === "profiles") {
|
|
2021
|
-
return `Chrome profiles: ${data.length}`;
|
|
2022
|
-
}
|
|
2023
|
-
if (Array.isArray(data) && commandInfo.command === "skills" && commandInfo.subcommand === "list") {
|
|
2024
|
-
return `agent-browser skills: ${data.length}`;
|
|
2025
|
-
}
|
|
2026
|
-
if (commandInfo.command === "skills" && commandInfo.subcommand === "get") {
|
|
2027
|
-
return "agent-browser skill loaded";
|
|
2028
|
-
}
|
|
2029
|
-
if (commandInfo.command === "skills" && commandInfo.subcommand === "path") {
|
|
2030
|
-
return "agent-browser skill path";
|
|
2031
|
-
}
|
|
2032
|
-
if (isRecord(data)) {
|
|
2033
|
-
const navigationSummary = getNavigationSummary(data);
|
|
2034
|
-
if (navigationSummary && isNavigationObservableCommand(commandInfo.command)) {
|
|
2035
|
-
const navigationText = formatNavigationSummary(navigationSummary);
|
|
2036
|
-
if (navigationText) {
|
|
2037
|
-
return `${commandInfo.command ?? "navigation"} → ${navigationText.split("\n", 1)[0] ?? navigationText}`;
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
if (commandInfo.command === "snapshot") {
|
|
2041
|
-
return formatSnapshotSummary(data);
|
|
2042
|
-
}
|
|
2043
|
-
if (commandInfo.command === "tab" && Array.isArray(data.tabs)) {
|
|
2044
|
-
return `Tabs: ${data.tabs.length}`;
|
|
2045
|
-
}
|
|
2046
|
-
if (commandInfo.command === "stream" && commandInfo.subcommand === "status") {
|
|
2047
|
-
const port = typeof data.port === "number" ? ` on port ${data.port}` : "";
|
|
2048
|
-
return `Stream ${data.enabled === true ? "enabled" : "disabled"}${port}`;
|
|
2049
|
-
}
|
|
2050
|
-
if (commandInfo.command === "screenshot" && typeof data.path === "string") {
|
|
2051
|
-
return `Screenshot saved: ${data.path}`;
|
|
2052
|
-
}
|
|
2053
|
-
const diagnosticSummary = formatDiagnosticSummary(commandInfo, data);
|
|
2054
|
-
if (diagnosticSummary) {
|
|
2055
|
-
return diagnosticSummary;
|
|
2056
|
-
}
|
|
2057
|
-
const extractionSummary = formatExtractionSummary(commandInfo, data);
|
|
2058
|
-
if (extractionSummary) {
|
|
2059
|
-
return extractionSummary;
|
|
2060
|
-
}
|
|
2061
|
-
const pageSummary = getPageSummary(data);
|
|
2062
|
-
if (pageSummary) {
|
|
2063
|
-
return pageSummary.split("\n", 1)[0] ?? "agent-browser result";
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
if (typeof data === "string" && data.length > 0) {
|
|
2068
|
-
return data.split("\n", 1)[0] ?? data;
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
const primaryCommand = commandInfo.command ?? "agent-browser";
|
|
2072
|
-
return `${primaryCommand} completed`;
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
function formatContentText(commandInfo: CommandInfo, data: unknown): string {
|
|
2076
|
-
const confirmationRequired = detectConfirmationRequired(data);
|
|
2077
|
-
if (confirmationRequired) {
|
|
2078
|
-
return formatConfirmationRequiredText(confirmationRequired);
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
const skillsText = formatSkillsText(commandInfo, data);
|
|
2082
|
-
if (skillsText) {
|
|
2083
|
-
return skillsText;
|
|
2084
|
-
}
|
|
2085
|
-
if (typeof data === "string") {
|
|
2086
|
-
return redactModelFacingText(data);
|
|
2087
|
-
}
|
|
2088
|
-
if (typeof data === "number" || typeof data === "boolean") {
|
|
2089
|
-
return String(data);
|
|
2090
|
-
}
|
|
2091
|
-
if (Array.isArray(data) && commandInfo.command === "profiles") {
|
|
2092
|
-
return formatProfilesText(data, "Chrome profiles");
|
|
2093
|
-
}
|
|
2094
|
-
if (!isRecord(data)) {
|
|
2095
|
-
return stringifyModelFacing(data);
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
const navigationSummary = getNavigationSummary(data);
|
|
2099
|
-
if (navigationSummary && isNavigationObservableCommand(commandInfo.command)) {
|
|
2100
|
-
const navigationText = formatNavigationSummary(navigationSummary);
|
|
2101
|
-
if (navigationText) {
|
|
2102
|
-
const actionText = formatNavigationActionResult(data);
|
|
2103
|
-
return actionText ? `${actionText}\n\nCurrent page:\n${navigationText}` : `Current page:\n${navigationText}`;
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
if (commandInfo.command === "snapshot") {
|
|
2108
|
-
return formatRawSnapshotText(data);
|
|
2109
|
-
}
|
|
2110
|
-
if (commandInfo.command === "tab") {
|
|
2111
|
-
const tabSummary = getTabSummary(data);
|
|
2112
|
-
if (tabSummary) return tabSummary;
|
|
2113
|
-
}
|
|
2114
|
-
if (commandInfo.command === "stream" && commandInfo.subcommand === "status") {
|
|
2115
|
-
const streamSummary = getStreamSummary(data);
|
|
2116
|
-
if (streamSummary) return streamSummary;
|
|
2117
|
-
}
|
|
2118
|
-
if (commandInfo.command === "screenshot") {
|
|
2119
|
-
const screenshotSummary = getScreenshotSummary(data);
|
|
2120
|
-
if (screenshotSummary) return screenshotSummary;
|
|
2121
|
-
}
|
|
2122
|
-
const extractionText = formatExtractionText(commandInfo, data);
|
|
2123
|
-
if (extractionText) {
|
|
2124
|
-
return extractionText;
|
|
2125
|
-
}
|
|
2126
|
-
|
|
2127
|
-
const diagnosticText = formatDiagnosticText(commandInfo, data);
|
|
2128
|
-
if (diagnosticText) {
|
|
2129
|
-
return diagnosticText;
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
const pageSummary = getPageSummary(data);
|
|
2133
|
-
if (pageSummary) {
|
|
2134
|
-
return redactModelFacingText(pageSummary);
|
|
2135
|
-
}
|
|
2136
|
-
|
|
2137
|
-
return stringifyModelFacing(data);
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
function isTrustedScreenshotOutput(commandInfo: CommandInfo): boolean {
|
|
2141
|
-
return commandInfo.command === "screenshot";
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
function extractImagePath(commandInfo: CommandInfo, cwd: string, data: unknown): string | undefined {
|
|
2145
|
-
if (!isTrustedScreenshotOutput(commandInfo)) {
|
|
2146
|
-
return undefined;
|
|
2147
|
-
}
|
|
2148
|
-
if (typeof data === "string") {
|
|
2149
|
-
const mimeType = getImageMimeType(data);
|
|
2150
|
-
return mimeType ? resolve(cwd, data) : undefined;
|
|
2151
|
-
}
|
|
2152
|
-
if (!isRecord(data) || typeof data.path !== "string") {
|
|
2153
|
-
return undefined;
|
|
2154
|
-
}
|
|
2155
|
-
const mimeType = getImageMimeType(data.path);
|
|
2156
|
-
return mimeType ? resolve(cwd, data.path) : undefined;
|
|
2157
|
-
}
|
|
7
|
+
import { isRecord } from "../parsing.js";
|
|
8
|
+
import type { CommandInfo } from "../runtime.js";
|
|
9
|
+
import type { PersistentSessionArtifactStore } from "../temp.js";
|
|
10
|
+
import { buildAgentBrowserNextActions } from "./action-recommendations.js";
|
|
11
|
+
import { buildAgentBrowserResultCategoryDetails } from "./categories.js";
|
|
12
|
+
import { detectConfirmationRequired } from "./confirmation.js";
|
|
13
|
+
import type {
|
|
14
|
+
AgentBrowserEnvelope,
|
|
15
|
+
AgentBrowserNextAction,
|
|
16
|
+
SessionArtifactManifest,
|
|
17
|
+
ToolPresentation,
|
|
18
|
+
} from "./contracts.js";
|
|
19
|
+
import { buildSnapshotPresentation } from "./snapshot.js";
|
|
20
|
+
import { parseJsonPreviewString, redactModelFacingText, stringifyModelFacing } from "./presentation/common.js";
|
|
21
|
+
import {
|
|
22
|
+
applyArtifactManifest,
|
|
23
|
+
attachInlineImage,
|
|
24
|
+
buildArtifactVerificationSummary,
|
|
25
|
+
buildManifestEntriesForFileArtifacts,
|
|
26
|
+
classifyPresentationSuccessCategory,
|
|
27
|
+
extractFileArtifacts,
|
|
28
|
+
extractImagePath,
|
|
29
|
+
formatArtifactMetadataLines,
|
|
30
|
+
formatArtifactSummary,
|
|
31
|
+
getSavedFileDetails,
|
|
32
|
+
isManifestFileArtifact,
|
|
33
|
+
type ArtifactRequestContext,
|
|
34
|
+
} from "./presentation/artifacts.js";
|
|
35
|
+
import { buildBatchPresentation, isAgentBrowserBatchResultArray } from "./presentation/batch.js";
|
|
36
|
+
import { getPresentationPaths } from "./presentation/content.js";
|
|
37
|
+
import {
|
|
38
|
+
buildNetworkRequestsNextActions,
|
|
39
|
+
enrichStreamStatusData,
|
|
40
|
+
redactPresentationData,
|
|
41
|
+
} from "./presentation/diagnostics.js";
|
|
42
|
+
import { buildErrorPresentation } from "./presentation/errors.js";
|
|
43
|
+
import { compactLargePresentationOutput } from "./presentation/large-output.js";
|
|
44
|
+
import { buildPageChangeSummary } from "./presentation/navigation.js";
|
|
45
|
+
import { formatPresentationContentText, formatPresentationSummary } from "./presentation/registry.js";
|
|
2158
46
|
|
|
2159
47
|
function sanitizeModelFacingPresentation(presentation: ToolPresentation): ToolPresentation {
|
|
2160
48
|
presentation.content = presentation.content.map((item) => {
|
|
@@ -2166,194 +54,11 @@ function sanitizeModelFacingPresentation(presentation: ToolPresentation): ToolPr
|
|
|
2166
54
|
return presentation;
|
|
2167
55
|
}
|
|
2168
56
|
|
|
2169
|
-
async function attachInlineImage(presentation: ToolPresentation, imagePath: string): Promise<ToolPresentation> {
|
|
2170
|
-
const mimeType = getImageMimeType(imagePath);
|
|
2171
|
-
if (!mimeType) {
|
|
2172
|
-
return presentation;
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
try {
|
|
2176
|
-
const fileStats = await stat(imagePath);
|
|
2177
|
-
const inlineImageMaxBytes = getInlineImageMaxBytes();
|
|
2178
|
-
if (fileStats.size > inlineImageMaxBytes) {
|
|
2179
|
-
appendPresentationNotice(
|
|
2180
|
-
presentation,
|
|
2181
|
-
`Image attachment skipped: ${formatByteCount(fileStats.size)} exceeds the inline limit of ${formatByteCount(inlineImageMaxBytes)}.`,
|
|
2182
|
-
);
|
|
2183
|
-
presentation.imagePath = imagePath;
|
|
2184
|
-
return presentation;
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
const file = await readFile(imagePath);
|
|
2188
|
-
presentation.content.push({ type: "image", data: file.toString("base64"), mimeType });
|
|
2189
|
-
presentation.imagePath = imagePath;
|
|
2190
|
-
return presentation;
|
|
2191
|
-
} catch (error) {
|
|
2192
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2193
|
-
appendPresentationNotice(presentation, `Image attachment failed: ${message}`);
|
|
2194
|
-
presentation.imagePath = imagePath;
|
|
2195
|
-
return presentation;
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
function shouldCompactLargeOutput(text: string): boolean {
|
|
2200
|
-
return text.length > LARGE_OUTPUT_INLINE_MAX_CHARS || countLines(text) > LARGE_OUTPUT_INLINE_MAX_LINES;
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
function buildLargeOutputPreview(text: string): { omittedLineCount: number; previewText: string } {
|
|
2204
|
-
const lines = text.split("\n");
|
|
2205
|
-
const previewLines: string[] = [];
|
|
2206
|
-
let previewChars = 0;
|
|
2207
|
-
for (const line of lines) {
|
|
2208
|
-
if (previewLines.length >= LARGE_OUTPUT_PREVIEW_MAX_LINES || previewChars >= LARGE_OUTPUT_PREVIEW_MAX_CHARS) {
|
|
2209
|
-
break;
|
|
2210
|
-
}
|
|
2211
|
-
const remainingChars = LARGE_OUTPUT_PREVIEW_MAX_CHARS - previewChars;
|
|
2212
|
-
const previewLine = truncateText(line, Math.max(40, remainingChars));
|
|
2213
|
-
previewLines.push(previewLine);
|
|
2214
|
-
previewChars += previewLine.length + 1;
|
|
2215
|
-
}
|
|
2216
|
-
return {
|
|
2217
|
-
omittedLineCount: Math.max(0, lines.length - previewLines.length),
|
|
2218
|
-
previewText: previewLines.join("\n"),
|
|
2219
|
-
};
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
interface LargeOutputSpillWriteResult {
|
|
2223
|
-
evictedArtifacts: PersistentSessionArtifactEviction[];
|
|
2224
|
-
path: string;
|
|
2225
|
-
storageScope: ArtifactStorageScope;
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
async function writeLargeOutputSpillFile(options: {
|
|
2229
|
-
data: unknown;
|
|
2230
|
-
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
2231
|
-
text: string;
|
|
2232
|
-
}): Promise<LargeOutputSpillWriteResult> {
|
|
2233
|
-
const payload =
|
|
2234
|
-
typeof options.data === "string"
|
|
2235
|
-
? redactModelFacingText(options.data)
|
|
2236
|
-
: typeof options.data === "number" || typeof options.data === "boolean"
|
|
2237
|
-
? String(options.data)
|
|
2238
|
-
: options.data === undefined
|
|
2239
|
-
? redactModelFacingText(options.text)
|
|
2240
|
-
: stringifyModelFacing(options.data);
|
|
2241
|
-
const isStructuredPayload = typeof options.data !== "string" && typeof options.data !== "number" && typeof options.data !== "boolean";
|
|
2242
|
-
const fileOptions = {
|
|
2243
|
-
content: payload,
|
|
2244
|
-
prefix: LARGE_OUTPUT_FILE_PREFIX,
|
|
2245
|
-
suffix: isStructuredPayload ? ".json" : ".txt",
|
|
2246
|
-
};
|
|
2247
|
-
if (options.persistentArtifactStore) {
|
|
2248
|
-
const result = await writePersistentSessionArtifactFile({ ...fileOptions, store: options.persistentArtifactStore });
|
|
2249
|
-
return { ...result, storageScope: "persistent-session" };
|
|
2250
|
-
}
|
|
2251
|
-
return { evictedArtifacts: [], path: await writeSecureTempFile(fileOptions), storageScope: "process-temp" };
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
function buildSpillArtifactEntries(options: {
|
|
2255
|
-
commandInfo: CommandInfo;
|
|
2256
|
-
evictedArtifacts: PersistentSessionArtifactEviction[];
|
|
2257
|
-
path: string;
|
|
2258
|
-
storageScope: ArtifactStorageScope;
|
|
2259
|
-
}): SessionArtifactManifestEntry[] {
|
|
2260
|
-
const nowMs = Date.now();
|
|
2261
|
-
return [
|
|
2262
|
-
{
|
|
2263
|
-
command: options.commandInfo.command,
|
|
2264
|
-
createdAtMs: nowMs,
|
|
2265
|
-
kind: "spill",
|
|
2266
|
-
path: options.path,
|
|
2267
|
-
retentionState: options.storageScope === "persistent-session" ? "live" : "ephemeral",
|
|
2268
|
-
storageScope: options.storageScope,
|
|
2269
|
-
subcommand: options.commandInfo.subcommand,
|
|
2270
|
-
},
|
|
2271
|
-
...buildEvictedSessionArtifactEntries(options.evictedArtifacts, nowMs),
|
|
2272
|
-
];
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
57
|
function mergeNextActions(...groups: Array<AgentBrowserNextAction[] | undefined>): AgentBrowserNextAction[] | undefined {
|
|
2276
58
|
const merged = groups.flatMap((group) => group ?? []);
|
|
2277
59
|
return merged.length > 0 ? merged : undefined;
|
|
2278
60
|
}
|
|
2279
61
|
|
|
2280
|
-
async function compactLargePresentationOutput(options: {
|
|
2281
|
-
artifactManifest?: SessionArtifactManifest;
|
|
2282
|
-
commandInfo: CommandInfo;
|
|
2283
|
-
data: unknown;
|
|
2284
|
-
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
2285
|
-
presentation: ToolPresentation;
|
|
2286
|
-
}): Promise<ToolPresentation> {
|
|
2287
|
-
const text = getPresentationText(options.presentation);
|
|
2288
|
-
if (text.length === 0 || !shouldCompactLargeOutput(text)) {
|
|
2289
|
-
return options.presentation;
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2292
|
-
let fullOutputPath: string | undefined;
|
|
2293
|
-
let spill: LargeOutputSpillWriteResult | undefined;
|
|
2294
|
-
let spillErrorText: string | undefined;
|
|
2295
|
-
try {
|
|
2296
|
-
spill = await writeLargeOutputSpillFile({
|
|
2297
|
-
data: options.data,
|
|
2298
|
-
persistentArtifactStore: options.persistentArtifactStore,
|
|
2299
|
-
text,
|
|
2300
|
-
});
|
|
2301
|
-
fullOutputPath = spill.path;
|
|
2302
|
-
} catch (error) {
|
|
2303
|
-
spillErrorText = error instanceof Error ? error.message : String(error);
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
const { omittedLineCount, previewText } = buildLargeOutputPreview(text);
|
|
2307
|
-
const commandLabel = options.commandInfo.command ?? "agent-browser";
|
|
2308
|
-
const lines = [
|
|
2309
|
-
`Large ${commandLabel} output compacted.`,
|
|
2310
|
-
"",
|
|
2311
|
-
"Preview:",
|
|
2312
|
-
previewText,
|
|
2313
|
-
];
|
|
2314
|
-
if (omittedLineCount > 0) {
|
|
2315
|
-
lines.push(`- ... (${omittedLineCount} additional lines omitted)`);
|
|
2316
|
-
}
|
|
2317
|
-
lines.push(
|
|
2318
|
-
"",
|
|
2319
|
-
fullOutputPath
|
|
2320
|
-
? `Full output path: ${fullOutputPath}`
|
|
2321
|
-
: `Full output unavailable: ${spillErrorText ?? "spill file could not be created."}`,
|
|
2322
|
-
);
|
|
2323
|
-
|
|
2324
|
-
const firstTextIndex = options.presentation.content.findIndex((part) => part.type === "text");
|
|
2325
|
-
const compactedText = lines.join("\n");
|
|
2326
|
-
if (firstTextIndex >= 0) {
|
|
2327
|
-
options.presentation.content[firstTextIndex] = { type: "text", text: compactedText };
|
|
2328
|
-
} else {
|
|
2329
|
-
options.presentation.content.unshift({ type: "text", text: compactedText });
|
|
2330
|
-
}
|
|
2331
|
-
options.presentation.data = {
|
|
2332
|
-
compacted: true,
|
|
2333
|
-
fullOutputPath,
|
|
2334
|
-
outputCharCount: text.length,
|
|
2335
|
-
outputLineCount: countLines(text),
|
|
2336
|
-
previewCharCount: previewText.length,
|
|
2337
|
-
previewLineCount: countLines(previewText),
|
|
2338
|
-
spillError: spillErrorText,
|
|
2339
|
-
};
|
|
2340
|
-
options.presentation.fullOutputPath = fullOutputPath;
|
|
2341
|
-
options.presentation.summary = `${options.presentation.summary} (compact)`;
|
|
2342
|
-
if (fullOutputPath && spill) {
|
|
2343
|
-
return applyArtifactManifest(
|
|
2344
|
-
options.presentation,
|
|
2345
|
-
options.presentation.artifactManifest ?? options.artifactManifest,
|
|
2346
|
-
buildSpillArtifactEntries({
|
|
2347
|
-
commandInfo: options.commandInfo,
|
|
2348
|
-
evictedArtifacts: spill.evictedArtifacts,
|
|
2349
|
-
path: fullOutputPath,
|
|
2350
|
-
storageScope: spill.storageScope,
|
|
2351
|
-
}),
|
|
2352
|
-
);
|
|
2353
|
-
}
|
|
2354
|
-
return options.presentation;
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
62
|
export async function buildToolPresentation(options: {
|
|
2358
63
|
artifactManifest?: SessionArtifactManifest;
|
|
2359
64
|
args?: string[];
|
|
@@ -2366,26 +71,20 @@ export async function buildToolPresentation(options: {
|
|
|
2366
71
|
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
2367
72
|
sessionName?: string;
|
|
2368
73
|
}): Promise<ToolPresentation> {
|
|
2369
|
-
const {
|
|
74
|
+
const {
|
|
75
|
+
args,
|
|
76
|
+
artifactManifest,
|
|
77
|
+
artifactRequest,
|
|
78
|
+
commandInfo,
|
|
79
|
+
cwd,
|
|
80
|
+
envelope,
|
|
81
|
+
errorText,
|
|
82
|
+
persistentArtifactStore,
|
|
83
|
+
sessionName,
|
|
84
|
+
} = options;
|
|
85
|
+
|
|
2370
86
|
if (errorText) {
|
|
2371
|
-
|
|
2372
|
-
const selectorHintedErrorText = appendSelectorRecoveryHint(safeErrorText);
|
|
2373
|
-
const unknownCommandSuggestions = getUnknownCommandSuggestions(commandInfo.command, safeErrorText);
|
|
2374
|
-
const unknownCommandSuggestionText = formatUnknownCommandSuggestionText(unknownCommandSuggestions);
|
|
2375
|
-
const hintedErrorText = unknownCommandSuggestionText && !selectorHintedErrorText.includes("Agent-browser hint:")
|
|
2376
|
-
? `${selectorHintedErrorText}\n\n${unknownCommandSuggestionText}`
|
|
2377
|
-
: selectorHintedErrorText;
|
|
2378
|
-
const categoryDetails = buildAgentBrowserResultCategoryDetails({ args: [commandInfo.command, commandInfo.subcommand].filter((item): item is string => item !== undefined), command: commandInfo.command, errorText: hintedErrorText, succeeded: false });
|
|
2379
|
-
const nextActions = [
|
|
2380
|
-
...(buildUnknownCommandSuggestionActions(unknownCommandSuggestions, sessionName) ?? []),
|
|
2381
|
-
...(buildAgentBrowserNextActions({ args, command: commandInfo.command, failureCategory: categoryDetails.failureCategory, resultCategory: "failure" }) ?? []),
|
|
2382
|
-
];
|
|
2383
|
-
return {
|
|
2384
|
-
...categoryDetails,
|
|
2385
|
-
content: [{ type: "text", text: hintedErrorText }],
|
|
2386
|
-
nextActions: nextActions.length > 0 ? nextActions : undefined,
|
|
2387
|
-
summary: hintedErrorText,
|
|
2388
|
-
};
|
|
87
|
+
return buildErrorPresentation({ args, commandInfo, errorText, sessionName });
|
|
2389
88
|
}
|
|
2390
89
|
|
|
2391
90
|
const data = enrichStreamStatusData(commandInfo, envelope?.data);
|
|
@@ -2393,20 +92,33 @@ export async function buildToolPresentation(options: {
|
|
|
2393
92
|
const artifacts = await extractFileArtifacts({ artifactRequest, commandInfo, cwd, data, sessionName });
|
|
2394
93
|
const artifactVerification = buildArtifactVerificationSummary(artifacts);
|
|
2395
94
|
const artifactSummary = formatArtifactSummary(artifacts);
|
|
2396
|
-
const summary = artifactSummary ??
|
|
95
|
+
const summary = artifactSummary ?? formatPresentationSummary(commandInfo, data);
|
|
2397
96
|
const artifactText = artifacts.length > 0 ? formatArtifactMetadataLines(artifacts).join("\n") : undefined;
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
97
|
+
|
|
98
|
+
let presentation: ToolPresentation;
|
|
99
|
+
if (commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)) {
|
|
100
|
+
presentation = await buildBatchPresentation({
|
|
101
|
+
artifactManifest,
|
|
102
|
+
artifactRequests: options.batchArtifactRequests,
|
|
103
|
+
buildNestedToolPresentation: buildToolPresentation,
|
|
104
|
+
cwd,
|
|
105
|
+
data,
|
|
106
|
+
persistentArtifactStore,
|
|
107
|
+
sessionName,
|
|
108
|
+
summary,
|
|
109
|
+
});
|
|
110
|
+
} else if (commandInfo.command === "snapshot" && isRecord(data)) {
|
|
111
|
+
presentation = await buildSnapshotPresentation(data, persistentArtifactStore, artifactManifest);
|
|
112
|
+
} else {
|
|
113
|
+
presentation = {
|
|
114
|
+
artifactVerification,
|
|
115
|
+
artifacts: artifacts.length > 0 ? artifacts : undefined,
|
|
116
|
+
content: [{ type: "text", text: artifactText ?? formatPresentationContentText(commandInfo, data) }],
|
|
117
|
+
data: presentationData,
|
|
118
|
+
summary,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
2410
122
|
if (artifacts.length > 0 && !presentation.artifacts) {
|
|
2411
123
|
presentation.artifacts = artifacts;
|
|
2412
124
|
}
|
|
@@ -2442,6 +154,7 @@ export async function buildToolPresentation(options: {
|
|
|
2442
154
|
presentationWithManifest.artifactManifest,
|
|
2443
155
|
currentSpillPaths,
|
|
2444
156
|
) ?? presentationWithManifest.artifactVerification;
|
|
157
|
+
|
|
2445
158
|
const confirmationRequired = detectConfirmationRequired(data);
|
|
2446
159
|
if (!presentationWithManifest.resultCategory) {
|
|
2447
160
|
const categoryDetails = buildAgentBrowserResultCategoryDetails({
|
|
@@ -2469,6 +182,7 @@ export async function buildToolPresentation(options: {
|
|
|
2469
182
|
savedFile: presentationWithManifest.savedFile,
|
|
2470
183
|
});
|
|
2471
184
|
}
|
|
185
|
+
|
|
2472
186
|
const genericNextActions = presentationWithManifest.nextActions ? undefined : buildAgentBrowserNextActions({
|
|
2473
187
|
artifacts: presentationWithManifest.artifacts,
|
|
2474
188
|
args,
|
|
@@ -2482,7 +196,11 @@ export async function buildToolPresentation(options: {
|
|
|
2482
196
|
const networkNextActions = commandInfo.command === "network" && commandInfo.subcommand === "requests" && presentationWithManifest.resultCategory === "success"
|
|
2483
197
|
? buildNetworkRequestsNextActions(data, sessionName)
|
|
2484
198
|
: undefined;
|
|
2485
|
-
presentationWithManifest.nextActions = mergeNextActions(
|
|
199
|
+
presentationWithManifest.nextActions = mergeNextActions(
|
|
200
|
+
presentationWithManifest.nextActions,
|
|
201
|
+
genericNextActions,
|
|
202
|
+
networkNextActions,
|
|
203
|
+
);
|
|
2486
204
|
presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
|
|
2487
205
|
artifacts: presentationWithManifest.artifacts,
|
|
2488
206
|
commandInfo,
|