fix(web): ensure plugin discovery before web_*_tool registry lookups - #27584
fix(web): ensure plugin discovery before web_*_tool registry lookups#27584briandevans wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes issue #27580 where web tool dispatchers could return a misleading "No web extract provider configured" error when invoked from contexts that hadn't triggered plugin discovery (subprocess agents, delegate children, standalone scripts).
Changes:
- Adds
_ensure_web_plugins_loaded()helper intools/web_tools.pythat idempotently triggers plugin discovery. - Calls the helper at the start of dispatch in
web_search_tool,web_extract_tool, andweb_crawl_tool. - Adds regression tests verifying both extract and search dispatchers trigger discovery before registry lookup.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| tools/web_tools.py | Adds plugin-discovery shim and invokes it in three dispatchers. |
| tests/tools/test_web_providers.py | Adds two regression tests for the discovery-before-lookup invariant. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| monkeypatch.setattr( | ||
| plugins_mod, "_ensure_plugins_discovered", fake_discover | ||
| ) |
| """Reset the web_search registry to empty and return a callback | ||
| that restores the original contents. Used in a try/finally so the | ||
| snapshot is restored even when the dispatcher under test raises.""" | ||
| from agent import web_search_registry | ||
|
|
||
| with web_search_registry._lock: | ||
| original = dict(web_search_registry._providers) | ||
| web_search_registry._providers.clear() | ||
|
|
||
| def _restore(): | ||
| with web_search_registry._lock: | ||
| web_search_registry._providers.clear() | ||
| web_search_registry._providers.update(original) |
|
|
||
| _ensure_plugins_discovered() | ||
| except Exception as exc: # noqa: BLE001 | ||
| logger.debug("Web plugin discovery failed (non-fatal): %s", exc) |
| def fake_discover(force=False): | ||
| if web_search_registry.get_provider("firecrawl") is None: | ||
| web_search_registry.register_provider(FakeFirecrawl()) | ||
| return plugins_mod.get_plugin_manager() |
| from hermes_cli.plugins import _ensure_plugins_discovered | ||
|
|
||
| _ensure_plugins_discovered() |
66fddb0 to
7971488
Compare
|
@copilot All five findings addressed in commit 96792c549a672b61f7b1a58441cd84aa014046d6:
|
96792c5 to
0b7f882
Compare
`tools/web_tools.py` is the only plugin-backed dispatcher that did not
call `_ensure_plugins_discovered()` before consulting
`agent.web_search_registry`. Every other plugin-backed tool
(`tools/image_generation_tool.py`, `tools/video_generation_tool.py`,
`tools/browser_tool.py`, `tools/skills_tool.py`) defensively triggers
idempotent discovery so the registry is populated from contexts that
haven't gone through `hermes_cli/main.py` or `gateway/config.py` first.
Without discovery, `get_provider('firecrawl')` returns `None` on a cold
process even when the user has `web.extract_backend: firecrawl`
configured and `FIRECRAWL_API_KEY` set, and the dispatcher emits the
misleading "No web extract provider configured" error (issue NousResearch#27580).
The same root cause affects the search and crawl paths.
Add `_ensure_web_plugins_loaded()` (mirrors
`tools.browser_tool._ensure_browser_plugins_loaded` exactly) and invoke
it at all three registry-lookup sites: `web_search_tool`,
`web_extract_tool`, `web_crawl_tool`.
Regression guard: two new tests in `TestDispatchersTriggerPluginDiscovery`
empty the registry, point `_ensure_plugins_discovered` at a fake that
registers a provider on first call, and assert the dispatcher resolves
the configured backend instead of returning "No web extract provider
configured". Without the production change both tests fail with the
exact bug-report error text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per inline review on NousResearch#27584: - tools/web_tools.py: log discovery exceptions at warning, not debug. A genuinely broken plugin import was silenced into invisibility — the user then hits the same misleading "No web extract provider configured" error this helper is meant to eliminate, with nothing in normal logs pointing at the real cause. - tests/tools/test_web_providers.py: patch `tools.web_tools._ensure_web_plugins_loaded` directly rather than the upstream `hermes_cli.plugins._ensure_plugins_discovered`. The previous test silently stopped exercising the dispatcher path if the inline `from hermes_cli.plugins import ...` were ever hoisted to module scope; patching the helper directly is robust to that refactor. - tests/tools/test_web_providers.py: wrap the fake discovery hook with `MagicMock(wraps=...)` and assert `.called`. Previous version only asserted end-state (provider registered, no error string), which would silently regress if a future change dropped the `_ensure_web_plugins_loaded()` call but the registry happened to be pre-populated. - tests/tools/test_web_providers.py: add the corresponding regression test for `web_crawl_tool` so all three dispatchers (search, extract, crawl) are guarded against the same regression. Not addressed in this commit (left as broader follow-ups, mentioned in the PR reply): - promoting `_ensure_plugins_discovered` to a public API name (would also need a matching rename in `tools/browser_tool.py`); - replacing direct `_lock`/`_providers` access in the test's `_clear_registry()` helper with a public test-friendly snapshot API on `agent.web_search_registry`.
0b7f882 to
a0fb329
Compare
|
Merged via PR #34563. Your commits were cherry-picked onto current main with your authorship preserved in git log (commit 6e179c4). The original PR's third hunk (web_crawl_tool dispatcher) was dropped because that function no longer exists on main — the two live dispatchers (search + extract) carry your fix. Thanks for the clean diagnosis and the mirror-the-browser-pattern approach. |
Summary
tools/web_tools.pywas the only plugin-backed dispatcher that did not call_ensure_plugins_discovered()before consultingagent.web_search_registry. From cold-start contexts (subprocess agent runs, delegate children, standalone scripts) the registry stays empty and the dispatcher emits the misleading "No web extract provider configured" error even when the user hasweb.extract_backend: firecrawlandFIRECRAWL_API_KEYset (issue web_extract fails with "No web extract provider configured" despite extract_backend: firecrawl and valid API key #27580)._ensure_web_plugins_loaded()that mirrorstools.browser_tool._ensure_browser_plugins_loadedexactly, and invoke it at all three registry-lookup sites:web_search_tool,web_extract_tool,web_crawl_tool.Fixes #27580.
The bug
The reporter has both the config key and API key set, but
web_extractreturns:{"success": false, "error": "No web extract provider configured. Set web.extract_backend to firecrawl, tavily, exa, or parallel."}Their debug script confirms
get_provider('firecrawl')returnsNonedespite the plugin being bundled andFIRECRAWL_API_KEYbeing set — i.e. the plugin was never loaded into the registry at import time.Every other plugin-backed tool dispatcher in the tree (
tools/image_generation_tool.py:874-876,tools/video_generation_tool.py:207-209,tools/browser_tool.py:469-485,tools/skills_tool.py:878-893) defensively calls_ensure_plugins_discovered()(idempotent) before looking up the registry, precisely so that cold-start contexts that haven't gone throughhermes_cli/main.pyorgateway/config.pystill see registered providers.tools/web_tools.pywas the lone outlier — the post-#25182 dispatcher just imports the registry and queries it directly.The fix
Add
_ensure_web_plugins_loaded()(24 lines, mirrorstools.browser_tool._ensure_browser_plugins_loaded) and call it immediately before eachfrom agent.web_search_registry import …in the three dispatchers. Discovery is idempotent so subsequent invocations early-return inside_ensure_plugins_discovered.Test plan
tests/tools/test_web_providers.py::TestDispatchersTriggerPluginDiscovery(two tests, one each forweb_extract_toolandweb_search_tool). Each test empties the registry, points_ensure_plugins_discoveredat a fake that registers a provider on first call, and asserts the dispatcher resolves the configured backend instead of returning the bug-report error string.git stash push tools/web_tools.py), both new tests fail with the exact bug-report error text:AssertionError: assert 'No web extract provider configured' not in '...'Restoring the fix makes them pass.
tests/tools/test_web_tools_config.py tests/tools/test_web_providers.py tests/tools/test_web_providers_brave_free.py tests/tools/test_web_providers_ddgs.py tests/tools/test_web_providers_searxng.py tests/tools/test_web_tools_tavily.py tests/tools/test_website_policy.py— 167 passed in 46s (no regressions).uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/tools/test_web_providers.py -v.Related
from agent.web_search_registry import get_providerdirectly, missing the discovery-before-lookup guard that the other plugin-backed dispatchers already had.