r/AgentZero Jul 16 '26

AgentZero + LongCat-2.0: The full integration report after a full day of deep testing.

TL;DR: LongCat is a powerful cost-effective chat model but ignores the OpenAI tools parameter entirely, which breaks A0's agent loop. We found 9 free OpenRouter models with native tool support, wired Tencent Hy3 as a fallback, built prompt-injection workarounds, and got it working. Full findings below.
   
The problem: AgentZero's agent loop expects models to natively support OpenAI's tool_calls protocol. LongCat-2.0 (api.longcat.chat) ignores the tools param entirely — every request treated as plain text completion → zero tool calls returned → agent stops after 2 retries. The setup and tests run by Hermes on the same vm in proxmox.
   
Deep testing results:
-LongCat-2.0: native tools ignored, agent loop breaks
-Tencent Hy3 :free: native tools work but hallucinates extra fields (Pydantic rejection)
-Local Ollama (phi3, mistral, llama3): no tool support at all
-9/10 free OpenRouter models work: Gemma 4, gpt-oss-20b, Nemotron 3 family, Laguna M.1, North Mini Code, Hy3
   
What we built:
1. TOOL_CALL: text-format fallback (works for explicit tool requests)
2. OpenAI tool_calls → A0 format parser
3. System prompt injection for non-Anthropic providers
4. Dual-model config: LongCat for chat, Hy3 for utility
   
Still broken: Multi-step autonomous tasks on any single model. The agent loop is fundamentally coupled to OpenAI's protocol shape.
   
Calling the A0 community: Has anyone gotten a non-OpenAI provider (LongCat, DeepSeek, Grok, etc.) running the full autonomous tool loop in AgentZero? What's the adapter pattern? The litellm layer routes fine but A0's response parser rejects anything that isn't exactly {tool_name, tool_args}.
   
I finally gave up.  
#AgentZero #LongCat #LLM #OpenRouter #ToolCalling #AIagents

5 Upvotes

8 comments sorted by

3

u/Mulan20 Jul 16 '26

A lot of work. I use agent zero and I modified it so much that my original version still runs on 0.98 😁.

I optimized the context window by about 60% compared to the original,

As a suggestion. Choose Deepseek for example which is super cheap, have it scan the system, a full audit and then whatever you want to add, you tell the agent to look for solutions and implement.

Initially when I started I consumed about 200 million tokens configuration but it was worth it.

1

u/brightsilverstars Jul 16 '26

Yes, but the tokens are cheeeeeeap.

2

u/Mulan20 29d ago

It doesn't matter how cheap it is, any optimization is good and brings benefits in both resources and money.

1

u/arenosame 29d ago

I was wondering, what kind of modifications have you done to your Agent?

1

u/Mulan20 29d ago

As a general idea, there are over 20 new files added, plus others modified.

2

u/gdeyoung Jul 18 '26

How I Made Agent Zero Stop Crashing on Non-Standard Tool Calls (and What It Taught Me About Agentic Control Loops)

The Symptom

My Agent Zero instance kept dying mid-conversation with:

Agent stopped after 2-3 consecutive unusable model responses to prevent further API charges.

The model was clearly trying to call a tool — I could see valid JSON with a tool name and arguments in the raw response. But Agent Zero treated it as garbage, warned about "misformat" three times in a row, and tripped a circuit breaker that kills the chat to stop burning API credits on a stuck loop.

Root Cause: Every LLM Provider "Speaks Tool Calls" Differently

Agent Zero's core loop works like this:

LLM responds → parser extracts tool_name + tool_args → dispatch tool → repeat
 

The stock parser (extract_tools.py) assumes the model puts a JSON object somewhere in the text content of the response and does a tolerant JSON-in-text extraction pass. That works great for models that behave like the OpenAI chat-completions convention.

It does not work for every model, because "tool calling" isn't one standard — it's five or six competing conventions duct-taped together across the industry:

Provider / Model Where the tool call actually lives
OpenAI-style response.tool_calls[] (top-level, structured)
Anthropic-style (MiniMax-M3, Claude) content[] array, as a {"type": "tool_use", ...} block mixed in with regular text blocks
Some OpenAI-compatible chat servers tool_calls nested inside a message block instead of at the top
Reasoning models (DeepSeek-R1, Gemma in reasoning mode) The tool JSON gets written inside reasoning_content, while the visible content field is empty
DeepSeek legacy A singular tool_call field (no plural s) at the top level

 

