AI / ML ·

Google's Agent2Agent Protocol: How A2A Enables Cross-Framework Agent Communication in Production Systems

A deep dive into Google's Agent2Agent (A2A) protocol covering agent cards, task lifecycle, message parts, streaming via SSE, push notifications, and how A2A complements MCP. Includes TypeScript implementation examples, comparison with MCP and direct API integration, and production deployment patterns for multi-vendor agent ecosystems.

Google's Agent2Agent Protocol: How A2A Enables Cross-Framework Agent Communication in Production Systems

Multi-agent systems are becoming the practical unit of work in AI-heavy applications. A pipeline that handles a customer support ticket might involve a routing agent, a knowledge-retrieval agent, a sentiment-analysis agent, and a drafting agent. Each could be built with a different framework, hosted by a different vendor, and owned by a different team. When those agents need to collaborate, the integration story gets messy fast.

Google’s Agent2Agent protocol (A2A) is a proposed open standard for agent-to-agent communication across framework and vendor boundaries. It defines a wire protocol so that an agent built with CrewAI can hand off a task to one built with LangGraph, without either side needing to know the other’s internal structure. This article dissects how A2A works at the protocol level, what the task lifecycle looks like end to end, and what you need to handle in production.

What A2A Is and Is Not

Before going further, a boundary that matters in practice: A2A is not a replacement for the Model Context Protocol (MCP). The two protocols solve different problems at different layers.

MCP is about giving agents access to tools and context: file systems, databases, APIs, local executables. An agent uses MCP to call a tool and get a structured result back. The agent is the client; the tool server is passive.

A2A is about agents talking to other agents. Both sides are active participants that can reason, plan, delegate, and respond asynchronously. An agent uses A2A to say “here is a task, let me know when it is done” and the remote agent handles everything involved in completing it, potentially using MCP tools internally.

In a well-designed multi-agent system, MCP and A2A are complementary layers. MCP connects agents to the world of data and tools. A2A connects agents to each other.

The Agent Card

Agent discovery in A2A starts with an agent card, a JSON document served at a well-known URL path (/.well-known/agent.json) that describes what an agent can do and how to reach it.

interface AgentCard {
  name: string;
  description: string;
  url: string;
  version: string;
  capabilities: {
    streaming?: boolean;
    pushNotifications?: boolean;
    stateTransitionHistory?: boolean;
  };
  authentication: {
    schemes: string[]; // e.g. ["bearer", "oauth2"]
  };
  skills: AgentSkill[];
  defaultInputModes: string[];
  defaultOutputModes: string[];
}

interface AgentSkill {
  id: string;
  name: string;
  description: string;
  tags: string[];
  examples?: string[];
  inputModes?: string[];
  outputModes?: string[];
}

The skills list is the most operationally important part. Each skill is a named capability that a calling agent can reference when creating a task. The tags on a skill allow an orchestrating agent to match tasks to skills without requiring a hard-coded mapping of task types to agent URLs.

A minimal agent card for a research agent looks like this:

{
  "name": "Research Agent",
  "description": "Retrieves and synthesizes information from internal knowledge bases and the web.",
  "url": "https://research-agent.internal/a2a",
  "version": "1.0.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "authentication": {
    "schemes": ["bearer"]
  },
  "skills": [
    {
      "id": "web-research",
      "name": "Web Research",
      "description": "Search for and summarize information on a given topic.",
      "tags": ["research", "web", "summarization"],
      "inputModes": ["text"],
      "outputModes": ["text"]
    }
  ],
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"]
}

An orchestrator that discovers this card knows it can delegate research subtasks to this agent, stream intermediate updates back, and authenticate with a bearer token.

The Task Lifecycle

All communication in A2A is organized around tasks. A task is the primary unit of work: it has an ID, a status, a history of messages, and optional artifacts produced by the agent.

Task status follows a defined state machine:

submitted -> working -> (input-required | completed | failed | canceled)

The transition to input-required matters more than it might seem. It is how A2A handles multi-turn interactions, where the remote agent needs clarification or additional input before it can proceed. The calling agent polls or receives a push notification, provides the additional input, and the remote agent transitions back to working.

Here is the TypeScript shape of a task:

type TaskStatus =
  | "submitted"
  | "working"
  | "input-required"
  | "completed"
  | "failed"
  | "canceled";

interface Task {
  id: string;
  sessionId?: string;
  status: {
    state: TaskStatus;
    message?: Message;
    timestamp: string;
  };
  history?: Message[];
  artifacts?: Artifact[];
  metadata?: Record<string, unknown>;
}

