Skip to Content
DocsProductionA2A protocol

A2A protocol

A2A (Agent-to-Agent) is the open protocol for agents from different frameworks and vendors to discover and call each other. Loomflow speaks both sides: serve_a2a(agent) exposes an Agent as an A2A v1.0 endpoint, and A2AClient calls a remote A2A agent — optionally wrapped as a loomflow tool so a local agent can delegate to it.

The wire types are self-contained, implemented from the spec — no a2a-sdk dependency. The only optional dependency is httpx, for the client:

pip install 'loomflow[a2a]'

Server: serve_a2a(agent)

from loomflow import Agent from loomflow.a2a import serve_a2a agent = Agent("You research topics.", model="gpt-4.1-mini") app = serve_a2a( agent, name="research-bot", description="Researches topics and returns summaries.", url="https://bots.example/a2a", # the card's advertised endpoint )

serve_a2a returns a plain ASGI-3 callable — the same framework-free posture as loomflow.serve. It runs under any ASGI server and mounts inside FastAPI/Starlette apps.

The discovery card

The agent card is served at the A2A v1.0 well-known path:

GET /.well-known/agent-card.json

(/.well-known/agent.json, the legacy pre-1.0 path, is also accepted — some SDKs still probe it.) The card carries the name, description, advertised URL, version, and skills. skills= accepts AgentSkill instances or plain {id, name, description} mappings; when omitted a single catch-all run skill is advertised.

The JSON-RPC endpoint

POST / accepts JSON-RPC 2.0 requests. Methods:

MethodBehaviour
message/sendExtracts the text parts of the message, runs the agent, returns a completed Task whose output is a single text-part artifact. The message’s contextId (generated when absent) becomes the loomflow session_id; params.metadata.userId becomes user_id.
tasks/getReturns the last known task by id from an in-memory, bounded task table (1024 tasks; oldest evicted).
message/streamSSE stream of JSON-RPC responses: a working status update, then the run executes, then an artifact update and a final completed | failed status update.

Agent exceptions come back as a task in the failed state (with the error text in status.message), not as a JSON-RPC error — execution failure is task-level, protocol failure is RPC-level.

v1 bounds. Text parts only — a message with no text parts is rejected with invalid-params (file/data parts are not yet consumed). message/stream is coarse: one working frame, then the run executes to completion, then the result frames — per-token streaming via agent.stream is future work. The task table is process-local and in-memory; restarting the server forgets tasks.


Client: A2AClient

from loomflow.a2a import A2AClient async with A2AClient("https://bots.example/a2a") as remote: card = await remote.fetch_card() print(card.name, [s.id for s in card.skills]) reply = await remote.send("summarize today's tickets") # pass the same context_id across calls to hold one remote conversation follow_up = await remote.send("now prioritise them", context_id="ctx-1")

fetch_card() tries the v1.0 well-known path first, falling back to the legacy path on 404. send() raises A2AError on JSON-RPC errors, malformed results, and tasks that come back failed.

http= is the injection seam: any object with post(url, json=...) / get(url) works — an httpx.AsyncClient, a wrapper adding auth headers, or an in-test fake. When omitted, an httpx.AsyncClient is created lazily on first use; close it with aclose() or use the client as an async context manager.


The remote agent as a tool

as_tool() wraps send as a loomflow Tool, making the remote A2A agent a delegate target in any local agent’s tools=[...]:

from loomflow import Agent from loomflow.a2a import A2AClient remote = A2AClient("https://bots.example/a2a") coordinator = Agent( "You coordinate. Delegate research questions to the research bot.", model="gpt-4.1-mini", tools=[remote.as_tool(name="research_bot")], ) result = await coordinator.run("What changed in the EU AI Act this quarter?")

The generated tool takes prompt (required) and an optional context_id — the model can pass the same value across calls to keep one remote conversation. Default name is a2a_delegate; the default description embeds the remote base URL so the model knows what it’s delegating to.

This is the cross-org / cross-framework counterpart of local delegation patterns like Supervisor: same “agent as a tool” shape, but the callee runs behind someone else’s endpoint.


A2A vs plain HTTP serving

loomflow.serveloomflow.a2a
AudienceYour own clients / frontendsOther frameworks’ agents
ProtocolBespoke JSON routes + SSEA2A v1.0 (JSON-RPC 2.0 + discovery card)
DiscoveryNone (you know your own routes)GET /.well-known/agent-card.json
StreamingPer-event SSECoarse (v1): working → result frames

Both are pure-ASGI factories; you can mount both surfaces of the same Agent in one FastAPI app under different prefixes.

Last updated on