diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index 68404da5f527..3bfe49781bf8 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -2125,6 +2125,11 @@ def cleanup_task_resources(agent, task_id: str) -> None: ``terminal.lifetime_seconds`` is exceeded. Non-persistent backends are torn down per-turn as before to prevent resource leakage (the original intent of this hook for the Morph backend, see commit fbd3a2fd). + + Skips ``cleanup_browser`` in headed mode so the browser window stays + visible between turns. The inactivity reaper in + ``browser_tool._cleanup_inactive_browser_sessions`` still handles + idle sessions. """ try: if is_persistent_env(task_id): @@ -2139,7 +2144,14 @@ def cleanup_task_resources(agent, task_id: str) -> None: if agent.verbose_logging: logger.warning(f"Failed to cleanup VM for task {task_id}: {e}") try: - _ra().cleanup_browser(task_id) + if os.environ.get("AGENT_BROWSER_HEADED"): + if agent.verbose_logging: + logging.debug( + f"Skipping per-turn cleanup_browser for headed session {task_id}; " + f"idle reaper will handle it." + ) + else: + _ra().cleanup_browser(task_id) except Exception as e: if agent.verbose_logging: logger.warning(f"Failed to cleanup browser for task {task_id}: {e}") diff --git a/tools/browser_tool.py b/tools/browser_tool.py index 71c60237235e..bcc1455d7831 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -885,6 +885,40 @@ def _get_browser_engine() -> str: return _cached_browser_engine +_cached_headed_mode: Optional[bool] = None +_headed_mode_resolved = False + + +def _is_headed_mode() -> bool: + """Return True when the browser should launch in headed (visible) mode. + + Reads ``config["browser"]["headed"]`` with ``AGENT_BROWSER_HEADED`` env + var as fallback. Result is cached after the first call. + """ + global _cached_headed_mode, _headed_mode_resolved + if _headed_mode_resolved: + return _cached_headed_mode # type: ignore[return-value] + + _headed_mode_resolved = True + _cached_headed_mode = False + + try: + from hermes_cli.config import read_raw_config + cfg = read_raw_config() + val = cfg.get("browser", {}).get("headed") + if val is not None: + _cached_headed_mode = str(val).strip().lower() in ("true", "1", "yes") + except Exception as e: + logger.debug("Could not read browser.headed from config: %s", e) + + if not _cached_headed_mode: + env_val = os.environ.get("AGENT_BROWSER_HEADED", "").strip() + if env_val and env_val.lower() in ("true", "1", "yes"): + _cached_headed_mode = True + + return _cached_headed_mode + + def _should_inject_engine(engine: str) -> bool: """Return True when the engine flag should be added to agent-browser commands. @@ -2347,8 +2381,10 @@ def _run_browser_command( # --session creates a local browser instance and silently ignores --cdp. backend_args = ["--cdp", session_info["cdp_url"]] else: - # Local mode — launch a headless Chromium instance + # Local mode — launch Chromium (headless by default, headed when configured) backend_args = ["--session", session_info["session_name"]] + if _is_headed_mode(): + backend_args.append("--headed") # Lightpanda engine injection (local mode only, agent-browser v0.25.3+). # Use the resolved session backend rather than global cloud-provider state: