ANTIGRAVITY LABJP
Articles/Agents & Manager
Agents & Manager/2026-04-22Intermediate

Diagnosing and Stopping Runaway Agent Loops in Antigravity

Build agents with Antigravity and you will eventually meet the 'same tool called twenty times in a row' problem. Here is how to classify the failure mode and stop it at the implementation level.

antigravity429agents123debugging15troubleshooting105loop

Writing a multi-agent system in Antigravity works beautifully in the demo, and then you push it to production and watch the same tool fire ten, twenty, thirty times in a row. I've been there more than once. The first instinct is to blame the model, but nearly every time the root cause has been in the design or the prompt.

This article classifies the four kinds of agent loops I keep seeing, and shares the guardrails I now put in from the start. Once you know which variety you're dealing with, the fix is often a prompt tweak and a few dozen lines of code.

Four flavors of agent loop

From my experience, runaway agent behavior falls into one of these four buckets. Different bucket, different fix — so diagnosis starts by identifying which one you have.

  • A. Tool-call loop — the agent calls the same tool with the same args, repeatedly
  • B. Subagent recursion loop — an agent spawns another of the same type, indefinitely
  • C. Replan loop — Plan → try → fail → replan, forever
  • D. Completion-check loop — the acceptance criteria are vague, so the agent "feels" like there's still work to do

A and D are the most common, B is a design bug, C is usually a prompt bug.

A. Tool-call loops — the most common type

The classic pattern:

agent: search_docs(query="auth flow")
  -> []
agent: search_docs(query="auth flow")   # retries with identical args
  -> []
agent: search_docs(query="auth flow")   # forever

Two causes. First, the tool's return value is ambiguous — the model can't tell "nothing found" from "temporary failure." Second, there's no guardrail that says "don't call the same tool with the same args."

Fix 1: make the tool return value explicit

def search_docs(query: str) -> dict:
    """Return an explicit status so the model can reason about empties."""
    results = vector_search(query)
    if not results:
        return {
            "status": "empty",
            "query": query,
            "suggestion": "Try a more specific or different keyword.",
            "results": [],
        }
    return {
        "status": "ok",
        "results": [{"title": r.title, "excerpt": r.excerpt[:500]} for r in results],
    }

Returning status: "empty" with a suggestion helps the model pivot. Returning [] usually gets interpreted as "maybe I was unlucky, try again."

Fix 2: a repeat-call guardrail

from collections import deque
import json
 
class ToolCallGuard:
    """Detect identical tool calls within a rolling window."""
 
    def __init__(self, window: int = 5, max_repeats: int = 2):
        self.window = window
        self.max_repeats = max_repeats
        self.recent = deque(maxlen=window)
 
    def check(self, tool_name: str, args: dict) -> bool:
        """Return False if this exact call has already happened too many times."""
        key = (tool_name, json.dumps(args, sort_keys=True))
        count = sum(1 for k in self.recent if k == key)
        if count >= self.max_repeats:
            return False
        self.recent.append(key)
        return True
 
guard = ToolCallGuard()
def safe_call(tool_name, args, tools):
    if not guard.check(tool_name, args):
        return {
            "status": "blocked",
            "reason": f"Tool {tool_name} was called with identical args {args} too many times. Try a different approach.",
        }
    return tools[tool_name](**args)

When the guard blocks, the message to the model should read "you're repeating yourself, pivot." That single feedback line stops most of the loops I've seen.

B. Subagent recursion loops

Antigravity's power partly comes from agents spawning subagents. The flip side: an agent invoking another of its own type recursively gives you an unbounded call chain.

Two defenses. Put a depth cap on agent invocations, and declare explicitly which subagent types may not be invoked from within a given agent.

class AgentContext:
    """Track execution depth and the chain of agent types invoked so far."""
 
    def __init__(self, max_depth: int = 3, call_chain: tuple = ()):
        self.max_depth = max_depth
        self.call_chain = call_chain
 
    def can_invoke(self, agent_type: str) -> tuple[bool, str]:
        if len(self.call_chain) >= self.max_depth:
            return False, f"max agent depth {self.max_depth} reached"
        if agent_type in self.call_chain:
            return False, f"recursion detected ({agent_type} already in chain)"
        return True, ""
 
    def child(self, agent_type: str) -> "AgentContext":
        return AgentContext(self.max_depth, self.call_chain + (agent_type,))

