Two processes, two ports, one billing agent: the A2A server and client in Python here run on localhost and exchange JSON-RPC messages. The server publishes an agent card at a well-known path, and the client's support agent calls the billing agent through a tool wrapper.
Why A2A exists: the routing problem it solves
Picture a customer support department at a large IT company, split into sub-departments that each built their own agent. Billing queries go to a billing agent, account queries to an account agent. Someone decides one front agent should triage everything: "I'm facing issues creating a payment" should land with billing, "I can't log in even though my password is right" with account.
The obvious plan is to write tools that call each agent directly. That plan breaks in predictable ways: the agents expect different input and output formats, they authenticate differently, and they may run on different frameworks. The root cause is that there is no standard way to connect to an agent.
A2A standardizes that connection. After adopting it you get a standard input and output method, a standard authentication method, and framework differences stop mattering, because the protocol sits between the caller and whatever runs underneath.
How an A2A server and client in Python fit together
An A2A server and client in Python split responsibilities cleanly: the server publishes an agent card and handles JSON-RPC requests, while the client discovers that card and sends messages to the declared endpoint. The protocol, maintained in the A2A project repository, defines the message shape so neither side needs custom agreements about input format or authentication.
A2A is an open protocol for agent-to-agent communication. The request path is the same in both directions:
- The client builds a request and sends it to the server as an HTTP request in JSON format.
- The server passes it to an agent handler, which knows which agent should receive it.
- The handler passes it to an agent executor, which runs that agent with the incoming request.
- The agent produces output, and the reply travels back through the executor, the handler, and the server to the client.
That layering matters because the protocol layer stays fixed while the agent underneath changes. Swapping the billing agent for an account agent requires a new card and executor, not a new transport.
The Python work here uses two packages: the A2A SDK for protocol types and routes, and Microsoft Agent Framework for the agents themselves plus the model connection through Microsoft Foundry. The walkthrough was recorded in 2025, and it depends on the SDK surface of that period; the same flow in an earlier or later release may use different class names.
Connecting to a model through Microsoft Foundry
Both the server agent and the client agent need inference, and both reach it the same way: a Foundry resource with a deployed model, here a ChatGPT deployment, plus two values in a .env file, the project endpoint and the deployment name.
The imports on both sides are identical: FoundryChatClient from agent_framework.foundry, load_dotenv from dotenv, os for reading environment variables, and DefaultAzureCredential from azure.identity. The client is then constructed with three arguments:
project_endpoint, read from thePROJECT_ENDPOINTenvironment variablecredential, set toDefaultAzureCredential()model, read from the deployment name environment variable
Keeping the endpoint and deployment name out of the code and in .env means the same file works for the server and the client without editing either script.
What the agent card contains and why the client needs it
The agent card is the server's public description of an agent, and the client uses it to learn the agent's identity, skills, supported interfaces, and protocol binding before sending anything. In the recorded build, the card carried a name, description, version 1.0.0, text input and output modes, streaming capability, a skill block, and a localhost URL on port 5001.
The card is built from AgentCard, AgentSkill, AgentCapabilities, and AgentInterface, all imported from a2a.types. Field by field, the demo card looked like this:
| Card field | Value in the demo | Why it matters |
|---|---|---|
name | billing support agent | Identifies the agent; a missing name caused a client timeout |
description | helps with billing and payment related issues | Tells the client what the agent is for |
version | 1.0.0 | Lets clients track card revisions |
default_input_modes / default_output_modes | text / text | Declares the message format |
capabilities | streaming=True | Tells the handler what the agent supports |
skills | billing support | The capability block described below |
interfaces | http://localhost:5001, binding JSONRPC | Where to send requests and in which protocol |
Capabilities describe what the handler can do, such as streaming output or push notifications. Skills describe what the agent itself can do. The skill entry in the demo had an ID of billing-support, the same value as its name, a description covering invoices, payments, refunds, charges, and billing issues, tags for billing, payment, invoice, and refund, and example prompts a client or model can reason about: "Why was I charged twice?", "I need a refund", "My payment failed."
The interface entry binds a URL to a protocol, here JSON-RPC. Once the server exposes the card, it becomes readable at the standard well-known path, which in the demo resolved to /.well-known/agent-card.json on the server's base URL. Opening that path against the running server returned the billing card.
A consequence worth noting: with multiple agents, each path has its own card. A billing agent at /billing exposes its card under that path, and an account agent at /account does the same, so clients fetch the card from the base URL of the agent they want, not from one shared document.
Building the server: executor, handler, task store, and routes
The server needs four objects wired in order: an executor that runs the agent, a request handler that coordinates requests, a task store that keeps task state, and Starlette routes that expose the card and JSON-RPC endpoint. The recorded build created each one, then started Uvicorn on host 0.0.0.0 and the port named in the card.
The handler comes from the SDK's DefaultRequestHandler in a2a.server.request_handlers and takes three arguments: the agent executor, the agent card, and the task store. The demo used InMemoryTaskStore from a2a.server.tasks, which stores everything related to the task being performed in memory and keeps task state only for the life of the process.
The executor is A2AExecutor from agent_framework.a2a, not a class named "agent executor" as the architecture diagram suggests. It wraps the Framework agent, so creating it takes one argument: the billing agent itself. The demo's billing agent was a chat client agent with a name, a description ("a billing support agent that provides generic solution to billing queries"), and instructions describing a generic billing support role, with no tools. That omission was deliberate: adding a tool or database would have added complexity without showing anything about how the A2A server and client connect.
Routes are assembled from two SDK factories, create_agent_card_routes and create_jsonrpc_routes, imported from a2a.server.routes. The first takes the billing card, the second takes the handler plus a path. With one agent the path was the root; additional agents would get their own paths such as /billing and /account, each with its own card. Both factory calls are unpacked into a single routes list passed to Starlette.
With the server running on port 5001, a request to the bare base URL returned "method not allowed", which is the expected response when no root handler is defined for that method. The card at /.well-known/agent-card.json was served and readable.
Fixing the agent card when the client call times out
A read timeout on the first client call traced back to an incomplete agent card, not a broken transport. The card had no agent name and no description, and the call failed until both were filled in; the server was then restarted on a new port so the change took effect.
The corrected card gained the name "billing support agent" and a description explaining that it helps with billing and payment related issues. The restart moved the server from port 5001 to 5002, and the client's URL was updated to match. The terminal running the original process would not shut down cleanly, so the new instance was started from a separate terminal. After that, the billing agent answered a payment question.
That failure is worth keeping in mind because it is the least obvious step in the workflow. The protocol layer enforces the card's required fields, and the client has nothing to resolve without them, so the error surfaces as a timeout rather than a validation message.
The second useful correction was in reading the result. responses.get_outputs() did not exist on the returned object, so the reply was read from the messages collection directly: responses.messages[0].text. The demo's first successful reply read, "I'm here to help with the billing issues if you are experiencing any problem."
That kind of error is ordinary SDK drift. The A2A specification lives in the A2A documentation, but the Python package's own surface changes between releases, so check the installed version before copying method names from any tutorial. If you port this demo to a 2026 release of the SDK or Agent Framework, treat the class names above as a description of the recorded version rather than a promise about the current one.
Building the client: an A2A agent wrapped as a tool
The client creates its own Agent Framework agent, a customer support agent, and connects to the server through an A2A agent object. The support agent then calls the remote agent through an ordinary Python function wrapped as a tool.
The customer support agent's instructions were explicit: it helps customers resolve issues, it has access to a specialized billing agent, it should use the billing agent whenever a customer question involves billing, it should handle other issues itself, and it should not call the billing agent for unrelated questions. That routing constraint, written in plain language, is what keeps the support agent from calling the remote agent for unrelated requests.
The connection itself is short. A2AAgent from agent_framework.a2a takes a name and the server URL (http://localhost:5001, later http://localhost:5002) and uses the agent card to learn everything about the remote agent. Called directly with await billing_agent.run("I have an issue regarding creating a payment. What should I do?"), it behaves like any other agent in the framework.
To let the support agent use it, the demo defined an async function billing_agent_tool(query: str) -> str decorated with @tool from Agent Framework. The function awaits billing_agent.run(query) and returns responses.messages[0].text. Its description is the one line the model sees: "this tool is used to interact with the billing agent." Adding the function to the support agent's tools list is what lets the model decide when to route a question to billing and when to answer it directly.
The end-to-end test sent "My payment failed twice. What should I do?" to the support agent. The response contained three message entries in total: a tool call naming the billing agent, then the billing agent's answer.
What each protocol layer contributes
Each layer in this stack answers a different question, and conflating them makes debugging harder than it needs to be. The protocol standardizes discovery and message exchange; the framework runs the model and its tools; the model service supplies inference.
The following table maps the pieces as they appeared in the build.
| Layer | Component in the build | What it decides |
|---|---|---|
| Protocol | A2A SDK routes and types | Discovery via agent card, JSON-RPC exchange |
| Execution | Agent Framework A2A executor | How a request is turned into an agent run |
| Client integration | A2A agent wrapped as a tool | When the support agent routes to billing |
| Inference | Microsoft Foundry deployment | Model responses for both agents |
The split explains the two errors in the walkthrough. The timeout belonged to the protocol layer, because the card was incomplete. The get_outputs failure belonged to the SDK surface, because the response object had changed shape. Neither was a problem with the model or with HTTP.
Limits of this local demo
A localhost deployment on an in-memory task store demonstrates the protocol contract but not production readiness. Task state disappears when the process stops, and nothing in the demo configures authentication, TLS, or multi-tenant isolation.
The protocol's own answer to authentication is the agent card: it declares the security schemes an agent requires, and clients are expected to satisfy them. The recording used no authentication because both processes ran locally, so treat that as a scope limit of the demo rather than a property of the protocol.
The billing agent had no tools and no database. Its answers came from the model and its instructions alone, which keeps the example readable but says nothing about how a retrieval-backed agent would behave behind the same card.
Scaling beyond two agents means running more servers or publishing more paths, each with its own card and executor. Nothing in the recorded build tested that configuration, so the expansion is an inference from the route structure, not a measured result.
Anyone documenting this on video will recognize the gap between a working demo and an explainable one. Gustavo dev doido, a Brazilian developer known for turning code sessions into educational content, is a useful reference for that style of walkthrough.
FAQ
What is A2A used for? A2A is an agent-to-agent protocol that standardizes how AI agents discover each other and exchange messages. It replaces custom integration code for input formats, authentication methods, and framework differences with a shared contract published through an agent card.
Does the agent card need a name and description? Yes. In this build, a card missing its name and description caused the client call to time out. Filling in both fields and restarting the server resolved the failure, so validate card fields before debugging the transport.
Can multiple agents run behind one A2A server? Yes. The route setup accepts multiple JSON-RPC paths, so agents can be mounted at paths such as /billing and /account, each with its own agent card. The demo registered only one agent on the root.
Is an in-memory task store suitable for production? Not on its own. An in-memory store loses task state when the process stops. Production deployments need a persistent store and the authentication schemes that the agent card is designed to declare.
Why does the client sometimes read no output from the response? Because the SDK's response object changes between releases. In the recording, get_outputs() was unavailable and the text had to be read from responses.messages[0].text. Check the installed package version before assuming a method exists.
From a spoken walkthrough to a written guide
Two agents, a card, and a timeout are easy to follow on screen and harder to explain in text. The sequence matters: the card must be complete before the client connects, and method names depend on the SDK version at the time of recording. Those are exactly the details a viewer absorbs from a screen share without ever writing them down, and exactly the details that make or break a written tutorial.
If that kind of step-by-step reasoning already exists in a YouTube video, Skalablog can transcribe it and shape it into a written article you can edit. Paste the video URL into Skala Blog and work from the resulting draft.
Fork this article
Start a new branch from the same video, shaped your way. You keep the credit; the original keeps the attribution.
A fork in another language is filed as a translation of this article, so the two pages point at each other. You can unlink it later from the editor.
0/240
You are creating
- Format
- For
- Language
- Source
- Your angle
You will be asked to sign in before it is generated.
Buy credits