Skip to content
Forward Deployed

Forward Deployed
Lessons and contents
A2A: Agent-to-Agent ProtocolLesson 2 of 6
6 lessons
Learn

A2A basics: Agent Cards, tasks, messages

Source
From the RAG & Agents Engineering course
By
Sourav Banerjee
Last updated
9 Nov 2026

Report an issue

Agent Card

Larkspur knows a handbook agent's base URL but not its skills or supported methods. You will fetch the Agent Card, read the wire field names and follow one request through discovery to an agent reply.

Step 1: Discover the agent contract before sending work

An Agent Card is the JSON document an A2A server publishes about itself. A client that knows only a base URL fetches the card first, and everything else it needs comes from there: where to post requests, which skills the agent has, and whether it can stream.

In a2a-sdk 0.3.26, AgentCard has eight required fields:

Field What the client does with it
name, description, version Shows or logs which agent, and which release of it, answered
url Posts JSON-RPC requests here
capabilities Checks streaming before calling message/stream; the others are push_notifications, state_transition_history and extensions
skills Picks the agent for a job: each AgentSkill needs id, name, description and tags, and may add examples and its own input and output modes
default_input_modes, default_output_modes Learns which content types the agent accepts and returns

Step 2: Inspect defaults and authentication metadata

Two fields have defaults the reference implementations never set: protocol_version is 0.3.0 and preferred_transport is JSONRPC. Authentication also lives on the card, in security_schemes and security, and an agent can keep some skills on an authenticated extended card (supports_authenticated_extended_card, served at /agent/authenticatedExtendedCard).

On the wire the fields are camelCase. This is the card the lab's handbook agent serves, recorded from a live server:

{
  "capabilities": { "streaming": true },
  "defaultInputModes": ["text"],
  "defaultOutputModes": ["text"],
  "description": "Answers questions from the Larkspur Cloud handbook.",
  "name": "larkspur_handbook_agent",
  "preferredTransport": "JSONRPC",
  "protocolVersion": "0.3.0",
  "skills": [{
    "id": "handbook_qa", "name": "handbook_qa",
    "description": "Answers billing, export and outage questions from the Larkspur Cloud handbook.",
    "tags": ["rag", "handbook"],
    "examples": ["Can I export my data after I cancel my plan?", "How long does a refund take?"]
  }],
  "url": "http://localhost:10002/",
  "version": "1.0.0"
}

Inspect field names and version defaults without running an agent. The card is an illustrative local object for the SDK pinned in this repository.

from a2a.types import AgentCard, AgentCapabilities, AgentSkill

card = AgentCard(
    name="Handbook", description="Answer Larkspur policy questions.",
    url="http://127.0.0.1:10002/", version="1.0.0",
    capabilities=AgentCapabilities(), default_input_modes=["text"],
    default_output_modes=["text"],
    skills=[AgentSkill(id="handbook", name="Handbook", description="Search policy.", tags=["policy"])]
)
wire = card.model_dump(by_alias=True)
print(wire["protocolVersion"])
print(wire["preferredTransport"])
print("defaultInputModes" in wire)

Expected output

0.3.0
JSONRPC
True

Step 3: Fetch the SDK version's actual discovery path

The reference implementations say /.well-known/agent.json. In 0.3.26 the SDK's constant is AGENT_CARD_WELL_KNOWN_PATH = "/.well-known/agent-card.json", and A2ACardResolver fetches that path. A2AStarletteApplication serves both: the old path still returns the card and logs that it is deprecated and will be removed. Clients written against the new SDK never touch the old path, so the reference implementations work unchanged.

The card is a contract. A skill description is all a calling agent's model knows about the agent, the same way a tool description is all it knows about a tool (T9.4). Changing streaming, a skill id or the URL breaks callers that read the old card, so bump version when you do.

From discovery to reply

Step 1: Read the request envelope and reply kind

The echo-server example runs one server and one client in the same process. The server is an echo agent: its AgentExecutor puts a single agent message on the event queue. The client goes through four steps, and each maps to one HTTP call or one decision:

Diagram: Discover GET agent-card.json; Negotiate read skills and streaming; Submit POST message/send; Read the reply Message or TaskDiagram: Discover GET agent-card.json; Negotiate read skills and streaming; Submit POST message/send; Read the reply Message or Task

Step 2: Read the JSON-RPC request envelope

Submitting means posting a JSON-RPC 2.0 request. The message has a role (user or agent), a client-chosen messageId and a list of parts, each a text, file or data part:

{
  "jsonrpc": "2.0", "id": "req-1", "method": "message/send",
  "params": { "message": {
    "kind": "message", "role": "user", "messageId": "id-1",
    "parts": [{ "kind": "text", "text": "Can I export my data after I cancel my plan?" }]
  } }
}

Step 3: Distinguish a message reply from a persisted task

The echo agent answers with a Message: "kind": "message", "role": "agent", and the text in parts. The request context hands the executor a contextId, which groups related turns, and a taskId, and the executor copies both into its reply. It never created a task, though, so nothing was stored. In the lab, tasks/get on that id returns error -32001, Task not found. A Message is the whole answer; there is nothing to poll.

The server side of the example is three objects: an AgentExecutor with the agent logic, a DefaultRequestHandler that implements the JSON-RPC methods on top of it with an InMemoryTaskStore, and an A2AStarletteApplication that serves the card and the endpoint. The client side is A2ACardResolver and A2AClient. In 0.3.26, constructing A2AClient emits a DeprecationWarning that points to ClientFactory; the reference implementations' calls still work.

Try it yourself

Fetch the card at /.well-known/agent-card.json for the recorded SDK version and compare its url and skills with the request destination. Send one echo message and check kind=message. Do not poll it as a Task or infer that the server enforces authentication just because a security scheme appears on the card.

Practice notebook

Run the complete implementation from the practice material after checking the trace or calculation above. Use its stated environment for live model or server calls; recorded model choices and timings are observations, not guaranteed outputs.

Continue learning

Run the notebooks

The code for this lesson is in these notebooks. Colab runs one in your browser: install the packages its first cell imports and set the API keys it reads. To run every notebook as written, clone the repository and use its uv environment.

Notebook GitHub Colab
A2A protocol basics Open on GitHub Open in Colab

Key Takeaways

  • A client that knows only a base URL fetches the Agent Card first. It learns the endpoint, the skills, streaming support and accepted content types from it.
  • Treat the card as a contract. Skill descriptions are all a calling model knows, so bump version when a skill id, streaming or the URL changes.
  • A reply that is a Message is the whole answer and stores nothing to poll. A request is a JSON-RPC message/send with a role, a messageId and parts.

Check yourself

1. A client knows only an A2A agent's base URL. What does it fetch first, and what does it learn from it?

The Agent Card, which A2ACardResolver in 0.3.26 fetches from /.well-known/agent-card.json. The card tells it where to post requests (url), which skills the agent has, whether it can stream (capabilities), and which content types it accepts and returns.

2. The echo agent answers message/send with a Message. What happens if the client calls tasks/get with the taskId from that reply?

It gets error -32001, Task not found. The executor copied the contextId and taskId from the request context into its reply but never created a task, so nothing was stored. A Message is the whole answer; there is nothing to poll.

3. You rename the Larkspur handbook agent's skill id from handbook_qa to billing_qa. What else must change on the card, and why?

Bump version. The card is a contract: a calling agent's model knows the agent only through what the card says, and changing streaming, a skill id or the URL breaks callers that read the old card.

Rate your confidence and keep notes on the module page.