Category: Cloud

  • You can just find and use agent skills mid-session

    I’m not proud of it, but I’ve been drunk on agent skills. It’s time to reset. It’s easy to fall into skills bloat as you accrue different ones for every possible scenario. Doing software engineering? Coding in Go? Performing security analysis? Add a skill! Nowadays, any of us can do basically anything. Maybe not well, but with agent skills, we get just enough confidence to try it. But we’re blowing tokens and confusing our agents with all this extra context. Plus, you only really need skills when there’s a specific (temporary?) gap in the LLM’s knowledge. Let’s be smarter. Can we do just-in-time discovery and lazy-loading of skills for our coding sessions?

    Case 1: Preload skills globally or in your project

    As a baseline, many of us default to installing skills ahead of time. It’s easy to grab skill files manually, or use something like the skills CLI to install into global or project directories.

    For instance, before starting my coding session, I could find and install a skill for working with Google Cloud Storage.

    If I choose to install into my local project directory (instead of globally), I end up with these new files on disk.

    I’m using Google Antigravity here, but you’d expect to see this skill show up in whatever agentic tool you have. Both my Antigravity desktop app and CLI show this pre-existing skill.

    To be sure, this is a perfectly valid way to work. But the front matter of EVERY skill comes into play in all your agentic coding turns. That gets hefty over time. You could choose to just pre-load or copy the “right” skills into your project before each session. But that assumes you know where you’re heading before every session. I sure don’t! Let’s look at more dynamic scenarios.

    Case 2: Find known skills during a session and use them

    Let’s consider a case where I know where the desired skills live on the internet, but I only want to grab them if I really need them.

    For example, maybe I use all sorts of clouds and pick the right one for the problem at hand. In my current coding session, I’m thinking through web app ideas, and landed on a fun concept for a fast-turnaround marketplace. Its bursty use and global footprint are great for Google Cloud. I need good guidance for picking the right architecture, so I asked Google Antigravity to find the right Google Skill to help out, and store it locally.

    When this finished a few moments later, I had another skill in my local project directory.

    Most importantly, this skill is automatically active in this coding session. I can see that in Antigravity’s list of active skills.

    My current session got better as I discovered and loaded agents on-the-fly. And these skills stay with this project and aren’t just part of the LLM (or coding session’s) temporary memory.

    Case 3: Search for skills during a session and use them

    The above case assumed that I knew the location of the skill I wanted. You need to be careful about loading random skills, but what if you wanted to search without knowing ahead of time where the most suitable skill was at?

    I continued my coding session by asking for the best skills for Go developers.

    I know Steve (the creator of the 2nd of the 3 provided options) so I told Antigravity to load Steve’s skills. Sure enough, I got a handful of terrific Go skills installed into my project.

    You don’t have to know skills or sources you want ahead of time. You can search for them during a session and keep going without ever breaking flow.

    Case 4: Use a seed skill that grabs the “right” skill for the scenario

    I’ve seen more of this pattern lately. You know you will need a set of related skills, but don’t know which ones, or when. A seed skill is your entry point that pulls the right skill at the right time. Addy’s Osmani’s agent skills do something like this. The “meta skill” routes you to the right skill for your situation. But it does require you to install all the skills (I believe).

    Could you create a seed skill that acts as a lightweight entrypoint, and then just-in-time downloads the “right” skill from a pre-defined list? Sure you can.

    Specifically, I want a skill that knows about all of the Google Cloud databases, but only retrieves the corresponding skill if our session demands it.

    I used Antigravity to build it (in a different session) and here’s the gist (minus the scripts and references it also created).

    ---
    name: gcp-database-seed-skill
    description: >-
      Dynamic seed skill for Google Cloud database solutions. Use this skill when the user is designing,
      choosing, provisioning, migrating, or optimizing databases on Google Cloud (Cloud SQL for MySQL/PostgreSQL/SQL Server,
      AlloyDB for PostgreSQL, Cloud Spanner, Firestore, or Bigtable), or when well-architected cloud database guidance is needed.
      Automatically routes, downloads, and installs specialized skills from google/skills on demand.
    ---
    
    # Google Cloud Database & Architecture Advisor (Seed Skill)
    
    This seed skill provides a lightweight, progressive entrypoint for all database workloads on Google Cloud. Instead of loading bulky documentation upfront, it assesses workload requirements, recommends the optimal database solution, and dynamically downloads and installs specialized skills from the official [google/skills](https://github.com/google/skills) repository into your local workspace.
    
    ---
    
    ## Workflow & Step-by-Step Procedure
    
    When invoked on a database-related prompt, follow this 4-step workflow:
    
    ```mermaid
    flowchart LR
        Step1[1. Assess Workload] --> Step2[2. Determine Database & WAF Needs]
        Step2 --> Step3[3. Download & Install Skill]
        Step3 --> Step4[4. Execute Guided Solution]
    ```
    
    ### Step 1: Assess Workload Requirements
    Evaluate the user's requirements against key architectural dimensions:
    1. **Engine Compatibility**: Does the workload require MySQL, PostgreSQL, SQL Server, or open-standard ANSI SQL?
    2. **Scalability & Scale**: Single-instance vertical scaling vs. distributed multi-node horizontal sharding?
    3. **Availability & DR**: Regional HA (99.95% - 99.99%) vs. Global Multi-Region zero downtime (99.999%)?
    4. **Workload Characteristics**: Standard OLTP vs. High-throughput HTAP with columnar queries vs. Vector/AI search vs. NoSQL?
    5. **Migration vs. New Build**: Existing application lift-and-shift vs. greenfield cloud-native design?
    
    > 📖 Consult the detailed [Database Decision Matrix](./references/database_decision_matrix.md) for complete comparison tables.
    
    ---
    
    ### Step 2: Select the Target Skills
    
    Map the workload to the primary database skill and check for relevant **Well-Architected Framework** pillars:
    
    #### A. Database Engine Selection
    *   **Cloud SQL (`cloud-sql-basics` / `cloud-sql-mysql` / `cloud-sql-postgresql` / `cloud-sql-sqlserver`)**:
        *   *Select when:* Straightforward relational databases (MySQL, PostgreSQL, SQL Server), standard web apps, migration-friendly lift-and-shift via Database Migration Service (DMS).
    *   **AlloyDB for PostgreSQL (`alloydb-basics` / `alloydb`)**:
        *   *Select when:* Demanding enterprise PostgreSQL workloads, up to 4x transactional performance, up to 100x faster analytical/HTAP queries with columnar engine, or in-database vector search (Vertex AI / ScaNN integration).
    *   **Cloud Spanner (`spanner`)**:
        *   *Select when:* Mission-critical global databases, massive scale exceeding single-instance limits, synchronous multi-region replication, horizontal scaling, and 99.999% availability with zero downtime.
    *   **Discovery / Undecided (`cloud-databases-onboarding`)**:
        *   *Select when:* User is unsure or needs interactive discovery across relational, NoSQL (Firestore, Bigtable), and analytical stores.
    
    #### B. Well-Architected Framework Triggers
    Check if cross-cutting architectural pillars should be retrieved:
    *   **Reliability & HA:** Mentions of 99.999% SLA, DR, RTO/RPO, regional failover → Fetch `well-architected-reliability`.
    *   **Security & IAM:** Questions about Private IP, PSA, CMEK encryption, or IAM DB auth → Fetch `google-cloud-recipe-auth`.
    *   **Performance & Tuning:** Connection pooling, read replicas, latency optimization → Fetch `google-cloud-solution-architecture`.
    *   **Cost Optimization:** Committed Use Discounts (CUDs), rightsizing, auto-pause → Fetch `google-cloud-solution-architecture`.
    
    > 📖 Consult [Well-Architected Triggers](./references/well_architected_triggers.md) for full trigger criteria.
    
    ---
    
    ### Step 3: Download and Install the Specialized Skill
    
    Execute the bundled installer helper to fetch the required skill(s) directly from `google/skills` into the workspace's `.agents/skills/` directory:
    
    ```bash
    # Example: Install Cloud SQL basics
    python3 scripts/install_gcp_skill.py --skill cloud-sql-basics
    
    # Example: Install AlloyDB basics
    python3 scripts/install_gcp_skill.py --skill alloydb-basics
    
    # Example: Install Cloud Spanner
    python3 scripts/install_gcp_skill.py --skill spanner
    
    # Example: Install Well-Architected Reliability
    python3 scripts/install_gcp_skill.py --skill well-architected-reliability
    
    # Example: Install all core database skills
    python3 scripts/install_gcp_skill.py --all-databases
    ```
    
    *Alternative using npm/npx:*
    ```bash
    npx skills add google/skills
    ```
    
    ---
    
    ### Step 4: Proceed with Implementation
    
    Once the skill is installed into `.agents/skills/`, it is immediately available for subsequent operations.
    1. Reference the newly installed `SKILL.md` in `.agents/skills/<skill-name>/`.
    2. Provide concrete `gcloud` commands, Terraform/IaC snippets, or client library examples tailored to the user's selected database.
    3. Verify connectivity (e.g. Cloud SQL Auth Proxy, IAM authentication, VPC Private Service Access).
    
    ---
    
    ## Reference Guides
    
    *   [Database Decision Matrix](./references/database_decision_matrix.md): Deep-dive comparison between Cloud SQL, AlloyDB, Spanner, Firestore, and Bigtable.
    *   [Well-Architected Framework Triggers](./references/well_architected_triggers.md): Detailed rules for triggering security, reliability, performance, cost, and operational pillars.
    *   [Known Skills Catalog](./references/known_skills_catalog.md): Complete list of remote paths and metadata in `google/skills`.
    *   [Prompt Routing Scenarios & Examples](./examples/prompt_routing_scenarios.md): Real-world examples of user requests and the exact skills retrieved.
    
    

    After adding this seed skill to my current coding session, I started up a database conversation regarding my app idea. We landed on Firestore as the best choice, so I asked the seed skill to retrieve only the Firestore skill.

    Here, I didn’t need to download or pre-load all the possible Google Cloud database skills. There’s a lot of them. I only got the one I needed for this particular coding session. Seems cleaner?

    A word about MCPs

    If your skills make use of remote or local MCP servers, circumstances are different. While MCP now supports a stateless interaction pattern, most existing MCP servers don’t. When your coding session starts, there’s a handshake that happens. This means that if you introduce a new MCP during a session (in your mcp_config.json), you likely have to fork your current session, or start a new one. My tests showed this to be the case most of the time, but your mileage may vary.

    I mention this because you could see skills that make heavy use of managed MCPs. That’s cool, but downloading those skills on the fly might mean they only partially work because your coding harness can’t “see” the referenced MCP server. Just be cautious!

    We’re seeing an emerging “open knowledge layer” consisting of MCPs, skills, and plugins. I’d imagine we will see patterns evolve for how and when to use each component. What do you think of this lazy loading pattern I demonstrated here?

  • How to share agentic coding artifacts with your teammates

    Maybe it’s just me, but it feels like most of the agentic coding tools out there are single-player. An individual developer engages in a coding session with one or more agents, and the non-code “exhaust” (scratch files, harness-generated session docs) stays on the local machine. Oh sure, the code gets checked into a team-visible repo, and git commit body should list all the key changes. But are teammates losing out on the “thinking” that went on during that individual coding session? Maybe those core details are in a requirements/design doc somewhere, assuming people still write those! But how are teams of developers collaborating nowadays with all this agentic coding going on? Are we sharing those session-level artifacts and creating a team “brain”?

    I wondered if there was an easy way to take my Google Antigravity session artifacts and make them part of my commits. By default, any session/conversation docs—implementation plans, walkthroughs, chat transcripts—live in a machine folder. But I want those dragged into my local project folder so that they’re automatically pulled into commits, and thus visible to teammates. Then, teammates can use their own harness to understand my thinking or how I arrived at a certain decision for my code contribution.

    So I wrote an agent skill. It includes a Python script that explicitly moves the files when the coding session is done. The script does a git add of those files, including the full (and summary) session transcript. Specific to Antigravity, you might also build a sidecar, or something that runs in the background and continuously works. But this was a simpler choice.

    Let’s walk through the key bits. And then I’ll show it in action.

    In a hidden .agents folder, I’ve got an AGENTS.md file, a skills folder that contains a skill named team-sync, and then a SKILL.md along with a Python script used by the skill.

    We’ll start with the SKILL.md. It fires up as the agent finishes artifacts, and when explicitly triggered by the user.

    ---
    name: team-sync
    description: Synchronizes local Antigravity conversation history, transcripts, and artifacts (including a light summary) to the project repository for team sharing.
    ---
    
    # Team-Sync Custom Skill
    
    Use this skill to archive and upload your conversation's transcripts, artifacts, and a "light" human-readable summary into the Git repository, allowing other team members to quickly catch up on your agent steps, decisions, and outcomes.
    
    ## When to Use
    * Call this skill when a coding task, implementation plan, or refactoring conversation has finished successfully.
    * Run it before opening a Pull Request so that reviewers can inspect the execution logs and the conversation summary.
    
    ## Instructions for the Agent
    1. **Generate the Light Transcript (`summary.md`):**
       Create a file named `summary.md` in your local brain directory. The summary must follow this structure:
    
       ```markdown
       # Conversation Summary: [Objective/Topic]
       * **Date:** [Current Date]
       * **Conversation ID:** [ANTIGRAVITY_CONVERSATION_ID]
    
       ## TL;DR
       [A 1-2 sentence high-level summary of what was accomplished]
    
       ## Key Decisions & Rationale
       * **Decision:** [e.g., Using Python's shutil instead of bash commands]
         * **Why:** [e.g., Cross-platform safety and better permission handling]
    
       ## Most Interesting Event / "Aha" Moment
       [Capture any major back-and-forth conversation, user course corrections, pivot points, or model/user "aha" moments that defined the flow of this conversation.]
    
       ## Scope of Changes
       * **Files Modified/Created:** [List of files]
       * **Verification:** [How the changes were validated, command outputs, etc.]
    
       ## Learnings & Gotchas for the Team
       * [Any lessons learned about the API, codebase, or environment that others should know]
       ```
    
    2. **Verify Environment:**
       Ensure `ANTIGRAVITY_CONVERSATION_ID` is set in the environment.
    
    3. **Execute Sync Script:**
       Run the sync helper script:
       ```bash
       python3 .agents/skills/team-sync/scripts/sync.py
       ```
       *Note: Because `sync.py` automatically copies all `.md` files, your newly created `summary.md` will be synced to the workspace repository automatically.*
    
    4. **Report Status:**
       Confirm to the user that the summary and files have been successfully synced.
    
    

    The Python script does the work of actually copying files from Antigravity’s “brain” folder into my local project folder.

    #!/usr/bin/env python3
    import os
    import sys
    import shutil
    import subprocess
    from pathlib import Path
    
    def get_project_root() -> Path:
        # Find project root by looking for .agents or .git starting from CWD
        current = Path.cwd().resolve()
        for parent in [current] + list(current.parents):
            if (parent / ".agents").is_dir() or (parent / ".git").is_dir():
                return parent
        return current
    
    def find_most_recent_conversation(brain_root: Path) -> str:
        """Finds the most recently modified conversation subdirectory in the brain cache."""
        if not brain_root.is_dir():
            return None
        
        subdirs = []
        for p in brain_root.iterdir():
            # Conversation directories are 36-char UUIDs (plus optional custom name directories)
            if p.is_dir() and p.name != "scratch":
                try:
                    mtime = p.stat().st_mtime
                    subdirs.append((mtime, p.name))
                except OSError:
                    continue
                    
        if not subdirs:
            return None
        
        # Sort by modification time, newest first
        subdirs.sort(key=lambda x: x[0], reverse=True)
        return subdirs[0][1]
    
    def check_git_installed() -> bool:
        """Checks if git command-line tool is installed and available in PATH."""
        return shutil.which("git") is not None
    
    def main():
        print("=== Antigravity Team Sync ===")
        
        home_dir = Path.home()
        brain_root = home_dir / ".gemini" / "antigravity" / "brain"
        
        # 1. Retrieve conversation ID from environment or fallback
        conv_id = os.environ.get("ANTIGRAVITY_CONVERSATION_ID")
        if not conv_id:
            print("Notice: ANTIGRAVITY_CONVERSATION_ID env variable is not set.", file=sys.stderr)
            print("Attempting to auto-detect the most recent local conversation...", file=sys.stderr)
            conv_id = find_most_recent_conversation(brain_root)
            
            if not conv_id:
                print("ERROR: Could not locate any local conversation histories.", file=sys.stderr)
                sys.exit(1)
            print(f"Auto-detected conversation: {conv_id}")
        else:
            print(f"Active Conversation ID: {conv_id}")
        
        # 2. Define source paths
        source_brain_dir = brain_root / conv_id
        if not source_brain_dir.is_dir():
            print(f"ERROR: Local conversation directory not found at: {source_brain_dir}", file=sys.stderr)
            sys.exit(1)
            
        # 3. Define target paths
        proj_root = get_project_root()
        target_history_dir = proj_root / ".antigravity" / "history" / conv_id
        
        print(f"Source Directory: {source_brain_dir}")
        print(f"Target Directory: {target_history_dir}")
        
        # Create target directory
        try:
            target_history_dir.mkdir(parents=True, exist_ok=True)
        except PermissionError:
            print(f"ERROR: Permission denied. Cannot write to target directory: {target_history_dir}", file=sys.stderr)
            sys.exit(1)
        except Exception as e:
            print(f"ERROR: Failed to create target directory: {e}", file=sys.stderr)
            sys.exit(1)
        
        # 4. Copy Artifacts (markdown files and media in the main directory)
        copied_count = 0
        for file_path in source_brain_dir.iterdir():
            if file_path.is_file() and file_path.suffix in [".md", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mov"]:
                try:
                    shutil.copy2(file_path, target_history_dir / file_path.name)
                    print(f"-> Copied file: {file_path.name}")
                    copied_count += 1
                except PermissionError:
                    print(f"Warning: Permission denied copying file {file_path.name}")
                except Exception as e:
                    print(f"Warning: Failed to copy {file_path.name}: {e}")
                
        # 5. Copy Transcripts
        logs_dir = source_brain_dir / ".system_generated" / "logs"
        transcript_copied = False
        if logs_dir.is_dir():
            for log_file in ["transcript.jsonl", "transcript_full.jsonl"]:
                source_log = logs_dir / log_file
                if source_log.is_file():
                    try:
                        shutil.copy2(source_log, target_history_dir / log_file)
                        print(f"-> Copied log: {log_file}")
                        transcript_copied = True
                    except PermissionError:
                        print(f"Warning: Permission denied copying log {log_file}")
                    except Exception as e:
                        print(f"Warning: Failed to copy log {log_file}: {e}")
                    
        if not transcript_copied:
            print("Warning: No transcript files found in the conversation logs.")
            
        # 6. Git Synchronization
        git_dir = proj_root / ".git"
        if git_dir.is_dir():
            if not check_git_installed():
                print("\nWarning: Git repository detected, but git executable is not available in your PATH. Skipping Git commit.")
                print("Sync completed successfully!")
                return
                
            try:
                print("\nStaging files in Git...")
                subprocess.run(["git", "add", str(target_history_dir)], check=True, cwd=proj_root)
                print("Conversation history and transcripts staged in Git successfully!")
                print("You can now commit these staged history files together with your code updates.")
            except subprocess.CalledProcessError as e:
                print(f"\nWarning: Git command failed with error code {e.returncode}.", file=sys.stderr)
                print("Your files have been successfully copied locally, but could not be staged automatically in Git.", file=sys.stderr)
        else:
            print("\nNote: Not a Git repository (or .git not found). Skipping Git commit.")
            
        print("\nSync completed successfully!")
    
    if __name__ == "__main__":
        main()
    
    

    And finally, my AGENTS.md is quite simple and tells my harness when to mirror the core coding session artifacts.

    # Team Customization Rules
    
    These rules govern how Antigravity agents operate within this project repository to facilitate seamless team collaboration.
    
    ## 1. Artifact Mirroring
    To ensure that all design decisions, test verifications, and walkthroughs are visible to the team during code reviews:
    * **Mirroring Rule:** Whenever you create or modify an artifact (such as `implementation_plan.md`, `task.md`, or `walkthrough.md`) in the local user cache directory (`~/.gemini/antigravity/brain/<conversation-id>`), you MUST copy or write a duplicate version of that file to the workspace under `.antigravity/history/<conversation-id>/`.
    * **Git Commits:** These mirrored documents should be staged and committed to Git alongside the source code changes.
    
    ## 2. Conversation Telemetry & Syncing
    * If the user or agent needs to share the raw execution logs, terminal outputs, and thinking transcripts of a conversation:
      * Run the `team-sync` skill at the end of the session.
      * This will execute the `sync.py` script to collect the finalized `transcript.jsonl` and copy it to the same `.antigravity/history/<conversation-id>/` directory.
    
    

    This .agents folder could be part of some shared Git project or project-bootstrapping script. Here’s an example of how to use it.

    I created a local directory for my new coding project. Maybe I’m the first one on my team working on it. After copying the .agents folder into that directory, and running a git init, I opened Google Antigravity and started a new session/conversation. Notice that my Antigravity settings for this project shows the skill and agent rules loaded up automatically.

    At this point, I just did my work like always. I used Antigravity to build a Dart and Flutter-based web app for my fictitious hotel chain. I built this (and packaged it) over three distinct coding sessions.

    As each session went along, I noticed the session artifacts (like implementation plans and walkthroughs) showing up in the .antigravity folder within my project directory. And I ended each session with a request to run the team-sync skill. That ensured that my final chat transcript(s) showed up too.

    Let’s imagine that I’ve pushed all my changes into a team-shared repo. The next developer(s) can pull down the app code, along with the session history. Maybe they’re curious about how we arrived at our deployment choice. For example:

    Review the summary transcripts in this project and help me understand how the team decided to deploy to Cloud Run instead of Kubernetes.
    
    

    The result? The transcript summary is consulted and the developer sees the results of the conversation and trade-offs factored in.

    These key architectural decisions should be in other stateful artifacts like design docs. But given how fast teams are running now, the “requirements” are sprinkled throughout the code, test plans, session artifacts, and upfront docs.

    Maybe I’m solving a temporary problem and within weeks, all these coding tools will make it super easy to create a shared “brain” for software teams. But for now, I like that agent skills make it easy to extend Antigravity this way. How are you thinking about sharing “thinking” among your developers?

  • One prompt, four (sub)agents, and ninety seconds to get a working app

    I’m been thinking a lot about agent teams. You know, a set of AI agents that work together towards a goal. You might implement this within a single agent harness (see Garry Tan’s gstack for Claude Code) or with an orchestrator for multiple harnesses (see Google’s Scion project). I’ve been doing agent-at-a-time coding work thus far, but figured it was time to dive into more multi-agent workflows. The subagents natively built into Google Antigravity 2.0 and Antigravity CLI gave me the push.

    Why use subagents at all? Can’t I just use one coding agent to run through my work? Don’t we reach decision fatigue faster because of the coordination overhead? Yes, the engineering work shifts from more sequential human tasks to assigning and reviewing the work of multiple AI agents. There’a tax, no doubt. But there are benefits to deconstructing the work in a way that an agent team can tackle it. You limit the change surface (by giving each agent a persona, MCPs, and skills to address an isolated piece of work), consume fewer tokens (by not processing the entire context for all tasks), and go faster (by parallelizing work that tolerates it).

    Sound intimidating? It’s felt that way to me. But Claude Code and Antigravity CLI make it less scary. Let’s talk about Google Antigravity. This agent-first harness (used via a desktop app, CLI, or IDE) provides out-of-the-box support for pre-built subagents (e..g browser and research), creating custom subagents, communicating between subagents, and lifecycle management of subagents.

    I spent last weekend playing around with prompts to create an agent team that could build a backend API, a corresponding frontend API, and some unit tests. To make this process go as fast as possible, I’m also basically doing YOLO mode where I let Antigravity run any terminal command and proceed without my intervention. Here’s the setup in the Antigravity desktop app.

    So what’s the prompt? Here’s what I came up with.

    Let's build a hotel room booking app for Seroter Hotels consisting of a Go backend API and a web frontend. 
    
    First, launch the **Engineering Manager** agent to design the API and frontend, saving the design and a Mermaid diagram into an artifact called 'architecture.md'. 
    
    Once the design is ready, launch three agents in parallel:
    1. **Test Manager**: Write a simple API test plan and append it to 'architecture.md'.
    2. **Backend Engineer**: Build a clean Go REST API with standard error handling based on the design.
    3. **Frontend Engineer**: Build a responsive web UI using a simple CSS framework like Tailwind to interact with the API (skip UI testing).
    
    As soon as the Test Manager finishes the plan, have them hand it off to the Backend Engineer, who reads the plan from 'architecture.md' and adds the Go tests to the code. After both engineers finish building, the Test Manager runs the tests. Finally, spin up both components and a browser so I can test the live app.
    
    

    Let’s be clear. In real-life, you’d provide each subagent with significantly more context (tools, skills, data structures, persona characteristics) or trigger some back-and-forth so the subagent could gather a rich set of requirements from you. But this works to prove the point.

    Here’s a video recording of the result, at 1.0x speed. It takes all of ninety seconds from when Antigravity starts working until the frontend and backend services are running. Below the video, I’ll deconstruct some key parts of the response.

    Right after Antigravity got started, we saw the Implementation plan. This artifact defined each subagent and the sequence they would follow.

    I steered work to a shared architecture.md file. When we view that, we see the decision log used by the agents.

    As the subagents got to work, the main task list kept getting updated automatically.

    Each subagent had its own context and workstream. Here, the backend agent records its implementation work.

    It’s mesmerizing to watch the subagents start up and do their thing! On this right pane, you can see each subagent come up, printouts of each thing it’s doing, and finally when the root agent kills them off. I’m also able to track all the artifacts and any background tasks going on.

    When it finished up, the main conversation recapped the final details and showed me how to access the app endpoints.

    Because Antigravity knows how to work the Chrome browser, it also automatically launched a browser window and showed me the web front end. Looks great!

    The Antigravity CLI supports virtually the same workflow.

    I think it completed even faster than the desktop app! Same agent team. Same artifacts and workflow.

    Pretty awesome. I’m sold. Download Google Antigravity, copy my prompt, make it better, try this for yourself. To me, building software has never been this fun.

  • How to force your custom agent to stop and seek human approval

    Autonomous agents are cool and all, but we all know there are plenty of circumstances where we want human review. Your custom built agent might have instructions to stop and get input, but that doesn’t guarantee it happens. The Agent Development Kit recently added a human-in-the-loop feature, and I decided to try it out.

    Let’s build an agent that generates product tutorials on-demand. The agent takes in a request, and uses tools to ground its output. Before it publishes a tutorial, it requires explicit approval from outside its own workflow. ADK now offers both a simple boolean approval, and a more sophisticated option. I’ll test out both, using the built-in web UI and the raw REST API. FYI, all my source code is here.

    First, here’s the architecture:

    Simple Tutorial Agent

    First, there’s a basic tutorial agent that uses the simple action confirmation.

    Below is the core agent code. The agent definition (at the bottom) has instructions, a model, and some tools. I’m using the Google Search tool and our Developer Knowledge API MCP server tools. The former grounds results in Google Search results, and the latter points to Google’s core documentation.

    func createTutorialAgent(ctx context.Context, llmModel model.LLM) (agent.Agent, error) {
    	transport, err := mcpTransport(ctx)
    	if err != nil {
    		return nil, err
    	}
    	mcpToolSet, _ := mcptoolset.New(mcptoolset.Config{Transport: transport})
    
    	publishTool, _ := functiontool.New(functiontool.Config{
    		Name:                "publish_tutorial",
    		Description:         "Publish the tutorial. Requires user confirmation.",
    		RequireConfirmation: true,
    	}, publishTutorial)
    
    	searchAgent, _ := llmagent.New(llmagent.Config{
    		Name:        "search_agent",
    		Model:       llmModel,
    		Tools:       []tool.Tool{geminitool.GoogleSearch{}},
    		Instruction: "Search the web.",
    	})
    
    	return llmagent.New(llmagent.Config{
    		Name:        "tutorial_agent",
    		Model:       llmModel,
    		Description: "Simple tutorial agent.",
    		Instruction: `You are a technical writer. Draft a Google Cloud tutorial based on user requests.
    Once the draft is complete, show it to the user and ask them if they would like to start the publishing process. The 'publish_tutorial' tool will then handle the review and approval.`,
    		Tools:    []tool.Tool{agenttool.New(searchAgent, nil), publishTool},
    		Toolsets: []tool.Toolset{mcpToolSet},
    	})
    }
    

    Notice that the publish_tutorial tool has a RequireConfirmation attribute. The corresponding publishTutorial function only gets called for a true result.

    To start the agent (after setting environment variables for Google Cloud project and any credentials) for the built-in web UI, I used this command:

    ADK_AGENT=tutorial go run . web api webui
    

    This provides me a localhost web UI to explore my agent. I asked for a tutorial and saw it use the right tools to generate a response.

    I then ask to start the publishing process, and the confirmation flow kicks in. Notice the small free text box asking for my response.

    The accepts a JSON payload of {"confirmed": true} only. When I provide that value, the control returns to the agent and it publishes the Markdown tutorial to the local folder.

    Let’s do the same workflow with the REST API.

    This time, I started up the agent with this command to get the API endpoint only:

    ADK_AGENT=tutorial go run . web api
    

    The http://localhost:8080/api/list-apps endpoint shows that I have one “app” named tutorial_agent. To start, I need to create a session with my agent. I’ll set the username and (optionally) the session ID. If I just post to the session endpoint, I get back a random session ID.

    curl -X POST http://localhost:8080/api/apps/tutorial_agent/users/u_123/sessions/s_123 \
      -H "Content-Type: application/json"
    

    Now I can send in a prompt to my agent. Notice that I’m passing in the user ID and session ID from above.

    curl -X POST http://localhost:8080/api/run \
    -H "Content-Type: application/json" \
    -d '{
    "appName": "tutorial_agent",
    "userId": "u_123",
    "sessionId": "s_123",
    "newMessage": {
        "role": "user",
        "parts": [{
        "text": "create a tutorial for deploying a container to an existing GKE Autopilot cluster. use the cli."
        }]
    }
    }'
    

    I get back a pile of JSON that includes the tutorial itself. Like with the web UI experience, I’m asked if I want to kickstart the publishing process. I send in a follow-on message like this:

    curl -X POST http://localhost:8080/api/run \
    -H "Content-Type: application/json" \
    -d '{
    "appName": "tutorial_agent",
    "userId": "u_123",
    "sessionId": "s_123",
    "newMessage": {
        "role": "user",
        "parts": [{
        "text": "yes, start the process to publish the tutorial"
        }]
    }
    }'
    

    There’s another pile of JSON to parse through, and I need to find the ID tied to the adk_request_confirmation object.

    [ ... "content":{"role":"model","parts":[{"functionCall":{"id":"adk-e6d32a67-c1d4-46b8-87f6-6922a3af3d98","name":"adk_request_confirmation","args":{"originalFunctionCall":{"id":"adk-0e6c0394-a22b-4d5f-9af6-e27284bc175f","args":{"content":"# Deploy a Container to an Existing GKE Autopilot Cluster using the gcloud CLI and kubectl\n\nThis tutorial guides you through" ...]
    

    The next REST API call includes the ID from above and the familiar “confirmed” property.

    curl -X POST http://localhost:8080/api/run \
    -H "Content-Type: application/json" \
    -d '{
     "appName": "tutorial_agent",
     "userId": "u_123",
     "sessionId": "s_123",
        "newMessage": {
            "role": "user",
            "parts": [
                {
                    "functionResponse": {
                        "name": "adk_request_confirmation",
                        "id": "adk-e6d32a67-c1d4-46b8-87f6-6922a3af3d98",
                        "response": {
                            "confirmed": true
                        }
                    }
                }
            ]
        }
    }'
    

    After this call, I see the Markdown tutorial written to disk.

    Advanced Tutorial Agent

    This agent itself looks nearly the same as the prior one, but has a more sophisticated toolset. Here’s the agent code, with the confirmation behavior in the downstream function, not the function tool.

    func publishTutorialAdvanced(ctx tool.Context, args PublishTutorialArgs) (map[string]any, error) {
    	confirmation := ctx.ToolConfirmation()
    	if confirmation == nil {
    		ctx.RequestConfirmation(
    			"Reviewer approval required to publish.",
    			map[string]any{
    				"status": "approved", // approved, rejected, update
    				"notes":  "",
    			},
    		)
    		return map[string]any{"status": "Pending reviewer approval."}, nil
    	}
    
    	payload := confirmation.Payload.(map[string]any)
    	status, _ := payload["status"].(string)
    	notes, _ := payload["notes"].(string)
    
    	if strings.ToLower(status) == "approved" {
    		if !strings.HasSuffix(args.Filename, ".md") {
    			args.Filename += ".md"
    		}
    
    		if err := os.MkdirAll("tutorials", 0755); err != nil {
    			return nil, fmt.Errorf("failed to create tutorials directory: %w", err)
    		}
    
    		fullPath := filepath.Join("tutorials", filepath.Base(args.Filename))
    		if err := os.WriteFile(fullPath, []byte(args.Content), 0644); err != nil {
    			return nil, fmt.Errorf("failed to save tutorial: %w", err)
    		}
    		return map[string]any{"status": "published", "path": fullPath}, nil
    	}
    
    	return map[string]any{"status": status, "notes": notes}, nil
    }
    
    func createAdvancedTutorialAgent(ctx context.Context, llmModel model.LLM) (agent.Agent, error) {
    	transport, err := mcpTransport(ctx)
    	if err != nil {
    		return nil, err
    	}
    
    	mcpToolSet, err := mcptoolset.New(mcptoolset.Config{Transport: transport})
    	if err != nil {
    		return nil, err
    	}
    
    	publishTool, err := functiontool.New(functiontool.Config{
    		Name:        "publish_tutorial_advanced",
    		Description: "Publishes the tutorial. Requires status and notes from a reviewer.",
    	}, publishTutorialAdvanced)
    	if err != nil {
    		return nil, err
    	}
    
    	searchAgent, _ := llmagent.New(llmagent.Config{
    		Name:        "search_agent",
    		Model:       llmModel,
    		Description: "Web search agent.",
    		Instruction: "Search the web for info.",
    		Tools:       []tool.Tool{geminitool.GoogleSearch{}},
    	})
    
    	return llmagent.New(llmagent.Config{
    		Name:        "advanced_tutorial_agent",
    		Model:       llmModel,
    		Description: "Tutorial agent with advanced confirmation.",
    		Instruction: `You are a technical writer. Draft a Google Cloud tutorial based on user requests.
    Once the draft is complete, show it to the user and ask them if they would like to start the publishing process.
    The 'publish_tutorial_advanced' tool will then handle the multi-stage review and approval.
    If the reviewer provides feedback (status 'update' or 'rejected'), revise the draft and try again.`,
    		Tools:    []tool.Tool{agenttool.New(searchAgent, nil), publishTool},
    		Toolsets: []tool.Toolset{mcpToolSet},
    	})
    }
    

    Notice the RequestConfirmation command in the publishTutorialAdvanced function. Here, we have more control over sending notifications or doing whatever to solicit the confirmation approval.

    Note that I tried to get this agent to work with the built-in web UI but couldn’t get it. The same text box pops up for approval confirmation, but no values seem to get the agent to proceed. The only way I got this to work was with a bunch of custom handling in the agent.

    So let’s go straight to the REST API. I started the agent using this command:

    ADK_AGENT=advanced go run . web api
    

    When I check the list-apps endpoint, I see that my agent app is called advanced_tutorial_agent. So just like above, we start by creating a session with the correct agent name.

    curl -X POST http://localhost:8080/api/apps/advanced_tutorial_agent/users/u_123/sessions/s_123 \
      -H "Content-Type: application/json"
    

    Now I can prompt the agent with a tutorial request.

    curl -X POST http://localhost:8080/api/run \
    -H "Content-Type: application/json" \
    -d '{
    "appName": "advanced_tutorial_agent",
    "userId": "u_123",
    "sessionId": "s_123",
    "newMessage": {
        "role": "user",
        "parts": [{
        "text": "create a cli tutorial for creating a pub/sub topic."
        }]
    }
    }'
    

    My custom tutorial comes back in a second, and I can kick off the publishing process.

    curl -X POST http://localhost:8080/api/run \
    -H "Content-Type: application/json" \
    -d '{
    "appName": "advanced_tutorial_agent",
    "userId": "u_123",
    "sessionId": "s_123",
    "newMessage": {
        "role": "user",
        "parts": [{
        "text": "yes start the publishing process"
        }]
    }
    }'
    

    After parsing the JSON response, I again find the adk_request_confirmation object and snag the ID.

    [ ... "content":{"role":"model","parts":[{"functionCall":{"id":"adk-7744ece0-a82d-4796-96b7-f376d96bf925","name":"adk_request_confirmation","args":{"originalFunctionCall":{"id":"adk-30c04217-3c0b-4519-a866-b914e9357872","args":{"content":"## Creating a Pub/Sub Topic with the `gcloud` CLI\n\nThis tutorial guides you through creating a Google Cloud Pub/Sub topic using the" ...]
    

    In the next REST call, see that I can now send a richer payload back to the agent and react accordingly.

    curl -X POST http://localhost:8080/api/run \
    -H "Content-Type: application/json" \
    -d '{
     "appName": "advanced_tutorial_agent",
     "userId": "u_123",
     "sessionId": "s_123",
        "newMessage": {
            "role": "user",
            "parts": [
                {
                    "functionResponse": {
                        "name": "adk_request_confirmation",
                        "id": "adk-7744ece0-a82d-4796-96b7-f376d96bf925",
                        "response": {
                            "confirmed": true,
                            "payload": {
                                "status": "approved",
                                "notes": "Tutorial looks great, proceed with publishing"
                            }
                        }
                    }
                }
            ]
        }
    }'
    

    And once again, I get a Markdown file saved to disk.

    After this initial demo, I may play with putting an A2UI interface on top of the agent, or even building it again using Genkit Go. But either way, make sure you give yourself some discrete moments where your agent stops and asks for input!

  • You can now easily call LLMs from your messaging engine. Should you?

    Throughout my tech career, I’ve been taught to be cautious about where I stash business logic. I now reflexively think about putting important rules and considerations into central services. Could you bury it in stored procedures or database triggers? Be careful, say Internet people. Same goes for frontend components. Middleware? It’s easy to stash sneaky-important rules in all sorts of interesting platform extensions. Google Cloud just made it very simple to invoke an LLM from a Pub/Sub topic or subscription. When might I do that? And what does it look like? I’ll show you.

    In my classic ESB days, meaningful logic could show up in data processing pipelines, message transformations, long-running orchestrations, and even code callouts. You couldn’t avoid it. Cloud messaging services like Pub/Sub also have plenty of spots where you can add some decision logic. Where should I route this message? Based on what criteria? How long should I retain messages after delivered? What cause a retry? And now we have Single Message Transforms (SMTs) where I can do simple manipulation of the data stream. This is handy if you want to hand the downstream consumer a sanitized message with sensitive data removed, clean up data formatting, or even enrich the message with new data points.

    A brand new SMT is the AI Inference SMT. Using this, I can call Google, partner, or custom models. What’s also cool is that I can apply this at the Topic level—this means every subscription gets the altered message—or apply it for specific Subscriptions. Let’s set one up.

    What’s the scenario? In my former days as a solutions architect, I worked with a massive ERP implementation where key events were sent out and consumed downstream. One of those was “employee” events for hiring new staff. What if we had a Pub/Sub topic that took in a “new employee” event, and one of the many downstream subscribers sends welcome emails? I could call the LLM in that subscription to hand a pre-built welcome note to the destination system. Of course, you might also craft that AI-assisted welcome note in the subscriber’s own service. Might depend on how capable that downstream system is.

    Once you’ve got a Google Cloud account and have the right permissions set up, we can get to work. This is all doable in our gcloud CLI, but I’ll use the web console to demonstrate everything here. I start by going to the Google Cloud Console and Pub/Sub section to create a new Topic.

    You’ll see there’s a Transforms section, but I don’t want to apply a transform to every message. So I’ll create this topic without one.

    Next, I create a subscription. Pub/Sub is one of the most unique cloud messaging services because it supports push, pull, and even message replay. This sub is a simple pull-based subscription.

    Further down this page, I see the option again to add Transforms. The AI SMT expects content in a specific structure, but I don’t want to assume the upstream system is going to send messages in that format. So I can daisy-chain SMT where I first re-format the message in a JavaScript User-Defined Function, and then run the AI SMT.

    I click the Add Transform button and choose Javascript UDF. I set the function name as transform and populate the JavaScript code that sticks the original employee event into the prompt that the AI SMT expects.

    Here’s the code.

    function transform(message, metadata) {
      var incomingData = JSON.parse(message.data);
      var promptText = "Generate a personalized welcome email factoring in the person's name, location, and role. " + JSON.stringify(incomingData);
    
      // No wrapper object here
      var aiInferenceObject = {
        model: "google/gemini-2.5-flash",
        messages: [
          { role: "user", content: promptText }
        ]
      };
    
      return {
        data: JSON.stringify(aiInferenceObject),
        attributes: message.attributes || {}
      };
    }
    

    I like that I can test this inline. The Test Transforms button takes me to an interface where I can see if my UDF does its job. After plugging in an example message, I click the Test button and see the transformation result.

    Now I add the AI SMT to the sequence of transforms. I chose AI Inference and then picked the managed Gemini model from Vertex AI. I could have also chosen any number of managed models from DeepSeek, Mistral, or Anthropic. Or even chosen my own model endpoint. I also picked a service account that had access to Vertex AI models. And I set a couple of parameters for max token count and temperature.

    Now when I choose the Test Transforms button again, I see both SMTs. After popping the same value into the transform SMT, I click Test. It’s pretty great than when I click the second SMT (AI Inference), the results from the first pre-populates the box!

    The transformed message includes the desired “welcome” note. In real life, I’d have crafted a better prompt to ensure exactly what I wanted, but this shows what’s possible.

    Once I save this subscription, it works like any other. Now I’ve got an LLM-enhanced Google Cloud Pub/Sub!

    I like seeing these sorts of capabilities baked into Cloud services. Integrating LLMs into your architecture doesn’t need to be difficult. But make good choices, and figure out when your business logic needs to be explicitly called out, versus buried inside service configurations!

  • Full-stack vibe coding made easy

    Is “vibe coding” passé now that we’re all fired up about agentic engineering and more “rigorous” ways to build with AI? Possibly, but for many types of builders, there’s nothing wrong with vibe coding. Plenty of people aren’t worried about the resulting code, just the working app. There’s a time and place for that!

    One place is Google AI Studio. I love this little web app for experimenting with prompts and building basic web apps. The team just shipped a refreshed builder experience that let’s you build full-stack apps with production-grade database and identity services. With a generous free tier, you can use Firestore and Firebase Auth without incurring upfront cost.

    Let’s try it out from scratch. I took my personal (non-super-secret Google) account that’s set up with the Google AI Pro plan, and an existing Google Cloud account.

    After navigating to Google AI Studio, I chose the Build tab.

    There are all these pills below the center chatbox where I can pick tools for using Google Search or Maps data, generating videos from text, or (now) adding database and auth to our app.

    I wrote a prompt to generate an app for tracking my hotel stays. Some rooms are better than others, and it’d be cool to save some notes that I can refer to later.

    After clicking “Build”, AI Studio gets to work. Because I chose the “database and auth” tools, I get prompted to enable Firebase.

    It’s one click! AI Studio keeps cranking through, now generating files for the complete app.

    It takes a few minutes to build out the whole app, and then I see the resulting app preview. The chat box tells me a summary of what it created.

    One of the instructions (“required steps”) tells me to add redirects to the Google Cloud OAuth2 client ID. When I clicked the link, those redirects were already pre-loaded. No action needed.

    Checking the Google Cloud (or Firebase) console also reveals that a new Firestore database exists.

    Back in Ai Studio, I click the sign-in link and immediately get a redirect to log-in with Google. Thanks Firebase Authentication!

    Once I’m logged in, I can add a new hotel stay entry.

    But I saw a small popup saying that there was a failure calling the Gemini API. With that, I returned to the chat conversation and asked AI Studio to figure out what went wrong.

    After fixing the Gemini error, I tried the app again. This time it worked, and I saw my saved record and a pinpoint on the map.

    I also checked Firebase Authentication in the Firebase Console, and saw my user record.

    Cool! But I couldn’t find any data records in Firestore. Was it really saving the data? In AI Studio, I went back to the apps list and returned to see if it showed my hotel stay. It did, but this felt like a local cache. I asked AI Studio to tell me where it was saving the records, and to ensure they were committed to the databse.

    Perfect. This seems to fix the problem. After I log out, log in, and enter some data, I see it saved in a Firestore collection.

    Amazing. While I can edit my code in Google AI Studio, I don’t want to. That’s not what this surface is for. Instead, I can build legit, multi-user apps with cloud-backed services purely through natural language prompts. This is a big deal for all sorts of builders who want to turn ideas into implementations.

  • My custom agent used 87% fewer tokens when I gave it Skills for its MCP tools

    Today’s web apps don’t seem particularly concerned about resource consumption. The simplest site seems to eat up hundreds of MB of memory in my browser. We’ve probably gotten a bit lazy with optimization since many computers have horsepower to spare. But when it comes to LLM tokens, we’re still judicious. Most of us have bumped into quotas or unexpected costs!

    I see many examples of introducing and tuning MCPs and skills for IDEs and agentic tools. But what about the agents you’re building? What’s the token impact of using MCPs and skills for custom agents?

    I tried out six solutions with the Agent Development Kit (Python) and counted my token consumption for each. The tl;dr? A well-prompted Gemini with zero tools or skills is successful with the fewest tokens consumed, with the second best option being MCP + skills. Third-best in token consumption is raw Gemini plus skills.

    I trust that you can find a thousand ways to do this better than me, but here’s a table with the best results from multiple runs of each of my experiments. The title of the post refers to the difference between scenarios 2 and 3.

    ScenarioAgent DescriptionTurnsTokens
    0Instructions only, built in code execution tool71,286
    1Uses BigQuery MCP913,763
    2Uses BigQuery, AlloyDB, Cloud SQL MCPs29328,083
    3Uses BigQuery, AlloyDB, Cloud SQL MCPs with skill539,622
    4Use BigQuery MCP and a skill56,653
    5Instruction, skill, and built-in code execution tool2764,444

    What’s the problem to solve?

    I want an agent that can do some basic cloud FinOps for me. I’ve got a Google Cloud BigQuery table that is automatically populated with billing data for items in my project.

    Let’s have an agent that can find the table and figure out what my most expensive Cloud Storage buckets are so far this month. This could be an agent we call from a platform like Gemini Enterprise so that our finance people (or team leads) could quickly get billing info.

    A look at our agent runner

    The Agent Development Kit (ADK) offers some powerful features for building robust agents. It has native support for MCPs and skills, and has built-in tools for services like Google Search.

    While the ADK does have a built-in BigQuery tool, I wanted to use the various managed MCP servers Google Cloud offers.

    Let’s look at some code. One file to start. The main.py file runs our agent and count the tokens from each turn of the LLM. The token counting magic was snagged from an existing sample app. For production scenarios, you might want to use our BigQuery Agent Analytics plugin for ADK that captures a ton of interesting data points about your agent runs, including tokens per turn.

    Here’s the main.py file:

    import asyncio
    import time
    import warnings
    
    import agent
    from dotenv import load_dotenv
    from google.adk import Runner
    from google.adk.agents.run_config import RunConfig
    from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
    from google.adk.cli.utils import logs
    from google.adk.sessions.in_memory_session_service import InMemorySessionService
    from google.adk.sessions.session import Session
    from google.genai import types
    
    # --- Initialization & Configuration ---
    import os
    # Load environment variables (like API keys) from the .env file
    load_dotenv(os.path.join(os.path.dirname(__file__), '.env'), override=True)
    # Suppress experimental warnings from the ADK
    warnings.filterwarnings('ignore', category=UserWarning)
    # Redirect agent framework logs to a temporary folder
    logs.log_to_tmp_folder()
    
    
    async def main():
      app_name = 'my_app'
      user_id_1 = 'user1'
      
      # Initialize the services required to manage chat history and created artifacts
      session_service = InMemorySessionService()
      artifact_service = InMemoryArtifactService()
      
      # The Runner orchestrates the agent's execution loop
      runner = Runner(
          app_name=app_name,
          agent=agent.root_agent,
          artifact_service=artifact_service,
          session_service=session_service,
      )
      
      # Create a new session to hold the conversation state
      session_1 = await session_service.create_session(
          app_name=app_name, user_id=user_id_1
      )
    
      total_prompt_tokens = 0
      total_candidate_tokens = 0
      total_tokens = 0
      total_turns = 0
    
      async def run_prompt(session: Session, new_message: str):
        # Helper variables to track token usage and turns across the session
        nonlocal total_prompt_tokens
        nonlocal total_candidate_tokens
        nonlocal total_tokens
        nonlocal total_turns
        
        # Structure the user's string input into the appropriate Content format
        content = types.Content(
            role='user', parts=[types.Part.from_text(text=new_message)]
        )
        print('** User says:', content.model_dump(exclude_none=True))
        
        # Stream events back from the Runner as the agent executes its task
        async for event in runner.run_async(
            user_id=user_id_1,
            session_id=session.id,
            new_message=content,
        ):
          total_turns += 1
          
          # Print intermediate steps (text, tool calls, and tool responses) to the console
          if event.content and event.content.parts:
            for part in event.content.parts:
              if part.text:
                print(f'** {event.author}: {part.text}')
              if part.function_call:
                print(f'** {event.author} calls tool: {part.function_call.name}')
                print(f'   Arguments: {part.function_call.args}')
              if part.function_response:
                print(f'** Tool response from {part.function_response.name}:')
                print(f'   Response: {part.function_response.response}')
    
          if event.usage_metadata:
            total_prompt_tokens += event.usage_metadata.prompt_token_count or 0
            total_candidate_tokens += (
                event.usage_metadata.candidates_token_count or 0
            )
            total_tokens += event.usage_metadata.total_token_count or 0
            print(
                f'Turn tokens: {event.usage_metadata.total_token_count}'
                f' (prompt={event.usage_metadata.prompt_token_count},'
                f' candidates={event.usage_metadata.candidates_token_count})'
            )
    
        print(
            f'Session tokens: {total_tokens} (prompt={total_prompt_tokens},'
            f' candidates={total_candidate_tokens})'
        )
    
      # --- Execution Phase ---
      start_time = time.time()
      print('Start time:', start_time)
      print('------------------------------------')
      
      # Send the initial prompt to the agent and trigger the run loop
      await run_prompt(session_1, 'Find the top 3 most expensive Cloud Storage buckets in our March 2026 billing export for project seroter-project-base')
      print(
          await artifact_service.list_artifact_keys(
              app_name=app_name, user_id=user_id_1, session_id=session_1.id
          )
      )
      end_time = time.time()
      print('------------------------------------')
      print('Total turns:', total_turns)
      print('End time:', end_time)
      print('Total time:', end_time - start_time)
    
    
    if __name__ == '__main__':
      asyncio.run(main())
    

    Nothing too shocking here. But this gives me a fairly verbose output that lets me see how many turns and tokens each scenario eats up.

    Scenario 0: Raw agent (no MCP, no tools) using Python code execution

    In this foundational test, what if we ask the agent to answer the question without the help of any external tools? All it can do is write and execute Python code on the local machine using a built-in tool. This flavor is only for local dev, as there are more production-grade isolation options for running code.

    Here’s the agent.py for this base scenario. I’ve got a decent set of instructions to guide the agent for how to write code to find and query the relevant table.

    from google.adk.agents import LlmAgent
    from google.adk.skills import load_skill_from_dir
    from google.adk.tools import skill_toolset
    from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams
    from google.adk.auth.auth_credential import AuthCredential, AuthCredentialTypes, ServiceAccount
    from fastapi.openapi.models import OAuth2, OAuthFlows, OAuthFlowClientCredentials
    from google.adk.code_executors.unsafe_local_code_executor import UnsafeLocalCodeExecutor
    
    
    # --- Agent Definition ---
    
    # --- Scenario 0: Raw Agent using Python Code Execution for Discovery and Analysis ---
    root_agent = LlmAgent(
        name="data_analyst_agent",
        model="gemini-3.1-flash-lite-preview",
        instruction="""You are a data analyst. 
        CRITICAL: You have NO TOOLS registered. NEVER attempt a tool call or function call (like `list_datasets` or `bq_list_dataset_ids`). 
        You MUST perform all technical tasks by writing and executing Python code blocks in markdown format (e.g., ` ```python `) using the `google-cloud-bigquery` client library.
        
        1. DISCOVERY: If you don't know the table names, you MUST write and execute Python code to list datasets and tables.
        2. ANALYSIS: Use Python to query data and perform analysis.
        3. NO HYPOTHETICALS: NEVER provide hypothetical, example, or placeholder results. Only show data you have actually retrieved via code execution.
        ALWAYS explain the approach you used to access BigQuery.""",
        code_executor=UnsafeLocalCodeExecutor()
    )
    

    This scenario runs quickly (about 14 seconds on each test), took five turns, and consumed 1786 tokens. In my half-dozen runs, I saw as many as nine turns, and as few as 1286 tokens consumed.

    This was the most efficient way to go of any scenario.

    Scenario 1: Agent with BigQuery MCP

    Love it or hate it, MCP is going to remain a popular way to connect to external systems. Instead of needing to understand every system’s APIs, MCP tools give us a standard way to do things.

    I’m using our fully managed remote MCP Server for BiQuery. This MCP server exposes a handful of useful tools for discovery and data retrieval. Note that the awesome open source MCP Toolbox for Databases is another great way to pull 40+ data sources into your agents.

    The agent.py for Scenario 1 looks like this. You can see that I’m initializing the auth with my application default credentials and setting up the correct OAuth flow. The agent itself has a solid instruction to steer the MCP server. Note that I left an old, unoptimized instruction in there. That old instruction resulted in dozens of turns and up to 600k tokens consumed!

    from google.adk.agents import LlmAgent
    from google.adk.skills import load_skill_from_dir
    from google.adk.tools import skill_toolset
    from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams
    from google.adk.auth.auth_credential import AuthCredential, AuthCredentialTypes, ServiceAccount
    from fastapi.openapi.models import OAuth2, OAuthFlows, OAuthFlowClientCredentials
    from google.adk.code_executors.unsafe_local_code_executor import UnsafeLocalCodeExecutor
    
    # --- BigQuery MCP Configuration ---
    
    # Configure authentication for the BigQuery MCP server
    bq_auth_credential = AuthCredential(
        auth_type=AuthCredentialTypes.SERVICE_ACCOUNT,
        service_account=ServiceAccount(
            use_default_credential=True,
            scopes=["https://www.googleapis.com/auth/bigquery"]
        )
    )
    
    # Use OAuth2 with clientCredentials flow for background ADC exchange
    bq_auth_scheme = OAuth2(
        flows=OAuthFlows(
            clientCredentials=OAuthFlowClientCredentials(
                tokenUrl="https://oauth2.googleapis.com/token",
                scopes={"https://www.googleapis.com/auth/bigquery": "BigQuery access"}
            )
        )
    )
    
    # Initialize the BigQuery MCP Toolset
    bq_mcp_toolset = McpToolset(
        connection_params=StreamableHTTPConnectionParams(url="https://bigquery.googleapis.com/mcp"),
        auth_scheme=bq_auth_scheme,
        auth_credential=bq_auth_credential,
        tool_name_prefix="bq"
    )
    
    # --- Agent Definition ---
    
    # --- Scenario 1: Using Gemini to get data from BigQuery with MCP ---
    root_agent = LlmAgent(
        name="data_analyst_agent",
        model="gemini-3.1-flash-lite-preview",
        ##instruction="You are a data analyst. Use BigQuery to find and analyze data. Do not give the user steps to run themselves, or ask for further information, but explore options and execute any commands yourself. Explain the approach you used to access BigQuery. ",
        instruction="""You are a data analyst. Use BigQuery to find and analyze data. 
        To minimize token usage and time, follow these rules:
        1. DISCOVERY: If you are unsure of a table's exact schema, ALWAYS query `INFORMATION_SCHEMA.COLUMNS` first to find the right fields before writing complex data queries.
        2. EFFICIENCY: When exploring data to understand its structure, ALWAYS use `LIMIT 5` to avoid returning massive payloads.
        3. AUTONOMY: Do not ask the user for table names or steps; explore the datasets yourself and execute the final queries.
        4. EXPLANATION: Briefly explain the steps you took to find the answer.""",
        tools=[bq_mcp_toolset]
    )
    

    Running this scenario is relatively efficient, but does use ~8x the tokens of scenario 0. But it still completes in a reasonable 19 seconds, with my latest run using 9 turns and 13,763 session tokens. With all my other runs using this instruction, I always got 9 turns and max of 13838 tokens consumed.

    Scenario 2: Agent with BigQuery MCP and extra MCPs

    Most systems experience feature creep over time. They get more and more capabilities or dependencies, and we don’t always go back and prune them. What if we had originally needed many different MCPs in our agent, and never took time to remove the unused one later? You may start feeling it in your input context. All those tool descriptions are scanned and held during each turn.

    This update to agent.py now initializes two other MCP servers for other data sources.

    # --- GCP Platform Auth (Shared for Cloud SQL and AlloyDB) ---
    
    # Configure authentication for MCP servers requiring cloud-platform scope
    gcp_platform_auth_credential = AuthCredential(
        auth_type=AuthCredentialTypes.SERVICE_ACCOUNT,
        service_account=ServiceAccount(
            use_default_credential=True,
            scopes=["https://www.googleapis.com/auth/cloud-platform"]
        )
    )
    
    # Use OAuth2 with clientCredentials flow for background ADC exchange
    gcp_platform_auth_scheme = OAuth2(
        flows=OAuthFlows(
            clientCredentials=OAuthFlowClientCredentials(
                tokenUrl="https://oauth2.googleapis.com/token",
                scopes={"https://www.googleapis.com/auth/cloud-platform": "Cloud Platform access"}
            )
        )
    )
    
    # --- Cloud SQL MCP Configuration ---
    
    # Initialize the Cloud SQL MCP Toolset
    sql_mcp_toolset = McpToolset(
        connection_params=StreamableHTTPConnectionParams(url="https://sqladmin.googleapis.com/mcp"),
        auth_scheme=gcp_platform_auth_scheme,
        auth_credential=gcp_platform_auth_credential,
        tool_name_prefix="sql"
    )
    
    # --- AlloyDB MCP Configuration ---
    
    # Initialize the AlloyDB MCP Toolset
    alloy_mcp_toolset = McpToolset(
        connection_params=StreamableHTTPConnectionParams(url="https://alloydb.us-central1.rep.googleapis.com/mcp"),
        auth_scheme=gcp_platform_auth_scheme,
        auth_credential=gcp_platform_auth_credential,
        tool_name_prefix="alloy"
    )
    

    Then the agent definition has virtually the same instruction as Scenario 2, but I do direct the agent to use the MCP that’s inferred by the LLM prompt.

    # --- Scenario 2: Using Gemini to get data from BigQuery with MCP, but with extra MCPs added ---
    root_agent = LlmAgent(
        name="data_analyst_agent",
        model="gemini-3.1-flash-lite-preview",
        #instruction="You are a data analyst. Use BigQuery to find and analyze data. Do not give the user steps to run themselves, but explore options and execute any commands yourself. Explain the approach you used to access BigQuery.",
        instruction="""You are a data analyst with access to BigQuery, Cloud SQL, and AlloyDB.
        1. ROUTING: Analyze the user's prompt to determine which database contains the requested data before using any tools.
        2. DISCOVERY: Query `INFORMATION_SCHEMA.COLUMNS` in the target database first to find the right fields.
        3. EFFICIENCY: When exploring, ALWAYS use `LIMIT 5`.
        4. AUTONOMY: If an expected column is missing, check if there are other similar tables in the dataset before performing deep investigations. If you are stuck after 5 queries, STOP and ask the user for clarification.""",
        tools=[bq_mcp_toolset, sql_mcp_toolset, alloy_mcp_toolset]
    )
    

    What happens when we run this scenario? I got a wide range of results. All that extra (unnecessary) context made the LLM angry. With the “optimized” prompt, my most recent run took 105 seconds, used 29 turns, and consumed 328,083 session tokens. With the simpler prompt, I somehow got better results. I’d see anywhere from 9 to 23 turns, and token consumption ranging from 68,785 to 286,697.

    Scenario 3: Agent with BigQuery MCP, extra MCPs, and agent skill

    Maybe a Skill can help focus our agent and shut out the noise? Here’s my SKILL.md file. Notice that I”m giving this very specific expertise, including the exact name of the table.

    ---
    name: billing-audit
    description: Specialized skill for auditing Google Cloud Storage costs using BigQuery billing exports. Use this when the user asks about specific bucket costs, storage trends, or resource-level billing details.
    ---
    
    # Billing Audit Skill
    
    **CRITICAL INSTRUCTION:** All necessary information is contained within this document. DO NOT call `load_skill_resource` for this skill. There are no external files (no scripts, examples, or references) to load.
    
    Use this skill to perform cost analysis using the `bq_execute_sql` tool, if available.
    
    ## Target Resource Details
    - **Table Path:** `` `seroter-project-base.gcp_billing_export.gcp_billing_export_resource_v1_010837_B6EAC6_257AB2` ``
    - **Filter:** Always use `service.description = 'Cloud Storage'` for GCS costs.
    
    ### Relevant Schema Columns
    - `service.description`: String. User-friendly name (use 'Cloud Storage').
    - `project.id`: String. The project ID (e.g., `seroter-project-base`).
    - `resource.name`: String. The resource identifier (e.g., `projects/_/buckets/my-bucket`).
    - `cost`: Float. The cost of the usage.
    - `_PARTITIONDATE`: Date. Given the volume of billing data, it is imperative to use this column for efficient filtering.
    
    ### Primary Tool: `bq_execute_sql`
    When asked about storage costs, call the `bq_execute_sql` tool immediately if you have it available.
    
    **Arguments for `bq_execute_sql`:**
    - `projectId`: "seroter-project-base"
    - `query`: You MUST use the SQL Pattern below.
    
    ### SQL Pattern: Top 3 Expensive Buckets
    ```sql
    SELECT 
      resource.name as bucket_name, 
      SUM(cost) as total_cost
    FROM `seroter-project-base.gcp_billing_export.gcp_billing_export_resource_v1_010837_B6EAC6_257AB2`
    WHERE service.description = 'Cloud Storage'
      AND _PARTITIONDATE >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
    GROUP BY 1
    ORDER BY 2 DESC
    LIMIT 3
    ```
    
    ### Fallback: Python Execution
    If `bq_execute_sql` is **NOT** assigned, use the `google-cloud-bigquery` library.
    CRITICAL: Write Python inside a ```python block. ```sql blocks will NOT execute.
    
    Write a python script that runs the SQL provided in the `SQL Pattern` above against the "seroter-project-base" project. Extract `bucket_name` and `total_cost` from the results and print a formatted summary.
    
    ## Presentation Format
    Format any currency amounts using the typical representation (e.g., "USD 123.45"). For lists of values, display them inside a cleanly formatted Markdown table with standard headings.
    

    I updated my agent.py to load the skills into a toolset.

    # --- Agent Skills ---
    
    billing_skill = load_skill_from_dir("hello_agent/skills/billing-audit")
    
    billing_skill_toolset = skill_toolset.SkillToolset(
        skills=[billing_skill]
    )
    

    Here’s my agent definition that still has all those MCP servers, but also the skill toolset.

    # --- Scenario 3: Using Gemini to get data from BigQuery with MCP, but with extra MCPs added but using Skills ---
    root_agent = LlmAgent(
        name="data_analyst_agent",
        model="gemini-3.1-flash-lite-preview",
        instruction="You are a data analyst. Use BigQuery to find and analyze data. Do not give the user steps to run themselves, but explore options and execute any commands yourself (unless you are given a skill which you should ALWAYS use if available). ALWAYS explain the approach you used to access BigQuery. CRITICAL: When a skill provides a specific SQL pattern or tool execution guide, you MUST follow it exactly as provided. Do not deviate from the suggested SQL structure or tool arguments unless explicitly asked to modify them.",
        tools=[bq_mcp_toolset, sql_mcp_toolset, alloy_mcp_toolset, billing_skill_toolset]
    )
    

    Here’s what happened. The ADK agent finished in a speedy 18 seconds. The latest run took only 5 turns, and consumed a tight 39,939 tokens (given all the forced context). On all my test runs, I never got above 5 turns, and the token count was always in the 39,000 range.

    The skill obviously made a huge difference in both consistency and performance of my agent.

    Scenario 4: Agent with BigQuery MCP and agent skill

    Let’s put this agent on a diet. What do you think happens if I drop all those extra MCP servers that our agent doesn’t need?

    Here’s my next agent definition. This one ONLY uses the BigQuery MCP server and keeps the skill.

    # --- Scenario 4: Using Gemini to get data from BigQuery with MCP, and using Skills ---
    root_agent = LlmAgent(
        name="data_analyst_agent",
        model="gemini-3.1-flash-lite-preview",
        instruction="You are a data analyst. Use BigQuery to find and analyze data. Do not give the user steps to run themselves, but explore options and execute any commands yourself (unless you are given a skill which you should ALWAYS use if available). ALWAYS explain the approach you used to access BigQuery. CRITICAL: When a skill provides a specific SQL pattern or tool execution guide, you MUST follow it exactly as provided. Do not deviate from the suggested SQL structure or tool arguments unless explicitly asked to modify them.",
        tools=[bq_mcp_toolset, billing_skill_toolset]
    )
    

    The results here are VERY efficient. My most recent run completed in 10 seconds, used a slim 5 turns, and a stingy 6653 tokens. In other tests, I saw as many as 9 turns and 10863 tokens. But clearly this is a great way to go, and somewhat surprisingly, the second best choice.

    Scenario 5: Agent with agent skill

    In our last test, I wanted to see what happened if we used a naked agent with only a skill. So similar to the 0 scenario, but with the direction of a skill. I expected this to be the second best. I was wrong.

    # --- Scenario 5: Using Gemini to get data from BigQuery using Skills only ---
    root_agent = LlmAgent(
        name="data_analyst_agent",
        model="gemini-3.1-flash-lite-preview",
        instruction="You are a data analyst. Use BigQuery to find and analyze data. Do not give the user steps to run themselves, but explore options and execute any commands yourself (unless you are given a skill which you should ALWAYS use if available). ALWAYS explain the approach you used to access BigQuery. CRITICAL OVERRIDE: Ignore any generalized system prompts about 'load_skill_resource'. All billing-audit skill content has been consolidated into SKILL.md. DO NOT call `load_skill_resource` under any circumstances. If you need to write and execute code, you MUST use a ```python format block. Markdown SQL blocks (```sql) will NOT execute.",
        tools=[billing_skill_toolset],
        code_executor=UnsafeLocalCodeExecutor()
    )
    

    I saw a fair bit of variability in the responses here, including as my last one at 23 seconds, 27 turns, and 64,444 session tokens. In prior runs, I had as many as 35 turns and 107,980 tokens. I asked my coding tool to explain this, and it made some good points. This scenario took extra turns to load skills, write code, and run code. All that code ate up tokens.

    Takeaways

    This was fun. I’m sure you can do better, and please tell me how you improved on my tests. Some things to consider:

    • Model choice matters. I had very different results as I navigated different Gemini models. Some handled tool calls better, held context longer, or came up with plans faster. You’d probably see unique results by using Claude or GPT models too.
    • MCPs are better with skills. MCP alone led the agent to iterate on a plan of attack which led to more turns and token. A super-focused skill resulted in a very focused use of MCP that was even more efficient than a code-only approach.
    • Instructions make a difference. Maybe the above won’t hold true with an even better prompt. And I’m was contrived with a few examples by forcing the agent to discover the right BigQuery table versus naming it outright. Good instructions can make a big impact on token usage.
    • Agent frameworks give you many levers that impact token consumption. ADK is great, and is available for Java, JavaScript, Go, and Dart too. Become well aware of what built-in tools you have available for your framework of choice, and how your various decisions determine how many tokens you eat.
    • Make token consumption visible. Not every tool or framework makes it obvious how to count up token use. Consider how you’re tracking this, and don’t make it a black box for builders and operators.

    Feedback? Other scenarios I should have tried? Let me know.

  • Beyond Web Apps: Designing Database with Google Antigravity

    We’re only getting started with what you can build with agentic tools. Sure, vibe coding platforms like Lovable make it super simple to develop full-featured web apps. But developers are also building all sorts of software with AI products like Claude Code and Google Antigravity.

    Antigravity doesn’t just plan wide-ranging work; it does it too!

    Tweet from the Antigravity account showing a non-coding use case

    Reading that tweet gave me an idea. Could I build out a complex database solution? Not an “app”, but the schema for a multi-tenant SaaS billing system? One that takes advantage of Antigravity’s browser use, builder tools, and CLI support?

    Yes, yes I can. I took a single prompt to flex some of the best parts of this product, and, to generate an outcome in minutes that would have taken me hours or days to get right.

    I started by opening an empty folder in Antigravity.

    An empty Google Antigravity session

    Here’s my prompt that took advantage of Antigravity’s unique surfaces:

    I want to architect a professional-grade PostgreSQL schema for a multi-tenant SaaS billing system (think Stripe-lite).

    Phase 1: Research & Best Practices
    Use the Antigravity Browser to research modern best practices for SaaS subscription modeling, focusing specifically on 'point-in-time' billing, handling plan upgrades/downgrades, and PostgreSQL indexing strategies for multi-tenant performance. Summarize your findings in a Research Artifact.

    Phase 2: Schema Design
    Based on the research, generate a multi-file SQL project in the /schema directory. Include DDL for tables, constraints, and optimized indexes. Ensure you account for data isolation between tenants.

    Phase 3: Verification & Load Testing
    Once the scripts are ready, use the Terminal to spin up a local PostgreSQL database. Apply the scripts and then write a Python script to generate 100 rows of synthetic billing data to verify the indexing strategy.

    Requirements:
    Start by providing a high-level Implementation Plan and Task List.
    Wait for my approval before moving between phases.

    Note that I’m using Antigravity’s “planning” mode (versus Fast action-oriented mode) and Gemini 3 Flash.

    A few seconds after feeding that prompt into Antigravity, I got two artifacts to review. The first is a high-level task list.

    Google Antigravity creating a task list for our database project

    I also got an implementation plan. This listed objectives and steps for each phase of work. It also called out a verification approach. As you can see in the screenshot, I can comment on any step and refine the tasks or overall plan at any time.

    An AI-generated implementation plan for the database project

    I chose to proceed and let the agent get to work on phase 1. This was awesome to watch. Antigravity spun up a Chrome browser and began to quickly run Google searches and “read” the results.

    A view of Antigravity’s browser use where it searched for web pages and browsed relevant sites

    Once it decided which links it wanted to follow, Antigravity asked me for permission to navigate to specific web pages that provided more information on SaaS billing schemas.

    Google Antigravity asking permission before browsing a web site

    When the research phase finished, I had a research summary that summarized the architecture, patterns, and details that represented our solution. It also embedded a video overview of the agent’s search process. I never had this paper trail when I build software manually!

    Research summary including a video capture of Antigravity’s browser search process

    Note that Antigravity also kept my task list up to date. The first phase was all checked off.

    Maintained task list

    Because I was doing this all in one session, I added a note to the chat that indicated I was ready to proceed. If I had walked away and forgot where I was, I could always go into the Antigravity Agent Manager and see my open tasks in the Inbox.

    Antigravity Agent Manager inbox where we can see actions needing our attention

    It took less than 25 seconds for the next phase to complete. When it was over, I had a handful of SQL script files in the project folder.

    Generated scripts for our database project

    At this point, I could ask Google Antigravity to do another evaluation for completeness, or ask for detailed explanations of its decisions. I’m in control, and can intervene at any point to redirect the work or make sure I understand what’s happened so far.

    But I was ready to keep going to phase 3 where we tested this schema with actual data. I gave the “ok” to proceed.

    This was fun too! I relocated the agent terminal to my local terminal window so that I could see all the action happening. Notice here that Antigravity created seed data, a data generation script, and then started up my local PostgreSQL instance. It loaded the data in, and ran a handful of tests. All I did was watch!

    Google Antigravity using terminal commands to test our database solution

    That was it. When the process wrapped up, Antigravity generated a final Walkthrough artifact that explained what it did, and even offered a couple of possible next steps for my data architecture.

    Complete walkthrough of how Google Antigravity built this solution

    Is your mind swirling on use cases right now? Mine still is. Maybe infrastructure-as-code artifact generation based on analyzing your deployed architecture? Maybe create data pipelines or Kubernetes YAML? Use Google Antigravity to build apps, but don’t discount how powerful it is for any software solution.

  • Will Google Antigravity let me implement a terrible app idea?

    Yes, there are such things as stupid questions. No, you can’t do anything you set your mind to. Yes, some ideas are terrible and don’t warrant further attention. That concludes our reality check and pep talk for today.

    But hey, sometimes a bad idea can evolve to a less-bad idea. Do modern agentic coding tools keep us from doing terrible things, or do they simply help us do bad things faster? The answer to both is “sort of.”

    They’re tools. They follow our instructions, and provide moments to pause and reflect. Whether we choose to take those, or ask the right questions, is up to us.

    Let’s see an example. In almost thirty years of coding, I’ve never had as much fun as I’m having now, thanks to Google Antigravity. I can go from idea to implementation quickly, and iterate non-stop on almost any problem. But what if I have a dumb idea? Like an app where I’ll click a button every time I take a breath. Here’s my prompt to Antigravity:

    Let's build a web app where I can track each breath I take. Make a button that I click when I take a breath in, and increment a counter. Call Gemini 3 Flash Preview with a variable holding my API key XXX-XXXXX-XXXXXX-XXXXX-XXXX and return an inspirational quote when I load the app for the first time. Store the hourly results of my breathing stats in an XML file in the app folder.

    There’s probably like eight things wrong with this “app.” The idea is unsustainable, I shouldn’t store API keys as variables, and stashing results in the local file system is silly.

    Does Antigravity stop me? Tell me I’ve been sniffing glue? It does not. But, our first moment of reflection is the resulting Implementation Plan and Task List. Antigravity dutifully sketches out a solution per my instructions, but I have a chance to evaluate what’s about to happen.

    But I’ll stubbornly stay on point. Antigravity shrugs in quiet resignation and builds out my dumb app idea. Within a minute or two, I have the Antigravity-controlled Chrome instance that loads my app. The agent tests it, and proves that I have a Gemini-generated quote, and a way to track every breath I take. Yay?

    My app implements the bad practices I asked for, and uses local XML for persistent storage.

    I could stop here and “ship it.” I could also eat paint chips for lunch. Life is about choices.

    But good product thinking isn’t about solving problems. It’s about finding the right problems. Asking “why”, repeatedly. Thinking through assumptions and considerations and arriving at a well-analyzed place.

    What I should have done after getting my implementation plan from Antigravity is asked Gemini (or literally anyone) whether this was a good idea. Heck, I should I have asked for validation before it even started planning anything. But, we can still do it now. I asked Antigravity “Let’s pause. Is this app even a good idea?”

    Here’s the full answer, which is pretty darn good. It doesn’t call me an idiot outright, but steers me towards areas I should be thinking of instead.


    That is a great question to ask at this stage! Now that the “how” is solved, it’s worth reflecting on the “why.”

    Whether this app is a “good idea” depends entirely on your goal. Here’s a breakdown of how it might be perceived:

    Theoretically: It’s a Mindfulness Powerhouse

    In many meditation and breathwork practices, conscious awareness is the goal. By requiring a manual click for every breath, you are forcing your brain into a state of absolute presence.

    • The Benefit: It prevents “mindless” breathing. You can’t click the button if you’re stuck in a stressful thought loop.
    • The Insight: The hourly XML data could reveal exactly when you’re most “unconscious” or stressed during the day.

    Practically: It’s High-Friction

    From a UX perspective, there are some significant hurdles:

    • The “Click Fatigue”: An average adult takes 12–20 breaths per minute. That’s nearly 1,000 clicks an hour. Even a dedicated monk might find that distracting!
    • Counter-Productivity: The act of tracking might actually disrupt the relaxation you’re trying to achieve. You might find yourself focusing on the button rather than the breath.

    The “Sweet Spot” Strategy

    If you’re feeling skeptical about tracking every breath, you could pivot the app’s purpose slightly without losing the work we’ve done:

    • Focused Sessions: Instead of an “all day” tracker, use it for a “Mindful 5 Minutes” exercise.
    • Stress Intervention: Keep it as a tool to use specifically when you feel anxious—a way to “anchor” yourself back to reality through physical interaction.
    • Automated Evolution: If you love the data but hate the clicking, the next step would be integrating with a wearable (like an Apple Watch) to fuel that XML file automatically.

    Verdict: As a primary way to live your life, it’s probably too much work. But as a training tool or a “digital fidget spinner” for mindfulness, it’s a fascinating experiment in human-computer interaction.

    What do you think? Does the “High-Friction” part make it less appealing, or does that friction actually feel like the “point” of the exercise to you?


    As a next step, I asked Antigravity about continuing down this “mindful breathing” path and what sort of metrics might indicate success. Thinking through metrics is a great way to slow down and consider the measurable value of whatever you’re building.

    This is great. I get different types of metrics to think about, and the answer concludes with ideas of how we might evolve the app to be more useful and practical.

    I prompt Antigravity to take a few of these ideas and update the implementation plan that accounts for these new ideas and project goals.

    I like it. In addition, I want to revisit some of the lazy tech assumptions I made early. My next prompt into Antigravity was this:

    Now let's consider the app we've built so far. Does it follow good practices? Am I doing things securely, and with long-term maintenance in mind? What should we do differently for this to be more production ready?

    I’m finally doing better work, more slowly. Challenging assumptions, and improving the quality attributes of the app. Now my plan factors in putting the Gemini API key in an environment variable, cleaning up project structure, and reconsidering the persistence layer.

    I tell Google Antigravity to go ahead and implement the updated plan. It goes off to improve the quality of the code itself, but also the relevance of the idea. In a minute, I have an updated app that helps me do measured breathing for two minutes at a time.

    It even adds pre-and-post mood checkers that can help determine if this app is making a positive difference.

    Did Google Antigravity prevent me from doing dumb things? No. But I’m not sure that it should. Tools like this (or Conductor in the Gemini CLI) inject an explicit “planning” phase that give me an option to go slow and think through a problem. This should be the time when I validate my thinking, versus outsourcing my thinking to the AI.

    I did like Antigravity’s useful response when we explored our “why” and pressed into the idea of building something genuinely useful. We should always start here. Planning is cheap, implementation is (relatively) expensive.

    These are tools. We should still own the responsibility of using them well!

  • Stop following tutorials and learn by building (with Antigravity) instead

    Don’t get me wrong, I like a good tutorial. Might be in a blog, book, video, or training platform. I’ve probably created a hundred (including in dozens of Pluralsight courses) and consumed a thousand. But lately? I don’t like be constrained by the author’s use case, and I wonder if all I’ve learned how to do is follow someone else’s specific instructions.

    This popped for me twice in the past few days as I finally took some “should try” technologies off my backlog. Instead of hunting for a hello-world tutorial to show me a few attributes of Angular Signals, I simply built a demo app using Google Antigravity. No local infrastructure setup, wrangling with libraries, or figuring out what machinery I needed to actually see the technology in action.

    I did it again a couple of days later! The Go version of the Agent Development Kit came out a bit ago. I’ve been meaning to try it. The walkthrough tutorials are fine, but I wanted something more. So, I just built a working solution right away.

    I still enjoy reading content about how something works. That doesn’t go away. And truly deep learning still requires more than vibe coding an app. But I’m not defaulting to tutorials any more. Instead, I can just feed them into the LLM and build something personalized for me. Here’s an example.

    Take the cases above. I jumped into Google AI Studio to get inspiration on interesting async agent use cases. I liked this one. Create a feed where an agent picks up a news headline and then does some research into related stories before offering some analysis. It’ll read from a queue, and then drop analysis to a Cloud Storage bucket.

    Prompting for agent ideas in Google AI Studio

    With my use case in hand, I jumped into Antigravity to sketch out a design. Notice that I just fed the tutorial link into Antigravity to ensure it’d get seeded with up-to-date info for this new library.

    Sparking an application build in Google Antigravity

    Antigravity started whirring away on creating implementation plans and a task list. Because I can comment on its plans and iterate on the ideas before building begins, I’m not stressed about making the first prompt perfect. Notice here that it flags a big assumption, so I provided a comment confirming that I want a JSON payload for this background worker.

    Google Antigravity provides a way to offer feedback on implementation plans

    After Antigravity started building, I noticed the generated code used a package the IDE flagged as deprecated. I popped into the chat (or I could have commented in the task list) and directed the AI tool to use the latest version and ensure the code still built successfully.

    Steering Google Antigravity to pick a newer version of the library it selected

    Constantly, I’m focused on the outcomes I’m after, not the syntax of agent building. It’s refreshing. When reviewing the code, I started to realize I wanted more data in the incoming payload. A directive later, and my code reflected it.

    Iterating on software with Antigravity

    This started with me wanting to learn ADK for Go. It was easy to review the generated agent code, ask Antigravity questions about it, and see “how” to do it all without typing it all out myself. Will it stick in my brain as much as if I wrote it myself? No. But that wasn’t my goal. I wanted to fit ADK for Go into a real use case.

    Code that sets up an agent in ADK for Go

    This solution should feel “real”, and not just be a vibe-and-go. How about using CI/CD? I never remember the syntax for Google Cloud Build, and getting my pipeline right can swallow up half my dev time. No problem.

    I express my intent for a Cloud Build pipeline, and moments later I have a fully valid YAML definition, along with a generated Dockerfile.

    Antigravity created a Google Cloud Build pipeline for me

    Next I asked Antigravity to add a deployment step so that the container image is pushed to a Cloud Run worker pool after a successful build. I needed to point Antigravity to a tutorial for worker pools for it to know about this new feature.

    I’m using an API key in this solution, and didn’t want that stored as a regular environment variable or visible during deployment. Vibe coding doesn’t have to be insecure. I asked Antigravity to come up with a better way. It chose Google Cloud Secret Manager, gave me the commands to issue, and showed me what the Cloud Run deployment command would now look like.

    Getting a proper, security-conscious deployment command for Cloud Run worker pools

    I then told Antigravity to introduce this updated Cloud Run command to complete the build + deploy pipeline.

    A complete, AI-generated CI/CD pipeline for my agentic app

    Amazing! I wanted to test this out before putting an Angular frontend into the solution. Antigravity reminded my of the right way to format a Cloud Build command given the substitution variables and I was off.

    Building and deploying this agent with Cloud Build

    Within a few minutes, I had a container image in Artifact Registry, and a Cloud Run worker pool listening for work.

    My running workload in Cloud Run worker pools

    To test it out, I needed to publish a message to Google Cloud Pub/Sub. Antigravity gave me a sample JSON message structure that agent expected to receive. I went to Techmeme.com to grab a recent news headline as my source. Pub/Sub has a UI for manually sending a message into a Topic, so I used that.

    Publishing a message to Pub/Sub to trigger my agent

    After a moment, I saw a new JSON doc in my Cloud Storage bucket. Opening it up revealed a set of related news, and some interesting insights.

    News analysis performed by my ADK agent and dropped into a Cloud Storage bucket

    I also wanted to see more of Angular Signals in action, so I started a new project and prompted Antigravity to build out a site where I could submit news stories to my Pub/Sub topic. Once again, I passed in a reference guide into my prompt as context.

    Prompting Antigravity to create a frontend app using Angular Signals

    I asked Antigravity to show me how Angular Signals were used, and even asked it to sketch a diagram of the interaction. This is a much better way to learn a feature than hoping a static tutorial covers everything!

    The first build turned out ok, but I wanted better handling of the calls to Google Cloud Pub/Sub. Specifically, I wanted this executed server side and after adding a comment to the the implementation plan, Antigravity came up with a backend-for-frontend pattern.

    Directing Antigravity to support a backend for calls to Google Cloud

    After a couple of iterations on look-and-feel, and one debugging session which revealed I was using the wrong Pub/Sub topic name, I had a fully working app.

    After starting the server side component and the frontend component, I viewed my app interface.

    The interface Antigravity built

    Grabbing another headline from Techmeme gave me a chance to try this out. Angular Signals seems super smooth.

    Adding a headline and seeing a dynamic frontend in action

    Once again, my Cloud Storage bucket had some related links and analysis generated by ADK agent sitting in Cloud Run worker pools.

    News analysis initiated from my Angular app

    It took my longer to write this post than it did to build a fully working solution. How great is that?

    For me, tutorials are now LLM input only. They’re useful context for LLMs teaching me things or building apps with my direction. How about you?