pi-agent-browser-native 0.3.0 → 0.5.0
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 +127 -0
- package/README.md +63 -20
- package/dist/extensions/agent-browser/index.js +787 -105
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
- package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
- package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
- package/dist/extensions/agent-browser/lib/playbook.js +18 -15
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
- package/dist/extensions/agent-browser/lib/process.js +131 -41
- package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
- package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
- package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
- package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
- package/dist/extensions/agent-browser/lib/runtime.js +116 -39
- package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +3 -0
- package/docs/ARCHITECTURE.md +40 -21
- package/docs/COMMAND_REFERENCE.md +90 -35
- package/docs/RELEASE.md +3 -3
- package/docs/REQUIREMENTS.md +4 -2
- package/docs/SUPPORT_MATRIX.md +26 -19
- package/docs/TOOL_CONTRACT.md +93 -52
- package/package.json +3 -1
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +24 -6
- package/scripts/agent-browser-target.mjs +3 -0
- package/scripts/build.mjs +41 -0
- package/scripts/doctor.mjs +7 -6
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const BATCH_STDIN_EXAMPLE = ' Example: { "args": ["batch"], "stdin": "[[\\"get\\",\\"title\\"],[\\"get\\",\\"url\\"]]" }';
|
|
1
2
|
// Mirror upstream commands::shell_words_split so policy inspection sees the same argv.
|
|
2
3
|
export function parseBatchCommandArgument(command) {
|
|
3
4
|
const tokens = [];
|
|
@@ -36,20 +37,20 @@ export function parseBatchCommandArgument(command) {
|
|
|
36
37
|
function validateUserBatchStep(step, index) {
|
|
37
38
|
if (!Array.isArray(step)) {
|
|
38
39
|
return {
|
|
39
|
-
error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens
|
|
40
|
+
error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.${BATCH_STDIN_EXAMPLE}`,
|
|
40
41
|
ok: false,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
if (step.length === 0) {
|
|
44
45
|
return {
|
|
45
|
-
error: `agent_browser batch stdin step ${index} must not be empty
|
|
46
|
+
error: `agent_browser batch stdin step ${index} must not be empty.${BATCH_STDIN_EXAMPLE}`,
|
|
46
47
|
ok: false,
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
50
|
const invalidTokenIndex = step.findIndex((token) => typeof token !== "string");
|
|
50
51
|
if (invalidTokenIndex !== -1) {
|
|
51
52
|
return {
|
|
52
|
-
error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string
|
|
53
|
+
error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.${BATCH_STDIN_EXAMPLE}`,
|
|
53
54
|
ok: false,
|
|
54
55
|
};
|
|
55
56
|
}
|
|
@@ -62,13 +63,13 @@ export function parseBatchStdinJsonArray(stdin) {
|
|
|
62
63
|
try {
|
|
63
64
|
const parsed = JSON.parse(stdin);
|
|
64
65
|
if (!Array.isArray(parsed)) {
|
|
65
|
-
return { error:
|
|
66
|
+
return { error: `agent_browser batch stdin must be a JSON array of command steps.${BATCH_STDIN_EXAMPLE}` };
|
|
66
67
|
}
|
|
67
68
|
return { steps: parsed };
|
|
68
69
|
}
|
|
69
70
|
catch (error) {
|
|
70
71
|
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
-
return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}` };
|
|
72
|
+
return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}.${BATCH_STDIN_EXAMPLE}` };
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
export function parseUserBatchStdin(stdin) {
|
|
@@ -86,6 +87,26 @@ export function parseUserBatchStdin(stdin) {
|
|
|
86
87
|
}
|
|
87
88
|
return { steps };
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The batch steps upstream will actually execute: run_batch uses raw batch
|
|
92
|
+
* arguments exclusively when any exist and reads stdin only otherwise.
|
|
93
|
+
* Upstream filters only the exact `--bail` token, so an equals form such as
|
|
94
|
+
* `--bail=true` stays a raw command (an unknown-command row) and keeps stdin
|
|
95
|
+
* ignored.
|
|
96
|
+
*/
|
|
97
|
+
export function getUpstreamEffectiveBatchSteps(commandTokens, stdin) {
|
|
98
|
+
if (commandTokens[0] !== "batch")
|
|
99
|
+
return [];
|
|
100
|
+
const argumentSteps = commandTokens.slice(1).flatMap((command) => {
|
|
101
|
+
if (command === "--bail")
|
|
102
|
+
return [];
|
|
103
|
+
const step = parseBatchCommandArgument(command).step;
|
|
104
|
+
return step ? [step] : [];
|
|
105
|
+
});
|
|
106
|
+
if (argumentSteps.length > 0)
|
|
107
|
+
return argumentSteps;
|
|
108
|
+
return parseUserBatchStdin(stdin).steps ?? [];
|
|
109
|
+
}
|
|
89
110
|
export function parseValidBatchStepEntries(stdin) {
|
|
90
111
|
const parsed = parseBatchStdinJsonArray(stdin);
|
|
91
112
|
if (parsed.error || parsed.steps === undefined)
|
|
@@ -1,46 +1,126 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { foldAgentBrowserFilesystemIdentity } from "../../argv-grammar.js";
|
|
4
|
+
import { parseWaitCommandTokens } from "../../argv-descriptor.js";
|
|
3
5
|
const SCREENSHOT_BOOLEAN_FLAGS = new Set(["--annotate", "--full", "-f"]);
|
|
4
6
|
const SCREENSHOT_VALUE_FLAGS = new Set(["--screenshot-dir", "--screenshot-format", "--screenshot-quality"]);
|
|
5
|
-
const SCREENSHOT_IMAGE_EXTENSIONS =
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
|
|
7
|
+
const SCREENSHOT_IMAGE_EXTENSIONS = [".jpeg", ".jpg", ".png", ".webp"];
|
|
8
|
+
function isSingleScreenshotPathToken(token) {
|
|
9
|
+
const explicitlyRelative = token.startsWith("./") || token.startsWith("../");
|
|
10
|
+
if (token.startsWith("#") || token.startsWith("@") || (token.startsWith(".") && !explicitlyRelative && !token.includes("/")))
|
|
11
|
+
return false;
|
|
12
|
+
return explicitlyRelative || token.includes("/") || SCREENSHOT_IMAGE_EXTENSIONS.some((extension) => token.endsWith(extension));
|
|
13
|
+
}
|
|
14
|
+
function getScreenshotPositionalIndices(commandTokens) {
|
|
15
|
+
if (commandTokens[0] !== "screenshot")
|
|
16
|
+
return [];
|
|
17
|
+
const positionalIndices = [];
|
|
18
|
+
for (let index = 1; index < commandTokens.length; index += 1) {
|
|
19
|
+
const token = commandTokens[index];
|
|
20
|
+
if (SCREENSHOT_VALUE_FLAGS.has(token)) {
|
|
21
|
+
index += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (SCREENSHOT_BOOLEAN_FLAGS.has(token))
|
|
25
|
+
continue;
|
|
26
|
+
positionalIndices.push(index);
|
|
27
|
+
}
|
|
28
|
+
return positionalIndices;
|
|
9
29
|
}
|
|
10
30
|
export function getScreenshotPathTokenIndex(commandTokens) {
|
|
11
|
-
|
|
31
|
+
const positionalIndices = getScreenshotPositionalIndices(commandTokens);
|
|
32
|
+
if (positionalIndices.length === 0)
|
|
12
33
|
return undefined;
|
|
34
|
+
const candidateIndex = positionalIndices.length >= 2 ? positionalIndices[1] : positionalIndices[0];
|
|
35
|
+
const candidate = commandTokens[candidateIndex];
|
|
36
|
+
if (positionalIndices.length >= 2 || isSingleScreenshotPathToken(candidate)) {
|
|
37
|
+
return candidateIndex;
|
|
13
38
|
}
|
|
14
|
-
|
|
15
|
-
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const DIFF_SCREENSHOT_VALUE_FLAGS = new Set(["-b", "--baseline", "-o", "--output", "-s", "--selector", "-t", "--threshold"]);
|
|
42
|
+
function getDiffScreenshotOutputPath(commandTokens) {
|
|
43
|
+
let outputPath;
|
|
44
|
+
for (let index = 2; index < commandTokens.length; index += 1) {
|
|
16
45
|
const token = commandTokens[index];
|
|
17
|
-
if (token
|
|
18
|
-
|
|
19
|
-
|
|
46
|
+
if (!DIFF_SCREENSHOT_VALUE_FLAGS.has(token))
|
|
47
|
+
continue;
|
|
48
|
+
const value = commandTokens[index + 1];
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (token === "-o" || token === "--output")
|
|
52
|
+
outputPath = value;
|
|
53
|
+
index += 1;
|
|
54
|
+
}
|
|
55
|
+
return outputPath;
|
|
56
|
+
}
|
|
57
|
+
function foldArtifactPath(path, platform) {
|
|
58
|
+
return foldAgentBrowserFilesystemIdentity(path, platform);
|
|
59
|
+
}
|
|
60
|
+
function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
|
|
61
|
+
let cursor = absolutePath;
|
|
62
|
+
const suffix = [];
|
|
63
|
+
while (true) {
|
|
64
|
+
try {
|
|
65
|
+
const canonicalPath = join(realpathSync.native(cursor), ...suffix);
|
|
66
|
+
try {
|
|
67
|
+
const stats = statSync(canonicalPath, { bigint: true });
|
|
68
|
+
if (stats.ino > 0n)
|
|
69
|
+
return `inode:${stats.dev}:${stats.ino}`;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// The destination does not exist yet; canonical ancestry still catches aliases.
|
|
20
73
|
}
|
|
21
|
-
|
|
74
|
+
return foldArtifactPath(canonicalPath, platform);
|
|
22
75
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
76
|
+
catch {
|
|
77
|
+
let symlinkTarget;
|
|
78
|
+
try {
|
|
79
|
+
if (lstatSync(cursor).isSymbolicLink())
|
|
80
|
+
symlinkTarget = resolve(dirname(cursor), readlinkSync(cursor));
|
|
28
81
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
82
|
+
catch { }
|
|
83
|
+
if (symlinkTarget) {
|
|
84
|
+
if (seenSymlinks.has(cursor))
|
|
85
|
+
throw new Error(`Artifact destination contains a symlink loop: ${absolutePath}`);
|
|
86
|
+
if (seenSymlinks.size >= 32)
|
|
87
|
+
throw new Error(`Artifact destination has too many symlink hops: ${absolutePath}`);
|
|
88
|
+
seenSymlinks.add(cursor);
|
|
89
|
+
return canonicalizeArtifactPath(join(symlinkTarget, ...suffix), platform, seenSymlinks);
|
|
33
90
|
}
|
|
91
|
+
const parent = dirname(cursor);
|
|
92
|
+
if (parent === cursor)
|
|
93
|
+
return foldArtifactPath(absolutePath, platform);
|
|
94
|
+
suffix.unshift(basename(cursor));
|
|
95
|
+
cursor = parent;
|
|
34
96
|
}
|
|
35
|
-
positionalIndices.push(index);
|
|
36
97
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
}
|
|
99
|
+
export function canonicalizeExplicitArtifactDestination(cwd, destination, platform = process.platform) {
|
|
100
|
+
return canonicalizeArtifactPath(resolve(cwd, destination), platform, new Set());
|
|
101
|
+
}
|
|
102
|
+
export function getExplicitArtifactDestination(commandTokens) {
|
|
103
|
+
const command = commandTokens[0];
|
|
104
|
+
const subcommand = commandTokens[1];
|
|
105
|
+
if (command === "screenshot") {
|
|
106
|
+
const index = getScreenshotPathTokenIndex(commandTokens);
|
|
107
|
+
return index === undefined ? undefined : commandTokens[index];
|
|
44
108
|
}
|
|
109
|
+
if (command === "download")
|
|
110
|
+
return commandTokens[2];
|
|
111
|
+
if (command === "pdf")
|
|
112
|
+
return commandTokens[1];
|
|
113
|
+
if (command === "wait")
|
|
114
|
+
return parseWaitCommandTokens(commandTokens).downloadPath;
|
|
115
|
+
if (command === "state" && subcommand === "save")
|
|
116
|
+
return commandTokens[2];
|
|
117
|
+
if (command === "diff" && subcommand === "screenshot")
|
|
118
|
+
return getDiffScreenshotOutputPath(commandTokens);
|
|
119
|
+
if (command === "network" && subcommand === "har" && commandTokens[2] === "stop")
|
|
120
|
+
return commandTokens[3];
|
|
121
|
+
if ((command === "trace" || command === "profiler") && subcommand === "stop")
|
|
122
|
+
return commandTokens[2];
|
|
123
|
+
if (command === "record" && (subcommand === "start" || subcommand === "restart"))
|
|
124
|
+
return commandTokens[2];
|
|
45
125
|
return undefined;
|
|
46
126
|
}
|
|
@@ -245,6 +245,7 @@ export async function collectClickDispatchDiagnostic(options) {
|
|
|
245
245
|
const result = getEvalResultRecord(data);
|
|
246
246
|
if (!result)
|
|
247
247
|
return undefined;
|
|
248
|
+
options.probe.cleaned = true;
|
|
248
249
|
const status = typeof result.status === "string" ? result.status : undefined;
|
|
249
250
|
if (status !== "no-native-event-observed")
|
|
250
251
|
return undefined;
|
|
@@ -264,7 +265,7 @@ export async function collectClickDispatchDiagnostic(options) {
|
|
|
264
265
|
};
|
|
265
266
|
}
|
|
266
267
|
export async function cleanupClickDispatchProbe(options) {
|
|
267
|
-
if (!options.probe || !options.sessionName)
|
|
268
|
+
if (!options.probe || options.probe.cleaned || !options.sessionName)
|
|
268
269
|
return;
|
|
269
270
|
await runSessionCommandData({
|
|
270
271
|
args: ["eval", "--stdin"],
|
|
@@ -7,8 +7,8 @@ import { isHttpOrHttpsUrl } from "../../input-modes/job.js";
|
|
|
7
7
|
import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
|
|
8
8
|
import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
9
9
|
import { buildVisibleRefFallbackDiagnosticFromSnapshot, getVisibleRefFallbackTarget } from "../../results/selector-recovery.js";
|
|
10
|
-
import { extractRefSnapshotFromData, normalizeComparableUrl } from "../../session-page-state.js";
|
|
11
|
-
import { redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
|
|
10
|
+
import { extractRefSnapshotFromData, isAboutBlankUrl, normalizeComparableUrl } from "../../session-page-state.js";
|
|
11
|
+
import { extractUpstreamCommandTokens, parseWaitCommandTokens, redactInvocationArgs, redactSensitiveText } from "../../runtime.js";
|
|
12
12
|
import { isRecord } from "../../parsing.js";
|
|
13
13
|
import { getManagedSessionStateAccessValidationError, isFileUrl } from "../../managed-session-state-policy.js";
|
|
14
14
|
import { extractBatchResultCommand, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, runSessionCommandData, } from "./session-state.js";
|
|
@@ -22,8 +22,14 @@ export async function collectNavigationSummary(options) {
|
|
|
22
22
|
const url = extractStringResultField(await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal }), "url");
|
|
23
23
|
if (!url || !/^[a-z][a-z0-9+.-]*:/i.test(url))
|
|
24
24
|
return undefined;
|
|
25
|
-
if (isFileUrl(url))
|
|
25
|
+
if (isFileUrl(url) || isAboutBlankUrl(url))
|
|
26
26
|
return { url };
|
|
27
|
+
// Reuse the title already observed for this exact URL instead of spending a second probe. Titles can
|
|
28
|
+
// change without a URL change on SPAs, but this summary is only a "last observed" page label; the URL
|
|
29
|
+
// stays live-probed on every call.
|
|
30
|
+
if (options.priorTarget?.title && normalizeComparableUrl(options.priorTarget.url) === normalizeComparableUrl(url)) {
|
|
31
|
+
return { title: options.priorTarget.title, url };
|
|
32
|
+
}
|
|
27
33
|
const title = extractStringResultField(await runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal }), "title");
|
|
28
34
|
return { title, url };
|
|
29
35
|
}
|
|
@@ -390,14 +396,14 @@ function isBroadGetTextSelector(selector) {
|
|
|
390
396
|
return normalized === "body" || normalized === "html" || normalized === ":root" || normalized === "*" || normalized === "main" || normalized === "div" || normalized === "section" || normalized === "article" || /^\[role=(?:"application"|'application'|application)\]$/i.test(normalized);
|
|
391
397
|
}
|
|
392
398
|
function getElectronTextScopeContext(options) {
|
|
393
|
-
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
399
|
+
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords, options.namespace);
|
|
394
400
|
if (!record)
|
|
395
401
|
return undefined;
|
|
396
402
|
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
397
403
|
return { launchId: record.launchId, sessionName: record.sessionName ?? options.sessionName, url };
|
|
398
404
|
}
|
|
399
405
|
export function getSourceLookupElectronContext(options) {
|
|
400
|
-
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
406
|
+
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords, options.namespace);
|
|
401
407
|
if (!record)
|
|
402
408
|
return undefined;
|
|
403
409
|
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
@@ -469,9 +475,11 @@ export function formatEvalResultWarningText(warning) {
|
|
|
469
475
|
return warning ? `Eval result warning: ${warning.reason} ${warning.suggestion}` : undefined;
|
|
470
476
|
}
|
|
471
477
|
export async function getArtifactCleanupGuidance(options) {
|
|
472
|
-
if (!options.succeeded || !isCloseCommand(options.command) || !options.manifest
|
|
478
|
+
if (!options.succeeded || !isCloseCommand(options.command) || !options.manifest)
|
|
473
479
|
return undefined;
|
|
474
480
|
const explicitEntries = options.manifest.entries.filter((entry) => entry.storageScope === "explicit-path");
|
|
481
|
+
if (explicitEntries.length === 0)
|
|
482
|
+
return undefined;
|
|
475
483
|
const explicitArtifactPaths = [];
|
|
476
484
|
const seenPaths = new Set();
|
|
477
485
|
for (const entry of explicitEntries) {
|
|
@@ -490,16 +498,15 @@ export async function getArtifactCleanupGuidance(options) {
|
|
|
490
498
|
seenPaths.add(displayPath);
|
|
491
499
|
explicitArtifactPaths.push(displayPath);
|
|
492
500
|
}
|
|
501
|
+
if (explicitArtifactPaths.length === 0)
|
|
502
|
+
return undefined;
|
|
493
503
|
return { explicitArtifactPaths, note: "Closing the browser session does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; clean existing paths with host file tools when no longer needed.", owner: "host-file-tools", summary: formatSessionArtifactRetentionSummary(options.manifest) };
|
|
494
504
|
}
|
|
495
505
|
export function formatArtifactCleanupGuidanceText(guidance) {
|
|
496
|
-
if (!guidance)
|
|
506
|
+
if (!guidance || guidance.explicitArtifactPaths.length === 0)
|
|
497
507
|
return undefined;
|
|
498
508
|
const explicitCount = guidance.explicitArtifactPaths.length;
|
|
499
|
-
|
|
500
|
-
? "No existing explicit artifact paths were found in the recent manifest."
|
|
501
|
-
: `${explicitCount} explicit artifact${explicitCount === 1 ? "" : "s"} remain${explicitCount === 1 ? "s" : ""}; expand or inspect details.artifactCleanup.explicitArtifactPaths for paths.`;
|
|
502
|
-
return `Artifact lifecycle: ${explicitSummary} Browser close does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; use host file tools for cleanup.`;
|
|
509
|
+
return `Artifact lifecycle: ${explicitCount} explicit artifact${explicitCount === 1 ? "" : "s"} remain${explicitCount === 1 ? "s" : ""}; expand or inspect details.artifactCleanup.explicitArtifactPaths for paths. Browser close does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; use host file tools for cleanup.`;
|
|
503
510
|
}
|
|
504
511
|
async function collectManagedSessionCommandData(options) {
|
|
505
512
|
try {
|
|
@@ -718,24 +725,18 @@ function getLastPositionalToken(args, startIndex = 1) {
|
|
|
718
725
|
return undefined;
|
|
719
726
|
}
|
|
720
727
|
function getTimeoutStepArtifactPath(args) {
|
|
721
|
-
const
|
|
728
|
+
const commandArgs = extractUpstreamCommandTokens(args);
|
|
729
|
+
const [command] = commandArgs;
|
|
722
730
|
if (command === "screenshot") {
|
|
723
|
-
const index = getScreenshotPathTokenIndex(
|
|
724
|
-
return index === undefined ? undefined :
|
|
731
|
+
const index = getScreenshotPathTokenIndex(commandArgs);
|
|
732
|
+
return index === undefined ? undefined : commandArgs[index];
|
|
725
733
|
}
|
|
726
734
|
if (command === "pdf")
|
|
727
|
-
return getLastPositionalToken(
|
|
735
|
+
return getLastPositionalToken(commandArgs);
|
|
728
736
|
if (command === "download")
|
|
729
|
-
return getLastPositionalToken(
|
|
730
|
-
if (command === "wait")
|
|
731
|
-
|
|
732
|
-
if (inlineDownload)
|
|
733
|
-
return inlineDownload.slice("--download=".length) || undefined;
|
|
734
|
-
const downloadIndex = args.indexOf("--download");
|
|
735
|
-
const downloadPath = downloadIndex >= 0 ? args[downloadIndex + 1] : undefined;
|
|
736
|
-
if (downloadPath && !downloadPath.startsWith("-"))
|
|
737
|
-
return downloadPath;
|
|
738
|
-
}
|
|
737
|
+
return getLastPositionalToken(commandArgs, 2);
|
|
738
|
+
if (command === "wait")
|
|
739
|
+
return parseWaitCommandTokens(commandArgs).downloadPath;
|
|
739
740
|
return undefined;
|
|
740
741
|
}
|
|
741
742
|
async function statTimeoutArtifactPath(absolutePath) {
|
|
@@ -195,7 +195,7 @@ export async function prepareFinalResultRecoveryState(options) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
const richInputRecoveryDiagnostic = buildRichInputRecoveryDiagnostic(visibleRefFallbackDiagnostic);
|
|
198
|
-
const noActivePageSnapshotFailure = categoryDetails.resultCategory === "failure" && (isNoActivePageSnapshotFailure(options.executionPlan.commandInfo.command, options.errorText ?? options.presentation.summary) || options.batchRefSnapshotState?.invalidation
|
|
198
|
+
const noActivePageSnapshotFailure = categoryDetails.resultCategory === "failure" && (isNoActivePageSnapshotFailure(options.executionPlan.commandInfo.command, options.errorText ?? options.presentation.summary) || options.batchRefSnapshotState?.invalidation?.reason === "no-active-page");
|
|
199
199
|
const executionSessionKey = getSessionContextKey(options.executionPlan.sessionName, options.executionPlan.namespace);
|
|
200
200
|
if (noActivePageSnapshotFailure && executionSessionKey) {
|
|
201
201
|
const refUpdate = options.sessionPageState.applyRefSnapshotInvalidation({ invalidation: buildNoActivePageRefSnapshotInvalidation(), sessionName: executionSessionKey, update: options.sessionPageStateUpdate });
|
|
@@ -329,6 +329,18 @@ function buildResultNextActions(options) {
|
|
|
329
329
|
append(buildAgentBrowserNextActions({ electron: { launchId: options.electronLaunchRecord.launchId, sessionName: options.electronLaunchRecord.sessionName, status: options.electronLaunchRecord.cleanupState }, failureCategory: options.categoryDetails.failureCategory, resultCategory: options.categoryDetails.resultCategory, successCategory: options.categoryDetails.successCategory }));
|
|
330
330
|
return nextActions.length > 0 ? nextActions : undefined;
|
|
331
331
|
}
|
|
332
|
+
function formatFailureNextActionsText(options, nextActions) {
|
|
333
|
+
if (options.categoryDetails.resultCategory !== "failure" || !nextActions || nextActions.length === 0)
|
|
334
|
+
return undefined;
|
|
335
|
+
const lines = nextActions.slice(0, 6).map((action) => {
|
|
336
|
+
const params = action.params
|
|
337
|
+
? { ...action.params, ...(action.params.stdin === undefined ? {} : { stdin: "[omitted; use details.nextActions]" }) }
|
|
338
|
+
: undefined;
|
|
339
|
+
const payload = action.artifactPath ? { artifactPath: action.artifactPath } : params;
|
|
340
|
+
return `- ${action.id}${payload ? ` ${JSON.stringify(payload)}` : ""}: ${action.reason}`;
|
|
341
|
+
});
|
|
342
|
+
return ["Next actions:", ...lines, "Use the exact redacted payloads in details.nextActions when available."].join("\n");
|
|
343
|
+
}
|
|
332
344
|
function buildAgentBrowserResultDetails(options, nextActions) {
|
|
333
345
|
const publicVisibleRefFallbackDiagnostic = options.visibleRefFallbackDiagnostic ? sanitizeVisibleRefFallbackDiagnostic(options.visibleRefFallbackDiagnostic) : undefined;
|
|
334
346
|
const rawPageChangeSummary = (options.scrollNoopDiagnostic || options.comboboxFocusDiagnostic) && options.presentation.pageChangeSummary ? { ...options.presentation.pageChangeSummary, nextActionIds: nextActions?.map((action) => action.id) } : options.presentation.pageChangeSummary;
|
|
@@ -357,6 +369,7 @@ function buildAgentBrowserResultDetails(options, nextActions) {
|
|
|
357
369
|
navigationSummary: options.navigationSummary,
|
|
358
370
|
electron: options.electronLaunchRecord ? { action: "launch", cleanup: options.electronFailedConnectCleanup, handoff: options.electronHandoff, identifiers: buildElectronIdentifiers(options.electronLaunchRecord), launch: options.electronLaunchRecord, profileIsolation: options.electronProfileIsolationDetails, status: options.succeeded ? "succeeded" : "failed", targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined,
|
|
359
371
|
...options.categoryDetails,
|
|
372
|
+
agentBrowserStarted: options.processResult.agentBrowserStarted,
|
|
360
373
|
aboutBlankSessionMismatch: options.aboutBlankSessionMismatch,
|
|
361
374
|
electronPostCommandHealth: options.electronPostCommandHealth,
|
|
362
375
|
electronRefFreshness: options.electronRefFreshnessDiagnostic,
|
|
@@ -434,7 +447,8 @@ export function buildFinalAgentBrowserToolResult(options) {
|
|
|
434
447
|
const artifactCleanupText = formatArtifactCleanupGuidanceText(options.artifactCleanup);
|
|
435
448
|
const timeoutPartialProgressText = options.timeoutPartialProgress ? formatTimeoutPartialProgressText(options.timeoutPartialProgress) : undefined;
|
|
436
449
|
const managedSessionOutcomeText = formatManagedSessionOutcomeText(options.managedSessionOutcome);
|
|
437
|
-
const
|
|
450
|
+
const failureNextActionsText = formatFailureNextActionsText(options, nextActions);
|
|
451
|
+
const rawAppendedDiagnosticText = [visibleRefFallbackText, richInputRecoveryText, semanticActionCandidateText, clickDispatchText, overlayBlockerText, fillVerificationText, electronRefFreshnessText, selectorTextVisibilityText, electronBroadGetTextScopeText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, evalResultWarningText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText, failureNextActionsText].filter((item) => item !== undefined).join("\n\n");
|
|
438
452
|
const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, options.exactSensitiveValues));
|
|
439
453
|
const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!options.userRequestedJson || options.plainTextInspection);
|
|
440
454
|
let content = shouldAppendDiagnosticText && options.redactedContent[0]?.type === "text" ? [{ ...options.redactedContent[0], text: `${options.redactedContent[0].text}\n\n${appendedDiagnosticText}` }, ...options.redactedContent.slice(1)] : options.redactedContent;
|
|
@@ -462,5 +476,5 @@ export async function buildMissingBinaryFailureResult(options) {
|
|
|
462
476
|
missingBinaryElectronRecord = missingBinaryElectronCleanup.record;
|
|
463
477
|
}
|
|
464
478
|
const textParts = [errorText, managedSessionOutcomeText, missingBinaryElectronCleanup ? `Electron cleanup after failed attach: ${missingBinaryElectronCleanup.summary}` : undefined].filter((part) => part !== undefined && part.length > 0);
|
|
465
|
-
return { content: [{ type: "text", text: textParts.join("\n\n") }], details: { args: options.redactedArgs, compatibilityWorkaround: options.compatibilityWorkaround, effectiveArgs: options.redactedProcessArgs, electron: missingBinaryElectronRecord ? { action: "launch", cleanup: missingBinaryElectronCleanup, launch: missingBinaryElectronRecord, status: "failed", targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined, managedSessionOutcome, namespace: options.executionPlan.namespace, nextActions: managedSessionRecoveryNextActions.length > 0 ? managedSessionRecoveryNextActions : undefined, sessionMode: options.sessionMode, sessionTabCorrection: options.sessionTabCorrection, ...buildAgentBrowserResultCategoryDetails({ args: options.redactedProcessArgs, command: options.executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError, succeeded: false }), spawnError }, isError: true };
|
|
479
|
+
return { content: [{ type: "text", text: textParts.join("\n\n") }], details: { agentBrowserStarted: options.processResult.agentBrowserStarted, args: options.redactedArgs, compatibilityWorkaround: options.compatibilityWorkaround, effectiveArgs: options.redactedProcessArgs, electron: missingBinaryElectronRecord ? { action: "launch", cleanup: missingBinaryElectronCleanup, launch: missingBinaryElectronRecord, status: "failed", targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined, managedSessionOutcome, namespace: options.executionPlan.namespace, nextActions: managedSessionRecoveryNextActions.length > 0 ? managedSessionRecoveryNextActions : undefined, sessionMode: options.sessionMode, sessionTabCorrection: options.sessionTabCorrection, ...buildAgentBrowserResultCategoryDetails({ args: options.redactedProcessArgs, command: options.executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError, succeeded: false }), spawnError }, isError: true };
|
|
466
480
|
}
|
|
@@ -20,6 +20,7 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
20
20
|
const ownedManagedSession = prepared.ownedManagedSessionContext;
|
|
21
21
|
return await withOwnedManagedSessionContext(ownedManagedSession, async () => {
|
|
22
22
|
try {
|
|
23
|
+
const artifactRunStartedAtMs = Date.now();
|
|
23
24
|
const processResult = await runAgentBrowserProcess({
|
|
24
25
|
args: prepared.processArgs,
|
|
25
26
|
cwd: options.cwd,
|
|
@@ -51,7 +52,7 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
51
52
|
});
|
|
52
53
|
if (missingBinaryResult)
|
|
53
54
|
return missingBinaryResult;
|
|
54
|
-
const output = await processBrowserOutput({ ...options, prepared, processResult });
|
|
55
|
+
const output = await processBrowserOutput({ ...options, artifactRunStartedAtMs, prepared, processResult });
|
|
55
56
|
applyBrowserRunStatePatch(options.state, output.statePatch);
|
|
56
57
|
return output.result;
|
|
57
58
|
}
|
package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js
CHANGED
|
@@ -3,13 +3,14 @@ import { acquireManagedSessionPolicyLock } from "../../managed-session-policy-lo
|
|
|
3
3
|
import { pruneOwnedManagedSessionRestoreSnapshots, resolveExplicitAutosaveInterval, } from "../../managed-session-restore.js";
|
|
4
4
|
import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
|
|
5
5
|
import { isRecord } from "../../parsing.js";
|
|
6
|
+
import { getAgentBrowserProcessEnvironment } from "../../process-environment.js";
|
|
6
7
|
import { runAgentBrowserProcess } from "../../process.js";
|
|
7
8
|
import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
|
|
8
9
|
import { redactInvocationArgs } from "../../runtime.js";
|
|
9
10
|
const MANAGED_SESSION_DAEMON_INSPECTION_TIMEOUT_MS = 35_000;
|
|
10
11
|
const RUNNING_HEADED_AUTOSAVE_POLICY_CHANGE_ERROR = "AGENT_BROWSER_AUTOSAVE_INTERVAL_MS cannot change a running wrapper-owned headed session's launch-time periodic autosave interval. Close that session first, then retry with sessionMode: \"fresh\" so the new daemon starts with the requested interval.";
|
|
11
12
|
export function getRunningHeadedAutosavePolicyChangeError(recordedInterval, closeCommand = false) {
|
|
12
|
-
const explicitInterval = resolveExplicitAutosaveInterval(
|
|
13
|
+
const explicitInterval = resolveExplicitAutosaveInterval(getAgentBrowserProcessEnvironment().AGENT_BROWSER_AUTOSAVE_INTERVAL_MS);
|
|
13
14
|
return recordedInterval !== undefined && !closeCommand && explicitInterval !== undefined && explicitInterval !== recordedInterval
|
|
14
15
|
? RUNNING_HEADED_AUTOSAVE_POLICY_CHANGE_ERROR
|
|
15
16
|
: undefined;
|
|
@@ -79,7 +80,7 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
|
|
|
79
80
|
if (options.mode === "close") {
|
|
80
81
|
if (daemon.status === "active")
|
|
81
82
|
context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey);
|
|
82
|
-
return { lock };
|
|
83
|
+
return { daemonStatus: daemon.status, lock };
|
|
83
84
|
}
|
|
84
85
|
const stickyDisabled = context.restoreState.isDisabled(context.sessionName, context.namespace);
|
|
85
86
|
const hasKnownDaemonRestoreKey = context.restoreState.hasDaemonRestoreKey(context.sessionName, context.namespace);
|
|
@@ -101,13 +102,14 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
|
|
|
101
102
|
context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey);
|
|
102
103
|
return !["inactive", "missing-binary"].includes(daemon.status) && !activePolicyMatches
|
|
103
104
|
? {
|
|
105
|
+
daemonStatus: daemon.status,
|
|
104
106
|
error: [
|
|
105
107
|
"This wrapper-owned session's live daemon does not match the requested managed-restore policy.",
|
|
106
108
|
"Close that session first, retry with sessionMode: \"fresh\", or use a distinct explicit --session.",
|
|
107
109
|
].join(" "),
|
|
108
110
|
lock,
|
|
109
111
|
}
|
|
110
|
-
: { lock };
|
|
112
|
+
: { daemonStatus: daemon.status, lock };
|
|
111
113
|
}
|
|
112
114
|
catch (error) {
|
|
113
115
|
await lock.release();
|
|
@@ -118,7 +120,7 @@ export async function closeManagedSession(options) {
|
|
|
118
120
|
const controller = new AbortController();
|
|
119
121
|
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
120
122
|
let stdoutSpillPath;
|
|
121
|
-
const closeArgs = [...(options.namespace ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
|
|
123
|
+
const closeArgs = [...(options.namespace !== undefined ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
|
|
122
124
|
const policyLock = options.policyLock ?? await acquireManagedSessionPolicyLock({
|
|
123
125
|
namespace: options.namespace,
|
|
124
126
|
sessionName: options.sessionName,
|