Summary
The agent enters an infinite web_search ↔ web_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:
- 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 → ...
- 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:
- Submit a task that requires web research
- The agent begins calling
web_search, then web_fetch on results
- The agent enters a
web_search → web_fetch → web_search → web_fetch → ... loop
- The loop continues until
recursion_limit is hit (default 100), consuming the entire step budget
- 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
Summary
The agent enters an infinite
web_search↔web_fetchloop that can persist for 200+ tool calls without being caught by the existingLoopDetectionMiddleware. This was discovered while running benchmark evaluations. The root cause is twofold:web_search → web_fetch → web_search → web_fetch → ...web_fetchreturns 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:
web_search, thenweb_fetchon resultsweb_search → web_fetch → web_search → web_fetch → ...looprecursion_limitis hit (default 100), consuming the entire step budgetObserved Tool Call Pattern
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_searchandweb_fetchalternate, each consecutive response produces a different hash, bypassing detection:A call to
web_search(query="X")followed byweb_fetch(url="Y")produces a different hash thanweb_fetch(url="Y")followed byweb_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 betweenweb_searchandweb_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_fetchreturns content (up to 4096 chars per call atcommunity/tavily/tools.py:60), repeated calls accumulate context. If summarization/compaction is triggered:The summarization config defaults (
summarization_config.py) showtrigger: tokens = 4000andkeep: 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 viabuild_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_limitvalues specifically for web tools, since a legitimate research workflow rarely needs more than 10-15 total web calls:C. Inject Remaining Step Count into Prompt
Inform the model how many steps it has left so it can self-regulate:
D. Result Deduplication Awareness
When the same URL returns identical content as a previous fetch, inject a hint:
Related Files
backend/packages/harness/deerflow/agents/middlewares/loop_detection_middleware.py— loop detection logicbackend/packages/harness/deerflow/community/tavily/tools.py— web_search/web_fetch toolsbackend/packages/harness/deerflow/config/summarization_config.py— compaction thresholdsbackend/packages/harness/deerflow/subagents/executor.py— subagent execution with middleware