One agent serving many tenants with strict context isolation, and how clients and servers determine the same protocol version. Including migration strategies from v0.3 to v1.0 without downtime.

In episode 9 your agent was secured with authentication and signed agent cards. But there's a practical problem we haven't touched: how does one agent instance serve dozens or hundreds of clients at once — bank A, marketplace B, logistics C — without mixing their data? And how can an agent written for v1.0 still talk to a client still on v0.3? This episode answers both, two features that are pillars of A2A v1.0's maturity.
This episode's roadmap: we dissect multi-tenancy — from the concept, to using tenant in the wire protocol, to data isolation — then version negotiation and migration strategies between versions.
A tenant is a logical isolation unit — usually one organization or one project subscribed to the agent. In a multi-tenant model, one agent process serves many tenants at once, and each tenant sees itself as the only user of the service. Imagine a single "Sales Analysis" agent serving Bank A and Marketplace B: both call the same endpoint, but their tasks, messages, and results must never leak into each other.
Per-tenant context affects three things at once: state (which tasks are running), data (which datasets are accessible), and configuration (keys, quota limits, each tenant's policies). All three must be scoped to the tenant, not shared globally. Without this separation, one small bug could show Bank A's data to Marketplace B — the classic SaaS incident.
A2A v1.0 supports multi-tenancy explicitly: a JSON-RPC request can carry a tenant parameter that marks the task's owner. The server reads this value and ensures the entire task lifecycle runs in that tenant's context:
{
"jsonrpc": "2.0",
"id": 7,
"method": "message/send",
"params": {
"tenant": "acme-corp",
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "Analisis tren penjualan Q3" }]
}
}
}On the SDK side, the tenant isn't just a label in the payload — it becomes part of the task storage key. The server can use a task store that separates tasks by tenant, so a tasks/get for tenant A will never find a task belonging to tenant B:
class TenantScopedTaskStore(InMemoryTaskStore):
def __init__(self) -> None:
self.tasks: dict[str, dict[str, Task]] = {}
def _key(self, tenant: str, task_id: str) -> str:
return f"{tenant}:{task_id}"
def get_task(self, tenant: str, task_id: str) -> Task | None:
return self.tasks.get(tenant, {}).get(task_id)This pattern reinforces an important principle: the tenant is part of the task's identity, not secondary metadata. It's carried through tasks/get, tasks/cancel, and push notifications so every access path uses the same tenant lens.
Info
When building a multi-tenant agent, make the tenant part of every path: authentication (the token carries a tenant claim), task store (combined tenant and task id key), artifacts (stored in a tenant namespace), and monitoring (tenant label on metrics). If any path forgets it, that's where the leak will happen.
Isolation isn't just about tasks — it covers the agent's entire data surface:
Two technical principles must be upheld. First, the tenant must be deterministic — the tenant value always comes from a trusted source (the authenticated token claim), not from task input that a client could manipulate. Second, default deny — without an explicit mapping, a task must not access another tenant's data.
The A2A ecosystem evolves fast — from v0.1 to v1.0 within the span of a year — and not everyone can upgrade at once. Version negotiation is the mechanism by which clients and servers determine the same protocol version before a transaction begins, and A2A makes it an official part of the Agent Card.
Two negotiation points are used:
protocol_version the server understands, and each supported_interfaces entry can list a version on its endpoint.{
"agentName": "Analisis Lead",
"protocol_version": "1.0",
"supported_interfaces": [
{ "protocol_binding": "JSONRPC", "url": "https://lead-agent.example.com/", "protocol_version": "1.0" },
{ "protocol_binding": "JSONRPC", "url": "https://lead-agent.example.com/v0.3", "protocol_version": "0.3" }
]
}Because the negotiation happens at the card level, which can be cached, v0.3 and v1.0 clients can coexist calling the same agent — each to their version's endpoint. This is the back-compat contract that makes staged rollouts possible.
A2A v1.0 introduces breaking changes compared to v0.3 — method name adjustments (tasks/send became message/send), event structure changes, and a new discovery mechanism. Safe migration doesn't happen in a single day. The common staged strategy:
a2a.compat.v0_3 for old handlers, so agent logic is written once.Warning
Don't postpone migration forever with the excuse "it's still compatible". The longer two versions run in parallel, the higher the maintenance cost and the more clients hang on the old endpoint. Set a retirement date from day one of launching the new endpoint.
Episode 10 brings your agents to the multi-client production level: multi-tenancy with the tenant parameter as part of the task identity, per-tenant data and configuration isolation with deterministic tenant and default deny principles, then version negotiation via protocol_version on the Agent Card that lets old and new clients coexist, capped off with a staged, measurable v0.3-to-v1.0 migration strategy.
Here's the core takeaway:
tenant is a logical isolation unit; one agent serves many tenants without mixing data.tenant, and the task store uses a combined tenant plus task id as the key.protocol_version on the Agent Card and supported_interfaces.The more tenants and the more diverse the clients, one thing starts to feel expensive: the HTTP+JSON overhead on every request. In episode 11 we discuss the solution — gRPC Support — A2A's third binding with protobuf, the advantages of streaming and backpressure, and when gRPC is more appropriate than HTTP/JSON. See you there!