Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
86cb85e
wip
guglielmo-san Dec 16, 2025
8f549ae
wip
guglielmo-san Dec 16, 2025
9b60ee7
wip
guglielmo-san Dec 17, 2025
ed3c51f
wip
guglielmo-san Dec 17, 2025
9cbf7de
wip
guglielmo-san Dec 17, 2025
5bb7a83
wip
guglielmo-san Dec 17, 2025
fd6e0c0
wip
guglielmo-san Dec 18, 2025
583319d
wip
guglielmo-san Dec 18, 2025
16d00dd
wip
guglielmo-san Dec 18, 2025
926cb78
wip
guglielmo-san Dec 18, 2025
4c6a043
wip
guglielmo-san Dec 18, 2025
7df1553
wip tests
guglielmo-san Dec 18, 2025
2e531f3
wip tests
guglielmo-san Dec 18, 2025
91f5b63
fix tests
guglielmo-san Dec 18, 2025
21669ab
revert modification on sample
guglielmo-san Dec 18, 2025
343af4e
Update src/server/grpc/grpc_handler.ts
guglielmo-san Dec 18, 2025
7f8f0f1
Update src/server/grpc/grpc_handler.ts
guglielmo-san Dec 18, 2025
7529971
run linter
guglielmo-san Dec 18, 2025
b92d070
fix typo
guglielmo-san Dec 19, 2025
b0b27b2
Merge branch 'main' into guglielmoc/ts-proto-for-grpc
guglielmo-san Dec 19, 2025
a467720
update error types
guglielmo-san Dec 19, 2025
24184a1
regenerate package lock
guglielmo-san Dec 19, 2025
e5b3bd2
split proto utils
guglielmo-san Dec 19, 2025
4385e1b
fix tests
guglielmo-san Dec 19, 2025
2f10f81
run linter
guglielmo-san Dec 19, 2025
8b9d93e
Merge branch 'main' into guglielmoc/ts-proto-for-grpc
guglielmo-san Dec 19, 2025
ad9a917
fix typo
guglielmo-san Dec 19, 2025
55cdd36
add tests
guglielmo-san Dec 19, 2025
16acf39
exclude edge tests
guglielmo-san Dec 19, 2025
b280877
update proto converters
guglielmo-san Dec 19, 2025
8894059
run linter
guglielmo-san Dec 19, 2025
a70768c
fix tests
guglielmo-san Dec 19, 2025
ed73c01
generate grpc type with buf
guglielmo-san Dec 19, 2025
e479531
add package dependency
guglielmo-san Dec 19, 2025
96c2e00
updates to proto converters
guglielmo-san Dec 19, 2025
af5f536
wip tck
guglielmo-san Dec 19, 2025
12cc825
fix linter
guglielmo-san Dec 19, 2025
3e0122f
improve error format
guglielmo-san Dec 19, 2025
3c18811
fix error codes
guglielmo-san Dec 21, 2025
55b6a18
added optional description to peerDependencies
guglielmo-san Dec 21, 2025
9374358
fix typo
guglielmo-san Dec 21, 2025
2ff522e
Add GRPC to run-tck.yaml
ishymko Dec 22, 2025
73c92b7
Sync package-lock.json (add peerDependenciesMeta for new packages)
ishymko Dec 22, 2025
47dfebd
Update src/server/grpc/grpc_handler.ts
guglielmo-san Dec 29, 2025
9b31a00
move tests to a new location
guglielmo-san Dec 29, 2025
0dd6d60
rename grpc transport handlers
guglielmo-san Dec 29, 2025
f5986ef
refactor from and to proto
guglielmo-san Dec 29, 2025
d04b654
revert package json samples
guglielmo-san Dec 29, 2025
31ef47a
fix typo
guglielmo-san Dec 29, 2025
f5bd2a8
create grpc userBuilder
guglielmo-san Dec 29, 2025
44f1dbe
add example for grpc handler
guglielmo-san Dec 29, 2025
929b8dd
fix tests
guglielmo-san Dec 29, 2025
ae7d2c2
fix typo
guglielmo-san Dec 29, 2025
d0e9b67
Merge branch 'main' into guglielmoc/ts-proto-for-grpc
guglielmo-san Dec 29, 2025
ca8e775
run linter
guglielmo-san Dec 29, 2025
ee204d0
change id regex
guglielmo-san Dec 29, 2025
736fa52
update regex id extraction
guglielmo-san Dec 29, 2025
1e8316f
run linter
guglielmo-san Dec 29, 2025
e60330c
wip
guglielmo-san Dec 29, 2025
350d651
wip
guglielmo-san Dec 30, 2025
09d66e2
wip
guglielmo-san Dec 30, 2025
28be6aa
wip tests
guglielmo-san Dec 30, 2025
bd46151
add tests
guglielmo-san Dec 30, 2025
3dd3d14
Update src/grpc/utils/from_proto.ts
guglielmo-san Dec 30, 2025
97b61e4
fix tests
guglielmo-san Dec 30, 2025
5463614
exclude tests
guglielmo-san Dec 30, 2025
fc451d0
remove legacy converters
guglielmo-san Jan 14, 2026
12b4e85
Merge branch 'main' into guglielmoc/ts-proto-for-grpc-client
guglielmo-san Jan 15, 2026
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"lint:fix": "npx eslint . --fix",
"coverage": "vitest run --coverage",
"generate": "curl https://raw.githubusercontent.com/google-a2a/A2A/refs/heads/main/specification/json/a2a.json > spec.json && node scripts/generateTypes.js && rm spec.json",
"generate-grpc-types": "cd ./src/grpc && curl -o ./a2a.proto https://raw.githubusercontent.com/a2aproject/A2A/v0.3.0/specification/grpc/a2a.proto && buf generate && rm ./a2a.proto",
"test-build": "esbuild ./dist/client/index.js ./dist/server/index.js ./dist/index.js --bundle --platform=neutral --outdir=dist/tmp-checks --outbase=./dist"
},
"dependencies": {
Expand Down
354 changes: 354 additions & 0 deletions src/client/transports/grpc_transport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,354 @@
import {
CallOptions,
credentials,
ServiceError,
Metadata,
ClientUnaryCall,
ClientReadableStream,
ChannelCredentials,
} from '@grpc/grpc-js';
import { TransportProtocolName } from '../../core.js';
import { A2AServiceClient } from '../../grpc/a2a.js';
import {
MessageSendParams,
TaskPushNotificationConfig,
TaskIdParams,
ListTaskPushNotificationConfigParams,
DeleteTaskPushNotificationConfigParams,
TaskQueryParams,
Task,
AgentCard,
GetTaskPushNotificationConfigParams,
} from '../../types.js';
import { A2AStreamEventData, SendMessageResult } from '../client.js';
import { RequestOptions } from '../multitransport-client.js';
import { Transport, TransportFactory } from './transport.js';
import { ToProto } from '../../grpc/utils/to_proto.js';

Check failure on line 26 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / eslint

Cannot find module '../../grpc/utils/to_proto.js' or its corresponding type declarations.

Check failure on line 26 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / coverage

test/client/transports/grpc_transport.spec.ts

Error: Cannot find module '../../grpc/utils/to_proto.js' imported from '/home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts' ❯ src/client/transports/grpc_transport.ts:26:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Caused by: Error: Failed to load url ../../grpc/utils/to_proto.js (resolved id: ../../grpc/utils/to_proto.js) in /home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts. Does the file exist? ❯ loadAndTransform node_modules/vite/dist/node/chunks/config.js:22662:33

Check failure on line 26 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / test (20)

test/client/transports/grpc_transport.spec.ts

Error: Cannot find module '../../grpc/utils/to_proto.js' imported from '/home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts' ❯ src/client/transports/grpc_transport.ts:26:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Caused by: Error: Failed to load url ../../grpc/utils/to_proto.js (resolved id: ../../grpc/utils/to_proto.js) in /home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts. Does the file exist? ❯ loadAndTransform node_modules/vite/dist/node/chunks/config.js:22662:33

Check failure on line 26 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / test (22)

test/client/transports/grpc_transport.spec.ts

Error: Cannot find module '../../grpc/utils/to_proto.js' imported from '/home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts' ❯ src/client/transports/grpc_transport.ts:26:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Caused by: Error: Failed to load url ../../grpc/utils/to_proto.js (resolved id: ../../grpc/utils/to_proto.js) in /home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts. Does the file exist? ❯ loadAndTransform node_modules/vite/dist/node/chunks/config.js:22662:33

Check failure on line 26 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / test (24)

test/client/transports/grpc_transport.spec.ts

Error: Cannot find module '../../grpc/utils/to_proto.js' imported from '/home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts' ❯ src/client/transports/grpc_transport.ts:26:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Caused by: Error: Failed to load url ../../grpc/utils/to_proto.js (resolved id: ../../grpc/utils/to_proto.js) in /home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts. Does the file exist? ❯ loadAndTransform node_modules/vite/dist/node/chunks/config.js:22662:33

Check failure on line 26 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / test (18)

test/client/transports/grpc_transport.spec.ts

Error: Cannot find module '../../grpc/utils/to_proto.js' imported from '/home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts' ❯ src/client/transports/grpc_transport.ts:26:1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' } Caused by: Caused by: Error: Failed to load url ../../grpc/utils/to_proto.js (resolved id: ../../grpc/utils/to_proto.js) in /home/runner/work/a2a-js/a2a-js/src/client/transports/grpc_transport.ts. Does the file exist? ❯ loadAndTransform node_modules/vite/dist/node/chunks/config.js:22662:33
import { FromProto } from '../../grpc/utils/from_proto.js';

Check failure on line 27 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / eslint

Cannot find module '../../grpc/utils/from_proto.js' or its corresponding type declarations.
import {
A2A_ERROR_CODE,
AuthenticatedExtendedCardNotConfiguredError,
ContentTypeNotSupportedError,
InvalidAgentResponseError,
PushNotificationNotSupportedError,
TaskNotFoundError,
TaskNotCancelableError,
UnsupportedOperationError,
} from '../../errors.js';

type GrpcUnaryCall<TReq, TRes> = (
request: TReq,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: TRes) => void
) => ClientUnaryCall;

