This guide covers the breaking changes in the @a2a-js/sdk when upgrading from
v0.3 to v1.0. It focuses on SDK-specific API changes. For protocol-level data
model changes (renamed fields, restructured types, new operations), see:
-
Node.js >= 20 is now required (v0.3 supported Node 18).
-
Install the v1.0 SDK:
npm install @a2a-js/sdk
Migrating all v0.3 clients to v1.0 during upgrade is not required: the v1.0 SDK ships an opt-in compatibility layer that lets a v1.0 server accept v0.3 clients (and a v1.0 client talk to v0.3 servers). See compatibility-v0_3.md for end-user setup and caveats.
The JSON-Schema-generated types in src/types.ts have been deleted. All
types now come from protobuf-generated definitions. The full data model is
defined in the Protocol Data Model
section of the spec. Below are the changes that affect how you write SDK code.
TextPart, FilePart, and DataPart are replaced by a single Part type
with a content oneof discriminated by $case:
// v0.3
const text: TextPart = { kind: 'text', text: 'Hello', metadata: {} };
const file: FilePart = {
kind: 'file',
file: { uri: '...', mimeType: 'image/png', name: 'photo.png' },
};
// v1.0
const text: Part = {
content: { $case: 'text', value: 'Hello' },
metadata: undefined,
filename: '',
mediaType: 'text/plain',
};
const file: Part = {
content: { $case: 'url', value: '...' },
metadata: undefined,
filename: 'photo.png',
mediaType: 'image/png',
};
// Discriminating:
switch (part.content?.$case) {
case 'text':
/* part.content.value is string */ break;
case 'url':
/* file by URL */ break;
case 'raw':
/* file by bytes (Buffer) */ break;
case 'data':
/* structured JSON data */ break;
}| v0.3 | v1.0 |
|---|---|
FilePart.file.mimeType |
Part.mediaType |
FilePart.file.name |
Part.filename |
FilePart.file.uri |
Part.content with $case: 'url' |
FilePart.file.bytes |
Part.content with $case: 'raw' |
The kind field has been removed from Message, Task,
TaskStatusUpdateEvent, and TaskArtifactUpdateEvent
(spec reference).
The SDK provides typed wrappers as replacements:
- Client side:
StreamResponse-- usepayload.$case(see Section 2.3) - Server side:
AgentExecutionEvent-- useevent.kindon the wrapper (see Section 3.4)
// v1.0 client -- StreamResponse.payload.$case
switch (streamResponse.payload?.$case) {
case 'message':
/* .value is Message */ break;
case 'task':
/* .value is Task */ break;
case 'statusUpdate':
/* .value is TaskStatusUpdateEvent */ break;
case 'artifactUpdate':
/* .value is TaskArtifactUpdateEvent */ break;
}
// v1.0 server -- AgentExecutionEvent.kind
switch (event.kind) {
case 'message':
/* event.data is Message */ break;
case 'task':
/* event.data is Task */ break;
case 'statusUpdate':
/* event.data is TaskStatusUpdateEvent */ break;
case 'artifactUpdate':
/* event.data is TaskArtifactUpdateEvent */ break;
}All enum values are now standardized to use SCREAMING_SNAKE_CASE format.
See the spec for the full
TaskState and
Role definitions.
// v0.3
task.status.state === 'completed';
message.role === 'user';
// v1.0
import { TaskState, Role } from '@a2a-js/sdk';
task.status.state === TaskState.TASK_STATE_COMPLETED;
message.role === Role.ROLE_USER;All JSON-RPC envelope types from src/types.ts are removed (A2ARequest,
A2AResponse, JSONRPCResponse, MessageSendParams, TaskQueryParams,
TaskIdParams, all *SuccessResponse types, etc.). The SDK now uses
protobuf-based request types directly and returns unwrapped domain objects.
The following changes are defined by the spec. See the linked sections for details; the SDK types reflect these changes directly.
| Change | Spec reference |
|---|---|
AgentCard restructured (supportedInterfaces replaces url/preferredTransport/additionalInterfaces) |
AgentCard, AgentInterface |
PushNotificationConfig flattened into TaskPushNotificationConfig; AuthenticationInfo.schemes (array) changed to .scheme (string) |
PushNotificationConfig, AuthenticationInfo |
MessageSendConfiguration renamed to SendMessageConfiguration; blocking replaced by returnImmediately (inverted semantics); pushNotificationConfig renamed to taskPushNotificationConfig |
SendMessageConfiguration |
TaskStatusUpdateEvent.final removed |
TaskStatusUpdateEvent |
ImplicitOAuthFlow and PasswordOAuthFlow deprecated; DeviceCodeOAuthFlow added |
OAuthFlows |
JSON-RPC method names changed (e.g., message/send -> SendMessage) |
Method Mapping Reference |
REST content type changed to application/a2a+json |
IANA Media Type |
Extension header renamed from X-A2A-Extensions to A2A-Extensions |
A2A-Extensions Header |
A2AClient was deprecated in v0.3 in favor of ClientFactory and Client
(see ClientFactory docs). It has now been removed entirely. If you are still
using A2AClient, migrate to ClientFactory:
// v1.0
import { ClientFactory } from '@a2a-js/sdk/client';
const factory = new ClientFactory();
const client = await factory.createFromAgentCard(agentCard);
// OR: const client = await factory.createFromUrl('https://agent.example.com');
const result = await client.sendMessage(request);
// result is directly Message | Task (no JSON-RPC envelope)All method parameter types changed from SDK-specific types to protobuf request types:
| v0.3 Type | v1.0 Type |
|---|---|
MessageSendParams |
SendMessageRequest |
TaskQueryParams |
GetTaskRequest |
TaskIdParams (for cancel) |
CancelTaskRequest |
TaskIdParams (for resubscribe) |
SubscribeToTaskRequest |
GetTaskPushNotificationConfigParams |
GetTaskPushNotificationConfigRequest |
ListTaskPushNotificationConfigParams |
ListTaskPushNotificationConfigsRequest |
DeleteTaskPushNotificationConfigParams |
DeleteTaskPushNotificationConfigRequest |
All v1.0 types are imported from @a2a-js/sdk.
Method rename: client.setTaskPushNotificationConfig() ->
client.createTaskPushNotificationConfig()
New method: client.listTasks(params) for paginated task listing.
Streaming methods now return AsyncGenerator<StreamResponse> instead of raw
event unions. Discriminate via payload.$case:
for await (const event of client.sendMessageStream(params)) {
switch (event.payload?.$case) {
case 'message':
handleMessage(event.payload.value);
break;
case 'task':
handleTask(event.payload.value);
break;
case 'statusUpdate':
handleStatus(event.payload.value);
break;
case 'artifactUpdate':
handleArtifact(event.payload.value);
break;
}
}If you implement a custom Transport:
- Add
get protocolName(): stringandget protocolVersion(): stringproperties. - Rename
setTaskPushNotificationConfigtocreateTaskPushNotificationConfig. - Add
listTasks()method. - Update all parameter types per Section 2.2.
- Streaming methods must return
AsyncGenerator<StreamResponse>. getExtendedAgentCard()now requires aGetExtendedAgentCardRequestas its first parameter.
JsonRpcTransport, RestTransport, and their options types are no longer
public exports. Use the factory classes (JsonRpcTransportFactory,
RestTransportFactory) instead.
A2AExpressApp was deprecated in v0.3 in favor of the individual handler
middlewares (jsonRpcHandler, restHandler, agentCardHandler -- see their
docs). It has now been removed entirely. If you are still using A2AExpressApp,
migrate to the individual handlers:
// v1.0
import { jsonRpcHandler, restHandler, agentCardHandler } from '@a2a-js/sdk/server/express';
app.use('/.well-known/agent-card.json', agentCardHandler({ agentCardProvider: requestHandler }));
app.use('/', jsonRpcHandler({ requestHandler, userBuilder }));
app.use('/', restHandler({ requestHandler, userBuilder }));agentCardHandler now supports caching via cache: { maxAge: 3600 } (sets
Cache-Control and ETag headers). The REST handler automatically registers
tenant-prefixed routes (/:tenant/tasks/:taskId, etc.) and validates the
A2A-Version header.
The monolithic A2AError class with static factory methods is removed. Errors
now form a shared transport-agnostic hierarchy — one A2AError base with
semantic subclasses (TaskNotFoundError, RequestMalformedError, …). They
live at @a2a-js/sdk/errors; gRPC-specific error helpers live at
@a2a-js/sdk/errors/grpc so consumers who don't use gRPC don't pull in
@bufbuild/protobuf.
// v0.3
import { A2AError } from '@a2a-js/sdk/server';
throw A2AError.taskNotFound('task-1');
throw A2AError.invalidParams('bad input');
// v1.0
import { TaskNotFoundError, RequestMalformedError } from '@a2a-js/sdk/errors';
throw new TaskNotFoundError({ message: 'task-1' });
throw new RequestMalformedError({ message: 'bad input' });Semantic classes: TaskNotFoundError, TaskNotCancelableError,
RequestMalformedError, UnsupportedOperationError,
PushNotificationNotSupportedError, ContentTypeNotSupportedError,
InvalidAgentResponseError, ExtendedAgentCardNotConfiguredError,
ExtensionSupportRequiredError, VersionNotSupportedError. A2AError
itself is the concrete fallback — instantiate it directly
(new A2AError('...')) when no semantic class fits.
Per-transport variants (RestTaskNotFoundError, GrpcTaskNotFoundError,
JsonRpcTaskNotFoundError, …) carry transport-native context; narrow via the
isRestError / isGrpcError / isJsonRpcError type guards. All catch-side
API surfaces are on the base:
import { isRestError, TaskNotFoundError } from '@a2a-js/sdk/errors';
try {
await client.getTask({ id });
} catch (e) {
if (e instanceof TaskNotFoundError) {
if (isRestError(e)) {
// e.statusCode, e.headers, e.cause are typed
if (e.statusCode === 429) backoff(e.headers?.['retry-after']);
}
}
}For gRPC callers, the transport variant + guard live in a separate subpath:
import { isGrpcError, TaskNotFoundError } from '@a2a-js/sdk/errors/grpc';Error codes and gRPC/HTTP status mappings are defined in the
spec
and live in a single registry (A2A_ERROR_SPECS) exported from
@a2a-js/sdk/errors.
context changed from optional to required on all interfaces
(A2ARequestHandler, TaskStore, PushNotificationStore,
PushNotificationSender, RequestContext, ExtendedAgentCardProvider).
Constructor now uses an options object:
// v0.3
new ServerCallContext(requestedExtensions, user);
// v1.0
new ServerCallContext({ requestedExtensions, user, tenant: 'my-tenant', requestedVersion: '1.0' });RequestContext now wraps the incoming SendMessageRequest,
and context moved from last (optional) to 4th (mandatory).
The loose userMessage parameter is replaced by request: SendMessageRequest;
agent executors read the message via ctx.userMessage (convenience accessor
guaranteed non-null) and the full payload -- including configuration and
request-level metadata -- via ctx.request:
// v0.3
new RequestContext(userMessage, taskId, contextId, task, referenceTasks, context);
// v1.0
new RequestContext(request, taskId, contextId, context, task, referenceTasks);
// Reading from an executor:
ctx.userMessage; // Message -- shorthand for ctx.request.message (non-null)
ctx.request.configuration; // SendMessageConfiguration | undefined -- newly exposed
ctx.request.metadata; // Record<string, unknown> | undefinedThe wrapped request is deep-cloned on construction so mutations inside the
executor cannot leak back to the caller's SendMessageRequest.
Events must now be wrapped with AgentEvent factories:
// v0.3 -- publish raw objects
eventBus.publish(myTask);
eventBus.publish(myMessage);
// v1.0 -- use AgentEvent factory
import { AgentEvent } from '@a2a-js/sdk/server';
eventBus.publish(AgentEvent.task(myTask));
eventBus.publish(AgentEvent.statusUpdate(myStatusUpdate));
eventBus.publish(AgentEvent.message(myMessage));
eventBus.publish(AgentEvent.artifactUpdate(myArtifact));When consuming, use event.kind and event.data on the AgentExecutionEvent
wrapper (see Section 1.2 for the pattern).
// v0.3
interface TaskStore {
save(task: Task, context?: ServerCallContext): Promise<void>;
load(taskId: string, context?: ServerCallContext): Promise<Task | undefined>;
}
// v1.0 -- context is mandatory, list() is new
interface TaskStore {
save(task: Task, context: ServerCallContext): Promise<void>;
load(taskId: string, context: ServerCallContext): Promise<Task | undefined>;
list(params: ListTasksRequest, context: ServerCallContext): Promise<ListTasksResponse>;
}InMemoryTaskStore is now tenant-scoped internally.
PushNotificationStore -- context added as 2nd parameter; type changed from
PushNotificationConfig to TaskPushNotificationConfig:
// v0.3
store.save(taskId, config);
store.load(taskId);
store.delete(taskId, configId);
// v1.0 -- context inserted as 2nd parameter
store.save(taskId, context, config);
store.load(taskId, context);
store.delete(taskId, context, configId);PushNotificationSender -- now accepts StreamResponse + context instead of
just Task:
// v0.3
interface PushNotificationSender {
send(task: Task): Promise<void>;
}
// v1.0
interface PushNotificationSender {
send(streamResponse: StreamResponse, context: ServerCallContext): Promise<void>;
}All A2ARequestHandler parameter types changed to protobuf request types (same
mappings as Section 2.2). Key changes:
setTaskPushNotificationConfig->createTaskPushNotificationConfiggetAuthenticatedExtendedAgentCard(context)->getAuthenticatedExtendedAgentCard(params, context)- New:
listTasks(params, context) - Streaming methods return
AsyncGenerator<StreamResponse>
DefaultRequestHandler constructor has a new optional 8th parameter
agentCardSignatureGenerator for agent card signing.
The SDK sends A2A-Version: 1.0 automatically on all client requests. Servers
validate the version via DefaultRequestHandler. New constants:
A2A_VERSION_HEADER, A2A_PROTOCOL_VERSION, A2A_CONTENT_TYPE.
If an AgentInterface has a tenant value, ClientFactory automatically wraps
the transport with TenantTransportDecorator. Server-side, access
context.tenant. All stores are tenant-scoped internally.
import { generateAgentCardSignature, verifyAgentCardSignature } from '@a2a-js/sdk';
const sign = generateAgentCardSignature(privateKey, { alg: 'RS256' });
const signedCard = await sign(agentCard);
const verify = verifyAgentCardSignature(async (header) => publicKey);
await verify(agentCard);| v0.3 Import | v1.0 Import |
|---|---|
import { A2AClient } from '@a2a-js/sdk/client' |
Removed -- use ClientFactory + Client |
import { TextPart, FilePart, DataPart } from '@a2a-js/sdk' |
Removed -- use Part |
import { MessageSendParams } from '@a2a-js/sdk' |
import { SendMessageRequest } from '@a2a-js/sdk' |
import { TaskQueryParams } from '@a2a-js/sdk' |
import { GetTaskRequest } from '@a2a-js/sdk' |
import { TaskIdParams } from '@a2a-js/sdk' |
import { CancelTaskRequest } from '@a2a-js/sdk' |
import { A2AError } from '@a2a-js/sdk/server' |
import { TaskNotFoundError, ... } from '@a2a-js/sdk/errors' (or @a2a-js/sdk/errors/grpc for gRPC helpers) |