pi-agent-browser-native 0.2.34 → 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 (38) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +14 -14
  3. package/docs/ARCHITECTURE.md +19 -13
  4. package/docs/COMMAND_REFERENCE.md +257 -42
  5. package/docs/ELECTRON.md +3 -3
  6. package/docs/RELEASE.md +11 -11
  7. package/docs/REQUIREMENTS.md +5 -5
  8. package/docs/SUPPORT_MATRIX.md +23 -21
  9. package/docs/TOOL_CONTRACT.md +38 -27
  10. package/extensions/agent-browser/index.ts +518 -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/params.ts +6 -6
  18. package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +65 -0
  19. package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +154 -0
  20. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +149 -0
  21. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +10 -28
  22. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +6 -2
  23. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +33 -27
  24. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +48 -22
  25. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +33 -10
  26. package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +93 -0
  27. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +19 -123
  28. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +26 -1
  29. package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +860 -0
  30. package/extensions/agent-browser/lib/playbook.ts +9 -9
  31. package/extensions/agent-browser/lib/prompt-policy.ts +122 -0
  32. package/extensions/agent-browser/lib/results/action-recommendations.ts +3 -23
  33. package/extensions/agent-browser/lib/results/presentation/navigation.ts +2 -34
  34. package/extensions/agent-browser/lib/runtime.ts +93 -227
  35. package/extensions/agent-browser/lib/session-page-state.ts +31 -14
  36. package/extensions/agent-browser/lib/temp.ts +148 -23
  37. package/package.json +4 -4
  38. package/scripts/agent-browser-capability-baseline.mjs +198 -1
@@ -26,8 +26,11 @@ import {
26
26
  runSessionCommandData,
27
27
  shouldPinSessionTabForCommand,
28
28
  } from "./session-state.js";
29
+ import { parseBatchStdinJsonArray, parseValidBatchStepEntries } from "../batch-stdin.js";
29
30
  import { buildElectronHostFailureResult, getElectronLaunchFailureCategory, redactRecoveryHint } from "./final-result.js";
31
+ import { prepareClickDispatchProbe } from "./click-dispatch.js";
30
32
  import { collectScrollPositionSnapshot, validateQaAttachedPrecondition } from "./diagnostics.js";
33
+ import { findRequestedArtifactCloseViolation, findStopBoundaryViolation } from "./prompt-guards.js";
31
34
  import type {
32
35
  BrowserRunInputFields,
33
36
  BrowserRunOptions,
@@ -141,19 +144,14 @@ async function prepareBatchScreenshotPaths(args: string[], stdin: string | undef
141
144
  if (commandTokens[0] !== "batch" || stdin === undefined) {
142
145
  return undefined;
143
146
  }
144
- let steps: unknown;
145
- try {
146
- steps = JSON.parse(stdin);
147
- } catch {
148
- return undefined;
149
- }
150
- if (!Array.isArray(steps)) {
147
+ const parsed = parseBatchStdinJsonArray(stdin);
148
+ if (parsed.error || parsed.steps === undefined) {
151
149
  return undefined;
152
150
  }
153
151
 
154
152
  let changed = false;
155
153
  const batchScreenshotPathRequests: Array<ScreenshotPathRequest | undefined> = [];
156
- const preparedSteps = await Promise.all(steps.map(async (step, index) => {
154
+ const preparedSteps = await Promise.all(parsed.steps.map(async (step, index) => {
157
155
  if (!Array.isArray(step) || !step.every((item) => typeof item === "string") || step[0] !== "screenshot") {
158
156
  return step;
159
157
  }
@@ -282,20 +280,7 @@ export function validateWaitIpcTimeoutContract(commandTokens: string[], stdin: s
282
280
  if (commandTokens[0] !== "batch" || stdin === undefined) {
283
281
  return undefined;
284
282
  }
285
- let steps: unknown;
286
- try {
287
- steps = JSON.parse(stdin);
288
- } catch {
289
- return undefined;
290
- }
291
- if (!Array.isArray(steps)) {
292
- return undefined;
293
- }
294
- for (let index = 0; index < steps.length; index += 1) {
295
- const step = steps[index];
296
- if (!Array.isArray(step) || !step.every((item) => typeof item === "string")) {
297
- continue;
298
- }
283
+ for (const { index, step } of parseValidBatchStepEntries(stdin)) {
299
284
  const waitTimeout = findWaitTimeoutMs(step);
300
285
  if (waitTimeout && waitTimeout.timeoutMs > SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS) {
301
286
  return buildIpcUnsafeWaitError(waitTimeout.source, waitTimeout.timeoutMs, index);
@@ -528,6 +513,43 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
528
513
  const resolvedSemanticActionRefSnapshot: SessionRefSnapshot | undefined = semanticActionVisibleRefResolution?.snapshot
529
514
  ? { ...semanticActionVisibleRefResolution.snapshot, target: semanticActionVisibleRefResolution.snapshot.target ?? priorSessionTabTarget }
530
515
  : undefined;
516
+ const promptRefSnapshot = resolvedSemanticActionRefSnapshot ?? priorRefSnapshotState;
517
+ const stopBoundaryViolation = findStopBoundaryViolation({ commandTokens, promptPolicy: options.promptPolicy, refSnapshot: promptRefSnapshot, stdin: runtimeToolStdin });
518
+ if (stopBoundaryViolation) {
519
+ return { kind: "early-result", statePatch, result: {
520
+ content: [{ type: "text", text: stopBoundaryViolation.message }],
521
+ details: {
522
+ args: redactedArgs,
523
+ command: executionPlan.commandInfo.command,
524
+ compatibilityWorkaround,
525
+ effectiveArgs: redactedEffectiveArgs,
526
+ promptGuard: stopBoundaryViolation,
527
+ sessionMode,
528
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: stopBoundaryViolation.message, failureCategory: "policy-blocked", succeeded: false, validationError: stopBoundaryViolation.message }),
529
+ validationError: stopBoundaryViolation.message,
530
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
531
+ },
532
+ isError: true,
533
+ } };
534
+ }
535
+ const requestedArtifactCloseViolation = await findRequestedArtifactCloseViolation({ artifactManifest: state.artifactManifest, command: executionPlan.commandInfo.command, cwd, promptPolicy: options.promptPolicy });
536
+ if (requestedArtifactCloseViolation) {
537
+ return { kind: "early-result", statePatch, result: {
538
+ content: [{ type: "text", text: requestedArtifactCloseViolation.message }],
539
+ details: {
540
+ args: redactedArgs,
541
+ command: executionPlan.commandInfo.command,
542
+ compatibilityWorkaround,
543
+ effectiveArgs: redactedEffectiveArgs,
544
+ promptGuard: requestedArtifactCloseViolation,
545
+ sessionMode,
546
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: requestedArtifactCloseViolation.message, failureCategory: "policy-blocked", succeeded: false, validationError: requestedArtifactCloseViolation.message }),
547
+ validationError: requestedArtifactCloseViolation.message,
548
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
549
+ },
550
+ isError: true,
551
+ } };
552
+ }
531
553
  const staleRefPreflight = buildStaleRefPreflight({
532
554
  commandTokens,
533
555
  currentTarget: priorSessionTabTarget,
@@ -676,6 +698,9 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
676
698
  }
677
699
  }
678
700
  }
701
+ const clickDispatchProbe = pinnedBatchUnwrapMode === undefined && compiledElectron === undefined
702
+ ? await prepareClickDispatchProbe({ commandTokens, cwd, sessionName: executionPlan.sessionName, signal })
703
+ : undefined;
679
704
  const redactedProcessArgs = redactInvocationArgs(processArgs);
680
705
  const shouldProbeScrollNoop = executionPlan.commandInfo.command === "scroll" && executionPlan.startupScopedFlags.length === 0;
681
706
  const scrollPositionBefore = shouldProbeScrollNoop
@@ -702,6 +727,7 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
702
727
  compiledSemanticAction,
703
728
  compiledSourceLookup,
704
729
  compatibilityWorkaround,
730
+ clickDispatchProbe,
705
731
  electronLaunch,
706
732
  exactSensitiveValues,
707
733
  executionPlan,
@@ -1,5 +1,6 @@
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 {
@@ -38,7 +39,7 @@ import {
38
39
  import type { PersistentSessionArtifactEviction, PersistentSessionArtifactStore } from "../../temp.js";
39
40
  import { writePersistentSessionArtifactFile, writeSecureTempFile } from "../../temp.js";
40
41
  import { isRecord } from "../../parsing.js";
41
- import { hasLaunchScopedTabCorrectionFlag, resolveManagedSessionState } from "../../runtime.js";
42
+ import { createFreshSessionName, hasLaunchScopedTabCorrectionFlag, resolveManagedSessionState } from "../../runtime.js";
42
43
  import {
43
44
  applyOpenResultTabCorrection,
44
45
  buildAboutBlankRecoveryHint,
@@ -62,6 +63,7 @@ import {
62
63
  unwrapPinnedSessionBatchEnvelope,
63
64
  updateTraceOwnerState,
64
65
  } from "./session-state.js";
66
+ import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
65
67
  import {
66
68
  buildScrollNoopDiagnostic,
67
69
  collectComboboxFocusDiagnostic,
@@ -177,6 +179,7 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
177
179
  const { prepared, processResult } = input;
178
180
  const { electronChildProcesses, electronLaunchRecords, sessionPageState, traceOwners } = state;
179
181
  let artifactManifest = state.artifactManifest;
182
+ let freshSessionOrdinal = state.freshSessionOrdinal;
180
183
  let managedSessionActive = state.managedSessionActive;
181
184
  let managedSessionCwd = state.managedSessionCwd;
182
185
  let managedSessionName = state.managedSessionName;
@@ -208,6 +211,15 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
208
211
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
209
212
  updateTraceOwnerState({ command: prepared.executionPlan.commandInfo.command, sessionName: prepared.executionPlan.sessionName, subcommand: prepared.executionPlan.commandInfo.subcommand, succeeded, traceOwners });
210
213
 
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
+
211
223
  if (
212
224
  succeeded &&
213
225
  !navigationSummary &&
@@ -220,7 +232,7 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
220
232
  let overlayBlockerDiagnostic: Awaited<ReturnType<typeof collectOverlayBlockerDiagnostic>>;
221
233
 
222
234
  let openResultTabCorrection: Awaited<ReturnType<typeof collectOpenResultTabCorrection>>;
223
- 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)) {
224
236
  const targetTitle = extractStringResultField(presentationEnvelope?.data, "title");
225
237
  const targetUrl = extractStringResultField(presentationEnvelope?.data, "url");
226
238
  const plannedTabCorrection = await collectOpenResultTabCorrection({ cwd, sessionName: prepared.executionPlan.sessionName, signal, targetTitle, targetUrl });
@@ -275,7 +287,7 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
275
287
  fillVerificationDiagnostic = await collectFillVerificationDiagnostic({ commandTokens: prepared.commandTokens, cwd, sessionName: prepared.executionPlan.sessionName, signal });
276
288
  electronRefFreshnessDiagnostic = buildElectronRefFreshnessDiagnostic({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, record: electronRecordForCommand, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin });
277
289
  }
278
- 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 });
279
291
  if (succeeded) {
280
292
  selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, cwd, data: presentationEnvelope?.data, sessionName: prepared.executionPlan.sessionName, signal });
281
293
  electronBroadGetTextScopeDiagnostics = collectElectronBroadGetTextScopeDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, currentTarget: currentSessionTabTarget, data: presentationEnvelope?.data, electronLaunchRecords, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName });
@@ -287,8 +299,10 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
287
299
  let currentRefSnapshotInvalidation: SessionRefSnapshotInvalidation | undefined;
288
300
  const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
289
301
  if (prepared.executionPlan.sessionName) {
290
- if (prepared.executionPlan.commandInfo.command === "close" && succeeded) sessionPageState.clearSession(prepared.executionPlan.sessionName);
291
- 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) {
292
306
  const tabUpdate = sessionPageState.applyTabTarget({ sessionName: prepared.executionPlan.sessionName, target: currentSessionTabTarget, update: sessionPageStateUpdate });
293
307
  if (!tabUpdate.applied && succeeded) sessionPageState.markPinning(prepared.executionPlan.sessionName, "drift");
294
308
  }
@@ -307,11 +321,19 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
307
321
  const priorManagedSessionActive = managedSessionActive;
308
322
  const priorManagedSessionCwd = managedSessionCwd;
309
323
  const priorManagedSessionName = managedSessionName;
310
- 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 });
311
329
  const replacedManagedSessionName = managedSessionState.replacedSessionName;
312
330
  managedSessionActive = managedSessionState.active;
313
331
  managedSessionName = managedSessionState.sessionName;
314
- 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 });
315
337
  if (prepared.executionPlan.managedSessionName && succeeded) managedSessionCwd = cwd;
316
338
  if (prepared.executionPlan.sessionName && succeeded) {
317
339
  if (openResultTabCorrection || sessionTabCorrection || aboutBlankSessionMismatch?.recoveryApplied) sessionPageState.markPinning(prepared.executionPlan.sessionName, "drift");
@@ -319,7 +341,8 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
319
341
  }
320
342
  if (replacedManagedSessionName) {
321
343
  sessionPageState.clearSession(replacedManagedSessionName);
322
- 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);
323
346
  }
324
347
 
325
348
  let electronLaunchRecord: ElectronLaunchRecord | undefined;
@@ -404,8 +427,8 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
404
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 });
405
428
  currentRefSnapshot = finalRecoveryState.currentRefSnapshot;
