DEV Community

Esther Irawati Setiawan
Esther Irawati Setiawan

Posted on

Ground Control: From Google AI Studio Prototype to Local Production with Antigravity 2.0

Your multi-agent system demos beautifully in the cloud sandbox. Here's the one-click lifecycle export, the CLI bridge, and the SDK harness that land it in a production codebase — without rewriting a single prompt.


This guide targets Antigravity 2.0 — the
four-surface release (desktop app, agy CLI, google-antigravity SDK, and
enterprise cloud) that shares one agent harness — together with the AI Studio
lifecycle export
that shipped alongside it. The SDK is pre-v1.0; symbol
names, bundle fields, and CLI flags below reflect the documented API as of
mid-2026. Treat the patterns as stable and re-check exact signatures against
the current docs before you ship.

Table of contents

  1. The prototype trap
  2. Architectural overview: what a "lifecycle" actually is
  3. Step 1 — Building the prototype in Google AI Studio
  4. Step 2 — The one-click lifecycle export
  5. Step 3 — Scaffolding the local environment with the Antigravity CLI
  6. Step 4 — Wiring for production with the Antigravity SDK
  7. Conclusion

1. The prototype trap

Every AI engineering team knows this moment. Someone spends an afternoon in
Google AI Studio and comes back with something genuinely impressive: a
multi-agent app that classifies incoming support tickets, investigates the
customer's history, checks refund policy, and drafts a resolution — with typed
handoffs between agents and a sandbox transcript to prove it works. The demo
kills. Leadership asks the only question leadership ever asks: "When can this
be in production?"

And then the project stalls, because between the sandbox and production sits a
canyon. On one side: a cloud playground optimized for iteration speed — prompts
in text boxes, tools declared in a UI, model parameters behind a slider, test
runs you eyeball. On the other side: everything production actually demands —
version control, typed contracts, security policy, audit logs, CI, and tool
implementations that hit your databases, not a mock.

Historically, teams escaped the canyon in one of two bad ways:

  • Ship the playground. Wrap the sandbox in an iframe or hit it through an ad-hoc API. Fast, and fragile: no code review on prompt changes, no policy enforcement, no way to diff what changed when quality regresses on a Tuesday.
  • Rewrite from screenshots. An engineer reconstructs the prototype by hand — copy-pasting prompts, re-guessing the temperature, re-typing tool schemas. The rewrite is lossy by construction. Three weeks later the local version behaves differently from the prototype everyone approved, and nobody can say why.

Both failure modes share a root cause: the prototype was never an artifact.
It was a pile of UI state, and UI state doesn't survive contact with a
repository.

Antigravity 2.0 closes the canyon from both ends. AI Studio's lifecycle
export
turns the entire prototype — agent architecture, prompts, tool
configurations, handoff schemas, generation parameters, even your sandbox test
transcripts — into a versionable bundle. The agy CLI ingests that bundle
into a local IDE workspace and verifies it. And the google-antigravity
SDK
runs the exported lifecycle inside the same harness that powered the
sandbox — so the thing you ship is, provably, the thing you demoed.

This tutorial walks the full bridge, end to end, with a real workload:
TriageDesk, a four-agent support-ticket triage system. We'll prototype it
in AI Studio, export it in one click, scaffold a local workspace with exact
terminal commands, and then wire it into a production execution harness with
typed contracts, default-deny policy, and a parity gate that replays the
sandbox transcripts as regression tests.

2. Architectural overview: what a "lifecycle" actually is

The word doing the heavy lifting in "lifecycle export" is lifecycle. It is
not a chat log, and it is not "the prompt." An agent lifecycle is the complete,
declarative description of a multi-agent system — everything needed to
reproduce its behavior, and nothing tied to the surface it happens to run on:

