Skip to content

Bug: Infinite web_search ↔ web_fetch tool-calling loop bypasses LoopDetectionMiddleware #2569

Description

@knight0940

Summary

The agent enters an infinite web_searchweb_fetch loop that can persist for 200+ tool calls without being caught by the existing LoopDetectionMiddleware. This was discovered while running benchmark evaluations. The root cause is twofold:

  1. The two-layer loop detection only matches identical tool call sets (hash-based) or single-tool frequency (frequency-based) — it does not detect alternating tool patterns like web_search → web_fetch → web_search → web_fetch → ...
  2. When web_fetch returns large content, repeated calls can trigger context compaction/summarization, causing the model to lose track of previously fetched results and re-initiate the search cycle.

Reproduction

Any task requiring iterative web research can reproduce this:

  1. Submit a task that requires web research
  2. The agent begins calling web_search, then web_fetch on results
  3. The agent enters a web_search → web_fetch → web_search → web_fetch → ... loop
  4. The loop continues until recursion_limit is hit (default 100), consuming the entire step budget
  5. The agent never reaches the synthesis phase, producing no final report

Observed Tool Call Pattern

'web_fetch', 'web_search', 'web_fetch', 'web_search', 'web_search', 'web_fetch', 
'web_search', 'web_fetch', 'web_fetch', 'web_search', 'web_search', 'web_fetch', 
'web_search', 'web_fetch', 'web_search', 'web_search', 'web_fetch', 'web_search', 
'web_fetch', 'web_search', 'web_search', 'web_fetch', 'web_search', 'web_fetch',
... (200+ calls)

Root Cause Analysis

1. Loop Detection Doesn't Catch Alternating Patterns

The LoopDetectionMiddleware (agents/middlewares/loop_detection_middleware.py) uses two detection layers:

Layer 1 (Hash-based): Hashes the entire tool call set per model response. Since web_search and web_fetch alternate, each consecutive response produces a different hash, bypassing detection:

# loop_detection_middleware.py:105-123
def _hash_tool_calls(tool_calls: list[dict]) -> str:
    normalized = []
    for tc in tool_calls:
        name = tc.get("name", "")
        args, fallback_key = _normalize_tool_call_args(tc.get("args", {}))
        key = _stable_tool_key(name, args, fallback_key)
        normalized.append(f"{name}:{key}")
    normalized.sort()
    blob = json.dumps(normalized, sort_keys=True, default=str)
    return hashlib.md5(blob.encode()).hexdigest()[:12]

A call to web_search(query="X") followed by web_fetch(url="Y") produces a different hash than web_fetch(url="Y") followed by web_search(query="Z").

Layer 2 (Frequency-based): The middleware tracks per-tool frequency, but the thresholds (warn=30, hard_limit=50) are based on a single tool type count. Since the calls alternate between web_search and web_fetch, neither individually hits the threshold as quickly — and the hard limit of 50 means up to 100 total tool calls (50 of each) before forced stop.

2. Context Compaction Compounds the Problem

When web_fetch returns content (up to 4096 chars per call at community/tavily/tools.py:60), repeated calls accumulate context. If summarization/compaction is triggered:

  • The compacted summary may lose the specific details of previously fetched results
  • The model, unable to recall what was already fetched, initiates another search cycle
  • Each new cycle returns similar results, creating a positive feedback loop

The summarization config defaults (summarization_config.py) show trigger: tokens = 4000 and keep: messages = 20, meaning compaction can occur relatively early in the conversation, especially with verbose web content.

3. SubAgent May Lack Sufficient Loop Detection

Subagents (subagents/executor.py) execute in isolated threads with fresh event loops. The middleware stack is built via build_subagent_runtime_middlewares(lazy_init=True), which may not include the same loop detection thresholds as the lead agent. Even when included, the alternating pattern issue remains.

Proposed Fix

A. Enhance Loop Detection for Alternating Patterns

Add a third detection layer that identifies tool-type-level cycles regardless of specific arguments. Track the set of tool types used within a sliding window and detect when the same set (e.g., {web_search, web_fetch}) appears repeatedly with no other tools interleaved.

B. Lower Frequency Thresholds for Web Tools

Consider lower tool_freq_hard_limit values specifically for web tools, since a legitimate research workflow rarely needs more than 10-15 total web calls:

TOOL_TYPE_LIMITS = {
    "web_search": {"warn": 8, "hard_limit": 15},
    "web_fetch": {"warn": 8, "hard_limit": 15},
}

C. Inject Remaining Step Count into Prompt

Inform the model how many steps it has left so it can self-regulate:

[Step Budget] You have used 45 of 100 steps. You have 55 steps remaining. 
If you have gathered enough information, synthesize your findings now.

D. Result Deduplication Awareness

When the same URL returns identical content as a previous fetch, inject a hint:

[DEDUP] web_fetch returned identical content to a previous call. 
Consider synthesizing with existing information instead of searching further.

Related Files

  • backend/packages/harness/deerflow/agents/middlewares/loop_detection_middleware.py — loop detection logic
  • backend/packages/harness/deerflow/community/tavily/tools.py — web_search/web_fetch tools
  • backend/packages/harness/deerflow/config/summarization_config.py — compaction thresholds
  • backend/packages/harness/deerflow/subagents/executor.py — subagent execution with middleware

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions