pi-agent-browser-native 0.6.10 → 0.6.12
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 +43 -0
- package/README.md +50 -7
- package/dist/extensions/agent-browser/index.js +416 -430
- package/dist/extensions/agent-browser/lib/argv-grammar.js +1 -1
- package/dist/extensions/agent-browser/lib/command-policy.js +41 -2
- package/dist/extensions/agent-browser/lib/input-modes/job.js +7 -7
- 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 +40 -10
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +3 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +8 -2
- 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 +25 -14
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +56 -10
- 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 +3 -3
- 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 +10 -9
- 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 +60 -29
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +19 -8
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +0 -20
- 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 +23 -7
- package/dist/extensions/agent-browser/lib/temp.js +4 -0
- package/docs/ARCHITECTURE.md +27 -11
- package/docs/COMMAND_REFERENCE.md +37 -17
- package/docs/RELEASE.md +3 -1
- package/docs/SUPPORT_MATRIX.md +10 -4
- package/docs/TOOL_CONTRACT.md +87 -25
- package/docs/platform-smoke.md +9 -1
- package/package.json +1 -1
- package/scripts/prepare.mjs +2 -4
|
@@ -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
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isRecord } from "../../parsing.js";
|
|
2
|
-
import {
|
|
2
|
+
import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
|
|
3
3
|
import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifactNoiseRequest, summarizeNetworkFailures } from "../network.js";
|
|
4
4
|
import { withOptionalSessionArgs } from "../next-actions.js";
|
|
5
5
|
import { stringifyUnknown, truncateText } from "../text.js";
|
|
6
|
-
import { firstLine, formatCount, getArrayField, getStringField,
|
|
6
|
+
import { firstLine, formatCount, getArrayField, getStringField, redactModelFacingText, stringifyModelFacing, } from "./common.js";
|
|
7
7
|
const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
|
|
8
8
|
const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
|
|
9
9
|
const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
|
|
@@ -185,11 +185,11 @@ export function formatDiagnosticSummary(commandInfo, data) {
|
|
|
185
185
|
}
|
|
186
186
|
if (commandInfo.subcommand === "route") {
|
|
187
187
|
const routed = getStringField(data, "routed") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
|
|
188
|
-
return routed ? `Network route: ${
|
|
188
|
+
return routed ? `Network route: ${redactSensitiveText(routed)}` : "Network route configured";
|
|
189
189
|
}
|
|
190
190
|
if (commandInfo.subcommand === "unroute") {
|
|
191
191
|
const unrouted = getStringField(data, "unrouted") ?? getStringField(data, "url") ?? getStringField(data, "pattern");
|
|
192
|
-
return unrouted ? `Network unroute: ${
|
|
192
|
+
return unrouted ? `Network unroute: ${redactSensitiveText(unrouted)}` : "Network route removed";
|
|
193
193
|
}
|
|
194
194
|
if (commandInfo.subcommand === "har") {
|
|
195
195
|
const state = getStringField(data, "state") ?? getStringField(data, "status") ?? commandInfo.subcommand;
|
|
@@ -273,14 +273,33 @@ function formatSessionText(data) {
|
|
|
273
273
|
const metadata = [
|
|
274
274
|
`active=${active ? "true" : "false"}`,
|
|
275
275
|
label ? `label=${redactModelFacingText(label)}` : undefined,
|
|
276
|
-
title ? `title=${
|
|
277
|
-
url ? `url=${
|
|
276
|
+
title ? `title=${redactSensitiveText(title)}` : undefined,
|
|
277
|
+
url ? `url=${redactSensitiveText(url)}` : undefined,
|
|
278
278
|
tabCount,
|
|
279
279
|
].filter(Boolean).join("; ");
|
|
280
280
|
return `${index + 1}. name=${name}${active ? " *active*" : ""}; ${metadata}`;
|
|
281
281
|
})
|
|
282
282
|
.join("\n");
|
|
283
283
|
}
|
|
284
|
+
if (typeof data.active === "boolean") {
|
|
285
|
+
const runtime = data.runtime;
|
|
286
|
+
const browser = isRecord(runtime) && isRecord(runtime.browser) ? runtime.browser : undefined;
|
|
287
|
+
const identity = [
|
|
288
|
+
`Daemon: ${data.active ? "active" : "inactive"}; PID: ${data.pid ?? "unknown"}`,
|
|
289
|
+
`Browser: ${browser?.status ?? "unknown"}; alive: ${browser?.alive ?? "unknown"}; Chrome PID: ${browser?.pid ?? "unknown"}`,
|
|
290
|
+
`Exact profile: ${browser?.userDataDir ?? "unknown"}`,
|
|
291
|
+
`Native browser ownership: ${browser?.ownership ?? "unknown"}; Pi cleanup ownership: ${data.piCleanupOwnership ?? "unknown"}`,
|
|
292
|
+
].join("\n");
|
|
293
|
+
return `${redactModelFacingText(identity)}\n\n${stringifyModelFacing({
|
|
294
|
+
...Object.fromEntries(["session", "namespace", "socketDir", "active", "pid", "version", "runtimeError"].map((key) => [key, data[key]])),
|
|
295
|
+
// Native runtime also includes restore check URLs, text and code; show status/identity only.
|
|
296
|
+
runtime: isRecord(runtime) ? Object.fromEntries([
|
|
297
|
+
"session", "namespace", "socketDir", "backgroundPid", "browserLaunched", "browser", "recording", "capabilities", "pageCount", "engine", "launchHash",
|
|
298
|
+
"compatibilityStatus", "restoreKey", "restoreStatus", "restoreLoadedPath", "restoreValidationPending",
|
|
299
|
+
"restoreSave", "saveStatus", "restoreSavedPath",
|
|
300
|
+
].map((key) => [key, runtime[key]])) : runtime,
|
|
301
|
+
})}`;
|
|
302
|
+
}
|
|
284
303
|
const session = getStringField(data, "session");
|
|
285
304
|
return session ? `Current session: ${redactModelFacingText(session)}` : undefined;
|
|
286
305
|
}
|
|
@@ -315,8 +334,7 @@ function getPreviewCandidate(item, keys) {
|
|
|
315
334
|
function formatNetworkPreviewValue(value, maxChars) {
|
|
316
335
|
if (value === undefined || value === null)
|
|
317
336
|
return undefined;
|
|
318
|
-
const
|
|
319
|
-
const redacted = redactSensitiveValue(previewValue);
|
|
337
|
+
const redacted = redactSensitiveValue(value);
|
|
320
338
|
const raw = typeof redacted === "string" ? redacted : stringifyUnknown(redacted);
|
|
321
339
|
const normalized = raw.replace(/\s+/g, " ").trim();
|
|
322
340
|
if (normalized.length === 0)
|
|
@@ -797,7 +815,7 @@ function valueContainsStorageSecret(value) {
|
|
|
797
815
|
return true;
|
|
798
816
|
}
|
|
799
817
|
catch { }
|
|
800
|
-
if (redactSensitiveText(trimmed) !== trimmed
|
|
818
|
+
if (redactSensitiveText(trimmed) !== trimmed)
|
|
801
819
|
return true;
|
|
802
820
|
try {
|
|
803
821
|
return valueContainsStorageSecret(JSON.parse(trimmed));
|
|
@@ -830,13 +848,13 @@ function formatStorageValue(key, value) {
|
|
|
830
848
|
}
|
|
831
849
|
function redactStorageEntryValue(item) {
|
|
832
850
|
if (!Object.hasOwn(item, "value"))
|
|
833
|
-
return
|
|
851
|
+
return redactSensitiveValue(item);
|
|
834
852
|
const key = getStringField(item, "key") ?? getStringField(item, "name");
|
|
835
853
|
const value = item.value;
|
|
836
854
|
if (shouldRevealStorageValue(key, value))
|
|
837
|
-
return
|
|
855
|
+
return redactSensitiveValue(item);
|
|
838
856
|
return {
|
|
839
|
-
...
|
|
857
|
+
...redactSensitiveValue({ ...item, value: undefined }),
|
|
840
858
|
value: "[REDACTED]",
|
|
841
859
|
valueRedacted: true,
|
|
842
860
|
valueRedactionReason: key && STORAGE_SECRET_KEY_PATTERN.test(key) ? "sensitive-key" : "sensitive-value",
|
|
@@ -846,15 +864,15 @@ function redactStorageData(value) {
|
|
|
846
864
|
if (Array.isArray(value))
|
|
847
865
|
return value.map((item) => redactStorageData(item));
|
|
848
866
|
if (!isRecord(value))
|
|
849
|
-
return
|
|
867
|
+
return redactSensitiveValue(value);
|
|
850
868
|
const entries = Object.fromEntries(Object.entries(value).map(([key, entryValue]) => {
|
|
851
869
|
if ((key === "entries" || key === "items") && Array.isArray(entryValue))
|
|
852
|
-
return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) :
|
|
870
|
+
return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) : redactSensitiveValue(item))];
|
|
853
871
|
if (key === "value") {
|
|
854
872
|
const itemKey = getStringField(value, "key") ?? getStringField(value, "name");
|
|
855
|
-
return [key, shouldRevealStorageValue(itemKey, entryValue) ?
|
|
873
|
+
return [key, shouldRevealStorageValue(itemKey, entryValue) ? redactSensitiveValue(entryValue) : "[REDACTED]"];
|
|
856
874
|
}
|
|
857
|
-
return [key,
|
|
875
|
+
return [key, redactSensitiveValue(entryValue)];
|
|
858
876
|
}));
|
|
859
877
|
if (Object.hasOwn(value, "value")) {
|
|
860
878
|
const key = getStringField(value, "key") ?? getStringField(value, "name");
|
|
@@ -899,7 +917,7 @@ function formatDialogText(data) {
|
|
|
899
917
|
lines.push(`Type: ${redactModelFacingText(type)}`);
|
|
900
918
|
const message = getStringField(data, "message");
|
|
901
919
|
if (message)
|
|
902
|
-
lines.push(`Message: ${
|
|
920
|
+
lines.push(`Message: ${redactModelFacingText(message)}`);
|
|
903
921
|
if (data.accepted === true)
|
|
904
922
|
lines.push("Accepted.");
|
|
905
923
|
if (data.dismissed === true)
|
|
@@ -910,7 +928,7 @@ function formatFrameText(data) {
|
|
|
910
928
|
const frame = getStringField(data, "frame") ?? getStringField(data, "name") ?? getStringField(data, "selector");
|
|
911
929
|
const url = getStringField(data, "url");
|
|
912
930
|
const title = getStringField(data, "title");
|
|
913
|
-
const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${
|
|
931
|
+
const lines = [frame ? `Frame: ${redactModelFacingText(frame)}` : undefined, title ? `Title: ${redactModelFacingText(title)}` : undefined, url ? `URL: ${redactSensitiveText(url)}` : undefined].filter(Boolean);
|
|
914
932
|
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
915
933
|
}
|
|
916
934
|
function formatStateText(data, subcommand) {
|
|
@@ -933,10 +951,10 @@ function formatStateText(data, subcommand) {
|
|
|
933
951
|
return states
|
|
934
952
|
.map((item, index) => {
|
|
935
953
|
if (!isRecord(item))
|
|
936
|
-
return `${index + 1}. ${
|
|
954
|
+
return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
937
955
|
const name = getStringField(item, "name") ?? getStringField(item, "file") ?? getStringField(item, "path") ?? `(state ${index + 1})`;
|
|
938
956
|
const url = getStringField(item, "url");
|
|
939
|
-
return url ? `${index + 1}. ${redactModelFacingText(name)} — ${
|
|
957
|
+
return url ? `${index + 1}. ${redactModelFacingText(name)} — ${redactSensitiveText(url)}` : `${index + 1}. ${redactModelFacingText(name)}`;
|
|
940
958
|
})
|
|
941
959
|
.join("\n");
|
|
942
960
|
}
|
|
@@ -946,23 +964,11 @@ function formatStateText(data, subcommand) {
|
|
|
946
964
|
return "State cleared.";
|
|
947
965
|
return undefined;
|
|
948
966
|
}
|
|
949
|
-
function redactStructuredPresentationValue(value) {
|
|
950
|
-
if (typeof value === "string")
|
|
951
|
-
return redactModelFacingTextIfSensitive(value);
|
|
952
|
-
if (Array.isArray(value))
|
|
953
|
-
return value.map((item) => redactStructuredPresentationValue(item));
|
|
954
|
-
if (!isRecord(value))
|
|
955
|
-
return value;
|
|
956
|
-
return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
|
|
957
|
-
key,
|
|
958
|
-
isSensitiveFieldName(key) ? "[REDACTED]" : redactStructuredPresentationValue(entryValue),
|
|
959
|
-
]));
|
|
960
|
-
}
|
|
961
967
|
function redactStatefulValues(value, sensitiveKeys) {
|
|
962
968
|
if (Array.isArray(value))
|
|
963
969
|
return value.map((item) => redactStatefulValues(item, sensitiveKeys));
|
|
964
970
|
if (!isRecord(value))
|
|
965
|
-
return
|
|
971
|
+
return redactSensitiveValue(value);
|
|
966
972
|
return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
|
|
967
973
|
key,
|
|
968
974
|
sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : redactStatefulValues(entryValue, sensitiveKeys),
|
|
@@ -973,13 +979,9 @@ export function redactPresentationData(commandInfo, data) {
|
|
|
973
979
|
return redactStatefulValues(data, new Set(["value"]));
|
|
974
980
|
if (commandInfo.command === "storage")
|
|
975
981
|
return redactStorageData(data);
|
|
976
|
-
if (commandInfo.command === "session" && commandInfo.subcommand === "list")
|
|
977
|
-
return redactStructuredPresentationValue(data);
|
|
978
|
-
if (commandInfo.command === "state" && commandInfo.subcommand === "list")
|
|
979
|
-
return redactStructuredPresentationValue(data);
|
|
980
982
|
if (commandInfo.command === "state" && commandInfo.subcommand === "show")
|
|
981
983
|
return redactStatefulValues(data, new Set(["value"]));
|
|
982
|
-
return
|
|
984
|
+
return redactSensitiveValue(data);
|
|
983
985
|
}
|
|
984
986
|
export function formatDiagnosticText(commandInfo, data) {
|
|
985
987
|
if (commandInfo.command === "session")
|
|
@@ -203,6 +203,7 @@ export function buildErrorPresentation(options) {
|
|
|
203
203
|
overlayBlockedClick: isOverlayBlockedClickError(presentationCommand ?? commandInfo.command, safeErrorText, args ?? commandInfo.commandTokens),
|
|
204
204
|
resultCategory: "failure",
|
|
205
205
|
sessionName,
|
|
206
|
+
subcommand: commandInfo.subcommand,
|
|
206
207
|
}) ?? []),
|
|
207
208
|
];
|
|
208
209
|
return {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isNavigationObservableCommandName, isOpenNavigationCommand, isPageChangeSummaryCommand } from "../../command-taxonomy.js";
|
|
2
2
|
import { isRecord } from "../../parsing.js";
|
|
3
3
|
import { detectConfirmationRequired } from "../confirmation.js";
|
|
4
|
-
import { omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
|
|
4
|
+
import { firstLine, omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
|
|
5
5
|
const NAVIGATION_SUMMARY_FIELD = "navigationSummary";
|
|
6
6
|
const GET_RESULT_FIELDS = {
|
|
7
7
|
attr: "value",
|
|
@@ -25,7 +25,7 @@ function getScalarExtractionResult(commandInfo, data) {
|
|
|
25
25
|
if (result === null || result === undefined)
|
|
26
26
|
return "null";
|
|
27
27
|
if (typeof result === "object")
|
|
28
|
-
return JSON.stringify(result);
|
|
28
|
+
return JSON.stringify(result, null, 2);
|
|
29
29
|
return undefined;
|
|
30
30
|
}
|
|
31
31
|
function getExtractionOrigin(data) {
|
|
@@ -52,7 +52,7 @@ export function formatExtractionSummary(commandInfo, data) {
|
|
|
52
52
|
return undefined;
|
|
53
53
|
}
|
|
54
54
|
const safeScalarResult = redactModelFacingText(scalarResult);
|
|
55
|
-
const firstResultLine = safeScalarResult
|
|
55
|
+
const firstResultLine = firstLine(safeScalarResult);
|
|
56
56
|
if (commandInfo.command === "get") {
|
|
57
57
|
return `${formatGetSummaryLabel(commandInfo.subcommand)}: ${firstResultLine}`;
|
|
58
58
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { isRecord } from "../parsing.js";
|
|
2
|
+
import { buildReadConfirmationNextActions, nextReadConfirmation } from "../read-confirmation.js";
|
|
2
3
|
import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs } from "../runtime.js";
|
|
3
4
|
import { buildAgentBrowserNextActions } from "./action-recommendations.js";
|
|
4
5
|
import { buildAgentBrowserResultCategoryDetails } from "./categories.js";
|
|
5
6
|
import { detectConfirmationRequired } from "./confirmation.js";
|
|
6
7
|
import { buildSnapshotPresentation } from "./snapshot.js";
|
|
7
|
-
import {
|
|
8
|
+
import { redactModelFacingText } from "./presentation/common.js";
|
|
8
9
|
import { applyArtifactManifest, attachInlineImage, buildArtifactVerificationSummary, buildManifestEntriesForFileArtifacts, classifyPresentationSuccessCategory, extractFileArtifacts, extractImagePath, formatArtifactMetadataLines, formatArtifactSummary, formatMissingArtifactFailureText, getSavedFileDetails, hasMissingFileArtifact, isManifestFileArtifact, } from "./presentation/artifacts.js";
|
|
9
10
|
import { buildBatchPresentation, isAgentBrowserBatchResultArray, redactBatchStepErrorData } from "./presentation/batch.js";
|
|
10
11
|
import { getPresentationPaths, isStringArray } from "./presentation/content.js";
|
|
@@ -18,8 +19,7 @@ function sanitizeModelFacingPresentation(presentation) {
|
|
|
18
19
|
presentation.content = presentation.content.map((item) => {
|
|
19
20
|
if (item.type !== "text")
|
|
20
21
|
return item;
|
|
21
|
-
|
|
22
|
-
return parsed === item.text ? item : { ...item, text: stringifyModelFacing(parsed) };
|
|
22
|
+
return { ...item, text: redactModelFacingText(item.text) };
|
|
23
23
|
});
|
|
24
24
|
presentation.summary = redactModelFacingText(presentation.summary);
|
|
25
25
|
return presentation;
|
|
@@ -52,7 +52,10 @@ export async function buildToolPresentation(options) {
|
|
|
52
52
|
const { args, artifactManifest, artifactRequest, commandInfo, compiledSemanticAction, cwd, envelope, errorText, namespace, networkRouteDiagnostics, networkRoutes, persistentArtifactStore, sessionName, } = options;
|
|
53
53
|
const commandInfoWithTokens = commandInfo.commandTokens || !args ? commandInfo : { ...commandInfo, commandTokens: extractUpstreamCommandTokens(args) };
|
|
54
54
|
const presentationCommandInfo = resolvePresentationCommandInfo(commandInfoWithTokens, compiledSemanticAction);
|
|
55
|
-
|
|
55
|
+
const recordingCommand = commandInfo.command === "record";
|
|
56
|
+
const recordingBatch = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(envelope?.data)
|
|
57
|
+
&& envelope.data.some((row) => row.command?.[0] === "record");
|
|
58
|
+
if (errorText && !recordingCommand && !recordingBatch) {
|
|
56
59
|
return buildErrorPresentation({
|
|
57
60
|
args,
|
|
58
61
|
commandInfo,
|
|
@@ -61,11 +64,15 @@ export async function buildToolPresentation(options) {
|
|
|
61
64
|
sessionName,
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
|
-
|
|
67
|
+
let data = enrichStreamStatusData(commandInfoWithTokens, envelope?.data);
|
|
68
|
+
if (commandInfo.command === "session" && commandInfo.subcommand === "info" && isRecord(data)) {
|
|
69
|
+
data = { ...data, piCleanupOwnership: options.piCleanupOwnership ?? "unknown" };
|
|
70
|
+
}
|
|
71
|
+
const readConfirmation = nextReadConfirmation({ commandTokens: commandInfoWithTokens.commandTokens ?? [], data, namespace, sessionName: sessionName ?? "default", succeeded: envelope?.success !== false });
|
|
65
72
|
const presentationData = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)
|
|
66
73
|
? redactBatchSpillData(data)
|
|
67
74
|
: redactPresentationData(commandInfoWithTokens, data);
|
|
68
|
-
const artifacts = await extractFileArtifacts({ artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, namespace, sessionName });
|
|
75
|
+
const artifacts = await extractFileArtifacts({ artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, namespace, recordingOutcome: recordingCommand ? envelope?.success : undefined, recordingPending: options.recordingPending, sessionName });
|
|
69
76
|
const artifactVerification = buildArtifactVerificationSummary(artifacts);
|
|
70
77
|
const artifactSummary = formatArtifactSummary(artifacts);
|
|
71
78
|
const summary = artifactSummary ?? formatPresentationSummary(commandInfoWithTokens, data, compiledSemanticAction);
|
|
@@ -83,6 +90,7 @@ export async function buildToolPresentation(options) {
|
|
|
83
90
|
namespace,
|
|
84
91
|
networkRoutes,
|
|
85
92
|
persistentArtifactStore,
|
|
93
|
+
piCleanupOwnership: options.piCleanupOwnership,
|
|
86
94
|
sessionName,
|
|
87
95
|
summary,
|
|
88
96
|
});
|
|
@@ -99,6 +107,12 @@ export async function buildToolPresentation(options) {
|
|
|
99
107
|
summary,
|
|
100
108
|
};
|
|
101
109
|
}
|
|
110
|
+
if (errorText && (recordingCommand || recordingBatch)) {
|
|
111
|
+
const errorPresentation = buildErrorPresentation({ args, commandInfo, errorText, presentationCommand: presentationCommandInfo.command, sessionName });
|
|
112
|
+
presentation = { ...presentation, resultCategory: "failure", failureCategory: errorPresentation.failureCategory, summary: errorPresentation.summary,
|
|
113
|
+
content: [{ type: "text", text: `${errorPresentation.content[0]?.type === "text" ? errorPresentation.content[0].text : errorText}\n\n${presentation.content[0]?.type === "text" ? presentation.content[0].text : ""}` }],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
102
116
|
if (networkRouteDiagnostics && networkRouteDiagnostics.length > 0 && presentation.content[0]?.type === "text") {
|
|
103
117
|
const diagnosticText = formatNetworkRouteDiagnosticsText(networkRouteDiagnostics);
|
|
104
118
|
if (diagnosticText)
|
|
@@ -141,7 +155,7 @@ export async function buildToolPresentation(options) {
|
|
|
141
155
|
presentationWithManifest.artifactVerification = buildArtifactVerificationSummary(artifacts, presentationWithManifest.artifactManifest, currentSpillPaths) ?? presentationWithManifest.artifactVerification;
|
|
142
156
|
const confirmationRequired = detectConfirmationRequired(data);
|
|
143
157
|
const missingArtifactFailureText = formatMissingArtifactFailureText(presentationWithManifest.artifacts);
|
|
144
|
-
if (missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
|
|
158
|
+
if (!errorText && missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
|
|
145
159
|
presentationWithManifest.resultCategory = "failure";
|
|
146
160
|
presentationWithManifest.failureCategory = "artifact-missing";
|
|
147
161
|
presentationWithManifest.successCategory = undefined;
|
|
@@ -153,6 +167,21 @@ export async function buildToolPresentation(options) {
|
|
|
153
167
|
presentationWithManifest.content.unshift({ type: "text", text: missingArtifactFailureText });
|
|
154
168
|
}
|
|
155
169
|
}
|
|
170
|
+
const failedRecording = presentationWithManifest.artifacts?.find((artifact) => artifact.recording?.success === false || artifact.recording?.output.encoderSucceeded === false);
|
|
171
|
+
if (failedRecording && !errorText) {
|
|
172
|
+
const failure = `Recording failed: ${failedRecording.recording?.error ?? "native capture/encoder failure"}`;
|
|
173
|
+
presentationWithManifest.resultCategory = "failure";
|
|
174
|
+
presentationWithManifest.failureCategory = "upstream-error";
|
|
175
|
+
presentationWithManifest.successCategory = undefined;
|
|
176
|
+
presentationWithManifest.summary = failure;
|
|
177
|
+
presentationWithManifest.content.unshift({ type: "text", text: failure });
|
|
178
|
+
}
|
|
179
|
+
if (readConfirmation?.state === "pending") {
|
|
180
|
+
presentationWithManifest.readConfirmation = readConfirmation;
|
|
181
|
+
presentationWithManifest.resultCategory = "failure";
|
|
182
|
+
presentationWithManifest.failureCategory = "confirmation-required";
|
|
183
|
+
presentationWithManifest.successCategory = undefined;
|
|
184
|
+
}
|
|
156
185
|
if (!presentationWithManifest.resultCategory) {
|
|
157
186
|
const categoryDetails = buildAgentBrowserResultCategoryDetails({
|
|
158
187
|
artifacts: presentationWithManifest.artifacts,
|
|
@@ -160,7 +189,7 @@ export async function buildToolPresentation(options) {
|
|
|
160
189
|
confirmationRequired: confirmationRequired !== undefined,
|
|
161
190
|
errorText: envelope?.success === false ? presentationWithManifest.summary : undefined,
|
|
162
191
|
savedFile: presentationWithManifest.savedFile,
|
|
163
|
-
succeeded: envelope?.success !== false,
|
|
192
|
+
succeeded: envelope?.success !== false && confirmationRequired === undefined,
|
|
164
193
|
});
|
|
165
194
|
presentationWithManifest.resultCategory = categoryDetails.resultCategory;
|
|
166
195
|
presentationWithManifest.successCategory = categoryDetails.resultCategory === "success"
|
|
@@ -196,7 +225,7 @@ export async function buildToolPresentation(options) {
|
|
|
196
225
|
? buildNetworkRequestsNextActions(data, sessionName, presentationWithManifest.networkRouteDiagnostics)
|
|
197
226
|
: undefined;
|
|
198
227
|
const streamNextActions = presentationWithManifest.resultCategory === "success" ? buildStreamNextActions(commandInfoWithTokens, data, sessionName) : undefined;
|
|
199
|
-
presentationWithManifest.nextActions = mergeNextActions(presentationWithManifest.nextActions, genericNextActions, networkNextActions, streamNextActions);
|
|
228
|
+
presentationWithManifest.nextActions = readConfirmation ? buildReadConfirmationNextActions(readConfirmation, true) : mergeNextActions(presentationWithManifest.nextActions, genericNextActions, networkNextActions, streamNextActions);
|
|
200
229
|
presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
|
|
201
230
|
artifacts: presentationWithManifest.artifacts,
|
|
202
231
|
commandInfo: presentationCommandInfo,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isRecord } from "../parsing.js";
|
|
2
|
+
const RECORDING_QUALITY_WARNING = "Capture quality warning: repaint-driven capture, held/repeated or static frames, and late/final-state-only frames cannot establish UI smoothness. Output FPS is not captured-frame rate; inspect the recording and capture window before judging motion.";
|
|
3
|
+
function number(value) {
|
|
4
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
5
|
+
}
|
|
6
|
+
function text(value) {
|
|
7
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
8
|
+
}
|
|
9
|
+
export function getRecordingReceipt(value, outcome) {
|
|
10
|
+
if (!isRecord(value) || typeof value.path !== "string" || !value.path)
|
|
11
|
+
return undefined;
|
|
12
|
+
const capture = isRecord(value.capture) ? value.capture : {};
|
|
13
|
+
const output = isRecord(value.output) ? value.output : {};
|
|
14
|
+
const file = isRecord(value.file) ? value.file : {};
|
|
15
|
+
return {
|
|
16
|
+
warning: RECORDING_QUALITY_WARNING,
|
|
17
|
+
recordingId: text(value.recordingId), path: value.path,
|
|
18
|
+
success: typeof value.success === "boolean" ? value.success : value.success === null ? null : outcome ?? null,
|
|
19
|
+
error: text(value.error), frames: number(value.frames), capturedFrames: number(value.capturedFrames), fps: number(value.fps),
|
|
20
|
+
capture: {
|
|
21
|
+
startedAt: text(capture.startedAt), endedAt: text(capture.endedAt), durationMs: number(capture.durationMs),
|
|
22
|
+
firstFrameAt: text(capture.firstFrameAt), lastFrameAt: text(capture.lastFrameAt), firstFrameAfterMs: number(capture.firstFrameAfterMs), lastFrameAfterMs: number(capture.lastFrameAfterMs),
|
|
23
|
+
averageFps: number(capture.averageFps), maxFrameGapMs: number(capture.maxFrameGapMs), timestampSource: text(capture.timestampSource),
|
|
24
|
+
},
|
|
25
|
+
output: {
|
|
26
|
+
frames: number(output.frames ?? value.frames), fps: number(output.fps ?? value.fps), encodedFrames: number(output.encodedFrames),
|
|
27
|
+
durationMs: number(output.durationMs), durationSource: text(output.durationSource), heldFrames: number(output.heldFrames), droppedFrames: number(output.droppedFrames), skippedFrames: number(output.skippedFrames),
|
|
28
|
+
encoderSucceeded: typeof output.encoderSucceeded === "boolean" ? output.encoderSucceeded : null,
|
|
29
|
+
},
|
|
30
|
+
file: { exists: typeof file.exists === "boolean" ? file.exists : null, sizeBytes: number(file.sizeBytes) },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function formatRecordingReceipt(receipt) {
|
|
34
|
+
const metric = (value, unit = "") => value === null ? "unknown" : `${value}${unit}`;
|
|
35
|
+
return [
|
|
36
|
+
`Recording ID: ${metric(receipt.recordingId)}`,
|
|
37
|
+
`Native recording outcome: ${receipt.success === true ? "succeeded" : receipt.success === false ? "failed" : "pending/unknown"}${receipt.error ? ` — ${receipt.error}` : ""}`,
|
|
38
|
+
`Capture started: ${metric(receipt.capture.startedAt)}; ended: ${metric(receipt.capture.endedAt)}`,
|
|
39
|
+
`Wall-clock capture duration: ${metric(receipt.capture.durationMs, " ms")}`,
|
|
40
|
+
`Captured frames (received, not pixel-unique): ${metric(receipt.capturedFrames)}`,
|
|
41
|
+
`Captured-frame rate: ${metric(receipt.capture.averageFps, " fps")}`,
|
|
42
|
+
`First frame: ${metric(receipt.capture.firstFrameAt)}; after start: ${metric(receipt.capture.firstFrameAfterMs, " ms")}`,
|
|
43
|
+
`Last frame: ${metric(receipt.capture.lastFrameAt)}; after start: ${metric(receipt.capture.lastFrameAfterMs, " ms")}`,
|
|
44
|
+
`Maximum frame gap: ${metric(receipt.capture.maxFrameGapMs, " ms")}; timestamp source: ${metric(receipt.capture.timestampSource)}`,
|
|
45
|
+
`Written frames: ${metric(receipt.output.frames)}; Encoded frames: ${metric(receipt.output.encodedFrames)}`,
|
|
46
|
+
`Held frames: ${metric(receipt.output.heldFrames)}; dropped frames: ${metric(receipt.output.droppedFrames)}; skipped frames: ${metric(receipt.output.skippedFrames)}`,
|
|
47
|
+
`Nominal/output FPS: ${metric(receipt.output.fps)}; output duration (encoded frames/fps): ${metric(receipt.output.durationMs, " ms")}`,
|
|
48
|
+
receipt.warning,
|
|
49
|
+
].join("\n");
|
|
50
|
+
}
|