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,4 +1,4 @@
|
|
|
1
|
-
import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, optionalGlobalValueFlagConsumesNext } from "./argv-grammar.js";
|
|
1
|
+
import { GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, VALUE_FLAGS, optionalGlobalValueFlagConsumesNext, stripUpstreamGlobalFlags } from "./argv-grammar.js";
|
|
2
2
|
import { isOpenNavigationCommand } from "./command-taxonomy.js";
|
|
3
3
|
function isBooleanLiteral(token) {
|
|
4
4
|
const normalized = token?.trim().toLowerCase();
|
|
@@ -33,6 +33,33 @@ export function extractCommandTokens(args) {
|
|
|
33
33
|
const commandStartIndex = findCommandStartIndex(args);
|
|
34
34
|
return commandStartIndex === undefined ? [] : args.slice(commandStartIndex);
|
|
35
35
|
}
|
|
36
|
+
export function extractUpstreamCommandTokens(args) {
|
|
37
|
+
return stripUpstreamGlobalFlags(extractCommandTokens(args));
|
|
38
|
+
}
|
|
39
|
+
export function parseWaitCommandTokens(commandTokens) {
|
|
40
|
+
if (commandTokens[0] !== "wait")
|
|
41
|
+
return {};
|
|
42
|
+
const considered = commandTokens.slice(1).map((token, offset) => ({ index: offset + 1, token }));
|
|
43
|
+
const timeoutIndex = considered.findIndex((entry) => entry.token === "--timeout");
|
|
44
|
+
if (timeoutIndex >= 0)
|
|
45
|
+
considered.splice(timeoutIndex, Math.min(2, considered.length - timeoutIndex));
|
|
46
|
+
for (const flags of [["--url", "-u"], ["--load", "-l"], ["--fn", "-f"], ["--text", "-t"]]) {
|
|
47
|
+
const match = considered.find((entry) => flags.includes(entry.token));
|
|
48
|
+
if (match)
|
|
49
|
+
return { subcommand: match.token };
|
|
50
|
+
}
|
|
51
|
+
const download = considered.find((entry) => entry.token === "--download" || entry.token === "-d");
|
|
52
|
+
if (download) {
|
|
53
|
+
const downloadPathIndex = download.index + 1;
|
|
54
|
+
const candidate = commandTokens[downloadPathIndex];
|
|
55
|
+
return {
|
|
56
|
+
downloadPath: candidate && !candidate.startsWith("--") ? candidate : undefined,
|
|
57
|
+
downloadPathIndex: candidate && !candidate.startsWith("--") ? downloadPathIndex : undefined,
|
|
58
|
+
subcommand: download.token,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { subcommand: considered[0]?.token };
|
|
62
|
+
}
|
|
36
63
|
function getOpenCommandTarget(commandTokens) {
|
|
37
64
|
for (let index = 1; index < commandTokens.length; index += 1) {
|
|
38
65
|
const token = commandTokens[index];
|
|
@@ -51,10 +78,13 @@ function getOpenCommandTarget(commandTokens) {
|
|
|
51
78
|
return undefined;
|
|
52
79
|
}
|
|
53
80
|
export function parseCommandInfoFromTokens(commandTokens) {
|
|
54
|
-
const
|
|
81
|
+
const upstreamCommandTokens = stripUpstreamGlobalFlags(commandTokens);
|
|
82
|
+
const command = upstreamCommandTokens[0];
|
|
55
83
|
return {
|
|
56
84
|
command,
|
|
57
|
-
subcommand: isOpenNavigationCommand(command)
|
|
85
|
+
subcommand: isOpenNavigationCommand(command)
|
|
86
|
+
? getOpenCommandTarget(upstreamCommandTokens)
|
|
87
|
+
: command === "wait" ? parseWaitCommandTokens(upstreamCommandTokens).subcommand : upstreamCommandTokens[1],
|
|
58
88
|
};
|
|
59
89
|
}
|
|
60
90
|
export function parseCommandInfo(args) {
|
|
@@ -62,8 +92,10 @@ export function parseCommandInfo(args) {
|
|
|
62
92
|
}
|
|
63
93
|
export function parseArgvDescriptor(args) {
|
|
64
94
|
const commandTokens = extractCommandTokens(args);
|
|
95
|
+
const upstreamCommandTokens = stripUpstreamGlobalFlags(commandTokens);
|
|
65
96
|
return {
|
|
66
97
|
commandInfo: parseCommandInfoFromTokens(commandTokens),
|
|
67
98
|
commandTokens,
|
|
99
|
+
upstreamCommandTokens,
|
|
68
100
|
};
|
|
69
101
|
}
|
|
@@ -90,6 +90,11 @@ export const GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES = new Set([
|
|
|
90
90
|
"--ignore-https-errors",
|
|
91
91
|
"--json",
|
|
92
92
|
"--no-auto-dialog",
|
|
93
|
+
"--no-pin-tab",
|
|
94
|
+
"--offline",
|
|
95
|
+
"--pin-tab",
|
|
96
|
+
"--quick",
|
|
97
|
+
"--fix",
|
|
93
98
|
"--quiet",
|
|
94
99
|
"-q",
|
|
95
100
|
"--verbose",
|
|
@@ -148,7 +153,7 @@ export function canonicalizeAgentBrowserNamespace(value) {
|
|
|
148
153
|
}
|
|
149
154
|
return normalized.replace(/[-_]+$/u, "") || undefined;
|
|
150
155
|
}
|
|
151
|
-
function foldAgentBrowserFilesystemIdentity(value, platform) {
|
|
156
|
+
export function foldAgentBrowserFilesystemIdentity(value, platform) {
|
|
152
157
|
if (platform !== "darwin" && platform !== "win32")
|
|
153
158
|
return value;
|
|
154
159
|
// APFS aliases include full Unicode folds such as ß/SS and ς/Σ, not just ASCII case.
|
|
@@ -160,7 +165,11 @@ export function getAgentBrowserSessionIdentityKey(sessionName, namespace, platfo
|
|
|
160
165
|
const canonicalSessionName = foldAgentBrowserFilesystemIdentity(sessionName, platform);
|
|
161
166
|
return identityNamespace ? `${identityNamespace}\0${canonicalSessionName}` : canonicalSessionName;
|
|
162
167
|
}
|
|
163
|
-
|
|
168
|
+
export function isAgentBrowserSessionIdentityKeyInNamespace(identityKey, namespace) {
|
|
169
|
+
const prefix = getAgentBrowserSessionIdentityKey("", namespace);
|
|
170
|
+
return prefix ? identityKey.startsWith(prefix) : !identityKey.includes("\0");
|
|
171
|
+
}
|
|
172
|
+
/** Mirror upstream 0.34.0 global parsing: full argv, no `--` sentinel, and only global value payloads are skipped. */
|
|
164
173
|
export function scanUpstreamGlobalFlagOccurrences(args, targetFlag) {
|
|
165
174
|
const occurrences = [];
|
|
166
175
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -257,6 +266,39 @@ export function optionalGlobalValueFlagConsumesNext(flag, nextToken) {
|
|
|
257
266
|
return false;
|
|
258
267
|
return !isKnownCommandToken(nextToken);
|
|
259
268
|
}
|
|
269
|
+
export function projectUpstreamGlobalFlags(args) {
|
|
270
|
+
const indices = [];
|
|
271
|
+
const tokens = [];
|
|
272
|
+
let seenCommand = false;
|
|
273
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
274
|
+
const token = args[index];
|
|
275
|
+
if (token.startsWith("--restore="))
|
|
276
|
+
continue;
|
|
277
|
+
if (token === "--restore") {
|
|
278
|
+
if (!seenCommand && optionalGlobalValueFlagConsumesNext(token, args[index + 1]))
|
|
279
|
+
index += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (PREVALIDATED_VALUE_FLAGS.has(token)) {
|
|
283
|
+
index += 1;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(token)) {
|
|
287
|
+
if (["true", "false"].includes(args[index + 1] ?? ""))
|
|
288
|
+
index += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
tokens.push(token);
|
|
292
|
+
indices.push(index);
|
|
293
|
+
if (isKnownCommandToken(token))
|
|
294
|
+
seenCommand = true;
|
|
295
|
+
}
|
|
296
|
+
return { indices, tokens };
|
|
297
|
+
}
|
|
298
|
+
/** Mirror upstream 0.34.0 clean_args: remove global flags wherever they appear before command parsing. */
|
|
299
|
+
export function stripUpstreamGlobalFlags(args) {
|
|
300
|
+
return projectUpstreamGlobalFlags(args).tokens;
|
|
301
|
+
}
|
|
260
302
|
export function stripSessionlessShapeGlobalFlags(commandTokens) {
|
|
261
303
|
const stripped = [];
|
|
262
304
|
for (let index = 0; index < commandTokens.length; index += 1) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
|
|
2
|
+
import { isCloseAllCommand, isCloseCommand } from "./command-taxonomy.js";
|
|
3
|
+
import { isRecord } from "./parsing.js";
|
|
4
|
+
function getRowBrowserLaunched(row) {
|
|
5
|
+
const result = isRecord(row.result) ? row.result : isRecord(row.data) ? row.data : undefined;
|
|
6
|
+
const lifecycle = isRecord(row.lifecycle) ? row.lifecycle : isRecord(result?.lifecycle) ? result.lifecycle : undefined;
|
|
7
|
+
const effectiveLaunch = isRecord(lifecycle?.effectiveLaunch) ? lifecycle.effectiveLaunch : undefined;
|
|
8
|
+
return typeof effectiveLaunch?.browserLaunched === "boolean" ? effectiveLaunch.browserLaunched : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function batchHasSuccessfulCloseAll(data, fallbackCommands = []) {
|
|
11
|
+
if (!Array.isArray(data))
|
|
12
|
+
return false;
|
|
13
|
+
return data.some((row, index) => {
|
|
14
|
+
if (!isRecord(row) || row.success !== true)
|
|
15
|
+
return false;
|
|
16
|
+
const rowCommand = Array.isArray(row.command) && row.command.every((token) => typeof token === "string")
|
|
17
|
+
? row.command
|
|
18
|
+
: fallbackCommands[index];
|
|
19
|
+
return rowCommand ? isCloseAllCommand(extractUpstreamCommandTokens(rowCommand)) : false;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export function getSuccessfulBatchCloseLifecycle(rows, fallbackCommands = []) {
|
|
23
|
+
if (!Array.isArray(rows))
|
|
24
|
+
return undefined;
|
|
25
|
+
let sawClose = false;
|
|
26
|
+
let endsClosed = false;
|
|
27
|
+
let browserActiveAfterClose = false;
|
|
28
|
+
let recordingClosedAfterBatch = false;
|
|
29
|
+
let statePath;
|
|
30
|
+
for (const [index, row] of rows.entries()) {
|
|
31
|
+
if (!isRecord(row))
|
|
32
|
+
continue;
|
|
33
|
+
const stepSucceeded = row.success === true;
|
|
34
|
+
const rowCommand = Array.isArray(row.command) && row.command.every((token) => typeof token === "string")
|
|
35
|
+
? row.command
|
|
36
|
+
: fallbackCommands[index];
|
|
37
|
+
const browserLaunched = getRowBrowserLaunched(row);
|
|
38
|
+
if (!rowCommand) {
|
|
39
|
+
if (sawClose && browserLaunched !== false) {
|
|
40
|
+
endsClosed = false;
|
|
41
|
+
browserActiveAfterClose = true;
|
|
42
|
+
recordingClosedAfterBatch = false;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const [command, subcommand] = extractUpstreamCommandTokens(rowCommand);
|
|
47
|
+
if (stepSucceeded && isCloseCommand(command)) {
|
|
48
|
+
sawClose = true;
|
|
49
|
+
endsClosed = true;
|
|
50
|
+
browserActiveAfterClose = false;
|
|
51
|
+
recordingClosedAfterBatch = true;
|
|
52
|
+
const result = isRecord(row.result) ? row.result : isRecord(row.data) ? row.data : undefined;
|
|
53
|
+
statePath = typeof result?.statePath === "string" ? result.statePath : undefined;
|
|
54
|
+
}
|
|
55
|
+
else if (sawClose && command === "record") {
|
|
56
|
+
if (browserLaunched === true || (subcommand === "stop" && browserLaunched === undefined)) {
|
|
57
|
+
endsClosed = false;
|
|
58
|
+
browserActiveAfterClose = true;
|
|
59
|
+
}
|
|
60
|
+
if (stepSucceeded && subcommand === "stop")
|
|
61
|
+
recordingClosedAfterBatch = true;
|
|
62
|
+
else if (stepSucceeded && browserActiveAfterClose && (subcommand === "start" || subcommand === "restart"))
|
|
63
|
+
recordingClosedAfterBatch = false;
|
|
64
|
+
}
|
|
65
|
+
else if (sawClose && browserLaunched !== false) {
|
|
66
|
+
endsClosed = false;
|
|
67
|
+
browserActiveAfterClose = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return sawClose ? { endsClosed, recordingClosedAfterBatch, statePath } : undefined;
|
|
71
|
+
}
|
|
@@ -61,6 +61,10 @@ const COMMAND_CAPABILITIES = [
|
|
|
61
61
|
invalidatesBatchRefs: true,
|
|
62
62
|
triggersPostMutationSnapshot: true,
|
|
63
63
|
},
|
|
64
|
+
{
|
|
65
|
+
command: "diff",
|
|
66
|
+
guardsPageRefs: true,
|
|
67
|
+
},
|
|
64
68
|
{
|
|
65
69
|
command: "download",
|
|
66
70
|
eligibleForPageChangeSummary: true,
|
|
@@ -92,6 +96,10 @@ const COMMAND_CAPABILITIES = [
|
|
|
92
96
|
command: "find",
|
|
93
97
|
eligibleForElectronHealthProbe: true,
|
|
94
98
|
},
|
|
99
|
+
{
|
|
100
|
+
command: "frame",
|
|
101
|
+
guardsPageRefs: true,
|
|
102
|
+
},
|
|
95
103
|
{
|
|
96
104
|
command: "focus",
|
|
97
105
|
guardsPageRefs: true,
|
|
@@ -104,6 +112,14 @@ const COMMAND_CAPABILITIES = [
|
|
|
104
112
|
navigationObservable: true,
|
|
105
113
|
triggersPostMutationSnapshot: true,
|
|
106
114
|
},
|
|
115
|
+
{
|
|
116
|
+
command: "get",
|
|
117
|
+
guardsPageRefs: true,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
command: "highlight",
|
|
121
|
+
guardsPageRefs: true,
|
|
122
|
+
},
|
|
107
123
|
{
|
|
108
124
|
command: "hover",
|
|
109
125
|
eligibleForPageChangeSummary: true,
|
|
@@ -111,6 +127,10 @@ const COMMAND_CAPABILITIES = [
|
|
|
111
127
|
invalidatesBatchRefs: true,
|
|
112
128
|
triggersPostMutationSnapshot: true,
|
|
113
129
|
},
|
|
130
|
+
{
|
|
131
|
+
command: "is",
|
|
132
|
+
guardsPageRefs: true,
|
|
133
|
+
},
|
|
114
134
|
{
|
|
115
135
|
command: "keydown",
|
|
116
136
|
eligibleForElectronHealthProbe: true,
|
|
@@ -185,10 +205,12 @@ const COMMAND_CAPABILITIES = [
|
|
|
185
205
|
{
|
|
186
206
|
command: "screenshot",
|
|
187
207
|
eligibleForPageChangeSummary: true,
|
|
208
|
+
guardsPageRefs: true,
|
|
188
209
|
},
|
|
189
210
|
{
|
|
190
211
|
command: "scroll",
|
|
191
212
|
eligibleForPageChangeSummary: true,
|
|
213
|
+
guardsPageRefs: true,
|
|
192
214
|
invalidatesBatchRefs: true,
|
|
193
215
|
triggersPostMutationSnapshot: true,
|
|
194
216
|
},
|
|
@@ -281,6 +303,9 @@ export function normalizeCommandName(command) {
|
|
|
281
303
|
export function isCloseCommand(command) {
|
|
282
304
|
return hasCommandCapability(command, "closesSession");
|
|
283
305
|
}
|
|
306
|
+
export function isCloseAllCommand(commandTokens) {
|
|
307
|
+
return isCloseCommand(commandTokens[0]) && commandTokens.slice(1).includes("--all");
|
|
308
|
+
}
|
|
284
309
|
export function isOpenNavigationCommand(command) {
|
|
285
310
|
return hasCommandCapability(command, "openNavigation");
|
|
286
311
|
}
|
|
@@ -293,8 +318,16 @@ export function isSessionTabPinningExcludedCommand(command) {
|
|
|
293
318
|
export function isSessionTabPostCommandCorrectionExcludedCommand(command) {
|
|
294
319
|
return hasCommandCapability(command, "excludedFromPostCommandCorrection");
|
|
295
320
|
}
|
|
296
|
-
|
|
297
|
-
|
|
321
|
+
/** Upstream 0.33.2 record start swaps to a fresh active page before its already-active check, so even a failed start can replace the page; record restart navigates the current page only when a URL operand (any fourth token, mirroring upstream's positional slot) is present. */
|
|
322
|
+
export function isRecordPageTransitionCommand(tokens) {
|
|
323
|
+
if (tokens[0] !== "record")
|
|
324
|
+
return false;
|
|
325
|
+
if (tokens[1] === "start")
|
|
326
|
+
return true;
|
|
327
|
+
return tokens[1] === "restart" && tokens.length >= 4;
|
|
328
|
+
}
|
|
329
|
+
export function isRefInvalidatingBatchCommand(step) {
|
|
330
|
+
return hasCommandCapability(step[0], "invalidatesBatchRefs") || isRecordPageTransitionCommand(step);
|
|
298
331
|
}
|
|
299
332
|
export function isRefGuardedCommand(command) {
|
|
300
333
|
return hasCommandCapability(command, "guardsPageRefs");
|
|
@@ -266,7 +266,7 @@ export function buildQaCompactPassText(options) {
|
|
|
266
266
|
lines.push(`Page: ${pageParts.join(" — ")}`);
|
|
267
267
|
lines.push(`Checks run: ${describeQaChecksRun(options.checks)} (${options.batchStepCount} batch step${options.batchStepCount === 1 ? "" : "s"})`);
|
|
268
268
|
if (options.checks.diagnosticsResetAtStart && (options.checks.checkNetwork || options.checks.checkConsole || options.checks.checkErrors)) {
|
|
269
|
-
lines.push("Diagnostic
|
|
269
|
+
lines.push("Diagnostic isolation: URL QA clears enabled network/console buffers, then snapshots any page-error residue before opening the target. Only unchanged residue is ignored because upstream page-error clear is not reliable.");
|
|
270
270
|
}
|
|
271
271
|
if (options.checks.attached && !options.checks.diagnosticsResetAtStart && (options.checks.checkNetwork || options.checks.checkConsole || options.checks.checkErrors)) {
|
|
272
272
|
lines.push("Attached diagnostics: existing upstream session console/network/error buffers were preserved; rows may include events from before qa.attached started.");
|
|
@@ -280,6 +280,19 @@ export function buildQaCompactPassText(options) {
|
|
|
280
280
|
lines.push("Full diagnostic matrix: see details.qaPreset and details.batchSteps.");
|
|
281
281
|
return lines.join("\n");
|
|
282
282
|
}
|
|
283
|
+
export function buildQaCompactFailureText(options) {
|
|
284
|
+
const lines = [options.qaPreset.summary];
|
|
285
|
+
const pageParts = [options.page?.title, options.page?.url].filter((part) => typeof part === "string" && part.length > 0);
|
|
286
|
+
if (pageParts.length > 0)
|
|
287
|
+
lines.push(`Page: ${pageParts.join(" — ")}`);
|
|
288
|
+
if (options.qaPreset.failedChecks.length > 0)
|
|
289
|
+
lines.push("Failed checks:", ...options.qaPreset.failedChecks.map((failure) => `- ${failure}`));
|
|
290
|
+
if (options.qaPreset.warnings.length > 0)
|
|
291
|
+
lines.push("Warnings:", ...options.qaPreset.warnings.map((warning) => `- ${warning}`));
|
|
292
|
+
lines.push(`Checks run: ${describeQaChecksRun(options.checks)} (${options.batchStepCount} batch step${options.batchStepCount === 1 ? "" : "s"})`);
|
|
293
|
+
lines.push("Full diagnostic matrix: see details.qaPreset and details.batchSteps.");
|
|
294
|
+
return lines.join("\n");
|
|
295
|
+
}
|
|
283
296
|
const QA_VISIBLE_TEXT_TIMEOUT_MS = 5_000;
|
|
284
297
|
function formatQaExpectedTextPreview(text) {
|
|
285
298
|
return JSON.stringify(text.length > 80 ? `${text.slice(0, 77)}...` : text);
|
|
@@ -348,6 +361,34 @@ function extractQaTextAssertionResultText(item) {
|
|
|
348
361
|
}
|
|
349
362
|
return undefined;
|
|
350
363
|
}
|
|
364
|
+
function qaErrorSignature(error) {
|
|
365
|
+
if (typeof error === "string")
|
|
366
|
+
return error;
|
|
367
|
+
try {
|
|
368
|
+
return JSON.stringify(error);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return String(error);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function subtractQaBaselineErrors(errors, baselineErrors) {
|
|
375
|
+
const baselineCounts = new Map();
|
|
376
|
+
for (const error of baselineErrors) {
|
|
377
|
+
const signature = qaErrorSignature(error);
|
|
378
|
+
baselineCounts.set(signature, (baselineCounts.get(signature) ?? 0) + 1);
|
|
379
|
+
}
|
|
380
|
+
let ignoredCount = 0;
|
|
381
|
+
const novelErrors = errors.filter((error) => {
|
|
382
|
+
const signature = qaErrorSignature(error);
|
|
383
|
+
const count = baselineCounts.get(signature) ?? 0;
|
|
384
|
+
if (count === 0)
|
|
385
|
+
return true;
|
|
386
|
+
baselineCounts.set(signature, count - 1);
|
|
387
|
+
ignoredCount += 1;
|
|
388
|
+
return false;
|
|
389
|
+
});
|
|
390
|
+
return { ignoredCount, novelErrors };
|
|
391
|
+
}
|
|
351
392
|
function isDiagnosticResetCommand(item) {
|
|
352
393
|
const command = item.command;
|
|
353
394
|
if (!Array.isArray(command) || !command.every((token) => typeof token === "string"))
|
|
@@ -372,17 +413,29 @@ export function analyzeQaPresetResults(data, compiled) {
|
|
|
372
413
|
return undefined;
|
|
373
414
|
const failedChecks = [];
|
|
374
415
|
const warnings = [];
|
|
375
|
-
|
|
416
|
+
const baselineErrorIndex = compiled?.checks.diagnosticsResetAtStart && compiled.checks.checkErrors
|
|
417
|
+
? compiled.steps.findIndex((step) => step.generatedFrom === "qa.errorBaselineAfterClear")
|
|
418
|
+
: -1;
|
|
419
|
+
const baselineErrorItem = baselineErrorIndex >= 0 ? items[baselineErrorIndex] : undefined;
|
|
420
|
+
const baselineErrorResult = isRecord(baselineErrorItem?.result) ? baselineErrorItem.result : undefined;
|
|
421
|
+
const baselineErrors = Array.isArray(baselineErrorResult?.errors) ? baselineErrorResult.errors : [];
|
|
422
|
+
for (const [index, item] of items.entries()) {
|
|
376
423
|
if (item.success === false) {
|
|
377
424
|
failedChecks.push(`${getCommandNameFromBatchItem(item) ?? "step"} failed`);
|
|
378
425
|
}
|
|
426
|
+
if (index === baselineErrorIndex)
|
|
427
|
+
continue;
|
|
379
428
|
const result = isRecord(item.result) ? item.result : undefined;
|
|
380
429
|
const commandName = getCommandNameFromBatchItem(item);
|
|
381
430
|
if (compiled?.checks.diagnosticsResetAtStart && isDiagnosticResetCommand(item)) {
|
|
382
431
|
continue;
|
|
383
432
|
}
|
|
384
433
|
if (commandName === "errors" && Array.isArray(result?.errors) && result.errors.length > 0) {
|
|
385
|
-
|
|
434
|
+
const { ignoredCount, novelErrors } = subtractQaBaselineErrors(result.errors, baselineErrors);
|
|
435
|
+
if (novelErrors.length > 0)
|
|
436
|
+
failedChecks.push(`${novelErrors.length} page error(s)`);
|
|
437
|
+
if (ignoredCount > 0)
|
|
438
|
+
warnings.push(`${ignoredCount} post-clear page error residue row(s) ignored as unchanged`);
|
|
386
439
|
}
|
|
387
440
|
if (commandName === "console" && Array.isArray(result?.messages)) {
|
|
388
441
|
const errorCount = result.messages.filter((message) => isRecord(message) && /error/i.test(String(message.type ?? message.level ?? ""))).length;
|
|
@@ -477,11 +530,15 @@ export function compileAgentBrowserQaPreset(input) {
|
|
|
477
530
|
steps.push({ action: "wait", args: ["network", "requests", "--clear"] });
|
|
478
531
|
if (diagnosticsResetAtStart && checkConsole)
|
|
479
532
|
steps.push({ action: "wait", args: ["console", "--clear"] });
|
|
480
|
-
if (diagnosticsResetAtStart && checkErrors)
|
|
533
|
+
if (diagnosticsResetAtStart && checkErrors) {
|
|
481
534
|
steps.push({ action: "wait", args: ["errors", "--clear"] });
|
|
535
|
+
steps.push({ action: "wait", args: ["errors"], generatedFrom: "qa.errorBaselineAfterClear" });
|
|
536
|
+
}
|
|
482
537
|
if (!attached && normalizedUrl)
|
|
483
538
|
steps.push({ action: "open", args: ["open", normalizedUrl] });
|
|
484
539
|
steps.push({ action: "wait", args: ["wait", "--load", loadState] });
|
|
540
|
+
if (checkConsole || checkErrors)
|
|
541
|
+
steps.push({ action: "wait", args: ["wait", "150"], generatedFrom: "qa.diagnosticSettle" });
|
|
485
542
|
for (const text of expectedText) {
|
|
486
543
|
steps.push({ action: "assertText", args: ["wait", "--fn", buildQaVisibleTextPredicate(text), "--timeout", String(QA_VISIBLE_TEXT_TIMEOUT_MS)] });
|
|
487
544
|
}
|
|
@@ -261,8 +261,8 @@ export function compileAgentBrowserNetworkSourceLookup(input) {
|
|
|
261
261
|
return { error: "networkSourceLookup.filter must be a non-empty string when provided." };
|
|
262
262
|
if (requestId !== undefined && (typeof requestId !== "string" || requestId.trim().length === 0))
|
|
263
263
|
return { error: "networkSourceLookup.requestId must be a non-empty string when provided." };
|
|
264
|
-
if (namespace !== undefined && (typeof namespace !== "string" || namespace.trim().length === 0))
|
|
265
|
-
return { error: "networkSourceLookup.namespace must be a non-empty string when provided." };
|
|
264
|
+
if (namespace !== undefined && (typeof namespace !== "string" || (namespace !== "" && namespace.trim().length === 0)))
|
|
265
|
+
return { error: "networkSourceLookup.namespace must be a non-empty string or the empty default namespace when provided." };
|
|
266
266
|
if (session !== undefined && (typeof session !== "string" || session.trim().length === 0))
|
|
267
267
|
return { error: "networkSourceLookup.session must be a non-empty string when provided." };
|
|
268
268
|
if (url !== undefined && (typeof url !== "string" || url.trim().length === 0))
|
|
@@ -1,34 +1,24 @@
|
|
|
1
1
|
import { JsonSchema } from "../json-schema.js";
|
|
2
2
|
import { StringEnum as localStringEnum } from "../string-enum-schema.js";
|
|
3
|
+
import { AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES } from "./script.js";
|
|
3
4
|
import { ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS, ELECTRON_DISCOVERY_MAX_RESULTS, } from "../electron/discovery.js";
|
|
4
5
|
import { AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_JOB_STEP_ACTIONS, AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
|
|
5
6
|
// Keep descriptions terse: Pi sends this schema every turn; workflows belong in prompt guidance and docs.
|
|
6
|
-
// ponytail: the four electron.launch variants differ only in their single target field
|
|
7
|
-
// (appPath/appName/bundleId/executablePath); the action literal and shared launch fields
|
|
8
|
-
// are identical, so this helper keeps the schema variants in sync.
|
|
9
|
-
function electronLaunchVariant(Type, StringEnum, targetField) {
|
|
10
|
-
return Type.Object({
|
|
11
|
-
action: StringEnum(["launch"]),
|
|
12
|
-
...targetField,
|
|
13
|
-
appArgs: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
14
|
-
handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS)),
|
|
15
|
-
targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES)),
|
|
16
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
17
|
-
allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
18
|
-
deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
19
|
-
}, { additionalProperties: false });
|
|
20
|
-
}
|
|
21
7
|
export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
|
|
22
8
|
return Type.Object({
|
|
9
|
+
script: Type.Optional(Type.String({
|
|
10
|
+
description: "One-shot JavaScript orchestration with async browser({ args, stdin?, timeoutMs? }) and emit(value); isolated session and no host access.",
|
|
11
|
+
maxLength: AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES,
|
|
12
|
+
})),
|
|
23
13
|
args: Type.Optional(Type.Array(Type.String(), {
|
|
24
14
|
description: "Raw agent-browser argv only: no binary, shell operators, or --json. Start with open → snapshot -i → act on current @refs; re-snapshot after page changes.",
|
|
25
15
|
minItems: 1,
|
|
26
16
|
})),
|
|
27
17
|
semanticAction: Type.Optional(Type.Object({
|
|
28
18
|
action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS),
|
|
29
|
-
locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, { description: "Locator for check/click/fill." })),
|
|
30
|
-
value: Type.Optional(Type.String({ description: "Locator value
|
|
31
|
-
values: Type.Optional(Type.Array(Type.String(), { description: "Select options.", minItems: 1 })),
|
|
19
|
+
locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, { description: "Locator for check/click/fill; select supports role or label." })),
|
|
20
|
+
value: Type.Optional(Type.String({ description: "Locator value or one select option; for select by label, this is the label text." })),
|
|
21
|
+
values: Type.Optional(Type.Array(Type.String(), { description: "Select options; required for select by label.", minItems: 1 })),
|
|
32
22
|
selector: Type.Optional(Type.String({ description: "Direct selector or @ref." })),
|
|
33
23
|
text: Type.Optional(Type.String({ description: "Fill text." })),
|
|
34
24
|
role: Type.Optional(Type.String({ description: "Role locator; alternative to value." })),
|
|
@@ -79,10 +69,19 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
79
69
|
query: Type.Optional(Type.String({ description: "Case-insensitive app filter.", minLength: 1 })),
|
|
80
70
|
maxResults: Type.Optional(Type.Integer({ description: `Result cap; default ${ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS}, values over ${ELECTRON_DISCOVERY_MAX_RESULTS} are clamped.`, minimum: 1 })),
|
|
81
71
|
}, { additionalProperties: false }),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
72
|
+
Type.Object({
|
|
73
|
+
action: StringEnum(["launch"]),
|
|
74
|
+
appPath: Type.Optional(Type.String({ description: "macOS .app path.", minLength: 1 })),
|
|
75
|
+
appName: Type.Optional(Type.String({ description: "Name from electron.list.", minLength: 1 })),
|
|
76
|
+
bundleId: Type.Optional(Type.String({ description: "Bundle id from electron.list.", minLength: 1 })),
|
|
77
|
+
executablePath: Type.Optional(Type.String({ description: "Executable path.", minLength: 1 })),
|
|
78
|
+
appArgs: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
79
|
+
handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS)),
|
|
80
|
+
targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES)),
|
|
81
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
82
|
+
allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
83
|
+
deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
84
|
+
}, { additionalProperties: false }),
|
|
86
85
|
Type.Object({
|
|
87
86
|
action: StringEnum(["status", "cleanup"]),
|
|
88
87
|
launchId: Type.String({ description: "Tracked launch id.", minLength: 1 }),
|
|
@@ -131,7 +130,7 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
131
130
|
})),
|
|
132
131
|
}, {
|
|
133
132
|
additionalProperties: false,
|
|
134
|
-
description: "Choose one input mode: args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron.",
|
|
133
|
+
description: "Choose one input mode: script, args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron.",
|
|
135
134
|
});
|
|
136
135
|
}
|
|
137
136
|
export const AGENT_BROWSER_PARAMS = createAgentBrowserParamsSchema();
|