pi-agent-browser-native 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/README.md +59 -165
- package/dist/extensions/agent-browser/index.js +634 -501
- package/dist/extensions/agent-browser/lib/browser-transcript.js +33 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +8 -4
- package/dist/extensions/agent-browser/lib/execution-cwd.js +66 -0
- package/dist/extensions/agent-browser/lib/input-modes/job.js +2 -213
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/params.js +146 -142
- package/dist/extensions/agent-browser/lib/input-modes/script.js +64 -92
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +170 -24
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +27 -20
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +14 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +26 -13
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +21 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +4 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +41 -24
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +3 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +79 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -4
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +19 -52
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +3 -3
- package/dist/extensions/agent-browser/lib/orchestration/operation-paths.js +76 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -3
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +91 -278
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +9 -9
- package/dist/extensions/agent-browser/lib/playbook.js +46 -25
- package/dist/extensions/agent-browser/lib/process-identity.js +22 -6
- package/dist/extensions/agent-browser/lib/process.js +16 -13
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +2 -2
- package/dist/extensions/agent-browser/lib/results/next-actions.js +1 -2
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +25 -4
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +6 -2
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -0
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +39 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +3 -3
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +41 -1
- package/dist/extensions/agent-browser/lib/results/presentation.js +16 -11
- package/dist/extensions/agent-browser/lib/runtime.js +18 -6
- package/dist/extensions/agent-browser/lib/session-page-state.js +3 -4
- package/dist/extensions/agent-browser/lib/tool-surface.js +131 -0
- package/dist/extensions/agent-browser/script-worker.js +4 -1
- package/docs/ARCHITECTURE.md +47 -61
- package/docs/COMMAND_REFERENCE.md +78 -137
- package/docs/ELECTRON.md +51 -49
- package/docs/RELEASE.md +31 -15
- package/docs/REQUIREMENTS.md +18 -14
- package/docs/SUPPORT_MATRIX.md +33 -15
- package/docs/TOOL_CONTRACT.md +218 -401
- package/package.json +4 -4
- package/scripts/doctor.mjs +2 -2
|
@@ -4,7 +4,7 @@ import { parseArgvDescriptor } from "../../argv-descriptor.js";
|
|
|
4
4
|
import { isBrowserIndependentRead, needsManagedSession } from "../../command-policy.js";
|
|
5
5
|
import { deleteIdentityKeysInNamespace, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
|
|
6
6
|
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
|
|
7
|
-
import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
|
|
7
|
+
import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isPageMutationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
|
|
8
8
|
import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
|
|
9
9
|
import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../../electron/cleanup.js";
|
|
10
10
|
import { getResultingPageTargetState, commandRequiresLivePageVerification } from "../../page-target-validation.js";
|
|
@@ -147,6 +147,7 @@ function applyBatchNetworkRouteState(options) {
|
|
|
147
147
|
}
|
|
148
148
|
export async function processBrowserOutput(input) {
|
|
149
149
|
const { ctx, cwd, electronPostCommandStatusSettleMs, implicitSessionCloseTimeoutMs, sessionPageStateUpdate, signal, state } = input;
|
|
150
|
+
const operationCwd = input.operationCwd ?? cwd;
|
|
150
151
|
const { prepared, processResult } = input;
|
|
151
152
|
const { electronChildProcesses, electronLaunchRecords, sessionPageState, traceOwners } = state;
|
|
152
153
|
let artifactManifest = state.artifactManifest;
|
|
@@ -167,6 +168,7 @@ export async function processBrowserOutput(input) {
|
|
|
167
168
|
observeNativeWebMcp(parsed.envelope?.data);
|
|
168
169
|
let parseError = parsed.parseError;
|
|
169
170
|
const recordingStopRecovery = await recoverRecordingStop({
|
|
171
|
+
modelVisible: input.modelVisible,
|
|
170
172
|
artifactManifest, artifactRunStartedAtMs: input.artifactRunStartedAtMs, commandTokens: prepared.commandTokens, cwd,
|
|
171
173
|
envelope: parsed.envelope, namespace: prepared.executionPlan.namespace, parseError, processResult,
|
|
172
174
|
reservation: prepared.executionPlan.sessionName ? state.activeRecordingReservations?.get(getAgentBrowserSessionIdentityKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace)) : undefined,
|
|
@@ -240,6 +242,12 @@ export async function processBrowserOutput(input) {
|
|
|
240
242
|
const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
|
|
241
243
|
let succeeded = (processSucceeded && parseSucceeded && envelopeSuccess) || recordingStopRecovery?.recovery.healed === true;
|
|
242
244
|
const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
|
|
245
|
+
const unobservedMutation = processResult.agentBrowserStarted && !plainTextInspection
|
|
246
|
+
&& (processResult.aborted || processResult.timedOut || !parseSucceeded || presentationEnvelope === undefined)
|
|
247
|
+
&& (prepared.executionPlan.commandInfo.command === "batch" ? batchCommandSteps : [prepared.commandTokens]).some(step => {
|
|
248
|
+
const action = step[0] === "find" ? step[step[1] === "nth" ? 4 : 3] ?? "click" : step[0];
|
|
249
|
+
return isPageMutationCommand(action, step[1]);
|
|
250
|
+
});
|
|
243
251
|
const sessionStateKey = getSessionContextKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
|
|
244
252
|
const closeAllApplied = nestedBatchClosesAll || (directCloseAllRequested && succeeded);
|
|
245
253
|
if (sessionStateKey && processResult.agentBrowserStarted && sessionPageState.get(sessionStateKey).tabReopenPending === true) {
|
|
@@ -334,14 +342,14 @@ export async function processBrowserOutput(input) {
|
|
|
334
342
|
const failedWebMcpSettlement = prepared.priorSessionTabTargetUnknown === true && ((!succeeded && isWebMcpSettlementCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand))
|
|
335
343
|
|| (prepared.executionPlan.commandInfo.command === "batch" && batchHasFailedWebMcpSettlement(presentationEnvelope?.data)));
|
|
336
344
|
const unsettledWebMcpMutation = pendingWebMcpMutation || failedWebMcpSettlement;
|
|
337
|
-
const observedSessionTabTarget = unsettledWebMcpMutation
|
|
345
|
+
const observedSessionTabTarget = unsettledWebMcpMutation || (unobservedMutation && !failedTransitionReverification)
|
|
338
346
|
? undefined
|
|
339
347
|
: normalizeSessionTabTarget(navigationSummary)
|
|
340
348
|
?? (trustsReportedPageTarget ? extractSessionTabTargetFromBatchResults(presentationEnvelope?.data) : undefined)
|
|
341
349
|
?? (succeeded && trustsReportedPageTarget ? extractSessionTabTargetFromCommandData(prepared.commandTokens, presentationEnvelope?.data) : undefined);
|
|
342
350
|
const safeObservedSessionTabTarget = observedSessionTabTarget;
|
|
343
351
|
let currentSessionTabTarget = safeObservedSessionTabTarget;
|
|
344
|
-
if (!currentSessionTabTarget && nestedBatchClose === undefined) {
|
|
352
|
+
if (!currentSessionTabTarget && nestedBatchClose === undefined && !unobservedMutation) {
|
|
345
353
|
// Window/diff responses do not report the final URL; URL2 is intent, not redirect evidence.
|
|
346
354
|
currentSessionTabTarget = resultingPageState.pageTargetMayHaveChanged
|
|
347
355
|
? succeeded && !destinationTransition ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
|
|
@@ -395,8 +403,8 @@ export async function processBrowserOutput(input) {
|
|
|
395
403
|
let fillVerificationDiagnostic;
|
|
396
404
|
let selectorTextVisibilityDiagnostics = [];
|
|
397
405
|
let electronBroadGetTextScopeDiagnostics = [];
|
|
398
|
-
const timeoutPartialProgress = processResult.timedOut && !recordingStopRecovery && !prepared.readConfirmation ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
|
|
399
|
-
if (!currentSessionTabTarget && timeoutPartialProgress?.
|
|
406
|
+
const timeoutPartialProgress = processResult.timedOut && !recordingStopRecovery && !prepared.readConfirmation ? await collectTimeoutPartialProgress({ commandTokens: prepared.commandTokens, compiledJob: prepared.compiledJob, cwd, operationCwd: input.operationCwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
|
|
407
|
+
if (!currentSessionTabTarget && timeoutPartialProgress?.liveUrlRecovered === true) {
|
|
400
408
|
currentSessionTabTarget = normalizeSessionTabTarget(timeoutPartialProgress.currentPage);
|
|
401
409
|
}
|
|
402
410
|
if (succeeded) {
|
|
@@ -444,24 +452,26 @@ export async function processBrowserOutput(input) {
|
|
|
444
452
|
const plannedBatchTransitionInvalidation = !Array.isArray(presentationEnvelope?.data)
|
|
445
453
|
? batchCommandSteps.map(getCommandRefSnapshotInvalidation).find((invalidation) => invalidation !== undefined)
|
|
446
454
|
: undefined;
|
|
447
|
-
const pageTransitionInvalidation =
|
|
448
|
-
? buildPageTransitionRefSnapshotInvalidation("A
|
|
449
|
-
:
|
|
450
|
-
?
|
|
451
|
-
:
|
|
452
|
-
?
|
|
453
|
-
:
|
|
454
|
-
?
|
|
455
|
-
:
|
|
455
|
+
const pageTransitionInvalidation = unobservedMutation && !failedTransitionReverification
|
|
456
|
+
? buildPageTransitionRefSnapshotInvalidation("A dispatched mutation was interrupted without a final outcome. Verify the current URL and take a fresh snapshot before deciding whether to retry.")
|
|
457
|
+
: unsettledWebMcpMutation
|
|
458
|
+
? buildPageTransitionRefSnapshotInvalidation("A detached WebMCP invocation is still pending or failed to settle and can mutate, rerender, or navigate the page, so prior snapshot refs remain invalid after URL verification. Run webmcp result or cancel, then take a fresh snapshot before using page-scoped refs.")
|
|
459
|
+
: processResult.agentBrowserStarted && (directTransitionInvalidation || plannedBatchTransitionInvalidation)
|
|
460
|
+
? directTransitionInvalidation ?? plannedBatchTransitionInvalidation
|
|
461
|
+
: failedTransitionReverification
|
|
462
|
+
? buildPageTransitionRefSnapshotInvalidation("A failed eval/back/forward/reload/connect/state-load/tab command may still have changed the page, so the prior snapshot refs were invalidated. Run snapshot -i before using page-scoped refs.")
|
|
463
|
+
: batchRefSnapshotState?.invalidation?.reason === "page-transition"
|
|
464
|
+
? batchRefSnapshotState.invalidation
|
|
465
|
+
: undefined;
|
|
456
466
|
if (currentSessionTabTarget) {
|
|
457
467
|
const tabUpdate = sessionPageState.applyTabTarget({ sessionName: sessionStateKey, target: currentSessionTabTarget, update: sessionPageStateUpdate });
|
|
458
468
|
if (!tabUpdate.applied && succeeded)
|
|
459
469
|
sessionPageState.markPinning(sessionStateKey, "drift");
|
|
460
470
|
}
|
|
461
|
-
else if (processResult.agentBrowserStarted && (resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged) && !(prepared.commandTokens[0] === "session" && prepared.commandTokens[1] === "info")) {
|
|
471
|
+
else if (processResult.agentBrowserStarted && (unobservedMutation || resultingPageState.pageUrlUnknown || resultingPageState.pageTargetMayHaveChanged) && !(prepared.commandTokens[0] === "session" && prepared.commandTokens[1] === "info")) {
|
|
462
472
|
sessionPageState.markTabTargetUnknown({ sessionName: sessionStateKey, update: sessionPageStateUpdate });
|
|
463
473
|
}
|
|
464
|
-
let refSnapshot = unsettledWebMcpMutation
|
|
474
|
+
let refSnapshot = unsettledWebMcpMutation || unobservedMutation
|
|
465
475
|
? undefined
|
|
466
476
|
: prepared.executionPlan.commandInfo.command === "batch"
|
|
467
477
|
? batchRefSnapshotState?.snapshot
|
|
@@ -472,7 +482,7 @@ export async function processBrowserOutput(input) {
|
|
|
472
482
|
? prepared.commandTokens.filter((token) => token !== "--delta" && token !== "--full") : undefined);
|
|
473
483
|
// Native owns delta baselines. Read complete refs without advancing that baseline
|
|
474
484
|
// rather than maintaining a second revision/tree cache in the wrapper.
|
|
475
|
-
if (refreshArgs && !unsettledWebMcpMutation) {
|
|
485
|
+
if (refreshArgs && !unsettledWebMcpMutation && !unobservedMutation) {
|
|
476
486
|
refSnapshot = extractRefSnapshotFromData(await runSessionCommandData({ args: refreshArgs, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal }));
|
|
477
487
|
if (!refSnapshot)
|
|
478
488
|
sessionPageState.applyRefSnapshotInvalidation({ invalidation: buildPageTransitionRefSnapshotInvalidation("The native snapshot delta did not include complete refs and the full ref read failed. Run snapshot -i before using refs."), sessionName: sessionStateKey, update: sessionPageStateUpdate });
|
|
@@ -574,7 +584,8 @@ export async function processBrowserOutput(input) {
|
|
|
574
584
|
}
|
|
575
585
|
let managedSessionOutcome = buildManagedSessionOutcome({ activeAfter: managedSessionActive, activeBefore: priorManagedSessionActive, attemptedSessionName: managedCloseSessionName, command: commandClosesSession ? "close" : prepared.executionPlan.commandInfo.command, currentSessionName: managedSessionName, currentSessionNamespace: managedSessionNamespace, previousSessionName: priorManagedSessionName, replacedSessionName: replacedManagedSessionName, replacedSessionNamespace: priorManagedSessionNamespace, sessionMode: prepared.sessionMode, succeeded: managedTransitionSucceeded });
|
|
576
586
|
if (prepared.executionPlan.managedSessionName && managedTransitionSucceeded && managedSessionActive) {
|
|
577
|
-
|
|
587
|
+
if (!priorManagedSessionActive || replacedManagedSessionName)
|
|
588
|
+
managedSessionCwd = cwd;
|
|
578
589
|
managedSessionNamespace = prepared.executionPlan.namespace;
|
|
579
590
|
}
|
|
580
591
|
if (sessionStateKey && succeeded) {
|
|
@@ -681,7 +692,7 @@ export async function processBrowserOutput(input) {
|
|
|
681
692
|
}
|
|
682
693
|
if (plainTextUpgrade && errorText)
|
|
683
694
|
presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
|
|
684
|
-
let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : recordingStopRecovery && !recordingStopRecovery.batch ? recordingStopRecovery.presentation : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
|
|
695
|
+
let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : recordingStopRecovery && !recordingStopRecovery.batch ? recordingStopRecovery.presentation : await buildToolPresentation({ modelVisible: input.modelVisible, args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd: operationCwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
|
|
685
696
|
if (recordingStopRecovery)
|
|
686
697
|
presentation = mergeRecordingRecoveryPresentation(presentation, recordingStopRecovery);
|
|
687
698
|
const confirmation = readConfirmationEvent ?? prepared.readConfirmation;
|
|
@@ -701,7 +712,8 @@ export async function processBrowserOutput(input) {
|
|
|
701
712
|
const errorContent = presentation.content[0];
|
|
702
713
|
if (errorContent?.type === "text" && presentationEnvelope.data)
|
|
703
714
|
errorContent.text += `\n\n${presentationEnvelope.data}`;
|
|
704
|
-
|
|
715
|
+
if (input.modelVisible !== false)
|
|
716
|
+
presentation = await compactLargePresentationOutput({ artifactManifest, commandInfo: prepared.executionPlan.commandInfo, data: presentation.data, persistentArtifactStore, presentation });
|
|
705
717
|
}
|
|
706
718
|
if (electronHandoff?.error && electronHandoff.failureCategory)
|
|
707
719
|
presentation.failureCategory = electronHandoff.failureCategory;
|
|
@@ -741,12 +753,17 @@ export async function processBrowserOutput(input) {
|
|
|
741
753
|
const qaPreset = prepared.compiledQaPreset
|
|
742
754
|
? (processResult.timedOut ? analyzeQaPresetTimeout(prepared.compiledQaPreset) ?? analyzeQaPresetResults(presentationEnvelope?.data, prepared.compiledQaPreset) : analyzeQaPresetResults(presentationEnvelope?.data, prepared.compiledQaPreset))
|
|
743
755
|
: undefined;
|
|
756
|
+
if (qaPreset?.passed && presentation.resultCategory === "failure") {
|
|
757
|
+
qaPreset.passed = false;
|
|
758
|
+
qaPreset.summary = presentation.summary;
|
|
759
|
+
qaPreset.failedChecks.push(presentation.summary);
|
|
760
|
+
}
|
|
744
761
|
let qaAttachedTarget = prepared.compiledQaPreset?.checks.attached
|
|
745
762
|
? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? prepared.priorSessionTabTarget, cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal })
|
|
746
763
|
: undefined;
|
|
747
764
|
const sourceLookupElectronContext = prepared.compiledSourceLookup ? getSourceLookupElectronContext({ currentTarget: currentSessionTabTarget, electronLaunchRecords, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName }) : undefined;
|
|
748
|
-
const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup,
|
|
749
|
-
const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup,
|
|
765
|
+
const sourceLookup = prepared.compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, prepared.compiledSourceLookup, operationCwd, { electronContext: sourceLookupElectronContext, workspaceRoot: operationCwd }) : undefined;
|
|
766
|
+
const networkSourceLookup = prepared.compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, prepared.compiledNetworkSourceLookup, operationCwd)) : undefined;
|
|
750
767
|
if (networkSourceLookup && presentation.content[0]?.type === "text")
|
|
751
768
|
presentation.content[0] = { ...presentation.content[0], text: `${networkSourceLookup.summary}\n\n${presentation.content[0].text}` };
|
|
752
769
|
else if (networkSourceLookup)
|
|
@@ -817,7 +834,7 @@ export async function processBrowserOutput(input) {
|
|
|
817
834
|
: undefined;
|
|
818
835
|
const sessionWarning = electronPostCommandHealth ? formatElectronPostCommandHealthText(electronPostCommandHealth) : electronSessionMismatch ? formatElectronSessionMismatchText(electronSessionMismatch) : aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
819
836
|
const warningText = [sessionWarning, recordingPageWarning].filter(Boolean).join("\n\n") || undefined;
|
|
820
|
-
const redactedContent = buildRedactedPresentationContent({ exactSensitiveValues: prepared.exactSensitiveValues, plainTextInspection, presentation, presentationEnvelope, succeeded, userRequestedJson: prepared.userRequestedJson, warningText });
|
|
837
|
+
const redactedContent = input.modelVisible === false ? [] : buildRedactedPresentationContent({ exactSensitiveValues: prepared.exactSensitiveValues, plainTextInspection, presentation, presentationEnvelope, succeeded, userRequestedJson: prepared.userRequestedJson, warningText });
|
|
821
838
|
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 });
|
|
822
839
|
currentRefSnapshot = finalRecoveryState.currentRefSnapshot;
|
|
823
840
|
currentRefSnapshotInvalidation = finalRecoveryState.currentRefSnapshotInvalidation;
|
|
@@ -834,7 +851,7 @@ export async function processBrowserOutput(input) {
|
|
|
834
851
|
const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !prepared.ownedManagedSessionContext?.reuseOnly && !(commandClosesSession && succeeded)
|
|
835
852
|
? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
|
|
836
853
|
: undefined;
|
|
837
|
-
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, currentSessionTabTargetUnknown, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalResultWarning, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, headedLaunch: prepared.headedLaunch, inspectionText, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession === true, providerLaunch: prepared.providerLaunch, managedSessionHeadedAutosaveDisabled: resultHeadedManagedAutosaveDisabled || undefined, managedSessionHeadedAutosaveInterval: resultHeadedManagedAutosaveInterval, managedSessionOutcome, managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace), 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, unsettledWebMcpMutation, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
|
|
854
|
+
const result = buildFinalAgentBrowserToolResult({ modelVisible: input.modelVisible, aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, clickDispatchDiagnostic, commandTokens: prepared.commandTokens, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, currentSessionTabTargetUnknown, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalResultWarning, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, headedLaunch: prepared.headedLaunch, inspectionText, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession === true, providerLaunch: prepared.providerLaunch, managedSessionHeadedAutosaveDisabled: resultHeadedManagedAutosaveDisabled || undefined, managedSessionHeadedAutosaveInterval: resultHeadedManagedAutosaveInterval, managedSessionOutcome, managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace), 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, unsettledWebMcpMutation, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
|
|
838
855
|
const resultWithCloseAll = closeAllApplied
|
|
839
856
|
? { ...result, details: { ...(isRecord(result.details) ? result.details : {}), closeAllApplied: true } }
|
|
840
857
|
: result;
|
|
@@ -95,6 +95,7 @@ export async function recoverRecordingStop(options) {
|
|
|
95
95
|
&& !currentUsesSamePath;
|
|
96
96
|
const data = matchedData ?? (expected ? { path: expected.absolutePath, recordingId: expected.recordingId, success: null } : undefined);
|
|
97
97
|
const presentation = await buildToolPresentation({
|
|
98
|
+
modelVisible: options.modelVisible,
|
|
98
99
|
artifactManifest: options.artifactManifest, artifactMinUpdatedAtMs: expected?.startedAtMs ?? options.artifactRunStartedAtMs, artifactMaxUpdatedAtMs: Date.now(),
|
|
99
100
|
artifactRequest: expected ? { path: expected.path, absolutePath: expected.absolutePath, status: terminalMeasurements ? undefined : "unverified" } : undefined,
|
|
100
101
|
commandInfo: { command: "record", subcommand: "stop" }, cwd: expected?.cwd ?? options.cwd,
|
|
@@ -139,7 +140,8 @@ export async function recoverRecordingStop(options) {
|
|
|
139
140
|
presentation.nextActions = followups;
|
|
140
141
|
presentation.recordingRecovery = recovery;
|
|
141
142
|
const text = `Recording receipt recovery (${recovery.status}): ${recovery.reason}\nOriginal attempt: ${attemptError}`;
|
|
142
|
-
|
|
143
|
+
if (options.modelVisible !== false)
|
|
144
|
+
presentation.content.unshift({ type: "text", text });
|
|
143
145
|
return { batch, envelope, partialBatch: batch && !rows, stopIndex, presentation, recovery };
|
|
144
146
|
}
|
|
145
147
|
export function mergeRecordingRecoveryPresentation(base, result) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isRecord } from "../../parsing.js";
|
|
2
|
+
import { getScreenshotPathTokenIndex, getScreenshotPositionalIndices } from "./artifact-paths.js";
|
|
3
|
+
import { runSessionCommandData } from "./session-state.js";
|
|
4
|
+
export function getScreenshotCapture(command) {
|
|
5
|
+
if (command[0] !== "screenshot")
|
|
6
|
+
return { kind: "unknown" };
|
|
7
|
+
if (command.includes("--full") || command.includes("-f"))
|
|
8
|
+
return { kind: "full-page" };
|
|
9
|
+
const positional = getScreenshotPositionalIndices(command);
|
|
10
|
+
const selectorIndex = positional.find(index => index !== getScreenshotPathTokenIndex(command));
|
|
11
|
+
return selectorIndex === undefined ? { kind: "viewport" } : { kind: "element", selector: command[selectorIndex] };
|
|
12
|
+
}
|
|
13
|
+
function finiteRecord(value, keys) {
|
|
14
|
+
return isRecord(value) && keys.every(key => typeof value[key] === "number" && Number.isFinite(value[key]));
|
|
15
|
+
}
|
|
16
|
+
export async function collectScreenshotSample(options) {
|
|
17
|
+
if (options.command[0] !== "screenshot" || options.signal?.aborted)
|
|
18
|
+
return undefined;
|
|
19
|
+
const { selector } = getScreenshotCapture(options.command);
|
|
20
|
+
const script = `(() => {
|
|
21
|
+
const root = document.documentElement, body = document.body, v = window.visualViewport;
|
|
22
|
+
let element;
|
|
23
|
+
try {
|
|
24
|
+
const selector = ${JSON.stringify(selector ?? null)};
|
|
25
|
+
const matches = selector ? document.querySelectorAll(selector) : [];
|
|
26
|
+
if (matches.length === 1) {
|
|
27
|
+
const rect = matches[0].getBoundingClientRect();
|
|
28
|
+
element = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
return { url: location.href, frame: window === window.top ? "main" : "child", childFrameCount: window.frames.length,
|
|
32
|
+
viewport: { width: innerWidth, height: innerHeight },
|
|
33
|
+
document: { width: Math.max(root.scrollWidth, body?.scrollWidth || 0, innerWidth), height: Math.max(root.scrollHeight, body?.scrollHeight || 0, innerHeight) },
|
|
34
|
+
scroll: { x: scrollX, y: scrollY }, dpr: devicePixelRatio,
|
|
35
|
+
visualViewport: { x: v?.offsetLeft ?? 0, y: v?.offsetTop ?? 0, scale: v?.scale ?? 1 }, element };
|
|
36
|
+
})()`;
|
|
37
|
+
try {
|
|
38
|
+
const data = await runSessionCommandData({ ...options, args: ["eval", "--stdin"], stdin: script, timeoutMs: 2_000 });
|
|
39
|
+
const sample = isRecord(data) ? data.result : undefined;
|
|
40
|
+
if (!isRecord(sample) || typeof sample.url !== "string" || !["main", "child"].includes(String(sample.frame))
|
|
41
|
+
|| !finiteRecord(sample.viewport, ["width", "height"]) || !finiteRecord(sample.document, ["width", "height"])
|
|
42
|
+
|| !finiteRecord(sample.scroll, ["x", "y"]) || !finiteRecord(sample.visualViewport, ["x", "y", "scale"])
|
|
43
|
+
|| typeof sample.childFrameCount !== "number" || !Number.isInteger(sample.childFrameCount) || sample.childFrameCount < 0
|
|
44
|
+
|| typeof sample.dpr !== "number" || !Number.isFinite(sample.dpr) || sample.dpr <= 0
|
|
45
|
+
|| (sample.element !== undefined && !finiteRecord(sample.element, ["x", "y", "width", "height"])))
|
|
46
|
+
return undefined;
|
|
47
|
+
return sample;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function buildScreenshotGeometry(options) {
|
|
54
|
+
const { before, after, capture, pixels } = options;
|
|
55
|
+
const unknown = (reason) => ({ status: "unknown", reason, before, after });
|
|
56
|
+
if (!before || !after)
|
|
57
|
+
return unknown("Capture was not bracketed by browser geometry samples; coordinates are unknown.");
|
|
58
|
+
if (JSON.stringify(before) !== JSON.stringify(after))
|
|
59
|
+
return unknown("Browser geometry changed across capture; no coordinate mapping is asserted.");
|
|
60
|
+
if (before.frame !== "main")
|
|
61
|
+
return unknown("Probe observed a child frame; its relation to the captured page is unknown.");
|
|
62
|
+
if (before.visualViewport.scale !== 1 || before.visualViewport.x !== 0 || before.visualViewport.y !== 0)
|
|
63
|
+
return unknown("Visual viewport is zoomed or offset; capture origin is unknown.");
|
|
64
|
+
let crop;
|
|
65
|
+
if (capture === "viewport")
|
|
66
|
+
crop = { ...before.scroll, ...before.viewport };
|
|
67
|
+
else if (capture === "full-page")
|
|
68
|
+
crop = { x: 0, y: 0, ...before.document };
|
|
69
|
+
else if (capture === "element" && before.element && before.childFrameCount === 0 && before.scroll.x === 0 && before.scroll.y === 0)
|
|
70
|
+
crop = before.element;
|
|
71
|
+
else
|
|
72
|
+
return unknown("Element crop/frame provenance is unverified (scrolled page, child frames, non-CSS selector, or ambiguous target).");
|
|
73
|
+
if (!pixels || crop.width <= 0 || crop.height <= 0 || crop.x < 0 || crop.y < 0
|
|
74
|
+
|| !Object.values(crop).every(Number.isInteger)
|
|
75
|
+
|| pixels.width !== crop.width * before.dpr || pixels.height !== crop.height * before.dpr)
|
|
76
|
+
return unknown("Image dimensions do not establish the sampled CSS crop at this DPR.");
|
|
77
|
+
return { status: "measured", before, after, crop, pixelsPerCssPixel: { x: pixels.width / crop.width, y: pixels.height / crop.height },
|
|
78
|
+
reason: "Matching pre/post samples, not an atomic capture guarantee. Pixels map to CSS document coordinates within crop; native mouse uses current viewport CSS coordinates. Recheck scroll/frame before input." };
|
|
79
|
+
}
|
|
@@ -4,6 +4,7 @@ import { boundElectronProbeString } from "../../electron/cdp.js";
|
|
|
4
4
|
import { buildOwnedManagedSessionRestoreContext, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
|
|
5
5
|
import { getPageTargetValidationError } from "../../page-target-validation.js";
|
|
6
6
|
import { isRecord } from "../../parsing.js";
|
|
7
|
+
import { getBrowserResultMessage } from "../../browser-transcript.js";
|
|
7
8
|
import { withAttachedBrowserSessionContext } from "../../process.js";
|
|
8
9
|
import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
|
|
9
10
|
import { buildAgentBrowserResultCategoryDetails } from "../../results/categories.js";
|
|
@@ -125,10 +126,8 @@ function isElectronLaunchRecord(value) {
|
|
|
125
126
|
export function restoreElectronLaunchRecordsFromBranch(branch) {
|
|
126
127
|
const records = new Map();
|
|
127
128
|
for (const entry of branch) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
131
|
-
if (!message || message.toolName !== "agent_browser")
|
|
129
|
+
const message = getBrowserResultMessage(entry);
|
|
130
|
+
if (!message)
|
|
132
131
|
continue;
|
|
133
132
|
const details = isRecord(message.details) ? message.details : undefined;
|
|
134
133
|
const electron = isRecord(details?.electron) ? details.electron : undefined;
|
|
@@ -3,10 +3,9 @@ import { normalizeUrlLessOpen } from "./batch-stdin.js";
|
|
|
3
3
|
import { isPlainTextInspectionArgs, validateToolArgs, redactInvocationArgs, redactSensitiveText } from "../runtime.js";
|
|
4
4
|
import { buildAgentBrowserResultCategoryDetails } from "../results/categories.js";
|
|
5
5
|
import { compileAgentBrowserElectron } from "../input-modes/electron.js";
|
|
6
|
-
import {
|
|
6
|
+
import { compileAgentBrowserQaPreset } from "../input-modes/job.js";
|
|
7
7
|
import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup, redactNetworkSourceLookupArgs, redactNetworkSourceLookupUrl } from "../input-modes/lookups.js";
|
|
8
8
|
import { compileAgentBrowserSemanticAction } from "../input-modes/semantic-action.js";
|
|
9
|
-
import { AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS, compileAgentBrowserScript } from "../input-modes/script.js";
|
|
10
9
|
function redactCompiledElectron(compiled) {
|
|
11
10
|
if (!compiled)
|
|
12
11
|
return undefined;
|
|
@@ -68,46 +67,39 @@ function normalizeExplicitEvalStdinArgs(args, stdin) {
|
|
|
68
67
|
export function resolveAgentBrowserInput(options) {
|
|
69
68
|
const { getBatchPreflightValidationError, params } = options;
|
|
70
69
|
const semanticActionResult = params.semanticAction === undefined ? {} : compileAgentBrowserSemanticAction(params.semanticAction);
|
|
71
|
-
const jobResult = params.job === undefined ? {} : compileAgentBrowserJob(params.job);
|
|
72
70
|
const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
|
|
73
71
|
const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
|
|
74
72
|
const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
|
|
75
73
|
const electronResult = params.electron === undefined ? {} : compileAgentBrowserElectron(params.electron);
|
|
76
|
-
const scriptResult = params.script === undefined ? {} : compileAgentBrowserScript(params.script);
|
|
77
74
|
const hasExplicitArgs = Array.isArray(params.args);
|
|
78
75
|
const explicitInputModes = [
|
|
79
76
|
hasExplicitArgs,
|
|
80
77
|
Boolean(semanticActionResult.compiled),
|
|
81
|
-
Boolean(jobResult.compiled),
|
|
82
78
|
Boolean(qaResult.compiled),
|
|
83
79
|
Boolean(sourceLookupResult.compiled),
|
|
84
80
|
Boolean(networkSourceLookupResult.compiled),
|
|
85
81
|
Boolean(electronResult.compiled),
|
|
86
|
-
Boolean(scriptResult.compiled),
|
|
87
82
|
].filter(Boolean).length;
|
|
88
83
|
const inputModeError = explicitInputModes !== 1
|
|
89
|
-
? "Provide exactly one of
|
|
84
|
+
? "Provide exactly one of args, semanticAction, qa, sourceLookup, networkSourceLookup, or electron."
|
|
90
85
|
: undefined;
|
|
91
86
|
const compiledSemanticAction = semanticActionResult.compiled;
|
|
92
87
|
const compiledQaPreset = qaResult.compiled;
|
|
93
88
|
const compiledSourceLookup = sourceLookupResult.compiled;
|
|
94
89
|
const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
|
|
95
90
|
const compiledElectron = electronResult.compiled;
|
|
96
|
-
const
|
|
97
|
-
const compiledJob = jobResult.compiled ?? compiledQaPreset;
|
|
91
|
+
const compiledJob = compiledQaPreset;
|
|
98
92
|
const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
|
|
99
93
|
const normalizedExplicitArgs = normalizeExplicitEvalStdinArgs(params.args ?? [], params.stdin);
|
|
100
|
-
const toolArgs = compiledElectron
|
|
94
|
+
const toolArgs = compiledElectron ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? normalizedExplicitArgs.args;
|
|
101
95
|
const toolStdin = compiledGeneratedBatch?.stdin ?? normalizedExplicitArgs.stdin;
|
|
102
96
|
const redactedArgs = redactInvocationArgs(toolArgs);
|
|
103
97
|
const generatedStdinError = params.stdin !== undefined
|
|
104
98
|
? compiledGeneratedBatch
|
|
105
|
-
? "Do not provide stdin with
|
|
99
|
+
? "Do not provide stdin with qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin."
|
|
106
100
|
: compiledElectron
|
|
107
101
|
? "Do not provide stdin with electron; electron mode is host-only or manages its own input."
|
|
108
|
-
:
|
|
109
|
-
? "Do not provide stdin with script; browser call stdin belongs inside browser({ args, stdin })."
|
|
110
|
-
: undefined
|
|
102
|
+
: undefined
|
|
111
103
|
: undefined;
|
|
112
104
|
const outputPathError = params.outputPath !== undefined && (typeof params.outputPath !== "string" || params.outputPath.trim().length === 0)
|
|
113
105
|
? "outputPath must be a non-empty string when provided."
|
|
@@ -118,52 +110,40 @@ export function resolveAgentBrowserInput(options) {
|
|
|
118
110
|
? compiledElectron.action === "list"
|
|
119
111
|
? "electron.list has no configurable timeout; remove top-level timeoutMs."
|
|
120
112
|
: "Use electron.timeoutMs for this action; top-level timeoutMs applies only to browser CLI subprocess calls."
|
|
121
|
-
:
|
|
122
|
-
? `script timeoutMs must be ${AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS} or less.`
|
|
123
|
-
: undefined;
|
|
124
|
-
const scriptSessionModeError = compiledScript && params.sessionMode !== undefined
|
|
125
|
-
? "Do not provide sessionMode with script; script always uses its own isolated session."
|
|
126
|
-
: undefined;
|
|
113
|
+
: undefined;
|
|
127
114
|
const attachedQaSessionError = compiledQaPreset?.checks.attached
|
|
128
115
|
? params.sessionMode === "fresh"
|
|
129
116
|
? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
|
|
130
117
|
: undefined
|
|
131
118
|
: undefined;
|
|
132
119
|
const validationError = semanticActionResult.error
|
|
133
|
-
?? jobResult.error
|
|
134
120
|
?? qaResult.error
|
|
135
121
|
?? sourceLookupResult.error
|
|
136
122
|
?? networkSourceLookupResult.error
|
|
137
123
|
?? electronResult.error
|
|
138
|
-
?? scriptResult.error
|
|
139
124
|
?? inputModeError
|
|
140
125
|
?? generatedStdinError
|
|
141
126
|
?? outputPathError
|
|
142
127
|
?? timeoutMsError
|
|
143
|
-
?? scriptSessionModeError
|
|
144
128
|
?? attachedQaSessionError
|
|
145
|
-
?? (compiledElectron
|
|
129
|
+
?? (compiledElectron ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
|
|
146
130
|
const redactedCompiledJob = redactCompiledJob(compiledJob);
|
|
147
131
|
const redactedCompiledSemanticAction = compiledSemanticAction
|
|
148
132
|
? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
|
|
149
133
|
: undefined;
|
|
150
134
|
const attemptedKind = compiledElectron
|
|
151
135
|
? "electron"
|
|
152
|
-
:
|
|
153
|
-
? "
|
|
154
|
-
:
|
|
155
|
-
? "
|
|
156
|
-
:
|
|
157
|
-
? "
|
|
158
|
-
:
|
|
159
|
-
? "
|
|
160
|
-
:
|
|
161
|
-
? "
|
|
162
|
-
:
|
|
163
|
-
? "semanticAction"
|
|
164
|
-
: hasExplicitArgs
|
|
165
|
-
? "args"
|
|
166
|
-
: undefined;
|
|
136
|
+
: compiledNetworkSourceLookup
|
|
137
|
+
? "networkSourceLookup"
|
|
138
|
+
: compiledSourceLookup
|
|
139
|
+
? "sourceLookup"
|
|
140
|
+
: compiledQaPreset
|
|
141
|
+
? "qa"
|
|
142
|
+
: compiledSemanticAction
|
|
143
|
+
? "semanticAction"
|
|
144
|
+
: hasExplicitArgs
|
|
145
|
+
? "args"
|
|
146
|
+
: undefined;
|
|
167
147
|
const redactedCompiledElectron = redactCompiledElectron(compiledElectron);
|
|
168
148
|
const redactedCompiledNetworkSourceLookup = redactCompiledNetworkSourceLookup(compiledNetworkSourceLookup);
|
|
169
149
|
const redactedCompiledQaPreset = compiledQaPreset && redactedCompiledJob ? { ...redactedCompiledJob, checks: compiledQaPreset.checks } : undefined;
|
|
@@ -195,9 +175,6 @@ export function resolveAgentBrowserInput(options) {
|
|
|
195
175
|
if (compiledElectron && redactedCompiledElectron) {
|
|
196
176
|
return { ...resolvedBase, compiledElectron, kind: "electron", redactedCompiledElectron, status: "valid" };
|
|
197
177
|
}
|
|
198
|
-
if (compiledScript) {
|
|
199
|
-
return { ...resolvedBase, compiledScript, kind: "script", status: "valid" };
|
|
200
|
-
}
|
|
201
178
|
if (compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookup) {
|
|
202
179
|
return {
|
|
203
180
|
...resolvedBase,
|
|
@@ -230,16 +207,6 @@ export function resolveAgentBrowserInput(options) {
|
|
|
230
207
|
status: "valid",
|
|
231
208
|
};
|
|
232
209
|
}
|
|
233
|
-
if (jobResult.compiled && redactedCompiledJob) {
|
|
234
|
-
return {
|
|
235
|
-
...resolvedBase,
|
|
236
|
-
compiledGeneratedBatch: jobResult.compiled,
|
|
237
|
-
compiledJob: jobResult.compiled,
|
|
238
|
-
kind: "job",
|
|
239
|
-
redactedCompiledJob,
|
|
240
|
-
status: "valid",
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
210
|
if (compiledSemanticAction && redactedCompiledSemanticAction) {
|
|
244
211
|
return { ...resolvedBase, compiledSemanticAction, kind: "semanticAction", redactedCompiledSemanticAction, status: "valid" };
|
|
245
212
|
}
|
|
@@ -75,7 +75,7 @@ async function readNativeIdentity(path, cwd, signal, rootFallback, browserComman
|
|
|
75
75
|
export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
|
|
76
76
|
if (input.kind === "electron" && input.compiledElectron.action === "launch")
|
|
77
77
|
return withAgentBrowserProcessEnvironment({ AGENT_BROWSER_SESSION: undefined }, () => run(input));
|
|
78
|
-
if (input.kind === "
|
|
78
|
+
if (input.kind === "electron" || isPlainTextInspectionArgs(input.toolArgs))
|
|
79
79
|
return run(input);
|
|
80
80
|
const env = getAgentBrowserProcessEnvironment();
|
|
81
81
|
const configArg = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--config")[0];
|
|
@@ -127,8 +127,8 @@ export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
|
|
|
127
127
|
...(configPath !== undefined ? { AGENT_BROWSER_CONFIG: resolve(cwd, configPath) } : {}),
|
|
128
128
|
...(session !== undefined ? { AGENT_BROWSER_SESSION: session } : {}),
|
|
129
129
|
...(namespace !== undefined ? { AGENT_BROWSER_NAMESPACE: namespace } : {}),
|
|
130
|
-
}, () => run({ ...input, toolArgs: args, chromeStartupArgs, persistentChromeArgs, configuredChromeLaunch }, !rootDefault ? undefined : async (browserRun) => {
|
|
131
|
-
const daemon = await inspectManagedSessionDaemon({ cwd, signal, sessionName: rootDefault,
|
|
130
|
+
}, () => run({ ...input, toolArgs: args, chromeStartupArgs, persistentChromeArgs, configuredChromeLaunch }, !rootDefault ? undefined : async (browserRun, launchSignal = signal) => {
|
|
131
|
+
const daemon = await inspectManagedSessionDaemon({ cwd, signal: launchSignal, sessionName: rootDefault,
|
|
132
132
|
namespace: scanUpstreamGlobalFlagOccurrences(args, "--namespace").at(-1)?.value ?? namespace, timeoutMs: 5_000 });
|
|
133
133
|
if (daemon.status === "missing-binary")
|
|
134
134
|
return {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
import { projectUpstreamGlobalFlags, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
|
|
3
|
+
import { getDiffFilePathIndices, getExplicitArtifactDestinationIndex } from "./browser-run/artifact-paths.js";
|
|
4
|
+
import { parseBatchCommandArgument, parseUserBatchStdin } from "./batch-stdin.js";
|
|
5
|
+
/** Bind caller file operands without moving the native process/config or interpreting literal form data. */
|
|
6
|
+
export function resolveOperationPaths(args, stdin, cwd, batchStep = false) {
|
|
7
|
+
let resolved = args;
|
|
8
|
+
const setPath = (index, prefix = "") => {
|
|
9
|
+
const path = index === undefined ? undefined : args[index]?.slice(prefix.length);
|
|
10
|
+
if (index === undefined || !path || isAbsolute(path))
|
|
11
|
+
return;
|
|
12
|
+
if (resolved === args)
|
|
13
|
+
resolved = [...args];
|
|
14
|
+
resolved[index] = prefix + resolve(cwd, path);
|
|
15
|
+
};
|
|
16
|
+
const { tokens, indices } = batchStep ? { tokens: args, indices: args.map((_, index) => index) } : projectUpstreamGlobalFlags(args);
|
|
17
|
+
if (!batchStep) {
|
|
18
|
+
for (const flag of ["--config", "--ca-cert", "--executable-path", "--extension", "--init-script", "--download-path", "--screenshot-dir", "--action-policy", "--state"]) {
|
|
19
|
+
for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, flag)) {
|
|
20
|
+
if (flag === "--state" && tokens[0] === "wait" && occurrence.index > indices[0])
|
|
21
|
+
continue;
|
|
22
|
+
setPath(occurrence.index + 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const occurrence of scanUpstreamGlobalFlagOccurrences(args, "--profile")) {
|
|
26
|
+
// Native Chrome profile names are identities, not workspace file paths.
|
|
27
|
+
if (occurrence.value && /[\\/]/.test(occurrence.value) && !occurrence.value.startsWith("~"))
|
|
28
|
+
setPath(occurrence.index + 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (tokens[0] === "batch") {
|
|
32
|
+
if (tokens.slice(1).some(token => token !== "--bail")) {
|
|
33
|
+
for (let index = 1; index < tokens.length; index++) {
|
|
34
|
+
if (tokens[index] === "--bail")
|
|
35
|
+
continue;
|
|
36
|
+
const row = parseBatchCommandArgument(tokens[index]).step;
|
|
37
|
+
if (!row)
|
|
38
|
+
continue;
|
|
39
|
+
const bound = resolveOperationPaths(row, undefined, cwd, true).args;
|
|
40
|
+
if (bound === row)
|
|
41
|
+
continue;
|
|
42
|
+
if (resolved === args)
|
|
43
|
+
resolved = [...args];
|
|
44
|
+
resolved[indices[index]] = bound.map(token => `'${token.replaceAll("'", "'\\''")}'`).join(" ");
|
|
45
|
+
}
|
|
46
|
+
return { args: resolved, stdin }; // Native ignores stdin when raw rows exist.
|
|
47
|
+
}
|
|
48
|
+
const rows = parseUserBatchStdin(stdin).steps;
|
|
49
|
+
if (!rows?.length)
|
|
50
|
+
return { args: resolved, stdin };
|
|
51
|
+
const bound = rows.map(row => resolveOperationPaths(row, undefined, cwd, true).args);
|
|
52
|
+
return { args: resolved, stdin: bound.some((row, index) => row !== rows[index]) ? JSON.stringify(bound) : stdin };
|
|
53
|
+
}
|
|
54
|
+
setPath(indices[getExplicitArtifactDestinationIndex(tokens) ?? -1]);
|
|
55
|
+
if (tokens[0] === "upload")
|
|
56
|
+
for (let index = 2; index < tokens.length; index++)
|
|
57
|
+
setPath(indices[index]);
|
|
58
|
+
if (tokens[0] === "state" && tokens[1] === "load")
|
|
59
|
+
setPath(indices[2]);
|
|
60
|
+
setPath(indices[getDiffFilePathIndices(tokens).baseline ?? -1]);
|
|
61
|
+
if (tokens[0] === "cookies" && tokens[1] === "set") {
|
|
62
|
+
const index = tokens.indexOf("--curl");
|
|
63
|
+
if (index >= 0)
|
|
64
|
+
setPath(indices[index + 1]);
|
|
65
|
+
}
|
|
66
|
+
if (tokens[0] === "webmcp" && tokens[1] === "invoke") {
|
|
67
|
+
for (let index = 3; index < tokens.length; index++) {
|
|
68
|
+
if (!["--params", "--frame", "--timeout"].includes(tokens[index]))
|
|
69
|
+
continue;
|
|
70
|
+
if (tokens[index] === "--params" && tokens[index + 1]?.startsWith("@"))
|
|
71
|
+
setPath(indices[index + 1], "@");
|
|
72
|
+
index++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { args: resolved, stdin };
|
|
76
|
+
}
|
|
@@ -138,7 +138,7 @@ export async function applyAgentBrowserOutputPath(options) {
|
|
|
138
138
|
return {
|
|
139
139
|
...options.result,
|
|
140
140
|
content: appendOutputFileNotice(options.result, `Output file rejected: ${message}`, true),
|
|
141
|
-
details: { ...details, failureCategory: "validation-error", outputFile, resultCategory: "failure" },
|
|
141
|
+
details: { ...details, error: message, summary: "Output file rejected.", failureCategory: "validation-error", outputFile, resultCategory: "failure" },
|
|
142
142
|
isError: true,
|
|
143
143
|
};
|
|
144
144
|
}
|
|
@@ -163,9 +163,9 @@ export async function applyAgentBrowserOutputPath(options) {
|
|
|
163
163
|
? (() => {
|
|
164
164
|
const rest = { ...options.result.details };
|
|
165
165
|
delete rest.successCategory;
|
|
166
|
-
return { ...rest, failureCategory: rest.failureCategory ?? "upstream-error", outputFile, resultCategory: "failure" };
|
|
166
|
+
return { ...rest, error: message, summary: "Output file failed.", failureCategory: rest.failureCategory ?? "upstream-error", outputFile, resultCategory: "failure" };
|
|
167
167
|
})()
|
|
168
|
-
: { failureCategory: "upstream-error", outputFile, resultCategory: "failure" };
|
|
168
|
+
: { error: message, summary: "Output file failed.", failureCategory: "upstream-error", outputFile, resultCategory: "failure" };
|
|
169
169
|
return {
|
|
170
170
|
...options.result,
|
|
171
171
|
content: appendOutputFileNotice(options.result, `Output file failed: ${requestedPath} (${message}).`, true),
|