interface Message {
  role: "user" | "agent";
  parts: Part[];
}

type Part = TextPart | FilePart | DataPart;

interface TextPart {
  type: "text";
  text: string;
  metadata?: Record<string, unknown>;
}

interface FilePart {
  type: "file";
  file: {
    name?: string;
    mimeType?: string;
    bytes?: string;   // base64
    uri?: string;
  };
}

interface DataPart {
  type: "data";
  data: Record<string, unknown>;
  metadata?: Record<string, unknown>;
}

interface Artifact {
  name?: string;
  description?: string;
  parts: Part[];
  index: number;
  append?: boolean;
  lastChunk?: boolean;
  metadata?: Record<string, unknown>;
}

The parts model on both messages and artifacts is what makes A2A content-type agnostic. A task response can include a text summary, a file attachment, and a structured JSON payload in the same artifact, without needing a custom schema for each agent type.

Sending a Task

The A2A API is JSON-RPC 2.0 over HTTP. Requests go to a single endpoint (the agent’s URL) with a method name in the body. The four core methods are tasks/send, tasks/get, tasks/cancel, and tasks/sendSubscribe for streaming.

Sending a task to the research agent above:

async function sendTask(
  agentUrl: string,
  token: string,
  query: string
): Promise<Task> {
  const payload = {
    jsonrpc: "2.0",
    id: crypto.randomUUID(),
    method: "tasks/send",
    params: {
      id: crypto.randomUUID(),
      message: {
        role: "user",
        parts: [
          {
            type: "text",
            text: query,
          },
        ],
      },
    },
  };

  const response = await fetch(agentUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify(payload),
  });

  if (!response.ok) {
    throw new Error(`A2A request failed: ${response.status}`);
  }

  const result = await response.json();

  if (result.error) {
    throw new Error(`A2A error ${result.error.code}: ${result.error.message}`);
  }

  return result.result as Task;
}

tasks/send is synchronous in the sense that it returns immediately with the current task state, but the task itself might still be in submitted or working state when it returns. For long-running tasks, you either poll with tasks/get or subscribe for streaming updates.

Streaming via SSE

For tasks that produce incremental output (a research summary being written section by section, a code agent streaming its work), A2A uses tasks/sendSubscribe which responds with a Server-Sent Events stream.

Each SSE event is a JSON-encoded TaskStatusUpdateEvent or TaskArtifactUpdateEvent:

interface TaskStatusUpdateEvent {
  id: string;
  status: Task["status"];
  final: boolean;
}

interface TaskArtifactUpdateEvent {
  id: string;
  artifact: Artifact;
}

The final: true flag on a status update tells the client that the stream is closing and the task has reached a terminal state.

async function streamTask(
  agentUrl: string,
  token: string,
  query: string,
  onChunk: (text: string) => void
): Promise<Task> {
  const taskId = crypto.randomUUID();

  const payload = {
    jsonrpc: "2.0",
    id: crypto.randomUUID(),
    method: "tasks/sendSubscribe",
    params: {
      id: taskId,
      message: {
        role: "user",
        parts: [{ type: "text", text: query }],
      },
    },
  };

  const response = await fetch(agentUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${token}`,
      Accept: "text/event-stream",
    },
    body: JSON.stringify(payload),
  });

  if (!response.ok || !response.body) {
    throw new Error(`Stream failed: ${response.status}`);
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let finalTask: Task | null = null;

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";

    for (const line of lines) {
      if (!line.startsWith("data: ")) continue;

      const event = JSON.parse(line.slice(6));

      if ("artifact" in event) {
        const artifact = event as TaskArtifactUpdateEvent;
        for (const part of artifact.artifact.parts) {
          if (part.type === "text") {
            onChunk(part.text);
          }
        }
      } else {
        const statusEvent = event as TaskStatusUpdateEvent;
        if (statusEvent.final) {
          // Fetch the complete task to get artifacts
          finalTask = await getTask(agentUrl, token, taskId);
        }
      }
    }
  }

  if (!finalTask) {
    throw new Error("Stream ended without final status event");
  }

  return finalTask;
}

One production detail here: artifact chunks can arrive with append: true and lastChunk: false, meaning the client needs to buffer and reassemble them rather than treating each event as a complete artifact. The index field on the artifact identifies which artifact the chunk belongs to when a task produces multiple artifacts concurrently.

Push Notifications

Not all A2A tasks are short enough to hold a connection open. For tasks that take minutes, the protocol supports webhook-style push notifications. The calling agent provides a push notification configuration when creating the task, and the remote agent POSTs status updates to that URL when the task state changes.

interface PushNotificationConfig {
  url: string;
  token?: string; // opaque value echoed back in notifications for validation
  authentication?: {
    schemes: string[];
    credentials?: string;
  };
}

// Task send with push notification
const payload = {
  jsonrpc: "2.0",
  id: crypto.randomUUID(),
  method: "tasks/send",
  params: {
    id: taskId,
    message: {
      role: "user",
      parts: [{ type: "text", text: query }],
    },
    pushNotification: {
      url: "https://orchestrator.internal/a2a/callbacks",
      token: webhookSecret,
    },
  },
};

The remote agent sends a TaskStatusUpdateEvent to your callback URL when the task completes, fails, or reaches input-required. Your callback handler needs to validate the token before acting on the notification, since A2A does not define a webhook signature scheme (HMAC verification is something you layer on top).

Handling the Input-Required State

The input-required state is how a remote agent pauses execution and signals that it needs more information. This is common in agents that clarify ambiguous requests before doing expensive work.

async function handleTaskWithClarification(
  agentUrl: string,
  token: string,
  initialQuery: string
): Promise<Artifact[]> {
  let taskId = crypto.randomUUID();
  let task = await sendTask(agentUrl, token, initialQuery);

  while (task.status.state !== "completed" && task.status.state !== "failed") {
    if (task.status.state === "input-required") {
      const clarifyingQuestion = task.status.message?.parts
        .filter((p): p is TextPart => p.type === "text")
        .map((p) => p.text)
        .join("");

      // In a real system, route this to the calling agent's reasoning loop
      // or back to the user. Here we demonstrate the send pattern.
      const clarification = await getClarificationFromUpstream(
        clarifyingQuestion ?? ""
      );

      const continuePayload = {
        jsonrpc: "2.0",
        id: crypto.randomUUID(),
        method: "tasks/send",
        params: {
          id: taskId,
          message: {
            role: "user",
            parts: [{ type: "text", text: clarification }],
          },
        },
      };

      const response = await fetch(agentUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify(continuePayload),
      });

      task = (await response.json()).result as Task;
    } else if (task.status.state === "working" || task.status.state === "submitted") {
      await new Promise((r) => setTimeout(r, 1000));
      task = await getTask(agentUrl, token, taskId);
    }
  }

  if (task.status.state === "failed") {
    throw new Error(`Task failed: ${task.status.message?.parts[0]}`);
  }

  return task.artifacts ?? [];
}

The key constraint is that follow-up messages use the same task ID. The conversation history is preserved on the remote agent, so you do not need to resend context.

A2A vs Direct API Integration

A comparison that comes up in real architectural decisions:

DimensionA2ADirect API
DiscoveryAgent card at well-known URLManual configuration
ContractProtocol-level (task/message/artifact)Schema-level (varies per API)
Capability negotiationAgent card skills and capabilities flagsNone, client must know ahead of time
StreamingSSE built into the protocolDepends on the API
Multi-turnInput-required state built inMust implement per-integration
AuthenticationDeclared in agent card, multiple schemesPer-API, no common declaration
InteroperabilityCross-framework by designFramework-specific
Operational overheadAgent card publishing, discovery infrastructurePer-vendor integration code
MaturityEarly (spec v0.2.x as of mid-2026)Stable where APIs are stable

The honest tradeoff: A2A gives you a consistent interface across heterogeneous agents at the cost of operational ceremony. Direct API integration is simpler for a pair of agents you control end to end, and there is no overhead of protocol compliance. The value of A2A compounds as the number of agents grows and as the agents cross team or vendor boundaries.

Production Deployment Patterns

Agent Card Freshness

Agent cards are cached by clients. If you update a skill or change an endpoint URL, clients that have cached the old card will send requests to the wrong place or use the wrong schema. Treat agent cards like API versioning: include a version field, publish changes with a deprecation window, and serve the card with appropriate Cache-Control headers.

Authentication in Multi-Vendor Ecosystems

When your agents cross organizational boundaries, bearer tokens alone are not sufficient. Consider using short-lived tokens scoped to specific task IDs, especially when delegating to third-party agents. The agent card declares supported schemes; the actual token issuance is out of scope for the protocol, which means you own that infrastructure.

For internal multi-agent systems, mTLS between agents is a practical choice. Declare "schemes": ["bearer", "mtls"] in the agent card and handle scheme negotiation at the calling agent.

Task Persistence

The A2A protocol is stateless from the transport perspective. The remote agent is responsible for persisting task state. If your agent implementation is serverless or ephemeral, you need task state stored externally (Redis, Postgres, DynamoDB) and indexed by task ID before you return a response to tasks/send. Losing task state mid-execution is recoverable only if the calling agent retries, and retry behavior is not specified by the protocol.

Observability

Every A2A interaction crosses a network boundary and introduces a distributed tracing surface. Propagate trace context in the metadata field of the task params. This is not defined in the spec, but nothing prevents you from including W3C TraceContext headers as metadata:

const params = {
  id: taskId,
  message: { role: "user", parts: [{ type: "text", text: query }] },
  metadata: {
    "traceparent": currentSpan.spanContext().traceId,
    "tracestate": "",
  },
};

Emit spans on both sides. The calling agent records “delegated task” spans; the remote agent records “task received, task completed” spans. Without this, debugging a compound failure across three agents is nearly impossible.

Idempotency

Task IDs are caller-assigned. If your orchestrator retries a failed tasks/send, use the same task ID on retry. A well-implemented remote agent will detect the duplicate and return the existing task state rather than starting a new execution. Document this expectation explicitly when you publish your agent.

Rate Limiting and Backpressure

The protocol does not define rate limits or backpressure signals. In practice you need both. Return HTTP 429 with Retry-After from your agent endpoint when under load. The calling agent is responsible for respecting that and backing off. Do not return a JSON-RPC error for rate limiting; use HTTP status codes so clients that check status before parsing the body fail fast.

Ecosystem Maturity

A2A was announced by Google in April 2025 alongside an initial set of enterprise partners (Atlassian, Salesforce, SAP, and others) and has seen framework support added across LangGraph, CrewAI, Vertex AI Agent Engine, and Amazon Bedrock Agent. The specification is at version 0.2.x as of mid-2026 and is being developed in the open at github.com/google-a2a/A2A.

The core mechanics (task lifecycle, message parts, agent cards, SSE streaming) are stable enough to build on. The edges that are still being refined involve multi-agent session management, long-horizon task delegation, and richer capability negotiation beyond the current skill tags. If you are building on A2A today, plan for schema evolution and test against the reference implementations rather than only against the spec text.

The Practical Boundary

A2A solves exactly one problem: giving agents a common interface to talk to each other without requiring either side to know the other’s internals. That is a real problem in multi-vendor, multi-team agent ecosystems. It does not solve agent reliability, does not guarantee that the remote agent produces correct results, and does not eliminate the need to think carefully about what tasks you delegate and when.

An agent that can delegate to any other agent via A2A is only as reliable as the agents it delegates to. The protocol gives you the plumbing. Evaluation, fallback, timeout handling, and result validation remain your responsibility at the application layer. A2A coordinates. Your orchestration logic decides.

More in AI / ML

How Mixture of Experts Works: Sparse Gating, Expert Routing, and the Architecture Behind Efficient Large Language Models
AI / ML ·

How Mixture of Experts Works: Sparse Gating, Expert Routing, and the Architecture Behind Efficient Large Language Models

A deep dive into MoE architecture: how the gating network routes tokens to experts, top-k selection, load balancing losses, capacity factor, token dropping, expert parallelism for serving, and the real production tradeoffs between dense transformers and sparse MoE models.

AI Agent Frameworks Compared: CrewAI, LangGraph, AutoGen, and Mastra for Production Systems
AI / ML ·

AI Agent Frameworks Compared: CrewAI, LangGraph, AutoGen, and Mastra for Production Systems

A practical comparison of CrewAI, LangGraph, AutoGen, and Mastra for building production AI agent systems. Covers architecture philosophy, state management, tool integration, observability, and deployment patterns with TypeScript code examples.

How Transformer Models Work: Self-Attention, Positional Encoding, and the Architecture Behind Modern LLMs
AI / ML ·

How Transformer Models Work: Self-Attention, Positional Encoding, and the Architecture Behind Modern LLMs

A technical deep dive into the Transformer architecture: tokenization, positional encoding, self-attention with Q/K/V matrices, multi-head attention, the encoder-decoder split, training dynamics, and what it all means for engineers building on top of LLMs.

How LLM Inference Engines Work: KV Caches, PagedAttention, and Continuous Batching From Prompt to Token
AI / ML ·

How LLM Inference Engines Work: KV Caches, PagedAttention, and Continuous Batching From Prompt to Token

A deep dive into the internal architecture of LLM inference engines: autoregressive generation, KV cache memory management, PagedAttention, continuous batching, speculative decoding, tensor and pipeline parallelism, quantization formats, and how to choose between vLLM, TGI, and Ollama in production.