NousResearch/hermes-agent

Tool loop warnings with recovery hints

Cluster 28204. Data as of

Nothing open on GitHub. Fix merged in #28592 on 19 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #28592. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #28592. That is separate from the fix event.
Still open
Nothing open on GitHub.

These items address adding recovery hints to tool loop warnings in the agent and related gateway tool-use hardening, including skill preload and loop-guard improvements. The work centers on improving guidance when tool loops are detected.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #28592 fix(agent): add recovery hints to tool loop warnings (#28256)

    merged to main

    files: agent/tool_guardrails.py, tests/agent/test_tool_guardrails.py, tests/run_agent/test_tool_call_guardrail_runtime.py

    Show the diff
    • agent/tool_guardrails.py
      @@ -336,10 +336,7 @@ def after_call(
                       return ToolGuardrailDecision(
                           action="warn",
                           code="same_tool_failure_warning",
      -                    message=(
      -                        f"{tool_name} has failed {same_count} times this turn. "
      -                        "This looks like a loop; change approach before retrying."
      -                    ),
      +                    message=_tool_failure_recovery_hint(tool_name, same_count),
                           tool_name=tool_name,
                           count=same_count,
                           signature=signature,
      @@ -406,6 +403,26 @@ def append_toolguard_guidance(result: str, decision: ToolGuardrailDecision) -> s
           return (result or "") + suffix
       
       
      +def _tool_failure_recovery_hint(tool_name: str, count: int) -> str:
      +    """Action-oriented guidance for recovering from repeated tool failures."""
      +    common = (
      +        f"{tool_name} has failed {count} times this turn. This looks like a loop. "
      +        "Do not switch to text-only replies; keep using tools, but diagnose before retrying. "
      +        "First inspect the latest error/output and verify your assumptions. "
      +    )
      +    if tool_name == "terminal":
      +        return common + (
      +            "For terminal failures, run a small diagnostic such as `pwd && ls -la` "
      +            "in the same tool, then try an absolute path, a simpler command, a different "
      +            "working directory, or a different tool such as read_file/write_file/patch."
      +        )
      +    return common + (
      +        "Try different arguments, a narrower query/path, an absolute path when relevant, "
      +        "or a different tool that can make progress. If the blocker is external, report "
      +        "the blocker after one diagnostic attempt instead of repeating the same failing path."
      +    )
      +
      +
       def _coerce_args(args: Mapping[str, Any] | None) -> Mapping[str, Any]:
           return args if isinstance(args, Mapping) else {}
       
    • tests/agent/test_tool_guardrails.py
      @@ -160,6 +160,10 @@ def test_same_tool_varying_args_warns_by_default_without_halting():
           assert first.action == "allow"
           assert [second.action, third.action, fourth.action] == ["warn", "warn", "warn"]
           assert {second.code, third.code, fourth.code} == {"same_tool_failure_warning"}
      +    assert "Do not switch to text-only replies" in second.message
      +    assert "keep using tools" in second.message
      +    assert "diagnose before retrying" in second.message
      +    assert "different tool" in second.message
           assert controller.halt_decision is None
       
       
    • tests/run_agent/test_tool_call_guardrail_runtime.py
      @@ -153,6 +153,37 @@ def test_sequential_after_call_appends_guidance_to_tool_result_without_extra_mes
           assert "repeated_exact_failure_warning" in messages[0]["content"]
       
       
      +def test_same_tool_failure_warning_tells_model_to_recover_with_tools():
      +    agent = _make_agent("terminal")
      +    guardrails = getattr(agent, "_tool_guardrails")
      +    guardrails.after_call(
      +        "terminal",
      +        {"command": "bad-1"},
      +        json.dumps({"exit_code": 1}),
      +        failed=True,
      +    )
      +    guardrails.after_call(
      +        "terminal",
      +        {"command": "bad-2"},
      +        json.dumps({"exit_code": 1}),
      +        failed=True,
      +    )
      +    tc = _mock_tool_call("terminal", json.dumps({"command": "bad-3"}), "c-recover")
      +    msg = SimpleNamespace(content="", tool_calls=[tc])
      +    messages = []
      +
      +    with patch("run_agent.handle_function_call", return_value=json.dumps({"exit_code": 1})):
      +        agent._execute_tool_calls_sequential(msg, messages, "task-1")
      +
      +    content = messages[0]["content"]
      +    assert "same_tool_failure_warning" in content
      +    assert "Do not switch to text-only replies" in content
      +    assert "keep using tools" in content
      +    assert "pwd && ls -la" in content
      +    assert "absolute path" in content
      +    assert "different tool" in content
      +
      +
       def test_config_enabled_hard_stop_concurrent_path_does_not_submit_blocked_calls_and_preserves_result_order():
           agent = _make_agent("web_search", config=_hard_stop_config())
           blocked_args = {"query": "blocked"}

1 PR closed without merging

From the close comments: merged via another change 1

  • #28256 fix(agent): add recovery hints to tool loop warnings · merged via another change → d759a67, #28592
Closed issues · 1
  • Closed#28204Gateway tool-use hardening: skill preload, loop-guard recovery, path resolution docs
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#28592
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
19 May 2026

For your agent

The same facts, typed.