The design goal: "no agent runs at greater depth than the designer intended." Three levels is plenty for a useful system. If you feel the urge to go deeper, the real answer is probably collaboration between peers, not deeper hierarchy.

C. Replan loops

"Decompose, execute, fail, re-decompose, execute, fail…" This is usually a prompt problem. It shows up when:

  • Step-level success criteria are vague, so pass/fail judgment is left to the model
  • Error information from a failed subtask isn't carried into the next plan
  • "Replan" is always available, with no explicit retreat condition

The fix is to count replans and put a hard cap on them.

class PlanController:
    """Manage plan/execute cycles with a replan cap."""
 
    def __init__(self, max_replans: int = 2):
        self.max_replans = max_replans
        self.replan_count = 0
        self.failure_history: list[dict] = []
 
    def can_replan(self) -> bool:
        return self.replan_count < self.max_replans
 
    def record_failure(self, step: str, error: str):
        self.failure_history.append({"step": step, "error": error})
 
    def replan_prompt(self) -> str:
        if not self.can_replan():
            return ("Replan limit reached. Write a final report based on what is complete, "
                    "explicitly listing what remains unfinished.")
        self.replan_count += 1
        past = "\n".join(f"- {h['step']}: {h['error']}" for h in self.failure_history)
        return f"Revise the plan considering these failures:\n{past}"

The point is to provide a graceful retreat: "if you fail twice, wrap up with what's done." Without that, you're asking the model to chase perfection forever.

D. Completion-check loops

When the finish line is fuzzy, the agent will keep feeling there's more to do. The fix is direct — define acceptance criteria up front and make the prompt converge on them.

def build_agent_prompt(task: str, acceptance_criteria: list[str]) -> str:
    criteria_str = "\n".join(f"- {c}" for c in acceptance_criteria)
    return f"""Task: {task}
 
This task is complete when ALL of the following hold:
{criteria_str}
 
Important:
- Call report_complete the moment the criteria are satisfied.
- You do not need to be perfect beyond the criteria.
- If you reach 15 steps without completion, call report_partial with the current state.
"""

Explicit acceptance_criteria remove the "not sure what else to do" limbo. Pair it with a step cap as a safety net.

The three guardrails I always deploy

Beyond design and prompt changes, I always ship these runtime guards:

  1. Total step cap — 25 steps max per task. Over that, force-terminate with an error report
  2. Wall-clock cap — tasks over 5 minutes get cancelled
  3. Per-tool call cap — warn at 8 identical tool uses, block at 10

Log which limit triggered. When loops happen again (and they will), the logs tell you immediately which class you're back in.

Anti-patterns I keep seeing

  • Infinite automatic retries — network-error retries without exponential backoff and a ceiling are a loop in disguise
  • Letting the same model judge its own completion — the executor and the judge should ideally be different models, or the completion check should be a structured rule rather than an LLM call
  • Swallowing error info — if a tool failure surfaces as an empty string, the model has no idea what happened and will try again. Return structured error objects

The first step to take

Agent runaways almost always point to missing constraints, not missing model capability. Start by adding ToolCallGuard and logging whenever it fires. Just that visibility — "my agent called search_docs 11 times in 30 seconds" — will tell you exactly where to intervene. The aim isn't a perfect agent; it's an agent you can confidently stop.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Agents & Manager2026-05-30
When the Antigravity Agent Says 'command not found' for node or python: Causes and Fixes
It works in your own terminal, but the Antigravity agent hits command not found. Starting from how PATH inheritance works, here are concrete fixes for nvm, pyenv, Homebrew, and WSL setups—plus how to confirm the fix actually took.
Agents & Manager2026-05-27
Record & Replay for Antigravity Agents — A Production Pattern to Reproduce Failures in 3 Minutes
How to deterministically replay a failed Antigravity Agent run offline, drawn from a month of running it across four production sites. Covers boundary recording, R2 + KV storage costs, PII masking, and a working TypeScript harness.
Agents & Manager2026-05-26
Why Antigravity's Browser Sub-Agent Reads SPAs as Empty Pages — and Three Wait Strategies That Stuck for Me
When you hand an SPA dashboard to Antigravity's Browser Sub-Agent, get_page_text often returns before the real content is rendered, and the agent reports an empty page. Here is how I diagnose the symptom and the three wait strategies that have stabilized my routine.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →