chore: remove a2a-adapter and bump @a2a-js/sdk to 0.3.8 (#16800)

This commit is contained in:
Adam Weidman
2026-01-16 20:01:39 -05:00
committed by GitHub
parent e03042657b
commit a76946189a
5 changed files with 7 additions and 229 deletions
+5 -5
View File
@@ -78,9 +78,9 @@
}
},
"node_modules/@a2a-js/sdk": {
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.7.tgz",
"integrity": "sha512-1WBghkOjgiKt4rPNje8jlB9VateVQXqyjlc887bY/H8yM82Hlf0+5JW8zB98BPExKAplI5XqtXVH980J6vqi+w==",
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@a2a-js/sdk/-/sdk-0.3.8.tgz",
"integrity": "sha512-vAg6JQbhOnHTzApsB7nGzCQ9r7PuY4GMr8gt88dIR8Wc8G8RSqVTyTmFeMurgzcYrtHYXS3ru2rnDoGj9UDeSw==",
"license": "Apache-2.0",
"dependencies": {
"uuid": "^11.1.0"
@@ -18395,7 +18395,7 @@
"name": "@google/gemini-cli-a2a-server",
"version": "0.26.0-nightly.20260115.6cb3ae4e0",
"dependencies": {
"@a2a-js/sdk": "^0.3.7",
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
@@ -18810,7 +18810,7 @@
"version": "0.26.0-nightly.20260115.6cb3ae4e0",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "^0.3.7",
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
+1 -1
View File
@@ -25,7 +25,7 @@
"dist"
],
"dependencies": {
"@a2a-js/sdk": "^0.3.7",
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/storage": "^7.16.0",
"@google/gemini-cli-core": "file:../core",
"express": "^5.1.0",
+1 -1
View File
@@ -21,7 +21,7 @@
"dist"
],
"dependencies": {
"@a2a-js/sdk": "^0.3.7",
"@a2a-js/sdk": "^0.3.8",
"@google-cloud/logging": "^11.2.1",
"@google-cloud/opentelemetry-cloud-monitoring-exporter": "^0.21.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^3.0.0",
@@ -8,7 +8,6 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
A2AClientManager,
type SendMessageResult,
createAdapterFetch,
} from './a2a-client-manager.js';
import type { AgentCard, Task } from '@a2a-js/sdk';
import type { AuthenticationHandler, Client } from '@a2a-js/sdk/client';
@@ -317,90 +316,4 @@ describe('A2AClientManager', () => {
).rejects.toThrow("Agent 'NonExistentAgent' not found.");
});
});
describe('createAdapterFetch', () => {
it('normalizes TASK_STATE_ enums to lower-case', async () => {
const baseFetch = vi
.fn()
.mockResolvedValue(
new Response(
JSON.stringify({ status: { state: 'TASK_STATE_WORKING' } }),
),
);
const adapter = createAdapterFetch(baseFetch as typeof fetch);
const response = await adapter('http://example.com', {
method: 'POST',
body: '{}',
});
const data = await response.json();
expect(data.status.state).toBe('working');
});
it('lowercases non-prefixed task states', async () => {
const baseFetch = vi
.fn()
.mockResolvedValue(
new Response(JSON.stringify({ status: { state: 'WORKING' } })),
);
const adapter = createAdapterFetch(baseFetch as typeof fetch);
const response = await adapter('http://example.com', {
method: 'POST',
body: '{}',
});
const data = await response.json();
expect(data.status.state).toBe('working');
});
it('bypasses adapter for JSON-RPC requests', async () => {
const baseFetch = vi.fn().mockResolvedValue(new Response('{}'));
const adapter = createAdapterFetch(baseFetch as typeof fetch);
const rpcBody = JSON.stringify({ jsonrpc: '2.0', method: 'foo' });
await adapter('http://example.com', {
method: 'POST',
body: rpcBody,
});
// Verify baseFetch was called with original body, not modified
expect(baseFetch).toHaveBeenCalledWith(
'http://example.com',
expect.objectContaining({ body: rpcBody }),
);
});
it('applies dialect translation for remote REST requests', async () => {
const baseFetch = vi.fn().mockResolvedValue(new Response('{}'));
const adapter = createAdapterFetch(baseFetch as typeof fetch);
const originalBody = JSON.stringify({
message: {
role: 'user',
parts: [{ kind: 'text', text: 'hi' }],
},
});
await adapter('https://remote-agent.com/v1/message:send', {
method: 'POST',
body: originalBody,
});
// Verify body WAS modified:
// 1. role: 'user' -> 'ROLE_USER'
// 2. parts mapped to content, kind stripped
const expectedBody = JSON.stringify({
message: {
role: 'ROLE_USER',
content: [{ text: 'hi' }],
},
});
expect(baseFetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({ body: expectedBody }),
);
});
});
});
@@ -73,10 +73,6 @@ export class A2AClientManager {
fetchImpl = createAuthenticatingFetchWithRetry(fetch, authHandler);
}
// Wrap with custom adapter for ADK Reasoning Engine compatibility
// TODO: Remove this when a2a-js fixes compatibility
fetchImpl = createAdapterFetch(fetchImpl);
const resolver = new DefaultAgentCardResolver({ fetchImpl });
const options = ClientFactoryOptions.createFrom(
@@ -220,134 +216,3 @@ export class A2AClientManager {
}
}
}
/**
* Maps TaskState proto-JSON enums to lower-case strings.
*/
function mapTaskState(state: string | undefined): string | undefined {
if (!state) return state;
if (state.startsWith('TASK_STATE_')) {
return state.replace('TASK_STATE_', '').toLowerCase();
}
return state.toLowerCase();
}
/**
* Creates a fetch implementation that adapts standard A2A SDK requests to the
* proto-JSON dialect and endpoint shapes required by Vertex AI Agent Engine.
*/
export function createAdapterFetch(baseFetch: typeof fetch): typeof fetch {
return async (
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> => {
const body = init?.body;
// Protocol Detection
// JSON-RPC requests bypass the adapter as they are standard-compliant and
// don't require the dialect translation intended for Vertex AI REST bindings.
// This logic can be removed when a2a-js/sdk is fully compliant.
let effectiveBody = body;
if (typeof body === 'string') {
try {
const jsonBody = JSON.parse(body);
// If the SDK decided to use JSON-RPC, we bypass the adapter because
// JSON-RPC requests are correctly supported in a2a-js/sdk.
if (jsonBody.jsonrpc === '2.0') {
return await baseFetch(input, init);
}
// Dialect Mapping (REST / HTTP+JSON)
// Apply translation for Vertex AI Agent Engine compatibility.
const message = jsonBody.message || jsonBody;
if (message && typeof message === 'object') {
// Role: user -> ROLE_USER, agent/model -> ROLE_AGENT
if (message.role === 'user') message.role = 'ROLE_USER';
if (message.role === 'agent' || message.role === 'model') {
message.role = 'ROLE_AGENT';
}
// Strip SDK-specific 'kind' field
delete message.kind;
// Map 'parts' to 'content' (Proto-JSON dialect often uses 'content' or typed parts)
// Also strip 'kind' from parts.
if (Array.isArray(message.parts)) {
message.content = message.parts.map(
(p: { kind?: string; text?: string }) => {
const { kind: _k, ...rest } = p;
// If it's a simple text part, ensure it matches { text: "..." }
if (p.kind === 'text') return { text: p.text };
return rest;
},
);
delete message.parts;
}
}
effectiveBody = JSON.stringify(jsonBody);
} catch (error) {
debugLogger.debug(
'[A2AClientManager] Failed to parse request body for dialect translation:',
error,
);
}
}
const response = await baseFetch(input, { ...init, body: effectiveBody });
if (response.ok) {
try {
const responseData = await response.clone().json();
const result =
responseData.task || responseData.message || responseData;
// Restore 'kind' for the SDK and a2aUtils parsing
if (result && typeof result === 'object' && !result.kind) {
if (responseData.task || (result.id && result.status)) {
result.kind = 'task';
} else if (responseData.message || result.messageId) {
result.kind = 'message';
}
}
// Restore 'kind' on parts so extractMessageText works
if (result?.parts && Array.isArray(result.parts)) {
for (const part of result.parts) {
if (!part.kind) {
if (part.file) part.kind = 'file';
else if (part.data) part.kind = 'data';
else if (part.text) part.kind = 'text';
}
}
}
// Recursively restore 'kind' on artifact parts
if (result?.artifacts && Array.isArray(result.artifacts)) {
for (const artifact of result.artifacts) {
if (artifact.parts && Array.isArray(artifact.parts)) {
for (const part of artifact.parts) {
if (!part.kind) {
if (part.file) part.kind = 'file';
else if (part.data) part.kind = 'data';
else if (part.text) part.kind = 'text';
}
}
}
}
}
// Map Task States back to SDK expectations
if (result && typeof result === 'object' && result.status) {
result.status.state = mapTaskState(result.status.state);
}
return new Response(JSON.stringify(result), response);
} catch (_e) {
// Non-JSON response or unwrapping failure
}
}
return response;
};
}