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

Building Validation Loops in Antigravity Agents: 3 Patterns for AI-Verified Output

Make your Antigravity agents verify their own work. This guide walks through three practical validation loop patterns — Self-Verifier, Critic-Approver, and Test-Driven — with working code examples in AgentKit 2.0.

antigravity429agents124validation4agentkit13production71

Have you ever asked an Antigravity agent to write a few hundred lines of code, only to discover the next morning that the build was broken the entire time? I have, more than once. The agent says "done," but in reality the type checks were failing, the tests didn't run, and a half-broken commit landed in the branch. No matter how carefully you craft the prompt, as long as you trust the output blindly, this kind of accident never reaches zero. Worse, the longer you've been running an agent unattended, the larger the blast radius when one of these silent failures turns out to be load-bearing.

That is where a validation loop comes in. The pattern is simple: generate, verify, fix on failure, verify again — a cycle the agent itself runs without human intervention. In this article I'll walk through three patterns I rely on in production — Self-Verifier, Critic-Approver, and Test-Driven — with concrete AgentKit 2.0 examples for each.

Why prompt tuning alone isn't enough

Telling the agent "fix it until tests pass" or "make sure there are no type errors" works to some degree. The catch is that nothing in the prompt verifies whether the agent actually did the check. In practice, agents frequently report success while skipping the verification step entirely.

The fix is to separate verification into its own explicit phase that returns a clear pass/fail signal. That's the heart of a validation loop. AgentKit 2.0 exposes this directly through helpers like runWithValidator(), which treat any agent that doesn't return a verdict as "loop incomplete" rather than "done."

Pattern 1: Self-Verifier — write it yourself, check it yourself

The simplest pattern asks the same agent to verify its own output within the same session. Context is shared, so the token cost is low. It's a good fit for lightweight quality gates.

// agents/self-verifier.ts
import { Agent, runWithValidator } from "@google/agentkit";
 
const codeAgent = new Agent({
  model: "gemini-2.5-pro",
  systemPrompt: `You are a TypeScript implementer. After writing code that meets
the requirements, verify it yourself and return JSON shaped like:
{ "code": "...", "checks": { "compiles": bool, "handlesEdgeCases": bool, "hasTests": bool }, "passed": bool }`,
});
 
const result = await runWithValidator(codeAgent, {
  task: "Write a zod schema in TypeScript that validates user input",
  validator: (output) => output.passed === true,
  maxRetries: 3,
});
 
if (!result.success) {
  // Fallback after three failed attempts
  console.error("Self-verification failed:", result.lastOutput.checks);
}

The known weakness: a Self-Verifier tends to be lenient about its own work. In my experience, roughly 20% of outputs that report compiles: true actually fail to build. It's fine for cosmetic checks (naming conventions, comments, obvious typos), but don't lean on it as the final gate for correctness.

Pattern 2: Critic-Approver — let a separate agent judge

The Critic-Approver pattern compensates for the leniency problem by separating the writer and the verifier. The critic can be the same model, but the prompt should cast it as a nitpicky QA reviewer to actually exercise judgment.

// agents/critic-approver.ts
import { Agent } from "@google/agentkit";
 
const writer = new Agent({
  model: "gemini-2.5-pro",
  systemPrompt: "You are an implementer. Write code that meets the requirements.",
});
 
const critic = new Agent({
  model: "gemini-2.5-flash", // a lighter model is plenty for review
  systemPrompt: `You are a strict code reviewer. For the code below, you MUST
list at least three concrete improvements across these dimensions:
- Type safety
- Error handling
- Robustness against unexpected input
End with: { "verdict": "APPROVE" | "REJECT", "reasons": string[] }`,
});
 
let code = await writer.run("Write an OAuth2 token refresh routine");
for (let i = 0; i < 3; i++) {
  const review = await critic.run(`Review this code:\n${code}`);
  if (review.verdict === "APPROVE") break;
  code = await writer.run(
    `Revise the code based on the review.\nCode: ${code}\nIssues: ${review.reasons.join("\n")}`
  );
}

The trick is forcing the critic to produce at least three concerns. Without that floor, the model defaults to "looks fine" and your validation collapses back to a no-op. Set a strong evaluation bar from the start — softening it makes the pattern indistinguishable from Self-Verifier.

Pattern 3: Test-Driven — let machine checks decide

The most reliable approach is to run actual tests — written by you or generated separately — and use the result as the validation signal. Because the verdict doesn't depend on the model's subjective judgment, pass/fail is unambiguous.

# agents/test_driven.py
from agentkit import Agent, run_with_validator
import subprocess
 
writer = Agent(model="gemini-2.5-pro", system="You are a Python implementer.")
 
def run_pytest(code: str) -> dict:
    """Write generated code to a file and verify it with pytest."""
    with open("/tmp/generated.py", "w") as f:
        f.write(code)
    result = subprocess.run(
        ["pytest", "tests/test_generated.py", "-q"],
        capture_output=True, text=True, timeout=60,
    )
    return {
        "passed": result.returncode == 0,
        "stdout": result.stdout[-2000:],  # tail only
        "stderr": result.stderr[-2000:],
    }
 
result = run_with_validator(
    writer,
    task="Write dedupe(items) that removes duplicates while preserving order",
    validator=lambda output: run_pytest(output["code"])["passed"],
    on_retry=lambda output: f"Tests failed. Output:\n{run_pytest(output['code'])['stderr']}",
    max_retries=5,
)

The strength is a clean machine-judged gate: "fix until tests pass." The weakness is that the tests themselves cap your quality. If your suite misses an edge case, the loop will exit successfully on broken code. Treat test design as part of the validator and write the awkward cases on purpose.

How I choose between them