406
429
  currentRefSnapshotInvalidation = finalRecoveryState.currentRefSnapshotInvalidation;
407
- 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 });
408
- 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 };
409
432
  return { result, statePatch };
410
433
  } finally {
411
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;
@@ -353,6 +375,7 @@ export interface PreparedBrowserRun {
353
375
  exactSensitiveValues: string[];
354
376
  executionPlan: AgentBrowserExecutionPlan;
355
377
  includePinnedNavigationSummary: boolean;
378
+ clickDispatchProbe?: ClickDispatchProbe;
356
379
  pinnedBatchUnwrapMode?: PinnedBatchUnwrapMode;
357
380
  preparedArgs: PreparedAgentBrowserArgs;
358
381
  priorRefSnapshotState?: SessionRefSnapshot;
@@ -417,6 +440,8 @@ export interface FinalResultInput {
417
440
  aboutBlankSessionMismatch?: AboutBlankSessionMismatch;
418
441
  artifactCleanup?: ArtifactCleanupGuidance;
419
442
  categoryDetails: AgentBrowserResultCategoryDetails;
443
+ clickDispatchDiagnostic?: ClickDispatchDiagnostic;
444
+ commandTokens: string[];
420
445
  comboboxFocusDiagnostic?: ComboboxFocusDiagnostic;
421
446
  compiledNetworkSourceLookup?: CompiledAgentBrowserNetworkSourceLookup;
422
447
  compiledSemanticAction?: CompiledAgentBrowserSemanticAction;