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