type GrpcStreamCall<TReq, TRes> = (
request: TReq,
metadata?: Metadata,
options?: Partial<CallOptions>
) => ClientReadableStream<TRes>;

export interface GrpcTransportOptions {
endpoint: string;
grpcClient?: A2AServiceClient;
grpcChannelCredentials?: ChannelCredentials;
grpcCallOptions?: Partial<CallOptions>;
}

export class GrpcTransport implements Transport {
private readonly grpcCallOptions?: Partial<CallOptions>;
private readonly grpcClient: A2AServiceClient;

constructor(options: GrpcTransportOptions) {
this.grpcCallOptions = options.grpcCallOptions;
this.grpcClient =
options.grpcClient ??
new A2AServiceClient(
options.endpoint,
options.grpcChannelCredentials ?? credentials.createInsecure()
);
}

async getExtendedAgentCard(options?: RequestOptions): Promise<AgentCard> {
const rpcResponse = await this._sendGrpcRequest(
'getAgentCard',
undefined,
options,
this.grpcClient.getAgentCard.bind(this.grpcClient),
() => {},
FromProto.agentCard
);
return rpcResponse;

Check failure on line 82 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / eslint

Type '{}' is missing the following properties from type 'AgentCard': capabilities, defaultInputModes, defaultOutputModes, description, and 5 more.
}

async sendMessage(
params: MessageSendParams,
options?: RequestOptions
): Promise<SendMessageResult> {
const rpcResponse = await this._sendGrpcRequest(
'sendMessage',
params,
options,
this.grpcClient.sendMessage.bind(this.grpcClient),
ToProto.messageSendParams,
FromProto.sendMessageResult
);
return rpcResponse;

Check failure on line 97 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / eslint

Type 'unknown' is not assignable to type 'SendMessageResult'.
}

async *sendMessageStream(
params: MessageSendParams,
options?: RequestOptions
): AsyncGenerator<A2AStreamEventData, void, undefined> {
yield* this._sendGrpcStreamingRequest(
'sendStreamingMessage',
params,
options,
this.grpcClient.sendStreamingMessage.bind(this.grpcClient),
ToProto.messageSendParams
);
}

async setTaskPushNotificationConfig(
params: TaskPushNotificationConfig,
options?: RequestOptions
): Promise<TaskPushNotificationConfig> {
const rpcResponse = await this._sendGrpcRequest(
'createTaskPushNotificationConfig',
params,
options,
this.grpcClient.createTaskPushNotificationConfig.bind(this.grpcClient),
ToProto.taskPushNotificationConfigCreate,
FromProto.getTaskPushNoticationConfig
);
return rpcResponse;

Check failure on line 125 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / eslint

Type '{}' is missing the following properties from type 'TaskPushNotificationConfig': pushNotificationConfig, taskId
}

async getTaskPushNotificationConfig(
params: GetTaskPushNotificationConfigParams,
options?: RequestOptions
): Promise<TaskPushNotificationConfig> {
const rpcResponse = await this._sendGrpcRequest(
'getTaskPushNotificationConfig',
params,
options,
this.grpcClient.getTaskPushNotificationConfig.bind(this.grpcClient),
ToProto.getTaskPushNotificationConfigRequest,
FromProto.getTaskPushNoticationConfig
);
return rpcResponse;

Check failure on line 140 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / eslint

Type '{}' is missing the following properties from type 'TaskPushNotificationConfig': pushNotificationConfig, taskId
}

async listTaskPushNotificationConfig(
params: ListTaskPushNotificationConfigParams,
options?: RequestOptions
): Promise<TaskPushNotificationConfig[]> {
const rpcResponse = await this._sendGrpcRequest(
'listTaskPushNotificationConfig',
params,
options,
this.grpcClient.listTaskPushNotificationConfig.bind(this.grpcClient),
ToProto.listTaskPushNotificationConfigRequest,
FromProto.listTaskPushNotificationConfig
);
return rpcResponse;

Check failure on line 155 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / eslint

Type '{}' is missing the following properties from type 'TaskPushNotificationConfig[]': length, pop, push, concat, and 29 more.
}

async deleteTaskPushNotificationConfig(
params: DeleteTaskPushNotificationConfigParams,
options?: RequestOptions
): Promise<void> {
await this._sendGrpcRequest(
'deleteTaskPushNotificationConfig',
params,
options,
this.grpcClient.deleteTaskPushNotificationConfig.bind(this.grpcClient),
ToProto.deleteTaskPushNotificationConfigRequest,
() => {}
);
}

async getTask(params: TaskQueryParams, options?: RequestOptions): Promise<Task> {
const rpcResponse = await this._sendGrpcRequest(
'getTask',
params,
options,
this.grpcClient.getTask.bind(this.grpcClient),
ToProto.getTaskRequest,
FromProto.task
);
return rpcResponse;

Check failure on line 181 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / eslint

Type '{}' is missing the following properties from type 'Task': contextId, id, kind, status
}

async cancelTask(params: TaskIdParams, options?: RequestOptions): Promise<Task> {
const rpcResponse = await this._sendGrpcRequest(
'cancelTask',
params,
options,
this.grpcClient.cancelTask.bind(this.grpcClient),
ToProto.cancelTaskRequest,
FromProto.task
);
return rpcResponse;

Check failure on line 193 in src/client/transports/grpc_transport.ts

View workflow job for this annotation

GitHub Actions / eslint

Type '{}' is missing the following properties from type 'Task': contextId, id, kind, status
}

async *resubscribeTask(
params: TaskIdParams,
options?: RequestOptions
): AsyncGenerator<A2AStreamEventData, void, undefined> {
yield* this._sendGrpcStreamingRequest(
'taskSubscription',
params,
options,
this.grpcClient.taskSubscription.bind(this.grpcClient),
ToProto.taskIdParams
);
}

private async _sendGrpcRequest<TReq, TRes, TParams, TResponse>(
method: keyof A2AServiceClient,
params: TParams,
options: RequestOptions | undefined,
call: GrpcUnaryCall<TReq, TRes>,
parser: (req: TParams) => TReq,
converter: (res: TRes) => TResponse
): Promise<TResponse> {
return new Promise((resolve, reject) => {
call(
parser(params),
this._buildMetadata(options),
this.grpcCallOptions ?? {},
(error, response) => {
if (error) {
if (this.isA2AServiceError(error)) {
return reject(GrpcTransport.mapToError(error));
}
const statusInfo = 'code' in error ? `(Status: ${error.code})` : '';
return reject(
new Error(`GRPC error for ${String(method)}! ${statusInfo} ${error.message}`, {
cause: error,
})
);
}
resolve(converter(response));
}
);
});
}

private async *_sendGrpcStreamingRequest<TReq, TRes, TParams>(
method: 'sendStreamingMessage' | 'taskSubscription',
params: TParams,
options: RequestOptions | undefined,
call: GrpcStreamCall<TReq, TRes>,
parser: (req: TParams) => TReq
): AsyncGenerator<A2AStreamEventData, void, undefined> {
const streamResponse = call(
parser(params),
this._buildMetadata(options),
this.grpcCallOptions ?? {}
);
try {
for await (const response of streamResponse) {
const payload = response.payload;
switch (payload.$case) {
case 'msg':
yield FromProto.message(payload.value);
break;
case 'task':
yield FromProto.task(payload.value);
break;
case 'statusUpdate':
yield FromProto.taskStatusUpdate(payload.value);
break;
case 'artifactUpdate':
yield FromProto.taskArtifactUpdate(payload.value);
break;
}
}
} catch (error) {
if (this.isServiceError(error)) {
if (this.isA2AServiceError(error)) {
throw GrpcTransport.mapToError(error);
}
throw new Error(`GRPC error for ${String(method)}! ${error.code} ${error.message}`, {
cause: error,
});
} else {
throw error;
}
} finally {
streamResponse.cancel();
}
}

private isA2AServiceError(error: ServiceError): boolean {
return (
typeof error === 'object' && error !== null && error.metadata?.get('a2a-error').length === 1
);
}

private isServiceError(error: unknown): error is ServiceError {
return typeof error === 'object' && error !== null && 'code' in error;
}

private _buildMetadata(options?: RequestOptions): Metadata {
const metadata = new Metadata();
if (options?.serviceParameters) {
for (const [key, value] of Object.entries(options.serviceParameters)) {
metadata.set(key, value);
}
}
return metadata;
}

private static mapToError(error: ServiceError): Error {
const a2aErrorCode = error.metadata.get('a2a-error');
switch (Number(a2aErrorCode[0])) {
case A2A_ERROR_CODE.TASK_NOT_FOUND:
return new TaskNotFoundError(error.message);
case A2A_ERROR_CODE.TASK_NOT_CANCELABLE:
return new TaskNotCancelableError(error.message);
case A2A_ERROR_CODE.PUSH_NOTIFICATION_NOT_SUPPORTED:
return new PushNotificationNotSupportedError(error.message);
case A2A_ERROR_CODE.UNSUPPORTED_OPERATION:
return new UnsupportedOperationError(error.message);
case A2A_ERROR_CODE.CONTENT_TYPE_NOT_SUPPORTED:
return new ContentTypeNotSupportedError(error.message);
case A2A_ERROR_CODE.INVALID_AGENT_RESPONSE:
return new InvalidAgentResponseError(error.message);
case A2A_ERROR_CODE.AUTHENTICATED_EXTENDED_CARD_NOT_CONFIGURED:
return new AuthenticatedExtendedCardNotConfiguredError(error.message);
default:
return new Error(
`GRPC error: ${error.message} Code: ${error.code} Details: ${error.details}`
);
}
}
}

export class GrpcTransportFactoryOptions {
grpcClient?: A2AServiceClient;
grpcChannelCredentials?: ChannelCredentials;
grpcCallOptions?: Partial<CallOptions>;
}

export class GrpcTransportFactory implements TransportFactory {
public static readonly name: TransportProtocolName = 'GRPC';

constructor(private readonly options?: GrpcTransportFactoryOptions) {}

get protocolName(): string {
return GrpcTransportFactory.name;
}

async create(url: string, _agentCard: AgentCard): Promise<Transport> {
return new GrpcTransport({
endpoint: url,
grpcClient: this.options?.grpcClient,
grpcChannelCredentials: this.options?.grpcChannelCredentials,
grpcCallOptions: this.options?.grpcCallOptions,
});
}
}
Loading
Loading