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
CLIgoogle-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 β
β 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
overrideschecksums
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):
curl -fsSL https://antigravity.google/cli/install.sh | bash
agy models
export GEMINI_API_KEY="your_key_here"
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:
agy init triagedesk --template studio-import
cd triagedesk
python -m venv .venv && source .venv/bin/activate
pip install google-antigravity pydantic python-dotenv
Two equivalent paths. If you downloaded the zip:
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:
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:
agy lifecycle validate
agy lifecycle diff --against studio
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.
import os
from typing import Callable, Sequence
from google.antigravity import LocalAgentConfig
from google.antigravity.hooks import hooks, policy
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.
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 that reads lifecycle.json
and mints one
governed LocalAgentConfig
per agent β verifying prompt checksums as it goes.
This function is the bridge.
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:
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.
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:
tclass = await run_once(
AGENTS["classifier"],
f"Classify this support ticket.\n\nTICKET:\n{ticket_text}",
TicketClass,
)
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),
)
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:
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
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
):