Skip to content

feat: web search fallback chain + loop-guard rework (Closes #467 #544 #571)#574

Merged
Lexus2016 merged 2 commits into
mainfrom
evolution/issue-467-544-571-rework-v3
Jun 27, 2026
Merged

feat: web search fallback chain + loop-guard rework (Closes #467 #544 #571)#574
Lexus2016 merged 2 commits into
mainfrom
evolution/issue-467-544-571-rework-v3

Conversation

@Lexus2016

Copy link
Copy Markdown
Owner

Automated evolution PR implementing the web-search fallback chain and loop-guard/tool-diagnostics improvements selected for this cycle.

Scope:

  • tools/web_tools.py: _search_with_fallbacks uses search_backend_fallback_chain; falls back on failure or on empty results when a chain is configured.
  • plugins/web/ddgs/provider.py: empty DDGS results trigger provider-dead only when a fallback chain exists.
  • hermes_cli/config.py: adds search_backend_fallback_chain default.
  • agent/loop_guard.py + agent/tool_diagnostics.py: carry forward prior loop-guard / provider-dead detection work.
  • Tests updated for the new semantics.

Closes #467
Closes #544
Closes #571

Co-Authored-By: Hermes Evolution evolution@hermes.ai

@github-actions

github-actions Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: evolution/issue-467-544-571-rework-v3 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11817 on HEAD, 11809 on base (🆕 +8)

🆕 New issues (6):

Rule Count
invalid-argument-type 3
invalid-assignment 2
unsupported-operator 1
First entries
tools/web_tools.py:1019: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["provider"]` and value of type `bool | str | None | Any` on object of type `dict[str, str | int]`
tests/tools/test_web_providers.py:304: [invalid-assignment] invalid-assignment: Object of type `() -> dict[Unknown, Unknown]` is not assignable to attribute `_load_web_config` of type `def _load_web_config() -> dict[Unknown, Unknown]`
tests/tools/test_web_providers.py:252: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `bound method str.__getitem__(key: SupportsIndex | slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> str` cannot be called with key of type `Literal["search_backend_fallback_chain"]` on object of type `str`
tests/tools/test_web_providers.py:252: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> Unknown, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[Unknown]]` cannot be called with key of type `Literal["search_backend_fallback_chain"]` on object of type `list[Unknown]`
tests/tools/test_web_providers.py:247: [unsupported-operator] unsupported-operator: Operator `in` is not supported between objects of type `Literal["search_backend_fallback_chain"]` and `str | dict[Unknown, Unknown] | list[Unknown] | ... omitted 37 union elements`
tests/tools/test_web_providers.py:252: [invalid-argument-type] invalid-argument-type: Method `__getitem__` of type `Overload[(i: SupportsIndex, /) -> str, (s: slice[SupportsIndex | None, SupportsIndex | None, SupportsIndex | None], /) -> list[str]]` cannot be called with key of type `Literal["search_backend_fallback_chain"]` on object of type `list[str]`

✅ Fixed issues (1):

Rule Count
invalid-method-override 1
First entries
tests/tools/test_web_providers.py:385: [invalid-method-override] invalid-method-override: Invalid override of method `extract`: Definition is incompatible with `WebSearchProvider.extract`

Unchanged: 6220 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

- _search_with_fallbacks now tries the configured fallback chain when the
  primary backend reports failure OR when it returns empty results and a
  fallback chain is configured. Empty results with no fallback chain remain
  success=True so real 'no hits' outcomes are not turned into provider-dead
  errors (#467 rework).
- DDGS provider surfaces provider-dead only when empty results occur and a
  fallback chain exists.
- Update fallback-chain tests to match the new semantics and keep
  regression coverage.
- Carry forward prior loop-guard / tool-diagnostics improvements from #544.

Closes #467
Closes #544
Closes #571

Co-Authored-By: Hermes Evolution <evolution@hermes.ai>
@Lexus2016 Lexus2016 force-pushed the evolution/issue-467-544-571-rework-v3 branch from 4dd96f3 to 97d671f Compare June 27, 2026 11:28
The #467 same-query short-circuit for spiral-prone idempotent tools
(web_search / web_extract / search_files) was functionally inert: the
dedup identity was computed by `_tool_result_arg_hash`, which parsed the
tool RESULT with `json.loads` to recover input args. But tool results do
not carry the input args, and web_search / web_extract outputs are
XML-wrapped in `<untrusted_tool_result>`, so the parse always raised and
the arg hash was always None -> the short-circuit never triggered. With
zero test coverage, CI stayed green over a dead feature.

Fix: derive the identity hash from the assistant tool-call INPUT args
(`tool_calls[].function.arguments`) instead of the result. New
`_tool_call_arg_hash` canonicalizes JSON-string OR dict args (key-order
insensitive), hashes unparseable string args verbatim, and returns None
on missing args (fail-safe: no spurious short-circuit). The generic
repeat/spiral detection is untouched.

Adds TDD coverage in tests/agent/test_loop_guard.py: same-query fires
(string args, dict args, key-order insensitive, web_extract), and
negatives (different queries do not short-circuit; varied queries still
hit the generic spiral nudge at the repeat threshold).
@Lexus2016 Lexus2016 merged commit e9016b0 into main Jun 27, 2026
57 of 59 checks passed
@Lexus2016 Lexus2016 deleted the evolution/issue-467-544-571-rework-v3 branch June 27, 2026 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant