Skip to content

Python SDK → PHP mapping

Reference: a2aproject/a2a-python @ 0d5473c (2026-09-24). This is PyPI a2a-sdk, 118 test files, Apache-2.0. This table is the "same shape" contract: every public Python class gets a PHP class with the same name, in the same position in the tree. Method names change from snake_case to camelCase. That is the only rename.

Public API vs internals

Some classes below exist to mirror Python's internals and are marked @internal in PHP, for example EventConsumer, ActiveTaskRegistry, the SSE parser, HttpSenderFactory, ResponseHelpers, Jcs and every Compat\V0_3 class. They are not covered by the backward-compatibility promise and are left out of the API reference.

Rule for changes: when Python adds, renames or removes a public class, update this table first, then the code.

Package layout

Python (src/a2a/…) PHP (namespace A2A\…) Package Notes
types/a2a_pb2.py Types\* (generated) core Generated from a2a.proto with protoc --php_out. php_namespace is set to A2A\Types through buf managed mode, so users don't see Lf\A2a\V1.
types/a2a_pb2_grpc.py Types\Grpc\* (generated) grpc (later)
helpers/proto_helpers.py Helpers\ProtoHelpers core newTaskFromUserMessage, text/data part builders, newAgentTextMessage, …
helpers/agent_card.py Helpers\AgentCardHelpers core
utils/constants.py Utils\Constants core Well-known path, header names, default versions.
utils/errors.py Utils\Errors\*Error core One exception class per A2A error, each carrying its JSON-RPC code + gRPC status.
utils/error_handlers.py, grpc_status.py Utils\ErrorHandlers core Exception → JSON-RPC / REST / gRPC error body.
utils/proto_utils.py Utils\ProtoUtils core ProtoJSON encode/decode, Struct ↔ PHP array; validateProtoRequiredFields(), warnOnMissingRequiredFields() (logs through a PSR-3 logger; Python also calls it from three logger-less helpers, which PHP does not).
utils/json_utils.py Utils\JsonUtils core A separate class, like Python. An empty PHP array means []; use new \stdClass() for {}.
utils/task.py Utils\TaskUtils core completedTask(), apply historyLength, page tokens: encodeListTasksCursor() / decodeListTasksCursor() (keyset cursor, a2a-sdk 1.2.1) and the legacy encodePageToken() / decodePageToken(). The cursor holds microseconds (us), not Python's nanoseconds (ts); tokens are opaque and only read by the store that issued them.
utils/_jcs.py, signing.py Utils\Jcs, Utils\Signing (createAgentCardSigner(), createSignatureVerifier(), canonicalizeAgentCard(), cleanEmpty()), Utils\Signing\{SignatureVerificationError, NoSignatureError, InvalidSignaturesError} core (+ suggest: firebase/php-jwt, Python's signing extra) RFC 8785 canonicalization + card JWS. HMAC keys must be as long as the hash (RFC 7518).
utils/push_url_validator.py Utils\PushUrlValidator core SSRF guard: resolve DNS, reject private/loopback/link-local addresses. resolve() returns the approved addresses so the sender can pin them; allowedHosts exempts named hosts (development).
utils/input_mode_validator.py (a2a-sdk 1.2.0, opt-in) not ported (yet) Rejects a message whose parts' media types the card's input modes don't accept (ContentTypeNotSupportedError). Off by default in Python.
utils/version_validator.py Utils\VersionValidator core Reads A2A-Version.
utils/telemetry.py Utils\Telemetry core (suggest: open-telemetry/api) No-op unless OTel is installed, same as Python.
extensions/common.py Extensions\Common core Parse A2A-Extensions, find extensions by URI. PHP adds activatableExtensions() and missingRequiredExtensions(), which the DefaultRequestHandler uses to enforce required extensions and activate + echo requested ones (Python leaves that to the app).
auth/user.py Auth\User, Auth\UnauthenticatedUser core

Server

Python PHP Notes
server/agent_execution/agent_executor.py AgentExecutor Server\AgentExecution\AgentExecutor (interface) execute(RequestContext, EventQueue): void, cancel(RequestContext, EventQueue): void. The one interface users write.
…/context.py RequestContext Server\AgentExecution\RequestContext getUserInput(), taskId(), contextId(), message(), currentTask(), relatedTasks(), configuration(), callContext(), metadata(), tenant(), requestedExtensions(). PHP adds isCancelled() (cooperative cancellation, see CancellationToken) and activatedExtensions(), isExtensionActive(), activateExtension().
…/request_context_builder.py, simple_request_context_builder.py same names
…/active_task.py ActiveTask, EventConsumer Server\AgentExecution\ActiveTask, Server\AgentExecution\EventConsumer Python runs the executor as an asyncio task. PHP runs it in a Fiber: each enqueueEvent() suspends it, the EventConsumer checks, saves and publishes the event, then it resumes. TaskCancelledException stands in for asyncio.CancelledError. This is the biggest place PHP has to differ. See Running agents in PHP.
…/active_task_registry.py Server\AgentExecution\ActiveTaskRegistry Builds a fresh ActiveTask per request: a PHP process usually serves one request, so what must be shared lives in the QueueManager.
— Server\AgentExecution\TaskRunner, InlineTaskRunner PHP-only. Decides where execute() runs (inline in the request here; on a queue worker in the Laravel bridge), holds the per-task run lease, and runs work deferred until after the response.
server/events/event_queue.py EventQueue Server\Events\EventQueue (interface) + InMemoryEventQueue enqueueEvent(Event).
…/queue_manager.py QueueManager, InMemoryQueueManager same, plus PdoQueueManager Different shape: a per-task append-only event log readable from a sequence number, plus the cancel flag and run leases, so separate PHP processes can stream and cancel the same task. PdoQueueManager uses SQLite/PostgreSQL/MySQL; the Laravel bridge adds Redis.
…/event_consumer.py, event_queue_v2.py not ported Python's legacy consumer and its asyncio queue plumbing. The v2 consumer lives in active_task.py (see above). PublishedEvent is the PHP form of the (event, updated_task) pair Python queues for subscribers.
server/tasks/task_store.py TaskStore Server\Tasks\TaskStore (interface) save / get / list / delete, each taking a ServerCallContext.
…/inmemory_task_store.py, copying_task_store.py InMemoryTaskStore, CopyingTaskStore In-memory only lives as long as the PHP process: use it in tests and long-running servers, not under PHP-FPM.
…/database_task_store.py (SQLAlchemy) Server\Tasks\PdoTaskStore core; plain PDO, supports pgsql/mysql/sqlite, same owner scoping, ordering and page tokens. The Laravel bridge runs it on the app's connection and ships the migration.
…/task_manager.py, result_aggregator.py same
…/task_updater.py TaskUpdater Server\Tasks\TaskUpdater updateStatus, addArtifact, complete, failed, reject, submit, startWork, cancel, requiresInput, requiresAuth, newAgentMessage.
…/push_notification_config_store.py + inmemory/database Server\Tasks\PushNotificationConfigStore + InMemoryPushNotificationConfigStore, PdoPushNotificationConfigStore, PushNotificationConfigs::normalize() (Python normalize_push_notification_config()) setInfo() still returns void: Python made it return the stored config in 1.2.2, but changing a PHP interface's return type would break custom stores within 1.x, so the handler returns normalize()'s copy instead (the same thing Python does for stores that return nothing).
…/push_notification_sender.py, base_push_notification_sender.py Server\Tasks\PushNotificationSender, BasePushNotificationSender Sends through the client's HttpSender layer (Guzzle, Symfony or any PSR-18 client). Adds the Authorization header from the config's authentication, retries with backoff, a stable X-A2A-Notification-Id, address pinning and no redirects; SSRF screening is on by default.
server/request_handlers/request_handler.py RequestHandler Server\RequestHandlers\RequestHandler (interface) onGetTask, onListTasks, onCancelTask, onMessageSend, onMessageSendStream (a Generator), onSubscribeToTask (a Generator; may yield null keep-alive ticks), on{Create,Get,List,Delete}TaskPushNotificationConfig, onGetExtendedAgentCard, and PHP's runBackgroundWork().
…/default_request_handler_v2.py DefaultRequestHandler(V2) Server\RequestHandlers\DefaultRequestHandler We port only V2. Python keeps the older LegacyRequestHandler around for its own compatibility; we have no old users to keep working, so we skip it. Differences are listed on Conformance.
…/response_helpers.py Utils\ErrorHandlers, Server\Routes\Common Error envelopes come from ErrorHandlers; Common holds the JSON helpers.
…/grpc_handler.py Server\RequestHandlers\GrpcHandler grpc package, later.
server/context.py ServerCallContext Server\ServerCallContext user, state, requestedExtensions, tenant, plus activatedExtensions (echoed in the A2A-Extensions response header).
server/owner_resolver.py, id_generator.py same Owner = which user a task belongs to. This is how "not found" and "not allowed" stay the same.
server/cluster/* (VersionedTaskStore, TaskEventStream, VersionedDatabaseTaskStore, DatabaseTaskEventStream; a2a-sdk 1.2.0) not ported (yet) Python's opt-in multi-replica mode. PHP already shares task state across processes (PHP-FPM is many processes): the event log, any-process subscribe and cancel, and serialized follow-ups come from QueueManager (PdoQueueManager, Laravel's RedisQueueManager) and its run lease. Missing: versioned (optimistic) task writes, so a cancel that gives up waiting and writes the task itself can be overwritten by a still-running executor.
server/routes/jsonrpc_dispatcher.py, rest_dispatcher.py Server\Routes\JsonRpcDispatcher, RestDispatcher PSR-15 RequestHandlerInterfaces. Any framework can mount them.
server/routes/{jsonrpc,rest,agent_card}_routes.py create_*_routes() Server\Routes\Routes::jsonRpc(), ::rest(), ::agentCard(), ::router() PSR-15 handlers. AgentCardHandler adds the caching headers the spec recommends and takes a signer that signs a copy of the card once per process (Python has no signing hook on the route: you wrap the signer in your own async card_modifier). Router combines all three for plain-PHP front controllers. Both dispatchers echo activated extensions in A2A-Extensions.
— Server\Routes\ResponseEmitter, ServerRequestFactory, Sse\SseStream PHP-only. Emit responses from plain PHP (flushing SSE per event, noticing disconnects, running background work after the response); build the PSR-7 request from globals; a PSR-7 body that streams SSE from a generator.
server/routes/fastapi_routes.py add_a2a_routes_to_fastapi Laravel bridge: Route::a2a(...) macro (A2A\Laravel\Routing\A2ARoutes) The framework glue lives in the bridge package, not in core.
server/routes/common.py ServerCallContextBuilder, DefaultServerCallContextBuilder same Builds the context from the PSR-7 request: headers, A2A-Extensions, and the user your auth middleware stored in the a2a.user request attribute.

Client

Namespace A2A\Client.

Python (src/a2a/client/…) PHP Notes
client.py Client, ClientConfig, ClientCallContext Client (abstract), ClientConfig, ClientCallContext sendMessage() / subscribe() return a Generator of StreamResponse, PHP's answer to Python's async iterator. ClientConfig::$httpClient replaces httpx_client and takes Guzzle, Symfony HttpClient, any PSR-18 client or an HttpSender. No grpc_channel_factory yet.
base_client.py BaseClient BaseClient Same config handling and interceptor semantics; agentCard() exposes the current card.
client_factory.py ClientFactory, create_client(), minimal_agent_card() ClientFactory, ClientFactory::createClient(), ClientFactory::minimalAgentCard() PHP can't have a static and an instance method both named create(), so Python's module-level create_client() is the static createClient(). Instance create() / createFromUrl() / register() as in Python. A v0.3 interface gets the Compat\V0_3 transports, as in Python.
card_resolver.py A2ACardResolver, parse_agent_card() A2ACardResolver, A2ACardResolver::parseAgentCard() Includes the pre-1.0 card field mapping. http_kwargs → ['headers' => [...], 'timeout' => ...].
interceptors.py ClientCallInterceptor, BeforeArgs, AfterArgs same names Interface instead of ABC; methods are synchronous.
auth/credentials.py, auth/interceptor.py Auth\CredentialService (interface), Auth\InMemoryContextCredentialStore, Auth\AuthInterceptor
errors.py Errors\A2AClientError, Errors\A2AClientTimeoutError, Errors\AgentCardResolutionError All extend A2A\Utils\Errors\A2AError, as in Python.
service_parameters.py ServiceParametersFactory, with_a2a_extensions() ServiceParametersFactory, ServiceParameters::withA2aExtensions() Updates return the new array (PHP arrays are values) instead of mutating a dict.
transports/base.py ClientTransport Transports\ClientTransport (interface)
transports/jsonrpc.py, transports/rest.py Transports\JsonRpcTransport, Transports\RestTransport Take an HttpSender where Python takes an httpx.AsyncClient. Every request carries A2A-Version: 1.0 (Python sets it on the factory's shared client).
transports/tenant_decorator.py TenantTransportDecorator Transports\TenantTransportDecorator
transports/http_helpers.py Transports\HttpHelpers (internal) + Sse\EventStreamParser The SSE parser is incremental (bytes arrive in arbitrary chunks); same rules as Python's parse_sse_stream().
transports/grpc.py — After 1.0.
(httpx) Http\HttpSender + Psr18HttpSender, GuzzleHttpSender, SymfonyHttpSender, HttpSenderFactory PHP-only: PSR-18 can't stream, so sending goes through this small interface. Guzzle and Symfony stream live; other PSR-18 clients get the buffered body. Guzzle stream requests go out as HTTP/1.0 (PHP's http:// wrapper holds chunked HTTP/1.1 bodies back until they end).

Compat, tooling, tests

Python PHP Notes
compat/v0_3/conversions.py Compat\V0_3\Conversions Function for function (to_core_x → toCoreX). v0.3 objects are the decoded JSON-RPC JSON (\stdClass) instead of pydantic models; request converters take the JSON-RPC params.
compat/v0_3/types.py — No model classes: the v0.3 JSON-RPC shape stays decoded JSON.
compat/v0_3/proto_utils.py ToProto, FromProto Compat\V0_3\ToProto, FromProto v0.3 proto ↔ v0.3 JSON, for the REST binding. Keeps the status timestamp (Python drops it).
compat/v0_3/a2a_v0_3_pb2.py Compat\V0_3\Types\* (generated) From the same v0.3 proto commit Python pins, vendored as proto/v0_3/a2a_v0_3.proto.
compat/v0_3/versions.py, extension_headers.py, context_builders.py Versions, ExtensionHeaders, V03ServerCallContextBuilder
compat/v0_3/request_handler.py RequestHandler03 RequestHandler03
compat/v0_3/jsonrpc_adapter.py JSONRPC03Adapter JsonRpc03Adapter Plugged into JsonRpcDispatcher by enableV03Compat. A2A errors keep their code (Python: -32603).
compat/v0_3/rest_adapter.py, rest_handler.py REST03Adapter, REST03Handler Rest03Adapter, Rest03Handler Plugged into RestDispatcher by enableV03Compat. Also serves DELETE on a push config; the card comes back camelCase.
compat/v0_3/jsonrpc_transport.py, rest_transport.py CompatJsonRpcTransport, CompatRestTransport same names Always state blocking. REST list/delete push configs use the v0.3 proto routes (Python: NotImplementedError).
compat/v0_3/grpc_* — No gRPC.
request_handlers/response_helpers.py agent_card_to_dict() Server\RequestHandlers\ResponseHelpers::agentCardToDict() Merges the v0.3 card fields when the card offers a v0.3 interface.
a2a_db_cli.py, migrations/ (alembic) bin/a2a-db (schema SQL) + Laravel migrations in the bridge
samples/hello_world_agent.py, cli.py examples/hello-world/server.php, examples/cli.php Line-for-line port. It is the first thing a Python user looks for.
tck/sut_agent.py tck/sut-agent.php The system-under-test agent the A2A TCK runs against in CI.
itk/ itk/ Cross-SDK tests: Python client ↔ PHP server and PHP client ↔ Python server.
tests/ (pytest, 118 files) tests/ (PHPUnit), same folder layout Port test by test. The Python tests are our spec for behaviour the written spec leaves open.

Python features that don't carry over directly

Python Why it's different in PHP PHP answer
asyncio tasks, async for PHP-FPM handles one request at a time; there is no event loop Generators for streams; a TaskRunner abstraction for background execution (see architecture.md)
asyncio.CancelledError on cancel A running PHP call can't be interrupted from outside Cooperative: a CancellationToken in RequestContext that executors check ($context->isCancelled()), and the store's cancel flag across processes
Empty Struct on the wire The pure-PHP protobuf runtime writes an empty Struct (e.g. metadata) as [], not {} Known runtime quirk. Leave metadata unset rather than empty
Pydantic models — readonly classes for SDK-side config objects; protobuf classes for wire types
httpx PSR-18 can only return complete responses, so it can't stream SSE Client\Http\HttpSender: Guzzle and Symfony HttpClient stream live; any other PSR-18 client works with buffered streams