11import json
2- from typing import Callable
2+ import time
3+ from typing import Any , Callable
34from computers .kernel_computer import KernelComputer
45from utils import (
56 create_response ,
@@ -80,13 +81,15 @@ def __init__(
8081 self .print_steps = True
8182 self .debug = False
8283 self .show_images = False
84+ self .on_event : Callable [[dict ], None ] | None = None
85+ self ._model_request_started_at : float | None = None
8386 self .acknowledge_safety_check_callback = acknowledge_safety_check_callback
8487
8588 if computer :
8689 dimensions = computer .get_dimensions ()
8790 self .tools += [
8891 {
89- "type" : "computer-preview " ,
92+ "type" : "computer_use_preview " ,
9093 "display_width" : dimensions [0 ],
9194 "display_height" : dimensions [1 ],
9295 "environment" : computer .get_environment (),
@@ -126,6 +129,86 @@ def debug_print(self, *args):
126129 if self .debug :
127130 pp (* args )
128131
132+ def _emit_event (self , event : str , data : dict | None = None ) -> None :
133+ if self .print_steps and self .on_event :
134+ self .on_event ({"event" : event , "data" : data or {}})
135+
136+ def _current_model_elapsed_ms (self ) -> int | None :
137+ if self ._model_request_started_at is None :
138+ return None
139+ return int ((time .time () - self ._model_request_started_at ) * 1000 )
140+
141+ def _extract_reasoning_text (self , item : dict [str , Any ]) -> str :
142+ summary = item .get ("summary" )
143+ if not isinstance (summary , list ):
144+ return ""
145+ pieces : list [str ] = []
146+ for part in summary :
147+ if not isinstance (part , dict ):
148+ continue
149+ text = part .get ("text" )
150+ if isinstance (text , str ) and text :
151+ pieces .append (text )
152+ return " " .join (pieces ).strip ()
153+
154+ def _extract_prompt_text (self , item : dict [str , Any ]) -> str | None :
155+ if item .get ("role" ) != "user" :
156+ return None
157+ content = item .get ("content" )
158+ if isinstance (content , str ):
159+ return content
160+ if not isinstance (content , list ):
161+ return None
162+ parts : list [str ] = []
163+ for entry in content :
164+ if not isinstance (entry , dict ):
165+ continue
166+ text = entry .get ("text" )
167+ if isinstance (text , str ) and text :
168+ parts .append (text )
169+ return " " .join (parts ) if parts else None
170+
171+ def _describe_action (self , action_type : str , action_args : dict [str , Any ]) -> str :
172+ if action_type == "click" :
173+ x = int (action_args .get ("x" , 0 ))
174+ y = int (action_args .get ("y" , 0 ))
175+ button = action_args .get ("button" , "left" )
176+ if button in ("" , "left" ):
177+ return f"click({ x } , { y } )"
178+ return f"click({ x } , { y } , { button } )"
179+ if action_type == "double_click" :
180+ return f"double_click({ int (action_args .get ('x' , 0 ))} , { int (action_args .get ('y' , 0 ))} )"
181+ if action_type == "type" :
182+ text = str (action_args .get ("text" , "" ))
183+ if len (text ) > 60 :
184+ text = f"{ text [:57 ]} ..."
185+ return f"type({ text !r} )"
186+ if action_type == "keypress" :
187+ keys = action_args .get ("keys" , [])
188+ return f"keypress({ keys } )"
189+ if action_type == "scroll" :
190+ return (
191+ f"scroll({ int (action_args .get ('x' , 0 ))} , { int (action_args .get ('y' , 0 ))} , "
192+ f"dx={ int (action_args .get ('scroll_x' , 0 ))} , dy={ int (action_args .get ('scroll_y' , 0 ))} )"
193+ )
194+ if action_type == "move" :
195+ return f"move({ int (action_args .get ('x' , 0 ))} , { int (action_args .get ('y' , 0 ))} )"
196+ if action_type == "drag" :
197+ return "drag(...)"
198+ if action_type == "wait" :
199+ return f"wait({ int (action_args .get ('ms' , 1000 ))} ms)"
200+ if action_type == "screenshot" :
201+ return "screenshot()"
202+ return action_type
203+
204+ def _describe_batch_actions (self , actions : list [dict [str , Any ]]) -> str :
205+ pieces : list [str ] = []
206+ for action in actions :
207+ action_type = str (action .get ("type" , "unknown" ))
208+ action_args = {k : v for k , v in action .items () if k != "type" }
209+ pieces .append (self ._describe_action (action_type , action_args ))
210+ return "batch[" + " -> " .join (pieces ) + "]"
211+
129212 def _execute_computer_action (self , action_type , action_args ):
130213 if action_type == "click" :
131214 self .computer .click (** action_args )
@@ -150,14 +233,50 @@ def _execute_computer_action(self, action_type, action_args):
150233
151234 def handle_item (self , item ):
152235 """Handle each item; may cause a computer action + screenshot."""
236+ if item ["type" ] == "reasoning" :
237+ text = self ._extract_reasoning_text (item )
238+ if text :
239+ self ._emit_event ("reasoning_delta" , {"text" : text })
240+
153241 if item ["type" ] == "message" :
154- if self .print_steps :
155- print (item ["content" ][0 ]["text" ])
242+ if item .get ("role" ) == "assistant" :
243+ content = item .get ("content" , [])
244+ if isinstance (content , list ):
245+ for part in content :
246+ if isinstance (part , dict ) and isinstance (part .get ("text" ), str ):
247+ self ._emit_event ("text_delta" , {"text" : part ["text" ]})
248+ self ._emit_event ("text_done" , {})
156249
157250 if item ["type" ] == "function_call" :
158251 name , args = item ["name" ], json .loads (item ["arguments" ])
159- if self .print_steps :
160- print (f"{ name } ({ args } )" )
252+ elapsed_ms = self ._current_model_elapsed_ms ()
253+ if name == BATCH_FUNC_NAME :
254+ actions = args .get ("actions" , [])
255+ if isinstance (actions , list ):
256+ typed_actions = [a for a in actions if isinstance (a , dict )]
257+ payload = {
258+ "action_type" : "batch" ,
259+ "description" : self ._describe_batch_actions (typed_actions ),
260+ "action" : {"type" : "batch" , "actions" : typed_actions },
261+ }
262+ if elapsed_ms is not None :
263+ payload ["elapsed_ms" ] = elapsed_ms
264+ self ._emit_event (
265+ "action" ,
266+ payload ,
267+ )
268+ else :
269+ payload = {
270+ "action_type" : name ,
271+ "description" : f"{ name } ({ json .dumps (args )} )" ,
272+ "action" : args ,
273+ }
274+ if elapsed_ms is not None :
275+ payload ["elapsed_ms" ] = elapsed_ms
276+ self ._emit_event (
277+ "action" ,
278+ payload ,
279+ )
161280
162281 if name == BATCH_FUNC_NAME :
163282 return self ._handle_batch_call (item ["call_id" ], args )
@@ -177,12 +296,26 @@ def handle_item(self, item):
177296 action = item ["action" ]
178297 action_type = action ["type" ]
179298 action_args = {k : v for k , v in action .items () if k != "type" }
180- if self .print_steps :
181- print (f"{ action_type } ({ action_args } )" )
299+ elapsed_ms = self ._current_model_elapsed_ms ()
300+ payload = {
301+ "action_type" : action_type ,
302+ "description" : self ._describe_action (action_type , action_args ),
303+ "action" : action ,
304+ }
305+ if elapsed_ms is not None :
306+ payload ["elapsed_ms" ] = elapsed_ms
307+ self ._emit_event (
308+ "action" ,
309+ payload ,
310+ )
182311
183312 self ._execute_computer_action (action_type , action_args )
184313
185314 screenshot_base64 = self .computer .screenshot ()
315+ self ._emit_event (
316+ "screenshot" ,
317+ {"captured" : True , "bytes_base64" : len (screenshot_base64 )},
318+ )
186319 if self .show_images :
187320 show_image (screenshot_base64 )
188321
@@ -228,31 +361,55 @@ def _handle_batch_call(self, call_id, args):
228361 ]
229362
230363 def run_full_turn (
231- self , input_items , print_steps = True , debug = False , show_images = False
364+ self ,
365+ input_items ,
366+ print_steps = True ,
367+ debug = False ,
368+ show_images = False ,
369+ on_event : Callable [[dict ], None ] | None = None ,
232370 ):
233371 self .print_steps = print_steps
234372 self .debug = debug
235373 self .show_images = show_images
374+ self .on_event = on_event
236375 new_items = []
376+ turns = 0
237377
238- while new_items [- 1 ].get ("role" ) != "assistant" if new_items else True :
239- self .debug_print ([sanitize_message (msg ) for msg in input_items + new_items ])
378+ for message in input_items :
379+ if isinstance (message , dict ):
380+ prompt = self ._extract_prompt_text (message )
381+ if prompt :
382+ self ._emit_event ("prompt" , {"text" : prompt })
240383
241- response = create_response (
242- model = self .model ,
243- input = input_items + new_items ,
244- tools = self .tools ,
245- truncation = "auto" ,
246- instructions = BATCH_INSTRUCTIONS ,
247- )
248- self .debug_print (response )
384+ try :
385+ while new_items [- 1 ].get ("role" ) != "assistant" if new_items else True :
386+ turns += 1
387+ self .debug_print ([sanitize_message (msg ) for msg in input_items + new_items ])
388+
389+ self ._model_request_started_at = time .time ()
390+ response = create_response (
391+ model = self .model ,
392+ input = input_items + new_items ,
393+ tools = self .tools ,
394+ truncation = "auto" ,
395+ instructions = BATCH_INSTRUCTIONS ,
396+ )
397+ self .debug_print (response )
398+
399+ if "output" not in response :
400+ if self .debug :
401+ print (response )
402+ raise ValueError ("No output from model" )
249403
250- if "output" not in response and self .debug :
251- print (response )
252- raise ValueError ("No output from model" )
253- else :
254404 new_items += response ["output" ]
255405 for item in response ["output" ]:
256406 new_items += self .handle_item (item )
407+ self ._model_request_started_at = None
408+ self ._emit_event ("turn_done" , {"turn" : turns })
409+ except Exception as exc :
410+ self ._model_request_started_at = None
411+ self ._emit_event ("error" , {"message" : str (exc )})
412+ raise
257413
414+ self ._emit_event ("run_complete" , {"turns" : turns })
258415 return new_items
0 commit comments