fix: align salvaged browser cleanup patch with current main

Resolve the cherry-pick against current browser_tool structure without carrying unrelated formatting churn, while preserving the intended cleanup, PATH, and screenshot recovery changes from PR #1001.
This commit is contained in:
teknium1 2026-03-14 11:34:31 -07:00
parent 895fe5a5d3
commit c1d1699a64

View file

@ -49,7 +49,6 @@ Usage:
browser_click("@e5", task_id="task_123") browser_click("@e5", task_id="task_123")
""" """
from tools.registry import registry
import atexit import atexit
import json import json
import logging import logging
@ -128,8 +127,7 @@ def _socket_safe_tmpdir() -> str:
# Track active sessions per task # Track active sessions per task
# Stores: session_name (always), bb_session_id + cdp_url (cloud mode only) # Stores: session_name (always), bb_session_id + cdp_url (cloud mode only)
# task_id -> {session_name, ...} _active_sessions: Dict[str, Dict[str, str]] = {} # task_id -> {session_name, ...}
_active_sessions: Dict[str, Dict[str, str]] = {}
_recording_sessions: set = set() # task_ids with active recordings _recording_sessions: set = set() # task_ids with active recordings
# Flag to track if cleanup has been done # Flag to track if cleanup has been done
@ -142,8 +140,7 @@ _cleanup_done = False
# Session inactivity timeout (seconds) - cleanup if no activity for this long # Session inactivity timeout (seconds) - cleanup if no activity for this long
# Default: 5 minutes. Needs headroom for LLM reasoning between browser commands, # Default: 5 minutes. Needs headroom for LLM reasoning between browser commands,
# especially when subagents are doing multi-step browser tasks. # especially when subagents are doing multi-step browser tasks.
BROWSER_SESSION_INACTIVITY_TIMEOUT = int( BROWSER_SESSION_INACTIVITY_TIMEOUT = int(os.environ.get("BROWSER_INACTIVITY_TIMEOUT", "300"))
os.environ.get("BROWSER_INACTIVITY_TIMEOUT", "300"))
# Track last activity time per session # Track last activity time per session
_session_last_activity: Dict[str, float] = {} _session_last_activity: Dict[str, float] = {}
@ -214,17 +211,14 @@ def _cleanup_inactive_browser_sessions():
for task_id in sessions_to_cleanup: for task_id in sessions_to_cleanup:
try: try:
elapsed = int( elapsed = int(current_time - _session_last_activity.get(task_id, current_time))
current_time - _session_last_activity.get(task_id, current_time)) logger.info("Cleaning up inactive session for task: %s (inactive for %ss)", task_id, elapsed)
logger.info(
"Cleaning up inactive session for task: %s (inactive for %ss)", task_id, elapsed)
cleanup_browser(task_id) cleanup_browser(task_id)
with _cleanup_lock: with _cleanup_lock:
if task_id in _session_last_activity: if task_id in _session_last_activity:
del _session_last_activity[task_id] del _session_last_activity[task_id]
except Exception as e: except Exception as e:
logger.warning( logger.warning("Error cleaning up inactive session %s: %s", task_id, e)
"Error cleaning up inactive session %s: %s", task_id, e)
def _browser_cleanup_thread_worker(): def _browser_cleanup_thread_worker():
@ -262,8 +256,7 @@ def _start_browser_cleanup_thread():
name="browser-cleanup" name="browser-cleanup"
) )
_cleanup_thread.start() _cleanup_thread.start()
logger.info("Started inactivity cleanup thread (timeout: %ss)", logger.info("Started inactivity cleanup thread (timeout: %ss)", BROWSER_SESSION_INACTIVITY_TIMEOUT)
BROWSER_SESSION_INACTIVITY_TIMEOUT)
def _stop_browser_cleanup_thread(): def _stop_browser_cleanup_thread():
@ -474,14 +467,11 @@ def _create_browserbase_session(task_id: str) -> Dict[str, str]:
# Check for optional settings from environment # Check for optional settings from environment
# Proxies: enabled by default for better CAPTCHA solving # Proxies: enabled by default for better CAPTCHA solving
enable_proxies = os.environ.get( enable_proxies = os.environ.get("BROWSERBASE_PROXIES", "true").lower() != "false"
"BROWSERBASE_PROXIES", "true").lower() != "false"
# Advanced Stealth: requires Scale Plan, disabled by default # Advanced Stealth: requires Scale Plan, disabled by default
enable_advanced_stealth = os.environ.get( enable_advanced_stealth = os.environ.get("BROWSERBASE_ADVANCED_STEALTH", "false").lower() == "true"
"BROWSERBASE_ADVANCED_STEALTH", "false").lower() == "true"
# keepAlive: enabled by default (requires paid plan) - allows reconnection after disconnects # keepAlive: enabled by default (requires paid plan) - allows reconnection after disconnects
enable_keep_alive = os.environ.get( enable_keep_alive = os.environ.get("BROWSERBASE_KEEP_ALIVE", "true").lower() != "false"
"BROWSERBASE_KEEP_ALIVE", "true").lower() != "false"
# Custom session timeout in milliseconds (optional) - extends session beyond project default # Custom session timeout in milliseconds (optional) - extends session beyond project default
custom_timeout_ms = os.environ.get("BROWSERBASE_SESSION_TIMEOUT") custom_timeout_ms = os.environ.get("BROWSERBASE_SESSION_TIMEOUT")
@ -513,8 +503,7 @@ def _create_browserbase_session(task_id: str) -> Dict[str, str]:
if timeout_val > 0: if timeout_val > 0:
session_config["timeout"] = timeout_val session_config["timeout"] = timeout_val
except ValueError: except ValueError:
logger.warning( logger.warning("Invalid BROWSERBASE_SESSION_TIMEOUT value: %s", custom_timeout_ms)
"Invalid BROWSERBASE_SESSION_TIMEOUT value: %s", custom_timeout_ms)
# Enable proxies for better CAPTCHA solving (default: true) # Enable proxies for better CAPTCHA solving (default: true)
# Routes traffic through residential IPs for more reliable access # Routes traffic through residential IPs for more reliable access
@ -579,8 +568,7 @@ def _create_browserbase_session(task_id: str) -> Dict[str, str]:
) )
if not response.ok: if not response.ok:
raise RuntimeError( raise RuntimeError(f"Failed to create Browserbase session: {response.status_code} {response.text}")
f"Failed to create Browserbase session: {response.status_code} {response.text}")
session_data = response.json() session_data = response.json()
session_name = f"hermes_{task_id}_{uuid.uuid4().hex[:8]}" session_name = f"hermes_{task_id}_{uuid.uuid4().hex[:8]}"
@ -597,8 +585,7 @@ def _create_browserbase_session(task_id: str) -> Dict[str, str]:
# Log session info for debugging # Log session info for debugging
feature_str = ", ".join(k for k, v in features_enabled.items() if v) feature_str = ", ".join(k for k, v in features_enabled.items() if v)
logger.info("Created session %s with features: %s", logger.info("Created session %s with features: %s", session_name, feature_str)
session_name, feature_str)
return { return {
"session_name": session_name, "session_name": session_name,
@ -793,8 +780,7 @@ def _run_browser_command(
try: try:
session_info = _get_session_info(task_id) session_info = _get_session_info(task_id)
except Exception as e: except Exception as e:
logger.warning( logger.warning("Failed to create browser session for task=%s: %s", task_id, e)
"Failed to create browser session for task=%s: %s", task_id, e)
return {"success": False, "error": f"Failed to create browser session: {str(e)}"} return {"success": False, "error": f"Failed to create browser session: {str(e)}"}
# Build the command with the appropriate backend flag. # Build the command with the appropriate backend flag.
@ -844,13 +830,6 @@ def _run_browser_command(
browser_env["PATH"] = ":".join(path_parts) browser_env["PATH"] = ":".join(path_parts)
browser_env["AGENT_BROWSER_SOCKET_DIR"] = task_socket_dir browser_env["AGENT_BROWSER_SOCKET_DIR"] = task_socket_dir
node_path = shutil.which("node", path=browser_env["PATH"])
if node_path:
logger.debug("browser subprocess using node at: %s", node_path)
else:
logger.warning("node not found in browser PATH: %s",
browser_env["PATH"])
result = subprocess.run( result = subprocess.run(
cmd_parts, cmd_parts,
capture_output=True, capture_output=True,
@ -862,8 +841,7 @@ def _run_browser_command(
# Log stderr for diagnostics — use warning level on failure so it's visible # Log stderr for diagnostics — use warning level on failure so it's visible
if result.stderr and result.stderr.strip(): if result.stderr and result.stderr.strip():
level = logging.WARNING if result.returncode != 0 else logging.DEBUG level = logging.WARNING if result.returncode != 0 else logging.DEBUG
logger.log(level, "browser '%s' stderr: %s", logger.log(level, "browser '%s' stderr: %s", command, result.stderr.strip()[:500])
command, result.stderr.strip()[:500])
# Log empty output as warning — common sign of broken agent-browser # Log empty output as warning — common sign of broken agent-browser
if not result.stdout.strip() and result.returncode == 0: if not result.stdout.strip() and result.returncode == 0:
@ -877,6 +855,7 @@ def _run_browser_command(
if stdout_text: if stdout_text:
try: try:
parsed = json.loads(stdout_text) parsed = json.loads(stdout_text)
# Warn if snapshot came back empty (common sign of daemon/CDP issues)
if command == "snapshot" and parsed.get("success"): if command == "snapshot" and parsed.get("success"):
snap_data = parsed.get("data", {}) snap_data = parsed.get("data", {})
if not snap_data.get("snapshot") and not snap_data.get("refs"): if not snap_data.get("snapshot") and not snap_data.get("refs"):
@ -894,8 +873,7 @@ def _run_browser_command(
combined_text = "\n".join( combined_text = "\n".join(
part for part in [stdout_text, stderr_text] if part part for part in [stdout_text, stderr_text] if part
) )
recovered_path = _extract_screenshot_path_from_text( recovered_path = _extract_screenshot_path_from_text(combined_text)
combined_text)
if recovered_path and Path(recovered_path).exists(): if recovered_path and Path(recovered_path).exists():
logger.info( logger.info(
@ -917,10 +895,8 @@ def _run_browser_command(
# Check for errors # Check for errors
if result.returncode != 0: if result.returncode != 0:
error_msg = result.stderr.strip( error_msg = result.stderr.strip() if result.stderr else f"Command failed with code {result.returncode}"
) if result.stderr else f"Command failed with code {result.returncode}" logger.warning("browser '%s' failed (rc=%s): %s", command, result.returncode, error_msg[:300])
logger.warning("browser '%s' failed (rc=%s): %s",
command, result.returncode, error_msg[:300])
return {"success": False, "error": error_msg} return {"success": False, "error": error_msg}
return {"success": True, "data": {}} return {"success": True, "data": {}}
@ -1319,10 +1295,8 @@ def browser_console(clear: bool = False, task_id: Optional[str] = None) -> str:
console_args = ["--clear"] if clear else [] console_args = ["--clear"] if clear else []
error_args = ["--clear"] if clear else [] error_args = ["--clear"] if clear else []
console_result = _run_browser_command( console_result = _run_browser_command(effective_task_id, "console", console_args)
effective_task_id, "console", console_args) errors_result = _run_browser_command(effective_task_id, "errors", error_args)
errors_result = _run_browser_command(
effective_task_id, "errors", error_args)
messages = [] messages = []
if console_result.get("success"): if console_result.get("success"):
@ -1355,16 +1329,14 @@ def _maybe_start_recording(task_id: str):
if task_id in _recording_sessions: if task_id in _recording_sessions:
return return
try: try:
hermes_home = Path(os.environ.get( hermes_home = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
"HERMES_HOME", Path.home() / ".hermes"))
config_path = hermes_home / "config.yaml" config_path = hermes_home / "config.yaml"
record_enabled = False record_enabled = False
if config_path.exists(): if config_path.exists():
import yaml import yaml
with open(config_path) as f: with open(config_path) as f:
cfg = yaml.safe_load(f) or {} cfg = yaml.safe_load(f) or {}
record_enabled = cfg.get("browser", {}).get( record_enabled = cfg.get("browser", {}).get("record_sessions", False)
"record_sessions", False)
if not record_enabled: if not record_enabled:
return return
@ -1375,18 +1347,14 @@ def _maybe_start_recording(task_id: str):
import time import time
timestamp = time.strftime("%Y%m%d_%H%M%S") timestamp = time.strftime("%Y%m%d_%H%M%S")
recording_path = recordings_dir / \ recording_path = recordings_dir / f"session_{timestamp}_{task_id[:16]}.webm"
f"session_{timestamp}_{task_id[:16]}.webm"
result = _run_browser_command( result = _run_browser_command(task_id, "record", ["start", str(recording_path)])
task_id, "record", ["start", str(recording_path)])
if result.get("success"): if result.get("success"):
_recording_sessions.add(task_id) _recording_sessions.add(task_id)
logger.info("Auto-recording browser session %s to %s", logger.info("Auto-recording browser session %s to %s", task_id, recording_path)
task_id, recording_path)
else: else:
logger.debug("Could not start auto-recording: %s", logger.debug("Could not start auto-recording: %s", result.get("error"))
result.get("error"))
except Exception as e: except Exception as e:
logger.debug("Auto-recording setup failed: %s", e) logger.debug("Auto-recording setup failed: %s", e)
@ -1399,8 +1367,7 @@ def _maybe_stop_recording(task_id: str):
result = _run_browser_command(task_id, "record", ["stop"]) result = _run_browser_command(task_id, "record", ["stop"])
if result.get("success"): if result.get("success"):
path = result.get("data", {}).get("path", "") path = result.get("data", {}).get("path", "")
logger.info( logger.info("Saved browser recording for session %s: %s", task_id, path)
"Saved browser recording for session %s: %s", task_id, path)
except Exception as e: except Exception as e:
logger.debug("Could not stop recording for %s: %s", task_id, e) logger.debug("Could not stop recording for %s: %s", task_id, e)
finally: finally:
@ -1486,11 +1453,11 @@ def browser_vision(question: str, annotate: bool = False, task_id: Optional[str]
from pathlib import Path from pathlib import Path
effective_task_id = task_id or "default" effective_task_id = task_id or "default"
# Save screenshot to persistent location so it can be shared with users # Save screenshot to persistent location so it can be shared with users
hermes_home = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes")) hermes_home = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
screenshots_dir = hermes_home / "browser_screenshots" screenshots_dir = hermes_home / "browser_screenshots"
screenshot_path = screenshots_dir / \ screenshot_path = screenshots_dir / f"browser_screenshot_{uuid_mod.uuid4().hex}.png"
f"browser_screenshot_{uuid_mod.uuid4().hex}.png"
try: try:
screenshots_dir.mkdir(parents=True, exist_ok=True) screenshots_dir.mkdir(parents=True, exist_ok=True)
@ -1589,8 +1556,7 @@ def browser_vision(question: str, annotate: bool = False, task_id: Optional[str]
# screenshot loses evidence the user might need. The 24-hour cleanup # screenshot loses evidence the user might need. The 24-hour cleanup
# in _cleanup_old_screenshots prevents unbounded disk growth. # in _cleanup_old_screenshots prevents unbounded disk growth.
logger.warning("browser_vision failed: %s", e, exc_info=True) logger.warning("browser_vision failed: %s", e, exc_info=True)
error_info = {"success": False, error_info = {"success": False, "error": f"Error during vision analysis: {str(e)}"}
"error": f"Error during vision analysis: {str(e)}"}
if screenshot_path.exists(): if screenshot_path.exists():
error_info["screenshot_path"] = str(screenshot_path) error_info["screenshot_path"] = str(screenshot_path)
error_info["note"] = "Screenshot was captured but vision analysis failed. You can still share it via MEDIA:<path>." error_info["note"] = "Screenshot was captured but vision analysis failed. You can still share it via MEDIA:<path>."
@ -1625,8 +1591,7 @@ def _cleanup_old_recordings(max_age_hours=72):
"""Remove browser recordings older than max_age_hours to prevent disk bloat.""" """Remove browser recordings older than max_age_hours to prevent disk bloat."""
import time import time
try: try:
hermes_home = Path(os.environ.get( hermes_home = Path(os.environ.get("HERMES_HOME", Path.home() / ".hermes"))
"HERMES_HOME", Path.home() / ".hermes"))
recordings_dir = hermes_home / "browser_recordings" recordings_dir = hermes_home / "browser_recordings"
if not recordings_dir.exists(): if not recordings_dir.exists():
return return
@ -1676,12 +1641,10 @@ def _close_browserbase_session(session_id: str, api_key: str, project_id: str) -
) )
if response.status_code in (200, 201, 204): if response.status_code in (200, 201, 204):
logger.debug( logger.debug("Successfully closed BrowserBase session %s", session_id)
"Successfully closed BrowserBase session %s", session_id)
return True return True
else: else:
logger.warning("Failed to close session %s: HTTP %s - %s", logger.warning("Failed to close session %s: HTTP %s - %s", session_id, response.status_code, response.text[:200])
session_id, response.status_code, response.text[:200])
return False return False
except Exception as e: except Exception as e:
@ -1712,8 +1675,7 @@ def cleanup_browser(task_id: Optional[str] = None) -> None:
if session_info: if session_info:
bb_session_id = session_info.get("bb_session_id", "unknown") bb_session_id = session_info.get("bb_session_id", "unknown")
logger.debug("Found session for task %s: bb_session_id=%s", logger.debug("Found session for task %s: bb_session_id=%s", task_id, bb_session_id)
task_id, bb_session_id)
# Stop auto-recording before closing (saves the file) # Stop auto-recording before closing (saves the file)
_maybe_stop_recording(task_id) _maybe_stop_recording(task_id)
@ -1721,11 +1683,9 @@ def cleanup_browser(task_id: Optional[str] = None) -> None:
# Try to close via agent-browser first (needs session in _active_sessions) # Try to close via agent-browser first (needs session in _active_sessions)
try: try:
_run_browser_command(task_id, "close", [], timeout=10) _run_browser_command(task_id, "close", [], timeout=10)
logger.debug( logger.debug("agent-browser close command completed for task %s", task_id)
"agent-browser close command completed for task %s", task_id)
except Exception as e: except Exception as e:
logger.warning( logger.warning("agent-browser close failed for task %s: %s", task_id, e)
"agent-browser close failed for task %s: %s", task_id, e)
# Now remove from tracking under lock # Now remove from tracking under lock
with _cleanup_lock: with _cleanup_lock:
@ -1736,20 +1696,16 @@ def cleanup_browser(task_id: Optional[str] = None) -> None:
if bb_session_id and not _is_local_mode(): if bb_session_id and not _is_local_mode():
try: try:
config = _get_browserbase_config() config = _get_browserbase_config()
success = _close_browserbase_session( success = _close_browserbase_session(bb_session_id, config["api_key"], config["project_id"])
bb_session_id, config["api_key"], config["project_id"])
if not success: if not success:
logger.warning( logger.warning("Could not close BrowserBase session %s", bb_session_id)
"Could not close BrowserBase session %s", bb_session_id)
except Exception as e: except Exception as e:
logger.error( logger.error("Exception during BrowserBase session close: %s", e)
"Exception during BrowserBase session close: %s", e)
# Kill the daemon process and clean up socket directory # Kill the daemon process and clean up socket directory
session_name = session_info.get("session_name", "") session_name = session_info.get("session_name", "")
if session_name: if session_name:
socket_dir = os.path.join( socket_dir = os.path.join(_socket_safe_tmpdir(), f"agent-browser-{session_name}")
_socket_safe_tmpdir(), f"agent-browser-{session_name}")
if os.path.exists(socket_dir): if os.path.exists(socket_dir):
# agent-browser writes {session}.pid in the socket dir # agent-browser writes {session}.pid in the socket dir
pid_file = os.path.join(socket_dir, f"{session_name}.pid") pid_file = os.path.join(socket_dir, f"{session_name}.pid")
@ -1757,11 +1713,9 @@ def cleanup_browser(task_id: Optional[str] = None) -> None:
try: try:
daemon_pid = int(Path(pid_file).read_text().strip()) daemon_pid = int(Path(pid_file).read_text().strip())
os.kill(daemon_pid, signal.SIGTERM) os.kill(daemon_pid, signal.SIGTERM)
logger.debug("Killed daemon pid %s for %s", logger.debug("Killed daemon pid %s for %s", daemon_pid, session_name)
daemon_pid, session_name)
except (ProcessLookupError, ValueError, PermissionError, OSError): except (ProcessLookupError, ValueError, PermissionError, OSError):
logger.debug( logger.debug("Could not kill daemon pid for %s (already dead or inaccessible)", session_name)
"Could not kill daemon pid for %s (already dead or inaccessible)", session_name)
shutil.rmtree(socket_dir, ignore_errors=True) shutil.rmtree(socket_dir, ignore_errors=True)
logger.debug("Removed task %s from active sessions", task_id) logger.debug("Removed task %s from active sessions", task_id)
@ -1848,8 +1802,7 @@ if __name__ == "__main__":
_find_agent_browser() _find_agent_browser()
except FileNotFoundError: except FileNotFoundError:
print(" - agent-browser CLI not found") print(" - agent-browser CLI not found")
print( print(" Install: npm install -g agent-browser && agent-browser install --with-deps")
" Install: npm install -g agent-browser && agent-browser install --with-deps")
if not _is_local_mode(): if not _is_local_mode():
if not os.environ.get("BROWSERBASE_API_KEY"): if not os.environ.get("BROWSERBASE_API_KEY"):
print(" - BROWSERBASE_API_KEY not set (required for cloud mode)") print(" - BROWSERBASE_API_KEY not set (required for cloud mode)")
@ -1870,6 +1823,7 @@ if __name__ == "__main__":
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Registry # Registry
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
from tools.registry import registry
_BROWSER_SCHEMA_MAP = {s["name"]: s for s in BROWSER_TOOL_SCHEMAS} _BROWSER_SCHEMA_MAP = {s["name"]: s for s in BROWSER_TOOL_SCHEMAS}
@ -1877,8 +1831,7 @@ registry.register(
name="browser_navigate", name="browser_navigate",
toolset="browser", toolset="browser",
schema=_BROWSER_SCHEMA_MAP["browser_navigate"], schema=_BROWSER_SCHEMA_MAP["browser_navigate"],
handler=lambda args, **kw: browser_navigate( handler=lambda args, **kw: browser_navigate(url=args.get("url", ""), task_id=kw.get("task_id")),
url=args.get("url", ""), task_id=kw.get("task_id")),
check_fn=check_browser_requirements, check_fn=check_browser_requirements,
) )
registry.register( registry.register(
@ -1893,8 +1846,7 @@ registry.register(
name="browser_click", name="browser_click",
toolset="browser", toolset="browser",
schema=_BROWSER_SCHEMA_MAP["browser_click"], schema=_BROWSER_SCHEMA_MAP["browser_click"],
handler=lambda args, **kw: browser_click(** handler=lambda args, **kw: browser_click(**args, task_id=kw.get("task_id")),
args, task_id=kw.get("task_id")),
check_fn=check_browser_requirements, check_fn=check_browser_requirements,
) )
registry.register( registry.register(
@ -1908,8 +1860,7 @@ registry.register(
name="browser_scroll", name="browser_scroll",
toolset="browser", toolset="browser",
schema=_BROWSER_SCHEMA_MAP["browser_scroll"], schema=_BROWSER_SCHEMA_MAP["browser_scroll"],
handler=lambda args, **kw: browser_scroll(** handler=lambda args, **kw: browser_scroll(**args, task_id=kw.get("task_id")),
args, task_id=kw.get("task_id")),
check_fn=check_browser_requirements, check_fn=check_browser_requirements,
) )
registry.register( registry.register(
@ -1923,8 +1874,7 @@ registry.register(
name="browser_press", name="browser_press",
toolset="browser", toolset="browser",
schema=_BROWSER_SCHEMA_MAP["browser_press"], schema=_BROWSER_SCHEMA_MAP["browser_press"],
handler=lambda args, **kw: browser_press( handler=lambda args, **kw: browser_press(key=args.get("key", ""), task_id=kw.get("task_id")),
key=args.get("key", ""), task_id=kw.get("task_id")),
check_fn=check_browser_requirements, check_fn=check_browser_requirements,
) )
registry.register( registry.register(
@ -1945,20 +1895,13 @@ registry.register(
name="browser_vision", name="browser_vision",
toolset="browser", toolset="browser",
schema=_BROWSER_SCHEMA_MAP["browser_vision"], schema=_BROWSER_SCHEMA_MAP["browser_vision"],
handler=lambda args, **kw: browser_vision( handler=lambda args, **kw: browser_vision(question=args.get("question", ""), annotate=args.get("annotate", False), task_id=kw.get("task_id")),
question=args.get("question", ""),
annotate=args.get("annotate", False),
task_id=kw.get("task_id"),
),
check_fn=check_browser_requirements, check_fn=check_browser_requirements,
) )
registry.register( registry.register(
name="browser_console", name="browser_console",
toolset="browser", toolset="browser",
schema=_BROWSER_SCHEMA_MAP["browser_console"], schema=_BROWSER_SCHEMA_MAP["browser_console"],
handler=lambda args, **kw: browser_console( handler=lambda args, **kw: browser_console(clear=args.get("clear", False), task_id=kw.get("task_id")),
clear=args.get("clear", False),
task_id=kw.get("task_id"),
),
check_fn=check_browser_requirements, check_fn=check_browser_requirements,
) )