diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 6bd88fa194f97..d4053fc869231 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -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" @@ -458,8 +458,8 @@ def _strip_yaml_frontmatter(content: str) -> str: "\n" "\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" "" diff --git a/model_tools.py b/model_tools.py index 9c354194b85fb..99cadd9030abd 100644 --- a/model_tools.py +++ b/model_tools.py @@ -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, @@ -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: diff --git a/tests/agent/test_prompt_builder.py b/tests/agent/test_prompt_builder.py index 28def42c05095..072f3c1f1c814 100644 --- a/tests/agent/test_prompt_builder.py +++ b/tests/agent/test_prompt_builder.py @@ -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) @@ -920,4 +923,3 @@ def test_has_a_heading(self): # Budget warning history stripping # ========================================================================= - diff --git a/tests/test_model_tools.py b/tests/test_model_tools.py index 5110f42162993..5b460d808ba78 100644 --- a/tests/test_model_tools.py +++ b/tests/test_model_tools.py @@ -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 diff --git a/tests/tools/test_browser_hardening.py b/tests/tools/test_browser_hardening.py index 185bd7c73efe1..c09c53dd1b9bd 100644 --- a/tests/tools/test_browser_hardening.py +++ b/tests/tools/test_browser_hardening.py @@ -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 # --------------------------------------------------------------------------- diff --git a/tools/browser_cdp_tool.py b/tools/browser_cdp_tool.py index eccd8f8fc1c56..7b70667dba330 100644 --- a/tools/browser_cdp_tool.py +++ b/tools/browser_cdp_tool.py @@ -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" diff --git a/tools/browser_tool.py b/tools/browser_tool.py index d81bab4406096..72f0b9083eab8 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -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": {