diff --git a/package-lock.json b/package-lock.json index 7d487301e1..8ef2b61688 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/packages/a2a-server/package.json b/packages/a2a-server/package.json index d8d42b3cc8..fa9ebc9ee3 100644 --- a/packages/a2a-server/package.json +++ b/packages/a2a-server/package.json @@ -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", diff --git a/packages/core/package.json b/packages/core/package.json index c07bde995f..ba3c80850e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/agents/a2a-client-manager.test.ts b/packages/core/src/agents/a2a-client-manager.test.ts index 6d6561c963..2f653ba176 100644 --- a/packages/core/src/agents/a2a-client-manager.test.ts +++ b/packages/core/src/agents/a2a-client-manager.test.ts @@ -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 }), - ); - }); - }); }); diff --git a/packages/core/src/agents/a2a-client-manager.ts b/packages/core/src/agents/a2a-client-manager.ts index 97355eef06..82adf2653c 100644 --- a/packages/core/src/agents/a2a-client-manager.ts +++ b/packages/core/src/agents/a2a-client-manager.ts @@ -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 => { - 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; - }; -}