Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 42 additions & 18 deletions docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,10 @@ tenant-prefixed routes (`/:tenant/tasks/:taskId`, etc.) and validates the

### 3.2 Error Classes Replaced

The monolithic `A2AError` class with static factory methods is removed.
Use specific error classes:
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`, …) — and
live at the SDK root, not under `/client` or `/server`:

```typescript
// v0.3
Expand All @@ -277,18 +279,40 @@ throw A2AError.taskNotFound('task-1');
throw A2AError.invalidParams('bad input');

// v1.0
import { TaskNotFoundError, RequestMalformedError } from '@a2a-js/sdk/server';
throw new TaskNotFoundError('task-1');
throw new RequestMalformedError('bad input');
import { TaskNotFoundError, RequestMalformedError } from '@a2a-js/sdk';
throw new TaskNotFoundError({ message: 'task-1' });
throw new RequestMalformedError({ message: 'bad input' });
```

Available error classes from `@a2a-js/sdk/server`: `TaskNotFoundError`,
`TaskNotCancelableError`, `RequestMalformedError`, `UnsupportedOperationError`,
Semantic classes: `TaskNotFoundError`, `TaskNotCancelableError`,
`RequestMalformedError`, `UnsupportedOperationError`,
`PushNotificationNotSupportedError`, `ContentTypeNotSupportedError`,
`ExtendedAgentCardNotConfiguredError`, `VersionNotSupportedError`.
`InvalidAgentResponseError`, `ExtendedAgentCardNotConfiguredError`,
`ExtensionSupportRequiredError`, `VersionNotSupportedError`, `GenericError`.

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:

```typescript
import { isRestError, TaskNotFoundError } from '@a2a-js/sdk';

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']);
}
}
}
```

Error codes and gRPC/HTTP status mappings are defined in the
[spec](https://a2a-protocol.org/v1.0.0/specification/#54-error-code-mappings).
[spec](https://a2a-protocol.org/v1.0.0/specification/#54-error-code-mappings)
and live in a single registry (`A2A_ERROR_SPECS`) exported from `@a2a-js/sdk`.

### 3.3 `ServerCallContext` -- Now Mandatory

Expand All @@ -305,7 +329,7 @@ new ServerCallContext(requestedExtensions, user);
new ServerCallContext({ requestedExtensions, user, tenant: 'my-tenant', requestedVersion: '1.0' });
```

`RequestContext` now wraps the incoming `SendMessageRequest`,
`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
Expand Down Expand Up @@ -445,11 +469,11 @@ await verify(agentCard);

## 5. Import Path Changes

| 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/server'` |
| 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'` |
11 changes: 0 additions & 11 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,3 @@ export {
withA2AVersion,
} from './service-parameters.js';
export { ClientCallContext, type ContextUpdate, ClientCallContextKey } from './context.js';
export {
ExtendedAgentCardNotConfiguredError,
ContentTypeNotSupportedError,
InvalidAgentResponseError,
PushNotificationNotSupportedError,
TaskNotCancelableError,
TaskNotFoundError,
UnsupportedOperationError,
RequestMalformedError,
VersionNotSupportedError,
} from '../errors.js';
2 changes: 1 addition & 1 deletion src/client/multitransport-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { withA2AVersion } from './service-parameters.js';
import { AgentCardSignatureVerifier } from '../signature.js';
import { LEGACY_HTTP_EXTENSION_HEADER } from '../compat/v0_3/index.js';
import { HTTP_EXTENSION_HEADER } from '../constants.js';
import { PushNotificationNotSupportedError } from '../errors.js';
import { PushNotificationNotSupportedError } from '../errors/index.js';
import { isLegacyVersion } from '../version_utils.js';
import { TaskPushNotificationConfig, Task, AgentCard, SendMessageResult } from '../index.js';
import {
Expand Down
40 changes: 5 additions & 35 deletions src/client/transports/grpc/grpc_transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ import { RequestOptions } from '../../multitransport-client.js';
import { Transport, TransportFactory } from '../transport.js';
import { FromProto } from '../../../types/converters/from_proto.js';

import { A2A_REASON_TO_ERROR_CLASS, ERROR_INFO_TYPE } from '../../../errors.js';
import { decodeStatus, decodeErrorInfo } from '../../../server/grpc/error_details.js';
import { fromGrpcError } from '../../../errors/index.js';
import { LegacyGrpcTransport } from '../../../compat/v0_3/client/transports/grpc/index.js';
import { isLegacyVersion } from '../../../version_utils.js';
import { pickMatchingInterface } from '../pick_interface.js';
Expand Down Expand Up @@ -310,43 +309,14 @@ export class GrpcTransport implements Transport {
return metadata;
}

private static mapFromErrorInfo(error: grpc.ServiceError): Error | undefined {
const bin = error.metadata?.get('grpc-status-details-bin');
if (!bin || bin.length === 0) return undefined;

const raw = bin[0];
const buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw, 'binary');

const status = decodeStatus(buffer);

for (const detail of status.details) {
if (detail.typeUrl === ERROR_INFO_TYPE) {
const errorInfo = decodeErrorInfo(detail.value);

const ErrorClass = A2A_REASON_TO_ERROR_CLASS[errorInfo.reason];
if (!ErrorClass) return undefined;

return new ErrorClass(error.details);
}
}

return undefined;
}

