One Agent, Three Builds: Interactions API vs ADK vs Antigravity SDK
The same Gemini agent built three ways, with the Interactions API, Google ADK and the Antigravity SDK, compared on loops, tools, auth and Cloud Run deploys.
This article is published in English.
Sources: I built the same agent three ways: Interactions API, ADK, and the Antigravity SDK
Google's stack now offers three quite different routes to a working agent: the Interactions API, which is the agent-era surface of the Gemini API, ADK, a full framework, and the Antigravity SDK, the harness behind the Antigravity CLI. A fair way to compare them is to build one agent three times and see what shifts: who owns the agent loop, how tools get attached and which models work. Each build also ships to Cloud Run.
The test agent
Each build answers one question type, namely public opinion on a topic as seen on Hacker News. It reads from bigquery-public-data.hacker_news.full, a public dataset covering 49 million stories and comments that trails real time by about a day. All three versions need the same trio of abilities: write SQL from a plain question, execute it and condense the opinions that come back.
BigQuery is wired in differently each time. The Interactions API gets a hand-written function, ADK gets its first-party toolset and the Antigravity SDK uses MCP.
Credentials
Two kinds of credentials are involved:
export GOOGLE_API_KEY="..." # get this at aistudio.google.com
gcloud auth application-default login
Two credentials exist because the Interactions API in this setup sits on the Gemini API, which wants a key, whereas ADK and the Antigravity SDK address the Cloud project directly and skip keys entirely.
A closer look at the Interactions API: it accepts either model=, a plain model, or agent=, an agent that Google hosts and whose loop and reachable tools run on the platform. The tool in this build is a local Python function, which rules out a hosted agent, so model= it is and the tool loop stays in local code. Google's enterprise platform (previously Vertex AI) has its own experimental flavor of the API, but testing it with several models produced rejections of every model= call.
BigQuery's free tier of 1 TB of queries a month easily absorbs the demo queries, which scan about 1 to 17 GB. Versions at the time of testing were google-genai 2.17.0, google-adk 2.6.2 and google-antigravity 0.1.10, with the model gemini-3.6-flash.
Route one: the Interactions API
Install the packages with pip install google-genai google-cloud-bigquery. The tool is an ordinary function of about ten lines:
def execute_sql(query: str) -> list[dict]:
client = bigquery.Client()
config = bigquery.QueryJobConfig(maximum_bytes_billed=20 * 10**9)
rows = client.query_and_wait(query, job_config=config, max_results=50)
return [{k: str(v) for k, v in dict(row).items()} for row in rows]
It runs whatever SQL the model produced and returns up to 50 rows as plain dicts. maximum_bytes_billed caps one query at 20 GB of scanning, so nothing gets expensive. No explicit credential handling appears because bigquery.Client() picks up the local gcloud login, and on Cloud Run the same line quietly uses the service account.
The model never sees the function body, only what's declared, so the declaration carries the schema:
execute_sql_tool = {
"type": "function",
"name": "execute_sql",
"description": (
"Run a BigQuery Standard SQL SELECT query against "
"`bigquery-public-data.hacker_news.full`, the public Hacker News "
"dataset (stories and comments since 2006). Columns: id INT, "
"type STRING (story/comment/job/poll), title STRING (stories only), "
"text STRING (body, HTML-escaped), `by` STRING (username), score INT, "
"parent INT, descendants INT, timestamp TIMESTAMP, url STRING, "
"dead BOOL, deleted BOOL. Returns at most 50 rows."
),
"parameters": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
}
The parameters block stays small: one required string holding the SQL. The description matters more, because it's the only channel through which the model learns the table name and its columns. Sharper wording there yields better queries.
What remains is the tool loop, around a dozen lines:
while True:
calls = [s for s in interaction.steps if s.type == "function_call"]
if not calls:
break
results = [{
"type": "function_result",
"name": step.name,
"call_id": step.id,
"result": [{"type": "text", "text": json.dumps(execute_sql(**step.arguments))}],
} for step in calls]
interaction = client.interactions.create(
model=MODEL,
input=results,
system_instruction=INSTRUCTION,
tools=[execute_sql_tool],
previous_interaction_id=interaction.id,
)
A round executes every function_call step the model emitted, submits the outputs as the next input and loops until a reply has no calls left. One detail stands out: the history is never resent. The server stores it, and previous_interaction_id simply references the turn before, which keeps each request small. The client object is the SDK's doorway to the Gemini API, and building it with no arguments pulls GOOGLE_API_KEY out of the environment. Deployed to Cloud Run, that same value is mounted as an environment variable from Secret Manager. Everything else, including which SQL to run and when to stop, is the model's call.
MCP isn't an option here yet. The Interactions API docs list it as a limitation, saying Gemini 3 doesn't support remote MCP and that it's coming soon.
Verdict: the most code, the fewest dependencies and total control.
Route two: ADK
Install with pip install google-adk google-cloud-dataplex google-cloud-bigquery.
import google.auth
from google.adk import Agent
from google.adk.integrations.bigquery import BigQueryCredentialsConfig, BigQueryToolset
from google.adk.integrations.bigquery.config import BigQueryToolConfig, WriteMode
credentials, _ = google.auth.default()
toolset = BigQueryToolset(
credentials_config=BigQueryCredentialsConfig(credentials=credentials),
bigquery_tool_config=BigQueryToolConfig(write_mode=WriteMode.BLOCKED),
)
root_agent = Agent(name="hn_opinion_agent", model=..., instruction=..., tools=[toolset])
ADK bundles a ready-made BigQuery integration called BigQueryToolset. Alongside execute_sql it exposes metadata helpers such as get_table_info, which lets the agent read the schema itself rather than receive it in a prompt. One setting, WriteMode.BLOCKED, makes everything read-only.
No API key appears anywhere. The BigQuery toolset insists on an explicit credentials object, which google.auth.default() returns from the local gcloud login together with a project ID that this build discards. On the model side, a single variable does the work: GOOGLE_GENAI_USE_ENTERPRISE=TRUE pushes model traffic through the Cloud account. Once on Cloud Run, both the queries and the model requests are made under the service account.
The framework runs the loop and keeps sessions, including conversation history across turns, in memory here and in a database when persistence is needed.
Verdict: minimal code and maximal capability, since the prebuilt tools allow the agent to poke around the data unaided.
Route three: the Antigravity SDK
Install with pip install google-antigravity.
from google.antigravity import Agent, LocalAgentConfig
from google.antigravity.types import McpStreamableHttpServer
bigquery_mcp = McpStreamableHttpServer(
name="bigquery",
type="http",
url="https://bigquery.googleapis.com/mcp",
headers={"Authorization": f"Bearer {token}",
"x-goog-user-project": project},
enabled_tools=["execute_sql_readonly", "get_table_info", ...],
)
config = LocalAgentConfig(system_instructions=..., mcp_servers=[bigquery_mcp])
async with Agent(config) as agent:
response = await agent.chat(question)
The harness is given a system instruction and one MCP server, and a single agent.chat() call covers planning, tool use and the closing summary. Underneath sits the loop that drives the Antigravity CLI, with its file and shell tools included.
BigQuery is reached over MCP, via bigquery.googleapis.com/mcp, a managed endpoint run by Google that only takes OAuth (from gcloud auth print-access-token). The enabled_tools list acts as an allowlist of the MCP tools the agent may call. Both execute_sql and execute_sql_readonly exist on the endpoint, and enabling only the second one is what keeps this agent from writing. Model authentication matches ADK, using the Cloud account on a laptop and the service account on Cloud Run.
Two caveats apply before choosing it:
- Models are limited to Gemini. The Antigravity IDE also provides Claude and GPT-OSS, which the SDK lacks, though a local OpenAI-compatible server like Ollama can stand in.
- The project ID belongs in the system instructions. Every tool on the hosted endpoint requires a
projectIdargument naming the Cloud project that runs the query, and the model fills it in when calling the tool.
Verdict: a convenient path to an agent that can also edit files and run shell commands.
Deploying to Cloud Run
All three deploy to Cloud Run. What differs is how much of the service must be written.
- Interactions API: a small FastAPI wrapper around the loop is enough. Because the server holds the conversation, the service itself keeps no state, and callers only return
previous_interaction_id. The catch: this API still can't authenticate as a service account, making it the lone service that needs an API key, which comes from Secret Manager. - ADK: nothing to write on the server side.
adk api_serverruns it locally, andadk deploy cloud_runproduces the server, container and deployment config. Watch for two gotchas. First, the toolset's extra packages (bigquery and dataplex) need to be listed in the agent folder's own requirements.txt. Second, environment variables aren't carried over during deployment, soGOOGLE_GENAI_USE_ENTERPRISEmust be added to the service afterward. A fully managed option, Agent Runtime, exists for anyone wanting no service to maintain. - Antigravity SDK: wrap
agent.chat()by hand and package it as a container image. One change applies only when serving: the MCP Authorization header can no longer come from a laptop's gcloud login, so the token must be minted and refreshed from the service account.
Restricting access takes little effort. Deploying with --no-allow-unauthenticated limits callers to those with IAM permission. For a test call, attach an identity token with curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" or start gcloud run services proxy, and for browsers add IAP in front.
Identity setup was nearly free. A Cloud Run service always runs as some identity, and unless told otherwise that's the default compute service account of the project. One identity handled BigQuery for all three services and the model calls for ADK and Antigravity. The only manual grant was reading the API key secret for the Interactions service, roles/secretmanager.secretAccessor, attached to the secret itself.
Don't copy that blindly, though. The default account is generously permissioned and often holds the Editor role at project level. Anything past a demo calls for a dedicated service account per service, limited to the roles required and supplied at deploy time via --service-account.
Which one to pick
ADK on Cloud Run makes a sensible starting point for most work. It offers a complete toolset and MCP support, simple session and history handling and freedom to choose any model, not just Gemini and Gemma. The Interactions API teaches how agents really operate, since the loop is out in the open. The Antigravity SDK fits best when an agent has to edit files and run shell commands.