pi-agent-browser-native 0.2.33 → 0.2.35

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 (44) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +47 -17
  3. package/docs/ARCHITECTURE.md +25 -13
  4. package/docs/COMMAND_REFERENCE.md +285 -47
  5. package/docs/ELECTRON.md +3 -3
  6. package/docs/RELEASE.md +22 -14
  7. package/docs/REQUIREMENTS.md +5 -5
  8. package/docs/SUPPORT_MATRIX.md +26 -22
  9. package/docs/TOOL_CONTRACT.md +97 -32
  10. package/extensions/agent-browser/index.ts +519 -2402
  11. package/extensions/agent-browser/lib/argv-descriptor.ts +90 -0
  12. package/extensions/agent-browser/lib/argv-grammar.ts +128 -0
  13. package/extensions/agent-browser/lib/command-policy.ts +71 -0
  14. package/extensions/agent-browser/lib/command-taxonomy.ts +336 -0
  15. package/extensions/agent-browser/lib/electron/cleanup.ts +1 -0
  16. package/extensions/agent-browser/lib/executable-path.ts +19 -0
  17. package/extensions/agent-browser/lib/input-modes/job.ts +62 -0
  18. package/extensions/agent-browser/lib/input-modes/params.ts +8 -8
  19. package/extensions/agent-browser/lib/input-modes.ts +3 -0
  20. package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +65 -0
  21. package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +154 -0
  22. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +149 -0
  23. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +77 -29
  24. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +6 -2
  25. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +33 -27
  26. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +74 -23
  27. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +67 -17
  28. package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +93 -0
  29. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +19 -123
  30. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +32 -1
  31. package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +860 -0
  32. package/extensions/agent-browser/lib/playbook.ts +24 -23
  33. package/extensions/agent-browser/lib/prompt-policy.ts +122 -0
  34. package/extensions/agent-browser/lib/results/action-recommendations.ts +3 -23
  35. package/extensions/agent-browser/lib/results/categories.ts +1 -1
  36. package/extensions/agent-browser/lib/results/presentation/navigation.ts +2 -34
  37. package/extensions/agent-browser/lib/results/presentation/registry.ts +34 -6
  38. package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +133 -0
  39. package/extensions/agent-browser/lib/results/presentation.ts +11 -6
  40. package/extensions/agent-browser/lib/runtime.ts +93 -227
  41. package/extensions/agent-browser/lib/session-page-state.ts +31 -14
  42. package/extensions/agent-browser/lib/temp.ts +148 -23
  43. package/package.json +4 -4
  44. package/scripts/agent-browser-capability-baseline.mjs +198 -1
@@ -1,11 +1,14 @@
1
1
  import { readFile, rm } from "node:fs/promises";
2
2
 
3
+ import { isCloseCommand, isOpenNavigationCommand } from "../../command-taxonomy.js";
3
4
  import { cleanupElectronLaunchResources, inspectElectronLaunchStatus, type ElectronCleanupResult } from "../../electron/cleanup.js";
4
5
  import type { ElectronLaunchRecord } from "../../electron/launch.js";
5
6
  import {
6
7
  analyzeNetworkSourceLookupResults,
7
8
  analyzeQaPresetResults,
8
9
  analyzeSourceLookupResults,
10
+ buildQaCompactPassText,
11
+ extractQaPageContext,
9
12
  redactNetworkSourceLookupAnalysis,
10
13
  } from "../../input-modes.js";
