Skip to content

Add verified browser action batches#60

Open
rgarcia wants to merge 18 commits into
mainfrom
hypeship/browser-action-outcomes
Open

Add verified browser action batches#60
rgarcia wants to merge 18 commits into
mainfrom
hypeship/browser-action-outcomes

Conversation

@rgarcia

@rgarcia rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

summary

  • separate complete, generation-fenced browser observations from filtered text presentation, including recursive iframe stitching and persisted ref ownership
  • add browser_act for dependent browser actions with semantic expectations, honest outcomes, stop boundaries, and stable successor diffs
  • keep mixed batches from continuing past browser stop boundaries and use compact reusable schemas for standalone and batched tools

testing

  • npm run typecheck
  • env -u GOOGLE_API_KEY -u GEMINI_API_KEY -u ANTHROPIC_API_KEY -u OPENAI_API_KEY npm test --workspace @onkernel/cua-ai (145 passed)
  • npm test --workspace @onkernel/cua-agent (191 passed, 12 live tests skipped)
  • npm run build --workspace @onkernel/cua-cli
  • git diff --check origin/main

Note

High Risk
Large change to CDP browser execution, ref lifecycle, and iframe handling; incorrect stop/expectation logic could mislead agents or leave stale refs. Extensive new tests mitigate but the surface area is core agent infrastructure.

Overview
Adds browser_act: a dependent list of ref/focus browser steps with optional per-step and final semantic expectations, honest worked / didnt / unknown outcomes, explicit stop reasons (navigation, dialog, stale ref, etc.), and a single successor snapshot plus accessibility-tree diff back to the baseline.

Refactors browser observation into generation-fenced BrowserObservation / presentation layers (browser-observation.ts, runBrowserAct in browser-act.ts), with recursive iframe stitching, navigation epochs, frame-parent tracking, and tighter ref invalidation. Snapshots and find now share this observe path with retries on mid-collection page changes.

Tooling: browser_act is wired through BrowserExecutor, batch execution stops after a browser_act stop boundary, model-facing text is formatted via formatBrowserActResult (capped diff lines), and @onkernel/cua-ai exposes schemas with $defs for nested expectations in unions/batches. Hybrid/browser modes include the new action as act / browser_act.

Reviewed by Cursor Bugbot for commit 0e9edb8. Bugbot is set up for automated code reviews on this repo. Configure here.

@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 5 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 5 issues found in the latest run.

  • ✅ Fixed: Iframe churn false navigation stops
    • Navigation change detection now compares only generations for frames present in both observations, so iframe insertions/removals alone no longer trigger false navigation stops.
  • ✅ Fixed: Main nav leaks frame ancestry
    • Main-frame invalidation now prunes descendant frame ancestry and generation entries while invalidating refs, preventing unbounded frameParents/generations growth across top-level navigations.
  • ✅ Fixed: Missing exported type docs
    • Added TSDoc comments to the exported BrowserAct and related result types to document outcome semantics, stop boundaries, and successor payload contracts.
  • ✅ Fixed: Final expect stop reason lost
    • Final expectation handling now overrides to expectation_failed when needed and only rewrites stopped_at to the final-expectation boundary when that phase actually introduced the stop.
  • ✅ Fixed: Stale iframe session fallback
    • Legacy refs without sessionTargetId now rebind to a frame session only when one exists and otherwise correctly fall back to the page session instead of treating frameId as a required owner target.

Create PR

Or push these changes by commenting:

@cursor push d8684e9c72
Preview (d8684e9c72)
diff --git a/packages/agent/src/translator/browser.ts b/packages/agent/src/translator/browser.ts
--- a/packages/agent/src/translator/browser.ts
+++ b/packages/agent/src/translator/browser.ts
@@ -636,6 +636,7 @@
 			steps.length === action.steps.length &&
 			steps.at(-1)?.evidence.includes("input delivered") === true;
 		if (action.expect && (!stopReason || terminalNavigation)) {
+			const stopReasonBeforeFinalExpectation = stopReason;
 			const before = this.evaluateExpectation(action.expect, baseline, baseline);
 			const deadline = Date.now() + EXPECTATION_TIMEOUT_MS;
 			while (true) {
@@ -673,9 +674,16 @@
 					break;
 				}
 			}
