Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion acp_adapter/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"browser_scroll": "execute",
"browser_press": "execute",
"browser_back": "execute",
"browser_close": "execute",
"browser_get_images": "read",
# Agent internals
"delegate_task": "execute",
Expand Down
2 changes: 0 additions & 2 deletions agent/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -890,8 +890,6 @@ def _wrap(line: str) -> str:
return _wrap(f"┊ ◀️ back {dur}")
if tool_name == "browser_press":
return _wrap(f"┊ ⌨️ press {args.get('key', '?')} {dur}")
if tool_name == "browser_close":
return _wrap(f"┊ 🚪 close browser {dur}")
if tool_name == "browser_get_images":
return _wrap(f"┊ 🖼️ images extracting {dur}")
if tool_name == "browser_vision":
Expand Down
1 change: 0 additions & 1 deletion agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,6 @@ def build_nous_subscription_prompt(valid_tool_names: "set[str] | None" = None) -
"browser_type",
"browser_scroll",
"browser_console",
"browser_close",
"browser_press",
"browser_get_images",
"browser_vision",
Expand Down
2 changes: 1 addition & 1 deletion cli-config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ platform_toolsets:
# terminal - terminal, process
# file - read_file, write_file, patch, search
# browser - browser_navigate, browser_snapshot, browser_click, browser_type,
# browser_scroll, browser_back, browser_press, browser_close,
# browser_scroll, browser_back, browser_press,
# browser_get_images, browser_vision (requires BROWSERBASE_API_KEY)
# vision - vision_analyze (requires OPENROUTER_API_KEY)
# image_gen - image_generate (requires FAL_KEY)
Expand Down
2 changes: 1 addition & 1 deletion model_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def _discover_tools():
"browser_tools": [
"browser_navigate", "browser_snapshot", "browser_click",
"browser_type", "browser_scroll", "browser_back",
"browser_press", "browser_close", "browser_get_images",
"browser_press", "browser_get_images",
"browser_vision", "browser_console"
],
"cronjob_tools": ["cronjob"],
Expand Down
3 changes: 1 addition & 2 deletions skills/dogfood/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This skill guides you through systematic exploratory QA testing of web applicati

## Prerequisites

- Browser toolset must be available (`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_vision`, `browser_console`, `browser_scroll`, `browser_back`, `browser_press`, `browser_close`)
- Browser toolset must be available (`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_vision`, `browser_console`, `browser_scroll`, `browser_back`, `browser_press`)
- A target URL and testing scope from the user

## Inputs
Expand Down Expand Up @@ -148,7 +148,6 @@ Save the report to `{output_dir}/report.md`.
| `browser_press` | Press a keyboard key |
| `browser_vision` | Screenshot + AI analysis; use `annotate=true` for element labels |
| `browser_console` | Get JS console output and errors |
| `browser_close` | Close the browser session |

## Tips

Expand Down
2 changes: 1 addition & 1 deletion tests/gateway/test_api_server_toolset.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_toolset_includes_browser_tools(self):
tools = resolve_toolset("hermes-api-server")
for tool in ["browser_navigate", "browser_snapshot", "browser_click",
"browser_type", "browser_scroll", "browser_back",
"browser_press", "browser_close"]:
"browser_press"]:
assert tool in tools, f"Missing browser tool: {tool}"

def test_toolset_includes_homeassistant_tools(self):
Expand Down
12 changes: 0 additions & 12 deletions tests/tools/test_browser_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,18 +65,6 @@ def test_cleanup_browser_clears_tracking_state(self):
mock_stop.assert_called_once_with("task-1")
mock_run.assert_called_once_with("task-1", "close", [], timeout=10)

def test_browser_close_delegates_to_cleanup_browser(self):
import json

browser_tool = self.browser_tool
browser_tool._active_sessions["task-2"] = {"session_name": "sess-2"}

with patch("tools.browser_tool.cleanup_browser") as mock_cleanup:
result = json.loads(browser_tool.browser_close("task-2"))

assert result == {"success": True, "closed": True}
mock_cleanup.assert_called_once_with("task-2")

