Antigravity × A2A Protocol: Complete Implementation Guide for Scalable Multi-Agent Systems
A complete guide to implementing Google's A2A (Agent-to-Agent) protocol with Antigravity. From agent card design to task delegation, streaming communication, and production deployment on Cloudflare Workers.
Setup and context — How A2A Is Reshaping Agent Architecture
In April 2025, Google released the A2A (Agent-to-Agent) protocol as an open standard. Designed to let AI agents communicate, delegate tasks, and collaborate with one another, A2A has now been adopted in many production systems. Combined with Antigravity IDE, it enables remarkably efficient multi-agent architectures that would have been painful to build even a year ago.
Before A2A, building multi-agent systems meant inventing a proprietary communication layer for each project. A2A changed that by providing a standard HTTP/SSE-based interface so agents built with different frameworks and languages can interoperate out of the box.
This guide walks you through building an A2A-compliant agent system with Antigravity, from first principles to production deployment. We cover agent card design, task delegation patterns, streaming, authentication, error handling, and deploying to Cloudflare Workers — everything you need to ship a robust multi-agent system.
Before writing any code, let's nail down the four building blocks of A2A.
Agent Card
The Agent Card is the agent's "business card" — a JSON manifest published at /.well-known/agent.json. It declares what the agent can do (skills), how to reach it (endpoint, auth schemes), and what input/output formats it supports.
A Task is the unit of work exchanged between agents. Each task has a unique ID and follows a lifecycle: submitted → working → completed (or failed). Tasks carry Messages (the conversation history) and Artifacts (the outputs).
Artifact
An Artifact is the deliverable produced by a task. It can contain multiple parts — text, files, or structured data — and can be streamed incrementally as the agent works.
Push Notifications
For long-running tasks, agents can notify clients via webhook when work is complete. This lets clients avoid holding open a persistent connection, making it ideal for batch workloads.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Connect every stage of an A2A build — Agent Card design, auth, registry-based discovery, and production deploy — under one coherent design philosophy
✦Learn where the two highest-impact tuning calls (Promise.all parallelism and Agent Card caching) actually pay off
✦Avoid the documentation-blind pitfalls: health-check intervals, task-ID idempotency, and JWT aud verification
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
In Antigravity, create .antigravity/rules.md at the project root so A2A compliance rules are reflected in AI code suggestions:
# A2A Agent Development Rules- Every agent must serve /.well-known/agent.json- Task IDs must be UUID v4- Error responses must follow the A2A JSONRPC error format- Streaming must be implemented with SSE (Server-Sent Events)- Authentication must use Bearer Token (JWT)
Designing and Serving the Agent Card
We'll use a "Code Review Agent" as our running example throughout this guide.
wrangler secret put GOOGLE_API_KEYwrangler secret put JWT_SECRETwrangler secret put AGENT_TOKENwrangler deploy# Verify the Agent Card is livecurl https://code-review-agent.your-subdomain.workers.dev/.well-known/agent.json | jq .
Real-World Use Case — Code Review + Documentation Pipeline
Here's a practical orchestration of three A2A agents working together:
// src/orchestrator.tsimport { A2AClient } from "./a2a-client";import { withRetry } from "./retry-client";export async function runCodePipeline(sourceCode: string) { const reviewClient = new A2AClient(process.env.REVIEW_AGENT_URL!, process.env.REVIEW_AGENT_TOKEN!); const docClient = new A2AClient(process.env.DOC_AGENT_URL!, process.env.DOC_AGENT_TOKEN!); // Verify both agents are reachable before starting const [reviewCard, docCard] = await Promise.all([ reviewClient.getAgentCard(), docClient.getAgentCard(), ]); console.log(`✓ ${reviewCard.name} (${reviewCard.version}) — online`); console.log(`✓ ${docCard.name} (${docCard.version}) — online`); // Run independent tasks in parallel to maximize throughput const [reviewResult, docResult] = await Promise.all([ withRetry(() => reviewClient.sendTask({ taskId: crypto.randomUUID(), message: `Review this code:\n\`\`\`typescript\n${sourceCode}\n\`\`\``, skillId: "review-code", })), withRetry(() => docClient.sendTask({ taskId: crypto.randomUUID(), message: `Generate JSDoc comments and a README section for:\n\`\`\`typescript\n${sourceCode}\n\`\`\``, })), ]); return { review: reviewResult.artifacts?.[0]?.parts?.[0]?.text ?? "", documentation: docResult.artifacts?.[0]?.parts?.[0]?.text ?? "", };}
Running the two tasks with Promise.all cuts wall-clock time by up to 50% compared to sequential execution. In Antigravity, add this to .antigravity/rules.md so the IDE surfaces this pattern automatically:
- Independent agent tasks must be executed with Promise.all, never sequentially
Advanced Patterns: Task Routing and Agent Discovery
As your A2A system grows, hardcoding agent URLs in your orchestrator becomes a maintenance problem. Here's a lightweight agent registry pattern that solves dynamic discovery.
Building an Agent Registry
// src/agent-registry.tstype AgentRegistration = { url: string; token: string; card?: AgentCard; lastSeen: Date; healthy: boolean;};export class AgentRegistry { private agents = new Map<string, AgentRegistration>(); register(name: string, url: string, token: string) { this.agents.set(name, { url, token, lastSeen: new Date(), healthy: true }); } async resolve(name: string): Promise<{ url: string; token: string; card: AgentCard }> { const registration = this.agents.get(name); if (!registration) throw new Error(`Agent not registered: ${name}`); if (!registration.healthy) throw new Error(`Agent unhealthy: ${name}`); // Fetch and cache the Agent Card on first use if (!registration.card) { const res = await fetch(`${registration.url}/.well-known/agent.json`); registration.card = await res.json(); } return { url: registration.url, token: registration.token, card: registration.card! }; } /** Find agents that support a given skill ID */ async findBySkill(skillId: string): Promise<string[]> { const matches: string[] = []; for (const [name, reg] of this.agents.entries()) { if (!reg.card) { try { const res = await fetch(`${reg.url}/.well-known/agent.json`); reg.card = await res.json(); } catch { reg.healthy = false; continue; } } const hasSkill = reg.card?.skills?.some(s => s.id === skillId); if (hasSkill) matches.push(name); } return matches; } /** Periodic health check — mark unresponsive agents as unhealthy */ async healthCheck() { const checks = Array.from(this.agents.entries()).map(async ([name, reg]) => { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 5000); const res = await fetch(`${reg.url}/.well-known/agent.json`, { signal: controller.signal, }); clearTimeout(timeout); reg.healthy = res.ok; reg.lastSeen = new Date(); } catch { reg.healthy = false; } return { name, healthy: reg.healthy }; }); return Promise.all(checks); }}// Usage in orchestratorconst registry = new AgentRegistry();registry.register("code-review", process.env.REVIEW_AGENT_URL!, process.env.REVIEW_AGENT_TOKEN!);registry.register("documentation", process.env.DOC_AGENT_URL!, process.env.DOC_AGENT_TOKEN!);registry.register("test-generator", process.env.TEST_AGENT_URL!, process.env.TEST_AGENT_TOKEN!);// Dynamically route by skillconst reviewAgents = await registry.findBySkill("review-code");console.log(`Found ${reviewAgents.length} agent(s) with review-code skill`);
Skill-Based Load Balancing
When multiple agents share the same skill (e.g., three instances of the code review agent for redundancy), a round-robin or least-loaded routing strategy prevents hotspots:
// src/load-balancer.tsexport class RoundRobinBalancer { private counters = new Map<string, number>(); next(candidates: string[]): string { if (candidates.length === 0) throw new Error("No candidates available"); const key = candidates.join("|"); const current = this.counters.get(key) ?? 0; const selected = candidates[current % candidates.length]; this.counters.set(key, current + 1); return selected; }}// In the orchestrator: pick the least-busy agent for a skillconst balancer = new RoundRobinBalancer();async function delegateToSkill(skillId: string, message: string) { const candidates = await registry.findBySkill(skillId); if (candidates.length === 0) throw new Error(`No agents available for skill: ${skillId}`); const agentName = balancer.next(candidates); const { url, token } = await registry.resolve(agentName); const client = new A2AClient(url, token); return client.sendTask({ taskId: crypto.randomUUID(), message, skillId });}
Performance Benchmarks and Optimization Tips
Understanding performance characteristics helps you make the right architectural tradeoffs.
Latency Breakdown
A typical A2A call has three latency components:
Network overhead: HTTP request/response round trip — typically 20–100ms for same-region Workers
Agent Card lookup: One-time per agent session — cache it after the first fetch
Task processing: Entirely depends on the model and prompt complexity
For the code review use case in this guide, typical performance looks like:
Streaming task (complex module, ~2,000 tokens): 2–4s to first chunk, 8–12s total
Optimization Strategies
1. Parallelize independent tasks aggressively. As shown in the orchestrator example, Promise.all is the single most impactful optimization. If your pipeline has any step that fans out to multiple agents with no dependencies between them, they should run in parallel.
2. Cache Agent Cards with a TTL. Agent Card metadata rarely changes. Cache it with a 1-hour TTL to avoid a redundant HTTP round trip before every task:
const cardCache = new Map<string, { card: AgentCard; expiresAt: number }>();async function getCachedAgentCard(url: string, ttlMs = 3_600_000): Promise<AgentCard> { const cached = cardCache.get(url); if (cached && Date.now() < cached.expiresAt) return cached.card; const res = await fetch(`${url}/.well-known/agent.json`); const card = await res.json() as AgentCard; cardCache.set(url, { card, expiresAt: Date.now() + ttlMs }); return card;}
3. Use streaming for tasks over 500ms. Any task that takes more than half a second benefits from streaming — it keeps your UI responsive and lets users see progress instead of staring at a spinner.
4. Tune retry parameters by error code. Not all errors deserve the same retry behavior. Rate limit errors (-32000) should back off aggressively (5–30 seconds). Internal errors (-32603) can retry quickly (500ms–2s). Network errors should retry 2–3 times before surfacing to the user.
5. Keep Agent Cards small and focused. Each skill in an Agent Card increases discovery time as orchestrators evaluate which agent to route to. Aim for 3–5 focused skills per agent rather than one monolithic agent with 20 skills.
Common Pitfalls and How to Avoid Them
Building A2A systems for the first time surfaces a few recurring mistakes. Here are the ones worth knowing about upfront.
Pitfall 1: Forgetting to set aud in JWT tokens. If your JWT doesn't specify the intended audience (aud), any compromised token can be replayed against any agent in your system. Always include aud: targetAgentId and verify it on the receiving end.
Pitfall 2: Not handling partial streaming artifacts. When reconnecting to a streaming task after a dropped connection, the client may receive overlapping chunks. Track the artifact.index field and skip chunks with an index lower than the last one you successfully processed.
Pitfall 3: Using the same task ID for different tasks. Task IDs must be unique per logical unit of work. Reusing a task ID for a retry is fine (and intentional for idempotency), but using it for a semantically different request causes unpredictable behavior in agents that cache task state.
Pitfall 4: Serving Agent Cards without caching headers. Without Cache-Control: public, max-age=3600, every agent discovery call hits your Worker's compute budget. This adds up quickly when you have many orchestrators polling for available agents.
Pitfall 5: Running health checks too frequently. Aggressive health checking (every 5 seconds across 20 agents) creates unnecessary load. A 30-second interval with a 5-second timeout is a reasonable starting point for most production systems.
What I Learned Running This — Where Standard Protocols Pay Off
The first time I felt what it means to "connect with someone different through a shared standard" was back in 1997, when I was sixteen and discovered the internet. With nothing but self-taught programming and a common communication protocol nobody in particular owned, I could reach a stranger across a national border. That sense of openness still sits somewhere in how I choose technology today. A2A's premise — that agents written in different frameworks and different languages can talk through a single standard interface — feels like a direct continuation of that feeling.
On the practical side, I've spent years running a personal app business (around 50 million cumulative downloads) where backend async jobs are delegated across multiple workers. The thing that hurt most there was never the features themselves — it was the maintenance cost of the bespoke protocols I'd invented. The sender and receiver schemas would quietly drift apart, and fixing one side would break the other. The value of A2A standardizing capability declaration (the Agent Card), task hand-off, and even error codes shows up precisely in that unglamorous maintenance reality.
Three judgment calls became clear only after running this in practice. First, Agent Card caching is usually framed as a speed optimization, but in practice it makes failure isolation far easier — with the card cached, a failed task can be split from the logs immediately into "discovery failed" versus "processing failed." Second, set your health-check interval based on the average task duration, not the number of agents — running 5-second checks on a system full of long-running tasks will flag perfectly healthy agents as unhealthy on a transient timeout. Third, don't build the perfect discovery mechanism up front — while you have three or four agents, a registry as lightweight as the one in this guide is plenty, and premature abstraction only ties your hands later.
Summary
In this guide, we built a production-grade A2A-compliant agent system with Antigravity — from Agent Card design all the way to Cloudflare Workers deployment.
Key takeaways:
Agent Cards are served at /.well-known/agent.json and declare capabilities, auth, and skills
Task delegation has three patterns: synchronous, streaming, and push notifications — choose based on expected latency
JWTs must include iss, aud, and scopes to identify and authorize agents precisely
Error handling should follow A2A standard error codes with exponential backoff for retryable failures
Cloudflare Workers is the ideal deployment target for A2A agents — edge distribution with minimal latency
Parallel task execution via Promise.all is the single biggest throughput win in orchestrator agents
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.