@@ -112,9 +112,10 @@ async def _auto_install_chromium() -> bool:
112112class BrowserCapability (AbstractCapability [Any ]):
113113 """Provides a real async Playwright browser to the agent.
114114
115- Manages the full browser lifecycle: Chromium is launched before the agent
116- run starts (via ``wrap_run``) and closed in a ``finally`` block, guaranteeing
117- cleanup on both success and failure paths.
115+ Manages the full browser lifecycle: Playwright and Chromium are started
116+ lazily on the first browser-tool call and closed in a ``finally`` block,
117+ guaranteeing cleanup on both success and failure paths. Runs that never
118+ invoke a browser tool incur zero Playwright overhead.
118119
119120 Requires the ``browser`` optional extra::
120121
@@ -228,10 +229,38 @@ async def prepare_tools(
228229 result .append (td )
229230 return result
230231
231- def _make_launcher (self , pw : Any ) -> Any :
232- """Return an async callable that lazily launches Chromium on first tool call."""
232+ async def wrap_run (
233+ self ,
234+ ctx : RunContext [Any ],
235+ * ,
236+ handler : WrapRunHandler ,
237+ ) -> AgentRunResult [Any ]:
238+ """Install a lazy browser launcher and clean up after the run.
239+
240+ Both Playwright and Chromium are started only when the first browser
241+ tool is actually called. Runs that never use the browser incur zero
242+ Playwright overhead — no subprocess is spawned, no browser window
243+ appears.
244+
245+ A ``finally`` block guarantees cleanup of the browser and the
246+ Playwright driver whether the run succeeds, raises, or is cancelled.
247+
248+ If Chromium is not installed and ``auto_install`` is ``True`` (the
249+ default), ``playwright install chromium`` is run automatically on the
250+ first tool call, and the launch is retried once.
251+ """
252+ _require_browser ()
253+ assert async_playwright is not None # guaranteed by _require_browser()
254+ _start_playwright = async_playwright # local non-None reference for the closure
255+
256+ _pw_ctx : Any = None # Playwright context manager — entered lazily
233257
234258 async def _launch () -> None :
259+ nonlocal _pw_ctx
260+ _pw_ctx = _start_playwright ()
261+ pw = await _pw_ctx .__aenter__ ()
262+ self ._state .playwright_instance = pw
263+
235264 browser = None
236265 try :
237266 browser = await pw .chromium .launch (headless = self .headless )
@@ -278,44 +307,19 @@ def _on_popup(popup: Any) -> None:
278307 self ._state .browser = browser
279308 self ._state .page = page
280309
281- return _launch
282-
283- async def wrap_run (
284- self ,
285- ctx : RunContext [Any ],
286- * ,
287- handler : WrapRunHandler ,
288- ) -> AgentRunResult [Any ]:
289- """Set up Playwright context and install a lazy browser launcher.
290-
291- Chromium is **not** launched here. Instead a launcher callable is
292- stored on ``_state._lazy_launcher`` and is invoked by
293- ``BrowserToolset._ensure_page()`` only when a browser tool is first
294- called. Runs that never use the browser incur zero Playwright
295- overhead.
296-
297- A ``finally`` block cleans up all state and closes the browser (if it
298- was ever launched) whether the run succeeds, raises, or is cancelled.
299-
300- If Chromium is not installed and ``auto_install`` is ``True`` (the
301- default), ``playwright install chromium`` is run automatically on the
302- first tool call, and the launch is retried once.
303- """
304- _require_browser ()
305- assert async_playwright is not None # guaranteed by _require_browser()
306- async with async_playwright () as pw :
307- self ._state .playwright_instance = pw
308- self ._state ._lazy_launcher = self ._make_launcher (pw )
309- try :
310- return await handler ()
311- finally :
312- self ._state ._lazy_launcher = None
313- self ._state .playwright_instance = None
314- self ._state .launch_error = None
315- if self ._state .browser is not None :
316- browser = self ._state .browser
317- self ._state .page = None
318- self ._state .browser = None
319- await browser .close ()
320- else :
321- self ._state .page = None
310+ self ._state ._lazy_launcher = _launch
311+ try :
312+ return await handler ()
313+ finally :
314+ self ._state ._lazy_launcher = None
315+ self ._state .playwright_instance = None
316+ self ._state .launch_error = None
317+ if self ._state .browser is not None :
318+ browser = self ._state .browser
319+ self ._state .page = None
320+ self ._state .browser = None
321+ await browser .close ()
322+ else :
323+ self ._state .page = None
324+ if _pw_ctx is not None :
325+ await _pw_ctx .__aexit__ (None , None , None )
0 commit comments