pi-agent-browser-native 0.2.34 → 0.2.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +25 -15
- package/docs/ARCHITECTURE.md +19 -13
- package/docs/COMMAND_REFERENCE.md +274 -44
- package/docs/ELECTRON.md +3 -3
- package/docs/RELEASE.md +11 -11
- package/docs/REQUIREMENTS.md +5 -5
- package/docs/SUPPORT_MATRIX.md +43 -24
- package/docs/TOOL_CONTRACT.md +50 -30
- package/extensions/agent-browser/index.ts +518 -2402
- package/extensions/agent-browser/lib/argv-descriptor.ts +90 -0
- package/extensions/agent-browser/lib/argv-grammar.ts +128 -0
- package/extensions/agent-browser/lib/command-policy.ts +71 -0
- package/extensions/agent-browser/lib/command-taxonomy.ts +336 -0
- package/extensions/agent-browser/lib/electron/cleanup.ts +1 -0
- package/extensions/agent-browser/lib/executable-path.ts +19 -0
- package/extensions/agent-browser/lib/input-modes/params.ts +6 -6
- package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +65 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +154 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +149 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +56 -30
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +13 -3
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +33 -27
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +48 -22
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +39 -10
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +93 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +98 -124
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +40 -1
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +860 -0
- package/extensions/agent-browser/lib/playbook.ts +10 -10
- package/extensions/agent-browser/lib/prompt-policy.ts +122 -0
- package/extensions/agent-browser/lib/results/action-recommendations.ts +3 -23
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +2 -34
- package/extensions/agent-browser/lib/runtime.ts +93 -227
- package/extensions/agent-browser/lib/session-page-state.ts +31 -14
- package/extensions/agent-browser/lib/temp.ts +148 -23
- package/package.json +4 -4
- package/scripts/agent-browser-capability-baseline.mjs +198 -1
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
} from "../../session-page-state.js";
|
|
44
44
|
import { extractExplicitSessionName, redactInvocationArgs, redactSensitiveText, redactSensitiveValue, type OpenResultTabCorrection } from "../../runtime.js";
|
|
45
45
|
import { isRecord } from "../../parsing.js";
|
|
46
|
+
import { buildClickDispatchNextActions, formatClickDispatchDiagnosticText } from "./click-dispatch.js";
|
|
46
47
|
import {
|
|
47
48
|
buildComboboxFocusNextActions,
|
|
48
49
|
buildElectronBroadGetTextScopeNextActions,
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
formatArtifactCleanupGuidanceText,
|
|
56
57
|
formatComboboxFocusDiagnosticText,
|
|
57
58
|
formatElectronBroadGetTextScopeText,
|
|
59
|
+
formatEvalResultWarningText,
|
|
58
60
|
formatEvalStdinHintText,
|
|
59
61
|
formatFillVerificationText,
|
|
60
62
|
formatOverlayBlockerText,
|
|
@@ -68,6 +70,7 @@ import {
|
|
|
68
70
|
buildElectronLifecycleNextActions,
|
|
69
71
|
buildElectronMismatchNextActions,
|
|
70
72
|
buildElectronRefFreshnessNextActions,
|
|
73
|
+
buildManagedSessionFreshFailureNextActions,
|
|
71
74
|
buildManagedSessionOutcome,
|
|
72
75
|
buildSessionDetailFields,
|
|
73
76
|
formatElectronPostCommandHealthText,
|
|
@@ -212,7 +215,7 @@ export function buildElectronHostFailureResult(options: {
|
|
|
212
215
|
return { content: [{ type: "text", text: redactSensitiveText(text) }], details: redactToolDetails(details, []), isError: true };
|
|
213
216
|
}
|
|
214
217
|
|
|
215
|
-
function formatElectronTargetLines(targets: ElectronCdpTarget[], limit = 8): string[] {
|
|
218
|
+
export function formatElectronTargetLines(targets: ElectronCdpTarget[], limit = 8): string[] {
|
|
216
219
|
const shownTargets = targets.slice(0, limit);
|
|
217
220
|
const lines = shownTargets.map((target) => {
|
|
218
221
|
const label = [target.type, target.title].filter(Boolean).join(" ") || target.id || "target";
|
|
@@ -319,8 +322,10 @@ function buildResultNextActions(options: FinalResultInput): AgentBrowserNextActi
|
|
|
319
322
|
if (options.selectorTextVisibilityDiagnostics.length > 0) nextActionCollector.append(buildSelectorTextVisibilityNextActions({ diagnostics: options.selectorTextVisibilityDiagnostics, sessionName: options.executionPlan.sessionName }));
|
|
320
323
|
if (options.electronBroadGetTextScopeDiagnostics.length > 0) nextActionCollector.append(buildElectronBroadGetTextScopeNextActions({ diagnostics: options.electronBroadGetTextScopeDiagnostics, sessionName: options.executionPlan.sessionName }));
|
|
321
324
|
if (options.sourceLookup?.electronContext) nextActionCollector.appendUnique(buildSourceLookupElectronNextActions(options.sourceLookup));
|
|
325
|
+
if (options.clickDispatchDiagnostic) nextActionCollector.append(buildClickDispatchNextActions({ commandTokens: options.commandTokens, sessionName: options.executionPlan.sessionName }));
|
|
322
326
|
if (options.scrollNoopDiagnostic) nextActionCollector.append(buildScrollNoopNextActions(options.executionPlan.sessionName));
|
|
323
327
|
if (options.comboboxFocusDiagnostic) nextActionCollector.append(buildComboboxFocusNextActions(options.executionPlan.sessionName));
|
|
328
|
+
if (options.managedSessionOutcome) nextActionCollector.appendUnique(buildManagedSessionFreshFailureNextActions(options.managedSessionOutcome));
|
|
324
329
|
if (options.categoryDetails.failureCategory === "stale-ref" && options.redactedCompiledSemanticAction && isCompiledSemanticActionFindCommand(options.compiledSemanticAction)) nextActionCollector.append([{ id: "retry-semantic-action-after-stale-ref", params: { args: options.redactedCompiledSemanticAction.args }, reason: "Retry the same semantic target via its compiled find command after the upstream stale-ref failure proves the prior action did not execute.", safety: "Use only for the same intended target; direct stale @refs still require a fresh snapshot or stable locator before retrying.", tool: "agent_browser" as const }]);
|
|
325
330
|
if (options.electronLaunchRecord) nextActionCollector.append(buildAgentBrowserNextActions({ electron: { launchId: options.electronLaunchRecord.launchId, sessionName: options.electronLaunchRecord.sessionName, status: options.electronLaunchRecord.cleanupState }, failureCategory: options.categoryDetails.failureCategory, resultCategory: options.categoryDetails.resultCategory, successCategory: options.categoryDetails.successCategory }));
|
|
326
331
|
return nextActionCollector.toArray();
|
|
@@ -369,6 +374,7 @@ function buildAgentBrowserResultDetails(options: FinalResultInput, nextActions:
|
|
|
369
374
|
imagePaths: options.presentation.imagePaths,
|
|
370
375
|
nextActions,
|
|
371
376
|
pageChangeSummary,
|
|
377
|
+
clickDispatch: options.clickDispatchDiagnostic,
|
|
372
378
|
overlayBlockers: options.overlayBlockerDiagnostic,
|
|
373
379
|
fillVerification: options.fillVerificationDiagnostic,
|
|
374
380
|
visibleRefFallback: publicVisibleRefFallbackDiagnostic,
|
|
@@ -383,6 +389,7 @@ function buildAgentBrowserResultDetails(options: FinalResultInput, nextActions:
|
|
|
383
389
|
selectorTextVisibility: options.selectorTextVisibilityDiagnostics[0],
|
|
384
390
|
selectorTextVisibilityAll: options.selectorTextVisibilityDiagnostics.length > 1 ? options.selectorTextVisibilityDiagnostics : undefined,
|
|
385
391
|
evalStdinHint: options.evalStdinHint,
|
|
392
|
+
evalResultWarning: options.evalResultWarning,
|
|
386
393
|
timeoutPartialProgress: options.timeoutPartialProgress,
|
|
387
394
|
parseError: options.plainTextInspection ? undefined : options.parseError,
|
|
388
395
|
savedFile: options.presentation.savedFile,
|
|
@@ -411,6 +418,7 @@ export function buildFinalAgentBrowserToolResult(options: FinalResultInput): Age
|
|
|
411
418
|
const visibleRefFallbackText = formatVisibleRefFallbackText(options.visibleRefFallbackDiagnostic);
|
|
412
419
|
const richInputRecoveryText = formatRichInputRecoveryText(options.richInputRecoveryDiagnostic);
|
|
413
420
|
const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
|
|
421
|
+
const clickDispatchText = options.clickDispatchDiagnostic ? formatClickDispatchDiagnosticText(options.clickDispatchDiagnostic) : undefined;
|
|
414
422
|
const overlayBlockerText = options.overlayBlockerDiagnostic ? formatOverlayBlockerText(options.overlayBlockerDiagnostic) : undefined;
|
|
415
423
|
const fillVerificationText = formatFillVerificationText(options.fillVerificationDiagnostic);
|
|
416
424
|
const electronRefFreshnessText = formatElectronRefFreshnessText(options.electronRefFreshnessDiagnostic);
|
|
@@ -420,10 +428,11 @@ export function buildFinalAgentBrowserToolResult(options: FinalResultInput): Age
|
|
|
420
428
|
const comboboxFocusDiagnosticText = formatComboboxFocusDiagnosticText(options.comboboxFocusDiagnostic);
|
|
421
429
|
const recordingDependencyWarningText = formatRecordingDependencyWarningText(options.recordingDependencyWarning);
|
|
422
430
|
const evalStdinHintText = formatEvalStdinHintText(options.evalStdinHint);
|
|
431
|
+
const evalResultWarningText = formatEvalResultWarningText(options.evalResultWarning);
|
|
423
432
|
const artifactCleanupText = formatArtifactCleanupGuidanceText(options.artifactCleanup);
|
|
424
433
|
const timeoutPartialProgressText = options.timeoutPartialProgress ? formatTimeoutPartialProgressText(options.timeoutPartialProgress) : undefined;
|
|
425
434
|
const managedSessionOutcomeText = formatManagedSessionOutcomeText(options.managedSessionOutcome);
|
|
426
|
-
const rawAppendedDiagnosticText = [visibleRefFallbackText, richInputRecoveryText, semanticActionCandidateText, overlayBlockerText, fillVerificationText, electronRefFreshnessText, selectorTextVisibilityText, electronBroadGetTextScopeText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
435
|
+
const rawAppendedDiagnosticText = [visibleRefFallbackText, richInputRecoveryText, semanticActionCandidateText, clickDispatchText, overlayBlockerText, fillVerificationText, electronRefFreshnessText, selectorTextVisibilityText, electronBroadGetTextScopeText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, evalResultWarningText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
427
436
|
const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, options.exactSensitiveValues));
|
|
428
437
|
const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!options.userRequestedJson || options.plainTextInspection);
|
|
429
438
|
let content = shouldAppendDiagnosticText && options.redactedContent[0]?.type === "text" ? [{ ...options.redactedContent[0], text: `${options.redactedContent[0].text}\n\n${appendedDiagnosticText}` }, ...options.redactedContent.slice(1)] : options.redactedContent;
|
|
@@ -439,6 +448,7 @@ export async function buildMissingBinaryFailureResult(options: { compatibilityWo
|
|
|
439
448
|
const errorText = buildMissingBinaryMessage();
|
|
440
449
|
const managedSessionOutcome = buildManagedSessionOutcome({ activeAfter: options.managedSessionActive, activeBefore: options.managedSessionActive, attemptedSessionName: options.executionPlan.managedSessionName, command: options.executionPlan.commandInfo.command, currentSessionName: options.managedSessionName, previousSessionName: options.managedSessionName, sessionMode: options.sessionMode, succeeded: false });
|
|
441
450
|
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
451
|
+
const managedSessionRecoveryNextActions = buildManagedSessionFreshFailureNextActions(managedSessionOutcome);
|
|
442
452
|
let missingBinaryElectronCleanup: ElectronCleanupResult | undefined;
|
|
443
453
|
let missingBinaryElectronRecord: ElectronLaunchRecord | undefined;
|
|
444
454
|
if (options.electronLaunch) {
|
|
@@ -446,5 +456,5 @@ export async function buildMissingBinaryFailureResult(options: { compatibilityWo
|
|
|
446
456
|
missingBinaryElectronRecord = missingBinaryElectronCleanup.record;
|
|
447
457
|
}
|
|
448
458
|
const textParts = [errorText, managedSessionOutcomeText, missingBinaryElectronCleanup ? `Electron cleanup after failed attach: ${missingBinaryElectronCleanup.summary}` : undefined].filter((part): part is string => part !== undefined && part.length > 0);
|
|
449
|
-
return { content: [{ type: "text", text: textParts.join("\n\n") }], details: { args: options.redactedArgs, compatibilityWorkaround: options.compatibilityWorkaround, effectiveArgs: options.redactedProcessArgs, electron: missingBinaryElectronRecord ? { action: "launch" as const, cleanup: missingBinaryElectronCleanup, launch: missingBinaryElectronRecord, status: "failed" as const, targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined, managedSessionOutcome, sessionMode: options.sessionMode, sessionTabCorrection: options.sessionTabCorrection, ...buildAgentBrowserResultCategoryDetails({ args: options.redactedProcessArgs, command: options.executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError: options.processResult.spawnError.message, succeeded: false }), spawnError: options.processResult.spawnError.message }, isError: true };
|
|
459
|
+
return { content: [{ type: "text", text: textParts.join("\n\n") }], details: { args: options.redactedArgs, compatibilityWorkaround: options.compatibilityWorkaround, effectiveArgs: options.redactedProcessArgs, electron: missingBinaryElectronRecord ? { action: "launch" as const, cleanup: missingBinaryElectronCleanup, launch: missingBinaryElectronRecord, status: "failed" as const, targets: options.electronLaunch?.targets, version: options.electronLaunch?.version } : undefined, managedSessionOutcome, nextActions: managedSessionRecoveryNextActions.length > 0 ? managedSessionRecoveryNextActions : undefined, sessionMode: options.sessionMode, sessionTabCorrection: options.sessionTabCorrection, ...buildAgentBrowserResultCategoryDetails({ args: options.redactedProcessArgs, command: options.executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError: options.processResult.spawnError.message, succeeded: false }), spawnError: options.processResult.spawnError.message }, isError: true };
|
|
450
460
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { runAgentBrowserProcess } from "../../process.js";
|
|
2
|
+
import { cleanupClickDispatchProbe } from "./click-dispatch.js";
|
|
2
3
|
import { applyBrowserRunStatePatch } from "./session-state.js";
|
|
3
4
|
import { buildMissingBinaryFailureResult } from "./final-result.js";
|
|
4
5
|
import { prepareBrowserRun } from "./prepare.js";
|
|
5
6
|
import { processBrowserOutput } from "./process-output.js";
|
|
6
7
|
import type { AgentBrowserToolResult, BrowserRunOptions } from "./types.js";
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
+
export { closeManagedSession } from "./session-state.js";
|
|
10
|
+
export type { AgentBrowserToolResult, BrowserRunOptions, BrowserRunState, TraceOwner } from "./types.js";
|
|
9
11
|
|
|
10
12
|
export async function runAgentBrowserTool(options: BrowserRunOptions): Promise<AgentBrowserToolResult> {
|
|
11
13
|
const preparedResult = await prepareBrowserRun(options);
|
|
@@ -15,32 +17,36 @@ export async function runAgentBrowserTool(options: BrowserRunOptions): Promise<A
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
const { prepared } = preparedResult;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
try {
|
|
21
|
+
const processResult = await runAgentBrowserProcess({
|
|
22
|
+
args: prepared.processArgs,
|
|
23
|
+
cwd: options.cwd,
|
|
24
|
+
env: prepared.executionPlan.managedSessionName ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: options.implicitSessionIdleTimeoutMs } : undefined,
|
|
25
|
+
signal: options.signal,
|
|
26
|
+
stdin: prepared.processStdin,
|
|
27
|
+
});
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
const missingBinaryResult = await buildMissingBinaryFailureResult({
|
|
30
|
+
compatibilityWorkaround: prepared.compatibilityWorkaround,
|
|
31
|
+
electronLaunch: prepared.electronLaunch,
|
|
32
|
+
executionPlan: prepared.executionPlan,
|
|
33
|
+
implicitSessionCloseTimeoutMs: options.implicitSessionCloseTimeoutMs,
|
|
34
|
+
managedSessionActive: options.state.managedSessionActive,
|
|
35
|
+
managedSessionName: options.state.managedSessionName,
|
|
36
|
+
processResult,
|
|
37
|
+
redactedArgs: prepared.redactedArgs,
|
|
38
|
+
redactedProcessArgs: prepared.redactedProcessArgs,
|
|
39
|
+
sessionMode: prepared.sessionMode,
|
|
40
|
+
sessionTabCorrection: prepared.sessionTabCorrection,
|
|
41
|
+
});
|
|
42
|
+
if (missingBinaryResult) {
|
|
43
|
+
return missingBinaryResult;
|
|
44
|
+
}
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
const output = await processBrowserOutput({ ...options, prepared, processResult });
|
|
47
|
+
applyBrowserRunStatePatch(options.state, output.statePatch);
|
|
48
|
+
return output.result;
|
|
49
|
+
} finally {
|
|
50
|
+
await cleanupClickDispatchProbe({ cwd: options.cwd, probe: prepared.clickDispatchProbe, sessionName: prepared.executionPlan.sessionName });
|
|
51
|
+
}
|
|
46
52
|
}
|
|
@@ -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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
closeManagedSession,
|
|
51
52
|
collectOpenResultTabCorrection,
|
|
52
53
|
collectSessionTabSelection,
|
|
54
|
+
extractNavigationSummaryFromData,
|
|
53
55
|
extractStringResultField,
|
|
54
56
|
findElectronLaunchRecordForSession,
|
|
55
57
|
formatElectronPostCommandHealthText,
|
|
@@ -62,6 +64,7 @@ import {
|
|
|
62
64
|
unwrapPinnedSessionBatchEnvelope,
|
|
63
65
|
updateTraceOwnerState,
|
|
64
66
|
} from "./session-state.js";
|
|
67
|
+
import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
|
|
65
68
|
import {
|
|
66
69
|
buildScrollNoopDiagnostic,
|
|
67
70
|
collectComboboxFocusDiagnostic,
|
|
@@ -77,6 +80,7 @@ import {
|
|
|
77
80
|
collectTimeoutPartialProgress,
|
|
78
81
|
formatQaAttachedTargetText,
|
|
79
82
|
getArtifactCleanupGuidance,
|
|
83
|
+
getEvalResultWarning,
|
|
80
84
|
getEvalStdinHint,
|
|
81
85
|
getSourceLookupElectronContext,
|
|
82
86
|
sleepMs,
|
|
@@ -177,6 +181,7 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
177
181
|
const { prepared, processResult } = input;
|
|
178
182
|
const { electronChildProcesses, electronLaunchRecords, sessionPageState, traceOwners } = state;
|
|
179
183
|
let artifactManifest = state.artifactManifest;
|
|
184
|
+
let freshSessionOrdinal = state.freshSessionOrdinal;
|
|
180
185
|
let managedSessionActive = state.managedSessionActive;
|
|
181
186
|
let managedSessionCwd = state.managedSessionCwd;
|
|
182
187
|
let managedSessionName = state.managedSessionName;
|
|
@@ -208,6 +213,15 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
208
213
|
const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
|
|
209
214
|
updateTraceOwnerState({ command: prepared.executionPlan.commandInfo.command, sessionName: prepared.executionPlan.sessionName, subcommand: prepared.executionPlan.commandInfo.subcommand, succeeded, traceOwners });
|
|
210
215
|
|
|
216
|
+
let clickDispatchDiagnostic: Awaited<ReturnType<typeof collectClickDispatchDiagnostic>>;
|
|
217
|
+
if (succeeded && prepared.clickDispatchProbe) {
|
|
218
|
+
clickDispatchDiagnostic = await collectClickDispatchDiagnostic({ cwd, probe: prepared.clickDispatchProbe, sessionName: prepared.executionPlan.sessionName, signal });
|
|
219
|
+
if (clickDispatchDiagnostic) {
|
|
220
|
+
succeeded = false;
|
|
221
|
+
presentationEnvelope = { ...(presentationEnvelope ?? {}), error: clickDispatchDiagnostic.summary, success: false };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
211
225
|
if (
|
|
212
226
|
succeeded &&
|
|
213
227
|
!navigationSummary &&
|
|
@@ -220,7 +234,7 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
220
234
|
let overlayBlockerDiagnostic: Awaited<ReturnType<typeof collectOverlayBlockerDiagnostic>>;
|
|
221
235
|
|
|
222
236
|
let openResultTabCorrection: Awaited<ReturnType<typeof collectOpenResultTabCorrection>>;
|
|
223
|
-
if (succeeded && prepared.executionPlan.sessionName && hasLaunchScopedTabCorrectionFlag(prepared.runtimeToolArgs) &&
|
|
237
|
+
if (succeeded && prepared.executionPlan.sessionName && hasLaunchScopedTabCorrectionFlag(prepared.runtimeToolArgs) && isOpenNavigationCommand(prepared.executionPlan.commandInfo.command)) {
|
|
224
238
|
const targetTitle = extractStringResultField(presentationEnvelope?.data, "title");
|
|
225
239
|
const targetUrl = extractStringResultField(presentationEnvelope?.data, "url");
|
|
226
240
|
const plannedTabCorrection = await collectOpenResultTabCorrection({ cwd, sessionName: prepared.executionPlan.sessionName, signal, targetTitle, targetUrl });
|
|
@@ -275,7 +289,7 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
275
289
|
fillVerificationDiagnostic = await collectFillVerificationDiagnostic({ commandTokens: prepared.commandTokens, cwd, sessionName: prepared.executionPlan.sessionName, signal });
|
|
276
290
|
electronRefFreshnessDiagnostic = buildElectronRefFreshnessDiagnostic({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, record: electronRecordForCommand, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin });
|
|
277
291
|
}
|
|
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 });
|
|
292
|
+
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
293
|
if (succeeded) {
|
|
280
294
|
selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, cwd, data: presentationEnvelope?.data, sessionName: prepared.executionPlan.sessionName, signal });
|
|
281
295
|
electronBroadGetTextScopeDiagnostics = collectElectronBroadGetTextScopeDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, currentTarget: currentSessionTabTarget, data: presentationEnvelope?.data, electronLaunchRecords, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName });
|
|
@@ -287,8 +301,10 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
287
301
|
let currentRefSnapshotInvalidation: SessionRefSnapshotInvalidation | undefined;
|
|
288
302
|
const batchRefSnapshotState = prepared.executionPlan.commandInfo.command === "batch" ? extractLatestRefSnapshotStateFromBatchResults(presentationEnvelope?.data) : undefined;
|
|
289
303
|
if (prepared.executionPlan.sessionName) {
|
|
290
|
-
if (prepared.executionPlan.commandInfo.command
|
|
291
|
-
|
|
304
|
+
if (isCloseCommand(prepared.executionPlan.commandInfo.command) && succeeded) {
|
|
305
|
+
sessionPageState.clearSession(prepared.executionPlan.sessionName);
|
|
306
|
+
state.closedManagedSessionNames.add(prepared.executionPlan.sessionName);
|
|
307
|
+
} else if (currentSessionTabTarget) {
|
|
292
308
|
const tabUpdate = sessionPageState.applyTabTarget({ sessionName: prepared.executionPlan.sessionName, target: currentSessionTabTarget, update: sessionPageStateUpdate });
|
|
293
309
|
if (!tabUpdate.applied && succeeded) sessionPageState.markPinning(prepared.executionPlan.sessionName, "drift");
|
|
294
310
|
}
|
|
@@ -307,11 +323,19 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
307
323
|
const priorManagedSessionActive = managedSessionActive;
|
|
308
324
|
const priorManagedSessionCwd = managedSessionCwd;
|
|
309
325
|
const priorManagedSessionName = managedSessionName;
|
|
310
|
-
const
|
|
326
|
+
const commandClosesSession = isCloseCommand(prepared.executionPlan.commandInfo.command);
|
|
327
|
+
const managedCloseSessionName = commandClosesSession && succeeded && prepared.executionPlan.sessionName === priorManagedSessionName
|
|
328
|
+
? prepared.executionPlan.sessionName
|
|
329
|
+
: prepared.executionPlan.managedSessionName;
|
|
330
|
+
const managedSessionState = resolveManagedSessionState({ command: prepared.executionPlan.commandInfo.command, managedSessionName: managedCloseSessionName, priorActive: priorManagedSessionActive, priorSessionName: priorManagedSessionName, succeeded });
|
|
311
331
|
const replacedManagedSessionName = managedSessionState.replacedSessionName;
|
|
312
332
|
managedSessionActive = managedSessionState.active;
|
|
313
333
|
managedSessionName = managedSessionState.sessionName;
|
|
314
|
-
|
|
334
|
+
if (commandClosesSession && succeeded && managedCloseSessionName === priorManagedSessionName && !managedSessionActive) {
|
|
335
|
+
freshSessionOrdinal += 1;
|
|
336
|
+
managedSessionName = createFreshSessionName(state.managedSessionBaseName, state.ephemeralSessionSeed, freshSessionOrdinal);
|
|
337
|
+
}
|
|
338
|
+
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
339
|
if (prepared.executionPlan.managedSessionName && succeeded) managedSessionCwd = cwd;
|
|
316
340
|
if (prepared.executionPlan.sessionName && succeeded) {
|
|
317
341
|
if (openResultTabCorrection || sessionTabCorrection || aboutBlankSessionMismatch?.recoveryApplied) sessionPageState.markPinning(prepared.executionPlan.sessionName, "drift");
|
|
@@ -319,7 +343,8 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
319
343
|
}
|
|
320
344
|
if (replacedManagedSessionName) {
|
|
321
345
|
sessionPageState.clearSession(replacedManagedSessionName);
|
|
322
|
-
await closeManagedSession({ cwd: priorManagedSessionCwd, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
|
|
346
|
+
const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
|
|
347
|
+
if (!replacedCloseError) state.closedManagedSessionNames.add(replacedManagedSessionName);
|
|
323
348
|
}
|
|
324
349
|
|
|
325
350
|
let electronLaunchRecord: ElectronLaunchRecord | undefined;
|
|
@@ -396,7 +421,11 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
396
421
|
if (!skipAttachedTargetBanner && qaAttachedTargetText && presentation.content[0]?.type === "text") presentation.content[0] = { ...presentation.content[0], text: `${qaAttachedTargetText}\n\n${presentation.content[0].text}` };
|
|
397
422
|
else if (!skipAttachedTargetBanner && qaAttachedTargetText) presentation.content.unshift({ type: "text", text: qaAttachedTargetText });
|
|
398
423
|
if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded) managedSessionOutcome = { ...managedSessionOutcome, succeeded };
|
|
424
|
+
const evalNavigationSummary = navigationSummary ?? extractNavigationSummaryFromData(presentationEnvelope?.data);
|
|
425
|
+
const evalSessionTabUrl = prepared.executionPlan.sessionName ? sessionPageState.get(prepared.executionPlan.sessionName).tabTarget?.url : undefined;
|
|
426
|
+
const evalPageUrl = evalNavigationSummary?.url ?? currentSessionTabTarget?.url ?? prepared.priorSessionTabTarget?.url ?? evalSessionTabUrl;
|
|
399
427
|
const evalStdinHint = getEvalStdinHint({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, stdin: prepared.runtimeToolStdin });
|
|
428
|
+
const evalResultWarning = getEvalResultWarning({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary: evalNavigationSummary, pageUrl: evalPageUrl, stdin: prepared.runtimeToolStdin });
|
|
400
429
|
const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
|
|
401
430
|
const artifactCleanup = await getArtifactCleanupGuidance({ command: prepared.executionPlan.commandInfo.command, cwd, manifest: resultArtifactManifest, succeeded });
|
|
402
431
|
const warningText = electronPostCommandHealth ? formatElectronPostCommandHealthText(electronPostCommandHealth) : electronSessionMismatch ? formatElectronSessionMismatchText(electronSessionMismatch) : aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
@@ -404,8 +433,8 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
404
433
|
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
434
|
currentRefSnapshot = finalRecoveryState.currentRefSnapshot;
|
|
406
435
|
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 };
|
|
436
|
+
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, evalResultWarning, 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 });
|
|
437
|
+
const statePatch: BrowserRunStatePatch = { artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCwd, managedSessionName };
|
|
409
438
|
return { result, statePatch };
|
|
410
439
|
} finally {
|
|
411
440
|
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
|
+
}
|