{"slug": "ground-control-from-google-ai-studio-prototype-to-local-production-with-2-0", "title": "Ground Control: From Google AI Studio Prototype to Local Production with Antigravity 2.0", "summary": "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.", "body_md": "This guide targets\n\nAntigravity 2.0— the\n\nfour-surface release (desktop app,`agy`\n\nCLI,`google-antigravity`\n\nSDK, and\n\nenterprise cloud) that shares one agent harness — together with theAI Studiothat shipped alongside it. The SDK is pre-v1.0; symbol\n\nlifecycle export\n\nnames, bundle fields, and CLI flags below reflect the documented API as of\n\nmid-2026. Treat thepatternsas stable and re-check exact signatures against\n\nthe current docs before you ship.\n\nEvery AI engineering team knows this moment. Someone spends an afternoon in\n\nGoogle AI Studio and comes back with something genuinely impressive: a\n\nmulti-agent app that classifies incoming support tickets, investigates the\n\ncustomer's history, checks refund policy, and drafts a resolution — with typed\n\nhandoffs between agents and a sandbox transcript to prove it works. The demo\n\nkills. Leadership asks the only question leadership ever asks: *\"When can this\nbe in production?\"*\n\nAnd then the project stalls, because between the sandbox and production sits a\n\ncanyon. On one side: a cloud playground optimized for iteration speed — prompts\n\nin text boxes, tools declared in a UI, model parameters behind a slider, test\n\nruns you eyeball. On the other side: everything production actually demands —\n\nversion control, typed contracts, security policy, audit logs, CI, and tool\n\nimplementations that hit *your* databases, not a mock.\n\nHistorically, teams escaped the canyon in one of two bad ways:\n\nBoth failure modes share a root cause: **the prototype was never an artifact.**\n\nIt was a pile of UI state, and UI state doesn't survive contact with a\n\nrepository.\n\nAntigravity 2.0 closes the canyon from both ends. AI Studio's **lifecycle\nexport** turns the entire prototype — agent architecture, prompts, tool\n\n`agy`\n\nCLI`google-antigravity`\n\nThis tutorial walks the full bridge, end to end, with a real workload:\n\n**TriageDesk**, a four-agent support-ticket triage system. We'll prototype it\n\nin AI Studio, export it in one click, scaffold a local workspace with exact\n\nterminal commands, and then wire it into a production execution harness with\n\ntyped contracts, default-deny policy, and a parity gate that replays the\n\nsandbox transcripts as regression tests.\n\nThe word doing the heavy lifting in \"lifecycle export\" is *lifecycle*. It is\n\nnot a chat log, and it is not \"the prompt.\" An agent lifecycle is the complete,\n\ndeclarative description of a multi-agent system — everything needed to\n\nreproduce its behavior, and nothing tied to the surface it happens to run on:\n\n| Lifecycle component | In the AI Studio sandbox | In your local codebase |\n|---|---|---|\nAgent architecture |\nNodes on the app canvas |\n`lifecycle.json` manifest (agents + edges) |\nPrompts |\nSystem-instruction text boxes |\n`prompts/*.md` , one file per agent, checksummed |\nTool configurations |\nFunction declarations in the UI |\n`tools/tools.json` (interfaces) + your Python implementations |\nHandoff contracts |\nStructured-output schemas |\n`schemas/handoffs.json` → Pydantic models |\nGeneration parameters |\nTemperature / top-p sliders | Pinned in the manifest, applied by the harness |\nEvidence |\nSandbox test runs you eyeballed |\n`eval/golden.jsonl` → replayable parity tests |\n\nRead that table column by column and you can see the whole thesis of this\n\narticle: **every piece of sandbox state has a canonical home in a repository.**\n\nThe export is a projection from one column to the other. Nothing is\n\nreconstructed from memory; nothing is lossy.\n\n```\n   GOOGLE AI STUDIO (cloud sandbox)                LOCAL WORKSPACE (production)\n  ┌──────────────────────────────┐               ┌──────────────────────────────┐\n  │  canvas: 4 agents, 2 edges   │               │  bridge/harness.py   policy  │\n  │  prompts in text boxes       │   1-click     │  bridge/lifecycle.py loader  │\n  │  tool declarations (UI)      │   export      │  bridge/tools.py     impls   │\n  │  output schemas (UI)         │ ───────────▶  │  bridge/schemas.py   typed   │\n  │  temp/top-p sliders          │  .agy bundle  │  bridge/orchestrator.py      │\n  │  sandbox test transcripts    │               │  bridge/parity.py    gate    │\n  └──────────────────────────────┘               └──────────────────────────────┘\n                 ▲                                              │\n                 │            agy studio pull / diff            │\n                 └──────────────── parity ◀────────────────────┘\n```\n\nThe reason \"rewrite from screenshots\" fails isn't laziness — it's that agent\n\nbehavior is *absurdly* sensitive to details humans don't transcribe faithfully.\n\nA prompt that lost a trailing instruction in copy-paste. A temperature of 0.7\n\nwhere the sandbox ran 0.2. A tool schema where `priority`\n\nsilently became a\n\nstring instead of an enum. Each one changes behavior; none of them shows up in\n\na code review, because there's no \"before\" to diff against.\n\nSo we treat parity as a **contract with four clauses**, each mechanically\n\ncheckable:\n\n`Resolution`\n\ndisagrees with the sandbox `Resolution`\n\non a golden\nticket, the deploy gate fails.Everything we build in Steps 3 and 4 exists to enforce one of those four\n\nclauses. Keep the table and the contract in your head; the rest is\n\nimplementation.\n\nThis is a tutorial about *leaving* the sandbox, so we'll keep the sandbox part\n\nbrief — but the shape of the prototype matters, because a well-structured\n\nprototype exports cleanly and a mushy one doesn't.\n\nIn AI Studio, create a new **multi-agent app** and lay out four agents:\n\n`TicketClass`\n\n:\ncategory (`billing`\n\n/ `technical`\n\n/ `account`\n\n/ `other`\n\n), severity, and a\none-line summary.`TicketClass`\n\n; declares two\nfunction tools, `search_tickets`\n\n(prior tickets from this customer) and\n`read_account`\n\n(plan, billing history). Emits an `Investigation`\n\n.`read_policy`\n\n. Emits a `PolicyVerdict`\n\n—\nwhat the refund/SLA policy permits for this category.`Investigation`\n\nand `PolicyVerdict`\n\nand emits the final `Resolution`\n\n: an action, a customer-facing reply draft,\nand an `escalate`\n\nflag with rationale.Wire the edges on the canvas: Classifier fans out to Investigator and Policy\n\nChecker (parallel branch), which join into Resolver. A diamond.\n\nThree habits at this stage pay for themselves at export time:\n\n`schemas/handoffs.json`\n\nin the bundle, and typed\nhandoffs are what make the local orchestrator deterministic. An agent\nwithout an output schema exports as a prose-emitter, and you'll pay for it\nlater with regex.`eval/golden.jsonl`\n\n:\nyour regression suite, for free.Tune the temperature down to 0.2 for the Classifier and Policy Checker (you\n\nwant determinism), leave the Resolver a little warmer for reply drafting, and\n\niterate until the diamond behaves. That's the prototype. Total UI time: an\n\nafternoon. Now let's make it an artifact.\n\nIn the app's toolbar: **Deploy ▸ Export lifecycle**. AI Studio bundles the\n\nentire app definition into a single archive — `triagedesk.agy.zip`\n\n— and offers\n\ntwo delivery paths: a direct download, or a push to your project's export\n\nregistry that the CLI can pull by app ID (we'll use both in Step 3).\n\nUnzip it and look around; this bundle is the contract between the two worlds:\n\n```\ntriagedesk.agy.zip\n├── lifecycle.json            # the manifest — architecture, params, checksums\n├── prompts/\n│   ├── classifier.md         # byte-exact system instructions, one per agent\n│   ├── investigator.md\n│   ├── policy_checker.md\n│   └── resolver.md\n├── tools/\n│   └── tools.json            # function DECLARATIONS (JSON Schema) — no impls\n├── schemas/\n│   └── handoffs.json         # structured-output contracts between agents\n└── eval/\n    └── golden.jsonl          # your approved sandbox runs: input → typed output\n```\n\nThe manifest is the piece worth reading line by line:\n\n```\n{\n  \"format\": \"antigravity.lifecycle/v1\",\n  \"name\": \"triagedesk\",\n  \"exported_from\": \"aistudio://apps/triagedesk-8f3a21\",\n  \"exported_at\": \"2026-07-08T14:32:00Z\",\n  \"model_defaults\": {\n    \"model\": \"gemini-3-pro\",\n    \"generation\": { \"temperature\": 0.2, \"top_p\": 0.95, \"max_output_tokens\": 2048 }\n  },\n  \"agents\": [\n    { \"id\": \"classifier\",\n      \"prompt\": \"prompts/classifier.md\",\n      \"tools\": [],\n      \"output_schema\": \"schemas/handoffs.json#/TicketClass\" },\n    { \"id\": \"investigator\",\n      \"prompt\": \"prompts/investigator.md\",\n      \"tools\": [\"search_tickets\", \"read_account\"],\n      \"output_schema\": \"schemas/handoffs.json#/Investigation\" },\n    { \"id\": \"policy_checker\",\n      \"prompt\": \"prompts/policy_checker.md\",\n      \"tools\": [\"read_policy\"],\n      \"output_schema\": \"schemas/handoffs.json#/PolicyVerdict\" },\n    { \"id\": \"resolver\",\n      \"prompt\": \"prompts/resolver.md\",\n      \"tools\": [],\n      \"generation\": { \"temperature\": 0.6 },\n      \"output_schema\": \"schemas/handoffs.json#/Resolution\" }\n  ],\n  \"edges\": [\n    { \"from\": \"classifier\", \"to\": [\"investigator\", \"policy_checker\"], \"mode\": \"parallel\" },\n    { \"from\": [\"investigator\", \"policy_checker\"], \"to\": \"resolver\", \"mode\": \"join\" }\n  ],\n  \"checksums\": {\n    \"prompts/classifier.md\":     \"sha256:1f9c2e…\",\n    \"prompts/investigator.md\":   \"sha256:aa07d1…\",\n    \"prompts/policy_checker.md\": \"sha256:83b4f0…\",\n    \"prompts/resolver.md\":       \"sha256:c95a72…\"\n  }\n}\n```\n\nWhat to notice, because each field maps to a parity clause from Section 2:\n\n`model_defaults`\n\n+ per-agent `generation`\n\noverrides`checksums`\n\n`agents[].tools`\n\n+ `tools/tools.json`\n\n`read_account`\n\nwith hand-typed JSON;\nyour production `read_account`\n\nwill hit the real billing database with real\ncredentials that never touch a bundle file.`edges`\n\n`eval/golden.jsonl`\n\n`{agent_inputs, expected_output}`\n\npairs.One click, and the prototype stopped being UI state. It's an artifact. Now we\n\nbring it home.\n\nEverything in this section is exact terminal commands. First, the two surfaces\n\nyou'll use locally — the **CLI** (`agy`\n\n, the Go-based terminal harness) and the\n\n**SDK** (`google-antigravity`\n\n, the same harness as a Python library):\n\n```\n# 1. Install the Antigravity CLI (macOS / Linux). It lands at ~/.local/bin/agy\ncurl -fsSL https://antigravity.google/cli/install.sh | bash\n\n# Windows PowerShell:  irm https://antigravity.google/cli/install.ps1 | iex\n\n# 2. Confirm the harness is live and see which models you can reach\nagy models\n\n# 3. Authenticate. Either a direct Gemini key…\nexport GEMINI_API_KEY=\"your_key_here\"\n#    …or route through Vertex AI with Application Default Credentials:\n#    gcloud auth application-default login   # then set VERTEX=1 in .env\n```\n\n`agy init`\n\nwith the `studio-import`\n\ntemplate creates a workspace pre-shaped for\n\nan ingested lifecycle — an `export/`\n\ndirectory the bundle lands in, a `bridge/`\n\npackage for the production wiring, and a `.agy/`\n\nconfig that tells the harness\n\nwhere the manifest lives:\n\n```\n# 4. Scaffold the local IDE workspace\nagy init triagedesk --template studio-import\ncd triagedesk\n\n# 5. Python environment + SDK. IMPORTANT: install the published wheel, not a\n#    source checkout — the wheel ships the `localharness` binary the SDK drives.\npython -m venv .venv && source .venv/bin/activate\npip install google-antigravity pydantic python-dotenv\n```\n\nTwo equivalent paths. If you downloaded the zip:\n\n```\n# 6a. Ingest the downloaded bundle into ./export/triagedesk/\nagy studio import ~/Downloads/triagedesk.agy.zip\n```\n\nOr skip the download entirely and pull from AI Studio's export registry by app\n\nID — this is the path you'll automate later, because it means \"re-sync the\n\nprototype\" is one non-interactive command:\n\n```\n# 6b. …or pull the latest export straight from AI Studio\nagy studio pull --app triagedesk-8f3a21\n```\n\nEither way, the CLI unpacks the bundle, records its provenance (`exported_from`\n\n,\n\n`exported_at`\n\n) in `.agy/lifecycle.lock`\n\n, and your workspace now looks like this:\n\n```\ntriagedesk/\n├── .agy/\n│   └── lifecycle.lock        # provenance pin: which export this workspace tracks\n├── export/\n│   └── triagedesk/           # the ingested bundle, verbatim (treat as read-only)\n│       ├── lifecycle.json\n│       ├── prompts/  tools/  schemas/  eval/\n├── bridge/                   # YOUR code — the production wiring (Step 4)\n├── main.py\n└── requirements.txt\n```\n\nThe discipline embedded in that layout: ** export/ is read-only, regenerated\nby re-import; bridge/ is yours, written once.** Prompt changes happen in AI\n\n`export/`\n\nin place — thatThree commands before you write any Python:\n\n```\n# 7. Validate the ingested lifecycle: manifest schema, checksums, schema refs,\n#    edge graph is acyclic, every declared tool has a JSON Schema entry\nagy lifecycle validate\n\n# 8. Parity check: has the cloud app drifted since this export?\nagy lifecycle diff --against studio\n#    → \"in sync with aistudio://apps/triagedesk-8f3a21 (exported 2026-07-08)\"\n#    …or a unified diff of any prompt/param/schema that changed upstream\n\n# 9. Smoke-test the lifecycle in the local harness — sandbox-mocked tools,\n#    no production wiring yet. Proves the bundle itself is runnable.\nagy run --lifecycle export/triagedesk/lifecycle.json \\\n    --mock-tools export/triagedesk/eval/golden.jsonl \\\n    -p \"Ticket: 'I was charged twice for my Pro plan this month.'\" \\\n    --output-format json\n```\n\nUnpacking the flags on that last command, because it's doing something subtle:\n\n`--lifecycle`\n\n`--mock-tools`\n\n`--output-format json`\n\n`Resolution`\n\non stdout, pipeable\ninto `jq`\n\nor CI.If step 9 prints the same `Resolution`\n\nthe sandbox produced for that ticket,\n\nthe bridge held: the prototype is now running on your laptop, byte-identical\n\nprompts, pinned parameters, same harness. What it doesn't have yet is\n\n*production* — real tools, real policy, real telemetry. That's Step 4.\n\nThe CLI proved the lifecycle is portable. The SDK is where it becomes\n\n*deployable*: a Python package (`bridge/`\n\n) that loads the exported manifest\n\ninto governed agent configs, binds real tool implementations against the\n\nexported interfaces, drives the diamond with typed handoffs, and gates deploys\n\nbehind a golden-transcript replay.\n\nEvery agent in the lifecycle gets minted through one factory with one set of\n\nguardrails. Prompts and parameters come from the *bundle*; policy and\n\nobservability come from *here*. That separation is the production boundary in\n\none sentence: **AI Studio decides what the agents say; your harness decides\nwhat they're allowed to do.**\n\n``` python\n# bridge/harness.py\nimport os\nfrom typing import Callable, Sequence\n\nfrom google.antigravity import LocalAgentConfig\nfrom google.antigravity.hooks import hooks, policy\n\n# Shared guardrails — least privilege, encoded once, inherited by every agent\n# the lifecycle loader mints. The exported bundle can never widen these.\nBASE_POLICIES = [\n    policy.allow(\"search_tickets\"),\n    policy.allow(\"read_account\"),\n    policy.allow(\"read_policy\"),\n    policy.deny(\"run_command\"),   # a triage agent never shells out\n    policy.deny(\"write_file\"),    # …or writes to disk\n    policy.deny(\"*\"),             # default-deny: anything undeclared is refused\n]\n\nclass AuditTrailHook(hooks.PostToolCallHook):\n    \"\"\"One structured audit line per tool call, in every agent of the diamond.\"\"\"\n    async def run(self, context: hooks.HookContext, data) -> None:\n        print(\n            f\"[audit] trajectory={getattr(context, 'trajectory_id', '?')} \"\n            f\"tool={getattr(data, 'name', '?')} ok={getattr(data, 'ok', True)}\"\n        )\n\ndef build_agent_config(\n    *,\n    system_instructions: str,\n    tools: Sequence[Callable] = (),\n    model: str,\n    generation: dict | None = None,\n) -> LocalAgentConfig:\n    kwargs = dict(\n        model=model,\n        system_instructions=system_instructions,\n        tools=list(tools),\n        policies=BASE_POLICIES,      # the production floor — non-negotiable\n        hooks=[AuditTrailHook()],    # telemetry on every node\n    )\n    if generation:\n        kwargs[\"generation\"] = dict(generation)   # parameter parity, applied\n    if os.getenv(\"VERTEX\") == \"1\":\n        kwargs[\"vertex\"] = True\n    elif os.getenv(\"GEMINI_API_KEY\"):\n        kwargs[\"api_key\"] = os.environ[\"GEMINI_API_KEY\"]\n    return LocalAgentConfig(**kwargs)\n```\n\nThe design decision that matters: `BASE_POLICIES`\n\nis **not** part of the\n\nexport, on purpose. Policy is an attribute of the *environment*, not the\n\n*prototype* — the same lifecycle might run with generous policies in staging\n\nand airtight ones in production. The bundle can name tools; only the harness\n\ncan grant them.\n\nThe bundle shipped `tools/tools.json`\n\n: three function declarations with full\n\nJSON Schema parameters. Locally, you write the real implementations and\n\n**validate them against the exported declarations at import time**. This is\n\ninterface parity as executable code — if AI Studio exported\n\n`read_account(customer_id: string)`\n\nand you wrote\n\n`read_account(account_id: str)`\n\n, you find out at startup, not in an incident\n\nreview.\n\n``` python\n# bridge/tools.py\nimport inspect\nimport json\nfrom pathlib import Path\n\nBUNDLE = Path(__file__).parent.parent / \"export\" / \"triagedesk\"\n\ndef search_tickets(customer_id: str, days: int = 90) -> str:\n    \"\"\"Return this customer's tickets from the last `days` days, as JSON.\"\"\"\n    from .backends import ticket_store            # real system, injected here\n    return json.dumps(ticket_store.recent(customer_id, days=days))\n\ndef read_account(customer_id: str) -> str:\n    \"\"\"Return the customer's plan, billing history, and account flags.\"\"\"\n    from .backends import billing\n    return json.dumps(billing.account_summary(customer_id))\n\ndef read_policy(category: str) -> str:\n    \"\"\"Return the support policy text applicable to the given ticket category.\"\"\"\n    from .backends import policy_docs\n    return policy_docs.for_category(category)\n\nTOOL_REGISTRY = {\n    \"search_tickets\": search_tickets,\n    \"read_account\": read_account,\n    \"read_policy\": read_policy,\n}\n\ndef validate_registry() -> None:\n    \"\"\"Interface parity: every exported declaration must have a local impl\n    whose signature matches the exported JSON Schema. Fail at startup.\"\"\"\n    declared = json.loads((BUNDLE / \"tools\" / \"tools.json\").read_text())\n    for decl in declared[\"functions\"]:\n        impl = TOOL_REGISTRY.get(decl[\"name\"])\n        if impl is None:\n            raise RuntimeError(f\"exported tool '{decl['name']}' has no local implementation\")\n        got = set(inspect.signature(impl).parameters)\n        want = set(decl[\"parameters\"][\"properties\"])\n        if got != want:\n            raise RuntimeError(\n                f\"signature drift on '{decl['name']}': bundle declares {sorted(want)}, \"\n                f\"local implementation has {sorted(got)}\"\n            )\n```\n\nLine by line, the load-bearing parts:\n\n`ticket_store`\n\n, `billing`\n\n,\n`policy_docs`\n\n). This is the code the sandbox `validate_registry()`\n\ncompares Python signatures to exported JSON Schema\nproperties.`account_id`\n\nand the tool keeps\nerroring\" three weeks after launch.Now the centerpiece: a loader that reads `lifecycle.json`\n\nand mints one\n\ngoverned `LocalAgentConfig`\n\nper agent — verifying prompt checksums as it goes.\n\nThis function *is* the bridge.\n\n``` python\n# bridge/lifecycle.py\nimport hashlib\nimport json\nfrom pathlib import Path\n\nfrom google.antigravity import LocalAgentConfig\n\nfrom .harness import build_agent_config\nfrom .tools import BUNDLE, TOOL_REGISTRY, validate_registry\n\nclass ParityError(RuntimeError):\n    \"\"\"The local bundle no longer matches what AI Studio exported.\"\"\"\n\ndef _verify_checksum(root: Path, rel: str, expected: str) -> None:\n    digest = hashlib.sha256((root / rel).read_bytes()).hexdigest()\n    if f\"sha256:{digest}\" != expected:\n        raise ParityError(\n            f\"prompt drift in {rel}: bundle expects {expected[:19]}…, \"\n            f\"file hashes to sha256:{digest[:12]}…. Re-import the export; \"\n            f\"never hand-edit export/.\"\n        )\n\ndef load_lifecycle(bundle: Path = BUNDLE) -> dict[str, LocalAgentConfig]:\n    manifest = json.loads((bundle / \"lifecycle.json\").read_text())\n    validate_registry()                              # interface parity gate\n\n    defaults = manifest[\"model_defaults\"]\n    configs: dict[str, LocalAgentConfig] = {}\n    for spec in manifest[\"agents\"]:\n        _verify_checksum(bundle, spec[\"prompt\"],     # prompt parity gate\n                         manifest[\"checksums\"][spec[\"prompt\"]])\n        configs[spec[\"id\"]] = build_agent_config(\n            system_instructions=(bundle / spec[\"prompt\"]).read_text(),\n            tools=[TOOL_REGISTRY[name] for name in spec[\"tools\"]],\n            model=spec.get(\"model\", defaults[\"model\"]),\n            generation={**defaults.get(\"generation\", {}),\n                        **spec.get(\"generation\", {})},   # parameter parity\n        )\n    return configs\n```\n\nWalk it:\n\n`validate_registry()`\n\nruns before any agent is built`_verify_checksum`\n\non every prompt.`ParityError`\n\nmessage tells the\noperator the `dict[str, LocalAgentConfig]`\n\n— means the rest of the\nsystem addresses agents by their `classifier`\n\n,\n`investigator`\n\n, …). The names on the AI Studio canvas are now the names in\nyour stack traces. Small thing; enormous debugging dividend.The bundle's `schemas/handoffs.json`\n\ndefines what flows along each edge. In\n\nPython, those become Pydantic models — the same contracts, now enforced by the\n\nharness's structured-output machinery:\n\n``` python\n# bridge/schemas.py\nfrom enum import Enum\nfrom pydantic import BaseModel, Field\n\nclass Category(str, Enum):\n    BILLING = \"billing\"; TECHNICAL = \"technical\"; ACCOUNT = \"account\"; OTHER = \"other\"\n\nclass TicketClass(BaseModel):\n    category: Category\n    severity: int = Field(ge=1, le=4)          # 1 = critical … 4 = low\n    summary: str\n\nclass Investigation(BaseModel):\n    customer_id: str\n    prior_tickets: int\n    account_standing: str\n    relevant_facts: list[str]\n\nclass PolicyVerdict(BaseModel):\n    policy_section: str\n    permitted_actions: list[str]\n    requires_human_approval: bool\n\nclass Resolution(BaseModel):\n    action: str\n    reply_draft: str\n    escalate: bool\n    rationale: str\n```\n\nThe manifest's `edges`\n\nsaid: classify, then fan out in parallel, then join.\n\nThe production orchestrator encodes exactly that — and nothing else. All the\n\nintelligence lives in the exported prompts; this file is just the topology,\n\nmade explicit and typed.\n\n``` python\n# bridge/orchestrator.py\nimport asyncio\nimport os\nfrom typing import Type, TypeVar\n\nfrom google.antigravity import Agent\nfrom pydantic import BaseModel\n\nfrom .lifecycle import load_lifecycle\nfrom .schemas import Investigation, PolicyVerdict, Resolution, TicketClass\n\nT = TypeVar(\"T\", bound=BaseModel)\n\nAGENTS = load_lifecycle()          # parity gates run HERE, at import time\n\nasync def run_once(config, prompt: str, schema: Type[T]) -> T:\n    \"\"\"One isolated agent, one typed turn, then tear down.\"\"\"\n    async with Agent(config) as agent:\n        resp = await agent.chat(prompt, response_schema=schema)\n        return await resp.parsed()               # a validated Pydantic instance\n\nasync def triage(ticket_text: str, customer_id: str) -> Resolution:\n    # Node 1 — classify (no tools, cold temperature, deterministic)\n    tclass = await run_once(\n        AGENTS[\"classifier\"],\n        f\"Classify this support ticket.\\n\\nTICKET:\\n{ticket_text}\",\n        TicketClass,\n    )\n\n    # Nodes 2 & 3 — the parallel branch from the manifest's edges.\n    # Two isolated contexts, launched at the same instant.\n    shared = (\n        f\"TICKET:\\n{ticket_text}\\n\\ncustomer_id: {customer_id}\\n\"\n        f\"classification: {tclass.model_dump_json()}\"\n    )\n    investigation, verdict = await asyncio.gather(\n        run_once(AGENTS[\"investigator\"],\n                 f\"Investigate. Use your tools.\\n\\n{shared}\", Investigation),\n        run_once(AGENTS[\"policy_checker\"],\n                 f\"Determine what policy permits.\\n\\n{shared}\", PolicyVerdict),\n    )\n\n    # Node 4 — the join. Tool-less: the resolution must be a pure function of\n    # the two typed findings, which is what makes it auditable.\n    return await run_once(\n        AGENTS[\"resolver\"],\n        \"Draft the resolution from these independent findings.\\n\\n\"\n        f\"Investigation: {investigation.model_dump_json(indent=2)}\\n\"\n        f\"PolicyVerdict: {verdict.model_dump_json(indent=2)}\",\n        Resolution,\n    )\n\nasync def triage_batch(tickets: list[tuple[str, str]]) -> list[Resolution]:\n    limit = int(os.getenv(\"TRIAGE_CONCURRENCY\", \"8\"))\n    sem = asyncio.Semaphore(limit)               # bound live model sessions\n\n    async def _guarded(t: tuple[str, str]) -> Resolution:\n        async with sem:\n            return await triage(*t)\n\n    return await asyncio.gather(*(_guarded(t) for t in tickets))\n```\n\nThe parts worth reading twice:\n\n`AGENTS = load_lifecycle()`\n\nat module import.`ParityError`\n\nnaming the exact\nfile, instead of serving subtly-wrong triage for a week.`run_once`\n\nopens a fresh `Agent`\n\nper node.`async with Agent(config)`\n\nis\na full harness session with its own isolated context window. The\nInvestigator's tool output never pollutes the Policy Checker's reasoning —\nthe same state isolation the sandbox gave the diamond, reproduced locally.`asyncio.gather`\n\nis the manifest's `\"mode\": \"parallel\"`\n\nedge, verbatim.`max(investigate, policy)`\n\n, not the\nsum — identical to how the AI Studio canvas ran it.`response_schema=schema`\n\non every turn.`resp.parsed()`\n\nhands\nback a validated instance. Fan-in stays deterministic.`triage_batch`\n\nLast clause of the contract: behavioral parity. The bundle's `eval/golden.jsonl`\n\nholds every sandbox run you approved. Replay them through the *local* system —\n\nreal harness, real policy, mocked tools (so backends don't flake the gate) —\n\nand compare the structured fields that matter:\n\n``` python\n# bridge/parity.py\nimport asyncio\nimport json\n\nfrom .orchestrator import triage\nfrom .tools import BUNDLE\n\nasync def replay_golden() -> tuple[int, int]:\n    \"\"\"Replay approved sandbox transcripts against the local lifecycle.\n    Returns (passed, total). Wire this into CI as the deploy gate.\"\"\"\n    passed = total = 0\n    for line in (BUNDLE / \"eval\" / \"golden.jsonl\").read_text().splitlines():\n        case = json.loads(line)\n        total += 1\n        local = await triage(case[\"ticket\"], case[\"customer_id\"])\n        expected = case[\"expected\"]                     # sandbox Resolution\n        ok = (local.action == expected[\"action\"]\n              and local.escalate == expected[\"escalate\"])\n        passed += ok\n        print(f\"[parity] {case['id']}: {'PASS' if ok else 'FAIL'}\")\n    return passed, total\n\nif __name__ == \"__main__\":\n    p, t = asyncio.run(replay_golden())\n    raise SystemExit(0 if p == t else 1)                # CI-friendly exit code\n```\n\nNote what we compare: `action`\n\nand `escalate`\n\n— the decisions — not\n\n`reply_draft`\n\n, which is legitimately creative at temperature 0.6. A parity gate\n\nthat string-matches prose will cry wolf until someone deletes it; a gate that\n\nchecks decisions earns its place in CI. `python -m bridge.parity`\n\nexits nonzero\n\non any regression, which makes the deploy pipeline one line:\n\n```\nagy lifecycle diff --against studio && python -m bridge.parity && ./deploy.sh\n```\n\nRead that line back slowly, because it's the whole article: *the cloud hasn't\ndrifted from the bundle, the local system still behaves like the approved\nsandbox runs, therefore ship.* Three worlds — AI Studio, the repository, and\n\n``` python\n# main.py\nimport asyncio, json, sys\nfrom bridge.orchestrator import triage\n\nasync def main():\n    ticket = sys.stdin.read().strip() or \"I was charged twice for my Pro plan.\"\n    resolution = await triage(ticket, customer_id=\"cus_4821\")\n    print(json.dumps(resolution.model_dump(), indent=2))\n\nasyncio.run(main())\nbash\n$ echo \"I was charged twice for my Pro plan this month.\" | python main.py\n[audit] trajectory=tj_01 tool=search_tickets ok=True\n[audit] trajectory=tj_01 tool=read_account ok=True\n[audit] trajectory=tj_02 tool=read_policy ok=True\n{\n  \"action\": \"refund_duplicate_charge\",\n  \"reply_draft\": \"Hi — you're right, and I'm sorry: our records confirm a duplicate charge…\",\n  \"escalate\": false,\n  \"rationale\": \"Billing history confirms duplicate charge on 2026-07-02; policy §4.2 permits immediate refund of verified duplicates without approval.\"\n}\n```\n\nSame prompts as the sandbox, byte-for-byte. Same parameters, applied by the\n\nharness. Same diamond, now with real tools, default-deny policy, an audit line\n\nper tool call, and a regression suite that was generated by *using the\nprototype*. The distance from demo to this was four files of wiring — and none\n\nThe canyon between \"look what I built in AI Studio\" and \"it's in production\"\n\nwas never really about code. It was about **state that lived in a UI** — and\n\nevery team that lost a week reconstructing a prototype from screenshots, or\n\nshipped a playground behind a proxy and prayed, was paying interest on that\n\none architectural debt.\n\nAntigravity 2.0's answer is to make the prototype an artifact with a defined\n\nprojection into a repository. The **lifecycle export** captures architecture,\n\nprompts, tool interfaces, handoff schemas, parameters, and evidence in one\n\nbundle. The ** agy CLI** makes ingesting, validating, and drift-checking that\n\nThe deeper shift is organizational. When export is one click and ingestion is\n\none command, the prototype stops being a throwaway and becomes **the first\ncommit** — product-minded builders iterate in AI Studio, engineers govern the\n\nPrototype where iteration is cheapest. Productionize where governance is\n\nstrongest. With a checksummed bridge between them, you no longer have to\n\nchoose — and the next time leadership asks *\"when can this be in production?\"*,\n\nthe honest answer is: it already compiled.\n\nGoogle Cloud Credits are provided for this project. #AgenticArchitectSprint #Antigravity\n\n`agy`\n\n):", "url": "https://wpnews.pro/news/ground-control-from-google-ai-studio-prototype-to-local-production-with-2-0", "canonical_source": "https://dev.to/estherirawati/ground-control-from-google-ai-studio-prototype-to-local-production-with-antigravity-20-1acd", "published_at": "2026-07-10 16:13:24+00:00", "updated_at": "2026-07-10 16:43:27.204568+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "developer-tools", "ai-infrastructure", "ai-products"], "entities": ["Google AI Studio", "Antigravity 2.0", "TriageDesk"], "alternates": {"html": "https://wpnews.pro/news/ground-control-from-google-ai-studio-prototype-to-local-production-with-2-0", "markdown": "https://wpnews.pro/news/ground-control-from-google-ai-studio-prototype-to-local-production-with-2-0.md", "text": "https://wpnews.pro/news/ground-control-from-google-ai-studio-prototype-to-local-production-with-2-0.txt", "jsonld": "https://wpnews.pro/news/ground-control-from-google-ai-studio-prototype-to-local-production-with-2-0.jsonld"}}