Skip to content

Talking to A2A 0.3 agents and clients

A2A 1.0 changed the wire format: method names (message/send → SendMessage), the kind discriminators, enum values (completed → TASK_STATE_COMPLETED) and the Agent Card layout. Many agents and clients still speak A2A 0.3. The SDK bridges the gap in both directions, the same way the Python SDK does (a2a.compat.v0_3). Your code stays on the v1.0 types either way.

You are What happens
A client calling a 0.3 agent Automatic. ClientFactory reads the card, sees a 0.3 interface and uses the 0.3 transports.
A server that 0.3 clients call Turn on enableV03Compat and list 0.3 interfaces in your card.

Call a 0.3 agent

Nothing to configure. A 0.3 card (url, preferredTransport, additionalInterfaces, protocolVersion: "0.3.x") becomes supportedInterfaces with version 0.3. ClientFactory then picks Compat\V0_3\CompatJsonRpcTransport or CompatRestTransport:

$client = ClientFactory::createClient('https://old-agent.example.com'); // a 0.3 agent

foreach ($client->sendMessage($request) as $event) {   // the same v1.0 types as always
    // ...
}

When a card offers both 1.0 and 0.3 interfaces, 1.0 wins.

The 0.3 transports send A2A-Version: 0.3 and both extension headers (A2A-Extensions and the 0.3 X-A2A-Extensions). They always state blocking explicitly, so a 0.3 server never falls back to its own default: over 0.3 REST that default is non-blocking.

Serve 0.3 clients

Add 0.3 interfaces next to the 1.0 ones (same URLs), and turn on enableV03Compat:

use A2A\Server\Routes\Routes;
use A2A\Types\{AgentCard, AgentInterface};

$agentCard = new AgentCard([
    // ...name, description, version, capabilities, skills as usual...
    'supported_interfaces' => [
        new AgentInterface(['protocol_binding' => 'JSONRPC',   'protocol_version' => '1.0', 'url' => $base . '/a2a/jsonrpc']),
        new AgentInterface(['protocol_binding' => 'HTTP+JSON', 'protocol_version' => '1.0', 'url' => $base . '/a2a/rest']),
        new AgentInterface(['protocol_binding' => 'JSONRPC',   'protocol_version' => '0.3', 'url' => $base . '/a2a/jsonrpc']),
        new AgentInterface(['protocol_binding' => 'HTTP+JSON', 'protocol_version' => '0.3', 'url' => $base . '/a2a/rest']),
    ],
]);

$router = Routes::router($handler, $agentCard, jsonRpcPath: '/a2a/jsonrpc', restPrefix: '/a2a/rest', enableV03Compat: true);

Routes::jsonRpc() and Routes::rest() take the same enableV03Compat flag. examples/hello-world/server.php does exactly this.

A2A_V0_3_COMPAT=true

or 'v0_3_compat' => true in config/a2a.php. When the bridge fills in the card's interfaces from your routes, it adds the 0.3 ones too.

create_jsonrpc_routes(request_handler=handler, rpc_url='/a2a/jsonrpc', enable_v0_3_compat=True)
create_rest_routes(request_handler=handler, path_prefix='/a2a/rest', enable_v0_3_compat=True)

It is off by default, as in Python.

What gets served

Binding v0.3 Notes
JSON-RPC message/send, message/stream, tasks/get, tasks/cancel, tasks/resubscribe, tasks/pushNotificationConfig/{set,get,list,delete}, agent/getAuthenticatedExtendedCard Same endpoint as 1.0. The method names never clash, so requests are routed by name.
HTTP+JSON /v1/message:send, /v1/message:stream, /v1/tasks/{id}, /v1/tasks/{id}:cancel, /v1/tasks/{id}:subscribe (GET or POST), /v1/tasks/{id}/pushNotificationConfigs[/{configId}], /v1/card Under the same REST prefix as 1.0. Bodies are the 0.3 ProtoJSON (content instead of parts, TASK_STATE_CANCELLED).
Agent Card the 0.3 fields (url, preferredTransport, additionalInterfaces, protocolVersion, the 0.3 security and scheme shapes) are merged into the served card Only when the card lists a 0.3 interface. The 1.0 fields are never changed, so 1.0 clients read the card as before.

Your executor always sees v1.0 objects. The SDK converts on the way in and out.

Differences to know

  • No ListTasks. It is not part of A2A 0.3. The 0.3 transports throw \BadMethodCallException, and the 0.3 routes don't serve it.
  • Error details. 0.3 JSON-RPC errors are {code, message} with the same codes as 1.0 (-32001 task not found, …). The PHP server keeps the real code. Python's compat adapter turns every A2A error into -32603.
  • 0.3 REST errors. 0.3 servers answer with a bare {"message": ...} and an HTTP status. So over REST, a 404 from a 0.3 server can't be told apart from a missing route, and becomes MethodNotFoundError, as in Python. PHP servers send the 1.0 error body, which maps back to the exact error.
  • Dropped in 0.3. stateTransitionHistory (card) and the OAuth device-code flow have no 0.3 counterpart. Non-object data parts travel as {"value": ...} with a data_part_compat marker, as in Python.
  • gRPC isn't supported, in 0.3 or 1.0.

Proof

The v0.3 layer is tested against the last 0.3 release of the official Python SDK (a2a-sdk 0.3.26) in CI, in both directions:

  • a real 0.3 client against the PHP server: plain PHP and a Laravel app with executors on queue workers
  • the PHP client against a real 0.3 server

See Conformance. To run it yourself: scripts/run-python-interop-v03.sh and scripts/run-tck-laravel.sh v03-interop.