You can blend all three, but starting with one is far easier to operate. My rough rule of thumb:

  • Drafts, design proposals, refactor suggestions — anything a human will review anyway: Self-Verifier is enough.
  • API endpoints, business logic, anything where mistakes have outsized impact: Critic-Approver.
  • Utility functions, data transforms, anything where inputs and outputs are well-defined and testable: Test-Driven.

If you do combine them, I'd recommend Test-Driven as the foundation, with a Critic-Approver on top to catch what tests can't measure (naming, comments, readability). That two-layer setup has held up well in production for me, and the failure-mode coverage tends to be complementary rather than redundant: tests catch what you anticipated, the critic catches what you didn't.

Pitfalls to watch for

Three traps I've fallen into more than once.

First, infinite loops. Always set maxRetries. Agents will happily say "fixed" while making the same mistake five times in a row. AgentKit 2.0's runWithValidator defaults to three attempts; if you're cost-sensitive, two is fine.

Second, validation cost. Critic-Approver doubles the agent calls; Test-Driven adds however long your test suite takes. In my own projects, I push validation-heavy tasks to Antigravity's Background Agents so they don't block whatever I'm working on in the foreground.

Third, runaway critics. A critic with too strict a bar will never approve anything. Add a rule like "if an issue is non-critical, log it as a warning but still approve" — without it, the loop never converges. The pragmatic version of "good code" should match what your team would actually merge in code review, not what a textbook would call ideal.

Logging makes the loop easier to operate

One small habit pays off enormously over time: keep a log of every validation attempt. Recording how many tries each task took, and what the rejection reasons were, gives you a steady stream of prompt-improvement signals. In my projects, I write each step out as JSON Lines via AgentKit's onValidationStep hook.

import { runWithValidator } from "@google/agentkit";
import { appendFile } from "fs/promises";
 
await runWithValidator(writer, {
  task: "...",
  validator: critic,
  maxRetries: 3,
  onValidationStep: async (step) => {
    await appendFile(
      "logs/validation.jsonl",
      JSON.stringify({
        ts: Date.now(), attempt: step.attempt,
        verdict: step.verdict, reasons: step.reasons,
      }) + "\n"
    );
  },
});

Skim that log once a week and patterns jump out: "this task always passes on attempt two — strengthen the initial prompt," or "the same critique keeps recurring — the underlying design guidance is wrong." If you've already wired up Antigravity's Langfuse integration, emitting each validation step as a span makes the analysis even easier.

When not to bother

Validation loops add real cost — both in tokens and in wall-clock time — so don't apply them by default. Skip the loop when the task is exploratory (you're going to read the output anyway), when the agent is producing a creative artifact whose quality is subjective, or when the failure mode is cheap to recover from. The pattern earns its keep on tasks that ship to production and on tasks long enough that you don't want to baby-sit them. A useful mental check before adding a loop: "if this output is wrong, who finds out, and when?" If the answer is "me, immediately, while reading it," skip it. If the answer is "a customer, three days later, in a Slack DM," wire one in.

A real example: validating a webhook handler

To make the trade-offs concrete, here's a case from one of my own projects. I needed an Antigravity agent to write a Stripe webhook handler that verified signatures, dispatched events to the right service method, and gracefully handled replays. I tried each pattern in order and watched what happened.

With Self-Verifier alone, the first attempt looked correct but silently swallowed the signature_invalid exception. The agent reported passed: true because every check it had defined for itself had passed — the problem was that "do not silently swallow errors" wasn't on its self-check list. Self-Verifier doesn't know what it doesn't know.

Adding Critic-Approver caught it on the second iteration. The critic, prompted to look at error handling specifically, flagged the swallowed exception and the writer fixed it. Total cost: about 1.7x of the single-shot generation, with a noticeably more robust handler.

Test-Driven would have caught it too, but only if my test suite had a "what happens when signature verification fails" case. That's the trap: Test-Driven gives you a strong gate, but only over the surface area you've thought to test. Critic-Approver, by contrast, opens up dimensions you didn't explicitly enumerate — at the cost of being less deterministic.

In production, that handler now uses Test-Driven as the primary gate, with a one-shot Critic pass after it. If the critic flags something the tests missed, I treat that as a sign that the test suite needs a new case rather than ignoring the warning.

Start with Self-Verifier on one task

The concept is simple, but implementations have unexpected corners. Rather than rolling out all three patterns at once, drop the lightest one — Self-Verifier — into a single task and watch the gap between what the agent self-reports and what's actually true. If that gap is large, that's your signal to graduate to Critic-Approver or Test-Driven.

If you want to dig deeper into validation design, Designing completion verification for Antigravity agents and Turning agent failure history into a learning loop cover the operational side. For broader background on model behavior that informs these patterns, Designing Machine Learning Systems by Chip Huyen is a useful reference for how production-grade AI systems handle reliability constraints.

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-07-05
Protecting Your Agent Stack's Known-Good State with a Single Lockfile — Change-Budget Design for an Era of Simultaneously Moving Parts
When the IDE build, CLI, model, and dependencies all move at once, you can no longer tell which one caused a regression. Here is a change-budget design that pins your known-good state to one lockfile, with working code and operational logs.
Agents & Manager2026-06-15
Containing Failure in Antigravity Multi-Agent Systems: Three Boundaries That Stop Cascades
Antigravity multi-agent setups run beautifully in isolation but cascade in production, where one small failure drags the whole orchestration down. These notes organize the fix around three boundaries—layered control, trust separation, and observability with idempotency—down to the TOML and the correlation-ID wrapper.
Agents & Manager2026-05-29
Supervising Long-Running Antigravity Agents — Watchdog and Tiered Recovery
Eight weeks of running AdMob revenue optimization on Antigravity background agents revealed three quiet failure modes. Here is the watchdog plus tiered recovery design I landed on.
📚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 →