feat(browser): add JS evaluation via browser_console expression parameter (#5303)

Add optional 'expression' parameter to browser_console that evaluates
JavaScript in the page context (like DevTools console). Returns structured
results with auto-JSON parsing.

No new tool — extends the existing browser_console schema with ~20 tokens
of overhead instead of adding a 12th browser tool.

Both backends supported:
- Browserbase: uses agent-browser 'eval' command via CDP
- Camofox: uses /tabs/{tab_id}/eval endpoint with graceful degradation

E2E verified: string eval, number eval, structured JSON, DOM manipulation,
error handling, and original console-output mode all working.
This commit is contained in:
Teknium
2026-04-05 12:42:52 -07:00
committed by GitHub
parent 8d5226753f
commit b63fb03f3f

View File

@@ -652,7 +652,7 @@ BROWSER_TOOL_SCHEMAS = [
},
{
"name": "browser_console",
"description": "Get browser console output and JavaScript errors from the current page. Returns console.log/warn/error/info messages and uncaught JS exceptions. Use this to detect silent JavaScript errors, failed API calls, and application warnings. Requires browser_navigate to be called first.",
"description": "Get browser console output and JavaScript errors from the current page. Returns console.log/warn/error/info messages and uncaught JS exceptions. Use this to detect silent JavaScript errors, failed API calls, and application warnings. Requires browser_navigate to be called first. When 'expression' is provided, evaluates JavaScript in the page context and returns the result — use this for DOM inspection, reading page state, or extracting data programmatically.",
"parameters": {
"type": "object",
"properties": {
@@ -660,6 +660,10 @@ BROWSER_TOOL_SCHEMAS = [
"type": "boolean",
"default": False,
"description": "If true, clear the message buffers after reading"
},
"expression": {
"type": "string",
"description": "JavaScript expression to evaluate in the page context. Runs in the browser like DevTools console — full access to DOM, window, document. Return values are serialized to JSON. Example: 'document.title' or 'document.querySelectorAll(\"a\").length'"
}
},
"required": []
@@ -1486,19 +1490,26 @@ def browser_close(task_id: Optional[str] = None) -> str:
return json.dumps(response, ensure_ascii=False)
def browser_console(clear: bool = False, task_id: Optional[str] = None) -> str:
"""Get browser console messages and JavaScript errors.
def browser_console(clear: bool = False, expression: Optional[str] = None, task_id: Optional[str] = None) -> str:
"""Get browser console messages and JavaScript errors, or evaluate JS in the page.
Returns both console output (log/warn/error/info from the page's JS)
and uncaught exceptions (crashes, unhandled promise rejections).
When ``expression`` is provided, evaluates JavaScript in the page context
(like the DevTools console) and returns the result. Otherwise returns
console output (log/warn/error/info) and uncaught exceptions.
Args:
clear: If True, clear the message/error buffers after reading
expression: JavaScript expression to evaluate in the page context
task_id: Task identifier for session isolation
Returns:
JSON string with console messages and JS errors
JSON string with console messages/errors, or eval result
"""
# --- JS evaluation mode ---
if expression is not None:
return _browser_eval(expression, task_id)
# --- Console output mode (original behaviour) ---
if _is_camofox_mode():
from tools.browser_camofox import camofox_console
return camofox_console(clear, task_id)
@@ -1537,6 +1548,80 @@ def browser_console(clear: bool = False, task_id: Optional[str] = None) -> str:
}, ensure_ascii=False)
def _browser_eval(expression: str, task_id: Optional[str] = None) -> str:
"""Evaluate a JavaScript expression in the page context and return the result."""
if _is_camofox_mode():
return _camofox_eval(expression, task_id)
effective_task_id = task_id or "default"
result = _run_browser_command(effective_task_id, "eval", [expression])
if not result.get("success"):
err = result.get("error", "eval failed")
# Detect backend capability gaps and give the model a clear signal
if any(hint in err.lower() for hint in ("unknown command", "not supported", "not found", "no such command")):
return json.dumps({
"success": False,
"error": f"JavaScript evaluation is not supported by this browser backend. {err}",
})
return json.dumps({
"success": False,
"error": err,
})
data = result.get("data", {})
raw_result = data.get("result")
# The eval command returns the JS result as a string. If the string
# is valid JSON, parse it so the model gets structured data.
parsed = raw_result
if isinstance(raw_result, str):
try:
parsed = json.loads(raw_result)
except (json.JSONDecodeError, ValueError):
pass # keep as string
return json.dumps({
"success": True,
"result": parsed,
"result_type": type(parsed).__name__,
}, ensure_ascii=False, default=str)
def _camofox_eval(expression: str, task_id: Optional[str] = None) -> str:
"""Evaluate JS via Camofox's /tabs/{tab_id}/eval endpoint (if available)."""
from tools.browser_camofox import _get_session, _ensure_tab, _post
try:
session = _get_session(task_id or "default")
tab_id = _ensure_tab(session)
resp = _post(f"/tabs/{tab_id}/eval", json_data={"expression": expression})
# Camofox returns the result in a JSON envelope
raw_result = resp.get("result") if isinstance(resp, dict) else resp
parsed = raw_result
if isinstance(raw_result, str):
try:
parsed = json.loads(raw_result)
except (json.JSONDecodeError, ValueError):
pass
return json.dumps({
"success": True,
"result": parsed,
"result_type": type(parsed).__name__,
}, ensure_ascii=False, default=str)
except Exception as e:
error_msg = str(e)
# Graceful degradation — server may not support eval
if any(code in error_msg for code in ("404", "405", "501")):
return json.dumps({
"success": False,
"error": "JavaScript evaluation is not supported by this Camofox server. "
"Use browser_snapshot or browser_vision to inspect page state.",
})
return json.dumps({"success": False, "error": error_msg})
def _maybe_start_recording(task_id: str):
"""Start recording if browser.record_sessions is enabled in config."""
if task_id in _recording_sessions:
@@ -2109,7 +2194,7 @@ registry.register(
name="browser_console",
toolset="browser",
schema=_BROWSER_SCHEMA_MAP["browser_console"],
handler=lambda args, **kw: browser_console(clear=args.get("clear", False), task_id=kw.get("task_id")),
handler=lambda args, **kw: browser_console(clear=args.get("clear", False), expression=args.get("expression"), task_id=kw.get("task_id")),
check_fn=check_browser_requirements,
emoji="🖥️",
)