pi-agent-browser-native 0.2.44 → 0.2.45
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 +26 -0
- package/README.md +20 -15
- package/docs/ARCHITECTURE.md +12 -10
- package/docs/COMMAND_REFERENCE.md +49 -27
- package/docs/ELECTRON.md +1 -1
- package/docs/RELEASE.md +6 -5
- package/docs/REQUIREMENTS.md +6 -3
- package/docs/SUPPORT_MATRIX.md +17 -13
- package/docs/TOOL_CONTRACT.md +87 -46
- package/docs/platform-smoke.md +4 -3
- package/extensions/agent-browser/index.ts +29 -445
- package/extensions/agent-browser/lib/bash-guard.ts +205 -0
- package/extensions/agent-browser/lib/electron/cdp.ts +69 -0
- package/extensions/agent-browser/lib/electron/cleanup.ts +5 -58
- package/extensions/agent-browser/lib/electron/discovery.ts +2 -9
- package/extensions/agent-browser/lib/electron/launch.ts +11 -65
- package/extensions/agent-browser/lib/electron/text.ts +13 -0
- package/extensions/agent-browser/lib/fs-utils.ts +18 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +207 -21
- package/extensions/agent-browser/lib/input-modes/params.ts +17 -7
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +22 -2
- package/extensions/agent-browser/lib/input-modes/types.ts +5 -1
- package/extensions/agent-browser/lib/input-modes.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +82 -11
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +153 -30
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +53 -2
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +751 -32
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +38 -7
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -46
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +10 -1
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +28 -1
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +1 -6
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +15 -3
- package/extensions/agent-browser/lib/orchestration/output-file.ts +86 -0
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +231 -0
- package/extensions/agent-browser/lib/playbook.ts +26 -26
- package/extensions/agent-browser/lib/process.ts +1 -1
- package/extensions/agent-browser/lib/prompt-policy.ts +1 -18
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +1 -4
- package/extensions/agent-browser/lib/results/artifact-state.ts +7 -3
- package/extensions/agent-browser/lib/results/contracts.ts +6 -2
- package/extensions/agent-browser/lib/results/envelope.ts +11 -2
- package/extensions/agent-browser/lib/results/network-routes.ts +7 -4
- package/extensions/agent-browser/lib/results/network.ts +7 -1
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +88 -20
- package/extensions/agent-browser/lib/results/presentation/batch.ts +84 -12
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +81 -26
- package/extensions/agent-browser/lib/results/presentation/errors.ts +13 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +60 -0
- package/extensions/agent-browser/lib/results/presentation.ts +10 -1
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +16 -5
- package/extensions/agent-browser/lib/results/snapshot.ts +2 -0
- package/extensions/agent-browser/lib/runtime.ts +10 -1
- package/extensions/agent-browser/lib/session-page-state.ts +15 -6
- package/extensions/agent-browser/lib/web-search.ts +1 -1
- package/package.json +2 -2
- package/platform-smoke.config.mjs +5 -2
- package/scripts/platform-smoke/build-ubuntu-image.mjs +25 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +5 -1
- package/scripts/platform-smoke/doctor.mjs +6 -2
- package/scripts/platform-smoke/linux-image/Dockerfile +3 -5
- package/scripts/platform-smoke/targets.mjs +2 -1
- package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +0 -154
|
@@ -3,6 +3,7 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { isCloseCommand, isOpenNavigationCommand } from "../../command-taxonomy.js";
|
|
5
5
|
import type { ElectronLaunchRecord } from "../../electron/launch.js";
|
|
6
|
+
import { boundElectronProbeString } from "../../electron/text.js";
|
|
6
7
|
import { executableExistsOnPath } from "../../executable-path.js";
|
|
7
8
|
import type { AgentBrowserSourceLookupAnalysis, CompiledAgentBrowserJob, CompiledAgentBrowserSemanticAction } from "../../input-modes.js";
|
|
8
9
|
import { isHttpOrHttpsUrl } from "../../input-modes/job.js";
|
|
@@ -10,7 +11,7 @@ import type { AgentBrowserNextAction } from "../../results.js";
|
|
|
10
11
|
import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
|
|
11
12
|
import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
12
13
|
import { buildVisibleRefFallbackDiagnosticFromSnapshot, getVisibleRefFallbackTarget, type VisibleRefFallbackDiagnostic } from "../../results/selector-recovery.js";
|
|
13
|
-
import { extractRefSnapshotFromData, normalizeComparableUrl, type SessionTabTarget } from "../../session-page-state.js";
|
|
14
|
+
import { extractRefSnapshotFromData, normalizeComparableUrl, type SessionRefSnapshot, type SessionTabTarget } from "../../session-page-state.js";
|
|
14
15
|
import { redactInvocationArgs, redactSensitiveText, type CommandInfo } from "../../runtime.js";
|
|
15
16
|
import { isRecord } from "../../parsing.js";
|
|
16
17
|
import {
|
|
@@ -41,6 +42,7 @@ import type {
|
|
|
41
42
|
SelectorTextVisibilityDiagnostic,
|
|
42
43
|
TimeoutArtifactEvidence,
|
|
43
44
|
TimeoutPartialProgress,
|
|
45
|
+
TimeoutProgressStep,
|
|
44
46
|
} from "./types.js";
|
|
45
47
|
import type { SessionArtifactManifest } from "../../results/contracts.js";
|
|
46
48
|
|
|
@@ -50,12 +52,6 @@ export function sleepMs(ms: number): Promise<void> {
|
|
|
50
52
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
function boundElectronProbeString(value: string | undefined, maxLength = 240): string | undefined {
|
|
54
|
-
const trimmed = value?.trim();
|
|
55
|
-
if (!trimmed) return undefined;
|
|
56
|
-
return trimmed.length > maxLength ? `${trimmed.slice(0, Math.max(0, maxLength - 3))}...` : trimmed;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
55
|
export async function collectNavigationSummary(options: {
|
|
60
56
|
cwd: string;
|
|
61
57
|
sessionName?: string;
|
|
@@ -288,16 +284,22 @@ export function buildOverlayBlockerNextActions(options: { diagnostic: OverlayBlo
|
|
|
288
284
|
return [{ id: "inspect-overlay-state", params: { args: withOptionalSessionArgs(options.sessionName, ["snapshot", "-i"]) }, reason: "Refresh interactive refs and inspect whether an overlay, banner, modal, or dialog is blocking the intended click.", safety: "Read-only inspection; use current refs from this snapshot before interacting.", tool: "agent_browser" }, ...options.diagnostic.candidates.map((candidate, index) => ({ id: `try-overlay-blocker-candidate-${index + 1}`, params: { args: withOptionalSessionArgs(options.sessionName, candidate.args) }, reason: candidate.reason, safety: "Only click this if the candidate is clearly a close/dismiss control for an overlay that blocks the intended workflow.", tool: "agent_browser" as const }))];
|
|
289
285
|
}
|
|
290
286
|
|
|
287
|
+
export function collectSnapshotOverlayBlockerDiagnostic(data: unknown): OverlayBlockerDiagnostic | undefined {
|
|
288
|
+
const candidates = getOverlayBlockerCandidates(data);
|
|
289
|
+
const snapshot = extractRefSnapshotFromData(data);
|
|
290
|
+
if (candidates.length === 0 || !snapshot) return undefined;
|
|
291
|
+
return { candidates, snapshot, summary: "Snapshot contains dialog/modal context plus likely close or dismiss controls; treat covered controls as potentially obstructed until the overlay state is resolved." };
|
|
292
|
+
}
|
|
293
|
+
|
|
291
294
|
export async function collectOverlayBlockerDiagnostic(options: { command?: string; cwd: string; data: unknown; navigationSummary?: NavigationSummary; priorTarget?: SessionTabTarget; sessionName?: string; signal?: AbortSignal }): Promise<OverlayBlockerDiagnostic | undefined> {
|
|
292
295
|
if (options.command !== "click" || !isRecord(options.data) || typeof options.data.clicked !== "string") return undefined;
|
|
293
296
|
const priorUrl = normalizeComparableUrl(options.priorTarget?.url);
|
|
294
297
|
const currentUrl = normalizeComparableUrl(options.navigationSummary?.url);
|
|
295
298
|
if (!priorUrl || !currentUrl || priorUrl !== currentUrl) return undefined;
|
|
296
299
|
const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return { candidates, snapshot, summary: `Click completed but the page stayed on ${currentUrl}; a fresh snapshot contains likely overlay close/dismiss controls.` };
|
|
300
|
+
const diagnostic = collectSnapshotOverlayBlockerDiagnostic(snapshotData);
|
|
301
|
+
if (!diagnostic) return undefined;
|
|
302
|
+
return { ...diagnostic, summary: `Click completed but the page stayed on ${currentUrl}; a fresh snapshot contains likely overlay close/dismiss controls.` };
|
|
301
303
|
}
|
|
302
304
|
|
|
303
305
|
const SELECTOR_TEXT_VISIBILITY_CANDIDATE_LIMIT = 8;
|
|
@@ -605,17 +607,25 @@ export async function validateQaAttachedPrecondition(options: {
|
|
|
605
607
|
return undefined;
|
|
606
608
|
}
|
|
607
609
|
|
|
608
|
-
function getTopLevelFillInvocation(commandTokens: string[]): { expected: string; selector: string } | undefined {
|
|
610
|
+
function getTopLevelFillInvocation(commandTokens: string[]): { expected: string; refId?: string; selector: string } | undefined {
|
|
609
611
|
if (commandTokens[0] !== "fill" || commandTokens.length < 3) return undefined;
|
|
610
612
|
const selector = commandTokens[1];
|
|
611
613
|
const expected = commandTokens.slice(2).join(" ");
|
|
612
|
-
|
|
614
|
+
const refId = selector?.match(/^@?(e\d+)$/)?.[1];
|
|
615
|
+
return selector && expected.length > 0 ? { expected, ...(refId ? { refId } : {}), selector } : undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function shouldVerifyContenteditableFill(fill: { refId?: string } | undefined, refSnapshot?: SessionRefSnapshot): boolean {
|
|
619
|
+
if (!fill?.refId) return false;
|
|
620
|
+
const ref = refSnapshot?.refs?.[fill.refId];
|
|
621
|
+
if (!ref) return false;
|
|
622
|
+
return ref.isContentEditable === true && (ref.role === "generic" || ref.role === "unknown" || ref.role === "textbox");
|
|
613
623
|
}
|
|
614
624
|
|
|
615
625
|
export function buildFillVerificationNextActions(diagnostic: FillVerificationDiagnostic, sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
616
626
|
return [
|
|
617
|
-
{ id: "inspect-after-fill-verification", params: { args: withOptionalSessionArgs(sessionName, ["snapshot", "-i"]) }, reason: "Refresh the UI after a fill that reported success but did not appear to update the
|
|
618
|
-
{ id: "verify-filled-value", params: { args: withOptionalSessionArgs(sessionName, ["get",
|
|
627
|
+
{ id: "inspect-after-fill-verification", params: { args: withOptionalSessionArgs(sessionName, ["snapshot", "-i"]) }, reason: "Refresh the UI after a fill that reported success but did not appear to update the target.", safety: "Read-only snapshot; use current refs before retrying.", tool: "agent_browser" },
|
|
628
|
+
{ id: "verify-filled-value", params: { args: withOptionalSessionArgs(sessionName, ["get", diagnostic.method, diagnostic.selector]) }, reason: `Check the target ${diagnostic.method} directly before submitting or creating files.`, safety: "Read-only check; selector may still be stale if the UI rerendered.", tool: "agent_browser" },
|
|
619
629
|
];
|
|
620
630
|
}
|
|
621
631
|
|
|
@@ -627,22 +637,30 @@ function extractFillVerificationValue(data: unknown): string | undefined {
|
|
|
627
637
|
return undefined;
|
|
628
638
|
}
|
|
629
639
|
|
|
630
|
-
export async function collectFillVerificationDiagnostic(options: { commandTokens: string[]; cwd: string; sessionName?: string; signal?: AbortSignal }): Promise<FillVerificationDiagnostic | undefined> {
|
|
640
|
+
export async function collectFillVerificationDiagnostic(options: { commandTokens: string[]; cwd: string; forceValueVerification?: boolean; refSnapshot?: SessionRefSnapshot; sessionName?: string; signal?: AbortSignal }): Promise<FillVerificationDiagnostic | undefined> {
|
|
631
641
|
const fill = getTopLevelFillInvocation(options.commandTokens);
|
|
632
642
|
if (!fill || !options.sessionName) return undefined;
|
|
643
|
+
const contenteditable = shouldVerifyContenteditableFill(fill, options.refSnapshot);
|
|
644
|
+
if (!contenteditable && !options.forceValueVerification) return undefined;
|
|
645
|
+
const method = contenteditable ? "text" : "value";
|
|
633
646
|
let valueData: unknown | undefined;
|
|
634
|
-
try { valueData = await runSessionCommandData({ args: ["get",
|
|
647
|
+
try { valueData = await runSessionCommandData({ args: ["get", method, fill.selector], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: ELECTRON_FILL_VERIFICATION_TIMEOUT_MS }); } catch { return undefined; }
|
|
635
648
|
const actual = extractFillVerificationValue(valueData);
|
|
636
649
|
if (actual === undefined || actual === fill.expected) return undefined;
|
|
637
|
-
const
|
|
650
|
+
const reason = contenteditable ? "contenteditable-fill-mismatch" : "value-fill-mismatch";
|
|
651
|
+
const actualPreview = actual.length > 0 ? `"${boundElectronProbeString(actual, 80)}"` : `an empty ${method}`;
|
|
652
|
+
const diagnostic: FillVerificationDiagnostic = { actual: actual.length > 0 ? boundElectronProbeString(actual, 160) : "", expected: boundElectronProbeString(fill.expected, 160) ?? fill.expected, method, nextActionIds: [], reason, selector: fill.selector, status: "mismatch", summary: `Fill verification warning: fill ${fill.selector} reported success, but get ${method} returned ${actualPreview}.` };
|
|
638
653
|
diagnostic.nextActionIds = buildFillVerificationNextActions(diagnostic, options.sessionName).map((action) => action.id);
|
|
639
654
|
return diagnostic;
|
|
640
655
|
}
|
|
641
656
|
|
|
642
657
|
export function formatFillVerificationText(diagnostic: FillVerificationDiagnostic | undefined): string | undefined {
|
|
643
658
|
if (!diagnostic) return undefined;
|
|
644
|
-
const actual = diagnostic.actual !== undefined ? `actual "${diagnostic.actual}"` :
|
|
645
|
-
|
|
659
|
+
const actual = diagnostic.actual !== undefined ? `actual "${diagnostic.actual}"` : `actual ${diagnostic.method} unavailable`;
|
|
660
|
+
const recovery = diagnostic.reason === "contenteditable-fill-mismatch"
|
|
661
|
+
? "Contenteditable fill may append or prepend instead of replacing. Re-run snapshot -i, then prefer focus/click plus keyboard shortcut selection or direct keyboard insertion only after verifying the editor state."
|
|
662
|
+
: "Re-run snapshot -i, then prefer click/focus plus keyboard type for custom quick-input controls before submitting.";
|
|
663
|
+
return `${diagnostic.summary}\nExpected: "${diagnostic.expected}"; ${actual}.\nNext: ${recovery}`;
|
|
646
664
|
}
|
|
647
665
|
|
|
648
666
|
export async function collectVisibleRefFallbackDiagnostic(options: { commandTokens: string[]; compiledSemanticAction?: CompiledAgentBrowserSemanticAction; cwd: string; sessionName?: string; signal?: AbortSignal }): Promise<VisibleRefFallbackDiagnostic | undefined> {
|
|
@@ -669,8 +687,8 @@ export async function collectElectronHandoff(options: { cwd: string; handoff: "c
|
|
|
669
687
|
return { handoff: "snapshot", refSnapshot, snapshot, ...(snapshotRetryCount > 0 ? { snapshotRetryCount } : {}), tabs };
|
|
670
688
|
}
|
|
671
689
|
|
|
672
|
-
function getTimeoutProgressSteps(compiledJob: CompiledAgentBrowserJob | undefined, command: string | undefined, stdin: string | undefined): Array<{ args: string[]; index: number }> {
|
|
673
|
-
if (compiledJob) return compiledJob.steps.map((step, index) => ({ args: step.args, index: index + 1 }));
|
|
690
|
+
function getTimeoutProgressSteps(compiledJob: CompiledAgentBrowserJob | undefined, command: string | undefined, stdin: string | undefined): Array<{ args: string[]; generatedFrom?: string; index: number }> {
|
|
691
|
+
if (compiledJob) return compiledJob.steps.map((step, index) => ({ args: step.args, generatedFrom: step.generatedFrom, index: index + 1 }));
|
|
674
692
|
if (command !== "batch" || !stdin) return [];
|
|
675
693
|
return parseValidBatchStepEntries(stdin).map(({ index, step }) => ({ args: step, index: index + 1 }));
|
|
676
694
|
}
|
|
@@ -721,8 +739,8 @@ async function collectTimeoutArtifactEvidence(cwd: string, steps: Array<{ args:
|
|
|
721
739
|
const absolutePath = isAbsolute(path) ? path : resolve(cwd, path);
|
|
722
740
|
const artifact = await statTimeoutArtifactPath(absolutePath);
|
|
723
741
|
evidence.push(artifact.exists
|
|
724
|
-
? { absolutePath, exists: true, path, sizeBytes: artifact.sizeBytes, stepIndex: step.index }
|
|
725
|
-
: { absolutePath, exists: false, path, stepIndex: step.index });
|
|
742
|
+
? { absolutePath, exists: true, path, sizeBytes: artifact.sizeBytes, state: "verified", stepIndex: step.index }
|
|
743
|
+
: { absolutePath, exists: false, path, state: "missing", stepIndex: step.index });
|
|
726
744
|
}
|
|
727
745
|
return evidence;
|
|
728
746
|
}
|
|
@@ -735,18 +753,116 @@ function getPlannedCurrentPageUrl(steps: Array<{ args: string[]; index: number }
|
|
|
735
753
|
return undefined;
|
|
736
754
|
}
|
|
737
755
|
|
|
756
|
+
const TIMEOUT_RETRYABLE_COMMANDS = new Set([
|
|
757
|
+
"console",
|
|
758
|
+
"diff",
|
|
759
|
+
"errors",
|
|
760
|
+
"get",
|
|
761
|
+
"goto",
|
|
762
|
+
"navigate",
|
|
763
|
+
"network",
|
|
764
|
+
"open",
|
|
765
|
+
"pdf",
|
|
766
|
+
"pushstate",
|
|
767
|
+
"screenshot",
|
|
768
|
+
"snapshot",
|
|
769
|
+
"tab",
|
|
770
|
+
"vitals",
|
|
771
|
+
"wait",
|
|
772
|
+
]);
|
|
773
|
+
|
|
774
|
+
function getTimeoutStepRetry(step: { args: string[] }): { args: string[] } | undefined {
|
|
775
|
+
const command = step.args[0];
|
|
776
|
+
return command && TIMEOUT_RETRYABLE_COMMANDS.has(command) ? { args: step.args } : undefined;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function normalizeUrlForTimeoutComparison(url: string | undefined): URL | undefined {
|
|
780
|
+
if (!url) return undefined;
|
|
781
|
+
try {
|
|
782
|
+
return new URL(url);
|
|
783
|
+
} catch {
|
|
784
|
+
return undefined;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function currentUrlMatchesNavigationStep(currentUrl: string | undefined, plannedUrl: string | undefined): boolean {
|
|
789
|
+
if (!currentUrl || !plannedUrl) return false;
|
|
790
|
+
if (currentUrl === plannedUrl) return true;
|
|
791
|
+
const current = normalizeUrlForTimeoutComparison(currentUrl);
|
|
792
|
+
const planned = normalizeUrlForTimeoutComparison(plannedUrl);
|
|
793
|
+
if (!current || !planned || current.origin !== planned.origin) return false;
|
|
794
|
+
const plannedPath = planned.pathname.endsWith("/") ? planned.pathname : `${planned.pathname}/`;
|
|
795
|
+
const currentPath = current.pathname.endsWith("/") ? current.pathname : `${current.pathname}/`;
|
|
796
|
+
return planned.pathname === "/" || currentPath.startsWith(plannedPath);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function buildTimeoutProgressSteps(options: {
|
|
800
|
+
artifacts: TimeoutArtifactEvidence[];
|
|
801
|
+
currentPageSource?: "live" | "planned";
|
|
802
|
+
currentPageUrl?: string;
|
|
803
|
+
steps: Array<{ args: string[]; generatedFrom?: string; index: number }>;
|
|
804
|
+
}): { openedButPostOpenTimedOut?: boolean; retryStep?: TimeoutProgressStep; steps: TimeoutProgressStep[] } {
|
|
805
|
+
let retryStep: TimeoutProgressStep | undefined;
|
|
806
|
+
let lastCompletedNavigationIndex: number | undefined;
|
|
807
|
+
const progressSteps = options.steps.map((step): TimeoutProgressStep => {
|
|
808
|
+
const stepArtifacts = options.artifacts.filter((artifact) => artifact.stepIndex === step.index);
|
|
809
|
+
const command = step.args[0];
|
|
810
|
+
const navigationUrl = isOpenNavigationCommand(command) || command === "pushstate" ? getLastPositionalToken(step.args) : undefined;
|
|
811
|
+
if (stepArtifacts.some((artifact) => artifact.exists)) {
|
|
812
|
+
return { ...step, reason: "Declared artifact exists on disk after timeout.", status: "completed" };
|
|
813
|
+
}
|
|
814
|
+
if (options.currentPageSource === "live" && currentUrlMatchesNavigationStep(options.currentPageUrl, navigationUrl)) {
|
|
815
|
+
lastCompletedNavigationIndex = step.index;
|
|
816
|
+
return { ...step, reason: "Live page URL was recovered after timeout.", status: "completed" };
|
|
817
|
+
}
|
|
818
|
+
return { ...step, reason: stepArtifacts.length > 0 ? "Declared artifact was not present when the watchdog fired." : undefined, status: "unknown" };
|
|
819
|
+
});
|
|
820
|
+
const highestCompletedIndex = Math.max(0, ...progressSteps.filter((step) => step.status === "completed").map((step) => step.index));
|
|
821
|
+
for (const step of progressSteps) {
|
|
822
|
+
if (step.status === "unknown" && step.index < highestCompletedIndex) {
|
|
823
|
+
step.status = "completed";
|
|
824
|
+
step.reason = "Later step completion evidence indicates the batch advanced past this step before timeout.";
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
for (const step of progressSteps) {
|
|
828
|
+
if (step.status === "completed") continue;
|
|
829
|
+
if (!retryStep) {
|
|
830
|
+
const retry = getTimeoutStepRetry(step);
|
|
831
|
+
retryStep = {
|
|
832
|
+
...step,
|
|
833
|
+
reason: step.reason ?? (retry ? "Likely active when the wrapper watchdog fired." : "Likely active when the wrapper watchdog fired; executable retry omitted because this step may have already mutated page state."),
|
|
834
|
+
retry,
|
|
835
|
+
status: "failed",
|
|
836
|
+
};
|
|
837
|
+
Object.assign(step, retryStep);
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
step.status = "pending";
|
|
841
|
+
step.reason = step.reason ?? `Pending behind timed-out step ${retryStep.index}.`;
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
openedButPostOpenTimedOut: lastCompletedNavigationIndex !== undefined && retryStep !== undefined && retryStep.index > lastCompletedNavigationIndex,
|
|
845
|
+
retryStep,
|
|
846
|
+
steps: progressSteps,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
738
850
|
export async function collectTimeoutPartialProgress(options: { command?: string; compiledJob?: CompiledAgentBrowserJob; cwd: string; sessionName?: string; stdin?: string }): Promise<TimeoutPartialProgress | undefined> {
|
|
739
|
-
const
|
|
740
|
-
const artifacts = await collectTimeoutArtifactEvidence(options.cwd,
|
|
851
|
+
const rawSteps = getTimeoutProgressSteps(options.compiledJob, options.command, options.stdin);
|
|
852
|
+
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, rawSteps);
|
|
741
853
|
const [urlData, titleData] = await Promise.all([runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName }), runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, sessionName: options.sessionName })]);
|
|
742
854
|
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
743
855
|
const title = extractStringResultField(titleData, "result") ?? extractStringResultField(titleData, "title");
|
|
744
|
-
const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(
|
|
856
|
+
const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(rawSteps);
|
|
745
857
|
const url = recoveredUrl ?? plannedUrl;
|
|
746
|
-
|
|
858
|
+
const currentPageSource = recoveredUrl ? "live" as const : plannedUrl ? "planned" as const : title ? "live" as const : undefined;
|
|
859
|
+
const stepProgress = buildTimeoutProgressSteps({ artifacts, currentPageSource: recoveredUrl ? "live" : undefined, currentPageUrl: recoveredUrl, steps: rawSteps });
|
|
860
|
+
if (rawSteps.length === 0 && artifacts.length === 0 && !url && !title) return undefined;
|
|
747
861
|
const foundArtifacts = artifacts.filter((artifact) => artifact.exists).length;
|
|
862
|
+
const completedSteps = stepProgress.steps.filter((step) => step.status === "completed").length;
|
|
748
863
|
const pageStateSummary = recoveredUrl || title ? " and current page state" : plannedUrl ? " and planned page URL" : "";
|
|
749
|
-
|
|
864
|
+
const retrySummary = stepProgress.retryStep ? ` Retry step ${stepProgress.retryStep.index} is the first incomplete step.` : "";
|
|
865
|
+
return { artifacts, currentPage: url || title ? { source: currentPageSource, title, url } : undefined, liveUrlRecovered: recoveredUrl !== undefined, openedButPostOpenTimedOut: stepProgress.openedButPostOpenTimedOut, retryStep: stepProgress.retryStep, steps: stepProgress.steps.length > 0 ? stepProgress.steps : undefined, summary: `Timed out before upstream returned final results; recovered ${completedSteps}/${rawSteps.length} planned step state${rawSteps.length === 1 ? "" : "s"} and ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}.${retrySummary}` };
|
|
750
866
|
}
|
|
751
867
|
|
|
752
868
|
function redactSensitivePathSegmentsForDiagnostic(path: string): string {
|
|
@@ -775,9 +891,16 @@ export function formatTimeoutPartialProgressText(progress: TimeoutPartialProgres
|
|
|
775
891
|
if (progress.steps && progress.steps.length > 0) {
|
|
776
892
|
const shownSteps = progress.steps.slice(0, 6);
|
|
777
893
|
lines.push("Planned steps:");
|
|
778
|
-
for (const step of shownSteps)
|
|
894
|
+
for (const step of shownSteps) {
|
|
895
|
+
const commandText = redactSensitivePathSegmentsForDiagnostic(redactInvocationArgs(step.args).join(" "));
|
|
896
|
+
const generatedFrom = step.generatedFrom ? `, generated from ${step.generatedFrom}` : "";
|
|
897
|
+
lines.push(`- Step ${step.index} [${step.status}${generatedFrom}]: ${commandText}${step.reason ? ` — ${redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(step.reason))}` : ""}`);
|
|
898
|
+
}
|
|
779
899
|
if (progress.steps.length > shownSteps.length) lines.push(`- ... ${progress.steps.length - shownSteps.length} more step${progress.steps.length - shownSteps.length === 1 ? "" : "s"} omitted`);
|
|
780
900
|
}
|
|
901
|
+
if (progress.retryStep?.retry?.args) {
|
|
902
|
+
lines.push(`Retry failed step: ${JSON.stringify({ args: redactInvocationArgs(progress.retryStep.retry.args) })}`);
|
|
903
|
+
}
|
|
781
904
|
for (const artifact of progress.artifacts) lines.push(`Artifact from step ${artifact.stepIndex}: ${redactSensitivePathSegmentsForDiagnostic(artifact.path)} (${artifact.exists ? `exists${typeof artifact.sizeBytes === "number" ? `, ${artifact.sizeBytes} bytes` : ""}` : "missing"})`);
|
|
782
905
|
return lines.join("\n");
|
|
783
906
|
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
AgentBrowserNextActionCollector,
|
|
20
20
|
alignPageChangeSummaryNextActionIds,
|
|
21
21
|
isStandaloneSnapshotNextAction,
|
|
22
|
+
withOptionalSessionArgs,
|
|
22
23
|
} from "../../results/next-actions.js";
|
|
23
24
|
import {
|
|
24
25
|
buildConnectedSessionNextActions,
|
|
@@ -282,7 +283,8 @@ export async function prepareFinalResultRecoveryState(options: {
|
|
|
282
283
|
let visibleRefFallbackDiagnostic: FinalRecoveryState["visibleRefFallbackDiagnostic"];
|
|
283
284
|
const visibleRefFallbackSessionName = options.executionPlan.sessionName ?? extractExplicitSessionName(options.runtimeToolArgs);
|
|
284
285
|
if (categoryDetails.failureCategory === "selector-not-found") {
|
|
285
|
-
|
|
286
|
+
const selectorRecoveryCommandTokens = options.presentation.batchFailure?.failedStep.command ?? options.commandTokens;
|
|
287
|
+
visibleRefFallbackDiagnostic = await collectVisibleRefFallbackDiagnostic({ commandTokens: selectorRecoveryCommandTokens, compiledSemanticAction: options.compiledSemanticAction, cwd: options.cwd, sessionName: visibleRefFallbackSessionName, signal: options.signal });
|
|
286
288
|
if (visibleRefFallbackDiagnostic && visibleRefFallbackSessionName) {
|
|
287
289
|
const refUpdate = options.sessionPageState.applyRefSnapshot({ fallbackTarget: options.currentSessionTabTarget, sessionName: visibleRefFallbackSessionName, snapshot: visibleRefFallbackDiagnostic.snapshot, update: options.sessionPageStateUpdate });
|
|
288
290
|
currentRefSnapshot = refUpdate.refSnapshot;
|
|
@@ -299,6 +301,51 @@ export async function prepareFinalResultRecoveryState(options: {
|
|
|
299
301
|
return { categoryDetails, currentRefSnapshot, currentRefSnapshotInvalidation, noActivePageSnapshotFailure, richInputRecoveryDiagnostic, visibleRefFallbackDiagnostic, visibleRefFallbackSessionName };
|
|
300
302
|
}
|
|
301
303
|
|
|
304
|
+
function buildTimeoutPartialProgressNextActions(options: FinalResultInput): AgentBrowserNextAction[] {
|
|
305
|
+
const retryArgs = options.timeoutPartialProgress?.retryStep?.retry?.args;
|
|
306
|
+
if (!retryArgs) return [];
|
|
307
|
+
const stepIndex = options.timeoutPartialProgress?.retryStep?.index;
|
|
308
|
+
const freshSessionAbandoned = options.sessionMode === "fresh" && options.timeoutPartialProgress?.liveUrlRecovered !== true;
|
|
309
|
+
return [{
|
|
310
|
+
id: "retry-timeout-step",
|
|
311
|
+
params: freshSessionAbandoned
|
|
312
|
+
? { args: retryArgs, sessionMode: "fresh" }
|
|
313
|
+
: { args: withOptionalSessionArgs(options.executionPlan.sessionName, retryArgs) },
|
|
314
|
+
reason: freshSessionAbandoned
|
|
315
|
+
? `Retry the first incomplete timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} in a fresh browser session because the timed-out fresh session was not proven live.`
|
|
316
|
+
: `Retry the first incomplete timed-out step${stepIndex === undefined ? "" : ` ${stepIndex}`} against the current browser session.`,
|
|
317
|
+
safety: "Only read-only or idempotent timeout steps get executable retry args; inspect current page/artifact state before using the action.",
|
|
318
|
+
tool: "agent_browser" as const,
|
|
319
|
+
}];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function buildDialogTimeoutNextActions(options: { command?: string; sessionName?: string }): AgentBrowserNextAction[] {
|
|
323
|
+
if (options.command !== "dialog" && options.command !== "click" && options.command !== "tap" && options.command !== "find" && options.command !== "eval") return [];
|
|
324
|
+
return [
|
|
325
|
+
{
|
|
326
|
+
id: "inspect-dialog-after-timeout",
|
|
327
|
+
params: { args: withOptionalSessionArgs(options.sessionName, ["dialog", "status"]) },
|
|
328
|
+
reason: "Check whether a blocking JavaScript dialog is pending after the timed-out interaction.",
|
|
329
|
+
safety: "Read-only dialog status; this wrapper bounds dialog commands so recovery attempts do not wait for the full default watchdog.",
|
|
330
|
+
tool: "agent_browser" as const,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: "dismiss-dialog-after-timeout",
|
|
334
|
+
params: { args: withOptionalSessionArgs(options.sessionName, ["dialog", "dismiss"]) },
|
|
335
|
+
reason: "Dismiss a pending alert/confirm/prompt when the workflow can safely abandon the dialog.",
|
|
336
|
+
safety: "Only run when dismissing/canceling the dialog is acceptable for the user flow.",
|
|
337
|
+
tool: "agent_browser" as const,
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: "recover-fresh-session-after-dialog-timeout",
|
|
341
|
+
params: { args: ["open", "about:blank"], sessionMode: "fresh" as const },
|
|
342
|
+
reason: "Start a clean browser session if the current session remains blocked behind a JavaScript dialog.",
|
|
343
|
+
safety: "Replace about:blank with the intended recovery URL; this abandons the blocked managed session.",
|
|
344
|
+
tool: "agent_browser" as const,
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
}
|
|
348
|
+
|
|
302
349
|
function buildResultNextActions(options: FinalResultInput): AgentBrowserNextAction[] | undefined {
|
|
303
350
|
const nextActionCollector = new AgentBrowserNextActionCollector(options.presentation.nextActions);
|
|
304
351
|
if (options.categoryDetails.resultCategory === "success" && options.executionPlan.commandInfo.command === "connect" && !options.electronLaunchRecord) nextActionCollector.appendUnique(buildConnectedSessionNextActions(options.executionPlan.sessionName));
|
|
@@ -322,10 +369,14 @@ function buildResultNextActions(options: FinalResultInput): AgentBrowserNextActi
|
|
|
322
369
|
if (options.selectorTextVisibilityDiagnostics.length > 0) nextActionCollector.append(buildSelectorTextVisibilityNextActions({ diagnostics: options.selectorTextVisibilityDiagnostics, sessionName: options.executionPlan.sessionName }));
|
|
323
370
|
if (options.electronBroadGetTextScopeDiagnostics.length > 0) nextActionCollector.append(buildElectronBroadGetTextScopeNextActions({ diagnostics: options.electronBroadGetTextScopeDiagnostics, sessionName: options.executionPlan.sessionName }));
|
|
324
371
|
if (options.sourceLookup?.electronContext) nextActionCollector.appendUnique(buildSourceLookupElectronNextActions(options.sourceLookup));
|
|
325
|
-
if (options.clickDispatchDiagnostic) nextActionCollector.append(buildClickDispatchNextActions({ commandTokens: options.commandTokens, sessionName: options.executionPlan.sessionName }));
|
|
372
|
+
if (options.clickDispatchDiagnostic) nextActionCollector.append(buildClickDispatchNextActions({ commandTokens: options.commandTokens, diagnostic: options.clickDispatchDiagnostic, sessionName: options.executionPlan.sessionName }));
|
|
326
373
|
if (options.scrollNoopDiagnostic) nextActionCollector.append(buildScrollNoopNextActions(options.executionPlan.sessionName));
|
|
327
374
|
if (options.comboboxFocusDiagnostic) nextActionCollector.append(buildComboboxFocusNextActions(options.executionPlan.sessionName));
|
|
328
375
|
if (options.managedSessionOutcome) nextActionCollector.appendUnique(buildManagedSessionFreshFailureNextActions(options.managedSessionOutcome));
|
|
376
|
+
if (options.categoryDetails.failureCategory === "timeout" && options.processResult.timedOut) {
|
|
377
|
+
nextActionCollector.appendUnique(buildTimeoutPartialProgressNextActions(options));
|
|
378
|
+
nextActionCollector.appendUnique(buildDialogTimeoutNextActions({ command: options.executionPlan.commandInfo.command, sessionName: options.executionPlan.sessionName }));
|
|
379
|
+
}
|
|
329
380
|
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 }]);
|
|
330
381
|
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 }));
|
|
331
382
|
return nextActionCollector.toArray();
|
|
@@ -24,6 +24,7 @@ export async function runAgentBrowserTool(options: BrowserRunOptions): Promise<A
|
|
|
24
24
|
env: prepared.executionPlan.managedSessionName ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: options.implicitSessionIdleTimeoutMs } : undefined,
|
|
25
25
|
signal: options.signal,
|
|
26
26
|
stdin: prepared.processStdin,
|
|
27
|
+
timeoutMs: prepared.processTimeoutMs,
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
const missingBinaryResult = await buildMissingBinaryFailureResult({
|