Changelog¶
All notable changes to this project are documented here. The format follows Keep a Changelog and the project uses Semantic Versioning.
Unreleased¶
1.0.3 - 2026-10-14¶
Fixed¶
- The PDO stores work on a SQLite connection that is already in a transaction. Building
PdoTaskStore,PdoQueueManagerorPdoPushNotificationConfigStoresetjournal_modeandsynchronous, which SQLite refuses inside a transaction ("Safety level may not be changed inside a transaction"). So every A2A request failed with a 500 in a Laravel app tested withRefreshDatabaseon SQLite (the default for new Laravel apps), and in any code that builds a store insideDB::transaction(). Those two settings are now skipped while a transaction is open;busy_timeoutis still set. Found while building the Laravel starter app.
1.0.2 - 2026-10-12¶
Catch-up with the Python SDK from 0d5473c to 494a8ec (a2a-sdk 1.2.0 to 1.2.2). Interop tests now pin a2a-sdk 1.2.2.
Fixed¶
- ListTasks page tokens survive the listing changing. A token named the first task of the next page and was looked up again, so deleting that task made the next request fail with
InvalidParamsError, and updating it restarted the listing with duplicates. Tokens now carry the sort position of the last task returned (TaskUtils::encodeListTasksCursor()), and the next page starts strictly after it. Tokens issued by 1.0.x are still accepted. BothInMemoryTaskStoreandPdoTaskStore; Python b7cba7c. CreateTaskPushNotificationConfigreturns the id the config was stored under. A config created without an id was stored under the task id but returned with an empty id, so the client could not get or delete it. The newPushNotificationConfigs::normalize()is what every store saves and what the handler returns; the store interface is unchanged. Python dac7d9d.- JCS depth bound counts open containers, the outermost at 1, so an empty object or array at depth 129 is refused like any other container past the bound (it was accepted). Also in
Signing::cleanEmpty(). Python 5653daf. - The client prefers an A2A 1.0 interface regardless of its patch version (
1.0.0and1.0.3count as1.0). Before,1.0.0was treated as "above 1.0" and could lose to another interface. Python cbb2d84. - A wrongly shaped response no longer slips through as a PHP warning. The pure-PHP protobuf runtime warns and carries on for some shape errors (for example
securitySchemesas a string); the client now raisesA2AClientErrorfor those, as for any unreadable response. Python 486f0f6. - Agent Card
If-None-Matchuses the weak comparison and accepts*(RFC 9110). Proxies that weaken ETags, such as nginx when it gzips, got a full response every time instead of a 304. Python 83d7f5d.
Changed¶
- A JSON-RPC request whose params cannot be parsed gets
InvalidParamsErrorwithdata.parseErrorholding the parser's message. Python 83f1cf8.
Added¶
- Dependabot: weekly grouped update PRs for GitHub Actions, Composer (
widen, so supported ranges are kept) and the interop test packages. Dependabot security alerts and security updates are enabled. ProtoUtils::warnOnMissingRequiredFields():DefaultRequestHandlerand the Agent Card route (given a logger) log a warning when a card misses REQUIRED fields, without failing. Python 4f4101c.- Tests: the cross-SDK Agent Card signature vectors (cards signed by a2a-python, @a2a-js/sdk, a2a-go and a reference signer verify in PHP, with identical canonical bytes) and the
jcs_depth_v1depth vectors, both copied from a2a-python.
Not ported yet¶
- Python's cluster mode (
a2a.server.cluster, versioned task store). PHP already shares task state across processes throughQueueManager; what is missing is versioned task writes. See the Python → PHP mapping. - Python's opt-in input-mode validation.
- Python's
application/a2a+jsonresponses (dc5a5da). Spec v1.0.0 and the A2A TCK still requireapplication/json, so HTTP+JSON keeps it.
1.0.1 - 2026-10-12¶
Documentation, plus one static-analysis fix with no behaviour change.
Fixed¶
a2a-laravel:RunAgentExecutorno longer passes bothJSON_THROW_ON_ERRORandJSON_PARTIAL_OUTPUT_ON_ERRORtojson_encodewhen recording a failed run. PHP already let partial output win, so the result is identical; PHPStan 2.3 now rejects the combination.
Added¶
- Docs: "Check the agent first with the A2A CLI" in Call an agent, showing how to test an agent with the official A2A CLI before debugging PHP code, including its
--echoand--proxyserver modes. Suggested by an A2A maintainer when the SDK was listed on the Community SDKs page. - README: a short tip pointing to it.
1.0.0 - 2026-09-26¶
The stable release. No API changes since 0.3.0 beyond marking implementation details @internal; from here the public API follows Semantic Versioning. Both packages are released as 1.0.0; a2a-laravel requires a2a-php ^1.0. Upgrading from 0.x: see the upgrade guide.
Added¶
- Backward-compatibility policy (
docs/reference/backward-compatibility.md): what 1.x covers (public classes, interfaces you implement, exceptions, Laravel config and routes, PDO schema, wire behaviour, supported PHP/Laravel versions) and what it doesn't (@internal, generated-code layout, the spec itself). - API reference generated with phpDocumentor (
phpdoc.dist.xml) and published under/api/on the docs site.@internalclasses are left out. - Upgrade guide (
docs/guides/upgrading.md): 0.1 → 0.2 → 0.3 → 1.0, and the policy from here. - Benchmarks:
scripts/benchmark.php(ProtoJSON, SSE parsing, full JSON-RPC requests) with results ondocs/reference/performance.md, and a manualbenchmark.ymlworkflow that compares the pure-PHP andext-protobufruntimes. - Upstream watch:
scripts/upstream-watch.pyand a weeklyupstream-watch.ymlworkflow that keep one GitHub issue listing new upstream commits (Python SDK,a2a.proto, TCK requirements) since the pins inUPSTREAM.md. - Docs sample test (
tests/Docs/DocsCodeSamplesTest.php): every inline PHP sample indocs/must parse, and every A2A class and static method it names must exist.
Changed¶
- Marked
@internal(not covered by the 1.x promise, left out of the API reference):Client\Http\HttpSenderFactory,Client\Sse\EventStreamParser,Client\Sse\SseEvent,Server\AgentExecution\ActiveTaskRegistry,Server\AgentExecution\EventConsumer,Server\RequestHandlers\ResponseHelpers,Server\Routes\Common,Utils\Jcs, every class inCompat\V0_3(conversions, adapters, 0.3 handlers and transports), and in the Laravel bridgeRouting\A2ARoutes,Auth\LaravelUser,Auth\SerializedUserand the four console command classes. These were already implementation details (plus the 11 classes that were already@internal); the public route to each is in the upgrade guide. - The docs site is built from the newest
v*release tag instead ofmain, so it never documents unreleased features (the workflow still runs frommain; a tag push re-runs it). praveendias1180/a2a-laravelrequirespraveendias1180/a2a-php^1.0.- README and docs describe the 1.x status; badges for CI, the TCK result, Packagist version, PHP version and license.
docs/guides/a2a-0-3.md: the 0.3 server example is now a complete, valid snippet.
0.3.0 - 2026-09-26¶
Phases 5 and 6: push notifications, Agent Card signing and extensions, and A2A 0.3 compatibility (server and client). The TCK MUST level now passes 161 distinct tests (was 137), in plain PHP and on a queued Laravel app. Both packages are released as 0.3.0; a2a-laravel requires a2a-php ^0.3.
Added¶
- A2A 0.3 compatibility (
A2A\Compat\V0_3, a port of Python'sa2a.compat.v0_3). - Client:
ClientFactoryusesCompatJsonRpcTransport/CompatRestTransportfor a v0.3 interface, so a 0.3 agent is called with the usual v1.0 types. Cards that only offer 0.3 no longer raise. - Server:
JsonRpcDispatcher,RestDispatcher,Routes::jsonRpc(),Routes::rest()andRoutes::router()takeenableV03Compat(off by default, as in Python). With it on, the v0.3 JSON-RPC methods and the v0.3 REST routes (/v1/...) are served on the same endpoints throughJsonRpc03Adapter/Rest03Adapter. - Agent Card: a card that offers a v0.3 interface is served with the v0.3 fields merged in (
ResponseHelpers::agentCardToDict()). - Building blocks:
Conversions(every v0.3 ↔ v1.0 conversion),ToProto/FromProto, the v0.3 proto types (A2A\Compat\V0_3\Types, generated from the v0.3 proto Python pins, vendored asproto/v0_3/a2a_v0_3.proto),RequestHandler03,Versions,ExtensionHeaders,V03ServerCallContextBuilder(acceptsX-A2A-Extensions). - Laravel:
a2a.v0_3_compat/A2A_V0_3_COMPAT. When the card's interfaces are filled in from the routes, the v0.3 ones are added too. - Interop: tested against a2a-sdk 0.3.26 in both directions (
scripts/run-python-interop-v03.sh,scripts/run-tck-laravel.sh v03-interop, both in CI). Docs: "Talking to A2A 0.3 agents and clients". - Push notifications.
PushNotificationSender+BasePushNotificationSender: every task update is POSTed to the task's webhooks as aStreamResponse, withAuthorization(from the config'sauthentication),X-A2A-Notification-Tokenand a stableX-A2A-Notification-Id; retries network errors, 408, 425, 429 and 5xx with exponential backoff (honoursRetry-After); never follows redirects. Wired intoDefaultRequestHandler(pushSender:), sent after each update is saved, in order; a failing sender never fails the task. - SSRF protection for webhooks: URLs are checked when a config is created and again before every attempt; with Guzzle (ext-curl) or Symfony HttpClient the connection is pinned to the checked address (DNS-rebinding safe).
PushUrlValidator::resolve()andallowedHosts(for a local receiver in development).HttpRequestgainspinnedAddressandfollowRedirects;PinsAddressesmarks senders that can pin. PdoPushNotificationConfigStore(SQLite, PostgreSQL, MySQL) with optionalencrypt/decryptclosures;PushNotificationConfigStore::getInfoForDispatch()(every owner's configs for a task).- Agent Card signing (
Utils\Signing, needsfirebase/php-jwt):createAgentCardSigner(),createSignatureVerifier(),canonicalizeAgentCard(),cleanEmpty()and theSignatureVerificationErrorfamily, ported from Python'sutils/signing.py.Routes::agentCard(..., signer:)/Routes::router(..., cardSigner:)serve the card signed (a copy, signed once per process). Cross-SDK check in CI: Python-signed cards verify in PHP and PHP-signed cards verify in Python (ES256, HS256). - Extensions. Required extensions are enforced (
ExtensionSupportRequiredErrorbefore the executor runs), requested extensions the card declares are activated, and activated extensions are echoed in theA2A-Extensionsresponse header on JSON-RPC and HTTP+JSON.ServerCallContext::$activatedExtensions,RequestContext::activatedExtensions()/isExtensionActive()/activateExtension(),Extensions\Common::activatableExtensions()/missingRequiredExtensions(). Example extension:examples/extensions/TimestampExtension.php. - Laravel: push notifications sent from a queue job (
SendPushNotification; credentials never enter the queue payload),a2a.pushconfig (queue, attempts, backoff, timeout, allowed hosts), webhook URLs checked on create; Agent Card signing viaa2a.signing(public and extended card). - TCK:
tck/sut-agent.phpand the Laravel TCK app takeA2A_SUT_PROFILE(minimal,full,required-extension);scripts/run-tck.shandscripts/run-tck-laravel.shrun every profile, so the push-notification, extended-card and required-extension requirements now run instead of being skipped. - Docs: guides for push notifications, card signing and extensions.
Security¶
- The card-signature verifier never uses PEM or OpenSSH public-key material as an HMAC secret, so a card forged with
HS256and the public key is rejected even when the allow-list mixes HMAC and public-key algorithms (the same guard PyJWT has).
Changed¶
PushNotificationConfigStoregainedgetInfoForDispatch(): custom stores must implement it.DefaultRequestHandler'spushUrlValidatoralso accepts aPushUrlValidatorinstance.- The Laravel bridge now checks webhook URLs when a push config is created (private and unresolvable hosts are rejected).
0.2.0 - 2026-09-25¶
Adds the Laravel bridge, released as praveendias1180/a2a-laravel 0.2.0 (both packages are versioned together from here on). The official A2A TCK passes at the MUST level against a queued Laravel app on PHP-FPM + nginx.
Added¶
- Laravel bridge (
praveendias1180/a2a-laravel,packages/laravel), for Laravel 12 and 13 (11 allowed, untested): Route::a2a('/a2a', agentCard: ..., executor: ...)mounts the Agent Card, JSON-RPC and HTTP+JSON, fills the card's interfaces in from the routes, and isroute:cachesafe. Agents can also come fromconfig/a2a.php.QueuedTaskRunner+RunAgentExecutorjob: executors run on queue workers while the web request streams their events live; one run per task at a time (run lease); executor errors fail the call the same way as with the inline runner.RedisQueueManager(Redis Streams,XREAD BLOCK, atomic sequence numbers, expiring keys), or the corePdoQueueManageron the app's connection.- Tasks in the core
PdoTaskStoreon the app's connection; push-notification configs in the database, encrypted with the app key. Owner scoping from the authenticated Laravel user. - Security schemes in the card map to route middleware (
a2a.security_schemes); CSRF is off for the protocol routes. A2A::client()facade; artisana2a:make-executor,a2a:card,a2a:prune,a2a:tck; publishable config and migration.scripts/run-tck-laravel.sh: the official A2A TCK against a real Laravel app on PHP-FPM + nginx with the queued runner and queue workers (also a CI job), and a 20 s long-task proof.ResponseEmitter::streamSse(): the SSE pump, public so framework bridges stream exactly like the plain-PHP emitter.examples/laravel/: the Laravel examples shown in the docs, run by the test suite.- Docs: Laravel guides (install and first agent, queued execution, streaming behind nginx, storage and pruning).
Changed¶
praveendias1180/a2a-laravelrequirespraveendias1180/a2a-php^0.2.tck/TckAgentExecutor.php: the TCK executor moved out oftck/sut-agent.php, so the Laravel TCK app reuses it.
0.1.0 - 2026-09-25¶
First release: A2A 1.0 client and server for PHP 8.2+. The server passes the official A2A TCK at the MUST level (137 passed) over JSON-RPC and HTTP+JSON. The Laravel bridge is not included yet (phase 4).
Added¶
- Wire types generated from the A2A v1.0.0
a2a.proto(A2A\Types\*). A2A\Utils\ConstantsandA2A\Utils\TransportProtocol.- ProtoJSON round-trip tests against the specification's JSON shapes.
- Laravel bridge package skeleton (
packages/laravel). - Errors (
A2A\Utils\Errors\*): one exception per A2A and JSON-RPC error, with the same JSON-RPC codes, HTTP statuses, gRPC statuses and ErrorInfo reasons as the Python SDK (ErrorMapping). A2A\Utils\ErrorHandlers: REST error payloads and JSON-RPC error objects, includinggoogle.rpc.ErrorInfoandgoogle.rpc.BadRequestdetails. Unlike the Python SDK, a non-A2A exception becomes JSON-RPC-32603 "Internal error"without its message, so internal details never reach the caller (REST already hid them).A2A\Utils\ProtoUtils: stream-response wrapping,Struct/Value↔ PHP conversion, REST query-parameter parsing, and REQUIRED-field validation driven by a table generated froma2a.proto(A2A\Types\Meta\RequiredFields,scripts/generate-required-fields.php).A2A\Utils\TaskUtils(history length, page size, page tokens),JsonUtils,VersionValidator(A2A-Versionheader) andPushUrlValidator(SSRF guard for push-notification URLs, with an injectable resolver).A2A\Utils\Jcs: RFC 8785 JSON canonicalization, checked against the a2a-jcs-v01 vector corpus and against ECMAScript number formatting.A2A\Helpers\ProtoHelpersandAgentCardHelpers,A2A\Extensions\Common,A2A\Auth\UserandUnauthenticatedUser.- The client (
A2A\Client\*), in the shape of the Python SDK'sa2a.client:ClientFactory(withcreateClient()andminimalAgentCard()),BaseClient,ClientConfig,ClientCallContext,A2ACardResolver(including the pre-1.0 card field mapping), interceptors (ClientCallInterceptor,BeforeArgs,AfterArgs),AuthInterceptorwithInMemoryContextCredentialStore, service parameters, and the client errors (A2AClientError,A2AClientTimeoutError,AgentCardResolutionError). - JSON-RPC and HTTP+JSON transports (
JsonRpcTransport,RestTransport,TenantTransportDecorator) with every A2A operation. Errors from the agent come back as the matchingA2A\Utils\Errors\*exception. Streams are PHP Generators. A2A\Client\Http\HttpSenderwith senders for Guzzle and Symfony HttpClient (live SSE streaming) and any PSR-18 client (buffered), picked byHttpSenderFactory; an incremental SSE parser (A2A\Client\Sse\EventStreamParser).- Interop tests against the official Python SDK's sample agent (a2a-sdk 1.1.5): every
client operation over JSON-RPC and HTTP+JSON, with Guzzle, Symfony HttpClient and a
PSR-18 client (
scripts/run-python-interop.sh, CI jobpython-interop). - The server (
A2A\Server\*), in the shape of the Python SDK'sa2a.server:AgentExecutor,RequestContext(withisCancelled()),RequestContextBuilder,SimpleRequestContextBuilder,ActiveTask,EventConsumer,ActiveTaskRegistry,EventQueue,InMemoryEventQueue,TaskStore,InMemoryTaskStore,CopyingTaskStore,TaskManager,TaskUpdater,ResultAggregator,PushNotificationConfigStore(in-memory),RequestHandler,DefaultRequestHandler(a port of Python'sDefaultRequestHandlerV2),ServerCallContext,OwnerResolverandIdGenerator/UuidGenerator. - The executor runs in a PHP Fiber: every enqueued event is checked, saved, published
and streamed before the executor continues.
TaskRunner/InlineTaskRunnerdecide where it runs, hold a per-task run lease and finish deferred work after the response. QueueManageras a per-task event log with cancel flags and run leases, so separate PHP processes can subscribe to, stream and cancel the same task:InMemoryQueueManagerandPdoQueueManager(SQLite, PostgreSQL, MySQL).PdoTaskStore(SQLite, PostgreSQL, MySQL; owner scoping, keyset pagination), brought forward from phase 5.- PSR-15 handlers:
JsonRpcDispatcher,RestDispatcher(with tenant prefixes),AgentCardHandler(Cache-Control,ETag,Last-Modified, 304),Router,Routes;ResponseEmitter(per-event SSE flushing, disconnect detection, background work after the response),ServerRequestFactory,Sse\SseStream,DefaultServerCallContextBuilder. examples/hello-world(a port of the Python SDK's sample agent) andtck/sut-agent.php.- Conformance: the official A2A TCK passes at the MUST (137/137), SHOULD and MAY levels
over JSON-RPC and HTTP+JSON (
scripts/run-tck.sh, CI jobtck), and the official Python SDK client works against the PHP server over both transports.
Changed¶
TaskNotCancelableErrormaps to HTTP 409 (the A2A TCK's CORE-CANCEL-002; the released v1.0.0 spec table and the Python SDK say 400). The ErrorInfo reason is unchanged.examples/call-an-agent.php, shown on the docs site and run in CI.
Changed¶
- Requires
php-http/discovery(finds a PSR-18 client when none is given) andpsr/http-factory^1.1.