Skip to content

Commit ed09120

Browse files
committed
Add verified browser action plans
1 parent 6bba50b commit ed09120

15 files changed

Lines changed: 1952 additions & 53 deletions

‎docs/agent-tool-configuration-spec.md‎

Lines changed: 464 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/agent/src/index.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@ export type { BrowserRefState } from "./translator/browser-ref-lifecycle";
1010
export type {
1111
BatchExecutionResult,
1212
BatchReadResult,
13+
BrowserActExpectationEvidence,
14+
BrowserActExpectationStatus,
15+
BrowserActObservedSuccessor,
16+
BrowserActOutcome,
17+
BrowserActResult,
18+
BrowserActStepResult,
19+
BrowserActStopReason,
20+
BrowserActSuccessor,
21+
BrowserActUnavailableSuccessor,
1322
BrowserExpectationEvidence,
23+
BrowserExpectationState,
24+
BrowserObservationDiff,
25+
BrowserObservationDiffEntry,
1426
BrowserWaitForResult,
1527
BrowserWaitReason,
1628
} from "./translator/types";

‎packages/agent/src/tools.ts‎

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ import {
1616
} from "@onkernel/cua-ai";
1717
import { InternalComputerTranslator, type KernelBrowser } from "./translator/translator";
1818
import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core";
19-
import type { BrowserWaitForResult } from "./translator/types";
19+
import type { BrowserActResult, BrowserWaitForResult } from "./translator/types";
20+
21+
const BROWSER_ACT_DIFF_ENTRY_LIMIT = 200;
22+
const BROWSER_ACT_DIFF_CHAR_LIMIT = 20_000;
23+
const BROWSER_ACT_RESULT_CHAR_LIMIT = 50_000;
2024

2125
export interface ComputerToolOptions {
2226
browser: KernelBrowser;
@@ -41,6 +45,7 @@ export interface BatchDetails {
4145
| { type: "cursor_position"; x: number; y: number }
4246
| { type: "browser_text"; label: string; bytes: number }
4347
| { type: "browser_wait_for"; result: BrowserWaitForResult }
48+
| { type: "browser_act"; result: BrowserActResult }
4449
>;
4550
}
4651

@@ -179,6 +184,9 @@ async function executeBatchTool(
179184
} else if (read.type === "browser_wait_for") {
180185
readResults.push(read);
181186
content.push({ type: "text", text: formatBrowserWaitResult(read.result) });
187+
} else if (read.type === "browser_act") {
188+
readResults.push(read);
189+
content.push({ type: "text", text: formatBrowserActResult(read.result) });
182190
} else {
183191
readResults.push({ type: "screenshot", bytes: read.data.length });
184192
content.push({ type: "image", data: read.data.toString("base64"), mimeType: read.mimeType });
@@ -194,13 +202,18 @@ async function executeBatchTool(
194202
} catch (err) {
195203
throw new Error(`Actions failed: ${errorMessage(err)}`, { cause: err });
196204
}
205+
const acts = readResults.flatMap((read) => read.type === "browser_act" ? [read.result] : []);
197206
const waits = readResults.flatMap((read) => read.type === "browser_wait_for" ? [read.result] : []);
198207
const failedWait = ["interrupted", "timed_out", "unverifiable"].find((status) => waits.some((wait) => wait.status === status));
199-
let statusText = waits.length === 0
200-
? "Actions executed successfully."
208+
let statusText = acts.some((act) => act.outcome === "didnt")
209+
? "Browser action plan did not satisfy its expectations."
201210
: failedWait
202211
? `Browser condition ${failedWait}.`
203-
: "Browser condition satisfied.";
212+
: acts.some((act) => act.outcome === "unknown")
213+
? "Browser action plan outcome is unknown."
214+
: acts.length > 0
215+
? "Browser action plan worked."
216+
: waits.length > 0 ? "Browser condition satisfied." : "Actions executed successfully.";
204217
if (skippedActions) {
205218
const skipped = `${skippedActions} subsequent action${skippedActions === 1 ? " was" : "s were"} skipped.`;
206219
statusText = `${statusText} ${skipped}`;
@@ -290,6 +303,52 @@ function formatBrowserWaitResult(result: BrowserWaitForResult): string {
290303
return [`wait_for: ${result.status}/${result.evidence}${reason} after ${result.elapsed_ms}ms`, ...result.details].join("\n");
291304
}
292305

306+
/** Format model-facing plan feedback with bounded diff entries, diff characters, and total characters. */
307+
export function formatBrowserActResult(result: BrowserActResult): string {
308+
const lines = [`browser_act: ${result.outcome}`];
309+
if (result.stopped_at !== undefined) lines.push(`stopped_at: ${result.stopped_at} (${result.stop_reason ?? "unknown"})`);
310+
for (const step of result.steps) {
311+
lines.push(`step ${step.index} ${step.type}: ${step.outcome} — ${step.diagnostics.join("; ")}`);
312+
for (const diagnostic of step.expectation?.diagnostics ?? []) lines.push(` ${diagnostic}`);
313+
}
314+
if (result.final_expectation) {
315+
lines.push(`final expectation: ${result.final_expectation.status}`);
316+
for (const diagnostic of result.final_expectation.diagnostics) lines.push(` ${diagnostic}`);
317+
}
318+
if (result.successor.status === "unavailable") {
319+
lines.push(`successor unavailable: ${result.successor.error}`);
320+
return boundedBrowserActOutput(lines);
321+
}
322+
const { diff } = result.successor;
323+
const addedCount = diff.added.reduce((total, entry) => total + entry.count, 0);
324+
const removedCount = diff.removed.reduce((total, entry) => total + entry.count, 0);
325+
lines.push(`successor: ${result.successor.title} (${result.successor.url})`);
326+
lines.push(`diff: ${diff.changed ? `+${addedCount} -${removedCount}` : "unchanged"}`);
327+
if (diff.url) lines.push(` url: ${diff.url.before} -> ${diff.url.after}`);
328+
if (diff.title) lines.push(` title: ${diff.title.before} -> ${diff.title.after}`);
329+
const changes = [
330+
...diff.added.map((entry) => ` + ${entry.line}${entry.count === 1 ? "" : ` ×${entry.count}`}`),
331+
...diff.removed.map((entry) => ` - ${entry.line}${entry.count === 1 ? "" : ` ×${entry.count}`}`),
332+
];
333+
let emittedChars = 0;
334+
let emittedEntries = 0;
335+
for (const change of changes) {
336+
if (emittedEntries >= BROWSER_ACT_DIFF_ENTRY_LIMIT || emittedChars + change.length > BROWSER_ACT_DIFF_CHAR_LIMIT) break;
337+
lines.push(change);
338+
emittedEntries += 1;
339+
emittedChars += change.length;
340+
}
341+
if (emittedEntries < changes.length) lines.push(` … ${changes.length - emittedEntries} more diff entries omitted`);
342+
lines.push(result.successor.text);
343+
return boundedBrowserActOutput(lines);
344+
}
345+
346+
function boundedBrowserActOutput(lines: readonly string[]): string {
347+
const output = lines.join("\n");
348+
if (output.length <= BROWSER_ACT_RESULT_CHAR_LIMIT) return output;
349+
return `${output.slice(0, BROWSER_ACT_RESULT_CHAR_LIMIT)}\n… browser_act output truncated at ${BROWSER_ACT_RESULT_CHAR_LIMIT} characters`;
350+
}
351+
293352
function formatPlaywrightResult(result: unknown): string {
294353
return typeof result === "string" ? result : JSON.stringify(result);
295354
}

0 commit comments

Comments
 (0)