ANTIGRAVITY LABJP
Articles/Agents & Manager
Agents & Manager/2026-07-25Advanced

The night my agent shipped nothing: giving generation agents an abstain outcome

When you score a background generation agent by how much it produces, the quality gate quietly loosens over time. Here is a three-valued ACCEPT / ABSTAIN / REJECT design that counts a zero-artifact run as a success, with the code and the measurements from running it.

Antigravity341agent design9SLObackground executionquality gate2

Premium Article

A little past 2 a.m., the completion notice for a scheduled run arrived. The payload: "artifacts produced: 0." Back then I treated that as a failure, and my first habit each morning was to start digging into why.

But those zero artifacts were not an error. Candidates had been generated, and the quality gate had rejected every one of them. The gate had done exactly its job. My operations were simply wired around the assumption that zero equals broken.

This article is the record of inverting that assumption. For a background generation agent — the kind you run as an Antigravity 2.0 scheduled task or managed agent, autonomously producing artifacts like posts, assets, or reports — what breaks when you make "how much it produced" the success metric? And how does operations change once the gate can return a third outcome: abstain? I will walk through it with the code I actually run.

Score by throughput, and the gate always loosens

When you first wire up a generation agent, the metric you reach for is artifacts per run. It is easy to chart, and the up-and-down is legible at a glance.

That is where the trap is. The moment production count becomes the success signal, a zero-artifact run becomes a failure. Failures pile up, alerts fire, and the operator quietly reacts. There are two available moves: improve the inputs (topics, context) or loosen the gate. The first takes time; the second is a one-line change.

Under a deadline, tired, people pick the cheap move. Nudge the threshold down. Wave one candidate through "just this once." And so the gate loosens, without anyone intending harm, a little at a time.

The generation pipeline I run as an indie developer, for my own apps, hit exactly this. During the stretch when I chased production count, something shipped almost every night — and some of what shipped passed the syntactic checks while leaving the reader with nothing. Count went up, value thinned out. The metric looked healthy while the system was wasting away.

The fix was to change the design of the metric itself. The target is "net positive, or nothing." Ship only artifacts that provably raise the system's average quality; otherwise ship nothing. A mediocre single artifact is a negative, and zero is zero. Zero ranks above negative — that ordering has to be written into the code.

Make the gate three-valued: ACCEPT / ABSTAIN / REJECT

The starting move is to change the gate's return from binary (pass/fail) to three-valued.

VerdictMeaningContribution to the run
ACCEPTJudged net positive. Safe to publishCommits one artifact
ABSTAINNot bad, but not clearly net positive. Skip it this timeZero artifacts, but not a failure
REJECTRequirement violation, broken, or duplicate. Must not passZero artifacts; target for regeneration

A binary world has no ABSTAIN. So the most common state — "not bad, but not worth shipping" — gets shoved onto the ACCEPT side. The point of going three-valued is to give that middle band a home of its own.

from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
 
 
class Verdict(str, Enum):
    ACCEPT = "accept"
    ABSTAIN = "abstain"
    REJECT = "reject"
 
 
@dataclass
class GateResult:
    verdict: Verdict
    reasons: list[str] = field(default_factory=list)
    signals: dict[str, float] = field(default_factory=dict)
 
 
class AcceptanceGate:
    """Score a candidate three ways. REJECT = violation, ABSTAIN = below net-positive."""
 
    def __init__(self, *, min_utility_signals: int = 3, novelty_floor: float = 0.35):
        self.min_utility_signals = min_utility_signals
        self.novelty_floor = novelty_floor
 
    def evaluate(self, candidate: "Candidate") -> GateResult:
        reasons: list[str] = []
 
        # --- 1) Hard requirements: any one missing -> REJECT (before net-positive) ---
        if candidate.has_broken_frontmatter():
            reasons.append("broken frontmatter (YAML parses as a map, 500s)")
        if candidate.duplicates_existing(threshold=0.90):
            reasons.append("near-identical to an existing artifact (dilution, not gain)")
        if candidate.violates_policy():
            reasons.append("policy violation (banned terms, out-of-scope drift)")
        if reasons:
            return GateResult(Verdict.REJECT, reasons)
 
        # --- 2) Net-positive test: if unmet, ABSTAIN (skip, do not fail) ---
        utility = candidate.count_utility_signals()  # runnable code / measurements / steps...
        novelty = candidate.novelty_score()          # delta against the existing corpus
        signals = {"utility": float(utility), "novelty": novelty}
 
        if utility < self.min_utility_signals:
            reasons.append(f"utility signals short {utility}/{self.min_utility_signals}")
        if novelty < self.novelty_floor:
            reasons.append(f"novelty below floor {novelty:.2f} < {self.novelty_floor}")
 
        if reasons:
            return GateResult(Verdict.ABSTAIN, reasons, signals)
        return GateResult(Verdict.ACCEPT, ["judged net positive"], signals)

There is one thing I deliberately do not do here: auto-promote an ABSTAIN candidate to ACCEPT because "it's so close." Auto-promoting near-misses only invites the looseness back inside the code. Let the middle band be the middle band and skip it honestly. That restraint pays off later.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
A three-valued gate that returns ACCEPT / ABSTAIN / REJECT with reasons, and deliberately refuses to auto-promote near-misses
A run classifier that decouples success from artifact count, plus abstention rate read as a leading indicator
A fail-closed grounding check that abstains when reference data is missing or stale, so unattended nights fail loud instead of silent
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

Agents & Manager2026-07-14
I Changed One Line in AGENTS.md and Broke the Agent — Regression-Testing the Instructions Themselves
Every time you edit the instructions you hand an agent — AGENTS.md, skill text — some other task quietly breaks. Here is a regression harness whose subject is the instructions, not the code: fixed tasks, deterministic assertions, and a way to wire it into unattended runs.
Agents & Manager2026-06-15
Stop Letting Antigravity Agents Self-Report 'Done' — Completion Contracts and External Verification
An Antigravity agent reporting 'done' when the work was not actually finished is a failure mode I kept hitting. Moving the completion decision out of the agent and into code fixed it. Here is the contract, a three-layer verifier, and how it holds up under unattended, scheduled runs.
Agents & Manager2026-05-06
Giving AI Agents an Aesthetic Sense — Building a UI Quality Evaluation Pipeline with Antigravity × Gemini Vision
Explore how to encode the vague judgment of 'is this UI good or bad' into code. Combines Antigravity with Gemini Vision to implement a complete pipeline — from screenshot capture to AI evaluation, improvement suggestions, automated fixes, and CI/CD integration.
📚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 →