# A2A SDK splits: 14 behaviors across the official SDKs Specification pinned to A2A v1.0.1 (2026-05-28); prose quotes are from `research/sources/specification.md` at that tag and proto quotes from `a2a.proto` at the same tag. The reference SDK is a2a-sdk (Python) 1.2.2. Compiled 2026-10-06 from the five per-SDK notes under `research/v2/notes/`, the wire captures in `research/sdk-wire/FINDINGS.md`, the Python clone at `research/a2a-python` (v1.2.2) and the capture kit at `/tmp/aiefs-manuals/manuals/a2a-101/capture/`. Every link is a permalink at the release tag; kit references are local file paths because the kit is not published. Reading the tables: "Server" is what the SDK's request handler does on its 1.0 path; "Client" is what the SDK's own client sends. JSON-RPC errors always travel inside an HTTP 200 envelope in every implementation; only the REST status and the gRPC status vary. ".NET tag" means v1.0.0-preview2; where main (unreleased) differs the cell says so. ## 1. Implementations compared | Implementation | Tag | Commit | Date | Protocol version | Bindings it implements (server / client) | |---|---|---|---|---|---| | Capture kit (`capture/a2a_ref.py`) | none (manual tree) | n/a | 2026-10-06 | 1.0, accepts exactly `1.0` | JSON-RPC and HTTP+JSON over `http.server`; push-config operations over JSON-RPC only; no gRPC | | Python `a2a-sdk` ([a2aproject/a2a-python](https://github.com/a2aproject/a2a-python)) | `v1.2.2` | `2f9e44af243df5c7c7a3da361c8d095324bcd5e3` | 2026-10-05 | 1.0 ([`PROTOCOL_VERSION_CURRENT = PROTOCOL_VERSION_1_0`](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/constants.py#L27-L31)) | JSON-RPC, HTTP+JSON, gRPC, server and client; opt-in 0.3 compat (`enable_v0_3_compat`) | | JavaScript `@a2a-js/sdk` ([a2aproject/a2a-js](https://github.com/a2aproject/a2a-js)) | `v1.3.0` | `29417a5bb4038f804f310ce4fffd267a9375aa90` | 2026-09-29 | 1.0 ([`A2A_PROTOCOL_VERSION = '1.0'`](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/constants.ts#L12-L13)); proto generated from A2A tag v1.0.0 | JSON-RPC, HTTP+JSON (Express), gRPC (Node), server and client; opt-in 0.3 compat | | Java ([a2aproject/a2a-java](https://github.com/a2aproject/a2a-java)) | `v1.4.0.Final` | `6408194186cb708de9672b40a9e9e026563028f2` | 2026-09-28 | 1.0 ([`CURRENT_PROTOCOL_VERSION = "1.0"`](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/AgentInterface.java#L32-L32)); bundled proto "From tag v1.0.0" | JSON-RPC, REST, gRPC transports plus Quarkus reference servers; client for all three; 0.3 in separate `compat-0.3` modules | | Go ([a2aproject/a2a-go](https://github.com/a2aproject/a2a-go)) | `v2.6.0` | `ebf17c56ef7e63c72883a45454a538bbc0df66b8` | 2026-09-25 | 1.0 ([`const Version ProtocolVersion = "1.0"`](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2a/core.go#L32-L33)) | JSON-RPC, HTTP+JSON, gRPC servers; client registers JSON-RPC and REST by default, gRPC on request; 0.3 in `a2acompat/a2av0` and `a2agrpc/v0` | | .NET ([a2aproject/a2a-dotnet](https://github.com/a2aproject/a2a-dotnet)) | `v1.0.0-preview2` | `87fd44843dd16339cdb59c2ff547fe374ac46736` | 2026-04-09 | 1.0 ([`ProtocolVersion { get; set; } = "1.0"`](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Models/AgentInterface.cs#L21-L21)) | JSON-RPC and HTTP+JSON only at the tag (gRPC server and client exist on main `6894043`, unreleased); 0.3 in `A2A.V0_3` and `A2A.V0_3Compat` | | Rust ([a2aproject/a2a-rs](https://github.com/a2aproject/a2a-rs)) | `a2a-server-lf-v0.5.1` (same commit as `a2a-lf-v0.4.1`, `a2a-client-lf-v0.2.7`, `a2a-grpc-v0.3.9`, `a2a-pb-v0.3.1`, `a2a-cli-v0.3.1`) | `32c31f69cf3b2b7697de56d1528005bb3f10768f` | 2026-09-30 | 1.0 ([`pub const VERSION: &str = "1.0"`](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a/src/lib.rs#L20-L21)) | JSON-RPC and REST (`a2a-server`, `a2a-client`), gRPC (`a2a-grpc`), SLIMRPC (`a2a-slimrpc`, not examined); no 0.3 layer | ## 2. The fourteen behaviors ### B1. Which HTTP method the REST `SubscribeToTask` route accepts **Specification.** The prose routes it as POST: §5.3 lists "Subscribe to task | `SubscribeToTask` | `SubscribeToTask` | `POST /tasks/{id}:subscribe`" and §11.3.2 says "`POST /tasks/{id}:subscribe` - Subscribe to task updates (SSE response, returns error for terminal tasks)". The normative proto routes it as GET: `a2a.proto` lines 76-82, `option (google.api.http) = { get: "/tasks/{id=*}:subscribe" additional_bindings: { get: "/{tenant}/tasks/{id=*}:subscribe" } }`. | Implementation | Server accepts | Client sends | Evidence | |---|---|---|---| | Kit | any method on `/tasks/{id}:subscribe` (the route check has no verb test) | the REST scenario does not call subscribe (the resubscribe capture is JSON-RPC) | `capture/a2a_ref.py:644-647` | | Python | GET and POST | POST | [rest_routes.py:87-88](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/routes/rest_routes.py#L87-L88); [client rest.py:307-309](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/client/transports/rest.py#L307-L309) | | JS | GET and POST, also under `/:tenant` | POST | [rest_handler.ts:395-396](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/express/rest_handler.ts#L395-L396); [rest_transport.ts:359-360](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/client/transports/rest_transport.ts#L359-L360) | | Java | POST only (GET route excludes `:` from the id) | POST | [A2AServerRoutes.java:212](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java#L212-L212); [RestTransport.java:378-379](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/client/transport/rest/src/main/java/org/a2aproject/sdk/client/transport/rest/RestTransport.java#L378-L379) | | Go | GET and POST, also under a tenant prefix (GET added in 2.4.0) | POST (the 0.3 compat client sends GET) | [a2asrv/rest.go:54-55](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/rest.go#L54-L55); [a2aclient/rest.go:318-320](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2aclient/rest.go#L318-L320) | | .NET | POST only, no tenant route | POST | [A2AEndpointRouteBuilderExtensions.cs:111-112](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A.AspNetCore/A2AEndpointRouteBuilderExtensions.cs#L111-L112); [A2AHttpJsonClient.cs:347-349](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Client/A2AHttpJsonClient.cs#L347-L349) | | Rust | GET and POST, plus legacy `/tasks/{id}/subscribe`; no tenant routes | GET, retrying the legacy path on 404, 405, -32601 or -32004 | [rest.rs:81-83](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/rest.rs#L81-L83); [client rest.rs:354-355](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-client/src/rest.rs#L354-L355), [218-219](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-client/src/rest.rs#L218-L219) | **Expect.** Route both verbs on your server and send POST from your client; the one client that sends GET (Rust) cannot subscribe to a Java or .NET server, which the ITK records as a known failure. ### B2. When the server closes a stream **Specification.** §3.1.2: "The stream MUST close when the task reaches a terminal state (`TASK_STATE_COMPLETED`, `TASK_STATE_FAILED`, `TASK_STATE_CANCELED`, `TASK_STATE_REJECTED`)." §3.1.6: "The stream MUST terminate when the task reaches a terminal state". §11.7: "then zero or more status or artifact update events until the task reaches a terminal or interrupted state, at which point the stream closes." §7.6.1: "the agent SHOULD maintain any active response streams with the client after setting the TaskState to `TASK_STATE_AUTH_REQUIRED`." The three statements do not agree on interrupted states. | Implementation | SendStreamingMessage closes after | Subscribe to a parked (INPUT/AUTH_REQUIRED) task | Subscribe to a terminal task | Evidence | |---|---|---|---|---| | Kit | a status update that is terminal or `INPUT_REQUIRED`; stays open through `AUTH_REQUIRED` | accepted, snapshot first, stream stays open | `UnsupportedOperationError` | `capture/a2a_ref.py:369-375`, `433-434` | | Python | the agent's `execute()` returning, whatever the last state (so after terminal, `INPUT_REQUIRED` and `AUTH_REQUIRED` alike) | accepted, snapshot first, stays open | -32004 / 400 | [active_task.py:610-619](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/agent_execution/active_task.py#L610-L619), [750-760](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/agent_execution/active_task.py#L750-L760); [default_request_handler_v2.py:559-564](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/request_handlers/default_request_handler_v2.py#L559-L564) | | JS | a terminal or `INPUT_REQUIRED` status update or a Message; `AUTH_REQUIRED` keeps it open; executor return ends it unless the last state is a keep-alive state | accepted, snapshot, stays open while the bus lives | -32004 / 400 / FAILED_PRECONDITION | [execution_event_queue.ts:48-55](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/events/execution_event_queue.ts#L48-L55); [default_request_handler.ts:516-519](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L516-L519), [1013-1016](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L1013-L1016) | | Java | a terminal state, a Message or an error; `INPUT_REQUIRED` and `AUTH_REQUIRED` keep it open even after `execute()` returns; an agent that returns in a non-interrupted state ends the stream after about 1.5 s of empty polls | accepted, stays open | -32004 / 400 / UNIMPLEMENTED | [EventConsumer.java:233](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/events/EventConsumer.java#L233-L233), [143-145](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/events/EventConsumer.java#L143-L145), [194-197](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/events/EventConsumer.java#L194-L197); [DefaultRequestHandler.java:1190-1194](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java#L1190-L1194) | | Go | a Message, a terminal state or `INPUT_REQUIRED`; `AUTH_REQUIRED` is not final; executor return alone does not close | one snapshot, then the stream closes (no live execution) | -32004 / 400 / FAILED_PRECONDITION | [final.go:37](https://github.com/a2aproject/a2a-go/blob/v2.6.0/internal/taskupdate/final.go#L37-L37); [agentexec.go:45-48](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/agentexec.go#L45-L48); [handler.go:379](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/handler.go#L379-L379), [382](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/handler.go#L382-L382) | | .NET | the handler returning, or a `TaskUpdater` call that is terminal, `RequireInputAsync` or `RequireAuthAsync` (each completes the queue) | accepted; subscriptions close only on terminal events | -32004 | [A2AServer.cs:274-276](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L274-L276); [TaskUpdater.cs:170-171](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/TaskUpdater.cs#L170-L171), [189-190](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/TaskUpdater.cs#L189-L190); [ChannelEventNotifier.cs:105-106](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/ChannelEventNotifier.cs#L105-L106); [A2AServer.cs:491-493](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L491-L493) | | Rust | a terminal event, or the executor's stream ending; interrupted states are not special | live execution: snapshot then events; executor already returned: `TaskNotFoundError` -32001 although the task exists | -32004 / 400 / FAILED_PRECONDITION | [handler.rs:153-155](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L153-L155), [339](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L339-L339), [1057-1059](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L1057-L1059), [1065](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L1065-L1065) | **Expect.** A client must treat `INPUT_REQUIRED` as end of stream (every implementation except Java ends there) and must not depend on `AUTH_REQUIRED` keeping the stream open: Go, JS, Java and the kit keep it open, Python, Rust and .NET close when the agent code returns, and no client in any SDK stops reading on its own. ### B3. What the push notification request looks like **Specification.** §4.3.3: "the agent sends an HTTP POST request to the configured webhook URL. The payload uses the same `StreamResponse` format as streaming operations"; the example shows `Authorization: {authentication_scheme} {credentials}` and `Content-Type: application/a2a+json`; "The agent MUST include authentication credentials in the request headers as specified in the `PushNotificationConfig.authentication` field"; "Agents MUST attempt delivery at least once for each configured webhook", "MAY implement retry logic", "SHOULD include a reasonable timeout for webhook requests (recommended: 10-30 seconds)". The `token` field (§4.3.1) is given no transport. | Implementation | Body | Content-Type | Token header | `authentication` | Events pushed | Retry / timeout | Evidence | |---|---|---|---|---|---|---|---| | Kit | StreamResponse wrapper | `application/a2a+json` | `X-A2A-Notification-Token` | `Authorization: ` | status and artifact updates, never the first Task snapshot | none / 10 s | `capture/a2a_ref.py:367-391` | | Python | StreamResponse wrapper (`MessageToDict(to_stream_response(event))`) | `application/json` | `X-A2A-Notification-Token` | `Authorization: ` | every task event (initial Task included when the config arrives inline); never Message-only replies | none / httpx default; no `A2A-Version` | [base_push_notification_sender.py:100-122](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/tasks/base_push_notification_sender.py#L100-L122); [active_task.py:339-350](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/agent_execution/active_task.py#L339-L350) | | JS | StreamResponse wrapper (task events carry the full stored Task) | `application/a2a+json` | `X-A2A-Notification-Token`, only when no `authentication` | `Authorization: ` (then no token header) | initial Task, every status and artifact update, Messages with a taskId | none / 5 s | [push_notification_serializer.ts:34-35](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/push_notification/push_notification_serializer.ts#L34-L35); [default_push_notification_sender.ts:58-59](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/push_notification/default_push_notification_sender.ts#L58-L59), [202-205](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/push_notification/default_push_notification_sender.ts#L202-L205) | | Java | StreamResponse-style wrapper (Gson, not ProtoJSON) | `application/json` | `X-A2A-Notification-Token` | `Authorization: ` | every persisted streaming event | none / JDK client default; `https` only and no private targets by default | [BasePushNotificationSender.java:212](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/tasks/BasePushNotificationSender.java#L212-L212), [228](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/tasks/BasePushNotificationSender.java#L228-L228), [241](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/tasks/BasePushNotificationSender.java#L241-L241), [248](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/tasks/BasePushNotificationSender.java#L248-L248); [a2a-defaults.properties:57](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/resources/META-INF/a2a-defaults.properties#L57-L57) | | Go | StreamResponse wrapper | `application/json` | `A2A-Notification-Token` (no `X-`) | only `bearer` and `basic` schemes become `Authorization`; others dropped | every event incl. the agent's initial Task and direct Messages; not the synthetic FAILED Task | none / 30 s; SSRF guard on by default | [sender.go:40](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/push/sender.go#L40-L40), [160](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/push/sender.go#L160-L160), [170-173](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/push/sender.go#L170-L173), [174-177](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/push/sender.go#L174-L177), [71](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/push/sender.go#L71-L71) | | .NET | not implemented: no sender exists; all four push-config operations throw `PushNotificationNotSupported` (-32003, REST 400) | n/a | n/a | n/a | none | n/a (unchanged on main) | [A2AServer.cs:518-521](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L518-L521); [docs/security.md:74](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/docs/security.md#L74-L74) | | Rust | StreamResponse wrapper (serde) | `application/json` | `A2A-Notification-Token` | only `bearer` and `basic`; credentials verbatim | every executor event; not the handler's own SUBMITTED snapshot | none / 30 s; URL validation on by default (localhost refused) | [push/sender.rs:110](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/push/sender.rs#L110-L110), [117-118](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/push/sender.rs#L117-L118), [121-122](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/push/sender.rs#L121-L122), [127-129](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/push/sender.rs#L127-L129), [42-44](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/push/sender.rs#L42-L44) | **Expect.** A webhook must parse the body as a `StreamResponse` whatever the `Content-Type` says, read the shared secret from either `X-A2A-Notification-Token` or `A2A-Notification-Token`, accept that it may or may not receive the first Task snapshot, and never count on a second attempt. ### B4. `historyLength` left unset **Specification.** §3.2.4: "Unset/undefined: No limit imposed; server returns its default amount of history (implementation-defined, may be all history)"; "0: No history should be returned; the `history` field SHOULD be omitted"; "> 0: Return at most this many recent messages". `a2a.proto` lines 667-670 (GetTaskRequest): "An unset value means the client does not impose any limit. A value of zero is a request to not include any messages. The server MUST NOT return more messages than the provided value, but MAY apply a lower limit." | Implementation | GetTask unset | ListTasks unset | SendMessage `configuration.historyLength` | 0 | negative | Evidence | |---|---|---|---|---|---|---| | Kit | all | all | honored | `[]` | `[]` | `capture/a2a_ref.py:233-237` | | Python | all | all | honored | GetTask: key omitted; ListTasks: `"history": []` | -32602 / 400 "history length must be non-negative" | [utils/task.py:29-72](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/task.py#L29-L72); [default_request_handler_v2.py:185-220](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/request_handlers/default_request_handler_v2.py#L185-L220) | | JS | all | all | honored (also on `task` stream payloads) | omitted | JSON-RPC and gRPC: as 0; REST query: 400 | [default_request_handler.ts:1219-1222](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L1219-L1222); [rest_transport_handler.ts:204-206](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/transports/rest/rest_transport_handler.ts#L204-L206) | | Java | all | none (default 0) | honored (unset = all) | empty | InvalidParams (-32602 / 422) | [DefaultRequestHandler.java:513](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java#L513-L513); [ListTasksParams.java:95-97](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/ListTasksParams.java#L95-L97); [TaskQueryParams.java:28-29](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/TaskQueryParams.java#L28-L29) | | Go | all | last 100 (`defaultMaxHistoryLength`) | ignored | empty (key omitted) | empty over JSON-RPC and REST; gRPC drops negatives (as unset) | [handler.go:265-266](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/handler.go#L265-L266), [268-271](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/handler.go#L268-L271); [inmemory.go:296](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/taskstore/inmemory.go#L296-L296), [302-305](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/taskstore/inmemory.go#L302-L305) | | .NET | all | all | ignored | GetTask `[]`; ListTasks key omitted | -32602 (ListTasks over REST: empty list) | [A2AServer.cs:396-398](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L396-L398); [AgentTaskExtensions.cs:16](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Extensions/AgentTaskExtensions.cs#L16-L16); [InMemoryTaskStore.cs:85-88](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/InMemoryTaskStore.cs#L85-L88) | | Rust | all | all | honored (blocking only; streaming ignores it) | cleared | cleared, no error | [task_store/mod.rs:18-24](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/task_store/mod.rs#L18-L24); [handler.rs:912](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L912-L912), [941-943](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L941-L943) | **Expect.** Unset gives the whole history on `GetTask` everywhere, but on `ListTasks` Java gives none and Go caps at 100, and `SendMessage`'s own `historyLength` is ignored by Go and .NET; send `0` when you want none, send a positive number when you want a bound, and never send a negative one. ### B5. `ListTasks` pagination **Specification.** §3.1.4: "The `nextPageToken` field MUST always be present in the response. When there are no more results to retrieve (i.e., this is the final page), the field MUST be set to an empty string ("")"; "MUST use cursor-based pagination"; "Implementations MUST return tasks sorted by their status timestamp time in descending order". `a2a.proto` lines 683-686: "If unspecified, at most 50 tasks will be returned. The minimum value is 1. The maximum value is 100." | Implementation | Token | Last page | Default / limits | Sort | Bad token | Evidence | |---|---|---|---|---|---|---| | Kit | URL-safe base64, no padding, of `{"ts":,"id":}` | `""` | 50; `min(pageSize, 100)` (clamped) | status timestamp desc, then id | InvalidParams | `capture/a2a_ref.py:438-465` | | Python | URL-safe base64, no padding, of `{"ts":,"id":}` (legacy base64 task-id token still accepted) | `""` | 50; 1..100 or -32602 / 400 ("minimum page size is 1", "maximum page size is 100") | status timestamp desc, id desc, no timestamp last | -32602 / 400 | [utils/task.py:75-86](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/task.py#L75-L86), [162-187](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/task.py#L162-L187); [inmemory_task_store.py:136-181](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/tasks/inmemory_task_store.py#L136-L181); [constants.py:8-12](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/constants.py#L8-L12) | | JS | standard base64 of `\|` | `''` | 50; 1..100 or -32602 / 400 (0 rejected) | timestamp desc, id desc | -32602 / 400; unknown cursor task: empty page | [utils.ts:132](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/utils.ts#L132-L132); [default_request_handler.ts:802-805](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L802-L805); [store.ts:94](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/store.ts#L94-L94) | | Java | plain `:` | `""` (domain `null`, printed as `""`) | 50; 1..100 or InvalidParams (-32602 / 422), 0 rejected | timestamp desc, id asc | InvalidParams | [PageToken.java:66](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/util/PageToken.java#L66-L66); [ListTasksParams.java:31-33](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/ListTasksParams.java#L31-L33), [52-53](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/ListTasksParams.java#L52-L53); [InMemoryTaskStore.java:153-154](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/tasks/InMemoryTaskStore.java#L153-L154) | | Go | URL-safe padded base64 of `_` | `""` | 50; 1..100 or `ErrInvalidRequest` (-32600 / 400) | store write time desc, id desc | `ErrParseError` (-32700 / 400) | [inmemory.go:320-322](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/taskstore/inmemory.go#L320-L322), [190](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/taskstore/inmemory.go#L190-L190), [199-200](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/taskstore/inmemory.go#L199-L200), [326-328](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/taskstore/inmemory.go#L326-L328), [208-211](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/taskstore/inmemory.go#L208-L211) | | .NET | plain integer offset (`"50"`) | `""` | 50; JSON-RPC rejects outside 1..100 (-32602); REST and gRPC do not limit | status timestamp desc, no tie-break | -32602 | [InMemoryTaskStore.cs:67](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/InMemoryTaskStore.cs#L67-L67), [77-78](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/InMemoryTaskStore.cs#L77-L78), [107-109](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/InMemoryTaskStore.cs#L107-L109); [A2AJsonRpcProcessor.cs:115](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A.AspNetCore/A2AJsonRpcProcessor.cs#L115-L115) | | Rust | plain decimal offset | `""` | 50; >100 clamped to 100, <=0 means 50 | task id ascending (not by timestamp) | -32602 / 400; offset past the end: empty page | [pagination.rs:17](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/pagination.rs#L17-L17), [29-32](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/pagination.rs#L29-L32); [inmemory.rs:113-114](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/task_store/inmemory.rs#L113-L114), [122-125](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/task_store/inmemory.rs#L122-L125), [136-137](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/task_store/inmemory.rs#L136-L137) | **Expect.** Treat the token as opaque and only echo it back, stop on `nextPageToken == ""` (every implementation emits it), keep `pageSize` inside 1..100 because four implementations reject values outside it with three different codes, and do not assume newest-first ordering against Rust or Go. ### B6. A message to a terminal task **Specification.** §3.1.1, Errors: "`UnsupportedOperationError`: Messages sent to Tasks that are in a terminal state (`TASK_STATE_COMPLETED`, `TASK_STATE_FAILED`, `TASK_STATE_CANCELED`, `TASK_STATE_REJECTED`) cannot accept further messages." §5.4: "`UnsupportedOperationError` | `-32004` | `FAILED_PRECONDITION` | `400 Bad Request`". | Implementation | Error | JSON-RPC | REST | gRPC | Evidence | |---|---|---|---|---|---| | Kit | UnsupportedOperationError | -32004 | 400 FAILED_PRECONDITION | n/a | `capture/a2a_ref.py:272-273`, `29` | | Python | UnsupportedOperationError, before the agent runs | -32004 | 400 FAILED_PRECONDITION | not captured | [active_task.py:529-532](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/agent_execution/active_task.py#L529-L532); [errors.py:137-151](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/errors.py#L137-L151) | | JS | UnsupportedOperationError, before the contextId check | -32004 | 400 FAILED_PRECONDITION | FAILED_PRECONDITION | [default_request_handler.ts:224-226](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L224-L226); [errors/rest.ts:80](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/errors/rest.ts#L80-L80), [97](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/errors/rest.ts#L97-L97); [errors/grpc/grpc.ts:61](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/errors/grpc/grpc.ts#L61-L61) | | Java | UnsupportedOperationError, after the contextId check | -32004 | 400 (status string `UNIMPLEMENTED`) | UNIMPLEMENTED | [DefaultRequestHandler.java:1569-1570](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java#L1569-L1570); [A2AErrorCodes.java:29](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/A2AErrorCodes.java#L29-L29) | | Go | `ErrUnsupportedOperation` since 2.6.0 (cluster mode still `ErrInvalidParams` -32602) | -32004 | 400 FAILED_PRECONDITION | FAILED_PRECONDITION | [agentexec.go:227-229](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/agentexec.go#L227-L229); [rest.go:201](https://github.com/a2aproject/a2a-go/blob/v2.6.0/internal/rest/rest.go#L201-L201); [grpcutil/errors.go:41](https://github.com/a2aproject/a2a-go/blob/v2.6.0/internal/grpcutil/errors.go#L41-L41); [distributed_manager.go:112-114](https://github.com/a2aproject/a2a-go/blob/v2.6.0/internal/taskexec/distributed_manager.go#L112-L114) | | .NET | UnsupportedOperation | -32004 | 400 ProblemDetails at the tag (streaming: HTTP 200 plus a generic SSE error frame); 400 google.rpc.Status on main | FAILED_PRECONDITION on main | [A2AServer.cs:585-587](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L585-L587); [A2AHttpProcessor.cs:113-117](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A.AspNetCore/A2AHttpProcessor.cs#L113-L117) | | Rust | UnsupportedOperationError, before the contextId check | -32004 | 400 FAILED_PRECONDITION | FAILED_PRECONDITION | [handler.rs:702-705](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L702-L705); [errors.rs:185](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a/src/errors.rs#L185-L185); [rest.rs:519-520](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/rest.rs#L519-L520); [a2a-grpc errors.rs:16](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-grpc/src/errors.rs#L16-L16) | **Expect.** Every implementation agrees on -32004 and HTTP 400; on gRPC expect `FAILED_PRECONDITION` from all but Java, which answers `UNIMPLEMENTED` (and the TCK's own table expects `UNIMPLEMENTED` too, against §5.4). ### B7. The `A2A-Version` header **Specification.** §3.6.1: "Clients MUST send the `A2A-Version` header with each request"; "Clients MAY provide the `A2A-Version` as a request parameter instead of a header." §3.6.2: "Agents MUST process requests using the semantics of the requested `A2A-Version` (matching `Major.Minor`). If the version is not supported by the interface, agents MUST return a `VersionNotSupportedError`"; "Agents MUST interpret empty value as 0.3 version." §5.4: `-32009` / `FAILED_PRECONDITION` / `400 Bad Request`. | Implementation | Missing or empty | Accepted values | Query parameter | Error (JSON-RPC / REST / gRPC) | Client sends | Evidence | |---|---|---|---|---|---|---| | Kit | rejected (reported as "0.3 (no header)") | exactly `1.0` | read | -32009 / 400 | `A2A-Version: 1.0` on every request | `capture/a2a_ref.py:524-527` | | Python | treated as 0.3, rejected | major must be 1: `1`, `1.0`, `1.0.0`, `1.5` | not read | -32009 / 400 FAILED_PRECONDITION | `1.0` on every request including the card fetch (httpx default header) | [version_validator.py:68-90](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/version_validator.py#L68-L90), [95-108](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/version_validator.py#L95-L108); [routes/common.py:113-122](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/routes/common.py#L113-L122); [client_factory.py:81-84](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/client/client_factory.py#L81-L84) | | JS | treated as `'0.3'`, rejected unless the card declares it | exact string membership in the card's `supportedInterfaces[].protocolVersion` for that binding (`1.0.0` rejected) | not read | -32009 / 400 FAILED_PRECONDITION / FAILED_PRECONDITION | `1.0` on every call including the card fetch | [context.ts:4-6](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/context.ts#L4-L6); [version.ts:41-44](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/version.ts#L41-L44); [card-resolver.ts:41-42](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/client/card-resolver.ts#L41-L42) | | Java | treated as `0.3`, rejected | major compare, needs `major.minor` form (`1` rejected, `1.1` and `1.0.0` accepted) | only by the optional multi-version router | -32009 / 400 / UNIMPLEMENTED | `1.0` on every call except the card fetch | [A2AVersionValidator.java:37-39](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/version/A2AVersionValidator.java#L37-L39), [80](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/version/A2AVersionValidator.java#L80-L80), [105-106](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/version/A2AVersionValidator.java#L105-L106); [A2AErrorCodes.java:46](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/A2AErrorCodes.java#L46-L46); [VersionRouter.java:21-27](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/reference/common/src/main/java/org/a2aproject/sdk/server/common/quarkus/VersionRouter.java#L21-L27) | | Go | accepted: the server never reads the header | anything | not read | -32009 mapped but never emitted | `1.0` on every call except the card fetch | [svcparams.go:19](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2a/svcparams.go#L19-L19); [client.go:269-270](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2aclient/client.go#L269-L270); [resolver.go:151](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2aclient/agentcard/resolver.go#L151-L151); [jsonrpc.go:83](https://github.com/a2aproject/a2a-go/blob/v2.6.0/internal/jsonrpc/jsonrpc.go#L83-L83) | | .NET | JSON-RPC: accepted and processed as 1.0; REST: never checked | JSON-RPC: `1.0` or `0.3` (0.3 not translated); main gRPC: only `1.0` | not read | -32009 (HTTP 200) / none / FAILED_PRECONDITION on main | `1.0` on every call except the card fetch | [A2AJsonRpcProcessor.cs:16-17](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A.AspNetCore/A2AJsonRpcProcessor.cs#L16-L17); [A2AClient.cs:136](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Client/A2AClient.cs#L136-L136); [A2ACardResolver.cs:67](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Client/A2ACardResolver.cs#L67-L67) | | Rust | JSON-RPC: defaulted to `0.3` and rejected; REST and gRPC: never checked | JSON-RPC: major 1 (`1`, `1.0`, `1.0.0`, `1.x`) | not read | -32009 (HTTP 200) / none / none | `1.0` on every call, not on the card fetch | [jsonrpc.rs:307-310](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/jsonrpc.rs#L307-L310), [311-313](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/jsonrpc.rs#L311-L313), [296-297](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/jsonrpc.rs#L296-L297); [transports_e2e.rs:434-435](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/examples/tests/transports_e2e.rs#L434-L435); [client.rs:25](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-client/src/client.rs#L25-L25) | **Expect.** Always send the literal string `A2A-Version: 1.0` (JS rejects `1.0.0`, Java rejects `1`), never rely on a server rejecting a missing header (Go, .NET REST, Rust REST and gRPC accept it), and never rely on the query parameter (only the kit and Java's multi-version router read it). ### B8. Content type of success and error responses **Specification.** §9.1 (JSON-RPC): "Content-Type: `application/json` for requests and responses"; "Streaming: Server-Sent Events (`text/event-stream`)". §11.1 (REST): "Content-Type: application/a2a+json SHOULD be used for requests and responses". §11.4 shows `Content-Type: application/a2a+json` on a success response and §11.6 shows `HTTP/1.1 404 Not Found` with `Content-Type: application/a2a+json` and a `google.rpc.Status`-shaped body `{"error":{"code":404,"status":"NOT_FOUND",...,"details":[ErrorInfo]}}`. | Implementation | JSON-RPC (success and error) | REST success | REST error | SSE | Request type enforced | Evidence | |---|---|---|---|---|---|---| | Kit | `application/json`, HTTP 200 | `application/a2a+json` | `application/a2a+json` (401 before any method: `application/json`) | `text/event-stream` | yes (`application/json` or `application/a2a+json`) | `capture/a2a_ref.py:18`, `579-589`, `636-660`, `485-488` | | Python | `application/json`, HTTP 200 | `application/a2a+json` | `application/json` | `text/event-stream; charset=utf-8` | no | [rest_dispatcher.py:51-54](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/routes/rest_dispatcher.py#L51-L54); [error_handlers.py:157-161](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/error_handlers.py#L157-L161); [jsonrpc_dispatcher.py:206-209](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/routes/jsonrpc_dispatcher.py#L206-L209) | | JS | `application/json`, HTTP 200 | `application/a2a+json` | `application/a2a+json` | `text/event-stream` | yes (JSON-RPC: `application/json`; REST: either) | [rest_handler.ts:134-136](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/express/rest_handler.ts#L134-L136); [json_rpc_handler.ts:196](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/express/json_rpc_handler.ts#L196-L196); [constants.ts:38-39](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/constants.ts#L38-L39) | | Java | `application/json`, HTTP 200 | `application/json` | `application/json` (auth failures: bare 401/403) | `text/event-stream` | yes (JSON-RPC `consumes`; REST 415 otherwise) | [RestHandler.java:709](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java#L709-L709), [729](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java#L729-L729); [MediaType.java:9-10](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/common/src/main/java/org/a2aproject/sdk/common/MediaType.java#L9-L10) | | Go | `application/json`, HTTP 200 | `application/json` | `application/json` | `text/event-stream` | no | [jsonrpc.go:136](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/jsonrpc.go#L136-L136); [rest.go:62-63](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/rest.go#L62-L63), [491-494](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/rest.go#L491-L494) | | .NET | `application/json`, HTTP 200 | `application/json` | tag: `application/problem+json` (ASP.NET `Results.Problem`), 404s as a JSON string; main: `application/json` google.rpc.Status | `text/event-stream` | no | [JsonRpcResponseResult.cs:41-42](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A.AspNetCore/JsonRpcResponseResult.cs#L41-L42); [A2AHttpProcessor.cs:291](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A.AspNetCore/A2AHttpProcessor.cs#L291-L291), [117](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A.AspNetCore/A2AHttpProcessor.cs#L117-L117); [main A2AErrorResult.cs:20-21](https://github.com/a2aproject/a2a-dotnet/blob/6894043111ec20e8546d446ee2a2e5a2a190518f/src/A2A.AspNetCore/A2AErrorResult.cs#L20-L21) | | Rust | `application/json`, HTTP 200 | `application/a2a+json` | `application/problem+json` (google.rpc.Status body) | `text/event-stream` | REST yes (axum `Json`: `application/json` or `application/*+json`); JSON-RPC no | [rest.rs:398-400](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/rest.rs#L398-L400), [505-507](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/rest.rs#L505-L507); [jsonrpc.rs:254-256](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/jsonrpc.rs#L254-L256) | **Expect.** A REST client must accept `application/json`, `application/a2a+json` and `application/problem+json` interchangeably and parse error bodies by shape (`error.code`, `error.status`, `error.details[]`), and a REST server should know that the TCK (1.0.0.alpha2) checks for the substring `application/json`, which `application/a2a+json` does not contain. ### B9. A message that carries only `taskId` **Specification.** §3.4.3: "Agents MUST infer `contextId` from the task if only `taskId` is provided." | Implementation | Context handed to the agent | Stored user message in `history` | Task | Evidence | |---|---|---|---|---| | Kit | the task's contextId | rewritten with the task's contextId (`ordered_message(message, contextId=task["contextId"])`) | unchanged | `capture/a2a_ref.py:274-277` | | Python | a fresh random contextId: the `RequestContext` is built before the task is loaded, so the generator mints one and stamps it on the message | carries that fresh random contextId (observed on the wire) | keeps its own contextId | [context.py:68-80](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/agent_execution/context.py#L68-L80), [180-192](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/agent_execution/context.py#L180-L192); [default_request_handler_v2.py:353-360](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/request_handlers/default_request_handler_v2.py#L353-L360); FINDINGS §5.g | | JS | the task's contextId, also stamped on the `userMessage` copy | as sent (empty contextId) | unchanged | [default_request_handler.ts:258](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L258-L258), [271-274](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L271-L274), [240-241](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L240-L241) | | Java | the task's contextId; the request message and emitted events are normalized to it | as sent | unchanged | [DefaultRequestHandler.java:1473](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java#L1473-L1473), [1584-1587](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java#L1584-L1587), [1489](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java#L1489-L1489) | | Go | `ExecutorContext.ContextID` = the task's | as sent | unchanged; emitted events are validated against it | [agentexec.go:253-254](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/agentexec.go#L253-L254), [235-236](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/agentexec.go#L235-L236) | | .NET | `RequestContext.ContextId` = the task's | as sent (null contextId) | unchanged | [A2AServer.cs:561-565](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L561-L565), [570-574](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L570-L574) | | Rust | `ExecutorContext.context_id` = the task's | as received | unchanged | [handler.rs:711-713](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L711-L713), [722](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L722-L722), [738-741](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L738-L741) | **Expect.** The Task's `contextId` is stable everywhere, but read it from the Task, never from a history entry: five SDKs store the follow-up message with no contextId and Python stores it with a different, random one. ### B10. Mismatched `taskId` and `contextId` **Specification.** §3.4.3: "Agents MUST reject messages containing mismatching `contextId` and `taskId` (i.e., the provided `contextId` is different from that of the referenced `Task`)." No error type is named. | Implementation | Error | JSON-RPC | REST | gRPC | Order relative to the terminal check | Evidence | |---|---|---|---|---|---|---| | Kit | InvalidParamsError | -32602 | 400 | n/a | before | `capture/a2a_ref.py:270-273` | | Python | InvalidParamsError "Context X does not match context Y of task Z" | -32602 | 400 INVALID_ARGUMENT | not captured | before (checked in the handler) | [default_request_handler_v2.py:340-351](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/request_handlers/default_request_handler_v2.py#L340-L351) | | JS | RequestMalformedError | -32602 | 400 INVALID_ARGUMENT | INVALID_ARGUMENT | after | [default_request_handler.ts:230-233](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L230-L233), [235](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L235-L235) | | Java | InvalidParamsError | -32602 | 422 INVALID_ARGUMENT | INVALID_ARGUMENT | before (handler); streaming pre-check on JSON-RPC and REST sees the terminal state first | [DefaultRequestHandler.java:1562-1567](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java#L1562-L1567); [A2AErrorCodes.java:55](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/A2AErrorCodes.java#L55-L55) | | Go | `ErrInvalidParams` | -32602 | 400 INVALID_ARGUMENT | INVALID_ARGUMENT | before | [agentexec.go:223-224](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/agentexec.go#L223-L224) | | .NET | no check: the client's value is used as `RequestContext.ContextId` | none | none | none | n/a | [A2AServer.cs:565](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L565-L565), [573](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L573-L573) | | Rust | InvalidParams | -32602 | 400 INVALID_ARGUMENT | INVALID_ARGUMENT | after | [handler.rs:713-716](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L713-L716) | **Expect.** Expect -32602 with HTTP 400 (422 from Java) everywhere except .NET, which accepts the wrong contextId silently; and expect the terminal-state error instead when the task is already finished on JS and Rust. ### B11. `append: true` for an unknown `artifactId` **Specification.** `a2a.proto` lines 315-317 (`TaskArtifactUpdateEvent`): "If true, the content of this artifact should be appended to a previously sent artifact with the same ID." and `bool append = 4;`. The prose (§4.2.2) only embeds the proto table. Nothing says what to do when no such artifact exists. | Implementation | `append: true`, unknown id | `append` false or unset, known id | `lastChunk` | Evidence | |---|---|---|---|---| | Kit | new artifact created from the chunk | replaced | ignored | `capture/a2a_ref.py:355-362` | | Python | `InvalidAgentResponseError` -32006 ("append=True for nonexistent artifact_id") | replaced | ignored | [task_manager.py:32-96](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/tasks/task_manager.py#L32-L96) | | JS | new artifact pushed, no error | replaced | ignored | [result_manager.ts:322-323](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/result_manager.ts#L322-L323), [319-320](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/result_manager.ts#L319-L320) | | Java | chunk dropped with a WARNING log; no error, no artifact | replaced | ignored | [Utils.java:172-176](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/util/Utils.java#L172-L176), [141-145](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/spec/src/main/java/org/a2aproject/sdk/spec/util/Utils.java#L141-L145) | | Go | plain error "no artifact found for update"; the processor moves the task to FAILED (cause not stored) and broadcasts the FAILED Task | replaced | ignored | [a2aevent/event.go:93-96](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2aevent/event.go#L93-L96), [101-104](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2aevent/event.go#L101-L104); [agentexec.go:450-451](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/agentexec.go#L450-L451) | | .NET | new artifact added, no error | replaced by index | never read | [TaskProjection.cs:101-104](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/TaskProjection.cs#L101-L104), [58-62](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/TaskProjection.cs#L58-L62) | | Rust | `INVALID_AGENT_RESPONSE` -32006 (REST 500, gRPC INTERNAL); chunk dropped, run aborted, task left non-terminal | replaced | ignored | [handler.rs:274-276](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L274-L276), [283-286](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L283-L286) | **Expect.** An agent must always send the first chunk of an artifact with `append` unset or false: three implementations silently create an artifact, one drops the chunk, Python and Rust abort with -32006, and Go fails the whole task. ### B12. A task created by another caller **Specification.** §3.1.4: "The operation MUST return only tasks visible to the authenticated client". §13.1: "Servers MUST implement authorization checks on every A2A Protocol Operations request"; "Get Task: MUST verify the authenticated client has access to the requested task". §3.3.2: "Servers MUST NOT reveal the existence of resources the client is not authorized to access"; "Servers SHOULD NOT distinguish between 'does not exist' and 'not authorized'"; `TaskNotFoundError` is "does not correspond to an existing or accessible task". | Implementation | Scoping mechanism | Default | Another caller's task | Evidence | |---|---|---|---|---| | Kit | none per caller; the deployer requires one shared bearer token before any method (401 with `WWW-Authenticate`) | n/a | returned | `capture/a2a_ref.py:239-240`, `485-488`, `629-630` | | Python | in-memory and database task stores keyed by owner = `context.user.user_name` | on, but every unauthenticated caller has `user_name == ''` and shares one scope; the SDK ships no authentication middleware | `TaskNotFoundError` (-32001 / 404); `ListTasks` filtered | [owner_resolver.py:7-13](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/owner_resolver.py#L7-L13); [inmemory_task_store.py:47-62](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/tasks/inmemory_task_store.py#L47-L62), [76-95](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/tasks/inmemory_task_store.py#L76-L95); [auth/user.py:20-31](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/auth/user.py#L20-L31); [database_task_store.py:84](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/tasks/database_task_store.py#L84-L84) | | JS | tenant and owner (`user.userName`, else `'unknown'`) in every store and in the event-bus manager | on; anonymous callers share `'unknown'` | `TaskNotFoundError` (-32001 / 404 / NOT_FOUND); never 403 | [owner_resolver.ts:17](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/owner_resolver.ts#L17-L17); [utils.ts:163-165](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/utils.ts#L163-L165); [store.ts:51](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/store.ts#L51-L51) | | Java | `TaskAuthorizationProvider` SPI (no production implementation shipped) | fail-closed since 1.3.0: with no provider and `a2a.authorization.required=true` every task operation, including creating one, is `TaskNotFoundError` | `TaskNotFoundError` (-32001 / 404 / NOT_FOUND); `ListTasks` filtered | [TaskAuthorizationProvider.java:11-12](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/auth/TaskAuthorizationProvider.java#L11-L12); [DefaultRequestHandler.java:229](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/DefaultRequestHandler.java#L229-L229); [a2a-defaults.properties:47](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/resources/META-INF/a2a-defaults.properties#L47-L47) | | Go | default in-memory store records `CallContext.User.Name` as owner | wired by default but inert until an interceptor sets a name; with an empty name `ListTasks` fails `ErrUnauthenticated` (-31401 / 401) | `ErrTaskNotFound`; live-stream subscribe on a running task is unchecked | [handler.go:233-235](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/handler.go#L233-L235); [middleware.go:109](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/middleware.go#L109-L109); [inmemory.go:176-177](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/taskstore/inmemory.go#L176-L177), [191-194](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/taskstore/inmemory.go#L191-L194) | | .NET | none: `ITaskStore`, `RequestContext` and the handler carry no principal | off, no opt-in | returned, cancelable, messageable by anyone | [ITaskStore.cs:20](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/ITaskStore.cs#L20-L20); [docs/security.md:91](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/docs/security.md#L91-L91) | | Rust | none in stores; opt-in `RequestAuthorizer` hook sees only headers and the task id | off | returned | [handler.rs:565](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L565-L565), [571-574](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L571-L574); [task_store/store.rs:21-27](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/task_store/store.rs#L21-L27) | **Expect.** Where scoping exists (Python, JS, Go, Java with a provider) a foreign task looks like a missing one (`TaskNotFoundError`, never 403), and where it does not (.NET, Rust, any deployment without an authentication middleware) every task is visible to every caller. ### B13. Serving the extended agent card **Specification.** §3.1.11: "Authentication: The client MUST authenticate the request using one of the schemes declared in the public `AgentCard.securitySchemes` and `AgentCard.security` fields." §13.3: "The Get Extended Agent Card operation MUST require authentication"; "When `capabilities.extendedAgentCard` is `false` or not present, the operation MUST return `UnsupportedOperationError`"; "When support is declared but no extended card is configured, the operation MUST return `ExtendedAgentCardNotConfiguredError`." | Implementation | Authentication in the SDK core | Not declared | Declared, not configured | Client side | Evidence | |---|---|---|---|---|---| | Kit | the deployer checks its bearer token before every method, extended card included | UnsupportedOperationError -32004 | ExtendedAgentCardNotConfiguredError -32007 | the capture calls it without and with the token | `capture/a2a_ref.py:467-472`, `629-630` | | Python | none; an `extended_card_modifier(card, context)` hook can decide | -32004 / 400 | -32007 / 400 | n/a | [default_request_handler_v2.py:663-688](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/request_handlers/default_request_handler_v2.py#L663-L688) | | JS | a static card is returned only if `context.user.isAuthenticated`, otherwise the public card (no error); a callback provider decides itself | -32004 / 400 / FAILED_PRECONDITION | -32007 / 400 / FAILED_PRECONDITION | replaces the cached card; adds no credentials | [default_request_handler.ts:155-156](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L155-L156), [158-159](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L158-L159), [162-165](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L162-L165), [166-167](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L166-L167) | | Java | none in `server-common` (`TODO: Add authentication`, issue 77); the Quarkus reference servers annotate the routes `@Authenticated` | -32004 / 400 / UNIMPLEMENTED | -32007 / 400 / FAILED_PRECONDITION | replaces the cached card; credentials only via interceptors | [JSONRPCHandler.java:704](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java#L704-L704); [A2AServerRoutes.java:256-260](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java#L256-L260); [QuarkusGrpcHandler.java:76](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandler.java#L76-L76) | | Go | none; the `ExtendedAgentCardProducer` receives the `CallContext` | -32004 only when `WithCapabilityChecks` is configured | -32007 | result not cached; credentials only via interceptors | [handler.go:481-483](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/handler.go#L481-L483), [484-487](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/handler.go#L484-L487) | | .NET | none; the default method always throws NotConfigured and the application overrides it | tag: not distinguished (always -32007); main: -32004 | -32007 (REST 500 at the tag, 400 on main) | no card cache; no credentials | [A2AServer.cs:546-550](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L546-L550); [main A2AServer.cs:559-562](https://github.com/a2aproject/a2a-dotnet/blob/6894043111ec20e8546d446ee2a2e5a2a190518f/src/A2A/Server/A2AServer.cs#L559-L562) | | Rust | none; a resolver closure receives the request headers; opt-in `RequestAuthorizer` | -32004 only when the capability is explicitly `false` | -32007 / 400 / FAILED_PRECONDITION | result returned, not cached | [handler.rs:1135-1137](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L1135-L1137), [1141-1142](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L1141-L1142), [1147](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L1147-L1147) | **Expect.** No SDK core enforces §13.3; an integrator must put authentication in front of `GetExtendedAgentCard` (an interceptor, framework middleware, or a user object that JS can test), or anonymous callers receive the extended card. ### B14. A resent `messageId` **Specification.** §3.3.1: "Send Message operations MAY be idempotent. Agents may utilize the messageId to detect duplicate messages." | Implementation | Detection | Resend without `taskId` | Resend with `taskId` | Evidence | |---|---|---|---|---| | Kit | none (`messageId` only required) | a second task | appended to history again, agent runs again | `capture/a2a_ref.py:252-253`, `277` | | Python | none; the only `messageId` comparison avoids storing the user message twice | a second task | agent runs again; history not duplicated | [active_task.py:306-318](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/agent_execution/active_task.py#L306-L318) | | JS | none | a second task | appended again, agent runs again | [default_request_handler.ts:632-633](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L632-L633), [240](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L240-L240), [243](https://github.com/a2aproject/a2a-js/blob/v1.3.0/src/server/request_handler/default_request_handler.ts#L243-L243) | | Java | none | a second task | appended again, agent runs again | [TaskManager.java:218](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/tasks/TaskManager.java#L218-L218); [RequestContext.java:424-426](https://github.com/a2aproject/a2a-java/blob/v1.4.0.Final/server-common/src/main/java/org/a2aproject/sdk/server/agentexecution/RequestContext.java#L424-L426) | | Go | none (`TODO: handle idempotency once spec establishes the key`) | a second task | history not duplicated, agent runs again; while the first run is live: "execution is already in progress" (-32603 / 500) | [agentexec.go:231-233](https://github.com/a2aproject/a2a-go/blob/v2.6.0/a2asrv/agentexec.go#L231-L233); [local_manager.go:207-208](https://github.com/a2aproject/a2a-go/blob/v2.6.0/internal/taskexec/local_manager.go#L207-L208), [155-156](https://github.com/a2aproject/a2a-go/blob/v2.6.0/internal/taskexec/local_manager.go#L155-L156) | | .NET | none | a fresh task id | appended again, handler runs again | [A2AServer.cs:79-82](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L79-L82), [572](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/A2AServer.cs#L572-L572); [TaskProjection.cs:35](https://github.com/a2aproject/a2a-dotnet/blob/v1.0.0-preview2/src/A2A/Server/TaskProjection.cs#L35-L35) | | Rust | none | a second task | appended again, executor runs again; while the first run is live: `InvalidRequestError` -32600 | [handler.rs:692-695](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L692-L695), [731-741](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L731-L741), [97-101](https://github.com/a2aproject/a2a-rs/blob/a2a-server-lf-v0.5.1/a2a-server/src/handler.rs#L97-L101) | **Expect.** Idempotency does not exist anywhere: a retried `SendMessage` without `taskId` creates a second task, so a client must keep the first response and retry only with `taskId`. ## 3. Summary matrix | # | Behavior | Kit | Python 1.2.2 | JS 1.3.0 | Java 1.4.0 | Go 2.6.0 | .NET preview2 | Rust 0.5.1 | |---|---|---|---|---|---|---|---|---| | B1 | REST subscribe verb (server / client) | any / n/a | GET+POST / POST | GET+POST / POST | POST / POST | GET+POST / POST | POST / POST | GET+POST / GET | | B2 | Stream closes on | terminal, INPUT_REQ | executor return | terminal, INPUT_REQ, Message | terminal, Message | terminal, INPUT_REQ, Message | handler return or TaskUpdater | terminal or executor end | | B3 | Push body / type / token | wrapper / a2a+json / X-A2A-… | wrapper / json / X-A2A-… | wrapper / a2a+json / X-A2A-… | wrapper / json / X-A2A-… | wrapper / json / A2A-… | not implemented | wrapper / json / A2A-… | | B4 | historyLength unset (Get / List) | all / all | all / all | all / all | all / none | all / 100 | all / all | all / all | | B5 | Page token / out-of-range pageSize | b64 cursor / clamp | b64 cursor / -32602 | b64 cursor / -32602 | plain cursor / -32602 | b64 cursor / -32600 | offset / -32602 (JSON-RPC only) | offset / clamp | | B6 | Terminal task (JSON-RPC / REST / gRPC) | -32004 / 400 | -32004 / 400 | -32004 / 400 / FAILED_PRECONDITION | -32004 / 400 / UNIMPLEMENTED | -32004 / 400 / FAILED_PRECONDITION | -32004 / 400 | -32004 / 400 / FAILED_PRECONDITION | | B7 | Missing A2A-Version | reject | reject | reject | reject | accept | accept | JSON-RPC reject; REST, gRPC accept | | B8 | REST success / error type | a2a+json / a2a+json | a2a+json / json | a2a+json / a2a+json | json / json | json / json | json / problem+json | a2a+json / problem+json | | B9 | contextId inferred into | context and stored message | random id on context and message | context | context | context | context | context | | B10 | Mismatched contextId | -32602 | -32602 | -32602 | -32602 (REST 422) | -32602 | accepted | -32602 | | B11 | append to unknown artifact | new artifact | -32006 | new artifact | dropped | task FAILED | new artifact | -32006 | | B12 | Other caller's task | visible | NotFound (owner scope) | NotFound (owner scope) | NotFound (fail-closed SPI) | NotFound once a name is set | visible | visible | | B13 | Extended card auth in core | bearer gate (deployer) | none | isAuthenticated for static card | none (Quarkus annotations) | none | none | none | | B14 | Duplicate messageId | none | none | none | none | none | none | none | ## 4. Agreements: where every SDK agrees with each other and not with the specification 1. **Extended card authentication (B13).** §13.3 says the operation "MUST require authentication". Python, JS (callback form), Java, Go, .NET and Rust all hand the card to whoever asks unless the integrator adds a check outside the SDK core. Only the kit's deployer gates it, with a bearer check that runs before any A2A method. 2. **The `?A2A-Version=` request parameter (B7).** §3.6.1 allows clients to send the version "as a request parameter instead of a header". No single-version SDK server reads it: Python ([routes/common.py:113-122](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/server/routes/common.py#L113-L122) copies only headers), JS, Go, .NET and Rust read the header only; Java reads it only in the optional multi-version router. The kit reads both. 3. **The §6.4 version-error body.** §6.4 ("Version Negotiation Error") shows a version failure answered with HTTP 400 and a problem-details body that lists `supportedVersions`. Every SDK that rejects a version uses its ordinary A2A error envelope instead (`-32009` in a JSON-RPC error, or a `google.rpc.Status` body on REST) and none lists the supported versions in a structured field (Python's and Java's messages mention them in prose only). 4. **The push token header (B3).** §4.3.1 defines `PushNotificationConfig.token` but gives it no transport. Every SDK that pushes puts it in a custom header, under two spellings: `X-A2A-Notification-Token` (Python, JS, Java, kit) and `A2A-Notification-Token` (Go, Rust). 5. **Authorization scoping needs the integrator (B12).** §13.1 says servers "MUST implement authorization checks on every A2A Protocol Operations request". No SDK ships authentication, so the default deployment of every SDK (and the kit) serves every task to every caller; Java's fail-closed default denies everything rather than scoping, and Python, JS and Go scope only once something sets a user name. 6. **No `messageId` idempotency (B14).** Allowed by §3.3.1's MAY, but worth stating as an agreement: no implementation, kit included, detects a resent message, so a client retry without `taskId` always creates a second task. 7. **JSON-RPC errors travel in HTTP 200.** Every SDK and the kit answer JSON-RPC errors (including `VersionNotSupportedError`) with HTTP 200 and an `error` member; §9 does not say otherwise, but §6.4's 400 example does, and only JSON-RPC's own rules are followed. ## 5. Open points - Python gRPC statuses for B6, B7 and B10 were not captured (the wire findings cover JSON-RPC and HTTP+JSON only); the JSON-RPC and REST mappings come from [errors.py:137-160](https://github.com/a2aproject/a2a-python/blob/v1.2.2/src/a2a/utils/errors.py#L137-L160). - Java's unset-versus-zero distinction on gRPC and JSON-RPC `GetTask` (B4) depends on MapStruct-generated code that is not in the checkout (noted in the Java notes). - .NET's REST error content types at the tag (B8) rest on ASP.NET Core defaults (`Results.Problem`), cited to framework documentation in the .NET notes, not to repository code. - The Go cluster mode, the Java multi-version modules and every 0.3 compatibility layer were read only where they change a wire fact; they are labelled where they appear.