/**
* Maps a gRPC `ServiceError` to an SDK error class via
* `google.rpc.ErrorInfo` from `grpc-status-details-bin` metadata.
* Falls back to a generic `Error` carrying the gRPC code and details
* Maps a gRPC `ServiceError` to a semantic {@link import('../../../errors/index.js').GrpcA2AError}
* via `google.rpc.ErrorInfo` from `grpc-status-details-bin`. Falls
* back to `GrpcGenericError` preserving the gRPC status and details
Comment thread
JakubWorek marked this conversation as resolved.
Outdated
* when no ErrorInfo is present.
*/
private static mapToError(error: grpc.ServiceError, method?: keyof A2AServiceClient): Error {
const fromErrorInfo = GrpcTransport.mapFromErrorInfo(error);
if (fromErrorInfo) return fromErrorInfo;

const methodContext = method ? ' for ' + String(method) : '';
return new Error('gRPC error' + methodContext + ': ' + error.code + ' ' + error.details, {
cause: error,
});
return fromGrpcError(error, method ? String(method) : undefined);
Comment thread
JakubWorek marked this conversation as resolved.
Outdated
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/client/transports/json_rpc_transport.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { JSONRPCErrorResponse, TransportProtocolName } from '../../core.js';
import { mapJsonRpcErrorToSdkError } from '../../errors.js';
import { fromJsonRpcErrorResponse as mapJsonRpcErrorToSdkError } from '../../errors/index.js';
import {
Task,
AgentCard,
Expand Down
44 changes: 22 additions & 22 deletions src/client/transports/rest_transport.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TransportProtocolName } from '../../core.js';
import { A2A_REASON_TO_ERROR_CLASS, ERROR_INFO_TYPE } from '../../errors.js';
import { fromRestErrorBody } from '../../errors/index.js';

import { SendMessageResult, A2A_PROTOCOL_VERSION, A2A_CONTENT_TYPE } from '../../index.js';
import { JSON_CONTENT_TYPE } from '../../constants.js';
Expand Down Expand Up @@ -329,15 +329,27 @@ export class RestTransport implements Transport {
// Body wasn't JSON — fall through to a generic error.
}

const transportCtx = {
statusCode: response.status,
headers: RestTransport._collectHeaders(response),
};
if (errorStatus) {
throw RestTransport.mapToError(errorStatus);
throw fromRestErrorBody(errorStatus, transportCtx);
}

throw new Error(
`HTTP error for ${path}! Status: ${response.status} ${response.statusText}. Response: ${errorBodyText}`
throw fromRestErrorBody(
{ message: `HTTP error for ${path}: ${response.status} ${response.statusText}` },
transportCtx
);
}

private static _collectHeaders(response: Response): Record<string, string> {
const out: Record<string, string> = {};
response.headers.forEach((value, key) => {
out[key] = value;
});
return out;
}

private async *_sendStreamingRequest(
path: string,
body: unknown | undefined,
Expand Down Expand Up @@ -367,11 +379,15 @@ export class RestTransport implements Transport {
);
}

const sseTransportCtx = {
statusCode: response.status,
headers: RestTransport._collectHeaders(response),
};
for await (const event of parseSseStream(response)) {
if (event.type === 'error') {
const errorData = JSON.parse(event.data) as { error?: RestErrorStatus };
if (errorData.error && typeof errorData.error === 'object') {
throw RestTransport.mapToError(errorData.error);
throw fromRestErrorBody(errorData.error, sseTransportCtx);
}
throw new Error(`SSE error event: ${JSON.stringify(errorData)}`);
}
Expand All @@ -394,22 +410,6 @@ export class RestTransport implements Transport {
);
}
}

