Guarding the till while autonomous data agents do the digging A developer built a cost-safe autonomous data agent using the Google Antigravity SDK and the Data Agent Kit (DAK) extension to prevent runaway expenses from LLM-driven trial-and-error queries on BigQuery. The agent's unoptimized exploratory queries on terabyte-scale tables can rack up thousands of dollars in a single run, as BigQuery charges by bytes scanned across entire columns, not rows returned. Two guardrails in the implementation limit costs by restricting query scope and token usage. Autonomous agents are genuinely good at answering messy business questions. Give one an LLM and a set of tools, and it can query a database, dig through files, and build a chart to investigate why a metric moved. No fixed script required. The problem is what happens when nobody’s watching the meter. If a question is simple, you don’t need an agent. You write one SQL query and you’re done. But most real analytical questions aren’t simple. They’re ambiguous, open ended, and take a few rounds of trial and error before they resolve. An agent handles that the way a human analyst would if you turned them loose on a data warehouse with no supervision. It tries a query, gets a weird result, tries a different angle, hits a dead end, and tries again. On a warehouse the size of BigQuery, that trial-and-error loop gets expensive fast. Add in the token costs of a long-context LLM reasoning through fifteen turns, and a single agent run can blow past your budget before anyone notices. In this post, I’ll walk through how I built a cost-safe version of this kind of agent, using the Google Antigravity SDK and the Data Agent Kit DAK extension. Two guardrails do the work. To see why this matters, it helps to look at how BigQuery bills, how agents actually reason, and where the two collide. A lot of people assume that adding a LIMIT 10 or a WHERE clause keeps a query cheap. In a columnar database like BigQuery, that assumption will bite you. BigQuery charges by the bytes it reads from the columns you reference, across the whole table, not the rows you end up seeing. Run something like SELECT body FROM comments WHERE body LIKE '%pandas%' and it scans the entire body column for every row before it even gets to filtering. There's no row-level index to jump to matching rows, so evaluating that filter means reading every value in the column first. The LIMIT clause runs after that scan is already paid for, so it doesn't save you anything. The only real lever is filtering on a partitioned column, which restricts the query to specific physical blocks like a date range, or a clustered column, which prunes blocks based on sorted keys. A human analyst scopes a query carefully before running it. An agent doesn’t work that way. It loops. For an open-ended problem, the path usually looks like this. It runs a few queries to discover table structures and columns. It tries different combinations of joins, filters, and aggregations to test its hypotheses, and when it hits an empty result or a syntax error, it rewrites and tries again. It runs heavy string scans, LIKE '%error%' across a text column, to pull out logs or comments. And when it needs to merge sources, it runs wide multi-key joins that shuffle data across worker slots. Here’s where it gets real. In a production environment with tables in the hundreds of terabytes, a single on-demand query scanning 100 TB of one column costs $625 at standard $6.25/TB pricing. Stack a few window functions and nested joins on top and you’re triggering shuffle operations that move gigabytes between worker slots. If you’re on capacity-based billing instead of on-demand, those same queries burn through slot-hours and degrade everyone else’s performance. An agent running fifteen turns of unoptimized exploratory queries on TB-scale tables can rack up thousands of dollars in a single run. That’s not a hypothetical. That’s what happens the first time you point one of these things at a real warehouse without a guardrail. To build and test this, I used the Stack Overflow dataset from BigQuery Public datasets. Here’s the question I tried to get the answer to: Developer engagement metrics for Python data science libraries pandas, numpy on Stack Overflow dropped in late 2023. Answer rates and comment volumes fell. Did that correlate with a specific library release? Was it a boycott, or something more transient? That’s a genuinely open-ended problem, and answering it takes a few steps. First, verify the trend actually happened, and check whether it was unique to pandas and numpy or part of a broader Stack Overflow slowdown a seasonal dip, or the rise of ChatGPT, would show up across all topics . Second, cross-reference the comment trend with PyPI download stats to see whether people actually stopped installing the library, or just stopped needing to ask about it, say, after pandas 2.1 shipped in September 2023. Third, run text searches on the comments themselves to find the specific error messages or breaking changes people were complaining about, like the PyArrow dependency changes. To run this without setting up a bunch of infrastructure, the agent queries across two systems. BigQuery holds the Stack Overflow public dataset bigquery-public-data.stackoverflow , hundreds of gigabytes of partitioned post and comment data. A local CSV holds monthly PyPI download rates for pandas and numpy . Data Agent Kit DAK runs as an extension inside the Antigravity IDE or CLI, and it handles the data-connection plumbing so you don’t have to. It exposes database tools as MCP servers, bigquery remote being the one I used here, which run remotely in GCP and talk directly to your data in BigQuery. It also ships filesystem-based Agent Skills for performing various data operations, such as data cleaning, building data pipelines, querying data etc., using Google Cloud services and standard tools such as Python and Notebooks. Skills are pre-packaged directories with standard SKILL.md instruction files and references. Once you install DAK in your IDE, you configure it with the Google Cloud Project you want to work with. The MCP tools and skills get injected into the agent’s workspace automatically. I didn’t have to write connection pooling, manage GCP credentials by hand, or wire up custom Python imports for BigQuery access. The Antigravity SDK sits on top of that as the agent runtime. It’s where I write the governance logic, the lifecycle hooks that intercept and control whatever tools DAK injects. That’s where the guardrails live. python from google.antigravity.hooks import policy 2. Config is clean: the IDE automatically injects DAK tools and skills config = LocalAgentConfig system instructions="You are an autonomous data investigator...", policies= policy.deny "search web" Force the agent to use BigQuery instead of search shortcuts , hooks= bq cost guardrail hook, Intercepts the injected BQ tool for dry-run check check token budget hook Intercepts chat turns for budget validation Splitting the data platform layer DAK from the agent harness layer Antigravity SDK is what makes this clean. The guardrails live as harness hooks, and DAK just handles the database connections underneath. This one intercepts SQL queries with the Antigravity SDK’s @hooks.pre tool call decide hook. Before a query runs, I do a dry run. It’s a free operation, and it tells you exactly how many bytes the query will scan. If that number blows past the budget, the call gets rejected and the agent gets a message back telling it why, so it can rewrite the query with a proper partition filter. One thing worth saying up front. The 10 GB limit below is small on purpose, small enough that you’ll actually see the guardrail trigger and self-correct within a couple of turns. Your real limit depends on your table sizes, your pricing tier, and what you’re actually willing to spend on a single exploratory query. Treat it as a placeholder and set your own before you point this at a real workload. python from google.cloud import bigquery from google.antigravity import types from google.antigravity.hooks import hooks Cost settings SINGLE QUERY SCAN LIMIT GB = 10.0 10 GB limit per query BQ PRICE PER TB = 6.25 Standard BigQuery on-demand pricing def estimate bq query cost sql: str - tuple float, float : """Perform a dry run to estimate the query scan size and cost.""" client = bigquery.Client job config = bigquery.QueryJobConfig dry run=True, use query cache=False try: query job = client.query sql, job config=job config bytes scanned = query job.total bytes processed gb scanned = bytes scanned / 1024 3 cost = bytes scanned / 1024 4 BQ PRICE PER TB return gb scanned, cost except Exception: If the dry run fails e.g. syntax error , let the actual query run fail naturally return 0.0, 0.0 @hooks.pre tool call decide async def bq cost guardrail hook data: types.ToolCall - types.HookResult: Intercept BigQuery tool execution from DAK MCP tools if data.name in "execute sql", "execute sql readonly", "query bigquery" : sql = data.args.get "sql" or data.args.get "query" if not sql: return types.HookResult allow=True gb scanned, cost = estimate bq query cost sql Print status so users running this in a terminal can see the guardrail in action print f"\n Guardrail Check Intercepted Query: {sql :120 .strip }..." print f" Guardrail Check Estimated Scan: {gb scanned:.2f} GB Estimated Cost: ~${cost:.4f} " if gb scanned SINGLE QUERY SCAN LIMIT GB: error message = f"Query rejected by Cost Guardrail " f"This query is estimated to scan {gb scanned:.2f} GB of data costing ~${cost:.4f} , " f"which exceeds your safety limit of {SINGLE QUERY SCAN LIMIT GB} GB per query. " f"Please rewrite your query to limit scan costs by filtering on partitioned/clustered " f"columns e.g., creation date and avoiding SELECT ." print f"❌ Guardrail Triggered BLOCKING query scan {gb scanned:.2f} GB {SINGLE QUERY SCAN LIMIT GB} GB ." Block the tool execution and send the guidance back to the agent's context return types.HookResult allow=False, message=error message else: print f"✅ Guardrail Approved Query scan approved {gb scanned:.2f} GB <= {SINGLE QUERY SCAN LIMIT GB} GB ." return types.HookResult allow=True Here is the visual flow of how this cost interceptor and the agent self-correction loop function: When the agent receives this rejection as the “tool result,” its LLM reads the guidance, understands the partition requirement, and automatically rewrites the query. Here is what the self-correction log looks like when the agent attempts a raw scan: Agent Turn 1 Thinking: I need to analyze comment text containing "pandas". Let me query the comments table. Tool Call: execute sql readonly sql="SELECT text FROM bigquery-public-data.stackoverflow.comments WHERE text LIKE '%pandas%'" Guardrail Triggered System Hook: Query rejected by Cost Guardrail This query is estimated to scan 14.09 GB of data costing ~$0.09 , which exceeds your safety limit of 10.0 GB. Please rewrite your query to filter on partitioned columns e.g., creation date . Agent Turn 2 Thinking: Ah, my query was blocked because it scans the entire comments table. I should restrict the scan size by adding a date filter. The 'comments' table is partitioned by 'creation date'. Tool Call: execute sql readonly sql="SELECT text FROM bigquery-public-data.stackoverflow.comments WHERE creation date = '2022-01-01' AND creation date < '2022-02-01' AND text LIKE '%pandas%'" Guardrail Approved System Hook: Query estimated to scan 1.8 GB. Approved. The BigQuery guardrail watches the data layer. This one watches the reasoning budget. Autonomous agents run in a loop, and every turn the conversation history grows, more prompts, more tool calls, more results, more reasoning steps. Token usage climbs with it, and so does your API bill. Because the Antigravity SDK manages the agent’s internal reasoning loop asynchronously inside a single await agent.chat call, you can't just check usage in a manual loop. You need an SDK hook. I built a stateful manager that checks the conversation's total usage before each turn and asks a human for confirmation if the budget is breached. python import asyncio from google.antigravity import Agent, LocalAgentConfig, types from google.antigravity.hooks import hooks class SessionGuardrail: def init self, token budget: int : self.token budget = token budget self.agent = None def register self, agent: Agent : """Bind the agent reference to the guardrail manager.""" self.agent = agent async def check token budget self, data: str - types.HookResult: """Intercept before a chat turn starts to enforce the token budget.""" if self.agent and self.agent.conversation: usage = self.agent.conversation.total usage Show active token monitoring in the terminal on every turn print f" Token Monitor Session token count: {usage.total token count:,} / {self.token budget:,}" if usage.total token count self.token budget: Prompt the user asynchronously for authorization print f"\n Budget Alert Cumulative token usage is {usage.total token count}. Budget is {self.token budget}." confirm = await asyncio.to thread input, "Authorize additional tokens? y/n : " if confirm.strip .lower = 'y': return types.HookResult allow=False, message=f"Session suspended: Token budget of {self.token budget} exceeded." return types.HookResult allow=True One thing I want to call out here: Using input wrapped in asyncio.to thread is a fast way to demonstrate human-in-the-loop validation from a local terminal, but it won't hold up in production. Blocking stdin reads can hang the event loop or throw an EOFError in a web backend, a CI runner, or a notebook. For anything real, swap the CLI prompt for an async request to a Slack webhook, a PagerDuty trigger, or a queue that resolves when someone clicks approve. Now it all comes together in one config. The token budget here is another demo value. 150,000 tokens is enough to watch the guardrail trigger without waiting forever, not a number to carry into production. Set yours based on your actual session lengths and what a runaway session would cost you. python import asyncio from google.antigravity import Agent, LocalAgentConfig from google.antigravity.hooks import hooks, policy Global instance of the guardrail manager to hold state guardrail = SessionGuardrail token budget=150 000 @hooks.pre turn async def check token budget hook data: str - types.HookResult: return await guardrail.check token budget data async def run safe investigation : 1. Register cost guardrail hooks — DAK tools and skills are injected automatically by the IDE config = LocalAgentConfig system instructions= "You are an autonomous data investigator. " "When outputting your reasoning, steps, or planned actions, you MUST format them clearly " "with newlines, bullet points, and spacing so they are clean and readable in a terminal console. " "Do not merge your planned actions into a single dense paragraph." , policies= policy.deny "search web" Force the agent to use BigQuery instead of search shortcuts , hooks= bq cost guardrail hook, Intercepts SQL queries check token budget hook Intercepts chat turns 3. Execute the agent async with Agent config as agent: guardrail.register agent Give the manager access to the running agent user query = "You MUST run a live SQL query using execute sql or query bigquery to search the Stack Overflow " "comments table for pandas comments in Q4 2023, even if you think the table is unpartitioned. " "Use the standard file writing tools like write file to save the final markdown report as " "'output/analysis results.md' and the script as 'output/analyze pandas.py' in the workspace. " "Do not use the save artifact tool." response = await agent.chat user query print await response.text if name == " main ": asyncio.run run safe investigation Ready to run this yourself? Here’s the walkthrough. git clone https://github.com/sireeshapulipati/guarding-the-till.git cd guarding-the-till The directory splits into two clean categories. The inputs, what you actually need to run it, are agent.py setup, hooks, and the execution loop , requirements.txt , data/pypi downloads.csv monthly PyPI download stats for pandas and numpy , and .env.example . The outputs are pre-generated reference examples. output/analysis results example.md is the full markdown report from my run, and output/analyze pandas example.py is a standalone cost-guarded script the agent wrote to automate this in the future. Before you boot the agent, make sure your terminal has access to Google Cloud. You need a project with the BigQuery API enabled, and you need to authenticate with Application Default Credentials. gcloud auth application-default login You’ve got two options here, and the .env.example file lays both out. Authenticate with ADC like above and the Antigravity SDK picks up your active GCP context automatically, hooking straight into Vertex AI's Gemini models, as long as the Gemini for Google Cloud API is enabled on your project. Or skip GCP auth entirely and drop a Gemini API key straight into .env instead. Either path works, pick whichever fits how you're already set up. python -m venv .venv source .venv/bin/activate On Windows, use: .venv\Scripts\activate pip install -r requirements.txt cp .env.example .env Open .env and swap your gcp project id here for your actual GCP project ID. python agent.py Watch the logs. The agent starts up, formulates a plan, and immediately goes for the comments table. Agents are non-deterministic, so your exact run will vary depending on the prompt, but here’s roughly what happens. The session opens with the token monitor reporting the starting count, something like Token Monitor Session token count: 0 / 150,000 . This demo wraps in a single turn, so that's the only time you'll see it print. In a longer run with several self-correction turns, it fires at the start of each one, printing the cumulative count and pausing for approval once you cross budget. From there the agent reads the local files and narrates its plan, inspect the schema, then query Stack Overflow comments in BigQuery. Then it tries the raw query, and the guardrail catches it before it runs. A dry run against the unpartitioned comments table comes back at 14.09 GB, over the 10 GB limit, so the hook blocks it and hands the rejection back as feedback. The agent reads that, realizes it needs the creation date partition filter, rewrites the query, and resubmits. This time the scan comes in at 1.8 GB and gets approved. Once the analysis wraps, the agent reaches for its file tools and writes the results straight into ./output/ , a markdown report as analysis results.md and a standalone script as analyze pandas.py Here is what the agent found: | Month | Pandas PyPI Downloads | Download MoM % | Pandas SO Comment Count | SO MoM % | |---|---|---|---|---| Oct 2023 | 150,354,240 | Baseline | 6,210 | Baseline | Nov 2023 | 147,923,892 | -1.62% | 5,845 | -5.88% | Dec 2023 | 151,553,549 | +2.45% | 5,420 | -7.27% | The agent calculated a Pearson Correlation Coefficient of $r \approx -0.3654$. This is a weak-to-moderate negative correlation. Under normal circumstances, you would expect forum questions to rise as library installations rise. Instead, we see: This divergence is likely driven by seasonality. In December, human developers take vacations and stop posting questions or answers on public forums, leading to a dip in active comments. Meanwhile, automated build systems, CI/CD runners, and deployment scripts continue pulling the package continuously, keeping the download numbers high. The agent also wrote output/analyze pandas example.py as part of its delivery. If you want to rerun this analysis every month in a production pipeline, an Airflow DAG, a cron job, you don't need to run the slow, non-deterministic agent loop again. Just run this. python output/analyze pandas example.py It’s a fast, deterministic ETL pipeline. It queries BigQuery, calculates the correlation, and spits out a fresh markdown report, no LLM reasoning layer involved, while keeping the exact same dry-run safety guardrails. These two guardrails do two different jobs. The BigQuery dry-run watches the data layer and stops runaway scan bills before they happen. The session token guardrail watches the reasoning layer and caps what the LLM itself costs you. Together they give the agent a bounded sandbox to actually explore in. The semantic error feedback is what actually makes this useful. Instead of a fatal exception that kills the run, the scan guardrail hands the LLM a plain-language explanation of what went wrong. That turns a runtime constraint into something the agent can actually act on, and it rewrites its own query instead of just failing. And wrapping the token budget check in a non-blocking async hook means the human-in-the-loop step doesn’t freeze the rest of the system while it waits for someone to click approve. If you’re building agents that need to explore large, cross-system datasets, this is the difference between an agent you can trust with a warehouse and one you have to babysit.