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.
Files changed (73) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +63 -20
  3. package/dist/extensions/agent-browser/index.js +787 -105
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
  10. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  11. package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
  12. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  13. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  14. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
  15. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
  17. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  18. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
  19. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
  20. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
  30. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
  31. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  32. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  33. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  34. package/dist/extensions/agent-browser/lib/playbook.js +18 -15
  35. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  36. package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
  37. package/dist/extensions/agent-browser/lib/process.js +131 -41
  38. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  39. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
  40. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  41. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  42. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  43. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  44. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  45. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
  46. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
  47. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  48. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
  49. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  50. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  51. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
  52. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
  53. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
  54. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  55. package/dist/extensions/agent-browser/lib/runtime.js +116 -39
  56. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
  57. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  58. package/dist/extensions/agent-browser/script-worker.js +169 -0
  59. package/dist/scripts/agent-browser-target.mjs +3 -0
  60. package/docs/ARCHITECTURE.md +40 -21
  61. package/docs/COMMAND_REFERENCE.md +90 -35
  62. package/docs/RELEASE.md +3 -3
  63. package/docs/REQUIREMENTS.md +4 -2
  64. package/docs/SUPPORT_MATRIX.md +26 -19
  65. package/docs/TOOL_CONTRACT.md +93 -52
  66. package/package.json +3 -1
  67. package/platform-smoke.config.mjs +2 -2
  68. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  69. package/scripts/agent-browser-target.mjs +3 -0
  70. package/scripts/build.mjs +41 -0
  71. package/scripts/doctor.mjs +7 -6
  72. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  73. 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 command = commandTokens[0];
81
+ const upstreamCommandTokens = stripUpstreamGlobalFlags(commandTokens);
82
+ const command = upstreamCommandTokens[0];
55
83
  return {
56
84
  command,
57
- subcommand: isOpenNavigationCommand(command) ? getOpenCommandTarget(commandTokens) : commandTokens[1],
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
- /** Mirror upstream 0.33.2 global parsing: full argv, no `--` sentinel, and only global value payloads are skipped. */
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
+ }
@@ -96,5 +96,5 @@ function isSessionlessCommand(commandTokens) {
96
96
  return false;
97
97
  }
98
98
  export function needsManagedSession(descriptor) {
99
- return !isSessionlessCommand(descriptor.commandTokens);
99
+ return !isSessionlessCommand(descriptor.upstreamCommandTokens);
100
100
  }
@@ -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
- export function isRefInvalidatingBatchCommand(command) {
297
- return hasCommandCapability(command, "invalidatesBatchRefs");
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 reset: URL QA cleared enabled network/console/page-error buffers before opening the target; reset rows in details.batchSteps are not counted as current-page failures.");
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
- for (const item of items) {
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
- failedChecks.push(`${result.errors.length} page error(s)`);
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, or one select option." })),
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
- electronLaunchVariant(Type, StringEnum, { appPath: Type.String({ description: "macOS .app path.", minLength: 1 }) }),
83
- electronLaunchVariant(Type, StringEnum, { appName: Type.String({ description: "Name from electron.list.", minLength: 1 }) }),
84
- electronLaunchVariant(Type, StringEnum, { bundleId: Type.String({ description: "Bundle id from electron.list.", minLength: 1 }) }),
85
- electronLaunchVariant(Type, StringEnum, { executablePath: Type.String({ description: "Executable path.", minLength: 1 }) }),
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();