private static mapToError(error: RestErrorStatus): Error {
const message = error.message || 'Unknown error';

if (Array.isArray(error.details)) {
const errorInfo = error.details.find((d) => d['@type'] === ERROR_INFO_TYPE);
if (errorInfo && typeof errorInfo['reason'] === 'string') {
const ErrorClass = A2A_REASON_TO_ERROR_CLASS[errorInfo['reason'] as string];
if (ErrorClass) return new ErrorClass(message);
}
}

return new Error(
`REST error: ${error.status || 'UNKNOWN'} (${error.code || 'unknown code'}) - ${message}`
);
}
}

export class RestTransportFactoryOptions {
Expand Down
40 changes: 3 additions & 37 deletions src/compat/v0_3/client/transports/grpc/grpc_transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,11 @@

import * as grpc from '@grpc/grpc-js';
import { TransportProtocolName } from '../../../../../core.js';
import {
A2A_REASON_TO_ERROR_CLASS,
ERROR_INFO_TYPE,
UnsupportedOperationError,
} from '../../../../../errors.js';
import { fromGrpcError, UnsupportedOperationError } from '../../../../../errors/index.js';
import { A2A_LEGACY_PROTOCOL_VERSION } from '../../../../../constants.js';
import type { SendMessageResult } from '../../../../../index.js';
import type { RequestOptions } from '../../../../../client/multitransport-client.js';
import type { Transport } from '../../../../../client/transports/transport.js';
import { decodeErrorInfo, decodeStatus } from '../../../../../server/grpc/error_details.js';
import {
A2AServiceClient,
type CreateTaskPushNotificationConfigRequest,
Expand Down Expand Up @@ -423,38 +418,9 @@ export class LegacyGrpcTransport implements Transport {
/**
* Decodes `google.rpc.ErrorInfo` from `grpc-status-details-bin` when
* present (this SDK's `legacyGrpcService` emits it); otherwise returns
* a generic `Error` preserving the gRPC code and details.
* `GrpcGenericError` preserving the gRPC code and details.
*/
private static _mapToError(error: grpc.ServiceError, method?: string): Error {
Comment thread
JakubWorek marked this conversation as resolved.
Outdated
const fromErrorInfo = LegacyGrpcTransport._mapFromErrorInfo(error);
if (fromErrorInfo) return fromErrorInfo;

const methodContext = method ? ' for ' + method : '';
return new Error('gRPC error' + methodContext + ': ' + error.code + ' ' + error.details, {
cause: error,
});
}

private static _mapFromErrorInfo(error: grpc.ServiceError): Error | undefined {
const bin = error.metadata?.get('grpc-status-details-bin');
if (!bin || bin.length === 0) return undefined;

const raw = bin[0];
const buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw, 'binary');

const status = decodeStatus(buffer);

for (const detail of status.details) {
if (detail.typeUrl === ERROR_INFO_TYPE) {
const errorInfo = decodeErrorInfo(detail.value);

const ErrorClass = A2A_REASON_TO_ERROR_CLASS[errorInfo.reason];
if (!ErrorClass) return undefined;

return new ErrorClass(error.details);
}
}

return undefined;
return fromGrpcError(error, method);
}
}
8 changes: 4 additions & 4 deletions src/compat/v0_3/client/transports/json_rpc_transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import { JSON_CONTENT_TYPE } from '../../../../constants.js';
import type { JSONRPCErrorResponse, TransportProtocolName } from '../../../../core.js';
import {
A2A_ERROR_CODE,
fromJsonRpcErrorResponse as mapJsonRpcErrorToSdkError,
InvalidAgentResponseError,
JSONRPCTransportError,
mapJsonRpcErrorToSdkError,
} from '../../../../errors.js';
JsonRpcTransportError,
} from '../../../../errors/index.js';
import type { SendMessageResult } from '../../../../index.js';
import type { RequestOptions } from '../../../../client/multitransport-client.js';
import { Transport } from '../../../../client/transports/transport.js';
Expand Down Expand Up @@ -216,7 +216,7 @@ export class LegacyJsonRpcTransport implements Transport {
_params: V1ListTasksRequest,
_options?: RequestOptions
): Promise<V1ListTasksResponse> {
throw new JSONRPCTransportError({
throw new JsonRpcTransportError({
jsonrpc: '2.0',
id: null,
error: {
Expand Down
28 changes: 17 additions & 11 deletions src/compat/v0_3/client/transports/rest_transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
*/

import {
A2A_ERROR_CLASSES,
A2A_ERROR_SPECS_BY_CODE,
InvalidAgentResponseError,
mapA2aErrorToSdkError,
UnsupportedOperationError,
} from '../../../../errors.js';
} from '../../../../errors/index.js';
import type { TransportProtocolName } from '../../../../core.js';
import type { SendMessageResult } from '../../../../index.js';
import type { RequestOptions } from '../../../../client/multitransport-client.js';
Expand Down Expand Up @@ -421,10 +422,7 @@ export class LegacyRestTransport implements Transport {
try {
const parsed = JSON.parse(jsonData) as unknown;
if (LegacyRestTransport._isLegacyRestErrorBody(parsed)) {
return mapA2aErrorToSdkError(parsed, () => {
const dataSuffix = parsed.data ? ` Data: ${JSON.stringify(parsed.data)}` : '';
return new Error(`REST error: ${parsed.message} (Code: ${parsed.code})${dataSuffix}`);
});
return LegacyRestTransport._errorFromLegacyBody(parsed);
}
return new Error(`SSE error event: ${jsonData}`);
} catch {
Expand All @@ -450,18 +448,26 @@ export class LegacyRestTransport implements Transport {
}

if (errorBody) {
const body = errorBody;
throw mapA2aErrorToSdkError(body, () => {
const dataSuffix = body.data ? ` Data: ${JSON.stringify(body.data)}` : '';
return new Error(`REST error: ${body.message} (Code: ${body.code})${dataSuffix}`);
});
throw LegacyRestTransport._errorFromLegacyBody(errorBody);
}

throw new Error(
`HTTP error for ${path}! Status: ${response.status} ${response.statusText}. Response: ${errorBodyText}`
);
}

/**
* Reconstructs a semantic SDK error from a v0.3 error body. Unknown
* codes fall through to a generic `Error` preserving the code/data
* in the message for debugging.
*/
private static _errorFromLegacyBody(body: LegacyRestErrorBody): Error {
const spec = A2A_ERROR_SPECS_BY_CODE[body.code];
if (spec) return new A2A_ERROR_CLASSES[spec.name]({ message: body.message });
const dataSuffix = body.data ? ` Data: ${JSON.stringify(body.data)}` : '';
return new Error(`REST error: ${body.message} (Code: ${body.code})${dataSuffix}`);
}

private static _isLegacyRestErrorBody(value: unknown): value is LegacyRestErrorBody {
return (
typeof value === 'object' &&
Expand Down
Loading
Loading