Lifecycle component In the AI Studio sandbox In your local codebase
Agent architecture Nodes on the app canvas lifecycle.json manifest (agents + edges)
Prompts System-instruction text boxes prompts/*.md, one file per agent, checksummed
Tool configurations Function declarations in the UI tools/tools.json (interfaces) + your Python implementations
Handoff contracts Structured-output schemas schemas/handoffs.json → Pydantic models
Generation parameters Temperature / top-p sliders Pinned in the manifest, applied by the harness
Evidence Sandbox test runs you eyeballed eval/golden.jsonl → replayable parity tests

Read that table column by column and you can see the whole thesis of this
article: every piece of sandbox state has a canonical home in a repository.
The export is a projection from one column to the other. Nothing is
reconstructed from memory; nothing is lossy.

   GOOGLE AI STUDIO (cloud sandbox)                LOCAL WORKSPACE (production)
  ┌──────────────────────────────┐               ┌──────────────────────────────┐
  │  canvas: 4 agents, 2 edges   │               │  bridge/harness.py   policy  │
  │  prompts in text boxes       │   1-click     │  bridge/lifecycle.py loader  │
  │  tool declarations (UI)      │   export      │  bridge/tools.py     impls   │
  │  output schemas (UI)         │ ───────────▶  │  bridge/schemas.py   typed   │
  │  temp/top-p sliders          │  .agy bundle  │  bridge/orchestrator.py      │
  │  sandbox test transcripts    │               │  bridge/parity.py    gate    │
  └──────────────────────────────┘               └──────────────────────────────┘
                 ▲                                              │
                 │            agy studio pull / diff            │
                 └──────────────── parity ◀────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Environment parity is the whole game

The reason "rewrite from screenshots" fails isn't laziness — it's that agent
behavior is absurdly sensitive to details humans don't transcribe faithfully.
A prompt that lost a trailing instruction in copy-paste. A temperature of 0.7
where the sandbox ran 0.2. A tool schema where priority silently became a
string instead of an enum. Each one changes behavior; none of them shows up in
a code review, because there's no "before" to diff against.

So we treat parity as a contract with four clauses, each mechanically
checkable:

  1. Prompt parity — the exported bundle carries a SHA-256 checksum per prompt file; the local loader refuses to start if a byte drifted.
  2. Parameter parity — model name and generation parameters live in the manifest, not in your code. The harness applies them; nobody re-guesses a slider.
  3. Interface parity — tool declarations are exported; your local implementations are validated against them at load time. Signature drift is a startup error, not a runtime surprise.
  4. Behavioral parity — the sandbox transcripts you tested with ship in the bundle, and a replay harness runs them against the local system. If the local Resolution disagrees with the sandbox Resolution on a golden ticket, the deploy gate fails.

Everything we build in Steps 3 and 4 exists to enforce one of those four
clauses. Keep the table and the contract in your head; the rest is
implementation.

3. Step 1 — Building the prototype in Google AI Studio

This is a tutorial about leaving the sandbox, so we'll keep the sandbox part
brief — but the shape of the prototype matters, because a well-structured
prototype exports cleanly and a mushy one doesn't.

In AI Studio, create a new multi-agent app and lay out four agents:

  • Classifier — reads the raw ticket text; no tools. Emits a TicketClass: category (billing / technical / account / other), severity, and a one-line summary.
  • Investigator — receives the ticket and its TicketClass; declares two function tools, search_tickets (prior tickets from this customer) and read_account (plan, billing history). Emits an Investigation.
  • Policy Checker — receives the same inputs in parallel with the Investigator; declares one tool, read_policy. Emits a PolicyVerdict — what the refund/SLA policy permits for this category.
  • Resolver — no tools. Receives the Investigation and PolicyVerdict and emits the final Resolution: an action, a customer-facing reply draft, and an escalate flag with rationale.

Wire the edges on the canvas: Classifier fans out to Investigator and Policy
Checker (parallel branch), which join into Resolver. A diamond.

Three habits at this stage pay for themselves at export time:

  • Declare structured output on every agent. In the sandbox it feels optional — you can read prose. But the schemas you attach in the UI are exactly what becomes schemas/handoffs.json in the bundle, and typed handoffs are what make the local orchestrator deterministic. An agent without an output schema exports as a prose-emitter, and you'll pay for it later with regex.
  • Declare tools honestly, even though the sandbox mocks them. AI Studio lets you stub tool responses by hand while prototyping. Do it — but keep the declarations (names, parameters, types, descriptions) production-true, because the declarations are what export. The mocks stay behind; the interfaces travel.
  • Test like you mean it, in the sandbox. Run a dozen realistic tickets through the app — the double-charge, the angry enterprise admin, the password reset that's actually an account takeover. Every sandbox run you mark as approved is captured, and in Step 2 those become eval/golden.jsonl: your regression suite, for free.

Tune the temperature down to 0.2 for the Classifier and Policy Checker (you
want determinism), leave the Resolver a little warmer for reply drafting, and
iterate until the diamond behaves. That's the prototype. Total UI time: an
afternoon. Now let's make it an artifact.

4. Step 2 — The one-click lifecycle export

In the app's toolbar: Deploy ▸ Export lifecycle. AI Studio bundles the
entire app definition into a single archive — triagedesk.agy.zip — and offers
two delivery paths: a direct download, or a push to your project's export
registry that the CLI can pull by app ID (we'll use both in Step 3).

Unzip it and look around; this bundle is the contract between the two worlds:

triagedesk.agy.zip
├── lifecycle.json            # the manifest — architecture, params, checksums
├── prompts/
│   ├── classifier.md         # byte-exact system instructions, one per agent
│   ├── investigator.md
│   ├── policy_checker.md
│   └── resolver.md
├── tools/
│   └── tools.json            # function DECLARATIONS (JSON Schema) — no impls
├── schemas/
│   └── handoffs.json         # structured-output contracts between agents
└── eval/
    └── golden.jsonl          # your approved sandbox runs: input → typed output
Enter fullscreen mode Exit fullscreen mode

The manifest is the piece worth reading line by line:

{
  "format": "antigravity.lifecycle/v1",
  "name": "triagedesk",
  "exported_from": "aistudio://apps/triagedesk-8f3a21",
  "exported_at": "2026-07-08T14:32:00Z",
  "model_defaults": {
    "model": "gemini-3-pro",
    "generation": { "temperature": 0.2, "top_p": 0.95, "max_output_tokens": 2048 }
  },
  "agents": [
    { "id": "classifier",
      "prompt": "prompts/classifier.md",
      "tools": [],
      "output_schema": "schemas/handoffs.json#/TicketClass" },
    { "id": "investigator",
      "prompt": "prompts/investigator.md",
      "tools": ["search_tickets", "read_account"],
      "output_schema": "schemas/handoffs.json#/Investigation" },
    { "id": "policy_checker",
      "prompt": "prompts/policy_checker.md",
      "tools": ["read_policy"],
      "output_schema": "schemas/handoffs.json#/PolicyVerdict" },
    { "id": "resolver",
      "prompt": "prompts/resolver.md",
      "tools": [],
      "generation": { "temperature": 0.6 },
      "output_schema": "schemas/handoffs.json#/Resolution" }
  ],
  "edges": [
    { "from": "classifier", "to": ["investigator", "policy_checker"], "mode": "parallel" },
    { "from": ["investigator", "policy_checker"], "to": "resolver", "mode": "join" }
  ],
  "checksums": {
    "prompts/classifier.md":     "sha256:1f9c2e…",
    "prompts/investigator.md":   "sha256:aa07d1…",
    "prompts/policy_checker.md": "sha256:83b4f0…",
    "prompts/resolver.md":       "sha256:c95a72…"
  }
}
Enter fullscreen mode Exit fullscreen mode

What to notice, because each field maps to a parity clause from Section 2:

  • model_defaults + per-agent generation overrides are parameter parity. The Resolver's warmer temperature (0.6) travels with the bundle. No one will ever re-guess a slider value.
  • checksums are prompt parity. These aren't decorative — the local loader we build in Step 4 verifies them and refuses to boot on a mismatch.
  • agents[].tools + tools/tools.json are interface parity. Note what's not in the bundle: tool implementations and secrets. That's deliberate, and it's the single most important design decision in the whole export format. AI Studio exports the interface; production supplies the implementation. Your sandbox mocked read_account with hand-typed JSON; your production read_account will hit the real billing database with real credentials that never touch a bundle file.
  • edges are the architecture itself. The diamond you drew on the canvas is now data — reviewable in a pull request, diffable across exports.
  • eval/golden.jsonl is behavioral parity: every approved sandbox run, serialized as {agent_inputs, expected_output} pairs.

One click, and the prototype stopped being UI state. It's an artifact. Now we
bring it home.

5. Step 3 — Scaffolding the local environment with the Antigravity CLI

Everything in this section is exact terminal commands. First, the two surfaces
you'll use locally — the CLI (agy, the Go-based terminal harness) and the
SDK (google-antigravity, the same harness as a Python library):

# 1. Install the Antigravity CLI (macOS / Linux). It lands at ~/.local/bin/agy
curl -fsSL https://antigravity.google/cli/install.sh | bash

# Windows PowerShell:  irm https://antigravity.google/cli/install.ps1 | iex

# 2. Confirm the harness is live and see which models you can reach
agy models

# 3. Authenticate. Either a direct Gemini key…
export GEMINI_API_KEY="your_key_here"
#    …or route through Vertex AI with Application Default Credentials:
#    gcloud auth application-default login   # then set VERTEX=1 in .env
Enter fullscreen mode Exit fullscreen mode

Scaffold the workspace

agy init with the studio-import template creates a workspace pre-shaped for
an ingested lifecycle — an export/ directory the bundle lands in, a bridge/
package for the production wiring, and a .agy/ config that tells the harness
where the manifest lives:

# 4. Scaffold the local IDE workspace
agy init triagedesk --template studio-import
cd triagedesk

# 5. Python environment + SDK. IMPORTANT: install the published wheel, not a
#    source checkout — the wheel ships the `localharness` binary the SDK drives.
python -m venv .venv && source .venv/bin/activate
pip install google-antigravity pydantic python-dotenv
Enter fullscreen mode Exit fullscreen mode

Ingest the export

Two equivalent paths. If you downloaded the zip:

# 6a. Ingest the downloaded bundle into ./export/triagedesk/
agy studio import ~/Downloads/triagedesk.agy.zip
Enter fullscreen mode Exit fullscreen mode

Or skip the download entirely and pull from AI Studio's export registry by app
ID — this is the path you'll automate later, because it means "re-sync the
prototype" is one non-interactive command:

# 6b. …or pull the latest export straight from AI Studio
agy studio pull --app triagedesk-8f3a21
Enter fullscreen mode Exit fullscreen mode

Either way, the CLI unpacks the bundle, records its provenance (exported_from,
exported_at) in .agy/lifecycle.lock, and your workspace now looks like this:

triagedesk/
├── .agy/
│   └── lifecycle.lock        # provenance pin: which export this workspace tracks
├── export/
│   └── triagedesk/           # the ingested bundle, verbatim (treat as read-only)
│       ├── lifecycle.json
│       ├── prompts/  tools/  schemas/  eval/
├── bridge/                   # YOUR code — the production wiring (Step 4)
├── main.py
└── requirements.txt
Enter fullscreen mode Exit fullscreen mode

The discipline embedded in that layout: export/ is read-only, regenerated
by re-import; bridge/ is yours, written once.
Prompt changes happen in AI
Studio (or in a future export), never by editing export/ in place — that
keeps the checksums honest and the provenance chain unbroken.

Validate, diff, smoke-test

Three commands before you write any Python:

# 7. Validate the ingested lifecycle: manifest schema, checksums, schema refs,
#    edge graph is acyclic, every declared tool has a JSON Schema entry
agy lifecycle validate

# 8. Parity check: has the cloud app drifted since this export?
agy lifecycle diff --against studio
#    → "in sync with aistudio://apps/triagedesk-8f3a21 (exported 2026-07-08)"
#    …or a unified diff of any prompt/param/schema that changed upstream

# 9. Smoke-test the lifecycle in the local harness — sandbox-mocked tools,
#    no production wiring yet. Proves the bundle itself is runnable.
agy run --lifecycle export/triagedesk/lifecycle.json \
    --mock-tools export/triagedesk/eval/golden.jsonl \
    -p "Ticket: 'I was charged twice for my Pro plan this month.'" \
    --output-format json
Enter fullscreen mode Exit fullscreen mode

Unpacking the flags on that last command, because it's doing something subtle:

  • --lifecycle points the harness at the manifest — the CLI builds the whole diamond (four agents, parallel branch, join) from data. You have written zero orchestration code and you're already running the system.
  • --mock-tools replays tool responses from the golden transcripts, the same way the sandbox mocked them. This isolates the question "did the lifecycle survive the trip?" from "are my production tools wired right?" — two failure domains you never want to debug simultaneously.
  • --output-format json emits the typed Resolution on stdout, pipeable into jq or CI.

If step 9 prints the same Resolution the sandbox produced for that ticket,
the bridge held: the prototype is now running on your laptop, byte-identical
prompts, pinned parameters, same harness. What it doesn't have yet is
production — real tools, real policy, real telemetry. That's Step 4.

6. Step 4 — Wiring for production with the Antigravity SDK

The CLI proved the lifecycle is portable. The SDK is where it becomes
deployable: a Python package (bridge/) that loads the exported manifest
into governed agent configs, binds real tool implementations against the
exported interfaces, drives the diamond with typed handoffs, and gates deploys
behind a golden-transcript replay.

6.1 The Shared Agent Harness — policy and telemetry, defined once

Every agent in the lifecycle gets minted through one factory with one set of
guardrails. Prompts and parameters come from the bundle; policy and
observability come from here. That separation is the production boundary in
one sentence: AI Studio decides what the agents say; your harness decides
what they're allowed to do.

# bridge/harness.py
import os
from typing import Callable, Sequence

from google.antigravity import LocalAgentConfig
from google.antigravity.hooks import hooks, policy

# Shared guardrails — least privilege, encoded once, inherited by every agent
# the lifecycle loader mints. The exported bundle can never widen these.
BASE_POLICIES = [
    policy.allow("search_tickets"),
    policy.allow("read_account"),
    policy.allow("read_policy"),
    policy.deny("run_command"),   # a triage agent never shells out
    policy.deny("write_file"),    # …or writes to disk
    policy.deny("*"),             # default-deny: anything undeclared is refused
]

class AuditTrailHook(hooks.PostToolCallHook):
    """One structured audit line per tool call, in every agent of the diamond."""
    async def run(self, context: hooks.HookContext, data) -> None:
        print(
            f"[audit] trajectory={getattr(context, 'trajectory_id', '?')} "
            f"tool={getattr(data, 'name', '?')} ok={getattr(data, 'ok', True)}"
        )

def build_agent_config(
    *,
    system_instructions: str,
    tools: Sequence[Callable] = (),
    model: str,
    generation: dict | None = None,
) -> LocalAgentConfig:
    kwargs = dict(
        model=model,
        system_instructions=system_instructions,
        tools=list(tools),
        policies=BASE_POLICIES,      # the production floor — non-negotiable
        hooks=[AuditTrailHook()],    # telemetry on every node
    )
    if generation:
        kwargs["generation"] = dict(generation)   # parameter parity, applied
    if os.getenv("VERTEX") == "1":
        kwargs["vertex"] = True
    elif os.getenv("GEMINI_API_KEY"):
        kwargs["api_key"] = os.environ["GEMINI_API_KEY"]
    return LocalAgentConfig(**kwargs)
Enter fullscreen mode Exit fullscreen mode

The design decision that matters: BASE_POLICIES is not part of the
export, on purpose. Policy is an attribute of the environment, not the
prototype — the same lifecycle might run with generous policies in staging
and airtight ones in production. The bundle can name tools; only the harness
can grant them.

6.2 Tool binding — exported interfaces, production implementations

The bundle shipped tools/tools.json: three function declarations with full
JSON Schema parameters. Locally, you write the real implementations and
validate them against the exported declarations at import time. This is
interface parity as executable code — if AI Studio exported
read_account(customer_id: string) and you wrote
read_account(account_id: str), you find out at startup, not in an incident
review.

# bridge/tools.py
import inspect
import json
from pathlib import Path

BUNDLE = Path(__file__).parent.parent / "export" / "triagedesk"

def search_tickets(customer_id: str, days: int = 90) -> str:
    """Return this customer's tickets from the last `days` days, as JSON."""
    from .backends import ticket_store            # real system, injected here
    return json.dumps(ticket_store.recent(customer_id, days=days))

def read_account(customer_id: str) -> str:
    """Return the customer's plan, billing history, and account flags."""
    from .backends import billing
    return json.dumps(billing.account_summary(customer_id))

def read_policy(category: str) -> str:
    """Return the support policy text applicable to the given ticket category."""
    from .backends import policy_docs
    return policy_docs.for_category(category)

TOOL_REGISTRY = {
    "search_tickets": search_tickets,
    "read_account": read_account,
    "read_policy": read_policy,
}

def validate_registry() -> None:
    """Interface parity: every exported declaration must have a local impl
    whose signature matches the exported JSON Schema. Fail at startup."""
    declared = json.loads((BUNDLE / "tools" / "tools.json").read_text())
    for decl in declared["functions"]:
        impl = TOOL_REGISTRY.get(decl["name"])
        if impl is None:
            raise RuntimeError(f"exported tool '{decl['name']}' has no local implementation")
        got = set(inspect.signature(impl).parameters)
        want = set(decl["parameters"]["properties"])
        if got != want:
            raise RuntimeError(
                f"signature drift on '{decl['name']}': bundle declares {sorted(want)}, "
                f"local implementation has {sorted(got)}"
            )
Enter fullscreen mode Exit fullscreen mode

Line by line, the load-bearing parts:

  • The implementations import real backends (ticket_store, billing, policy_docs). This is the code the sandbox couldn't have — and the only code in the whole system you wrote from scratch. Everything else came over the bridge.
  • validate_registry() compares Python signatures to exported JSON Schema properties. Cheap, dumb, and it catches the exact class of drift that otherwise produces "the agent keeps passing account_id and the tool keeps erroring" three weeks after launch.

6.3 The lifecycle loader — the bundle becomes agent configs

Now the centerpiece: a loader that reads lifecycle.json and mints one
governed LocalAgentConfig per agent — verifying prompt checksums as it goes.
This function is the bridge.

# bridge/lifecycle.py
import hashlib
import json
from pathlib import Path

from google.antigravity import LocalAgentConfig

from .harness import build_agent_config
from .tools import BUNDLE, TOOL_REGISTRY, validate_registry

class ParityError(RuntimeError):
    """The local bundle no longer matches what AI Studio exported."""

def _verify_checksum(root: Path, rel: str, expected: str) -> None:
    digest = hashlib.sha256((root / rel).read_bytes()).hexdigest()
    if f"sha256:{digest}" != expected:
        raise ParityError(
            f"prompt drift in {rel}: bundle expects {expected[:19]}…, "
            f"file hashes to sha256:{digest[:12]}…. Re-import the export; "
            f"never hand-edit export/."
        )

def load_lifecycle(bundle: Path = BUNDLE) -> dict[str, LocalAgentConfig]:
    manifest = json.loads((bundle / "lifecycle.json").read_text())
    validate_registry()                              # interface parity gate

    defaults = manifest["model_defaults"]
    configs: dict[str, LocalAgentConfig] = {}
    for spec in manifest["agents"]:
        _verify_checksum(bundle, spec["prompt"],     # prompt parity gate
                         manifest["checksums"][spec["prompt"]])
        configs[spec["id"]] = build_agent_config(
            system_instructions=(bundle / spec["prompt"]).read_text(),
            tools=[TOOL_REGISTRY[name] for name in spec["tools"]],
            model=spec.get("model", defaults["model"]),
            generation={**defaults.get("generation", {}),
                        **spec.get("generation", {})},   # parameter parity
        )
    return configs
Enter fullscreen mode Exit fullscreen mode

Walk it:

  • validate_registry() runs before any agent is built — a bundle whose interfaces you haven't implemented is unloadable, full stop.
  • _verify_checksum on every prompt. The ParityError message tells the operator the correct remediation (re-import) rather than the tempting one (edit the hash). Guardrails should teach.
  • `{defaults, spec.get("generation", {})}` — per-agent overrides merged over model defaults, exactly the semantics the manifest promised. The Resolver runs at 0.6 locally because it ran at 0.6 in the sandbox, and no human ever re-typed that number.
  • The return type — dict[str, LocalAgentConfig] — means the rest of the system addresses agents by their exported IDs (classifier, investigator, …). The names on the AI Studio canvas are now the names in your stack traces. Small thing; enormous debugging dividend.

6.4 Typed handoffs — the exported schemas as Pydantic contracts

The bundle's schemas/handoffs.json defines what flows along each edge. In
Python, those become Pydantic models — the same contracts, now enforced by the
harness's structured-output machinery:

# bridge/schemas.py
from enum import Enum
from pydantic import BaseModel, Field

class Category(str, Enum):
    BILLING = "billing"; TECHNICAL = "technical"; ACCOUNT = "account"; OTHER = "other"

class TicketClass(BaseModel):
    category: Category
    severity: int = Field(ge=1, le=4)          # 1 = critical … 4 = low
    summary: str

class Investigation(BaseModel):
    customer_id: str
    prior_tickets: int
    account_standing: str
    relevant_facts: list[str]

class PolicyVerdict(BaseModel):
    policy_section: str
    permitted_actions: list[str]
    requires_human_approval: bool

class Resolution(BaseModel):
    action: str
    reply_draft: str
    escalate: bool
    rationale: str
Enter fullscreen mode Exit fullscreen mode

6.5 The orchestrator — driving the exported diamond

The manifest's edges said: classify, then fan out in parallel, then join.
The production orchestrator encodes exactly that — and nothing else. All the
intelligence lives in the exported prompts; this file is just the topology,
made explicit and typed.

# bridge/orchestrator.py
import asyncio
import os
from typing import Type, TypeVar

from google.antigravity import Agent
from pydantic import BaseModel

from .lifecycle import load_lifecycle
from .schemas import Investigation, PolicyVerdict, Resolution, TicketClass

T = TypeVar("T", bound=BaseModel)

AGENTS = load_lifecycle()          # parity gates run HERE, at import time

async def run_once(config, prompt: str, schema: Type[T]) -> T:
    """One isolated agent, one typed turn, then tear down."""
    async with Agent(config) as agent:
        resp = await agent.chat(prompt, response_schema=schema)
        return await resp.parsed()               # a validated Pydantic instance

async def triage(ticket_text: str, customer_id: str) -> Resolution:
    # Node 1 — classify (no tools, cold temperature, deterministic)
    tclass = await run_once(
        AGENTS["classifier"],
        f"Classify this support ticket.\n\nTICKET:\n{ticket_text}",
        TicketClass,
    )

    # Nodes 2 & 3 — the parallel branch from the manifest's edges.
    # Two isolated contexts, launched at the same instant.
    shared = (
        f"TICKET:\n{ticket_text}\n\ncustomer_id: {customer_id}\n"
        f"classification: {tclass.model_dump_json()}"
    )
    investigation, verdict = await asyncio.gather(
        run_once(AGENTS["investigator"],
                 f"Investigate. Use your tools.\n\n{shared}", Investigation),
        run_once(AGENTS["policy_checker"],
                 f"Determine what policy permits.\n\n{shared}", PolicyVerdict),
    )

    # Node 4 — the join. Tool-less: the resolution must be a pure function of
    # the two typed findings, which is what makes it auditable.
    return await run_once(
        AGENTS["resolver"],
        "Draft the resolution from these independent findings.\n\n"
        f"Investigation: {investigation.model_dump_json(indent=2)}\n"
        f"PolicyVerdict: {verdict.model_dump_json(indent=2)}",
        Resolution,
    )

async def triage_batch(tickets: list[tuple[str, str]]) -> list[Resolution]:
    limit = int(os.getenv("TRIAGE_CONCURRENCY", "8"))
    sem = asyncio.Semaphore(limit)               # bound live model sessions

    async def _guarded(t: tuple[str, str]) -> Resolution:
        async with sem:
            return await triage(*t)

    return await asyncio.gather(*(_guarded(t) for t in tickets))
Enter fullscreen mode Exit fullscreen mode

The parts worth reading twice:

  • AGENTS = load_lifecycle() at module import. Every parity gate — prompt checksums, interface validation — fires before the first request is served. A drifted deployment crashes at boot with a ParityError naming the exact file, instead of serving subtly-wrong triage for a week.
  • run_once opens a fresh Agent per node. async with Agent(config) is a full harness session with its own isolated context window. The Investigator's tool output never pollutes the Policy Checker's reasoning — the same state isolation the sandbox gave the diamond, reproduced locally.
  • asyncio.gather is the manifest's "mode": "parallel" edge, verbatim. Latency for the middle of the diamond is max(investigate, policy), not the sum — identical to how the AI Studio canvas ran it.
  • response_schema=schema on every turn. If the model emits malformed JSON, the harness retries the model, not your code; resp.parsed() hands back a validated instance. Fan-in stays deterministic.
  • The semaphore in triage_batch is the entire scale story: Monday morning's 3,000-ticket backlog never opens 3,000 concurrent model sessions.

6.6 The parity gate — golden transcripts as a deploy check

Last clause of the contract: behavioral parity. The bundle's eval/golden.jsonl
holds every sandbox run you approved. Replay them through the local system —
real harness, real policy, mocked tools (so backends don't flake the gate) —
and compare the structured fields that matter:

# bridge/parity.py
import asyncio
import json

from .orchestrator import triage
from .tools import BUNDLE

async def replay_golden() -> tuple[int, int]:
    """Replay approved sandbox transcripts against the local lifecycle.
    Returns (passed, total). Wire this into CI as the deploy gate."""
    passed = total = 0
    for line in (BUNDLE / "eval" / "golden.jsonl").read_text().splitlines():
        case = json.loads(line)
        total += 1
        local = await triage(case["ticket"], case["customer_id"])
        expected = case["expected"]                     # sandbox Resolution
        ok = (local.action == expected["action"]
              and local.escalate == expected["escalate"])
        passed += ok
        print(f"[parity] {case['id']}: {'PASS' if ok else 'FAIL'}")
    return passed, total

if __name__ == "__main__":
    p, t = asyncio.run(replay_golden())
    raise SystemExit(0 if p == t else 1)                # CI-friendly exit code
Enter fullscreen mode Exit fullscreen mode

Note what we compare: action and escalate — the decisions — not
reply_draft, which is legitimately creative at temperature 0.6. A parity gate
that string-matches prose will cry wolf until someone deletes it; a gate that
checks decisions earns its place in CI. python -m bridge.parity exits nonzero
on any regression, which makes the deploy pipeline one line:

agy lifecycle diff --against studio && python -m bridge.parity && ./deploy.sh
Enter fullscreen mode Exit fullscreen mode

Read that line back slowly, because it's the whole article: the cloud hasn't
drifted from the bundle, the local system still behaves like the approved
sandbox runs, therefore ship.
Three worlds — AI Studio, the repository, and
production — held equal by two commands.

6.7 Run it

# main.py
import asyncio, json, sys
from bridge.orchestrator import triage

async def main():
    ticket = sys.stdin.read().strip() or "I was charged twice for my Pro plan."
    resolution = await triage(ticket, customer_id="cus_4821")
    print(json.dumps(resolution.model_dump(), indent=2))

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode
$ echo "I was charged twice for my Pro plan this month." | python main.py
[audit] trajectory=tj_01 tool=search_tickets ok=True
[audit] trajectory=tj_01 tool=read_account ok=True
[audit] trajectory=tj_02 tool=read_policy ok=True
{
  "action": "refund_duplicate_charge",
  "reply_draft": "Hi — you're right, and I'm sorry: our records confirm a duplicate charge…",
  "escalate": false,
  "rationale": "Billing history confirms duplicate charge on 2026-07-02; policy §4.2 permits immediate refund of verified duplicates without approval."
}
Enter fullscreen mode Exit fullscreen mode

Same prompts as the sandbox, byte-for-byte. Same parameters, applied by the
harness. Same diamond, now with real tools, default-deny policy, an audit line
per tool call, and a regression suite that was generated by using the
prototype
. The distance from demo to this was four files of wiring — and none
of them contain a prompt.

7. Conclusion

The canyon between "look what I built in AI Studio" and "it's in production"
was never really about code. It was about state that lived in a UI — and
every team that lost a week reconstructing a prototype from screenshots, or
shipped a playground behind a proxy and prayed, was paying interest on that
one architectural debt.

Antigravity 2.0's answer is to make the prototype an artifact with a defined
projection into a repository. The lifecycle export captures architecture,
prompts, tool interfaces, handoff schemas, parameters, and evidence in one
bundle. The agy CLI makes ingesting, validating, and drift-checking that
bundle a three-command routine. And the SDK gives the exported lifecycle
what a sandbox never could: real tool implementations behind validated
interfaces, default-deny policy, audit telemetry, typed fan-out/fan-in, and a
parity gate that turns your sandbox test runs into a CI regression suite.

The deeper shift is organizational. When export is one click and ingestion is
one command, the prototype stops being a throwaway and becomes the first
commit
— product-minded builders iterate in AI Studio, engineers govern the
bridge, and the two sides diff against the same manifest instead of arguing
from memory. Time-to-production stops being measured in rewrite-weeks and
starts being measured in the time it takes to implement three tool functions
honestly.

Prototype where iteration is cheapest. Productionize where governance is
strongest. With a checksummed bridge between them, you no longer have to
choose — and the next time leadership asks "when can this be in production?",
the honest answer is: it already compiled.


Acknowledgement

Google Cloud Credits are provided for this project. #AgenticArchitectSprint #Antigravity


Further reading

Top comments (0)