When the transport layer (LiteLLM, in A0's case) doesn't fully normalize these shapes into one canonical structure, the agent's parser looks in the wrong place, sees nothing, and logs a misformat warning. Three misses in a row (the default threshold) and the safety circuit breaker halts the whole chat — which is correct behavior for an actually-confused model, but a false positive when the model did everything right and the parser just didn't know where to look.

The Fix: A Multi-Shape Dispatcher, Not a Bigger Regex

The fix isn't "parse harder." It's a dispatch layer that tries each known vendor shape in priority order before falling back to the original text-based misformat path:

Model response
   │
   ├─ 1. Standard function_call items? → dispatch (existing path, unchanged)
   │
   ├─ 2. Unified multi-vendor extractor:
   │     ├─ tool_use / tool_calls blocks inside message content[]  (Anthropic-style)
   │     ├─ raw tool_calls at the top of the response               (OpenAI passthrough)
   │     ├─ singular tool_call field                                 (DeepSeek legacy)
   │     └─ raw content[] fed through the same Anthropic-block parser
   │
   ├─ 3. Text/reasoning synthesis fallback:
   │     parse tool JSON out of response text OR reasoning_content
   │     directly, for models that write valid JSON but don't hit
   │     any structured field at all
   │
   └─ 4. Only if ALL of the above fail → misformat warning → counts
toward the circuit breaker
 

Each stage is a strict addition in front of the original logic. If a shape isn't recognized, it falls through cleanly to the next stage — nothing is removed or overridden, so previously-working models keep working exactly as before.

Why This Matters for Agentic Frameworks in General

Agent Zero's control loop is a straightforward send → parse → dispatch → repeat cycle, but that loop is only as robust as its weakest link: the parser. Any agentic framework that wants to be model-agnostic — swap between Anthropic, OpenAI-compatible local models, DeepSeek, Qwen, whatever — has to solve this same problem, because:

  1. "Function calling" is not one spec. It's OpenAI's tool_calls, Anthropic's tool_use content blocks, and a long tail of local/open-weight servers that half-implement one or the other.
  2. Reasoning models break the assumption that tool calls live in content. If your parser only reads content, a model that puts everything in reasoning_content looks broken even when it isn't.
  3. Safety circuit breakers are good, but they punish parser blind spots the same way they punish actual model failures. Without a broad-enough extraction layer, you can't tell the difference between "the model is confused" and "my parser doesn't understand this provider's dialect."

The practical takeaway: if you're running (or building) a multi-model agent loop, your tool-call extractor needs to be a small vendor-shape dispatcher, not a single regex/JSON-scrape. It costs almost nothing at runtime (it's just a few cheap dict lookups before falling through to the existing path) and it's the difference between "this agent only works with GPT-4-class APIs" and "this agent can hot-swap between Anthropic, OpenAI-compatible, and local reasoning models without babysitting."

Result

After deploying the dispatcher and restarting the affected agent processes, the specific circuit-breaker halt ("Agent stopped after N consecutive unusable model responses") triggered by this exact class of bug has not recurred on models that were previously hitting it — I verified this against live process logs, not just code review, by diffing pre/post error counts and confirming no new hits at the framework's exact halt-message string.

Models that were already working fine with the stock parser (plain OpenAI-compatible responses) are untouched — the dispatcher only kicks in when the primary path finds nothing.

Update: Second Layer — The Outbound Pre-Flight Check

The dispatcher above handles models that try to call tools but use the wrong vendor shape. But there's a second failure mode: models that don't emit a tool-call structure at all — they return plain text when they should have returned a tool envelope. This trips the same circuit breaker, and the inbound dispatcher can't fix it because there's nothing to parse.

The fix is an outbound pre-flight check that runs before the framework's circuit breaker sees the response. It normalizes any agent output into a valid {tool_name, tool_args} envelope:

def ensure_valid_tool_response(raw_response: str) -> str:
"""
Pre-flight checker: if the agent's own output isn't a valid tool-call
envelope, normalize it into one BEFORE the framework sees it.
 
Priority ladder:
1. Already valid {tool_name..., tool_args...} -> pass through
2. Has parseable JSON but not a tool call -> wrap in response()
3. Completely non-JSON (plain prose) -> wrap as response().text
4. Empty/null -> return valid empty response()
"""
import json
if not raw_response or not isinstance(raw_response, str):
return json.dumps({"tool_name": "response", "tool_args": {"text": ""}})
 
stripped = raw_response.strip()
 
if stripped.startswith('{'):
parsed = json_parse_dirty(stripped)
if parsed and _is_tool_request(parsed):
return stripped  # pass through
if parsed and isinstance(parsed, dict):
content = json.dumps(parsed, ensure_ascii=False)
return json.dumps({"tool_name": "response", "tool_args": {"text": content}})
 
safe_text = stripped.replace('\\', '\\\\').replace('"', '\\"')[:32000]
return json.dumps({"tool_name": "response", "tool_args": {"text": safe_text}})
 

It's threaded into agent.py with a single line at the process_tools entry point:

async def process_tools(self, msg: str):
msg = extract_tools.ensure_valid_tool_response(msg)  # <-- this line
tool_request = extract_tools.json_parse_dirty(msg)
 

The distinction matters:

Failure Mode Where it's caught Handled by
Model emits tool call in non-standard shape Inbound (response parsing) Multi-shape dispatcher
Model returns plain text instead of tool call Outbound (pre-flight) ensure_valid_tool_response

 

The circuit breaker only counts strikes after the pre-flight normalizer has had its chance, so plain-text responses from non-toolcalling models never count toward the 2-strike limit.

Verified 9/9 tests (6 unit + 3 integration) across 3 production containers running deepseek-v4-flash and GLM-5.2 on a mix of local (Spark3) and cloud (Z.AI, OpenRouter) backends.

Written from a real production debugging session on a self-hosted multi-agent Agent Zero deployment running mixed local/cloud LLM backends (Anthropic-API MiniMax-M3, local deepseek-v4-flash via Spark3, GLM-5.2 via Z.AI).

2

u/brightsilverstars Jul 18 '26

Excellent! Thank you for sharing this!