pi-agent-browser-native 0.6.9 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +62 -19
- package/dist/extensions/agent-browser/index.js +424 -451
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +6 -7
- package/dist/extensions/agent-browser/lib/argv-grammar.js +7 -1
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +4 -8
- package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +15 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/script.js +3 -2
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +42 -12
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +3 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +6 -14
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +14 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +12 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +3 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +38 -31
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +60 -20
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +161 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +5 -5
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +2 -4
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +68 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -6
- package/dist/extensions/agent-browser/lib/page-target-validation.js +9 -5
- package/dist/extensions/agent-browser/lib/playbook.js +13 -12
- package/dist/extensions/agent-browser/lib/process-environment.js +26 -8
- package/dist/extensions/agent-browser/lib/process.js +8 -5
- package/dist/extensions/agent-browser/lib/read-confirmation.js +59 -0
- package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -1
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +8 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +6 -5
- package/dist/extensions/agent-browser/lib/results/categories.js +4 -2
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +76 -57
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +5 -25
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +40 -38
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +1 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +3 -3
- package/dist/extensions/agent-browser/lib/results/presentation.js +38 -9
- package/dist/extensions/agent-browser/lib/results/recording.js +50 -0
- package/dist/extensions/agent-browser/lib/runtime.js +72 -20
- package/dist/extensions/agent-browser/lib/session-page-state.js +24 -8
- package/dist/extensions/agent-browser/lib/temp.js +4 -0
- package/dist/scripts/agent-browser-target.mjs +1 -1
- package/docs/ARCHITECTURE.md +29 -12
- package/docs/COMMAND_REFERENCE.md +67 -31
- package/docs/RELEASE.md +6 -4
- package/docs/SUPPORT_MATRIX.md +24 -16
- package/docs/TOOL_CONTRACT.md +82 -28
- package/package.json +1 -1
- package/scripts/agent-browser-capability-baseline.mjs +10 -3
- package/scripts/agent-browser-target.mjs +1 -1
- package/scripts/prepare.mjs +2 -4
|
@@ -4,7 +4,8 @@ export function isPendingRecordingCommand(command, subcommand, kind) {
|
|
|
4
4
|
return command === "record" && (subcommand === "start" || subcommand === "restart") && kind === "video";
|
|
5
5
|
}
|
|
6
6
|
export function isPendingRecordingArtifact(artifact) {
|
|
7
|
-
return
|
|
7
|
+
return artifact.recordingState === "openRecording" || artifact.status === "pending"
|
|
8
|
+
|| (artifact.status === undefined && isPendingRecordingCommand(artifact.command, artifact.subcommand, artifact.kind));
|
|
8
9
|
}
|
|
9
10
|
export const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
|
|
10
11
|
export const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
|
|
@@ -98,10 +99,10 @@ export function retirePendingRecordingManifestEntries(manifest, sessionName, nam
|
|
|
98
99
|
|| !entry.session
|
|
99
100
|
|| getAgentBrowserSessionIdentityKey(entry.session, entry.namespace) !== sessionKey
|
|
100
101
|
|| entry.kind !== "video"
|
|
101
|
-
|| !
|
|
102
|
+
|| !isPendingRecordingArtifact(entry))
|
|
102
103
|
return entry;
|
|
103
104
|
changed = true;
|
|
104
|
-
return { ...entry,
|
|
105
|
+
return { ...entry, recordingState: undefined, status: "unverified", subcommand: "close-abandoned" };
|
|
105
106
|
});
|
|
106
107
|
if (!changed)
|
|
107
108
|
return manifest;
|
|
@@ -135,7 +136,7 @@ export function mergeSessionArtifactManifest(options) {
|
|
|
135
136
|
if (candidateKey !== key
|
|
136
137
|
&& sameRecordingSession
|
|
137
138
|
&& candidate.kind === "video"
|
|
138
|
-
&&
|
|
139
|
+
&& isPendingRecordingArtifact(candidate)) {
|
|
139
140
|
byPath.delete(candidateKey);
|
|
140
141
|
}
|
|
141
142
|
}
|
|
@@ -155,7 +156,7 @@ export function mergeSessionArtifactManifest(options) {
|
|
|
155
156
|
const leftTime = left.evictedAtMs ?? left.createdAtMs;
|
|
156
157
|
const rightTime = right.evictedAtMs ?? right.createdAtMs;
|
|
157
158
|
return rightTime - leftTime
|
|
158
|
-
|| Number(
|
|
159
|
+
|| Number(isPendingRecordingArtifact(right)) - Number(isPendingRecordingArtifact(left))
|
|
159
160
|
|| left.path.localeCompare(right.path);
|
|
160
161
|
})
|
|
161
162
|
.slice(0, maxEntries);
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { isPendingRecordingArtifact } from "./artifact-manifest.js";
|
|
2
2
|
function hasUnverifiedFileArtifact(artifacts) {
|
|
3
|
-
return (artifacts ?? []).some((artifact) => !isPendingRecordingArtifact(artifact) && artifact.exists !== true);
|
|
3
|
+
return (artifacts ?? []).some((artifact) => !isPendingRecordingArtifact(artifact) && (artifact.exists !== true || ["failed", "stale", "unverified"].includes(artifact.status ?? "")));
|
|
4
4
|
}
|
|
5
5
|
export function classifyAgentBrowserSuccessCategory(options) {
|
|
6
6
|
if (options.inspection)
|
|
7
7
|
return "inspection";
|
|
8
|
+
if (hasUnverifiedFileArtifact(options.artifacts))
|
|
9
|
+
return "artifact-unverified";
|
|
8
10
|
if ((options.artifacts ?? []).some(isPendingRecordingArtifact))
|
|
9
11
|
return "artifact-pending";
|
|
10
12
|
if ((options.artifacts ?? []).length > 0)
|
|
11
|
-
return
|
|
13
|
+
return "artifact-saved";
|
|
12
14
|
if (options.savedFile)
|
|
13
15
|
return "artifact-saved";
|
|
14
16
|
return "completed";
|
|
@@ -3,9 +3,9 @@ import { extname, resolve } from "node:path";
|
|
|
3
3
|
import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
|
|
4
4
|
import { getExplicitArtifactDestination } from "../../orchestration/browser-run/artifact-paths.js";
|
|
5
5
|
import { isRecord, parsePositiveInteger } from "../../parsing.js";
|
|
6
|
-
import { extractUpstreamCommandTokens } from "../../runtime.js";
|
|
7
6
|
import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isPendingRecordingCommand, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
|
|
8
7
|
import { classifyAgentBrowserSuccessCategory } from "../categories.js";
|
|
8
|
+
import { formatRecordingReceipt, getRecordingReceipt } from "../recording.js";
|
|
9
9
|
const PNG_HEADER = Buffer.from("89504e470d0a1a0a0000000d49484452", "hex");
|
|
10
10
|
const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
|
|
11
11
|
const DEFAULT_INLINE_IMAGE_MAX_BYTES = 5 * 1_024 * 1_024;
|
|
@@ -160,24 +160,26 @@ async function buildFileArtifactMetadata(options) {
|
|
|
160
160
|
const absolutePath = options.artifactRequest?.absolutePath ?? resolve(options.cwd, options.path);
|
|
161
161
|
const displayPath = options.artifactRequest?.path ?? options.path;
|
|
162
162
|
const extension = extname(absolutePath || options.path).toLowerCase() || undefined;
|
|
163
|
-
const pendingRecording = isPendingRecordingCommand(options.commandInfo.command, options.commandInfo.subcommand, kind);
|
|
163
|
+
const pendingRecording = options.recordingPending === true || (isPendingRecordingCommand(options.commandInfo.command, options.commandInfo.subcommand, kind) && options.recordingOutcome !== false && options.recording?.success !== false);
|
|
164
|
+
const captureStartedAtMs = options.recording?.capture.startedAt ? Date.parse(options.recording.capture.startedAt) : NaN;
|
|
165
|
+
const recordingStartedAtMs = Number.isFinite(captureStartedAtMs) ? captureStartedAtMs : options.artifactMinUpdatedAtMs;
|
|
164
166
|
let exists;
|
|
165
167
|
let sizeBytes;
|
|
166
168
|
let mediaType;
|
|
167
169
|
let stale = false;
|
|
168
170
|
let updatedAtMs;
|
|
169
|
-
if (!pendingRecording) {
|
|
171
|
+
if (!pendingRecording || options.commandInfo.subcommand === "stop") {
|
|
170
172
|
try {
|
|
171
173
|
const fileStats = await stat(absolutePath);
|
|
172
|
-
exists =
|
|
174
|
+
exists = fileStats.isFile();
|
|
173
175
|
sizeBytes = fileStats.size;
|
|
174
176
|
updatedAtMs = fileStats.mtimeMs;
|
|
175
177
|
mediaType = fileStats.isFile() ? await getFileImageMimeType(absolutePath) : undefined;
|
|
176
178
|
const commandCreatesArtifact = !(options.commandInfo.command === "wait" && isDownloadWaitSubcommand(options.commandInfo.subcommand));
|
|
177
|
-
stale = commandCreatesArtifact && artifactMtimeIsOutsideCommandWindow(updatedAtMs, options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
|
|
179
|
+
stale = commandCreatesArtifact && artifactMtimeIsOutsideCommandWindow(updatedAtMs, kind === "video" ? recordingStartedAtMs : options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
|
|
178
180
|
}
|
|
179
|
-
catch {
|
|
180
|
-
exists = false;
|
|
181
|
+
catch (error) {
|
|
182
|
+
exists = ["ENOENT", "ENOTDIR"].includes(error.code ?? "") ? false : undefined;
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
return {
|
|
@@ -191,11 +193,17 @@ async function buildFileArtifactMetadata(options) {
|
|
|
191
193
|
mediaType,
|
|
192
194
|
namespace: options.namespace,
|
|
193
195
|
path: displayPath,
|
|
196
|
+
recording: options.recording,
|
|
197
|
+
recordingStartedAtMs: kind === "video" ? recordingStartedAtMs : undefined,
|
|
194
198
|
recordingState: pendingRecording ? "openRecording" : undefined,
|
|
195
|
-
requestedPath: options.artifactRequest?.path ?? getExplicitArtifactDestination(
|
|
199
|
+
requestedPath: options.artifactRequest?.path ?? getExplicitArtifactDestination(options.commandInfo.commandTokens ?? []),
|
|
196
200
|
session: options.sessionName,
|
|
197
201
|
sizeBytes,
|
|
198
|
-
status: pendingRecording ? "pending" : exists === false ? "missing" :
|
|
202
|
+
status: pendingRecording ? "pending" : exists !== true ? exists === false ? "missing" : "unverified" : stale ? "stale"
|
|
203
|
+
: kind === "video" && (options.recording?.success === false || options.recording?.output.encoderSucceeded === false || (options.recordingOutcome === false && options.recording?.success !== null)) ? "failed"
|
|
204
|
+
: kind === "video" && ((options.recording?.success !== true && options.recordingOutcome !== true)
|
|
205
|
+
|| (options.recording?.file.sizeBytes != null && options.recording.file.sizeBytes !== sizeBytes)) ? "unverified"
|
|
206
|
+
: options.artifactRequest?.status ?? "saved",
|
|
199
207
|
subcommand: options.commandInfo.subcommand,
|
|
200
208
|
tempPath: options.artifactRequest?.tempPath,
|
|
201
209
|
updatedAtMs,
|
|
@@ -205,6 +213,10 @@ async function buildFileArtifactMetadata(options) {
|
|
|
205
213
|
async function buildPreviousRestartRecordingArtifact(options) {
|
|
206
214
|
if (options.commandInfo.command !== "record" || options.commandInfo.subcommand !== "restart")
|
|
207
215
|
return undefined;
|
|
216
|
+
if (isRecord(options.data) && "previousRecording" in options.data) {
|
|
217
|
+
const recording = getRecordingReceipt(options.data.previousRecording);
|
|
218
|
+
return recording ? buildFileArtifactMetadata({ ...options, commandInfo: { command: "record", subcommand: "restart-previous" }, path: recording.path, recording }) : undefined;
|
|
219
|
+
}
|
|
208
220
|
const sessionKey = options.sessionName ? getAgentBrowserSessionIdentityKey(options.sessionName, options.namespace) : undefined;
|
|
209
221
|
const previousRecording = options.artifactManifest?.entries.find((entry) => (entry.command === "record" &&
|
|
210
222
|
(entry.subcommand === "start" || entry.subcommand === "restart") &&
|
|
@@ -213,50 +225,42 @@ async function buildPreviousRestartRecordingArtifact(options) {
|
|
|
213
225
|
if (!previousRecording)
|
|
214
226
|
return undefined;
|
|
215
227
|
const absolutePath = previousRecording.absolutePath ?? resolve(options.cwd, previousRecording.path);
|
|
228
|
+
const base = {
|
|
229
|
+
absolutePath,
|
|
230
|
+
artifactType: "video",
|
|
231
|
+
command: "record",
|
|
232
|
+
cwd: previousRecording.cwd ?? options.cwd,
|
|
233
|
+
extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
|
|
234
|
+
kind: "video",
|
|
235
|
+
namespace: previousRecording.namespace ?? options.namespace,
|
|
236
|
+
path: previousRecording.path,
|
|
237
|
+
requestedPath: previousRecording.requestedPath,
|
|
238
|
+
session: previousRecording.session ?? options.sessionName,
|
|
239
|
+
subcommand: "restart-previous",
|
|
240
|
+
};
|
|
216
241
|
try {
|
|
217
242
|
const fileStats = await stat(absolutePath);
|
|
218
243
|
const stale = artifactMtimeIsOutsideCommandWindow(fileStats.mtimeMs, options.artifactMinUpdatedAtMs, options.artifactMaxUpdatedAtMs);
|
|
219
244
|
return {
|
|
220
|
-
|
|
221
|
-
artifactType: "video",
|
|
222
|
-
command: "record",
|
|
223
|
-
cwd: previousRecording.cwd ?? options.cwd,
|
|
245
|
+
...base,
|
|
224
246
|
exists: true,
|
|
225
|
-
extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
|
|
226
|
-
kind: "video",
|
|
227
247
|
mediaType: fileStats.isFile() ? await getFileImageMimeType(absolutePath) : undefined,
|
|
228
|
-
namespace: previousRecording.namespace ?? options.namespace,
|
|
229
|
-
path: previousRecording.path,
|
|
230
|
-
requestedPath: previousRecording.requestedPath,
|
|
231
|
-
session: previousRecording.session ?? options.sessionName,
|
|
232
248
|
sizeBytes: fileStats.size,
|
|
233
|
-
status: stale ? "stale" : "
|
|
234
|
-
subcommand: "restart-previous",
|
|
249
|
+
status: stale ? "stale" : "unverified",
|
|
235
250
|
updatedAtMs: fileStats.mtimeMs,
|
|
236
251
|
};
|
|
237
252
|
}
|
|
238
|
-
catch {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
artifactType: "video",
|
|
242
|
-
command: "record",
|
|
243
|
-
cwd: previousRecording.cwd ?? options.cwd,
|
|
244
|
-
exists: false,
|
|
245
|
-
extension: previousRecording.extension ?? (extname(absolutePath).toLowerCase() || undefined),
|
|
246
|
-
kind: "video",
|
|
247
|
-
namespace: previousRecording.namespace ?? options.namespace,
|
|
248
|
-
path: previousRecording.path,
|
|
249
|
-
requestedPath: previousRecording.requestedPath,
|
|
250
|
-
session: previousRecording.session ?? options.sessionName,
|
|
251
|
-
status: "missing",
|
|
252
|
-
subcommand: "restart-previous",
|
|
253
|
-
};
|
|
253
|
+
catch (error) {
|
|
254
|
+
const missing = ["ENOENT", "ENOTDIR"].includes(error.code ?? "");
|
|
255
|
+
return { ...base, exists: missing ? false : undefined, status: missing ? "missing" : "unverified" };
|
|
254
256
|
}
|
|
255
257
|
}
|
|
256
258
|
export async function extractFileArtifacts(options) {
|
|
257
259
|
const candidates = extractPathStrings(options.data);
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
+
const recording = options.commandInfo.command === "record" ? getRecordingReceipt(options.data, options.commandInfo.subcommand === "stop" ? options.recordingOutcome : undefined) : undefined;
|
|
261
|
+
const recordingPending = options.recordingPending ?? (recording?.success === null && isRecord(options.data) && isRecord(options.data.capture) && recording.capture.endedAt === null);
|
|
262
|
+
const currentArtifacts = (await Promise.all(candidates.map((path) => buildFileArtifactMetadata({ ...options, path, recording, recordingPending })))).filter((artifact) => artifact !== undefined);
|
|
263
|
+
const previousRestartRecordingArtifact = await buildPreviousRestartRecordingArtifact(options);
|
|
260
264
|
return previousRestartRecordingArtifact ? [previousRestartRecordingArtifact, ...currentArtifacts] : currentArtifacts;
|
|
261
265
|
}
|
|
262
266
|
export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now()) {
|
|
@@ -271,6 +275,10 @@ export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now
|
|
|
271
275
|
mediaType: artifact.mediaType,
|
|
272
276
|
namespace: artifact.namespace,
|
|
273
277
|
path: artifact.path,
|
|
278
|
+
recording: artifact.recording,
|
|
279
|
+
recordingStartedAtMs: artifact.recordingStartedAtMs,
|
|
280
|
+
recordingState: artifact.recordingState,
|
|
281
|
+
status: artifact.status,
|
|
274
282
|
requestedPath: artifact.requestedPath,
|
|
275
283
|
retentionState: artifact.exists === false || artifact.status === "stale" ? "missing" : "live",
|
|
276
284
|
session: artifact.session,
|
|
@@ -280,10 +288,7 @@ export function buildManifestEntriesForFileArtifacts(artifacts, nowMs = Date.now
|
|
|
280
288
|
}));
|
|
281
289
|
}
|
|
282
290
|
export function isManifestFileArtifact(artifact) {
|
|
283
|
-
|
|
284
|
-
return artifact.kind === "video" && artifact.command === "record" && artifact.subcommand === "restart-previous";
|
|
285
|
-
}
|
|
286
|
-
return artifact.kind === "video" && artifact.command === "record" ? true : !isPendingRecordingArtifact(artifact);
|
|
291
|
+
return artifact.kind === "video" && artifact.command === "record" ? true : artifact.status !== "stale" && !isPendingRecordingArtifact(artifact);
|
|
287
292
|
}
|
|
288
293
|
function getArtifactVerificationEntry(artifact) {
|
|
289
294
|
if (isPendingRecordingArtifact(artifact)) {
|
|
@@ -291,7 +296,9 @@ function getArtifactVerificationEntry(artifact) {
|
|
|
291
296
|
absolutePath: artifact.absolutePath,
|
|
292
297
|
exists: artifact.exists,
|
|
293
298
|
kind: artifact.kind,
|
|
294
|
-
limitation: "Recording output is pending until
|
|
299
|
+
limitation: "Recording output is pending until native finalization succeeds and the file is verified.",
|
|
300
|
+
recording: artifact.recording,
|
|
301
|
+
recordingStartedAtMs: artifact.recordingStartedAtMs,
|
|
295
302
|
mediaType: artifact.mediaType,
|
|
296
303
|
path: artifact.path,
|
|
297
304
|
recordingState: artifact.recordingState ?? "openRecording",
|
|
@@ -304,7 +311,7 @@ function getArtifactVerificationEntry(artifact) {
|
|
|
304
311
|
willExistOnStop: artifact.willExistOnStop ?? true,
|
|
305
312
|
};
|
|
306
313
|
}
|
|
307
|
-
const state = artifact.status
|
|
314
|
+
const state = ["failed", "stale", "unverified"].includes(artifact.status ?? "")
|
|
308
315
|
? "unverified"
|
|
309
316
|
: artifact.exists === true
|
|
310
317
|
? "verified"
|
|
@@ -315,13 +322,17 @@ function getArtifactVerificationEntry(artifact) {
|
|
|
315
322
|
absolutePath: artifact.absolutePath,
|
|
316
323
|
exists: artifact.exists,
|
|
317
324
|
kind: artifact.kind,
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
+
recording: artifact.recording,
|
|
326
|
+
recordingStartedAtMs: artifact.recordingStartedAtMs,
|
|
327
|
+
limitation: artifact.status === "failed" || artifact.status === "unverified"
|
|
328
|
+
? "File presence does not prove successful native recording finalization or encoding. Inspect the receipt and original failure."
|
|
329
|
+
: artifact.status === "stale"
|
|
330
|
+
? "The reported path's modification time fell outside this command's bounded artifact window. Treat the artifact as stale until regenerated."
|
|
331
|
+
: state === "missing"
|
|
332
|
+
? "The wrapper did not find the reported artifact at absolutePath. Treat the path as unverified until recovered or regenerated."
|
|
333
|
+
: state === "unverified"
|
|
334
|
+
? "The wrapper could not prove local filesystem existence for this artifact."
|
|
335
|
+
: undefined,
|
|
325
336
|
mediaType: artifact.mediaType,
|
|
326
337
|
path: artifact.path,
|
|
327
338
|
requestedPath: artifact.requestedPath,
|
|
@@ -427,6 +438,10 @@ function formatArtifactLabel(artifact) {
|
|
|
427
438
|
case "trace":
|
|
428
439
|
return "Saved trace";
|
|
429
440
|
case "video":
|
|
441
|
+
if (artifact.status === "failed")
|
|
442
|
+
return artifact.subcommand === "restart-previous" ? "Previous recording failed" : "Recording failed";
|
|
443
|
+
if (artifact.status === "unverified")
|
|
444
|
+
return artifact.subcommand === "restart-previous" ? "Previous recording unverified" : "Recording unverified";
|
|
430
445
|
if (artifact.command === "record" && artifact.subcommand === "restart-previous") {
|
|
431
446
|
if (artifact.status === "stale")
|
|
432
447
|
return "Previous recording stale";
|
|
@@ -435,8 +450,10 @@ function formatArtifactLabel(artifact) {
|
|
|
435
450
|
return "Previous recording saved";
|
|
436
451
|
}
|
|
437
452
|
if (!isPendingRecordingArtifact(artifact))
|
|
438
|
-
return "Saved recording";
|
|
439
|
-
|
|
453
|
+
return artifact.status === "saved" ? "Saved recording" : "Recording reported; file not verified";
|
|
454
|
+
if (artifact.subcommand === "stop")
|
|
455
|
+
return "Recording finalization pending";
|
|
456
|
+
return artifact.subcommand === "restart" ? "Recording restarted; output will be written on stop" : "Recording started; output will be written on stop";
|
|
440
457
|
}
|
|
441
458
|
}
|
|
442
459
|
export function formatArtifactSummary(artifacts) {
|
|
@@ -452,7 +469,7 @@ export function formatArtifactSummary(artifacts) {
|
|
|
452
469
|
if (restartArtifact && previousRecordingArtifacts.length > 0) {
|
|
453
470
|
return [...previousRecordingArtifacts, restartArtifact].map((artifact) => `${formatArtifactLabel(artifact)}: ${artifact.path}`).join("\n");
|
|
454
471
|
}
|
|
455
|
-
return
|
|
472
|
+
return `${artifacts.every((artifact) => artifact.status === "saved") ? "Saved" : "Reported"} ${artifacts.length} artifacts: ${artifacts.map((artifact) => `${artifact.kind} ${artifact.path}`).join(", ")}`;
|
|
456
473
|
}
|
|
457
474
|
export function formatArtifactMetadataLines(artifacts) {
|
|
458
475
|
return artifacts.map((artifact, index) => {
|
|
@@ -462,12 +479,13 @@ export function formatArtifactMetadataLines(artifacts) {
|
|
|
462
479
|
`Artifact type: ${artifact.kind}`,
|
|
463
480
|
artifact.requestedPath ? `Requested path: ${artifact.requestedPath}` : undefined,
|
|
464
481
|
`Absolute path: ${artifact.absolutePath}`,
|
|
465
|
-
|
|
482
|
+
`Exists: ${artifact.exists ?? "pending until record stop"}`,
|
|
466
483
|
`Status: ${artifact.status ?? "pending"}`,
|
|
467
484
|
`Recording state: ${artifact.recordingState ?? "openRecording"}`,
|
|
468
485
|
`Will exist on stop: ${artifact.willExistOnStop !== false}`,
|
|
469
486
|
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
470
487
|
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
488
|
+
artifact.recording ? formatRecordingReceipt(artifact.recording) : undefined,
|
|
471
489
|
`Machine data: details.artifacts[${index}]`,
|
|
472
490
|
].filter((item) => item !== undefined).join("\n");
|
|
473
491
|
}
|
|
@@ -476,7 +494,7 @@ export function formatArtifactMetadataLines(artifacts) {
|
|
|
476
494
|
`Artifact type: ${artifact.kind}`,
|
|
477
495
|
artifact.requestedPath ? `Requested path: ${artifact.requestedPath}` : undefined,
|
|
478
496
|
`Absolute path: ${artifact.absolutePath}`,
|
|
479
|
-
`Exists: ${artifact.exists
|
|
497
|
+
`Exists: ${artifact.exists ?? "unknown"}`,
|
|
480
498
|
artifact.exists === false ? "not found on disk" : undefined,
|
|
481
499
|
typeof artifact.sizeBytes === "number" ? `Size: ${formatByteCount(artifact.sizeBytes)}` : undefined,
|
|
482
500
|
typeof artifact.sizeBytes === "number" ? `Size bytes: ${artifact.sizeBytes}` : undefined,
|
|
@@ -485,6 +503,7 @@ export function formatArtifactMetadataLines(artifacts) {
|
|
|
485
503
|
artifact.mediaType ? `Media type: ${artifact.mediaType}` : undefined,
|
|
486
504
|
artifact.session ? `Session: ${artifact.session}` : undefined,
|
|
487
505
|
artifact.cwd ? `CWD: ${artifact.cwd}` : undefined,
|
|
506
|
+
artifact.recording ? formatRecordingReceipt(artifact.recording) : undefined,
|
|
488
507
|
`Machine data: details.artifacts[${index}]`,
|
|
489
508
|
].filter((item) => item !== undefined).join("\n");
|
|
490
509
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
1
2
|
import { isCloseCommand } from "../../command-taxonomy.js";
|
|
2
3
|
import { isRecord } from "../../parsing.js";
|
|
3
4
|
import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
|
|
@@ -194,7 +195,7 @@ async function buildBatchStepPresentation(options) {
|
|
|
194
195
|
const redactedCommand = command ? redactInvocationArgs(command) : undefined;
|
|
195
196
|
const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
|
|
196
197
|
const lifecycle = extractAgentBrowserLifecycle(item.result);
|
|
197
|
-
if (item.success === false) {
|
|
198
|
+
if (item.success === false && command?.[0] !== "record") {
|
|
198
199
|
const redactedErrorData = redactBatchStepErrorData(command, item.error);
|
|
199
200
|
const errorText = formatBatchStepError(redactedErrorData);
|
|
200
201
|
const failureCategory = classifyAgentBrowserFailureCategory({
|
|
@@ -252,7 +253,9 @@ async function buildBatchStepPresentation(options) {
|
|
|
252
253
|
commandInfo: commandInfoWithTokens,
|
|
253
254
|
cwd,
|
|
254
255
|
args: command,
|
|
255
|
-
envelope: { data: item.result, success:
|
|
256
|
+
envelope: { data: item.result, success: item.success !== false, error: item.error },
|
|
257
|
+
errorText: item.success === false ? formatBatchStepError(redactBatchStepErrorData(command, item.error)) : undefined,
|
|
258
|
+
piCleanupOwnership: options.piCleanupOwnership,
|
|
256
259
|
networkRouteDiagnostics,
|
|
257
260
|
namespace,
|
|
258
261
|
persistentArtifactStore,
|
|
@@ -320,11 +323,18 @@ async function buildBatchStepPresentation(options) {
|
|
|
320
323
|
presentation,
|
|
321
324
|
};
|
|
322
325
|
}
|
|
323
|
-
function abandonedRecordingArtifact(artifact) {
|
|
326
|
+
async function abandonedRecordingArtifact(artifact) {
|
|
324
327
|
const { recordingState: _recordingState, willExistOnStop: _willExistOnStop, ...terminal } = artifact;
|
|
325
|
-
|
|
328
|
+
try {
|
|
329
|
+
const file = await stat(artifact.absolutePath);
|
|
330
|
+
return { ...terminal, exists: file.isFile(), sizeBytes: file.size, status: file.isFile() ? "unverified" : "missing", subcommand: "close-abandoned" };
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
const missing = error.code === "ENOENT";
|
|
334
|
+
return { ...terminal, exists: missing ? false : undefined, status: missing ? "missing" : "unverified", subcommand: "close-abandoned" };
|
|
335
|
+
}
|
|
326
336
|
}
|
|
327
|
-
function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace) {
|
|
337
|
+
async function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace) {
|
|
328
338
|
const artifacts = [];
|
|
329
339
|
const pendingIndexesBySession = new Map();
|
|
330
340
|
const removedPendingIndexes = new Set();
|
|
@@ -351,7 +361,7 @@ function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace)
|
|
|
351
361
|
for (const pendingIndex of pendingIndexesBySession.get(session) ?? []) {
|
|
352
362
|
const pending = artifacts[pendingIndex];
|
|
353
363
|
if (pending && !removedPendingIndexes.has(pendingIndex))
|
|
354
|
-
artifacts[pendingIndex] = abandonedRecordingArtifact(pending);
|
|
364
|
+
artifacts[pendingIndex] = await abandonedRecordingArtifact(pending);
|
|
355
365
|
}
|
|
356
366
|
}
|
|
357
367
|
return artifacts.filter((_, index) => !removedPendingIndexes.has(index));
|
|
@@ -372,6 +382,7 @@ export async function buildBatchPresentation(options) {
|
|
|
372
382
|
cwd,
|
|
373
383
|
index,
|
|
374
384
|
item,
|
|
385
|
+
piCleanupOwnership: options.piCleanupOwnership,
|
|
375
386
|
namespace,
|
|
376
387
|
networkRoutes: currentNetworkRoutes,
|
|
377
388
|
persistentArtifactStore: persistentArtifactStore ? { ...persistentArtifactStore, protectedPaths: protectedPersistentPaths } : undefined,
|
|
@@ -387,7 +398,7 @@ export async function buildBatchPresentation(options) {
|
|
|
387
398
|
}
|
|
388
399
|
const batchFailure = getBatchFailureDetails(steps);
|
|
389
400
|
const images = steps.flatMap((step) => getPresentationImages(step.presentation));
|
|
390
|
-
const artifacts = coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace);
|
|
401
|
+
const artifacts = await coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace);
|
|
391
402
|
const artifactVerification = buildArtifactVerificationSummary(artifacts);
|
|
392
403
|
const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
|
|
393
404
|
primaryPath: step.presentation.fullOutputPath,
|
|
@@ -399,7 +410,7 @@ export async function buildBatchPresentation(options) {
|
|
|
399
410
|
}));
|
|
400
411
|
const redactedBatchData = steps.map(({ details }) => (details.success
|
|
401
412
|
? { command: details.command, result: details.data, success: true }
|
|
402
|
-
: { command: details.command, error: details.text, success: false }));
|
|
413
|
+
: { command: details.command, error: details.text, ...(details.command?.[0] === "record" ? { result: details.data } : {}), success: false }));
|
|
403
414
|
const unverifiedMutationCount = steps.filter((step) => step.details.pageChangeSummary?.changeType === "mutation" && step.details.pageChangeSummary.observed === false).length;
|
|
404
415
|
const mutationEvidenceText = unverifiedMutationCount > 0
|
|
405
416
|
? `Mutation evidence: ${unverifiedMutationCount} action result${unverifiedMutationCount === 1 ? " proves" : "s prove"} dispatch only, not application state change. Use explicit later assertions or external receipts as postconditions; fixed waits are not postconditions.`
|
|
@@ -5,29 +5,9 @@ const UNTITLED_PAGE_SUMMARY = "(untitled page)";
|
|
|
5
5
|
export function stringifyModelFacing(value) {
|
|
6
6
|
return stringifyUnknown(redactSensitiveValue(value));
|
|
7
7
|
}
|
|
8
|
-
export function parseJsonPreviewString(value) {
|
|
9
|
-
const trimmed = value.trim();
|
|
10
|
-
if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
|
|
11
|
-
return value;
|
|
12
|
-
try {
|
|
13
|
-
return JSON.parse(trimmed);
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
8
|
export function redactModelFacingText(text) {
|
|
20
|
-
const parsed = parseJsonPreviewString(text);
|
|
21
|
-
if (parsed !== text) {
|
|
22
|
-
return stringifyModelFacing(parsed);
|
|
23
|
-
}
|
|
24
9
|
return redactSensitiveText(text);
|
|
25
10
|
}
|
|
26
|
-
export function redactModelFacingTextIfSensitive(text) {
|
|
27
|
-
return /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
|
|
28
|
-
? redactModelFacingText(text)
|
|
29
|
-
: text;
|
|
30
|
-
}
|
|
31
11
|
export function getArrayField(data, key) {
|
|
32
12
|
return Array.isArray(data[key]) ? data[key] : undefined;
|
|
33
13
|
}
|
|
@@ -64,11 +44,11 @@ export function getPageSummary(data) {
|
|
|
64
44
|
const url = typeof data.url === "string" ? data.url : undefined;
|
|
65
45
|
if (title === undefined && url === undefined)
|
|
66
46
|
return undefined;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
47
|
+
const summary = title && url ? `${title}\n${url}` : url || title || UNTITLED_PAGE_SUMMARY;
|
|
48
|
+
const webmcp = isRecord(data.webmcp) ? data.webmcp : undefined;
|
|
49
|
+
return webmcp?.available === true && typeof webmcp.toolCount === "number" && Number.isInteger(webmcp.toolCount) && webmcp.toolCount > 0
|
|
50
|
+
? `${summary}\n\nWebMCP tools are available on this page (experimental). Run webmcp list to view them.`
|
|
51
|
+
: summary;
|
|
72
52
|
}
|
|
73
53
|
export function formatCount(count, singular, plural = `${singular}s`) {
|
|
74
54
|
return `${count} ${count === 1 ? singular : plural}`;
|