pi-agent-browser-native 0.2.44 → 0.2.46
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 +42 -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 +43 -450
- 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 +28 -11
- 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/json-schema.ts +73 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +82 -11
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +159 -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 +252 -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/string-enum-schema.ts +20 -0
- package/extensions/agent-browser/lib/web-search.ts +31 -13
- 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
|
@@ -71,7 +71,48 @@ function buildClickDispatchProbeInstallScript(probe: ClickDispatchProbe): string
|
|
|
71
71
|
const marker = ${JSON.stringify(probe.marker)};
|
|
72
72
|
const element = ${resolveTarget};
|
|
73
73
|
if (!element) return { status: "target-not-found", marker };
|
|
74
|
-
const
|
|
74
|
+
const cssEscape = (value) => {
|
|
75
|
+
if (window.CSS && typeof window.CSS.escape === "function") return window.CSS.escape(value);
|
|
76
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
77
|
+
};
|
|
78
|
+
const getSelector = (node) => {
|
|
79
|
+
if (!(node instanceof Element)) return undefined;
|
|
80
|
+
if (node.id) return "#" + cssEscape(node.id);
|
|
81
|
+
const testId = node.getAttribute("data-testid") || node.getAttribute("data-test-id");
|
|
82
|
+
if (testId) return '[data-testid="' + cssEscape(testId) + '"]';
|
|
83
|
+
const parts = [];
|
|
84
|
+
let current = node;
|
|
85
|
+
while (current && current !== document.body && parts.length < 4) {
|
|
86
|
+
const tag = current.tagName.toLowerCase();
|
|
87
|
+
const parent = current.parentElement;
|
|
88
|
+
if (!parent) break;
|
|
89
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === current.tagName);
|
|
90
|
+
const index = siblings.indexOf(current) + 1;
|
|
91
|
+
parts.unshift(siblings.length > 1 ? tag + ':nth-of-type(' + index + ')' : tag);
|
|
92
|
+
current = parent;
|
|
93
|
+
}
|
|
94
|
+
return parts.length > 0 ? parts.join(" > ") : undefined;
|
|
95
|
+
};
|
|
96
|
+
const rectInfo = (rect) => ({ bottom: rect.bottom, left: rect.left, right: rect.right, top: rect.top });
|
|
97
|
+
const targetRect = element.getBoundingClientRect();
|
|
98
|
+
const targetOutsideViewport = targetRect.bottom < 0 || targetRect.right < 0 || targetRect.top > window.innerHeight || targetRect.left > window.innerWidth;
|
|
99
|
+
let nearestScrollContainer;
|
|
100
|
+
for (let current = element.parentElement; current && current !== document.body; current = current.parentElement) {
|
|
101
|
+
if (current.scrollHeight > current.clientHeight + 1 || current.scrollWidth > current.clientWidth + 1) {
|
|
102
|
+
const containerRect = current.getBoundingClientRect();
|
|
103
|
+
nearestScrollContainer = {
|
|
104
|
+
selector: getSelector(current),
|
|
105
|
+
tagName: current.tagName.toLowerCase(),
|
|
106
|
+
targetOutsideContainer: targetRect.bottom < containerRect.top || targetRect.top > containerRect.bottom || targetRect.right < containerRect.left || targetRect.left > containerRect.right,
|
|
107
|
+
targetOutsideViewport,
|
|
108
|
+
rect: rectInfo(containerRect),
|
|
109
|
+
scrollLeft: current.scrollLeft,
|
|
110
|
+
scrollTop: current.scrollTop,
|
|
111
|
+
};
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const state = { events: [], target: { tagName: element.tagName.toLowerCase(), nearestScrollContainer, rect: rectInfo(targetRect), targetOutsideViewport } };
|
|
75
116
|
const eventTypes = ["pointerdown", "mousedown", "pointerup", "mouseup", "click"];
|
|
76
117
|
const listeners = eventTypes.map((type) => {
|
|
77
118
|
const listener = (event) => {
|
|
@@ -126,9 +167,9 @@ export function formatClickDispatchDiagnosticText(diagnostic: ClickDispatchDiagn
|
|
|
126
167
|
return `Click dispatch diagnostic: ${diagnostic.summary}`;
|
|
127
168
|
}
|
|
128
169
|
|
|
129
|
-
export function buildClickDispatchNextActions(options: { commandTokens: string[]; sessionName?: string }): AgentBrowserNextAction[] {
|
|
170
|
+
export function buildClickDispatchNextActions(options: { commandTokens: string[]; diagnostic?: ClickDispatchDiagnostic; sessionName?: string }): AgentBrowserNextAction[] {
|
|
130
171
|
const retryArgs = options.commandTokens[0] === "click" ? options.commandTokens : ["click", ...options.commandTokens];
|
|
131
|
-
|
|
172
|
+
const actions: AgentBrowserNextAction[] = [
|
|
132
173
|
{
|
|
133
174
|
id: "inspect-click-dispatch-miss",
|
|
134
175
|
params: { args: withOptionalSessionArgs(options.sessionName, ["snapshot", "-i"]) },
|
|
@@ -136,14 +177,26 @@ export function buildClickDispatchNextActions(options: { commandTokens: string[]
|
|
|
136
177
|
safety: "Read-only snapshot; the wrapper does not replay clicks in-page when upstream reports success without DOM events.",
|
|
137
178
|
tool: "agent_browser",
|
|
138
179
|
},
|
|
139
|
-
{
|
|
140
|
-
id: "retry-click-after-dispatch-miss",
|
|
141
|
-
params: { args: withOptionalSessionArgs(options.sessionName, retryArgs) },
|
|
142
|
-
reason: "Retry the same upstream click after confirming the target is visible; do not assume the prior success mutated the page.",
|
|
143
|
-
safety: "Only retry when the target is still intended; use page-change evidence or a fresh snapshot before continuing the workflow.",
|
|
144
|
-
tool: "agent_browser",
|
|
145
|
-
},
|
|
146
180
|
];
|
|
181
|
+
if (options.diagnostic?.scrollContainer) {
|
|
182
|
+
actions.push({
|
|
183
|
+
id: "scroll-target-into-view-after-dispatch-miss",
|
|
184
|
+
params: { args: withOptionalSessionArgs(options.sessionName, ["scrollintoview", retryArgs[1]].filter((item): item is string => typeof item === "string")) },
|
|
185
|
+
reason: options.diagnostic.scrollContainer.selector
|
|
186
|
+
? `The target may be outside nested scroll container ${options.diagnostic.scrollContainer.selector}; scroll the target into view before retrying the click.`
|
|
187
|
+
: "The target may be inside an offscreen nested scroll container; scroll the target into view before retrying the click.",
|
|
188
|
+
safety: "Use only for the same current page and target; run snapshot -i again if the page rerendered.",
|
|
189
|
+
tool: "agent_browser",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
actions.push({
|
|
193
|
+
id: "retry-click-after-dispatch-miss",
|
|
194
|
+
params: { args: withOptionalSessionArgs(options.sessionName, retryArgs) },
|
|
195
|
+
reason: "Retry the same upstream click after confirming the target is visible; do not assume the prior success mutated the page.",
|
|
196
|
+
safety: "Only retry when the target is still intended; use page-change evidence or a fresh snapshot before continuing the workflow.",
|
|
197
|
+
tool: "agent_browser",
|
|
198
|
+
});
|
|
199
|
+
return actions;
|
|
147
200
|
}
|
|
148
201
|
|
|
149
202
|
export async function prepareClickDispatchProbe(options: { commandTokens: string[]; cwd: string; refSnapshot?: SessionRefSnapshot; sessionName?: string; signal?: AbortSignal }): Promise<ClickDispatchProbe | undefined> {
|
|
@@ -156,6 +209,20 @@ export async function prepareClickDispatchProbe(options: { commandTokens: string
|
|
|
156
209
|
return installResult?.status === "installed" ? probe : undefined;
|
|
157
210
|
}
|
|
158
211
|
|
|
212
|
+
function getClickDispatchScrollContainerDiagnostic(result: Record<string, unknown>): ClickDispatchDiagnostic["scrollContainer"] {
|
|
213
|
+
const target = isRecord(result.target) ? result.target : undefined;
|
|
214
|
+
const scrollContainer = isRecord(target?.nearestScrollContainer) ? target.nearestScrollContainer : undefined;
|
|
215
|
+
const targetOutsideViewport = typeof target?.targetOutsideViewport === "boolean" ? target.targetOutsideViewport : undefined;
|
|
216
|
+
const targetOutsideContainer = typeof scrollContainer?.targetOutsideContainer === "boolean" ? scrollContainer.targetOutsideContainer : undefined;
|
|
217
|
+
if (!scrollContainer && !targetOutsideViewport) return undefined;
|
|
218
|
+
if (targetOutsideContainer !== true && targetOutsideViewport !== true) return undefined;
|
|
219
|
+
const selector = typeof scrollContainer?.selector === "string" ? redactSensitiveText(scrollContainer.selector) : undefined;
|
|
220
|
+
const summary = selector
|
|
221
|
+
? `Target appears outside nested scroll container ${selector}; use scrollintoview on the target or scroll that container before retrying.`
|
|
222
|
+
: "Target appears outside the viewport or a nested scroll container; use scrollintoview on the target before retrying.";
|
|
223
|
+
return { selector, summary, targetOutsideContainer, targetOutsideViewport };
|
|
224
|
+
}
|
|
225
|
+
|
|
159
226
|
export async function collectClickDispatchDiagnostic(options: { cwd: string; probe?: ClickDispatchProbe; sessionName?: string; signal?: AbortSignal }): Promise<ClickDispatchDiagnostic | undefined> {
|
|
160
227
|
if (!options.probe || !options.sessionName) return undefined;
|
|
161
228
|
const data = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, stdin: buildClickDispatchProbeCheckScript(options.probe) });
|
|
@@ -164,10 +231,14 @@ export async function collectClickDispatchDiagnostic(options: { cwd: string; pro
|
|
|
164
231
|
const status = typeof result.status === "string" ? result.status : undefined;
|
|
165
232
|
if (status !== "no-native-event-observed") return undefined;
|
|
166
233
|
const nativeEventCount = typeof result.nativeEventCount === "number" ? result.nativeEventCount : 0;
|
|
167
|
-
const
|
|
234
|
+
const scrollContainer = getClickDispatchScrollContainerDiagnostic(result);
|
|
235
|
+
const summary = scrollContainer
|
|
236
|
+
? `Upstream click reported success but no trusted DOM event reached the selected element. ${scrollContainer.summary}`
|
|
237
|
+
: "Upstream click reported success but no trusted DOM event reached the selected element. Gather evidence with snapshot or page-change checks, then retry upstream click or report the workflow issue; the wrapper does not replay clicks in-page.";
|
|
168
238
|
return {
|
|
169
239
|
nativeEventCount,
|
|
170
240
|
reason: "native-click-produced-no-target-dom-event",
|
|
241
|
+
...(scrollContainer ? { scrollContainer } : {}),
|
|
171
242
|
status,
|
|
172
243
|
summary,
|
|
173
244
|
target: redactClickDispatchTarget(options.probe.target),
|
|
@@ -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,122 @@ 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
|
+
const command = step.args[0];
|
|
829
|
+
if (step.status === "completed" && (isOpenNavigationCommand(command) || command === "pushstate")) {
|
|
830
|
+
lastCompletedNavigationIndex = Math.max(lastCompletedNavigationIndex ?? 0, step.index);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
for (const step of progressSteps) {
|
|
834
|
+
if (step.status === "completed") continue;
|
|
835
|
+
if (!retryStep) {
|
|
836
|
+
const retry = getTimeoutStepRetry(step);
|
|
837
|
+
retryStep = {
|
|
838
|
+
...step,
|
|
839
|
+
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."),
|
|
840
|
+
retry,
|
|
841
|
+
status: "failed",
|
|
842
|
+
};
|
|
843
|
+
Object.assign(step, retryStep);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
step.status = "pending";
|
|
847
|
+
step.reason = step.reason ?? `Pending behind timed-out step ${retryStep.index}.`;
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
openedButPostOpenTimedOut: lastCompletedNavigationIndex !== undefined && retryStep !== undefined && retryStep.index > lastCompletedNavigationIndex,
|
|
851
|
+
retryStep,
|
|
852
|
+
steps: progressSteps,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
738
856
|
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,
|
|
857
|
+
const rawSteps = getTimeoutProgressSteps(options.compiledJob, options.command, options.stdin);
|
|
858
|
+
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, rawSteps);
|
|
741
859
|
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
860
|
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
743
861
|
const title = extractStringResultField(titleData, "result") ?? extractStringResultField(titleData, "title");
|
|
744
|
-
const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(
|
|
862
|
+
const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(rawSteps);
|
|
745
863
|
const url = recoveredUrl ?? plannedUrl;
|
|
746
|
-
|
|
864
|
+
const currentPageSource = recoveredUrl ? "live" as const : plannedUrl ? "planned" as const : title ? "live" as const : undefined;
|
|
865
|
+
const stepProgress = buildTimeoutProgressSteps({ artifacts, currentPageSource: recoveredUrl ? "live" : undefined, currentPageUrl: recoveredUrl, steps: rawSteps });
|
|
866
|
+
if (rawSteps.length === 0 && artifacts.length === 0 && !url && !title) return undefined;
|
|
747
867
|
const foundArtifacts = artifacts.filter((artifact) => artifact.exists).length;
|
|
868
|
+
const completedSteps = stepProgress.steps.filter((step) => step.status === "completed").length;
|
|
748
869
|
const pageStateSummary = recoveredUrl || title ? " and current page state" : plannedUrl ? " and planned page URL" : "";
|
|
749
|
-
|
|
870
|
+
const retrySummary = stepProgress.retryStep ? ` Retry step ${stepProgress.retryStep.index} is the first incomplete step.` : "";
|
|
871
|
+
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
872
|
}
|
|
751
873
|
|
|
752
874
|
function redactSensitivePathSegmentsForDiagnostic(path: string): string {
|
|
@@ -775,9 +897,16 @@ export function formatTimeoutPartialProgressText(progress: TimeoutPartialProgres
|
|
|
775
897
|
if (progress.steps && progress.steps.length > 0) {
|
|
776
898
|
const shownSteps = progress.steps.slice(0, 6);
|
|
777
899
|
lines.push("Planned steps:");
|
|
778
|
-
for (const step of shownSteps)
|
|
900
|
+
for (const step of shownSteps) {
|
|
901
|
+
const commandText = redactSensitivePathSegmentsForDiagnostic(redactInvocationArgs(step.args).join(" "));
|
|
902
|
+
const generatedFrom = step.generatedFrom ? `, generated from ${step.generatedFrom}` : "";
|
|
903
|
+
lines.push(`- Step ${step.index} [${step.status}${generatedFrom}]: ${commandText}${step.reason ? ` — ${redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(step.reason))}` : ""}`);
|
|
904
|
+
}
|
|
779
905
|
if (progress.steps.length > shownSteps.length) lines.push(`- ... ${progress.steps.length - shownSteps.length} more step${progress.steps.length - shownSteps.length === 1 ? "" : "s"} omitted`);
|
|
780
906
|
}
|
|
907
|
+
if (progress.retryStep?.retry?.args) {
|
|
908
|
+
lines.push(`Retry failed step: ${JSON.stringify({ args: redactInvocationArgs(progress.retryStep.retry.args) })}`);
|
|
909
|
+
}
|
|
781
910
|
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
911
|
return lines.join("\n");
|
|
783
912
|
}
|
|
@@ -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({
|