-			if (!stopReason && finalExpectation?.status === "failed") stopReason = "expectation_failed";
-			else if (!stopReason && finalExpectation?.status === "unverifiable") stopReason = "control_flow";
-			if (stopReason) stoppedAt = action.steps.length;
+			if (finalExpectation?.status === "failed") stopReason = "expectation_failed";
+			else if (finalExpectation?.status === "unverifiable") stopReason ??= "control_flow";
+			if (
+				stopReason &&
+				(stopReason !== stopReasonBeforeFinalExpectation ||
+					finalExpectation?.status === "failed" ||
+					finalExpectation?.status === "unverifiable")
+			) {
+				stoppedAt = action.steps.length;
+			}
 		}
 
 		let successor: BrowserActResult["successor"] | undefined;
@@ -1294,13 +1302,18 @@
 	 */
 	private async refSession(entry: RefEntry): Promise<string> {
 		if (!entry.sessionId) {
-			await this.attach(entry.targetId);
-			const sessionTargetId = entry.sessionTargetId ?? entry.frameId;
-			const frameSession = this.frameSessions.get(sessionTargetId);
-			if (sessionTargetId !== entry.targetId && !frameSession) {
+			const pageSession = await this.attach(entry.targetId);
+			if (!entry.sessionTargetId) {
+				const frameSession = this.frameSessions.get(entry.frameId);
+				entry.sessionTargetId = frameSession ? entry.frameId : entry.targetId;
+				entry.sessionId = frameSession ?? pageSession;
+				return entry.sessionId;
+			}
+			const frameSession = this.frameSessions.get(entry.sessionTargetId);
+			if (entry.sessionTargetId !== entry.targetId && !frameSession) {
 				throw new Error("owning frame session is unavailable; the element ref is stale");
 			}
-			entry.sessionId = frameSession ?? (await this.attach(entry.targetId));
+			entry.sessionId = frameSession ?? pageSession;
 		}
 		return entry.sessionId;
 	}
@@ -1320,9 +1333,24 @@
 	}
 
 	private invalidateRefs(targetId: string): void {
+		const descendants = new Set<string>();
+		let found = true;
+		while (found) {
+			found = false;
+			for (const [child, parent] of this.frameParents) {
+				if ((parent === targetId || descendants.has(parent)) && !descendants.has(child)) {
+					descendants.add(child);
+					found = true;
+				}
+			}
+		}
 		this.generations.set(targetId, this.generation(targetId) + 1);
+		for (const frameId of descendants) {
+			this.frameParents.delete(frameId);
+			this.generations.delete(frameId);
+		}
 		for (const [ref, entry] of this.refs) {
-			if (entry.targetId === targetId) this.refs.delete(ref);
+			if (entry.targetId === targetId || descendants.has(entry.frameId)) this.refs.delete(ref);
 		}
 	}
 
