pi-agent-browser-native 0.5.0 → 0.6.6
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 +144 -0
- package/README.md +75 -42
- package/dist/extensions/agent-browser/index.js +13 -83
- package/dist/extensions/agent-browser/lib/argv-grammar.js +8 -2
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +1 -1
- package/dist/extensions/agent-browser/lib/command-policy.js +4 -7
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +19 -11
- package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
- package/dist/extensions/agent-browser/lib/config.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/job.js +0 -9
- package/dist/extensions/agent-browser/lib/input-modes/params.js +1 -1
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +18 -4
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -138
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -81
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +4 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +31 -26
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +60 -8
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +1 -4
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +31 -40
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +76 -70
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +6 -8
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +5 -10
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
- package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
- package/dist/extensions/agent-browser/lib/playbook.js +13 -12
- package/dist/extensions/agent-browser/lib/process-identity.js +11 -10
- package/dist/extensions/agent-browser/lib/process.js +18 -82
- package/dist/extensions/agent-browser/lib/recording-reservations.js +11 -78
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +1 -1
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +27 -14
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +20 -2
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +11 -16
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +12 -9
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +2 -2
- package/dist/extensions/agent-browser/lib/results/presentation.js +31 -4
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +1 -1
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +3 -3
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
- package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
- package/dist/extensions/agent-browser/lib/runtime.js +73 -72
- package/dist/extensions/agent-browser/lib/session-page-state.js +12 -3
- package/dist/extensions/agent-browser/lib/temp.js +1 -2
- package/dist/extensions/agent-browser/lib/upstream-version.js +5 -5
- package/dist/extensions/agent-browser/lib/web-search.js +108 -24
- package/dist/scripts/agent-browser-target.mjs +19 -1
- package/docs/ARCHITECTURE.md +24 -20
- package/docs/COMMAND_REFERENCE.md +181 -49
- package/docs/ELECTRON.md +2 -2
- package/docs/RELEASE.md +10 -8
- package/docs/REQUIREMENTS.md +8 -7
- package/docs/SUPPORT_MATRIX.md +31 -26
- package/docs/TOOL_CONTRACT.md +89 -56
- package/package.json +1 -1
- package/scripts/agent-browser-capability-baseline.mjs +65 -5
- package/scripts/agent-browser-target.mjs +19 -1
- package/scripts/config.mjs +1 -0
- package/scripts/doctor.mjs +15 -9
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -601
- package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
|
@@ -4,12 +4,11 @@ import { lstat, mkdir, readdir } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, isAbsolute, join } from "node:path";
|
|
5
5
|
import { env as processEnv, platform as processPlatform } from "node:process";
|
|
6
6
|
import { parseArgvDescriptor } from "./argv-descriptor.js";
|
|
7
|
-
import { needsManagedSession } from "./command-policy.js";
|
|
8
7
|
import { isKnownCommandToken } from "./command-taxonomy.js";
|
|
9
8
|
import { extractExplicitSessionName, getFlagName, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, resolveAgentBrowserNamespace, } from "./argv-grammar.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { getImplicitSessionIdleTimeoutMs
|
|
9
|
+
import { commitManagedSessionRestoreSuppression, getManagedSessionRestoreEnv, getManagedSessionRestoreProtectedEnv, getOwnedManagedSessionCompatibilityEnv, getOwnedManagedSessionNamespaceEnv, isOwnedManagedSessionTarget, validateManagedSessionRestoreContextForSpawn, } from "./managed-session-restore.js";
|
|
10
|
+
import { getPageTargetValidationError, } from "./page-target-validation.js";
|
|
11
|
+
import { getImplicitSessionIdleTimeoutMs } from "./runtime.js";
|
|
13
12
|
import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
|
|
14
13
|
import { openSecureTempFile, writeSecureTempChunk } from "./temp.js";
|
|
15
14
|
const MAX_BUFFERED_STDOUT_BYTES = 512 * 1_024;
|
|
@@ -17,7 +16,6 @@ const MAX_BUFFERED_STDERR_CHARS = 32_000;
|
|
|
17
16
|
const MAX_BUFFERED_STDOUT_TAIL_CHARS = 32_000;
|
|
18
17
|
const PROCESS_STDOUT_SPILL_FILE_PREFIX = "process-stdout";
|
|
19
18
|
const AGENT_BROWSER_SOCKET_DIR_ENV = "AGENT_BROWSER_SOCKET_DIR";
|
|
20
|
-
const AGENT_BROWSER_ARGS_ENV = "AGENT_BROWSER_ARGS";
|
|
21
19
|
const AGENT_BROWSER_DEFAULT_TIMEOUT_ENV = "AGENT_BROWSER_DEFAULT_TIMEOUT";
|
|
22
20
|
const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
|
|
23
21
|
const PI_AGENT_BROWSER_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_PROCESS_TIMEOUT_MS";
|
|
@@ -29,12 +27,11 @@ const DEFAULT_AGENT_BROWSER_PROCESS_TIMEOUT_MS = 35_000;
|
|
|
29
27
|
/** Grace period after `exit` before resolving when `close` is delayed by inherited stdio handles. */
|
|
30
28
|
const EXIT_STDIO_GRACE_MS = 100;
|
|
31
29
|
const WINDOWS_AGENT_BROWSER_MISSING_MARKER = "PI_AGENT_BROWSER_COMMAND_NOT_FOUND:agent-browser.cmd";
|
|
32
|
-
const UNTRUSTED_LOCAL_FILE_SESSION_MESSAGE = "Local file navigation requires a wrapper-managed local browser because caller-owned or attached sessions can retain unsafe file-access launch flags. Omit the explicit session or attachment and retry with sessionMode fresh.";
|
|
33
30
|
const attachedBrowserSessionContext = new AsyncLocalStorage();
|
|
34
31
|
const WINDOWS_COMMANDS_WITH_ADJACENT_SUBCOMMAND = new Set([
|
|
35
32
|
"auth", "clipboard", "cookies", "dashboard", "device", "dialog", "diff", "find", "get", "is", "keyboard",
|
|
36
33
|
"mouse", "network", "plugin", "profiler", "react", "record", "session", "set", "skills", "state", "storage",
|
|
37
|
-
"stream", "tab", "trace", "window",
|
|
34
|
+
"stream", "tab", "trace", "webmcp", "window",
|
|
38
35
|
]);
|
|
39
36
|
export function withAttachedBrowserSessionContext(preserve, run) {
|
|
40
37
|
return attachedBrowserSessionContext.run(preserve || attachedBrowserSessionContext.getStore() === true, run);
|
|
@@ -111,27 +108,10 @@ export function reorderWindowsLeadingGlobalArgs(args) {
|
|
|
111
108
|
}
|
|
112
109
|
return args;
|
|
113
110
|
}
|
|
114
|
-
export function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (token.startsWith("--allow-file-access="))
|
|
119
|
-
continue;
|
|
120
|
-
if (token === "--allow-file-access") {
|
|
121
|
-
if (["false", "true"].includes(args[index + 1] ?? ""))
|
|
122
|
-
index += 1;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
filtered.push(token);
|
|
126
|
-
}
|
|
127
|
-
// These are launch-only controls. Sending them on an attached-session follow-up makes upstream replace the CDP connection with a local browser.
|
|
128
|
-
if (preserveAttachedBrowserSession)
|
|
129
|
-
return filtered;
|
|
130
|
-
// Upstream's flag overrides only the active CDP target; the Chrome arg covers new tabs. Its --args parser splits commas/newlines.
|
|
131
|
-
const browserArgs = wrapperCompatibilityUserAgent
|
|
132
|
-
? ["--args", `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`]
|
|
133
|
-
: [];
|
|
134
|
-
return [...browserArgs, "--allow-file-access", "false", ...filtered];
|
|
111
|
+
export function prepareAgentBrowserSpawnArgs(args, wrapperCompatibilityUserAgent, preserveAttachedBrowserSession = false) {
|
|
112
|
+
if (preserveAttachedBrowserSession || !wrapperCompatibilityUserAgent)
|
|
113
|
+
return args;
|
|
114
|
+
return ["--args", `--user-agent=${wrapperCompatibilityUserAgent.replaceAll(/[\r\n,]/g, "")}`, ...args];
|
|
135
115
|
}
|
|
136
116
|
export function buildAgentBrowserSpawnCommand(args, platform = processPlatform) {
|
|
137
117
|
if (platform !== "win32") {
|
|
@@ -384,54 +364,27 @@ export function buildAgentBrowserProcessEnv(baseEnv = processEnv, overrides = un
|
|
|
384
364
|
clampUpstreamDefaultTimeout(childEnv);
|
|
385
365
|
return childEnv;
|
|
386
366
|
}
|
|
387
|
-
function getManagedPreSpawnPolicyError(options,
|
|
388
|
-
const policyEnv = effectiveEnv ?? { ...(options.parentEnv ?? processEnv), ...options.env };
|
|
389
|
-
const managedSessionTargetError = getManagedSessionTargetAccessValidationError(options.args, allowManagedSessionTarget || options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args), policyEnv);
|
|
390
|
-
if (managedSessionTargetError)
|
|
391
|
-
return managedSessionTargetError;
|
|
367
|
+
function getManagedPreSpawnPolicyError(options, currentPageUrl, pageUrlUnknown = false, trustedFirstBatchTabSelection = false) {
|
|
392
368
|
if (!validateManagedSessionRestoreContextForSpawn(options)) {
|
|
393
369
|
return "Managed session restore policy, storage, or checkout identity changed after planning; refusing to start agent-browser.";
|
|
394
370
|
}
|
|
395
|
-
return
|
|
371
|
+
return getPageTargetValidationError({
|
|
396
372
|
args: options.args,
|
|
397
373
|
currentPageUrl,
|
|
398
|
-
cwd: options.cwd,
|
|
399
|
-
env: effectiveEnv ?? options.env,
|
|
400
|
-
managedSessionRestoreKey: getOwnedManagedSessionRestoreKey(),
|
|
401
374
|
pageUrlUnknown,
|
|
402
|
-
parentEnv: effectiveEnv ? {} : options.parentEnv ?? processEnv,
|
|
403
375
|
stdin: options.stdin,
|
|
404
376
|
trustedFirstBatchTabSelection,
|
|
405
|
-
trustedPinnedEmptyConfig,
|
|
406
377
|
});
|
|
407
378
|
}
|
|
408
379
|
export async function runAgentBrowserProcess(options) {
|
|
409
|
-
const {
|
|
380
|
+
const { cwd, env, managedSessionRestoreState, managedStateCurrentPageUrl, managedStatePageUrlUnknown, signal, stdin, trustedFirstBatchTabSelection } = options;
|
|
410
381
|
const preserveAttachedBrowserSession = options.preserveAttachedBrowserSession === true || attachedBrowserSessionContext.getStore() === true;
|
|
411
382
|
const ownedManagedSession = options.ownedManagedSession === true || isOwnedManagedSessionTarget(options.args);
|
|
412
|
-
const args =
|
|
413
|
-
args: options.args,
|
|
414
|
-
cwd,
|
|
415
|
-
env,
|
|
416
|
-
ownedManagedSession,
|
|
417
|
-
restoreState: managedSessionRestoreState,
|
|
418
|
-
stdin,
|
|
419
|
-
});
|
|
383
|
+
const args = options.args;
|
|
420
384
|
const timeoutMs = options.timeoutMs ?? getAgentBrowserProcessTimeoutMs();
|
|
421
385
|
if (signal?.aborted) {
|
|
422
386
|
return { aborted: true, agentBrowserStarted: false, exitCode: 1, stderr: "", stdout: "", timedOut: false };
|
|
423
387
|
}
|
|
424
|
-
if (invocationMayNavigateToLocalFile(args, stdin) && (!ownedManagedSession || preserveAttachedBrowserSession)) {
|
|
425
|
-
return {
|
|
426
|
-
aborted: false,
|
|
427
|
-
agentBrowserStarted: false,
|
|
428
|
-
exitCode: 1,
|
|
429
|
-
spawnError: new Error(UNTRUSTED_LOCAL_FILE_SESSION_MESSAGE),
|
|
430
|
-
stderr: "",
|
|
431
|
-
stdout: "",
|
|
432
|
-
timedOut: false,
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
388
|
const parentEnv = getAgentBrowserProcessEnvironment();
|
|
436
389
|
const managedSessionRestoreOptions = {
|
|
437
390
|
args,
|
|
@@ -442,7 +395,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
442
395
|
restoreState: managedSessionRestoreState,
|
|
443
396
|
stdin,
|
|
444
397
|
};
|
|
445
|
-
const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions,
|
|
398
|
+
const planningPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
|
|
446
399
|
if (planningPolicyError) {
|
|
447
400
|
return {
|
|
448
401
|
aborted: false,
|
|
@@ -455,37 +408,18 @@ export async function runAgentBrowserProcess(options) {
|
|
|
455
408
|
};
|
|
456
409
|
}
|
|
457
410
|
const managedSessionRestoreEnv = getManagedSessionRestoreEnv(managedSessionRestoreOptions);
|
|
458
|
-
const ownedManagedSessionClose = shouldOmitOwnedManagedSessionRestoreEnv(managedSessionRestoreOptions);
|
|
459
|
-
const browserConfigPinRequired = !isPlainTextInspectionArgs(args) && needsManagedSession(parseArgvDescriptor(args));
|
|
460
|
-
const managedSessionRestoreConfigEnv = await getManagedSessionRestoreConfigEnv(managedSessionRestoreEnv, ownedManagedSessionClose || browserConfigPinRequired);
|
|
461
|
-
if (managedSessionRestoreConfigEnv === undefined) {
|
|
462
|
-
return {
|
|
463
|
-
aborted: false,
|
|
464
|
-
agentBrowserStarted: false,
|
|
465
|
-
exitCode: 1,
|
|
466
|
-
spawnError: new Error("Browser-backed agent-browser commands require a protected empty config, but secure temp storage was unavailable."),
|
|
467
|
-
stderr: "",
|
|
468
|
-
stdout: "",
|
|
469
|
-
timedOut: false,
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
411
|
const ownedManagedSessionCompatibilityEnv = getOwnedManagedSessionCompatibilityEnv(managedSessionRestoreOptions);
|
|
473
412
|
const processOverrides = {
|
|
474
413
|
[AGENT_BROWSER_IDLE_TIMEOUT_ENV]: String(getImplicitSessionIdleTimeoutMs()),
|
|
475
414
|
...managedSessionRestoreEnv,
|
|
476
415
|
...env,
|
|
477
|
-
...managedSessionRestoreConfigEnv,
|
|
478
416
|
...getManagedSessionRestoreProtectedEnv(managedSessionRestoreOptions, managedSessionRestoreEnv),
|
|
479
417
|
...getOwnedManagedSessionNamespaceEnv(managedSessionRestoreOptions),
|
|
480
418
|
...getWindowsExplicitDefaultNamespaceEnv(args, parentEnv.AGENT_BROWSER_NAMESPACE),
|
|
481
419
|
...ownedManagedSessionCompatibilityEnv,
|
|
482
|
-
AGENT_BROWSER_ALLOW_FILE_ACCESS: undefined,
|
|
483
|
-
[AGENT_BROWSER_ARGS_ENV]: undefined,
|
|
484
420
|
};
|
|
485
421
|
const explicitSocketDir = processOverrides[AGENT_BROWSER_SOCKET_DIR_ENV];
|
|
486
422
|
let effectiveEnv = explicitSocketDir === undefined ? { ...processOverrides, [AGENT_BROWSER_SOCKET_DIR_ENV]: undefined } : processOverrides;
|
|
487
|
-
if (ownedManagedSessionClose)
|
|
488
|
-
effectiveEnv = { ...effectiveEnv, AGENT_BROWSER_RESTORE: undefined };
|
|
489
423
|
const requestedSocketDir = explicitSocketDir ?? parentEnv[PI_AGENT_BROWSER_SOCKET_DIR_ENV] ?? getAgentBrowserSocketDir();
|
|
490
424
|
if (requestedSocketDir !== undefined) {
|
|
491
425
|
const socketDirError = requestedSocketDir.length > 0
|
|
@@ -522,6 +456,7 @@ export async function runAgentBrowserProcess(options) {
|
|
|
522
456
|
let stdoutTail = "";
|
|
523
457
|
let stdoutSpillHandle;
|
|
524
458
|
let stdoutSpillPath;
|
|
459
|
+
let stdoutSpillPending = false;
|
|
525
460
|
let pendingStdoutWrite = Promise.resolve();
|
|
526
461
|
let stdoutSpillError;
|
|
527
462
|
let killTimer;
|
|
@@ -533,11 +468,12 @@ export async function runAgentBrowserProcess(options) {
|
|
|
533
468
|
stdoutTail = appendTail(stdoutTail, buffer.toString("utf8"), MAX_BUFFERED_STDOUT_TAIL_CHARS);
|
|
534
469
|
if (stdoutSpillError)
|
|
535
470
|
return;
|
|
536
|
-
if (!stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
|
|
471
|
+
if (!stdoutSpillPending && !stdoutSpillPath && stdoutBufferedBytes + buffer.length <= MAX_BUFFERED_STDOUT_BYTES) {
|
|
537
472
|
stdoutBuffers.push(buffer);
|
|
538
473
|
stdoutBufferedBytes += buffer.length;
|
|
539
474
|
return;
|
|
540
475
|
}
|
|
476
|
+
stdoutSpillPending = true;
|
|
541
477
|
pendingStdoutWrite = pendingStdoutWrite
|
|
542
478
|
.then(async () => {
|
|
543
479
|
if (stdoutSpillError)
|
|
@@ -612,12 +548,12 @@ export async function runAgentBrowserProcess(options) {
|
|
|
612
548
|
});
|
|
613
549
|
};
|
|
614
550
|
const childEnv = buildAgentBrowserProcessEnv(parentEnv, effectiveEnv);
|
|
615
|
-
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions,
|
|
551
|
+
const spawnPolicyError = getManagedPreSpawnPolicyError(managedSessionRestoreOptions, managedStateCurrentPageUrl, managedStatePageUrlUnknown, trustedFirstBatchTabSelection);
|
|
616
552
|
if (spawnPolicyError) {
|
|
617
553
|
resolve({ aborted: false, agentBrowserStarted: false, exitCode: 1, spawnError: new Error(spawnPolicyError), stderr: "", stdout: "", timedOut: false });
|
|
618
554
|
return;
|
|
619
555
|
}
|
|
620
|
-
const spawnCommand = buildAgentBrowserSpawnCommand(
|
|
556
|
+
const spawnCommand = buildAgentBrowserSpawnCommand(prepareAgentBrowserSpawnArgs(args, ownedManagedSessionCompatibilityEnv.AGENT_BROWSER_USER_AGENT, preserveAttachedBrowserSession));
|
|
621
557
|
const child = spawn(spawnCommand.command, spawnCommand.args, {
|
|
622
558
|
cwd,
|
|
623
559
|
env: childEnv,
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
|
|
3
|
-
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
|
|
4
|
-
import { isCloseAllCommand, isCloseCommand } from "./command-taxonomy.js";
|
|
1
|
+
import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
|
|
5
2
|
import { isRecord } from "./parsing.js";
|
|
6
|
-
import { isPendingRecordingArtifact
|
|
3
|
+
import { isPendingRecordingArtifact } from "./results/artifact-manifest.js";
|
|
7
4
|
export const RECORDING_RESERVATION_ENTRY_TYPE = "agent-browser-recording-reservation";
|
|
8
5
|
function getReservationKey(reservation) {
|
|
9
6
|
return getAgentBrowserSessionIdentityKey(reservation.sessionName, reservation.namespace);
|
|
@@ -100,83 +97,19 @@ export function restoreRecordingReservationStateFromBranch(branch) {
|
|
|
100
97
|
const reservations = new Map();
|
|
101
98
|
const terminal = new Map();
|
|
102
99
|
for (const entry of branch) {
|
|
103
|
-
if (!isRecord(entry))
|
|
100
|
+
if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== RECORDING_RESERVATION_ENTRY_TYPE)
|
|
104
101
|
continue;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!transition)
|
|
108
|
-
continue;
|
|
109
|
-
const key = getReservationKey(transition.reservation);
|
|
110
|
-
if (transition.state === "active") {
|
|
111
|
-
reservations.set(key, transition.reservation);
|
|
112
|
-
terminal.delete(key);
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
reservations.delete(key);
|
|
116
|
-
terminal.set(key, transition.reservation);
|
|
117
|
-
}
|
|
102
|
+
const transition = parseReservationTransition(entry.data);
|
|
103
|
+
if (!transition)
|
|
118
104
|
continue;
|
|
105
|
+
const key = getReservationKey(transition.reservation);
|
|
106
|
+
if (transition.state === "active") {
|
|
107
|
+
reservations.set(key, transition.reservation);
|
|
108
|
+
terminal.delete(key);
|
|
119
109
|
}
|
|
120
|
-
|
|
121
|
-
continue;
|
|
122
|
-
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
123
|
-
const details = message?.toolName === "agent_browser" && isRecord(message.details) ? message.details : undefined;
|
|
124
|
-
if (!details)
|
|
125
|
-
continue;
|
|
126
|
-
if (isSessionArtifactManifest(details.artifactManifest)) {
|
|
127
|
-
const artifacts = [...details.artifactManifest.entries]
|
|
128
|
-
.sort((left, right) => left.createdAtMs - right.createdAtMs
|
|
129
|
-
|| Number(isPendingRecordingCommand(left.command, left.subcommand, left.kind)) - Number(isPendingRecordingCommand(right.command, right.subcommand, right.kind))
|
|
130
|
-
|| left.path.localeCompare(right.path))
|
|
131
|
-
.filter((manifestEntry) => manifestEntry.command === "record" && manifestEntry.kind === "video" && manifestEntry.session)
|
|
132
|
-
.map((manifestEntry) => ({
|
|
133
|
-
absolutePath: manifestEntry.absolutePath ?? manifestEntry.path,
|
|
134
|
-
command: manifestEntry.command,
|
|
135
|
-
cwd: manifestEntry.cwd,
|
|
136
|
-
kind: "video",
|
|
137
|
-
namespace: manifestEntry.namespace,
|
|
138
|
-
path: manifestEntry.path,
|
|
139
|
-
session: manifestEntry.session,
|
|
140
|
-
status: isPendingRecordingCommand(manifestEntry.command, manifestEntry.subcommand, "video") ? "pending" : "saved",
|
|
141
|
-
subcommand: manifestEntry.subcommand,
|
|
142
|
-
}));
|
|
143
|
-
for (const artifact of artifacts) {
|
|
144
|
-
const reservation = getArtifactReservation(artifact);
|
|
145
|
-
if (!reservation)
|
|
146
|
-
continue;
|
|
147
|
-
const key = getReservationKey(reservation);
|
|
148
|
-
applyRecordingArtifactsToReservations(reservations, [artifact]);
|
|
149
|
-
if (isPendingRecordingArtifact(artifact))
|
|
150
|
-
terminal.delete(key);
|
|
151
|
-
else
|
|
152
|
-
terminal.set(key, reservation);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
const messageSucceeded = typeof message?.isError === "boolean"
|
|
156
|
-
? !message.isError
|
|
157
|
-
: typeof details.exitCode !== "number" || details.exitCode === 0;
|
|
158
|
-
const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
|
|
159
|
-
const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
|
|
160
|
-
const closeAllApplied = details.closeAllApplied === true
|
|
161
|
-
|| (messageSucceeded && isCloseAllCommand(extractUpstreamCommandTokens(args)))
|
|
162
|
-
|| batchHasSuccessfulCloseAll(details.batchSteps);
|
|
163
|
-
if (closeAllApplied) {
|
|
164
|
-
for (const [key, reservation] of reservations) {
|
|
165
|
-
if (!isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
166
|
-
continue;
|
|
167
|
-
terminal.set(key, reservation);
|
|
168
|
-
reservations.delete(key);
|
|
169
|
-
}
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
const directCloseSucceeded = messageSucceeded && isCloseCommand(typeof details.command === "string" ? details.command : undefined);
|
|
173
|
-
const batchRecordingClosed = getSuccessfulBatchCloseLifecycle(details.batchSteps)?.recordingClosedAfterBatch === true;
|
|
174
|
-
if ((directCloseSucceeded || batchRecordingClosed) && typeof details.sessionName === "string") {
|
|
175
|
-
const key = getAgentBrowserSessionIdentityKey(details.sessionName, namespace);
|
|
176
|
-
const reservation = reservations.get(key);
|
|
177
|
-
if (reservation)
|
|
178
|
-
terminal.set(key, reservation);
|
|
110
|
+
else {
|
|
179
111
|
reservations.delete(key);
|
|
112
|
+
terminal.set(key, transition.reservation);
|
|
180
113
|
}
|
|
181
114
|
}
|
|
182
115
|
return { active: reservations, terminal };
|
|
@@ -106,7 +106,7 @@ export function buildAgentBrowserNextActions(options) {
|
|
|
106
106
|
reason: "Inspect the opened page before choosing interactive refs.",
|
|
107
107
|
}));
|
|
108
108
|
}
|
|
109
|
-
else if (isPageMutationCommand(options.command)) {
|
|
109
|
+
else if (isPageMutationCommand(options.command, options.subcommand)) {
|
|
110
110
|
actions.push(buildNextToolAction({
|
|
111
111
|
args: ["snapshot", "-i"],
|
|
112
112
|
id: "inspect-after-mutation",
|
|
@@ -3,12 +3,13 @@ import { isRecord } from "../../parsing.js";
|
|
|
3
3
|
import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
|
|
4
4
|
import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
|
|
5
5
|
import { buildAgentBrowserNextActions } from "../action-recommendations.js";
|
|
6
|
+
import { buildPendingWebMcpNextActions } from "../recovery-next-actions.js";
|
|
6
7
|
import { formatSessionArtifactRetentionSummary, isPendingRecordingArtifact } from "../artifact-manifest.js";
|
|
7
8
|
import { classifyAgentBrowserFailureCategory } from "../categories.js";
|
|
8
9
|
import { detectConfirmationRequired } from "../confirmation.js";
|
|
9
10
|
import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../network-routes.js";
|
|
10
11
|
import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, withOptionalSessionArgs } from "../next-actions.js";
|
|
11
|
-
import { stringifyModelFacing } from "./common.js";
|
|
12
|
+
import { extractAgentBrowserLifecycle, stringifyModelFacing } from "./common.js";
|
|
12
13
|
import { buildArtifactVerificationSummary, classifyPresentationSuccessCategory, manifestHasNewNoticeWorthyEntries } from "./artifacts.js";
|
|
13
14
|
import { formatBatchStepCommand, getPresentationImages, getPresentationPaths, getPresentationText, isStringArray } from "./content.js";
|
|
14
15
|
import { buildPageChangeSummary } from "./navigation.js";
|
|
@@ -94,6 +95,11 @@ function redactExactValues(value, sensitiveValues) {
|
|
|
94
95
|
return value;
|
|
95
96
|
return redactSensitiveValue(Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactValues(entryValue, sensitiveValues)])));
|
|
96
97
|
}
|
|
98
|
+
export function redactBatchStepErrorData(command, value) {
|
|
99
|
+
return command?.[0] === "clipboard"
|
|
100
|
+
? redactSensitiveValue(redactClipboardPermissionErrorValue({ command: "clipboard", subcommand: command[1] }, value, getClipboardWritePayloadCandidates(command)))
|
|
101
|
+
: redactExactValues(value, getStatefulCommandSensitiveValues(command));
|
|
102
|
+
}
|
|
97
103
|
function getTypedTextLength(command) {
|
|
98
104
|
return command?.[0] === "keyboard" && command[1] === "type" && typeof command[2] === "string"
|
|
99
105
|
? Array.from(command[2]).length
|
|
@@ -187,11 +193,9 @@ async function buildBatchStepPresentation(options) {
|
|
|
187
193
|
const command = isStringArray(item.command) ? item.command : undefined;
|
|
188
194
|
const redactedCommand = command ? redactInvocationArgs(command) : undefined;
|
|
189
195
|
const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
|
|
190
|
-
const lifecycle =
|
|
196
|
+
const lifecycle = extractAgentBrowserLifecycle(item.result);
|
|
191
197
|
if (item.success === false) {
|
|
192
|
-
const redactedErrorData = command
|
|
193
|
-
? redactSensitiveValue(redactClipboardPermissionErrorValue({ command: "clipboard", subcommand: command[1] }, item.error, getClipboardWritePayloadCandidates(command)))
|
|
194
|
-
: redactExactValues(item.error, getStatefulCommandSensitiveValues(command));
|
|
198
|
+
const redactedErrorData = redactBatchStepErrorData(command, item.error);
|
|
195
199
|
const errorText = formatBatchStepError(redactedErrorData);
|
|
196
200
|
const failureCategory = classifyAgentBrowserFailureCategory({
|
|
197
201
|
args: command,
|
|
@@ -206,6 +210,7 @@ async function buildBatchStepPresentation(options) {
|
|
|
206
210
|
failureCategory,
|
|
207
211
|
resultCategory: "failure",
|
|
208
212
|
sessionName,
|
|
213
|
+
subcommand: command?.[1],
|
|
209
214
|
})), namespace);
|
|
210
215
|
const presentation = {
|
|
211
216
|
content: [{ type: "text", text: errorText }],
|
|
@@ -262,7 +267,12 @@ async function buildBatchStepPresentation(options) {
|
|
|
262
267
|
});
|
|
263
268
|
const text = getPresentationText(presentation) || presentation.summary;
|
|
264
269
|
const stepSucceeded = presentation.resultCategory !== "failure";
|
|
265
|
-
const
|
|
270
|
+
const pendingWebMcpMutation = stepSucceeded
|
|
271
|
+
&& commandInfo.command === "webmcp"
|
|
272
|
+
&& ["invoke", "result"].includes(commandInfo.subcommand ?? "")
|
|
273
|
+
&& isRecord(presentation.data)
|
|
274
|
+
&& presentation.data.status === "pending";
|
|
275
|
+
const nextActions = applyNamespaceToNextActions(pendingWebMcpMutation ? buildPendingWebMcpNextActions(sessionName) : presentation.nextActions ?? buildAgentBrowserNextActions({
|
|
266
276
|
artifacts: presentation.artifacts,
|
|
267
277
|
args: command,
|
|
268
278
|
command: command?.[0],
|
|
@@ -270,6 +280,7 @@ async function buildBatchStepPresentation(options) {
|
|
|
270
280
|
resultCategory: stepSucceeded ? "success" : "failure",
|
|
271
281
|
savedFilePath: presentation.savedFilePath,
|
|
272
282
|
sessionName,
|
|
283
|
+
subcommand: command?.[1],
|
|
273
284
|
successCategory: presentation.successCategory,
|
|
274
285
|
}), namespace);
|
|
275
286
|
const pageChangeSummary = buildPageChangeSummary({
|
|
@@ -308,12 +319,6 @@ async function buildBatchStepPresentation(options) {
|
|
|
308
319
|
presentation,
|
|
309
320
|
};
|
|
310
321
|
}
|
|
311
|
-
function extractBatchStepLifecycle(result) {
|
|
312
|
-
if (!isRecord(result) || !isRecord(result.lifecycle) || !isRecord(result.lifecycle.effectiveLaunch))
|
|
313
|
-
return undefined;
|
|
314
|
-
const browserLaunched = result.lifecycle.effectiveLaunch.browserLaunched;
|
|
315
|
-
return typeof browserLaunched === "boolean" ? { effectiveLaunch: { browserLaunched } } : undefined;
|
|
316
|
-
}
|
|
317
322
|
function abandonedRecordingArtifact(artifact) {
|
|
318
323
|
const { recordingState: _recordingState, willExistOnStop: _willExistOnStop, ...terminal } = artifact;
|
|
319
324
|
return { ...terminal, exists: false, status: "missing", subcommand: "close-abandoned" };
|
|
@@ -394,6 +399,10 @@ export async function buildBatchPresentation(options) {
|
|
|
394
399
|
const redactedBatchData = steps.map(({ details }) => (details.success
|
|
395
400
|
? { command: details.command, result: details.data, success: true }
|
|
396
401
|
: { command: details.command, error: details.text, success: false }));
|
|
402
|
+
const unverifiedMutationCount = steps.filter((step) => step.details.pageChangeSummary?.changeType === "mutation" && step.details.pageChangeSummary.observed === false).length;
|
|
403
|
+
const mutationEvidenceText = unverifiedMutationCount > 0
|
|
404
|
+
? `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.`
|
|
405
|
+
: undefined;
|
|
397
406
|
const stepText = formatBatchStepsText(steps);
|
|
398
407
|
const batchSummary = batchFailure === undefined
|
|
399
408
|
? summary
|
|
@@ -405,7 +414,7 @@ export async function buildBatchPresentation(options) {
|
|
|
405
414
|
`First failing step: ${batchFailure.failedStep.index + 1} — ${batchFailure.failedStep.commandText}`,
|
|
406
415
|
batchFailure.failureCount > 1 ? `${batchFailure.failureCount} steps failed. See the per-step results below.` : "See the per-step results below.",
|
|
407
416
|
].join("\n");
|
|
408
|
-
const text = failureHeader
|
|
417
|
+
const text = [failureHeader, mutationEvidenceText, stepText].filter((line) => line !== undefined).join("\n\n");
|
|
409
418
|
const artifactRetentionSummary = currentArtifactManifest ? formatSessionArtifactRetentionSummary(currentArtifactManifest) : undefined;
|
|
410
419
|
const contentText = artifactRetentionSummary && manifestHasNewNoticeWorthyEntries(options.artifactManifest, currentArtifactManifest)
|
|
411
420
|
? `${text}\n\n${artifactRetentionSummary}`
|
|
@@ -415,6 +424,7 @@ export async function buildBatchPresentation(options) {
|
|
|
415
424
|
? appendUniqueAgentBrowserNextActions([...(batchFailure.failedStep.nextActions ?? [])], artifactLifecycleActions)
|
|
416
425
|
: artifactLifecycleActions;
|
|
417
426
|
const changedSteps = steps.map((step) => step.details).filter((details) => details.pageChangeSummary !== undefined);
|
|
427
|
+
const observedChangeCount = changedSteps.filter((details) => details.pageChangeSummary?.observed === true).length;
|
|
418
428
|
const pageChangeSummary = artifacts.length > 0
|
|
419
429
|
? buildPageChangeSummary({
|
|
420
430
|
artifacts,
|
|
@@ -428,7 +438,10 @@ export async function buildBatchPresentation(options) {
|
|
|
428
438
|
changeType: "mutation",
|
|
429
439
|
command: "batch",
|
|
430
440
|
nextActionIds: nextActions?.map((action) => action.id),
|
|
431
|
-
|
|
441
|
+
observed: observedChangeCount > 0,
|
|
442
|
+
summary: observedChangeCount > 0
|
|
443
|
+
? `batch → ${observedChangeCount} observed change${observedChangeCount === 1 ? "" : "s"}${unverifiedMutationCount > 0 ? `; ${unverifiedMutationCount} dispatched action${unverifiedMutationCount === 1 ? "" : "s"} unverified` : ""}`
|
|
444
|
+
: `batch → ${unverifiedMutationCount} action${unverifiedMutationCount === 1 ? "" : "s"} dispatched → application change unverified`,
|
|
432
445
|
}
|
|
433
446
|
: undefined;
|
|
434
447
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRecord } from "../../parsing.js";
|
|
2
2
|
import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
|
|
3
3
|
import { stringifyUnknown, truncateText } from "../text.js";
|
|
4
4
|
const UNTITLED_PAGE_SUMMARY = "(untitled page)";
|
|
@@ -24,7 +24,7 @@ export function redactModelFacingText(text) {
|
|
|
24
24
|
return redactSensitiveText(text);
|
|
25
25
|
}
|
|
26
26
|
export function redactModelFacingTextIfSensitive(text) {
|
|
27
|
-
return
|
|
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
28
|
? redactModelFacingText(text)
|
|
29
29
|
: text;
|
|
30
30
|
}
|
|
@@ -37,6 +37,24 @@ export function getStringField(data, key) {
|
|
|
37
37
|
}
|
|
38
38
|
// `lifecycle` is upstream launch/reuse bookkeeping, never page content, so it must not be the
|
|
39
39
|
// answer an agent reads when a command has no dedicated presenter.
|
|
40
|
+
export function extractAgentBrowserLifecycle(result) {
|
|
41
|
+
if (Array.isArray(result)) {
|
|
42
|
+
let latest;
|
|
43
|
+
for (const row of result) {
|
|
44
|
+
const nested = isRecord(row) ? row.result ?? row.data ?? row : row;
|
|
45
|
+
latest = extractAgentBrowserLifecycle(nested) ?? latest;
|
|
46
|
+
}
|
|
47
|
+
return latest;
|
|
48
|
+
}
|
|
49
|
+
if (!isRecord(result))
|
|
50
|
+
return undefined;
|
|
51
|
+
if (isRecord(result.lifecycle) && isRecord(result.lifecycle.effectiveLaunch)) {
|
|
52
|
+
const browserLaunched = result.lifecycle.effectiveLaunch.browserLaunched;
|
|
53
|
+
if (typeof browserLaunched === "boolean")
|
|
54
|
+
return { effectiveLaunch: { browserLaunched } };
|
|
55
|
+
}
|
|
56
|
+
return extractAgentBrowserLifecycle(result.result ?? result.data);
|
|
57
|
+
}
|
|
40
58
|
export function omitUpstreamLifecycle(data) {
|
|
41
59
|
const { lifecycle: _lifecycle, ...rest } = data;
|
|
42
60
|
return rest;
|
|
@@ -4,7 +4,6 @@ import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifa
|
|
|
4
4
|
import { withOptionalSessionArgs } from "../next-actions.js";
|
|
5
5
|
import { stringifyUnknown, truncateText } from "../text.js";
|
|
6
6
|
import { firstLine, formatCount, getArrayField, getStringField, parseJsonPreviewString, redactModelFacingText, redactModelFacingTextIfSensitive, stringifyModelFacing, } from "./common.js";
|
|
7
|
-
import { filterCallerOwnedSessionListItems, filterCallerOwnedStateListItems, filterManagedSessionListRows, filterManagedStateListRows, } from "./managed-list-filter.js";
|
|
8
7
|
const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
|
|
9
8
|
const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
|
|
10
9
|
const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
|
|
@@ -112,7 +111,7 @@ export function formatDiagnosticSummary(commandInfo, data) {
|
|
|
112
111
|
if (commandInfo.command === "session") {
|
|
113
112
|
const sessions = getArrayField(data, "sessions");
|
|
114
113
|
if (sessions)
|
|
115
|
-
return `Sessions: ${
|
|
114
|
+
return `Sessions: ${sessions.length}`;
|
|
116
115
|
const session = getStringField(data, "session");
|
|
117
116
|
if (session)
|
|
118
117
|
return `Session: ${session}`;
|
|
@@ -170,10 +169,8 @@ export function formatDiagnosticSummary(commandInfo, data) {
|
|
|
170
169
|
}
|
|
171
170
|
if (commandInfo.command === "state") {
|
|
172
171
|
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
173
|
-
if (states)
|
|
174
|
-
|
|
175
|
-
return `States: ${visibleStates.length}`;
|
|
176
|
-
}
|
|
172
|
+
if (states)
|
|
173
|
+
return `States: ${states.length}`;
|
|
177
174
|
if (commandInfo.subcommand === "load")
|
|
178
175
|
return undefined;
|
|
179
176
|
const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "filename") ?? getStringField(data, "path") ?? commandInfo.subcommand;
|
|
@@ -261,10 +258,9 @@ export function formatDiagnosticSummary(commandInfo, data) {
|
|
|
261
258
|
function formatSessionText(data) {
|
|
262
259
|
const sessions = getArrayField(data, "sessions");
|
|
263
260
|
if (sessions) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return visibleSessions
|
|
261
|
+
if (sessions.length === 0)
|
|
262
|
+
return "No active sessions.";
|
|
263
|
+
return sessions
|
|
268
264
|
.map((item, index) => {
|
|
269
265
|
if (!isRecord(item))
|
|
270
266
|
return `${index + 1}. ${stringifyModelFacing(item)}`;
|
|
@@ -932,10 +928,9 @@ function formatStateText(data, subcommand) {
|
|
|
932
928
|
}
|
|
933
929
|
const states = getArrayField(data, "states") ?? getArrayField(data, "files");
|
|
934
930
|
if (states) {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
return visibleStates
|
|
931
|
+
if (states.length === 0)
|
|
932
|
+
return "No saved states.";
|
|
933
|
+
return states
|
|
939
934
|
.map((item, index) => {
|
|
940
935
|
if (!isRecord(item))
|
|
941
936
|
return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
|
|
@@ -979,9 +974,9 @@ export function redactPresentationData(commandInfo, data) {
|
|
|
979
974
|
if (commandInfo.command === "storage")
|
|
980
975
|
return redactStorageData(data);
|
|
981
976
|
if (commandInfo.command === "session" && commandInfo.subcommand === "list")
|
|
982
|
-
return redactStructuredPresentationValue(
|
|
977
|
+
return redactStructuredPresentationValue(data);
|
|
983
978
|
if (commandInfo.command === "state" && commandInfo.subcommand === "list")
|
|
984
|
-
return redactStructuredPresentationValue(
|
|
979
|
+
return redactStructuredPresentationValue(data);
|
|
985
980
|
if (commandInfo.command === "state" && commandInfo.subcommand === "show")
|
|
986
981
|
return redactStatefulValues(data, new Set(["value"]));
|
|
987
982
|
return redactStructuredPresentationValue(data);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isNavigationObservableCommandName, isPageChangeSummaryCommand } from "../../command-taxonomy.js";
|
|
1
|
+
import { isNavigationObservableCommandName, isOpenNavigationCommand, isPageChangeSummaryCommand } from "../../command-taxonomy.js";
|
|
2
2
|
import { isRecord } from "../../parsing.js";
|
|
3
3
|
import { detectConfirmationRequired } from "../confirmation.js";
|
|
4
4
|
import { omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
|
|
@@ -74,8 +74,8 @@ export function formatExtractionText(commandInfo, data) {
|
|
|
74
74
|
const safeOrigin = origin ? redactModelFacingText(origin) : undefined;
|
|
75
75
|
return safeOrigin && safeOrigin !== safeScalarResult ? `${safeScalarResult}\n\nOrigin: ${safeOrigin}` : safeScalarResult;
|
|
76
76
|
}
|
|
77
|
-
export function isNavigationObservableCommand(command) {
|
|
78
|
-
return isNavigationObservableCommandName(command);
|
|
77
|
+
export function isNavigationObservableCommand(command, subcommand) {
|
|
78
|
+
return isNavigationObservableCommandName(command, subcommand);
|
|
79
79
|
}
|
|
80
80
|
function isNavigationSummary(value) {
|
|
81
81
|
return isRecord(value) && (typeof value.title === "string" || typeof value.url === "string");
|
|
@@ -95,7 +95,7 @@ function getTopLevelNavigationSummary(data) {
|
|
|
95
95
|
function getNormalizedNavigationSummary(summary) {
|
|
96
96
|
const title = typeof summary?.title === "string" && summary.title.trim().length > 0 ? summary.title.trim() : undefined;
|
|
97
97
|
const url = typeof summary?.url === "string" && summary.url.trim().length > 0 ? summary.url.trim() : undefined;
|
|
98
|
-
return title || url ? { title, url } : undefined;
|
|
98
|
+
return title || url ? { title, url, ...(typeof summary?.urlChanged === "boolean" ? { urlChanged: summary.urlChanged } : {}) } : undefined;
|
|
99
99
|
}
|
|
100
100
|
export function formatNavigationSummary(summary) {
|
|
101
101
|
const normalized = getNormalizedNavigationSummary(summary);
|
|
@@ -109,20 +109,22 @@ export function buildPageChangeSummary(options) {
|
|
|
109
109
|
const { artifacts, commandInfo, data, nextActions, savedFilePath } = options;
|
|
110
110
|
const artifactCount = artifacts?.length ?? 0;
|
|
111
111
|
const navigation = isRecord(data)
|
|
112
|
-
? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command) ? getTopLevelNavigationSummary(data) : undefined))
|
|
112
|
+
? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command, commandInfo.subcommand) ? getTopLevelNavigationSummary(data) : undefined))
|
|
113
113
|
: undefined;
|
|
114
114
|
const confirmationRequired = detectConfirmationRequired(data) !== undefined;
|
|
115
|
-
if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command)) {
|
|
115
|
+
if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command, commandInfo.subcommand)) {
|
|
116
116
|
return undefined;
|
|
117
117
|
}
|
|
118
|
+
const navigationObserved = navigation && (navigation.urlChanged === true || isOpenNavigationCommand(commandInfo.command) || ["back", "forward", "pushstate", "reload"].includes(commandInfo.command ?? ""));
|
|
118
119
|
const changeType = savedFilePath || artifactCount > 0
|
|
119
120
|
? "artifact"
|
|
120
|
-
:
|
|
121
|
+
: navigationObserved
|
|
121
122
|
? "navigation"
|
|
122
123
|
: confirmationRequired
|
|
123
124
|
? "confirmation"
|
|
124
125
|
: "mutation";
|
|
125
|
-
const
|
|
126
|
+
const observed = changeType !== "mutation";
|
|
127
|
+
const parts = [commandInfo.command ?? "agent-browser", observed ? changeType : "action dispatched"];
|
|
126
128
|
if (navigation?.title)
|
|
127
129
|
parts.push(navigation.title);
|
|
128
130
|
if (navigation?.url)
|
|
@@ -136,8 +138,9 @@ export function buildPageChangeSummary(options) {
|
|
|
136
138
|
changeType,
|
|
137
139
|
...(commandInfo.command ? { command: commandInfo.command } : {}),
|
|
138
140
|
...(nextActions ? { nextActionIds: nextActions.map((action) => action.id) } : {}),
|
|
141
|
+
observed,
|
|
139
142
|
...(savedFilePath ? { savedFilePath } : {}),
|
|
140
|
-
summary: parts.join(" → ")
|
|
143
|
+
summary: `${parts.join(" → ")}${observed ? "" : " → application change unverified"}`,
|
|
141
144
|
...(navigation?.title ? { title: navigation.title } : {}),
|
|
142
145
|
...(navigation?.url ? { url: navigation.url } : {}),
|
|
143
146
|
};
|