Ground Control: From Google AI Studio Prototype to Local Production with Antigravity 2.0 Antigravity 2.0, a multi-surface release including a desktop app, CLI, SDK, and enterprise cloud, bridges the gap between Google AI Studio prototypes and production deployment. The lifecycle export feature converts entire multi-agent prototypes—including agent architecture, prompts, tool configurations, and handoff contracts—into a declarative manifest that can be version-controlled and deployed locally. This enables teams to move from sandbox to production without losing fidelity or requiring manual reconstruction. 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 theAI Studiothat shipped alongside it. The SDK is pre-v1.0; symbol lifecycle export names, bundle fields, and CLI flags below reflect the documented API as of mid-2026. Treat thepatternsas stable and re-check exact signatures against the current docs before you ship. 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: 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 agy CLI google-antigravity 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. 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 ◀────────────────────┘ 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: 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. 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: TicketClass : category billing / technical / account / other , severity, and a one-line summary. TicketClass ; declares two function tools, search tickets prior tickets from this customer and read account plan, billing history . Emits an Investigation . read policy . Emits a PolicyVerdict — what the refund/SLA policy permits for this category. 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: 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. 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. 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 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…" } } What to notice, because each field maps to a parity clause from Section 2: model defaults + per-agent generation overrides checksums agents .tools + tools/tools.json 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 eval/golden.jsonl {agent inputs, expected output} pairs.One click, and the prototype stopped being UI state. It's an artifact. Now we bring it home. 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 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 Two equivalent paths. If you downloaded the zip: 6a. Ingest the downloaded bundle into ./export/triagedesk/ agy studio import ~/Downloads/triagedesk.agy.zip 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 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 The discipline embedded in that layout: export/ is read-only, regenerated by re-import; bridge/ is yours, written once. Prompt changes happen in AI export/ in place — thatThree 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 Unpacking the flags on that last command, because it's doing something subtle: --lifecycle --mock-tools --output-format json 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. 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. 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. python 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 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. 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. python 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 }" Line by line, the load-bearing parts: ticket store , billing , policy docs . This is the code the sandbox validate registry compares Python signatures to exported JSON Schema properties. account id and the tool keeps erroring" three weeks after launch.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. python 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 Walk it: validate registry runs before any agent is built verify checksum on every prompt. ParityError message tells the operator the dict str, LocalAgentConfig — means the rest of the system addresses agents by their classifier , investigator , … . The names on the AI Studio canvas are now the names in your stack traces. Small thing; enormous debugging dividend.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: python 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 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. python 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 The parts worth reading twice: AGENTS = load lifecycle at module import. 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. max investigate, policy , not the sum — identical to how the AI Studio canvas ran it. response schema=schema on every turn. resp.parsed hands back a validated instance. Fan-in stays deterministic. triage batch 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: python 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 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 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 python 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 bash $ 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." } 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 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 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 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. Google Cloud Credits are provided for this project. AgenticArchitectSprint Antigravity agy :