def test_emergency_cleanup_clears_all_tracking_state(self):
browser_tool = self.browser_tool
browser_tool._cleanup_done = False
Expand Down
19 changes: 19 additions & 0 deletions tools/browser_camofox.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,25 @@ def camofox_navigate(url: str, task_id: Optional[str] = None) -> str:
"Browser is visible via VNC. "
"Share this link with the user so they can watch the browser live."
)

# Auto-take a compact snapshot so the model can act immediately
try:
snap_data = _get(
f"/tabs/{session['tab_id']}/snapshot",
params={"userId": session["user_id"]},
)
snapshot_text = snap_data.get("snapshot", "")
from tools.browser_tool import (
SNAPSHOT_SUMMARIZE_THRESHOLD,
_truncate_snapshot,
)
if len(snapshot_text) > SNAPSHOT_SUMMARIZE_THRESHOLD:
snapshot_text = _truncate_snapshot(snapshot_text)
result["snapshot"] = snapshot_text
result["element_count"] = snap_data.get("refsCount", 0)
except Exception:
pass # Navigation succeeded; snapshot is a bonus

return json.dumps(result)
except requests.HTTPError as e:
return json.dumps({"success": False, "error": f"Navigation failed: {e}"})
Expand Down
123 changes: 57 additions & 66 deletions tools/browser_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ def _update_session_activity(task_id: str):
BROWSER_TOOL_SCHEMAS = [
{
"name": "browser_navigate",
"description": "Navigate to a URL in the browser. Initializes the session and loads the page. Must be called before other browser tools. For simple information retrieval, prefer web_search or web_extract (faster, cheaper). Use browser tools when you need to interact with a page (click, fill forms, dynamic content).",
"description": "Navigate to a URL in the browser. Initializes the session and loads the page. Must be called before other browser tools. For simple information retrieval, prefer web_search or web_extract (faster, cheaper). Use browser tools when you need to interact with a page (click, fill forms, dynamic content). Returns a compact page snapshot with interactive elements and ref IDs — no need to call browser_snapshot separately after navigating.",
"parameters": {
"type": "object",
"properties": {
Expand All @@ -532,7 +532,7 @@ def _update_session_activity(task_id: str):
},
{
"name": "browser_snapshot",
"description": "Get a text-based snapshot of the current page's accessibility tree. Returns interactive elements with ref IDs (like @e1, @e2) for browser_click and browser_type. full=false (default): compact view with interactive elements. full=true: complete page content. Snapshots over 8000 chars are truncated or LLM-summarized. Requires browser_navigate first.",
"description": "Get a text-based snapshot of the current page's accessibility tree. Returns interactive elements with ref IDs (like @e1, @e2) for browser_click and browser_type. full=false (default): compact view with interactive elements. full=true: complete page content. Snapshots over 8000 chars are truncated or LLM-summarized. Requires browser_navigate first. Note: browser_navigate already returns a compact snapshot — use this to refresh after interactions that change the page, or with full=true for complete content.",
"parameters": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -615,15 +615,7 @@ def _update_session_activity(task_id: str):
"required": ["key"]
}
},
{
"name": "browser_close",
"description": "Close the browser session and release resources. Call this when done with browser tasks to free up Browserbase session quota.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
},

{
"name": "browser_get_images",
"description": "Get a list of all images on the current page with their URLs and alt text. Useful for finding images to analyze with the vision tool. Requires browser_navigate to be called first.",
Expand Down Expand Up @@ -1229,7 +1221,22 @@ def browser_navigate(url: str, task_id: Optional[str] = None) -> str:
"Consider upgrading Browserbase plan for proxy support."
)
response["stealth_features"] = active_features


# Auto-take a compact snapshot so the model can act immediately
# without a separate browser_snapshot call.
try:
snap_result = _run_browser_command(effective_task_id, "snapshot", ["-c"])
if snap_result.get("success"):
snap_data = snap_result.get("data", {})
snapshot_text = snap_data.get("snapshot", "")
refs = snap_data.get("refs", {})
if len(snapshot_text) > SNAPSHOT_SUMMARIZE_THRESHOLD:
snapshot_text = _truncate_snapshot(snapshot_text)
response["snapshot"] = snapshot_text
response["element_count"] = len(refs) if refs else 0
except Exception as e:
logger.debug("Auto-snapshot after navigate failed: %s", e)

return json.dumps(response, ensure_ascii=False)
else:
return json.dumps({
Expand Down Expand Up @@ -1376,31 +1383,40 @@ def browser_scroll(direction: str, task_id: Optional[str] = None) -> str:
Returns:
JSON string with scroll result
"""
if _is_camofox_mode():
from tools.browser_camofox import camofox_scroll
return camofox_scroll(direction, task_id)

effective_task_id = task_id or "default"

# Validate direction
if direction not in ["up", "down"]:
return json.dumps({
"success": False,
"error": f"Invalid direction '{direction}'. Use 'up' or 'down'."
}, ensure_ascii=False)

result = _run_browser_command(effective_task_id, "scroll", [direction])

if result.get("success"):
return json.dumps({
"success": True,
"scrolled": direction
}, ensure_ascii=False)
else:
return json.dumps({
"success": False,
"error": result.get("error", f"Failed to scroll {direction}")
}, ensure_ascii=False)

# Repeat the scroll 5 times to get meaningful page movement.
# Most backends scroll ~100px per call, which is barely visible.
# 5x gives roughly half a viewport of travel, backend-agnostic.
_SCROLL_REPEATS = 5

if _is_camofox_mode():
from tools.browser_camofox import camofox_scroll
result = None
for _ in range(_SCROLL_REPEATS):
result = camofox_scroll(direction, task_id)
return result

effective_task_id = task_id or "default"

result = None
for _ in range(_SCROLL_REPEATS):
result = _run_browser_command(effective_task_id, "scroll", [direction])
if not result.get("success"):
return json.dumps({
"success": False,
"error": result.get("error", f"Failed to scroll {direction}")
}, ensure_ascii=False)

return json.dumps({
"success": True,
"scrolled": direction
}, ensure_ascii=False)


def browser_back(task_id: Optional[str] = None) -> str:
Expand Down Expand Up @@ -1463,33 +1479,7 @@ def browser_press(key: str, task_id: Optional[str] = None) -> str:
}, ensure_ascii=False)


def browser_close(task_id: Optional[str] = None) -> str:
"""
Close the browser session.

Args:
task_id: Task identifier for session isolation

Returns:
JSON string with close result
"""
if _is_camofox_mode():
from tools.browser_camofox import camofox_close
return camofox_close(task_id)

effective_task_id = task_id or "default"
with _cleanup_lock:
had_session = effective_task_id in _active_sessions

cleanup_browser(effective_task_id)

response = {
"success": True,
"closed": True,
}
if not had_session:
response["warning"] = "Session may not have been active"
return json.dumps(response, ensure_ascii=False)


def browser_console(clear: bool = False, expression: Optional[str] = None, task_id: Optional[str] = None) -> str:
Expand Down Expand Up @@ -1942,14 +1932,22 @@ def cleanup_browser(task_id: Optional[str] = None) -> None:
Clean up browser session for a task.

Called automatically when a task completes or when inactivity timeout is reached.
Closes both the agent-browser session and the Browserbase session.
Closes both the agent-browser/Browserbase session and Camofox sessions.

Args:
task_id: Task identifier to clean up
"""
if task_id is None:
task_id = "default"

# Also clean up Camofox session if running in Camofox mode
if _is_camofox_mode():
try:
from tools.browser_camofox import camofox_close
camofox_close(task_id)
except Exception as e:
logger.debug("Camofox cleanup for task %s: %s", task_id, e)

logger.debug("cleanup_browser called for task_id: %s", task_id)
logger.debug("Active sessions: %s", list(_active_sessions.keys()))

Expand Down Expand Up @@ -2168,14 +2166,7 @@ def check_browser_requirements() -> bool:
check_fn=check_browser_requirements,
emoji="⌨️",
)
registry.register(
name="browser_close",
toolset="browser",
schema=_BROWSER_SCHEMA_MAP["browser_close"],
handler=lambda args, **kw: browser_close(task_id=kw.get("task_id")),
check_fn=check_browser_requirements,
emoji="🚪",
)

registry.register(
name="browser_get_images",
toolset="browser",
Expand Down
Loading
Loading