@@ -1457,12 +1485,12 @@
 	after: BrowserObservation,
 	live: ReadonlyMap<string, number>,
 ): boolean {
-	const keys = new Set([...before.generations.keys(), ...after.generations.keys()]);
-	return [...keys].some(
-		(key) =>
-			before.generations.get(key) !== after.generations.get(key) ||
-			(after.generations.has(key) && after.generations.get(key) !== (live.get(key) ?? 0)),
-	);
+	for (const [key, generation] of before.generations) {
+		const afterGeneration = after.generations.get(key);
+		if (afterGeneration === undefined) continue;
+		if (generation !== afterGeneration || afterGeneration !== (live.get(key) ?? 0)) return true;
+	}
+	return false;
 }
 
 function browserControlChange(

diff --git a/packages/agent/src/translator/types.ts b/packages/agent/src/translator/types.ts
--- a/packages/agent/src/translator/types.ts
+++ b/packages/agent/src/translator/types.ts
@@ -1,6 +1,9 @@
+/** High-level result of a browser action plan. */
 export type BrowserActOutcome = "worked" | "didnt" | "unknown";
+/** Per-expectation verification status used in step and final expectation evidence. */
 export type BrowserExpectationStatus = "newly_verified" | "preexisting" | "failed" | "unverifiable";
 
+/** Before/after expectation evaluation details captured for diagnostics and tool output. */
 export interface BrowserExpectationEvidence {
 	status: BrowserExpectationStatus;
 	before?: boolean;
@@ -8,6 +11,7 @@
 	details: string[];
 }
 
+/** Execution outcome for one `browser_act` step in the submitted order. */
 export interface BrowserActStepResult {
 	index: number;
 	type: string;
@@ -16,6 +20,7 @@
 	expectation?: BrowserExpectationEvidence;
 }
 
+/** Structured diff between baseline and successor observations. */
 export interface BrowserObservationDiff {
 	changed: boolean;
 	added: string[];
@@ -24,6 +29,7 @@
 	title?: { before: string; after: string };
 }
 
+/** Successor payload when a post-act observation was collected successfully. */
 export interface BrowserActObservedSuccessor {
 	status: "observed";
 	text: string;
@@ -32,13 +38,23 @@
 	diff: BrowserObservationDiff;
 }
 
+/** Successor payload when the post-act observation cannot be collected. */
 export interface BrowserActUnavailableSuccessor {
 	status: "unavailable";
 	error: string;
 }
 
+/** Post-act observation payload describing either an observed or unavailable successor. */
 export type BrowserActSuccessor = BrowserActObservedSuccessor | BrowserActUnavailableSuccessor;
 
+/**
+ * Full `browser_act` result contract returned to callers.
+ *
+ * `stopped_at` is a zero-based step index when execution stops during dispatch,
+ * or `steps.length` when it stops while evaluating the final `expect`.
+ * `stop_reason` names the boundary that prevented continued verification or
+ * dispatch, while `successor` always describes the best-effort post-action state.
+ */
 export interface BrowserActResult {
 	outcome: BrowserActOutcome;
 	steps: BrowserActStepResult[];
@@ -48,6 +64,7 @@
 	successor: BrowserActSuccessor;
 }
 
+/** Normalized read payloads emitted by translator batch execution. */
 export type BatchReadResult =
 	| { type: "screenshot"; data: Buffer; mimeType: string }
 	| { type: "url"; url: string }
@@ -55,6 +72,7 @@
 	| { type: "browser_text"; label: string; text: string }
 	| { type: "browser_act"; result: BrowserActResult };
 
+/** Aggregate read results returned for a batch. */
 export interface BatchExecutionResult {
 	readResults: BatchReadResult[];
 }

You can send follow-ups to the cloud agent here.

Comment thread packages/agent/src/translator/browser.ts Outdated
Comment thread packages/agent/src/translator/browser.ts
Comment thread packages/agent/src/translator/types.ts
Comment thread packages/agent/src/translator/browser.ts Outdated
Comment thread packages/agent/src/translator/browser.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts Outdated
Comment thread packages/agent/src/translator/browser.ts
Comment thread packages/ai/src/actions/browser.ts
Comment thread packages/ai/src/providers/common.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts
Comment thread packages/ai/src/providers/common.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts Outdated
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts Outdated
Comment thread packages/agent/src/tools.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts Outdated
Comment thread packages/agent/src/translator/browser.ts
Comment thread packages/agent/src/translator/browser.ts Outdated
Comment thread packages/agent/src/translator/browser.ts
Comment thread packages/agent/src/tools.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: OOPIF root invalidated twice
    • invalidateSessionTarget now excludes refs whose frameId equals the targetId so the OOPIF root is invalidated only once per navigation.

Create PR

Or push these changes by commenting:

@cursor push 58b34f29dd
Preview (58b34f29dd)
diff --git a/packages/agent/src/translator/browser.ts b/packages/agent/src/translator/browser.ts
--- a/packages/agent/src/translator/browser.ts
+++ b/packages/agent/src/translator/browser.ts
@@ -1517,7 +1517,7 @@
 	private invalidateSessionTarget(targetId: string): void {
 		const ownedFrames = new Set<string>();
 		for (const entry of this.refs.values()) {
-			if (entry.sessionTargetId === targetId) ownedFrames.add(entry.frameId);
+			if (entry.sessionTargetId === targetId && entry.frameId !== targetId) ownedFrames.add(entry.frameId);
 		}
 		this.invalidateFrame(targetId);
 		for (const frameKey of ownedFrames) this.invalidateFrame(frameKey);

You can send follow-ups to the cloud agent here.

Comment thread packages/agent/src/translator/browser.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts Outdated
Comment thread packages/agent/src/translator/browser.ts Outdated
@pulumi

pulumi Bot commented Jul 13, 2026

Copy link
Copy Markdown

⚠️ Pulumi could not deploy preview(s) for this pull request because GitHub reports it is not mergeable (mergeable state: dirty). This usually means the branch has merge conflicts with its base branch. Resolve the conflicts and push a new commit to retry.

@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts Outdated
Comment thread packages/agent/src/translator/browser.ts Outdated
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b7656ef. Configure here.

@rgarcia rgarcia force-pushed the hypeship/browser-action-outcomes branch from b7656ef to 2a507d6 Compare July 13, 2026 17:35
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts Outdated
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts Outdated
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts
Comment thread packages/agent/src/translator/browser.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 469ffbf. Configure here.

@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

Comment thread packages/agent/src/translator/browser.ts Outdated
Comment thread packages/agent/src/translator/browser-observation.ts
Comment thread packages/agent/src/translator/browser-act.ts
@rgarcia

rgarcia commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0e9edb8. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant