Skip to content

fix(web): ensure plugin discovery before web_*_tool registry lookups - #27584

Closed
briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/web-tools-plugin-discovery-27580
Closed

fix(web): ensure plugin discovery before web_*_tool registry lookups#27584
briandevans wants to merge 2 commits into
NousResearch:mainfrom
briandevans:fix/web-tools-plugin-discovery-27580

Conversation

@briandevans

Copy link
Copy Markdown
Contributor

Summary

  • tools/web_tools.py was the only plugin-backed dispatcher that did not call _ensure_plugins_discovered() before consulting agent.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 has web.extract_backend: firecrawl and FIRECRAWL_API_KEY set (issue web_extract fails with "No web extract provider configured" despite extract_backend: firecrawl and valid API key #27580).
  • Add _ensure_web_plugins_loaded() that 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.

Fixes #27580.

The bug

The reporter has both the config key and API key set, but web_extract returns:

{"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') returns None despite the plugin being bundled and FIRECRAWL_API_KEY being 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 through hermes_cli/main.py or gateway/config.py still see registered providers. tools/web_tools.py was the lone outlier — the post-#25182 dispatcher just imports the registry and queries it directly.

The fix

Add _ensure_web_plugins_loaded() (24 lines, mirrors tools.browser_tool._ensure_browser_plugins_loaded) and call it immediately before each from agent.web_search_registry import … in the three dispatchers. Discovery is idempotent so subsequent invocations early-return inside _ensure_plugins_discovered.

Test plan

  • Regression testtests/tools/test_web_providers.py::TestDispatchersTriggerPluginDiscovery (two tests, one each for web_extract_tool and web_search_tool). Each test empties the registry, points _ensure_plugins_discovered at 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.
  • Regression guard — with the production change reverted (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.
  • Adjacent suitetests/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).
  • Run command: uv run --with pytest --with pytest-xdist --with pytest-asyncio python3 -m pytest tests/tools/test_web_providers.py -v.

Related

Copilot AI review requested due to automatic review settings May 17, 2026 20:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in tools/web_tools.py that idempotently triggers plugin discovery.
  • Calls the helper at the start of dispatch in web_search_tool, web_extract_tool, and web_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.

Comment on lines +423 to +425
monkeypatch.setattr(
plugins_mod, "_ensure_plugins_discovered", fake_discover
)
Comment on lines +355 to +367
"""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)
Comment thread tools/web_tools.py Outdated

_ensure_plugins_discovered()
except Exception as exc: # noqa: BLE001
logger.debug("Web plugin discovery failed (non-fatal): %s", exc)
Comment thread tests/tools/test_web_providers.py Outdated
Comment on lines +418 to +421
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()
Comment thread tools/web_tools.py
Comment on lines +754 to +756
from hermes_cli.plugins import _ensure_plugins_discovered

_ensure_plugins_discovered()
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tools Tool registry, model_tools, toolsets tool/web Web search and extraction comp/plugins Plugin system and bundled plugins labels May 17, 2026
@briandevans
briandevans force-pushed the fix/web-tools-plugin-discovery-27580 branch 2 times, most recently from 66fddb0 to 7971488 Compare May 22, 2026 17:16
@briandevans

Copy link
Copy Markdown
Contributor Author

@copilot All five findings addressed in commit 96792c549a672b61f7b1a58441cd84aa014046d6:

  • tests/test_web_providers.py:436 (patch-path fragility) — switched to patching tools.web_tools._ensure_web_plugins_loaded directly so the test stays valid if the inline from hermes_cli.plugins import ... is ever hoisted to module scope.
  • tests/test_web_providers.py:432 (no assertion on hook call + missing crawl test) — wrapped the fake hook with MagicMock(wraps=...) and added an explicit .called assertion in each test. Added the matching web_crawl_tool regression test so all three dispatchers (search / extract / crawl) are guarded.
  • tools/web_tools.py:768 (silent exception swallow) — bumped the failure log from debug to warning with a comment explaining why; a broken plugin import will now be visible in normal logs.
  • tools/web_tools.py:766 (private symbol crosses module boundary) — fair concern; promoting _ensure_plugins_discovered to a public name would also need a matching rename in tools/browser_tool._ensure_browser_plugins_loaded (which this helper explicitly mirrors). Happy to take that on as a separate refactor PR if useful, but left out of this fix to keep the diff small.
  • tests/test_web_providers.py:378 (private _lock/_providers access in _clear_registry) — same: agreed it's coupling to internals, but a clean fix wants a public test-friendly snapshot API on agent.web_search_registry (or migrating the existing test helpers across the suite). Left out of this fix; happy to follow up if you'd prefer that direction.

@briandevans
briandevans force-pushed the fix/web-tools-plugin-discovery-27580 branch from 96792c5 to 0b7f882 Compare May 24, 2026 23:15
briandevans and others added 2 commits May 27, 2026 01:09
`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`.
@teknium1

Copy link
Copy Markdown
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists tool/web Web search and extraction type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

web_extract fails with "No web extract provider configured" despite extract_backend: firecrawl and valid API key

4 participants