pi-agent-browser-native 0.2.32 → 0.2.34
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 +36 -0
- package/README.md +61 -20
- package/docs/ARCHITECTURE.md +9 -2
- package/docs/COMMAND_REFERENCE.md +45 -14
- package/docs/ELECTRON.md +23 -4
- package/docs/RELEASE.md +15 -5
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +36 -22
- package/docs/TOOL_CONTRACT.md +90 -31
- package/extensions/agent-browser/index.ts +407 -4373
- package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +265 -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 +44 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +762 -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 +736 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +413 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +482 -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 +22 -20
- 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 +182 -0
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +133 -0
- package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
- package/extensions/agent-browser/lib/results/presentation.ts +96 -2403
- 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 -789
- 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/package.json +2 -1
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Own file artifact detection, verification, manifest merging, and inline image attachment for tool presentation.
|
|
3
|
+
* Responsibilities: Build artifact metadata, verification summaries, saved-file details, artifact retention notices, and safe image content.
|
|
4
|
+
* Scope: Artifact and image presentation only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile, stat } from "node:fs/promises";
|
|
8
|
+
import { extname, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
import { isRecord, parsePositiveInteger } from "../../parsing.js";
|
|
11
|
+
import type { CommandInfo } from "../../runtime.js";
|
|
12
|
+
import {
|
|
13
|
+
formatSessionArtifactRetentionSummary,
|
|
14
|
+
mergeSessionArtifactManifest,
|
|
15
|
+
} from "../artifact-manifest.js";
|
|
16
|
+
import { isPendingRecordingArtifact } from "../artifact-state.js";
|
|
17
|
+
import { classifyAgentBrowserSuccessCategory } from "../categories.js";
|
|
18
|
+
import type {
|
|
19
|
+
ArtifactVerificationEntry,
|
|
20
|
+
ArtifactVerificationSummary,
|
|
21
|
+
FileArtifactKind,
|
|
22
|
+
FileArtifactMetadata,
|
|
23
|
+
SavedFilePresentationDetails,
|
|
24
|
+
SessionArtifactManifest,
|
|
25
|
+
SessionArtifactManifestEntry,
|
|
26
|
+
ToolPresentation,
|
|
27
|
+
} from "../contracts.js";
|
|
28
|
+
|
|
29
|
+
const IMAGE_EXTENSION_TO_MIME_TYPE: Record<string, string> = {
|
|
30
|
+
".gif": "image/gif",
|
|
31
|
+
".jpeg": "image/jpeg",
|
|
32
|
+
".jpg": "image/jpeg",
|
|
33
|
+
".png": "image/png",
|
|
34
|
+
".webp": "image/webp",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
|
|
38
|
+
|
|
39
|
+
const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
|
|
40
|
+
|
|
41
|
+
function getImageMimeType(filePath: string): string | undefined {
|
|
42
|
+
const extension = extname(filePath).toLowerCase();
|
|
43
|
+
return IMAGE_EXTENSION_TO_MIME_TYPE[extension];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getInlineImageMaxBytes(env: NodeJS.ProcessEnv = process.env): number {
|
|
47
|
+
return parsePositiveInteger(env[INLINE_IMAGE_MAX_BYTES_ENV]) ?? DEFAULT_INLINE_IMAGE_MAX_BYTES;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatByteCount(bytes: number): string {
|
|
51
|
+
if (bytes < 1_024) return `${bytes} B`;
|
|
52
|
+
if (bytes < 1_024 * 1_024) return `${(bytes / 1_024).toFixed(1)} KiB`;
|
|
53
|
+
return `${(bytes / (1_024 * 1_024)).toFixed(1)} MiB`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function appendPresentationNotice(presentation: ToolPresentation, message: string): void {
|
|
57
|
+
const existingText = presentation.content[0]?.type === "text" ? presentation.content[0].text : "";
|
|
58
|
+
presentation.content[0] = {
|
|
59
|
+
type: "text",
|
|
60
|
+
text: existingText.length > 0 ? `${existingText}\n\n${message}` : message,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shouldAppendArtifactRetentionNotice(entries: SessionArtifactManifestEntry[]): boolean {
|
|
65
|
+
return entries.some((entry) => entry.retentionState === "evicted" || entry.storageScope !== "explicit-path");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getManifestEntryKey(entry: SessionArtifactManifestEntry): string {
|
|
69
|
+
return entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function manifestHasNewNoticeWorthyEntries(base: SessionArtifactManifest | undefined, current: SessionArtifactManifest | undefined): boolean {
|
|
73
|
+
if (!current) return false;
|
|
74
|
+
const baseKeys = new Set((base?.entries ?? []).map(getManifestEntryKey));
|
|
75
|
+
return current.entries.some((entry) => !baseKeys.has(getManifestEntryKey(entry)) && (entry.retentionState === "evicted" || entry.storageScope !== "explicit-path"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function applyArtifactManifest(presentation: ToolPresentation, baseManifest: SessionArtifactManifest | undefined, entries: SessionArtifactManifestEntry[]): ToolPresentation {
|
|
79
|
+
if (entries.length === 0) return presentation;
|
|
80
|
+
const artifactManifest = mergeSessionArtifactManifest({ base: baseManifest, entries });
|
|
81
|
+
if (!artifactManifest) return presentation;
|
|
82
|
+
presentation.artifactManifest = artifactManifest;
|
|
83
|
+
presentation.artifactRetentionSummary = formatSessionArtifactRetentionSummary(artifactManifest);
|
|
84
|
+
if (shouldAppendArtifactRetentionNotice(entries)) {
|
|
85
|
+
appendPresentationNotice(presentation, presentation.artifactRetentionSummary);
|
|
86
|
+
}
|
|
87
|
+
return presentation;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getScreenshotSummary(data: Record<string, unknown>): string | undefined {
|
|
91
|
+
return typeof data.path === "string" ? `Saved image: ${data.path}` : undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const PATH_FIELD_CANDIDATES = [
|
|
95
|
+
"path",
|
|
96
|
+
"file",
|
|
97
|
+
"filePath",
|
|
98
|
+
"outputPath",
|
|
99
|
+
"downloadPath",
|
|
100
|
+
"diffPath",
|
|
101
|
+
"harPath",
|
|
102
|
+
"savedPath",
|
|
103
|
+
"statePath",
|
|
104
|
+
"tracePath",
|
|
105
|
+
"profilePath",
|
|
106
|
+
"videoPath",
|
|
107
|
+
] as const;
|
|
108
|
+
|
|
109
|
+
const ARTIFACT_EXTENSION_TO_MEDIA_TYPE: Record<string, string> = {
|
|
110
|
+
".cpuprofile": "application/json",
|
|
111
|
+
".har": "application/json",
|
|
112
|
+
".html": "text/html",
|
|
113
|
+
".json": "application/json",
|
|
114
|
+
".pdf": "application/pdf",
|
|
115
|
+
".txt": "text/plain",
|
|
116
|
+
".webm": "video/webm",
|
|
117
|
+
".zip": "application/zip",
|
|
118
|
+
...IMAGE_EXTENSION_TO_MIME_TYPE,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function getArtifactKind(commandInfo: CommandInfo): FileArtifactKind | undefined {
|
|
122
|
+
if (commandInfo.command === "screenshot") return "image";
|
|
123
|
+
if (commandInfo.command === "diff" && commandInfo.subcommand === "screenshot") return "image";
|
|
124
|
+
if (commandInfo.command === "pdf") return "pdf";
|
|
125
|
+
if (commandInfo.command === "download") return "download";
|
|
126
|
+
if (commandInfo.command === "wait" && commandInfo.subcommand === "--download") return "download";
|
|
127
|
+
if (commandInfo.command === "state" && commandInfo.subcommand === "save") return "file";
|
|
128
|
+
if (commandInfo.command === "trace") return "trace";
|
|
129
|
+
if (commandInfo.command === "profiler") return "profile";
|
|
130
|
+
if (commandInfo.command === "record") return "video";
|
|
131
|
+
if (commandInfo.command === "network" && commandInfo.subcommand === "har") return "har";
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function extractPathStrings(data: unknown): string[] {
|
|
136
|
+
if (typeof data === "string") {
|
|
137
|
+
return data.trim().length > 0 ? [data] : [];
|
|
138
|
+
}
|
|
139
|
+
if (!isRecord(data)) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const paths: string[] = [];
|
|
144
|
+
for (const key of PATH_FIELD_CANDIDATES) {
|
|
145
|
+
const value = data[key];
|
|
146
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
147
|
+
paths.push(value);
|
|
148
|
+
}
|
|
149
|
+
if (Array.isArray(value)) {
|
|
150
|
+
for (const item of value) {
|
|
151
|
+
if (typeof item === "string" && item.trim().length > 0) {
|
|
152
|
+
paths.push(item);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return [...new Set(paths)];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ArtifactRequestContext {
|
|
161
|
+
absolutePath: string;
|
|
162
|
+
path: string;
|
|
163
|
+
status?: FileArtifactMetadata["status"];
|
|
164
|
+
tempPath?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function buildFileArtifactMetadata(options: {
|
|
168
|
+
artifactRequest?: ArtifactRequestContext;
|
|
169
|
+
commandInfo: CommandInfo;
|
|
170
|
+
cwd: string;
|
|
171
|
+
path: string;
|
|
172
|
+
sessionName?: string;
|
|
173
|
+
}): Promise<FileArtifactMetadata | undefined> {
|
|
174
|
+
const kind = getArtifactKind(options.commandInfo);
|
|
175
|
+
if (!kind) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const absolutePath = options.artifactRequest?.absolutePath ?? resolve(options.cwd, options.path);
|
|
180
|
+
const displayPath = options.artifactRequest?.path ?? options.path;
|
|
181
|
+
const extension = extname(absolutePath || options.path).toLowerCase() || undefined;
|
|
182
|
+
let exists: boolean | undefined;
|
|
183
|
+
let sizeBytes: number | undefined;
|
|
184
|
+
try {
|
|
185
|
+
const fileStats = await stat(absolutePath);
|
|
186
|
+
exists = true;
|
|
187
|
+
sizeBytes = fileStats.size;
|
|
188
|
+
} catch {
|
|
189
|
+
exists = false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
absolutePath,
|
|
194
|
+
artifactType: kind,
|
|
195
|
+
command: options.commandInfo.command,
|
|
196
|
+
cwd: options.cwd,
|
|
197
|
+
exists,
|
|
198
|
+
extension,
|
|
199
|
+
kind,
|
|
200
|
+
mediaType: extension ? ARTIFACT_EXTENSION_TO_MEDIA_TYPE[extension] : undefined,
|
|
201
|
+
path: displayPath,
|
|
202
|
+
requestedPath: options.artifactRequest?.path,
|
|
203
|
+
session: options.sessionName,
|
|
204
|
+
sizeBytes,
|
|
205
|
+
status: options.artifactRequest?.status ?? (exists === false ? "missing" : "saved"),
|
|
206
|
+
subcommand: options.commandInfo.subcommand,
|
|
207
|
+
tempPath: options.artifactRequest?.tempPath,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function extractFileArtifacts(options: {
|
|
212
|
+
artifactRequest?: ArtifactRequestContext;
|
|
213
|
+
commandInfo: CommandInfo;
|
|
214
|
+
cwd: string;
|
|
215
|
+
data: unknown;
|
|
216
|
+
sessionName?: string;
|
|
217
|
+
}): Promise<FileArtifactMetadata[]> {
|
|
218
|
+
const candidates = extractPathStrings(options.data);
|
|
219
|
+
const artifacts = await Promise.all(candidates.map((path) => buildFileArtifactMetadata({ ...options, path })));
|
|
220
|
+
return artifacts.filter((artifact): artifact is FileArtifactMetadata => artifact !== undefined);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function buildManifestEntriesForFileArtifacts(artifacts: FileArtifactMetadata[], nowMs = Date.now()): SessionArtifactManifestEntry[] {
|
|
224
|
+
return artifacts.map((artifact) => ({
|
|
225
|
+
absolutePath: artifact.absolutePath,
|
|
226
|
+
command: artifact.command,
|
|
227
|
+
createdAtMs: nowMs,
|
|
228
|
+
cwd: artifact.cwd,
|
|
229
|
+
exists: artifact.exists,
|
|
230
|
+
extension: artifact.extension,
|
|
231
|
+
kind: artifact.kind,
|
|
232
|
+
mediaType: artifact.mediaType,
|
|
233
|
+
path: artifact.path,
|
|
234
|
+
requestedPath: artifact.requestedPath,
|
|
235
|
+
retentionState: artifact.exists === false ? "missing" : "live",
|
|
236
|
+
session: artifact.session,
|
|
237
|
+
sizeBytes: artifact.sizeBytes,
|
|
238
|
+
storageScope: "explicit-path",
|
|
239
|
+
subcommand: artifact.subcommand,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function isManifestFileArtifact(artifact: FileArtifactMetadata): boolean {
|
|
244
|
+
return !isPendingRecordingArtifact(artifact);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getArtifactVerificationEntry(artifact: FileArtifactMetadata): ArtifactVerificationEntry {
|
|
248
|
+
if (isPendingRecordingArtifact(artifact)) {
|
|
249
|
+
return {
|
|
250
|
+
absolutePath: artifact.absolutePath,
|
|
251
|
+
exists: artifact.exists,
|
|
252
|
+
kind: artifact.kind,
|
|
253
|
+
limitation: "Recording output is pending until record stop completes.",
|
|
254
|
+
mediaType: artifact.mediaType,
|
|
255
|
+
path: artifact.path,
|
|
256
|
+
requestedPath: artifact.requestedPath,
|
|
257
|
+
retentionState: undefined,
|
|
258
|
+
sizeBytes: artifact.sizeBytes,
|
|
259
|
+
state: "pending",
|
|
260
|
+
status: artifact.status,
|
|
261
|
+
storageScope: undefined,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const state = artifact.exists === true
|
|
265
|
+
? "verified"
|
|
266
|
+
: artifact.exists === false
|
|
267
|
+
? "missing"
|
|
268
|
+
: "unverified";
|
|
269
|
+
return {
|
|
270
|
+
absolutePath: artifact.absolutePath,
|
|
271
|
+
exists: artifact.exists,
|
|
272
|
+
kind: artifact.kind,
|
|
273
|
+
limitation: state === "missing"
|
|
274
|
+
? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
|
|
275
|
+
: state === "unverified"
|
|
276
|
+
? "The wrapper could not prove local filesystem existence for this artifact."
|
|
277
|
+
: undefined,
|
|
278
|
+
mediaType: artifact.mediaType,
|
|
279
|
+
path: artifact.path,
|
|
280
|
+
requestedPath: artifact.requestedPath,
|
|
281
|
+
retentionState: artifact.exists === false ? "missing" : "live",
|
|
282
|
+
sizeBytes: artifact.sizeBytes,
|
|
283
|
+
state,
|
|
284
|
+
status: artifact.status,
|
|
285
|
+
storageScope: "explicit-path",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getManifestVerificationEntry(entry: SessionArtifactManifestEntry): ArtifactVerificationEntry | undefined {
|
|
290
|
+
if (entry.storageScope === "explicit-path") return undefined;
|
|
291
|
+
const state = entry.retentionState === "live"
|
|
292
|
+
? "verified"
|
|
293
|
+
: entry.retentionState === "missing" || entry.retentionState === "evicted"
|
|
294
|
+
? "missing"
|
|
295
|
+
: "unverified";
|
|
296
|
+
return {
|
|
297
|
+
absolutePath: entry.absolutePath,
|
|
298
|
+
exists: entry.exists,
|
|
299
|
+
kind: entry.kind,
|
|
300
|
+
limitation: entry.retentionState === "ephemeral"
|
|
301
|
+
? "This spill file is process-temporary and may not survive reload or restart."
|
|
302
|
+
: entry.retentionState === "evicted"
|
|
303
|
+
? "This persisted spill file was evicted from the bounded session artifact store."
|
|
304
|
+
: undefined,
|
|
305
|
+
mediaType: entry.mediaType,
|
|
306
|
+
path: entry.path,
|
|
307
|
+
requestedPath: entry.requestedPath,
|
|
308
|
+
retentionState: entry.retentionState,
|
|
309
|
+
sizeBytes: entry.sizeBytes,
|
|
310
|
+
state,
|
|
311
|
+
storageScope: entry.storageScope,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function buildArtifactVerificationSummary(
|
|
316
|
+
artifacts: FileArtifactMetadata[],
|
|
317
|
+
manifest?: SessionArtifactManifest,
|
|
318
|
+
manifestPaths?: ReadonlySet<string>,
|
|
319
|
+
): ArtifactVerificationSummary | undefined {
|
|
320
|
+
const entries = [
|
|
321
|
+
...artifacts.map(getArtifactVerificationEntry),
|
|
322
|
+
...(manifest?.entries.flatMap((entry) => {
|
|
323
|
+
if (manifestPaths && !manifestPaths.has(entry.path)) return [];
|
|
324
|
+
const verificationEntry = getManifestVerificationEntry(entry);
|
|
325
|
+
return verificationEntry ? [verificationEntry] : [];
|
|
326
|
+
}) ?? []),
|
|
327
|
+
];
|
|
328
|
+
if (entries.length === 0) return undefined;
|
|
329
|
+
const verifiedCount = entries.filter((entry) => entry.state === "verified").length;
|
|
330
|
+
const missingCount = entries.filter((entry) => entry.state === "missing").length;
|
|
331
|
+
const pendingCount = entries.filter((entry) => entry.state === "pending").length;
|
|
332
|
+
const unverifiedCount = entries.filter((entry) => entry.state === "unverified").length;
|
|
333
|
+
return {
|
|
334
|
+
artifacts: entries,
|
|
335
|
+
missingCount,
|
|
336
|
+
pendingCount,
|
|
337
|
+
unverifiedCount,
|
|
338
|
+
verified: entries.length > 0 && verifiedCount === entries.length,
|
|
339
|
+
verifiedCount,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function classifyPresentationSuccessCategory(options: {
|
|
344
|
+
artifactVerification?: ArtifactVerificationSummary;
|
|
345
|
+
artifacts?: FileArtifactMetadata[];
|
|
346
|
+
inspection?: boolean;
|
|
347
|
+
savedFile?: SavedFilePresentationDetails;
|
|
348
|
+
}) {
|
|
349
|
+
if ((options.artifactVerification?.missingCount ?? 0) > 0 || (options.artifactVerification?.unverifiedCount ?? 0) > 0) {
|
|
350
|
+
return "artifact-unverified" as const;
|
|
351
|
+
}
|
|
352
|
+
return classifyAgentBrowserSuccessCategory(options);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function formatArtifactLabel(artifact: FileArtifactMetadata): string {
|
|
356
|
+
switch (artifact.kind) {
|
|
357
|
+
case "download":
|
|
358
|
+
return artifact.command === "wait" && artifact.subcommand === "--download" ? "Download completed" : "Downloaded file";
|
|
359
|
+
case "file":
|
|
360
|
+
return artifact.command === "state" ? "State file" : "Saved file";
|
|
361
|
+
case "har":
|
|
362
|
+
return "Saved HAR";
|
|
363
|
+
case "image":
|
|
364
|
+
return artifact.command === "diff" && artifact.subcommand === "screenshot" ? "Saved diff image" : "Saved image";
|
|
365
|
+
case "pdf":
|
|
366
|
+
return "Saved PDF";
|
|
367
|
+
case "profile":
|
|
368
|
+
return "Saved profile";
|
|
369
|
+
case "trace":
|
|
370
|
+
return "Saved trace";
|
|
371
|
+
case "video":
|
|
372
|
+
return isPendingRecordingArtifact(artifact) ? "Recording started; output will be written on stop" : "Saved recording";
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function formatArtifactSummary(artifacts: FileArtifactMetadata[]): string | undefined {
|
|
377
|
+
if (artifacts.length === 0) {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
if (artifacts.length === 1) {
|
|
381
|
+
const artifact = artifacts[0];
|
|
382
|
+
return `${formatArtifactLabel(artifact)}: ${artifact.path}`;
|
|
383
|
+
}
|
|
384
|
+
return `Saved ${artifacts.length} artifacts: ${artifacts.map((artifact) => `${artifact.kind} ${artifact.path}`).join(", ")}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function formatArtifactMetadataLines(artifacts: FileArtifactMetadata[]): string[] {
|
|
388
|
+
return artifacts.map((artifact, index) => {
|
|
389
|
+
if (isPendingRecordingArtifact(artifact)) {
|
|
390
|
+
return [
|
|
391
|
+
`${formatArtifactLabel(artifact)}: ${artifact.path}`,
|
|
392
|
+
`Artifact type: ${artifact.kind}`,
|
|
393
|
+
`Requested path: ${artifact.requestedPath ?? artifact.path}`,
|
|
394
|
+
`Absolute path: ${artifact.absolutePath}`,
|
|
395
|
+
`Exists: ${artifact.exists === true}`,
|
|
396
|
+
`Status: ${artifact.status ?? (artifact.exists === false ? "missing" : "saved")}`,
|
|
397
|
+
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
398
|
+
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
399
|
+
`Machine data: details.artifacts[${index}]`,
|
|
400
|
+
].filter((item): item is string => item !== undefined).join("\n");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return [
|
|
404
|
+
`${formatArtifactLabel(artifact)}: ${artifact.path}`,
|
|
405
|
+
`Artifact type: ${artifact.kind}`,
|
|
406
|
+
`Requested path: ${artifact.requestedPath ?? artifact.path}`,
|
|
407
|
+
`Absolute path: ${artifact.absolutePath}`,
|
|
408
|
+
`Exists: ${artifact.exists === true}`,
|
|
409
|
+
artifact.exists === false ? "not found on disk" : undefined,
|
|
410
|
+
typeof artifact.sizeBytes === "number" ? `Size: ${formatByteCount(artifact.sizeBytes)}` : undefined,
|
|
411
|
+
typeof artifact.sizeBytes === "number" ? `Size bytes: ${artifact.sizeBytes}` : undefined,
|
|
412
|
+
`Status: ${artifact.status ?? (artifact.exists === false ? "missing" : "saved")}`,
|
|
413
|
+
artifact.tempPath ? `Temp path: ${artifact.tempPath}` : undefined,
|
|
414
|
+
artifact.mediaType ? `Media type: ${artifact.mediaType}` : undefined,
|
|
415
|
+
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
416
|
+
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
417
|
+
`Machine data: details.artifacts[${index}]`,
|
|
418
|
+
].filter((item): item is string => item !== undefined).join("\n");
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function isDownloadWaitCommand(commandInfo: CommandInfo): boolean {
|
|
423
|
+
return commandInfo.command === "wait" && commandInfo.subcommand === "--download";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function extractSavedFilePath(data: Record<string, unknown>): string | undefined {
|
|
427
|
+
return typeof data.path === "string" && data.path.trim().length > 0 ? data.path : undefined;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function getSavedFileDetails(commandInfo: CommandInfo, data: Record<string, unknown>): SavedFilePresentationDetails | undefined {
|
|
431
|
+
const path = extractSavedFilePath(data);
|
|
432
|
+
if (!path) {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
const savedFileCommand = isDownloadWaitCommand(commandInfo)
|
|
436
|
+
? "wait"
|
|
437
|
+
: commandInfo.command === "download" || commandInfo.command === "pdf"
|
|
438
|
+
? commandInfo.command
|
|
439
|
+
: undefined;
|
|
440
|
+
if (!savedFileCommand) {
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const { path: _path, ...metadata } = data;
|
|
445
|
+
const details: SavedFilePresentationDetails = {
|
|
446
|
+
command: savedFileCommand,
|
|
447
|
+
kind: savedFileCommand === "pdf" ? "pdf" : "download",
|
|
448
|
+
path,
|
|
449
|
+
};
|
|
450
|
+
if (Object.keys(metadata).length > 0) {
|
|
451
|
+
details.metadata = metadata;
|
|
452
|
+
}
|
|
453
|
+
if (commandInfo.subcommand) {
|
|
454
|
+
details.subcommand = commandInfo.subcommand;
|
|
455
|
+
}
|
|
456
|
+
return details;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function isTrustedScreenshotOutput(commandInfo: CommandInfo): boolean {
|
|
460
|
+
return commandInfo.command === "screenshot";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function extractImagePath(commandInfo: CommandInfo, cwd: string, data: unknown): string | undefined {
|
|
464
|
+
if (!isTrustedScreenshotOutput(commandInfo)) {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
if (typeof data === "string") {
|
|
468
|
+
const mimeType = getImageMimeType(data);
|
|
469
|
+
return mimeType ? resolve(cwd, data) : undefined;
|
|
470
|
+
}
|
|
471
|
+
if (!isRecord(data) || typeof data.path !== "string") {
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
const mimeType = getImageMimeType(data.path);
|
|
475
|
+
return mimeType ? resolve(cwd, data.path) : undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function attachInlineImage(presentation: ToolPresentation, imagePath: string): Promise<ToolPresentation> {
|
|
479
|
+
const mimeType = getImageMimeType(imagePath);
|
|
480
|
+
if (!mimeType) {
|
|
481
|
+
return presentation;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const fileStats = await stat(imagePath);
|
|
486
|
+
const inlineImageMaxBytes = getInlineImageMaxBytes();
|
|
487
|
+
if (fileStats.size > inlineImageMaxBytes) {
|
|
488
|
+
appendPresentationNotice(
|
|
489
|
+
presentation,
|
|
490
|
+
`Image attachment skipped: ${formatByteCount(fileStats.size)} exceeds the inline limit of ${formatByteCount(inlineImageMaxBytes)}.`,
|
|
491
|
+
);
|
|
492
|
+
presentation.imagePath = imagePath;
|
|
493
|
+
return presentation;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const file = await readFile(imagePath);
|
|
497
|
+
presentation.content.push({ type: "image", data: file.toString("base64"), mimeType });
|
|
498
|
+
presentation.imagePath = imagePath;
|
|
499
|
+
return presentation;
|
|
500
|
+
} catch (error) {
|
|
501
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
502
|
+
appendPresentationNotice(presentation, `Image attachment failed: ${message}`);
|
|
503
|
+
presentation.imagePath = imagePath;
|
|
504
|
+
return presentation;
|
|
505
|
+
}
|
|
506
|
+
}
|