11
14
  import {
@@ -20,6 +23,7 @@ import {
20
23
  mergeSessionArtifactManifest,
21
24
  } from "../../results/artifact-manifest.js";
22
25
  import type { SessionArtifactManifest } from "../../results/contracts.js";
26
+ import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
23
27
  import {
24
28
  commandExplicitlyTargetsAboutBlank,
25
29
  deriveSessionTabTarget,
@@ -35,7 +39,7 @@ import {
35
39
  import type { PersistentSessionArtifactEviction, PersistentSessionArtifactStore } from "../../temp.js";
36
40
  import { writePersistentSessionArtifactFile, writeSecureTempFile } from "../../temp.js";
37
41
  import { isRecord } from "../../parsing.js";
38
- import { hasLaunchScopedTabCorrectionFlag, resolveManagedSessionState } from "../../runtime.js";
42
+ import { createFreshSessionName, hasLaunchScopedTabCorrectionFlag, resolveManagedSessionState } from "../../runtime.js";
39
43
  import {
40
44
  applyOpenResultTabCorrection,
41
45
  buildAboutBlankRecoveryHint,
@@ -59,6 +63,7 @@ import {
59
63
  unwrapPinnedSessionBatchEnvelope,
60
64
  updateTraceOwnerState,
61
65
  } from "./session-state.js";
66
+ import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
62
67
  import {
63
68
  buildScrollNoopDiagnostic,
64
69
  collectComboboxFocusDiagnostic,
@@ -174,6 +179,7 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
174
179
  const { prepared, processResult } = input;
175
180
  const { electronChildProcesses, electronLaunchRecords, sessionPageState, traceOwners } = state;
176
181
  let artifactManifest = state.artifactManifest;
182
+ let freshSessionOrdinal = state.freshSessionOrdinal;
177
183
  let managedSessionActive = state.managedSessionActive;
178
184
  let managedSessionCwd = state.managedSessionCwd;
179
185
  let managedSessionName = state.managedSessionName;
@@ -205,14 +211,28 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
205
211
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
206
212
  updateTraceOwnerState({ command: prepared.executionPlan.commandInfo.command, sessionName: prepared.executionPlan.sessionName, subcommand: prepared.executionPlan.commandInfo.subcommand, succeeded, traceOwners });
207
213
 
208
- if (succeeded && !navigationSummary && shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data)) {
214
+ let clickDispatchDiagnostic: Awaited<ReturnType<typeof collectClickDispatchDiagnostic>>;
215
+ if (succeeded && prepared.clickDispatchProbe) {
216
+ clickDispatchDiagnostic = await collectClickDispatchDiagnostic({ cwd, probe: prepared.clickDispatchProbe, sessionName: prepared.executionPlan.sessionName, signal });
217
+ if (clickDispatchDiagnostic) {
218
+ succeeded = false;
219
+ presentationEnvelope = { ...(presentationEnvelope ?? {}), error: clickDispatchDiagnostic.summary, success: false };
220
+ }
221
+ }
222
+
223
+ if (
224
+ succeeded &&
225
+ !navigationSummary &&
226
+ (shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data) ||
227
+ shouldCaptureSemanticActionNavigationSummary(prepared.compiledSemanticAction, presentationEnvelope?.data))
228
+ ) {
209
229
  navigationSummary = await collectNavigationSummary({ cwd, sessionName: prepared.executionPlan.sessionName, signal });
210
230
  }
211
231
  if (navigationSummary && presentationEnvelope) presentationEnvelope = { ...presentationEnvelope, data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary) };
212
232
  let overlayBlockerDiagnostic: Awaited<ReturnType<typeof collectOverlayBlockerDiagnostic>>;
213
233
 
214
234
  let openResultTabCorrection: Awaited<ReturnType<typeof collectOpenResultTabCorrection>>;
215
- if (succeeded && prepared.executionPlan.sessionName && hasLaunchScopedTabCorrectionFlag(prepared.runtimeToolArgs) && ["goto", "navigate", "open"].includes(prepared.executionPlan.commandInfo.command ?? "")) {
235
+ if (succeeded && prepared.executionPlan.sessionName && hasLaunchScopedTabCorrectionFlag(prepared.runtimeToolArgs) && isOpenNavigationCommand(prepared.executionPlan.commandInfo.command)) {
216
236
  const targetTitle = extractStringResultField(presentationEnvelope?.data, "title");
217
237
  const targetUrl = extractStringResultField(presentationEnvelope?.data, "url");
218
238
  const plannedTabCorrection = await collectOpenResultTabCorrection({ cwd, sessionName: prepared.executionPlan.sessionName, signal, targetTitle, targetUrl });
@@ -267,7 +287,7 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
267
287
  fillVerificationDiagnostic = await collectFillVerificationDiagnostic({ commandTokens: prepared.commandTokens, cwd, sessionName: prepared.executionPlan.sessionName, signal });
268
288
  electronRefFreshnessDiagnostic = buildElectronRefFreshnessDiagnostic({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, record: electronRecordForCommand, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin });
269
289
  }
270
- if (succeeded && !sessionTabCorrection && !aboutBlankSessionMismatch && !electronRecordForCommand) overlayBlockerDiagnostic = await collectOverlayBlockerDiagnostic({ command: prepared.executionPlan.commandInfo.command, cwd, data: presentationEnvelope?.data, navigationSummary, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
290
+ if (succeeded && !sessionTabCorrection && !aboutBlankSessionMismatch && !electronRecordForCommand && !clickDispatchDiagnostic) overlayBlockerDiagnostic = await collectOverlayBlockerDiagnostic({ command: prepared.executionPlan.commandInfo.command, cwd, data: presentationEnvelope?.data, navigationSummary, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
271
291
  if (succeeded) {
272
292
  selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, cwd, data: presentationEnvelope?.data, sessionName: prepared.executionPlan.sessionName, signal });
273
293
  electronBroadGetTextScopeDiagnostics = collectElectronBroadGetTextScopeDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, currentTarget: currentSessionTabTarget, data: presentationEnvelope?.data, electronLaunchRecords, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName });
@@ -279,8 +299,10 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
279
299
  let currentRefSnapshotInvalidation: SessionRefSnapshotInvalidation | undefined;
280
300
  const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
281
301
  if (prepared.executionPlan.sessionName) {
282
- if (prepared.executionPlan.commandInfo.command === "close" && succeeded) sessionPageState.clearSession(prepared.executionPlan.sessionName);
283
- else if (currentSessionTabTarget) {
302
+ if (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) {
303
+ sessionPageState.clearSession(prepared.executionPlan.sessionName);
304
+ state.closedManagedSessionNames.add(prepared.executionPlan.sessionName);
305
+ } else if (currentSessionTabTarget) {
284
306
  const tabUpdate = sessionPageState.applyTabTarget({ sessionName: prepared.executionPlan.sessionName, target: currentSessionTabTarget, update: sessionPageStateUpdate });
285
307
  if (!tabUpdate.applied && succeeded) sessionPageState.markPinning(prepared.executionPlan.sessionName, "drift");
286
308
  }
@@ -299,11 +321,19 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
299
321
  const priorManagedSessionActive = managedSessionActive;
300
322
  const priorManagedSessionCwd = managedSessionCwd;
301
323
  const priorManagedSessionName = managedSessionName;
302
- const managedSessionState = resolveManagedSessionState({ command: prepared.executionPlan.commandInfo.command, managedSessionName: prepared.executionPlan.managedSessionName, priorActive: priorManagedSessionActive, priorSessionName: priorManagedSessionName, succeeded });
324
+ const commandClosesSession = isCloseCommand(prepared.executionPlan.commandInfo.command);
325
+ const managedCloseSessionName = commandClosesSession && succeeded && prepared.executionPlan.sessionName === priorManagedSessionName
326
+ ? prepared.executionPlan.sessionName
327
+ : prepared.executionPlan.managedSessionName;
328
+ const managedSessionState = resolveManagedSessionState({ command: prepared.executionPlan.commandInfo.command, managedSessionName: managedCloseSessionName, priorActive: priorManagedSessionActive, priorSessionName: priorManagedSessionName, succeeded });
303
329
  const replacedManagedSessionName = managedSessionState.replacedSessionName;
304
330
  managedSessionActive = managedSessionState.active;
305
331
  managedSessionName = managedSessionState.sessionName;
306
- let managedSessionOutcome = buildManagedSessionOutcome({ activeAfter: managedSessionActive, activeBefore: priorManagedSessionActive, attemptedSessionName: prepared.executionPlan.managedSessionName, command: prepared.executionPlan.commandInfo.command, currentSessionName: managedSessionName, previousSessionName: priorManagedSessionName, replacedSessionName: replacedManagedSessionName, sessionMode: prepared.sessionMode, succeeded });
332
+ if (commandClosesSession && succeeded && managedCloseSessionName === priorManagedSessionName && !managedSessionActive) {
333
+ freshSessionOrdinal += 1;
334
+ managedSessionName = createFreshSessionName(state.managedSessionBaseName, state.ephemeralSessionSeed, freshSessionOrdinal);
335
+ }
336
+ let managedSessionOutcome = buildManagedSessionOutcome({ activeAfter: managedSessionActive, activeBefore: priorManagedSessionActive, attemptedSessionName: managedCloseSessionName, command: prepared.executionPlan.commandInfo.command, currentSessionName: managedSessionName, previousSessionName: priorManagedSessionName, replacedSessionName: replacedManagedSessionName, sessionMode: prepared.sessionMode, succeeded });
307
337
  if (prepared.executionPlan.managedSessionName && succeeded) managedSessionCwd = cwd;
308
338
  if (prepared.executionPlan.sessionName && succeeded) {
309
339
  if (openResultTabCorrection || sessionTabCorrection || aboutBlankSessionMismatch?.recoveryApplied) sessionPageState.markPinning(prepared.executionPlan.sessionName, "drift");
@@ -311,7 +341,8 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
311
341
  }
312
342
  if (replacedManagedSessionName) {
313
343
  sessionPageState.clearSession(replacedManagedSessionName);
314
- await closeManagedSession({ cwd: priorManagedSessionCwd, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
344
+ const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
345
+ if (!replacedCloseError) state.closedManagedSessionNames.add(replacedManagedSessionName);
315
346
  }
316
347
 
317
348
  let electronLaunchRecord: ElectronLaunchRecord | undefined;
@@ -341,7 +372,7 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
341
372
  }
342
373
 
343
374
  const errorText = getAgentBrowserErrorText({ aborted: processResult.aborted, command: prepared.executionPlan.commandInfo.command, effectiveArgs: prepared.redactedProcessArgs, envelope: presentationEnvelope, exitCode: processResult.exitCode, parseError, plainTextInspection, staleRefArgs: getStaleRefArgs(prepared.commandTokens, prepared.runtimeToolStdin), spawnError: processResult.spawnError, stderr: processResult.stderr, timedOut: processResult.timedOut, timeoutMs: processResult.timeoutMs, wrapperRecoveryHint: buildWrapperRecoveryHint({ pinnedBatchUnwrapMode: prepared.pinnedBatchUnwrapMode, sessionTabCorrection }) });
344
- const presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text" as const, text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, cwd, envelope: presentationEnvelope, errorText, persistentArtifactStore, sessionName: prepared.executionPlan.sessionName });
375
+ const presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text" as const, text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, persistentArtifactStore, sessionName: prepared.executionPlan.sessionName });
345
376
  if (parseFailureOutput.artifactManifest) { presentation.artifactManifest = parseFailureOutput.artifactManifest; presentation.artifactRetentionSummary = parseFailureOutput.artifactRetentionSummary; }
346
377
  if (parseFailureOutput.fullOutputPath || parseFailureOutput.fullOutputUnavailable) {
347
378
  const existingText = presentation.content[0]?.type === "text" ? presentation.content[0].text : "";
@@ -351,7 +382,9 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
351
382
  }
352
383
  if (presentation.artifactManifest) artifactManifest = presentation.artifactManifest;
353
384
  const qaPreset = prepared.compiledQaPreset ? analyzeQaPresetResults(presentationEnvelope?.data) : undefined;
354
- const qaAttachedTarget = prepared.compiledQaPreset?.checks.attached ? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? prepared.priorSessionTabTarget, cwd, sessionName: prepared.executionPlan.sessionName, signal }) : undefined;
385
+ let qaAttachedTarget = prepared.compiledQaPreset?.checks.attached
386
+ ? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? prepared.priorSessionTabTarget, cwd, sessionName: prepared.executionPlan.sessionName, signal })
387
+ : undefined;
355
388
  const sourceLookupElectronContext = prepared.compiledSourceLookup ? getSourceLookupElectronContext({ currentTarget: currentSessionTabTarget, electronLaunchRecords, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName }) : undefined;
356
389
  const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup, cwd, { electronContext: sourceLookupElectronContext, workspaceRoot: cwd }) : undefined;
357
390
  const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup, cwd)) : undefined;
@@ -359,15 +392,32 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
359
392
  else if (networkSourceLookup) presentation.content.unshift({ type: "text", text: networkSourceLookup.summary });
360
393
  if (sourceLookup && presentation.content[0]?.type === "text") presentation.content[0] = { ...presentation.content[0], text: `${sourceLookup.summary}\n\n${presentation.content[0].text}` };
361
394
  else if (sourceLookup) presentation.content.unshift({ type: "text", text: sourceLookup.summary });
362
- if (qaPreset && (!qaPreset.passed || qaPreset.warnings.length > 0)) {
363
- if (!qaPreset.passed) { succeeded = false; presentation.failureCategory = "qa-failure"; }
395
+ if (qaPreset && !qaPreset.passed) {
396
+ succeeded = false;
397
+ presentation.failureCategory = "qa-failure";
364
398
  presentation.summary = qaPreset.summary;
365
399
  if (presentation.content[0]?.type === "text") presentation.content[0] = { ...presentation.content[0], text: `${qaPreset.summary}\n\n${presentation.content[0].text}` };
366
400
  else presentation.content.unshift({ type: "text", text: qaPreset.summary });
401
+ } else if (qaPreset?.passed && prepared.compiledQaPreset) {
402
+ const compactText = buildQaCompactPassText({
403
+ artifactVerification: presentation.artifactVerification,
404
+ batchStepCount: presentation.batchSteps?.length ?? prepared.compiledQaPreset.steps.length,
405
+ checks: prepared.compiledQaPreset.checks,
406
+ page: extractQaPageContext({
407
+ attachedTarget: qaAttachedTarget,
408
+ batchData: presentationEnvelope?.data,
409
+ compiled: prepared.compiledQaPreset,
410
+ }),
411
+ qaPreset,
412
+ });
413
+ presentation.summary = qaPreset.summary;
414
+ const nonTextContent = presentation.content.filter((item) => item.type !== "text");
415
+ presentation.content = [{ type: "text", text: compactText }, ...nonTextContent];
367
416
  }
368
417
  const qaAttachedTargetText = formatQaAttachedTargetText(qaAttachedTarget);
369
- if (qaAttachedTargetText && presentation.content[0]?.type === "text") presentation.content[0] = { ...presentation.content[0], text: `${qaAttachedTargetText}\n\n${presentation.content[0].text}` };
370
- else if (qaAttachedTargetText) presentation.content.unshift({ type: "text", text: qaAttachedTargetText });
418
+ const skipAttachedTargetBanner = qaPreset?.passed && prepared.compiledQaPreset?.checks.attached;
419
+ if (!skipAttachedTargetBanner && qaAttachedTargetText && presentation.content[0]?.type === "text") presentation.content[0] = { ...presentation.content[0], text: `${qaAttachedTargetText}\n\n${presentation.content[0].text}` };
420
+ else if (!skipAttachedTargetBanner && qaAttachedTargetText) presentation.content.unshift({ type: "text", text: qaAttachedTargetText });
371
421
  if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded) managedSessionOutcome = { ...managedSessionOutcome, succeeded };
372
422
  const evalStdinHint = getEvalStdinHint({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, stdin: prepared.runtimeToolStdin });
373
423
  const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
@@ -377,8 +427,8 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
377
427
  const finalRecoveryState = await prepareFinalResultRecoveryState({ aboutBlankSessionMismatch, batchRefSnapshotState, commandTokens: prepared.commandTokens, compiledSemanticAction: prepared.compiledSemanticAction, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, cwd, electronPostCommandHealth, errorText, executionPlan: prepared.executionPlan, parseError, plainTextInspection, presentation, processResult, redactedProcessArgs: prepared.redactedProcessArgs, runtimeToolArgs: prepared.runtimeToolArgs, sessionPageState, sessionPageStateUpdate, sessionTabCorrection, signal, succeeded });
378
428
  currentRefSnapshot = finalRecoveryState.currentRefSnapshot;
379
429
  currentRefSnapshotInvalidation = finalRecoveryState.currentRefSnapshotInvalidation;
380
- const result = buildFinalAgentBrowserToolResult({ aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, inspectionText, managedSessionOutcome, navigationSummary, networkSourceLookup, noActivePageSnapshotFailure: finalRecoveryState.noActivePageSnapshotFailure, openResultTabCorrection, overlayBlockerDiagnostic, parseError, parseFailureOutput, parseSucceeded, plainTextInspection, presentation, presentationEnvelope, priorSessionTabTarget: prepared.priorSessionTabTarget, processResult, qaAttachedTarget, qaPreset, recordingDependencyWarning, redactedArgs: prepared.redactedArgs, redactedCompiledElectron: prepared.redactedCompiledElectron, redactedCompiledJob: prepared.redactedCompiledJob, redactedCompiledNetworkSourceLookup: prepared.redactedCompiledNetworkSourceLookup, redactedCompiledQaPreset: prepared.redactedCompiledQaPreset, redactedCompiledSemanticAction: prepared.redactedCompiledSemanticAction, redactedCompiledSourceLookup: prepared.redactedCompiledSourceLookup, redactedContent, redactedProcessArgs: prepared.redactedProcessArgs, redactedRecoveryHint: prepared.redactedRecoveryHint, resultArtifactManifest, richInputRecoveryDiagnostic: finalRecoveryState.richInputRecoveryDiagnostic, scrollNoopDiagnostic, selectorTextVisibilityDiagnostics, sessionMode: prepared.sessionMode, sessionTabCorrection, sourceLookup, succeeded, timeoutPartialProgress, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
381
- const statePatch: BrowserRunStatePatch = { artifactManifest, managedSessionActive, managedSessionCwd, managedSessionName };
430
+ const result = buildFinalAgentBrowserToolResult({ aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, clickDispatchDiagnostic, commandTokens: prepared.commandTokens, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, inspectionText, managedSessionOutcome, navigationSummary, networkSourceLookup, noActivePageSnapshotFailure: finalRecoveryState.noActivePageSnapshotFailure, openResultTabCorrection, overlayBlockerDiagnostic, parseError, parseFailureOutput, parseSucceeded, plainTextInspection, presentation, presentationEnvelope, priorSessionTabTarget: prepared.priorSessionTabTarget, processResult, qaAttachedTarget, qaPreset, recordingDependencyWarning, redactedArgs: prepared.redactedArgs, redactedCompiledElectron: prepared.redactedCompiledElectron, redactedCompiledJob: prepared.redactedCompiledJob, redactedCompiledNetworkSourceLookup: prepared.redactedCompiledNetworkSourceLookup, redactedCompiledQaPreset: prepared.redactedCompiledQaPreset, redactedCompiledSemanticAction: prepared.redactedCompiledSemanticAction, redactedCompiledSourceLookup: prepared.redactedCompiledSourceLookup, redactedContent, redactedProcessArgs: prepared.redactedProcessArgs, redactedRecoveryHint: prepared.redactedRecoveryHint, resultArtifactManifest, richInputRecoveryDiagnostic: finalRecoveryState.richInputRecoveryDiagnostic, scrollNoopDiagnostic, selectorTextVisibilityDiagnostics, sessionMode: prepared.sessionMode, sessionTabCorrection, sourceLookup, succeeded, timeoutPartialProgress, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
431
+ const statePatch: BrowserRunStatePatch = { artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCwd, managedSessionName };
382
432
  return { result, statePatch };
383
433
  } finally {
384
434
  if (processResult.stdoutSpillPath) await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
@@ -0,0 +1,93 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+
3
+ import { isCloseCommand } from "../../command-taxonomy.js";
4
+ import { executableExistsOnPath } from "../../executable-path.js";
5
+ import type { SessionArtifactManifest } from "../../results/contracts.js";
6
+ import type { PromptPolicy, PromptRequestedArtifact } from "../../prompt-policy.js";
7
+ import type { SessionRefSnapshot } from "../../session-page-state.js";
8
+ import { findBlockedFinalizingAction, STOP_BOUNDARY_GUARD_SCOPE, type BrowserFinalizingAction } from "./browser-action-model.js";
9
+
10
+ export interface StopBoundaryViolation {
11
+ action: BrowserFinalizingAction;
12
+ command: string[];
13
+ message: string;
14
+ reason: "explicit-user-stop-boundary";
15
+ stepIndex?: number;
16
+ target?: string;
17
+ }
18
+
19
+ export interface RequestedArtifactCloseViolation {
20
+ message: string;
21
+ missingArtifacts: PromptRequestedArtifact[];
22
+ reason: "requested-artifacts-missing-before-close";
23
+ }
24
+
25
+ function formatStopBoundaryActionPhrase(action: BrowserFinalizingAction): string {
26
+ if (action.kind === "keyboard-submit") return "keyboard submit (Enter/Return)";
27
+ return "click-like action";
28
+ }
29
+
30
+ export function findStopBoundaryViolation(options: { commandTokens: string[]; promptPolicy: PromptPolicy; refSnapshot?: SessionRefSnapshot; stdin?: string }): StopBoundaryViolation | undefined {
31
+ if (!options.promptPolicy.stopBoundary) return undefined;
32
+ const blocked = findBlockedFinalizingAction({
33
+ commandTokens: options.commandTokens,
34
+ refSnapshot: options.refSnapshot,
35
+ stdin: options.stdin,
36
+ });
37
+ if (!blocked) return undefined;
38
+ const target = blocked.targetLabel;
39
+ const actionPhrase = formatStopBoundaryActionPhrase(blocked);
40
+ const scopeNote = `Best-effort guard scope covers ${STOP_BOUNDARY_GUARD_SCOPE.covered.join(", ")}; it does not block ${STOP_BOUNDARY_GUARD_SCOPE.excluded.join(", ")}.`;
41
+ if (blocked.stepIndex === undefined) {
42
+ return {
43
+ action: blocked,
44
+ command: blocked.command,
45
+ message: `Blocked likely final submit/order ${actionPhrase} (${target}) because the latest user prompt set an explicit stop boundary. Gather evidence on the current page instead of activating the final action. ${scopeNote}`,
46
+ reason: "explicit-user-stop-boundary",
47
+ target,
48
+ };
49
+ }
50
+ return {
51
+ action: blocked,
52
+ command: blocked.command,
53
+ message: `Blocked likely final submit/order ${actionPhrase} in batch step ${blocked.stepIndex + 1} (${target}) because the latest user prompt set an explicit stop boundary. Gather evidence on the current page instead of activating the final action. ${scopeNote}`,
54
+ reason: "explicit-user-stop-boundary",
55
+ stepIndex: blocked.stepIndex,
56
+ target,
57
+ };
58
+ }
59
+
60
+ function resolveArtifactPath(cwd: string, path: string): string {
61
+ return isAbsolute(path) ? path : resolve(cwd, path);
62
+ }
63
+
64
+ function manifestContainsArtifact(manifest: SessionArtifactManifest | undefined, cwd: string, artifact: PromptRequestedArtifact): boolean {
65
+ if (!manifest) return false;
66
+ const requestedAbsolutePath = resolveArtifactPath(cwd, artifact.path);
67
+ const expectedKind = artifact.kind === "screenshot" ? "image" : "video";
68
+ return manifest.entries.some((entry) => {
69
+ const entryAbsolutePath = entry.absolutePath ?? resolveArtifactPath(cwd, entry.path);
70
+ return entry.storageScope === "explicit-path" && entry.kind === expectedKind && entryAbsolutePath === requestedAbsolutePath && entry.retentionState === "live" && entry.exists === true;
71
+ });
72
+ }
73
+
74
+ async function isArtifactRequired(artifact: PromptRequestedArtifact): Promise<boolean> {
75
+ if (artifact.required) return true;
76
+ return artifact.kind === "recording" && await executableExistsOnPath("ffmpeg");
77
+ }
78
+
79
+ export async function findRequestedArtifactCloseViolation(options: { artifactManifest?: SessionArtifactManifest; command: string | undefined; cwd: string; promptPolicy: PromptPolicy }): Promise<RequestedArtifactCloseViolation | undefined> {
80
+ if (!isCloseCommand(options.command)) return undefined;
81
+ const missingArtifacts: PromptRequestedArtifact[] = [];
82
+ for (const artifact of options.promptPolicy.requestedArtifacts) {
83
+ if (!await isArtifactRequired(artifact)) continue;
84
+ if (!manifestContainsArtifact(options.artifactManifest, options.cwd, artifact)) missingArtifacts.push(artifact);
85
+ }
86
+ if (missingArtifacts.length === 0) return undefined;
87
+ const missingList = missingArtifacts.map((artifact) => `${artifact.kind}: ${artifact.path}`).join(", ");
88
+ return {
89
+ message: `Blocked browser close because requested artifact path${missingArtifacts.length === 1 ? " is" : "s are"} missing or unverified: ${missingList}. Save the requested artifact path first, or report why an optional artifact is unavailable before closing.`,
90
+ missingArtifacts,
91
+ reason: "requested-artifacts-missing-before-close",
92
+ };
93
+ }
@@ -14,8 +14,18 @@ import {
14
14
  type SessionRefSnapshotInvalidation,
15
15
  type SessionTabTarget,
16
16
  } from "../../session-page-state.js";
17
+ import {
18
+ isCloseCommand,
19
+ isElectronPostCommandHealthCommand,
20
+ isNavigationObservableCommandName,
21
+ isRefGuardedCommand,
22
+ isRefInvalidatingBatchCommand,
23
+ isSessionTabPinningExcludedCommand,
24
+ isSessionTabPostCommandCorrectionExcludedCommand,
25
+ } from "../../command-taxonomy.js";
17
26
  import { chooseOpenResultTabCorrection, redactInvocationArgs, type OpenResultTabCorrection } from "../../runtime.js";
18
27
  import { isRecord } from "../../parsing.js";
28
+ import { parseUserBatchStdin } from "../batch-stdin.js";
19
29
  import type {
20
30
  AboutBlankSessionMismatch,
21
31
  BatchCommandStep,
@@ -35,65 +45,8 @@ import type {
35
45
  TraceOwner,
36
46
  } from "./types.js";
37
47
 
38
- export const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
39
48
  export const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
40
49
 
41
- const SESSION_TAB_PINNING_EXCLUDED_COMMANDS = new Set(["close", "goto", "navigate", "open", "session", "tab"]);
42
- const SESSION_TAB_POST_COMMAND_CORRECTION_EXCLUDED_COMMANDS = new Set(["batch", "close", "session", "tab"]);
43
- const REF_INVALIDATING_BATCH_COMMANDS = new Set([
44
- "back",
45
- "check",
46
- "click",
47
- "dblclick",
48
- "drag",
49
- "forward",
50
- "goto",
51
- "keyboard",
52
- "mouse",
53
- "navigate",
54
- "open",
55
- "press",
56
- "reload",
57
- "select",
58
- "type",
59
- "uncheck",
60
- "upload",
61
- ]);
62
- const REF_GUARDED_COMMANDS = new Set([
63
- "check",
64
- "click",
65
- "dblclick",
66
- "download",
67
- "drag",
68
- "fill",
69
- "focus",
70
- "hover",
71
- "keyboard",
72
- "mouse",
73
- "press",
74
- "scrollintoview",
75
- "select",
76
- "type",
77
- "uncheck",
78
- "upload",
79
- ]);
80
- const ELECTRON_POST_COMMAND_HEALTH_COMMANDS = new Set([
81
- "back",
82
- "check",
83
- "click",
84
- "dblclick",
85
- "fill",
86
- "find",
87
- "forward",
88
- "keyboard",
89
- "mouse",
90
- "press",
91
- "reload",
92
- "select",
93
- "type",
94
- "uncheck",
95
- ]);
96
-
97
50
  export function applyBrowserRunStatePatch(state: BrowserRunState, patch: BrowserRunStatePatch | undefined): void {
98
51
  if (!patch) return;
99
52
  if ("artifactManifest" in patch) state.artifactManifest = patch.artifactManifest;
@@ -103,10 +56,6 @@ export function applyBrowserRunStatePatch(state: BrowserRunState, patch: Browser
103
56
  if (patch.managedSessionName !== undefined) state.managedSessionName = patch.managedSessionName;
104
57
  }
105
58
 
106
- export function mergeBrowserRunStatePatch(left: BrowserRunStatePatch | undefined, right: BrowserRunStatePatch | undefined): BrowserRunStatePatch {
107
- return { ...(left ?? {}), ...(right ?? {}) };
108
- }
109
-
110
59
  export function buildSessionDetailFields(sessionName: string | undefined, usedImplicitSession: boolean): Record<string, unknown> {
111
60
  return sessionName ? { sessionName, usedImplicitSession } : {};
112
61
  }
@@ -126,7 +75,7 @@ export function buildManagedSessionOutcome(options: {
126
75
  if (!attemptedSessionName) return undefined;
127
76
  let status: ManagedSessionOutcome["status"];
128
77
  let summary: string;
129
- if (command === "close") {
78
+ if (isCloseCommand(command)) {
130
79
  status = succeeded ? "closed" : activeBefore ? "preserved" : "abandoned";
131
80
  summary = succeeded
132
81
  ? `Managed session ${attemptedSessionName} was closed.`
@@ -238,8 +187,7 @@ export function extractNavigationSummaryFromData(data: unknown): NavigationSumma
238
187
 
239
188
  export function shouldCaptureNavigationSummary(command: string | undefined, data: unknown): boolean {
240
189
  return (
241
- command !== undefined &&
242
- NAVIGATION_SUMMARY_COMMANDS.has(command) &&
190
+ isNavigationObservableCommandName(command) &&
243
191
  (!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string"))
244
192
  );
245
193
  }
@@ -263,58 +211,6 @@ function extractBatchResultCommand(item: Record<string, unknown>): string[] {
263
211
  return Array.isArray(item.command) ? item.command.filter((token): token is string => typeof token === "string") : [];
264
212
  }
265
213
 
266
- function validateUserBatchStep(
267
- step: unknown,
268
- index: number,
269
- ):
270
- | { ok: true; step: BatchCommandStep }
271
- | { ok: false; error: string } {
272
- if (!Array.isArray(step)) {
273
- return {
274
- ok: false,
275
- error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.`,
276
- };
277
- }
278
- if (step.length === 0) {
279
- return {
280
- ok: false,
281
- error: `agent_browser batch stdin step ${index} must not be empty.`,
282
- };
283
- }
284
- const invalidTokenIndex = step.findIndex((token) => typeof token !== "string");
285
- if (invalidTokenIndex !== -1) {
286
- return {
287
- ok: false,
288
- error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.`,
289
- };
290
- }
291
- return { ok: true, step: step as BatchCommandStep };
292
- }
293
-
294
- function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps?: BatchCommandStep[] } {
295
- if (stdin === undefined) {
296
- return { steps: [] };
297
- }
298
- try {
299
- const parsed = JSON.parse(stdin) as unknown;
300
- if (!Array.isArray(parsed)) {
301
- return { error: "agent_browser batch stdin must be a JSON array of command steps." };
302
- }
303
- const steps: BatchCommandStep[] = [];
304
- for (const [index, rawStep] of parsed.entries()) {
305
- const validated = validateUserBatchStep(rawStep, index);
306
- if (!validated.ok) {
307
- return { error: validated.error };
308
- }
309
- steps.push(validated.step);
310
- }
311
- return { steps };
312
- } catch (error) {
313
- const message = error instanceof Error ? error.message : String(error);
314
- return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}` };
315
- }
316
- }
317
-
318
214
  export function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
319
215
  if (commandTokens[0] !== "batch" || stdin === undefined) {
320
216
  return commandTokens;
@@ -331,7 +227,7 @@ function collectRefsFromTokens(tokens: string[]): string[] {
331
227
  }
332
228
 
333
229
  export function getGuardedRefUsage(commandTokens: string[], stdin?: string, options: { includeRefsAfterBatchSnapshot?: boolean } = {}): string[] {
334
- const collectFromStep = (step: string[]) => REF_GUARDED_COMMANDS.has(step[0] ?? "") ? collectRefsFromTokens(step) : [];
230
+ const collectFromStep = (step: string[]) => isRefGuardedCommand(step[0]) ? collectRefsFromTokens(step) : [];
335
231
  if (commandTokens[0] !== "batch" || stdin === undefined) {
336
232
  return collectFromStep(commandTokens);
337
233
  }
@@ -357,10 +253,10 @@ function getBatchRefInvalidationMessage(commandTokens: string[], stdin?: string)
357
253
  priorStepInvalidatesRefs = false;
358
254
  }
359
255
  const refIds = collectRefsFromTokens(step);
360
- if (refIds.length > 0 && REF_GUARDED_COMMANDS.has(step[0] ?? "") && priorStepInvalidatesRefs) {
256
+ if (refIds.length > 0 && isRefGuardedCommand(step[0]) && priorStepInvalidatesRefs) {
361
257
  return `Batch step ${step[0]} uses page-scoped ref ${refIds.map((refId) => `@${refId}`).join(", ")} after an earlier batch step can navigate or mutate the page. Split the batch, run snapshot -i after the page-changing step, then retry with current refs.`;
362
258
  }
363
- if (REF_INVALIDATING_BATCH_COMMANDS.has(step[0] ?? "")) {
259
+ if (isRefInvalidatingBatchCommand(step[0])) {
364
260
  priorStepInvalidatesRefs = true;
365
261
  }
366
262
  }
@@ -438,7 +334,7 @@ export function shouldPinSessionTabForCommand(options: {
438
334
  options.pinningRequired === true &&
439
335
  options.sessionName !== undefined &&
440
336
  options.command !== undefined &&
441
- !SESSION_TAB_PINNING_EXCLUDED_COMMANDS.has(options.command) &&
337
+ !isSessionTabPinningExcludedCommand(options.command) &&
442
338
  supportsPinnedStdinCommand(options)
443
339
  );
444
340
  }
@@ -464,7 +360,7 @@ export function buildPinnedBatchPlan(options: {
464
360
  if (options.commandTokens.length === 0) {
465
361
  return undefined;
466
362
  }
467
- const includeNavigationSummary = options.command !== undefined && NAVIGATION_SUMMARY_COMMANDS.has(options.command);
363
+ const includeNavigationSummary = isNavigationObservableCommandName(options.command);
468
364
  const tabSelectionStep: BatchCommandStep = ["tab", options.selectedTab];
469
365
  const commandStep = options.commandTokens as BatchCommandStep;
470
366
  const navigationSummarySteps: BatchCommandStep[] = includeNavigationSummary ? [["eval", NAVIGATION_SUMMARY_EVAL]] : [];
@@ -480,7 +376,7 @@ export function shouldCorrectSessionTabAfterCommand(options: { command?: string;
480
376
  options.pinningRequired === true &&
481
377
  options.sessionName !== undefined &&
482
378
  options.command !== undefined &&
483
- !SESSION_TAB_POST_COMMAND_CORRECTION_EXCLUDED_COMMANDS.has(options.command)
379
+ !isSessionTabPostCommandCorrectionExcludedCommand(options.command)
484
380
  );
485
381
  }
486
382
 
@@ -747,7 +643,7 @@ export function formatElectronSessionMismatchText(mismatch: ElectronSessionMisma
747
643
  }
748
644
 
749
645
  export function shouldInspectElectronPostCommandHealth(command: string | undefined): boolean {
750
- return command !== undefined && ELECTRON_POST_COMMAND_HEALTH_COMMANDS.has(command);
646
+ return isElectronPostCommandHealthCommand(command);
751
647
  }
752
648
 
753
649
  export function buildElectronLifecycleNextActions(record: ElectronLaunchRecord): AgentBrowserNextAction[] {
@@ -20,7 +20,9 @@ import type { SessionArtifactManifest } from "../../results/contracts.js";
20
20
  import type { RichInputRecoveryDiagnostic, VisibleRefFallbackDiagnostic } from "../../results/selector-recovery.js";
21
21
  import type { SessionPageState, SessionRefSnapshot, SessionRefSnapshotInvalidation, SessionTabTarget } from "../../session-page-state.js";
22
22
  import type { buildExecutionPlan, CompatibilityWorkaround, OpenResultTabCorrection } from "../../runtime.js";
23
+ import type { PromptPolicy } from "../../prompt-policy.js";
23
24
  import type { AgentBrowserExecuteParams, ResolvedAgentBrowserValidInput } from "../input-plan.js";
25
+ import type { BatchCommandStep } from "../batch-stdin.js";
24
26
 
25
27
  export type AgentBrowserToolResult = AgentToolResult<unknown> & { isError?: boolean };
26
28
  export type AgentBrowserProcessResult = Awaited<ReturnType<typeof runAgentBrowserProcess>>;
@@ -30,7 +32,7 @@ export type AgentBrowserResultCategoryDetails = ReturnType<typeof buildAgentBrow
30
32
 
31
33
  export type TraceOwner = "profiler" | "trace";
32
34
  export type PinnedBatchUnwrapMode = "single-command" | "user-batch";
33
- export type BatchCommandStep = [string, ...string[]];
35
+ export type { BatchCommandStep } from "../batch-stdin.js";
34
36
 
35
37
  export interface BrowserRunContext {
36
38
  cwd: string;
@@ -61,6 +63,7 @@ export interface BrowserRunInputFields {
61
63
 
62
64
  export interface BrowserRunState {
63
65
  artifactManifest?: SessionArtifactManifest;
66
+ closedManagedSessionNames: Set<string>;
64
67
  electronChildProcesses: Map<string, ChildProcess>;
65
68
  electronLaunchRecords: Map<string, ElectronLaunchRecord>;
66
69
  ephemeralSessionSeed: string;
@@ -91,6 +94,7 @@ export interface BrowserRunOptions {
91
94
  input: ResolvedAgentBrowserValidInput;
92
95
  onUpdate?: (result: AgentToolResult<unknown>) => void;
93
96
  params: AgentBrowserExecuteParams;
97
+ promptPolicy: PromptPolicy;
94
98
  sessionPageStateUpdate: ReturnType<SessionPageState["beginUpdate"]>;
95
99
  signal?: AbortSignal;
96
100
  state: BrowserRunState;
@@ -120,6 +124,24 @@ export interface OverlayBlockerDiagnostic {
120
124
  summary: string;
121
125
  }
122
126
 
127
+ export interface ClickDispatchProbeTarget {
128
+ kind: "selector" | "xpath";
129
+ selector: string;
130
+ }
131
+
132
+ export interface ClickDispatchProbe {
133
+ marker: string;
134
+ target: ClickDispatchProbeTarget;
135
+ }
136
+
137
+ export interface ClickDispatchDiagnostic {
138
+ nativeEventCount: number;
139
+ reason: "native-click-produced-no-target-dom-event";
140
+ status: "no-native-event-observed";
141
+ summary: string;
142
+ target: ClickDispatchProbeTarget;
143
+ }
144
+
123
145
  export interface SelectorTextVisibilityDiagnostic {
124
146
  firstMatchVisible?: boolean;
125
147
  firstVisibleTextPreview?: string;
@@ -140,11 +162,17 @@ export interface ElectronBroadGetTextScopeDiagnostic {
140
162
  }
141
163
 
142
164
  export interface QaAttachedTarget {
165
+ error?: string;
143
166
  sessionName: string;
144
167
  title?: string;
145
168
  url?: string;
146
169
  }
147
170
 
171
+ export interface QaAttachedPreconditionFailure {
172
+ error: string;
173
+ nextActions: AgentBrowserNextAction[];
174
+ }
175
+
148
176
  export interface TimeoutArtifactEvidence {
149
177
  absolutePath: string;
150
178
  exists: boolean;
@@ -347,6 +375,7 @@ export interface PreparedBrowserRun {
347
375
  exactSensitiveValues: string[];
348
376
  executionPlan: AgentBrowserExecutionPlan;
349
377
  includePinnedNavigationSummary: boolean;
378
+ clickDispatchProbe?: ClickDispatchProbe;
350
379
  pinnedBatchUnwrapMode?: PinnedBatchUnwrapMode;
351
380
  preparedArgs: PreparedAgentBrowserArgs;
352
381
  priorRefSnapshotState?: SessionRefSnapshot;
@@ -411,6 +440,8 @@ export interface FinalResultInput {
411
440
  aboutBlankSessionMismatch?: AboutBlankSessionMismatch;
412
441
  artifactCleanup?: ArtifactCleanupGuidance;
413
442
  categoryDetails: AgentBrowserResultCategoryDetails;
443
+ clickDispatchDiagnostic?: ClickDispatchDiagnostic;
444
+ commandTokens: string[];
414
445
  comboboxFocusDiagnostic?: ComboboxFocusDiagnostic;
415
446
  compiledNetworkSourceLookup?: CompiledAgentBrowserNetworkSourceLookup;
416
447
  compiledSemanticAction?: CompiledAgentBrowserSemanticAction;