@@ -16,7 +16,11 @@ import {
1616} from "@onkernel/cua-ai" ;
1717import { InternalComputerTranslator , type KernelBrowser } from "./translator/translator" ;
1818import 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
2125export 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+
293352function formatPlaywrightResult ( result : unknown ) : string {
294353 return typeof result === "string" ? result : JSON . stringify ( result ) ;
295354}
0 commit comments