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.

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.
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, …
utils/_jcs.py, signing.py Utils\Jcs, Utils\Signing core (+ suggest: web-token/jwt-library) RFC 8785 canonicalization + card JWS.
utils/push_url_validator.py Utils\PushUrlValidator core SSRF guard: resolve DNS, reject private/loopback/link-local addresses.
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\* core Parse A2A-Extensions, find extensions required by the card.
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).
…/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 The PDO version and the sender arrive in phase 5.
…/push_notification_sender.py, base_push_notification_sender.py Server\Tasks\PushNotificationSender, BasePushNotificationSender Sends through a PSR-18 client.
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 phase 1's 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.
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/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. Router combines all three for plain-PHP front controllers.
— 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

Built in phase 2. 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 card that only offers A2A 0.3 raises A2AClientError until the 0.3 layer lands.
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/* Compat\V0_3\* Phase 5. Translates 0.3 JSON to and from 1.0 types at the dispatcher edge.
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