This directory (src/compat/v0_3/) provides the foundational data representations necessary for modern v1.0 clients and servers to interoperate with legacy v0.3 A2A systems.
The compat layer is shipped as six subpath exports off @a2a-js/sdk. Each subpath carries only the peer dependencies (express, @grpc/grpc-js) its runtime needs, so a Workers consumer that only opts into the compat-aware client transports never has to pull in Node-only modules.
| Subpath | What it exports | Peer deps |
|---|---|---|
@a2a-js/sdk/compat/v0_3 |
v0.3 protocol constants (A2A_LEGACY_PROTOCOL_VERSION, LEGACY_HTTP_EXTENSION_HEADER, LEGACY_JSON_CONTENT_TYPE, the LEGACY_METHOD_* literals) and method-name translators (legacyJsonRpcToV1Method, v1MethodToLegacyJsonRpc, legacyJsonRpcToLegacyGrpcMethod, legacyGrpcToLegacyJsonRpcMethod, legacyGrpcToV1Method, v1MethodToLegacyGrpc, isLegacyJsonRpcMethod, isV1JsonRpcMethod). |
none (Workers-safe) |
@a2a-js/sdk/compat/v0_3/server |
Framework-agnostic transport handlers (LegacyJsonRpcTransportHandler, LegacyRestTransportHandler, toLegacyHTTPError), push-notification factory (createLegacyAwarePushNotificationSender) and serializer (V03PushNotificationSerializer), and the LegacyA2AError class. Mount the transport handlers from any HTTP runtime (Express, Fastify, Hono, Cloudflare Workers, …). |
none (Workers-safe) |
@a2a-js/sdk/compat/v0_3/server/express |
Express routers (legacyAgentCardRouter, legacyRestRouter) that wrap the handlers above with the v0.3 well-known agent-card and REST endpoint paths. Header-based dispatch on A2A-Version. |
express |
@a2a-js/sdk/compat/v0_3/server/grpc |
v0.3 gRPC service factory (legacyGrpcService), service descriptor (LegacyA2AService), and options type. Register alongside the v1.0 grpcService on the same gRPC Server. |
@grpc/grpc-js |
@a2a-js/sdk/compat/v0_3/client |
Card-resolver helpers (isLegacyAgentCard, parseLegacyAgentCard) and the v0.3 JSON-RPC + REST client transports (LegacyJsonRpcTransport, LegacyRestTransport). |
none (Workers-safe) |
@a2a-js/sdk/compat/v0_3/client/grpc |
v0.3 gRPC client transport (LegacyGrpcTransport), instantiated by the v1.0 GrpcTransportFactory when legacyCompat: { enabled: true } and the matched AgentInterface.protocolVersion falls in [0.3, 1.0). |
@grpc/grpc-js |
The bidirectional v0.3 ↔ v1.0 payload translators in ./translate/ are intentionally NOT part of the public surface and may change without a major-version bump.
To support cross-version compatibility across JSON, REST, and gRPC, this directory manages three distinct legacy data representations inside types/:
This file contains TypeScript interfaces generated from the legacy v0.3 JSON schema.
- Purpose: This is the primary legacy format. Legacy JSON-RPC and REST implementations natively serialize to/from these interfaces. It acts as the foundational data model for legacy message payloads.
The v0.3 protobuf bindings are split across two sibling directories, mirroring the v1.0 layer (src/types/pb/ vs. src/grpc/pb/):
-
types/pb/a2a.ts— generated byts-protowithoutputServices=false,outputEncodeMethods=false,emitImportedFiles=false. Contains message-typeinterfaces, enum declarations, and per-messagefromJSON/toJSONhelpers only — no wire encode/decode, no gRPC services, no well-known-type files. The converters (types/converters/from_proto.tsandto_proto.ts) — and anything that imports them transitively, including the v1.0JsonRpcTransportFactory/RestTransportFactory— stay Cloudflare Workers-compatible. Generated bysrc/compat/v0_3/types/buf.gen.yaml. -
grpc/pb/a2a.ts— generated byts-protowithoutputServices=grpc-jsand then post-processed to drop its duplicate message-type interfaces in favor ofexport type X = pb.Xre-exports from../../types/pb/a2a.js. Carries the wireencode/decoderuntime, the well-known-type files (google/protobuf/...), and theA2AServiceService/A2AServiceServer/A2AServiceClientgRPC bindings consumed bylegacyGrpcServiceandLegacyGrpcTransport. Generated bysrc/compat/v0_3/grpc/buf.gen.yaml; seesrc/compat/v0_3/grpc/README.mdfor the post-processing recipe. -
Purpose: To decode incoming bytes from legacy gRPC clients or encode outbound bytes to legacy gRPC servers, and to provide the service descriptors used by the compat-layer server and client transports.
The translate/ subdirectory contains bidirectional payload translators between the modern v1.0 protobuf types (in src/types/pb/a2a.ts) and the legacy v0.3 JSON types (in src/compat/v0_3/types/types.ts).
The naming convention is direction-anchored:
toCore<Entity>converts a v0.3 value into the equivalent v1.0 proto value.toCompat<Entity>converts a v1.0 proto value into the equivalent v0.3 JSON value.
Translators are split per entity group (parts.ts, messages.ts, tasks.ts, push_notifications.ts, security.ts, agent_card.ts, requests.ts, enums.ts, versions.ts) for clarity and tree-shaking. The full surface is re-exported from src/compat/v0_3/translate/index.ts.
Notable policy decisions:
PushNotificationAuthenticationInfo.schemesis truncated to a single scheme going v0.3 → v1.0; only the first entry is kept.- The v1.0
OAuthFlows.deviceCodeflow is silently dropped going v1.0 → v0.3 (v0.3 has no equivalent). TaskStatusUpdateEvent.finalis computed from the status state going v1.0 → v0.3 (trueforcompleted,canceled,failed,rejected).SendMessageConfiguration.returnImmediately↔MessageSendConfiguration.blockingwith inverted polarity.toCompatAgentCard(card)filterssupportedInterfacesto those whoseprotocolVersionis empty or in[0.3, 1.0)and throwsVersionNotSupportedErrorif none qualify.duplicateInterfacesForLegacy(interfaces, bindings)appends a v0.3 mirror entry for each listed binding that doesn't already have one. Idempotent; use it when declaring an agent card to opt specific bindings into v0.3 advertisement.
Per A2A spec §3.6.2, clients that omit the A2A-Version header are treated as v0.3 requests. v0.3 advertisement and routing are strictly per-interface: a binding is only reachable at v0.3 if the agent card declares an AgentInterface for it with protocolVersion: '0.3'.
validateVersion(requestedVersion, card, binding)accepts the request iffrequestedVersionis in the set of versions declared forbindinginsupportedInterfaces.legacyAgentCardRoutercallstoCompatAgentCard(card)(strict filter) and serves the resulting v0.3-shaped card to legacy-range requests. A v1.0-only card produces HTTP 400 on the legacy path.
Operators advertise a binding at v0.3 by declaring a per-interface protocolVersion: '0.3' — manually or via duplicateInterfacesForLegacy. The compat-v1-server sample shows the helper in use.
The v1.0 gRPC service factory (src/server/grpc/grpc_service.ts) intentionally does not carry a legacyCompat option; v0.3 gRPC clients are served by importing legacyGrpcService from @a2a-js/sdk/compat/v0_3/server/grpc and registering it alongside the v1.0 grpcService on the same Server. (The v1.0 @a2a-js/sdk/server/grpc barrel does not re-export legacyGrpcService; the explicit compat import keeps @grpc/grpc-js out of the v1.0 dependency graph for operators who only deploy the v1.0 service.)
jsonRpcHandler({ legacyCompat: { enabled: true } }) routes each request body based on its method field:
- Method matches a v1.0 PascalCase name (
isV1JsonRpcMethod→true, e.g.SendMessage,ListTasks) → v1.0 dispatcher. - Anything else — kebab-style v0.3 names (
message/send,tasks/get), unknown strings, or bodies with nomethodfield at all → v0.3 dispatcher.
The fallback exists so malformed and unknown requests surface v0.3-shaped errors (-32600 Invalid Request for missing method, -32602 for bad params) instead of the v1.0 path's blanket -32602, which is what header-less v0.3 clients expect per spec §3.6.2.
The legacy tasks/resubscribe handler always responds with Content-Type: text/event-stream and HTTP 200, even when the underlying call fails immediately (e.g. the task does not exist). Pre-stream errors are emitted as SSE error events on the open stream rather than as a plain JSON-RPC error response. Strict v0.3 clients reject anything other than text/event-stream on this method, so the header is committed before the first iterator pull. Other streaming methods (message/stream, SubscribeToTask) keep the peek-then-flush behaviour so a synchronous invalid-params error still surfaces as a JSON 200 error envelope.
Webhooks registered over v0.3 transports must receive the v0.3-shaped HTTP body, not the v1.0 StreamResponse wrapper. Per the v0.3 spec example (§9.5), the body is the bare event object (a v0.3 JSON Task, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent discriminated by its kind field) with Content-Type: application/json — no StreamResponse discriminator and no JSON-RPC envelope.
This is implemented by two pieces working together:
-
PushNotificationStorecaptures the wire version. TheInMemoryPushNotificationStore(and any conforming implementation) readscontext.requestedVersiononsave()and persists it alongside the config as aStoredPushNotificationConfig { config, wireVersion }. The wire version is surfaced via the optionalloadWithMetadataread method. -
DefaultPushNotificationSenderroutes per wire version. The sender prefersloadWithMetadatawhen the store implements it, otherwise falls back toloadand defaults every entry to wire version'0.3'per spec §3.6.2's absent-header rule. It always registersV1PushNotificationSerializerunder'1.0'and falls back to it (with a one-time warning per unknown version) when no serializer is registered for the entry's version.
Use createLegacyAwarePushNotificationSender (exported from @a2a-js/sdk/compat/v0_3/server) instead of constructing the sender directly. It pre-registers V03PushNotificationSerializer under the '0.3' key:
import { InMemoryPushNotificationStore } from '@a2a-js/sdk/server';
import { createLegacyAwarePushNotificationSender } from '@a2a-js/sdk/compat/v0_3/server';
const store = new InMemoryPushNotificationStore();
const sender = createLegacyAwarePushNotificationSender(store);
// Hand both to your DefaultRequestHandler as usual.v1.0-registered webhooks continue to receive the canonical StreamResponse body with application/a2a+json; v0.3-registered webhooks receive the bare-event JSON with application/json. Custom serializers (or overrides for the built-in '0.3' / '1.0' entries) can be supplied via the serializers option; user-supplied entries take precedence.
The PushNotificationStore.loadWithMetadata method is optional. The SDK's InMemoryPushNotificationStore implements it. Custom store implementations that omit it cause the sender to default each dispatch to the wire version of the request that triggered the dispatch (context.requestedVersion), falling back to '0.3' per spec §3.6.2 only when the triggering context carries no version.
What this means for the two deployment shapes a v1.0 server can take:
-
Pure v1.0 deployment (no compat layer opted in): no concern. Every triggering context carries
'1.0', the built-in V1 serializer handles every dispatch, and no warnings are emitted — even with a custom store implementation. -
v1.0 deployment with v0.3 compat opted in (
createLegacyAwarePushNotificationSender): each webhook receives the body shape of whichever client triggered the dispatch, not necessarily of the client that originally registered the webhook. If a v1.0 client triggers an event for a task with a webhook registered by a legacy v0.3 client, that v0.3 webhook will receive a v1.0 body (and vice versa). ImplementloadWithMetadataon your custom store (mirrorInMemoryPushNotificationStore's 3-line implementation) to preserve the originating wire version per config.
The compat layer (and therefore the caveat above) is opt-in and will be retired once the legacy v0.3 client base has migrated to v1.0.