Skip to content
Open
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
6 changes: 3 additions & 3 deletions agent/prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ def _strip_yaml_frontmatter(content: str) -> str:
"- System state: OS, CPU, memory, disk, ports, processes → use terminal\n"
"- File contents, sizes, line counts → use read_file, search_files, or terminal\n"
"- Git history, branches, diffs → use terminal\n"
"- Current facts (weather, news, versions) → use web_search\n"
"- Current facts (weather, news, versions) → use an appropriate permitted retrieval/search tool\n"
"Your memory and user profile describe the USER, not the system you are "
"running on. The execution environment may differ from what the user profile "
"says about their personal setup.\n"
Expand Down Expand Up @@ -458,8 +458,8 @@ def _strip_yaml_frontmatter(content: str) -> str:
"\n"
"<missing_context>\n"
"- If required context is missing, do NOT guess or hallucinate an answer.\n"
"- Use the appropriate lookup tool when missing information is retrievable "
"(search_files, web_search, read_file, etc.).\n"
"- Use the appropriate permitted lookup tool when missing information is "
"retrievable (search_files, read_file, or an available retrieval/search tool).\n"
"- Ask a clarifying question only when the information cannot be retrieved by tools.\n"
"- If you must proceed with incomplete information, label assumptions explicitly.\n"
"</missing_context>"
Expand Down
54 changes: 35 additions & 19 deletions model_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,38 @@ def _clear_tool_defs_cache() -> None:
_tool_defs_cache.clear()


def _apply_browser_retrieval_hints(
definitions: List[Dict[str, Any]],
available_tool_names: set[str],
) -> None:
"""Name retrieval helpers in browser schemas only when they are available.

Static tool schemas must stay toolset-neutral because registry availability is
resolved at runtime. This layer may safely add concrete names after check_fn
filtering has produced ``available_tool_names``.
"""
web_tools = [name for name in ("web_search", "web_extract") if name in available_tool_names]

for index, tool_definition in enumerate(definitions):
function = tool_definition.get("function", {})
name = function.get("name")
description = str(function.get("description") or "")
hint = ""

if name == "browser_navigate" and web_tools:
joined = " and ".join(web_tools)
noun = "tool" if len(web_tools) == 1 else "tools"
hint = f" Available lightweight retrieval {noun}: {joined}."
elif name == "browser_cdp" and "web_extract" in available_tool_names:
hint = " The web_extract tool is available for fetching CDP documentation URLs."

if hint and hint not in description:
definitions[index] = {
**tool_definition,
"function": {**function, "description": description + hint},
}


def get_tool_definitions(
enabled_toolsets: Optional[List[str]] = None,
disabled_toolsets: Optional[List[str]] = None,
Expand Down Expand Up @@ -506,25 +538,9 @@ def _compute_tool_definitions(
filtered_tools[i] = {"type": "function", "function": dynamic}
break

# Strip web tool cross-references from browser_navigate description when
# web_search / web_extract are not available. The static schema says
# "prefer web_search or web_extract" which causes the model to hallucinate
# those tools when they're missing.
if "browser_navigate" in available_tool_names:
web_tools_available = {"web_search", "web_extract"} & available_tool_names
if not web_tools_available:
for i, td in enumerate(filtered_tools):
if td.get("function", {}).get("name") == "browser_navigate":
desc = td["function"].get("description", "")
desc = desc.replace(
" For simple information retrieval, prefer web_search or web_extract (faster, cheaper).",
"",
)
filtered_tools[i] = {
"type": "function",
"function": {**td["function"], "description": desc},
}
break
# Static schemas stay toolset-neutral. Add concrete retrieval tool names
# only after check_fn filtering has established what is actually available.
_apply_browser_retrieval_hints(filtered_tools, available_tool_names)

if not quiet_mode:
if filtered_tools:
Expand Down
4 changes: 3 additions & 1 deletion tests/agent/test_prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,9 @@ def test_guidance_covers_verification(self):
assert "correctness" in text


def test_guidance_does_not_mandate_specific_web_tool(self):
assert "→ use web_search" not in OPENAI_MODEL_EXECUTION_GUIDANCE
assert "appropriate permitted retrieval/search tool" in OPENAI_MODEL_EXECUTION_GUIDANCE

def test_guidance_is_string(self):
assert isinstance(OPENAI_MODEL_EXECUTION_GUIDANCE, str)
Expand Down Expand Up @@ -920,4 +923,3 @@ def test_has_a_heading(self):
# Budget warning history stripping
# =========================================================================


40 changes: 40 additions & 0 deletions tests/test_model_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,43 @@ def test_disabling_coding_preserves_core_but_atomic_disables_still_remove(self):
)
}
assert "write_file" not in no_file


class TestBrowserRetrievalHints:
@staticmethod
def _definitions():
return [
{"type": "function", "function": {"name": "browser_navigate", "description": "Navigate."}},
{"type": "function", "function": {"name": "browser_cdp", "description": "CDP docs."}},
]

def test_names_only_tools_that_are_available(self):
from model_tools import _apply_browser_retrieval_hints

definitions = self._definitions()
_apply_browser_retrieval_hints(definitions, {"browser_navigate", "browser_cdp", "web_search"})

navigate = definitions[0]["function"]["description"]
cdp = definitions[1]["function"]["description"]
assert "web_search" in navigate
assert "web_extract" not in navigate
assert "web_extract" not in cdp

def test_adds_extract_hint_only_when_extract_is_available(self):
from model_tools import _apply_browser_retrieval_hints

definitions = self._definitions()
_apply_browser_retrieval_hints(definitions, {"browser_navigate", "browser_cdp", "web_extract"})

assert "web_extract" in definitions[0]["function"]["description"]
assert "web_extract" in definitions[1]["function"]["description"]

def test_leaves_names_absent_when_web_tools_are_unavailable(self):
from model_tools import _apply_browser_retrieval_hints

definitions = self._definitions()
_apply_browser_retrieval_hints(definitions, {"browser_navigate", "browser_cdp"})

rendered = " ".join(item["function"]["description"] for item in definitions)
assert "web_search" not in rendered
assert "web_extract" not in rendered
20 changes: 20 additions & 0 deletions tests/tools/test_browser_hardening.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@ def test_browser_close_schema_removed(self):
assert "browser_close" not in names


class TestBrowserNavigateSchemaToolReferences:
def test_static_description_is_toolset_neutral(self):
from tools.browser_tool import BROWSER_TOOL_SCHEMAS

schema = next(item for item in BROWSER_TOOL_SCHEMAS if item["name"] == "browser_navigate")
description = schema["description"]

assert "web_search" not in description
assert "web_extract" not in description
assert "lightweight retrieval tool" in description

def test_cdp_static_description_is_toolset_neutral(self):
from tools.browser_cdp_tool import BROWSER_CDP_SCHEMA

description = BROWSER_CDP_SCHEMA["description"]

assert "web_extract" not in description
assert "available documentation lookup or extraction tool" in description


# ---------------------------------------------------------------------------
# Caching: _find_agent_browser
# ---------------------------------------------------------------------------
Expand Down
5 changes: 3 additions & 2 deletions tools/browser_cdp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,8 +549,9 @@ def browser_cdp(
"Firecrawl) — those expose CDP per session but live-session routing is "
"a follow-up. Camofox is REST-only and will never support CDP. If the "
"tool is in your toolset at all, a CDP endpoint is already reachable.\n\n"
f"**CDP method reference:** {CDP_DOCS_URL} — use web_extract on a "
"method's URL (e.g. '/tot/Page/#method-handleJavaScriptDialog') "
f"**CDP method reference:** {CDP_DOCS_URL} — use an available "
"documentation lookup or extraction tool on a method's URL "
"(e.g. '/tot/Page/#method-handleJavaScriptDialog') "
"to look up parameters and return shape.\n\n"
"**Common patterns:**\n"
"- List tabs: method='Target.getTargets', params={}\n"
Expand Down
2 changes: 1 addition & 1 deletion tools/browser_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1944,7 +1944,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). For plain-text endpoints — URLs ending in .md, .txt, .json, .yaml, .yml, .csv, .xml, raw.githubusercontent.com, or any documented API endpoint — prefer curl via the terminal tool or web_extract; the browser stack is overkill and much slower for these. 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.",
"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 a lightweight retrieval tool when one is available (faster, cheaper). For plain-text endpoints — URLs ending in .md, .txt, .json, .yaml, .yml, .csv, .xml, raw.githubusercontent.com, or any documented API endpoint — prefer an available text-extraction or terminal-fetch tool; the browser stack is overkill and much slower for these. 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 Down
Loading