Skip to content

Client

A2AConfig is a ModelConfig — pass it to a regular Agent and the remote A2A server becomes that agent's LLM provider. Conversation history, tool calls and streaming are negotiated through the protocol; calling code keeps the familiar agent.ask(...) / reply.ask(...) shape.

Minimal Client#

from autogen.beta import Agent
from autogen.beta.a2a import A2AConfig

async def main() -> None:
    remote = Agent(
        "remote",
        config=A2AConfig(card_url="http://127.0.0.1:8000"),
    )
    reply = await remote.ask("Add 17 and 25 with calc_add. Just the number.")
    print(reply.response.content)  # -> "42"

card_url is the HTTP(S) base where the server publishes /.well-known/agent-card.json. The client fetches the card on first use, picks a binding from supported_interfaces, and uses the URL declared in the card for every subsequent request — you don't pass transport-specific URLs.

Selecting a Transport#

When the card declares multiple bindings, prefer=... forces a choice:

1
2
3
4
config = A2AConfig(
    card_url="http://127.0.0.1:8000",
    prefer="grpc",  # one of "jsonrpc" | "rest" | "grpc"
)

prefer=None (default) auto-picks: if exactly one declared interface URL matches card_url it wins; otherwise the first server-listed interface is used.

Tip

card_url is always an HTTP URL — even when the resolved transport is gRPC. The card is served over HTTP per spec; only the actual message exchange uses the resolved binding.

Multi-turn — reply.ask#

A2A servers are stateless from AG2's perspective: every call ships the full conversation history as a application/vnd.ag2.history+json DataPart attached to the outgoing message. The continuation API is the regular reply.ask(...):

1
2
3
4
5
remote = Agent("remote", config=A2AConfig(card_url="http://127.0.0.1:8000"))

reply = await remote.ask("Remember the number 42.")
reply = await reply.ask("What number did I ask you to remember?")
print(reply.response.content)

The remote agent recovers the entire prior context on every turn — there is no server-side session id to manage.

Local Tools (forwarded from the server)#

Tools declared on the client are advertised to the server in the AG2 client-tools extension. When the remote LLM picks one, the call is routed back to the client and executed locally; the tool result is sent back into the server-side LLM loop. The server LLM never sees your local environment.

from datetime import datetime

from autogen.beta import Agent
from autogen.beta.a2a import A2AConfig
from autogen.beta.tools import tool

@tool(description="Return the user's local wall-clock time as ISO-8601.")
def get_local_time() -> str:
    return datetime.now().isoformat(timespec="seconds")

remote = Agent(
    "remote",
    config=A2AConfig(card_url="http://127.0.0.1:8000"),
    tools=[get_local_time],
)
reply = await remote.ask("What time is it on my machine? Use get_local_time.")

The same agent can mix client-side and server-side tools — server tools execute remotely, client tools execute locally, and the LLM picks freely between them within one turn.

Warning

If the remote AgentCard does not advertise the urn:ag2:client-tools:v1 extension, passing tools=... raises A2AClientToolsNotSupportedError. Only AG2-backed servers support client-side tool forwarding today.

Remote Agent as a Sub-tool — as_tool()#

A remote A2A agent plugs into a local Agent like any other delegate via Agent.as_tool(). The local LLM decides when to delegate; the wrapper exposes a task_<name> tool that takes an objective (and optional context).

from autogen.beta import Agent
from autogen.beta.a2a import A2AConfig
from autogen.beta.config import AnthropicConfig

researcher = Agent(
    "researcher",
    config=A2AConfig(card_url="http://research.internal:8000"),
)

writer = Agent(
    "writer",
    prompt="Use the researcher tool to gather facts before writing a draft.",
    config=AnthropicConfig(model="claude-sonnet-4-6"),
    tools=[researcher.as_tool(description="Delegate research questions to the remote researcher.")],
)

reply = await writer.ask("Write a 3-paragraph brief on the latest A2A spec changes.")

This composes naturally with several remotes — give each as_tool() a distinct name= and let the local LLM route by capability. See Sub-task Delegation for the general as_tool() semantics.

Note

Each task_<name> call spawns a fresh sub-agent stream; history between calls is not preserved on the sub-task side. For a remote that remembers prior turns, prefer the reply.ask(...) pattern above instead of as_tool().

A2AConfig Reference#

Field Type Default Purpose
card_url str required Base URL where /.well-known/agent-card.json is served
prefer Optional[Literal["jsonrpc", "rest", "grpc"]] None Force a specific binding when the card declares more than one
streaming bool True Use sendStreaming when the server's card opts in. Falls back to polling otherwise
headers Optional[Mapping[str, str]] None Extra HTTP headers (auth, tracing)
timeout Optional[float] 60.0 Per-request timeout in seconds
max_reconnects int 3 Streaming reconnect attempts (see Advanced)
reconnect_backoff float 0.5 Backoff between reconnect attempts (seconds)
polling_interval float 0.5 Poll interval when streaming is off
input_required_timeout Optional[float] None Cap how long the client waits on a HITL hook
httpx_client_factory Optional[Callable[[], AsyncClient]] None Custom httpx.AsyncClient (proxies, custom TLS, etc.)
interceptors Sequence[ClientCallInterceptor] () A2A SDK call interceptors
grpc_channel_factory Optional[Callable[[str], Channel]] None Custom gRPC channel builder (defaults to insecure)
preset_card Optional[AgentCard] None Skip the discovery round-trip when the card is already known
tenant Optional[str] None Multi-tenancy scope on a shared backend
history_length Optional[int] None Server-side hint to truncate echoed Task.history

Constructing From a Pre-fetched Card#

When the card has already been resolved (discovery service, on-disk cache), A2AConfig.from_card(...) skips the network round-trip on connect:

1
2
3
from autogen.beta.a2a import A2AConfig

config = A2AConfig.from_card(card, prefer="jsonrpc", timeout=30.0)

card_url defaults to the first interface URL on the card; pass card_url=... to override.