Summary
extract_tool_request() in helpers/extract_tools.py requires the model's entire reply to be nothing but the raw JSON object — any wrapping content, including a markdown json ... fence, makes it return None ("misformatted"). But the codebase already has a second function, is_misformatted_tool_request(), that correctly locates and parses JSON wrapped in a fence via:
for fenced_content in re.findall(
r"(?:json)?\s*(.*?)", content, flags=re.IGNORECASE | re.DOTALL
):
request = json_parse_dirty(fenced_content)
if isinstance(request, dict) and _is_tool_request(request):
return True
So the framework can already tell the difference between "this is garbage" and "this is a perfectly valid tool call the model just wrapped in a fence" — it only uses that distinction to log a scolding warning, never to actually execute the call. A fenced-but-otherwise-correct reply is punished identically to a truly broken one.
Why this matters
This directly feeds the consecutive_misformat circuit breaker added in #1031. Wrapping JSON in a code fence is one of the most deeply RLHF-trained habits of virtually every current chat model — it isn't model-specific and isn't fixable by prompting alone over a long-running task. In production logs across many chats on our instance, "Message misformat, no valid tool request found" warnings consistently follow a large tool result (a text_editor write/patch dumping file content into context, vision_load, document_query) — i.e. exactly the moment a chat-tuned model's instinct is to briefly acknowledge the result, which it does by wrapping its next JSON tool call in a fence rather than emitting bare JSON. Two of these in an unbroken row and the #1031 circuit breaker kills the entire agent run, leaving the chat idle until a user manually clicks "Nudge" — with no indication that the discarded response was, in fact, a valid tool call.
This means the #1031 fix (a good and necessary one, preventing true infinite loops) currently also fires on a large class of false positives, converting "the agent is running fine" into "the agent is dead until a human notices and intervenes" — which defeats unattended/autonomous use, one of Agent Zero's core value propositions.
Proposed fix
Reuse the existing fence-detection regex in extract_tool_request() itself, so a fully-fence-wrapped valid tool call is salvaged and executed instead of discarded:
_FENCE_RE = re.compile(r"^(?:json)?\s*(.*?)\s*$", re.IGNORECASE | re.DOTALL)
def extract_tool_request(content: str) -> dict[str, Any] | None:
if not content or not isinstance(content, str):
return None
content = content.strip()
fence_match = _FENCE_RE.match(content)
if fence_match:
content = fence_match.group(1).strip()
root = extract_json_root_string(content)
if root != content:
return None
request = _parse_json_root_object(root)
return request if request is not None and _is_tool_request(request) else None
This only rescues the narrow, unambiguous case where the entire reply is one fenced JSON block (matching the anchored ^...$ pattern) — it does not loosen tolerance for genuine mixed prose-plus-JSON replies, which should still correctly count as misformatted.
We've been running this patch in production and it noticeably reduced (though did not eliminate) circuit-breaker trips, without any observed downside.
Secondary observation (less certain, worth a look)
Once a reply is flagged as unusable, its raw text doesn't appear to be persisted anywhere in the chat log — only the generic "Message misformat" warning is recorded. This makes after-the-fact diagnosis very hard for end users (you can see that it failed and roughly when, but never what the model actually output). Logging the discarded raw response text alongside the misformat warning (even truncated) would make this class of issue much easier for users to self-diagnose and report accurately.
Environment
Agent Zero v2.6, Docker deployment
Reproduced across multiple models/providers (ruling out a model-specific cause)
Summary
extract_tool_request() in helpers/extract_tools.py requires the model's entire reply to be nothing but the raw JSON object — any wrapping content, including a markdown
json ...fence, makes it return None ("misformatted"). But the codebase already has a second function, is_misformatted_tool_request(), that correctly locates and parses JSON wrapped in a fence via:for fenced_content in re.findall(
r"
(?:json)?\s*(.*?)", content, flags=re.IGNORECASE | re.DOTALL):
request = json_parse_dirty(fenced_content)
if isinstance(request, dict) and _is_tool_request(request):
return True
So the framework can already tell the difference between "this is garbage" and "this is a perfectly valid tool call the model just wrapped in a fence" — it only uses that distinction to log a scolding warning, never to actually execute the call. A fenced-but-otherwise-correct reply is punished identically to a truly broken one.
Why this matters
This directly feeds the consecutive_misformat circuit breaker added in #1031. Wrapping JSON in a code fence is one of the most deeply RLHF-trained habits of virtually every current chat model — it isn't model-specific and isn't fixable by prompting alone over a long-running task. In production logs across many chats on our instance, "Message misformat, no valid tool request found" warnings consistently follow a large tool result (a text_editor write/patch dumping file content into context, vision_load, document_query) — i.e. exactly the moment a chat-tuned model's instinct is to briefly acknowledge the result, which it does by wrapping its next JSON tool call in a fence rather than emitting bare JSON. Two of these in an unbroken row and the #1031 circuit breaker kills the entire agent run, leaving the chat idle until a user manually clicks "Nudge" — with no indication that the discarded response was, in fact, a valid tool call.
This means the #1031 fix (a good and necessary one, preventing true infinite loops) currently also fires on a large class of false positives, converting "the agent is running fine" into "the agent is dead until a human notices and intervenes" — which defeats unattended/autonomous use, one of Agent Zero's core value propositions.
Proposed fix
Reuse the existing fence-detection regex in extract_tool_request() itself, so a fully-fence-wrapped valid tool call is salvaged and executed instead of discarded:
_FENCE_RE = re.compile(r"^
(?:json)?\s*(.*?)\s*$", re.IGNORECASE | re.DOTALL)def extract_tool_request(content: str) -> dict[str, Any] | None:
if not content or not isinstance(content, str):
return None
This only rescues the narrow, unambiguous case where the entire reply is one fenced JSON block (matching the anchored ^...$ pattern) — it does not loosen tolerance for genuine mixed prose-plus-JSON replies, which should still correctly count as misformatted.
We've been running this patch in production and it noticeably reduced (though did not eliminate) circuit-breaker trips, without any observed downside.
Secondary observation (less certain, worth a look)
Once a reply is flagged as unusable, its raw text doesn't appear to be persisted anywhere in the chat log — only the generic "Message misformat" warning is recorded. This makes after-the-fact diagnosis very hard for end users (you can see that it failed and roughly when, but never what the model actually output). Logging the discarded raw response text alongside the misformat warning (even truncated) would make this class of issue much easier for users to self-diagnose and report accurately.
Environment
Agent Zero v2.6, Docker deployment
Reproduced across multiple models/providers (ruling out a model-specific cause)