diff --git a/package-lock.json b/package-lock.json index e0ccdf84fc..2d8f7d43d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,6 +105,15 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/@agentclientprotocol/sdk": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.11.0.tgz", + "integrity": "sha512-hngnMwQ13DCC7oEr0BUnrx+vTDFf/ToCLhF0YcCMWRs+v4X60rKQyAENsx0PdbQF21jC1VjMFkh2+vwNBLh6fQ==", + "license": "Apache-2.0", + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/@alcalzone/ansi-tokenize": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.2.tgz", @@ -2427,6 +2436,7 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2607,6 +2617,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2640,6 +2651,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -3008,6 +3020,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.0.1.tgz", "integrity": "sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -3041,6 +3054,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.0.1.tgz", "integrity": "sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1" @@ -3093,6 +3107,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.0.1.tgz", "integrity": "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", @@ -4303,6 +4318,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4590,6 +4606,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -5513,6 +5530,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5948,8 +5966,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.9", @@ -7213,7 +7230,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "5.2.1" }, @@ -8229,6 +8245,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8818,7 +8835,6 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -8828,7 +8844,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -8838,7 +8853,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -9092,7 +9106,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "license": "MIT", - "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", @@ -9111,7 +9124,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -9120,15 +9132,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/finalhandler/node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8" } @@ -10340,6 +10350,7 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.6.tgz", "integrity": "sha512-QHl6l1cl3zPCaRMzt9TUbTX6Q5SzvkGEZDDad0DmSf5SPmT1/90k6pGPejEvDCJprkitwObXpPaTWGHItqsy4g==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.0.0", @@ -13447,8 +13458,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/path-type": { "version": "3.0.0", @@ -14019,6 +14029,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -14029,6 +14040,7 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -16247,6 +16259,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16411,7 +16424,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -16419,6 +16433,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16603,6 +16618,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16765,7 +16781,6 @@ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4.0" } @@ -16821,6 +16836,7 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16937,6 +16953,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16950,6 +16967,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -17656,6 +17674,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -17984,6 +18003,7 @@ "version": "0.21.0-nightly.20251216.bb0c0d8ee", "license": "Apache-2.0", "dependencies": { + "@agentclientprotocol/sdk": "^0.11.0", "@google/gemini-cli-core": "file:../core", "@google/genai": "1.30.0", "@iarna/toml": "^2.2.5", @@ -18219,6 +18239,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/cli/package.json b/packages/cli/package.json index c55a43247b..9c7776377b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,6 +29,7 @@ "sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.21.0-nightly.20251216.bb0c0d8ee" }, "dependencies": { + "@agentclientprotocol/sdk": "^0.11.0", "@google/gemini-cli-core": "file:../core", "@google/genai": "1.30.0", "@iarna/toml": "^2.2.5", diff --git a/packages/cli/src/zed-integration/acp.test.ts b/packages/cli/src/zed-integration/acp.test.ts deleted file mode 100644 index 95e22a538b..0000000000 --- a/packages/cli/src/zed-integration/acp.test.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest'; -import { - AgentSideConnection, - RequestError, - type Agent, - type Client, -} from './acp.js'; -import { type ErrorResponse } from './schema.js'; -import { type MethodHandler } from './connection.js'; -import { ReadableStream, WritableStream } from 'node:stream/web'; - -const mockConnectionConstructor = vi.hoisted(() => - vi.fn< - ( - arg1: MethodHandler, - arg2: WritableStream, - arg3: ReadableStream, - ) => { sendRequest: Mock; sendNotification: Mock } - >(() => ({ - sendRequest: vi.fn(), - sendNotification: vi.fn(), - })), -); - -vi.mock('./connection.js', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...(actual as object), - Connection: mockConnectionConstructor, - }; -}); - -describe('acp', () => { - describe('RequestError', () => { - it('should create a parse error', () => { - const error = RequestError.parseError('details'); - expect(error.code).toBe(-32700); - expect(error.message).toBe('Parse error'); - expect(error.data?.details).toBe('details'); - }); - - it('should create a method not found error', () => { - const error = RequestError.methodNotFound('details'); - expect(error.code).toBe(-32601); - expect(error.message).toBe('Method not found'); - expect(error.data?.details).toBe('details'); - }); - - it('should convert to a result', () => { - const error = RequestError.internalError('details'); - const result = error.toResult() as { error: ErrorResponse }; - expect(result.error.code).toBe(-32603); - expect(result.error.message).toBe('Internal error'); - expect(result.error.data).toEqual({ details: 'details' }); - }); - }); - - describe('AgentSideConnection', () => { - let mockAgent: Agent; - - let toAgent: WritableStream; - let fromAgent: ReadableStream; - let agentSideConnection: AgentSideConnection; - let connectionInstance: InstanceType; - - beforeEach(() => { - vi.clearAllMocks(); - - const initializeResponse = { - agentCapabilities: { loadSession: true }, - authMethods: [], - protocolVersion: 1, - }; - const newSessionResponse = { sessionId: 'session-1' }; - const loadSessionResponse = { sessionId: 'session-1' }; - - mockAgent = { - initialize: vi.fn().mockResolvedValue(initializeResponse), - newSession: vi.fn().mockResolvedValue(newSessionResponse), - loadSession: vi.fn().mockResolvedValue(loadSessionResponse), - authenticate: vi.fn(), - prompt: vi.fn(), - cancel: vi.fn(), - }; - - toAgent = new WritableStream(); - fromAgent = new ReadableStream(); - - agentSideConnection = new AgentSideConnection( - (_client: Client) => mockAgent, - toAgent, - fromAgent, - ); - - // Get the mocked Connection instance - connectionInstance = mockConnectionConstructor.mock.results[0].value; - }); - - it('should initialize Connection with the correct handler and streams', () => { - expect(mockConnectionConstructor).toHaveBeenCalledTimes(1); - expect(mockConnectionConstructor).toHaveBeenCalledWith( - expect.any(Function), - toAgent, - fromAgent, - ); - }); - - it('should call agent.initialize when Connection handler receives initialize method', async () => { - const initializeParams = { - clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }, - protocolVersion: 1, - }; - const initializeResponse = { - agentCapabilities: { loadSession: true }, - authMethods: [], - protocolVersion: 1, - }; - const handler = mockConnectionConstructor.mock.calls[0][0]; - const result = await handler('initialize', initializeParams); - - expect(mockAgent.initialize).toHaveBeenCalledWith(initializeParams); - expect(result).toEqual(initializeResponse); - }); - - it('should call agent.newSession when Connection handler receives session_new method', async () => { - const newSessionParams = { cwd: '/tmp', mcpServers: [] }; - const newSessionResponse = { sessionId: 'session-1' }; - const handler = mockConnectionConstructor.mock.calls[0][0]; - const result = await handler('session/new', newSessionParams); - - expect(mockAgent.newSession).toHaveBeenCalledWith(newSessionParams); - expect(result).toEqual(newSessionResponse); - }); - - it('should call agent.loadSession when Connection handler receives session_load method', async () => { - const loadSessionParams = { - cwd: '/tmp', - mcpServers: [], - sessionId: 'session-1', - }; - const loadSessionResponse = { sessionId: 'session-1' }; - const handler = mockConnectionConstructor.mock.calls[0][0]; - const result = await handler('session/load', loadSessionParams); - - expect(mockAgent.loadSession).toHaveBeenCalledWith(loadSessionParams); - expect(result).toEqual(loadSessionResponse); - }); - - it('should throw methodNotFound if agent.loadSession is not implemented', async () => { - mockAgent.loadSession = undefined; // Simulate not implemented - const loadSessionParams = { - cwd: '/tmp', - mcpServers: [], - sessionId: 'session-1', - }; - const handler = mockConnectionConstructor.mock.calls[0][0]; - await expect(handler('session/load', loadSessionParams)).rejects.toThrow( - RequestError.methodNotFound().message, - ); - }); - - it('should call agent.authenticate when Connection handler receives authenticate method', async () => { - const authenticateParams = { - methodId: 'test-auth-method', - }; - const handler = mockConnectionConstructor.mock.calls[0][0]; - const result = await handler('authenticate', authenticateParams); - - expect(mockAgent.authenticate).toHaveBeenCalledWith(authenticateParams); - expect(result).toBeUndefined(); - }); - - it('should call agent.prompt when Connection handler receives session_prompt method', async () => { - const promptParams = { - prompt: [{ type: 'text', text: 'hi' }], - sessionId: 'session-1', - }; - const promptResponse = { - response: [{ type: 'text', text: 'hello' }], - traceId: 'trace-1', - }; - (mockAgent.prompt as Mock).mockResolvedValue(promptResponse); - const handler = mockConnectionConstructor.mock.calls[0][0]; - const result = await handler('session/prompt', promptParams); - - expect(mockAgent.prompt).toHaveBeenCalledWith(promptParams); - expect(result).toEqual(promptResponse); - }); - - it('should call agent.cancel when Connection handler receives session_cancel method', async () => { - const cancelParams = { sessionId: 'session-1' }; - const handler = mockConnectionConstructor.mock.calls[0][0]; - const result = await handler('session/cancel', cancelParams); - - expect(mockAgent.cancel).toHaveBeenCalledWith(cancelParams); - expect(result).toBeUndefined(); - }); - - it('should throw methodNotFound for unknown methods', async () => { - const handler = mockConnectionConstructor.mock.calls[0][0]; - await expect(handler('unknown_method', {})).rejects.toThrow( - RequestError.methodNotFound().message, - ); - }); - - it('should send sessionUpdate notification via connection', async () => { - const params = { - sessionId: '123', - update: { - sessionUpdate: 'user_message_chunk' as const, - content: { type: 'text' as const, text: 'hello' }, - }, - }; - await agentSideConnection.sessionUpdate(params); - }); - - it('should send requestPermission request via connection', async () => { - const params = { - sessionId: '123', - toolCall: { - toolCallId: 'tool-1', - title: 'Test Tool', - kind: 'other' as const, - status: 'pending' as const, - }, - options: [ - { - optionId: 'option-1', - name: 'Allow', - kind: 'allow_once' as const, - }, - ], - }; - const response = { - outcome: { outcome: 'selected', optionId: 'option-1' }, - }; - connectionInstance.sendRequest.mockResolvedValue(response); - - const result = await agentSideConnection.requestPermission(params); - expect(connectionInstance.sendRequest).toHaveBeenCalledWith( - 'session/request_permission', - params, - ); - expect(result).toEqual(response); - }); - - it('should send readTextFile request via connection', async () => { - const params = { path: '/a/b.txt', sessionId: 'session-1' }; - const response = { content: 'file content' }; - connectionInstance.sendRequest.mockResolvedValue(response); - - const result = await agentSideConnection.readTextFile(params); - expect(connectionInstance.sendRequest).toHaveBeenCalledWith( - 'fs/read_text_file', - params, - ); - expect(result).toEqual(response); - }); - - it('should send writeTextFile request via connection', async () => { - const params = { - path: '/a/b.txt', - content: 'new content', - sessionId: 'session-1', - }; - const response = { success: true }; - connectionInstance.sendRequest.mockResolvedValue(response); - - const result = await agentSideConnection.writeTextFile(params); - expect(connectionInstance.sendRequest).toHaveBeenCalledWith( - 'fs/write_text_file', - params, - ); - expect(result).toEqual(response); - }); - }); -}); diff --git a/packages/cli/src/zed-integration/acp.ts b/packages/cli/src/zed-integration/acp.ts deleted file mode 100644 index 16fe12403b..0000000000 --- a/packages/cli/src/zed-integration/acp.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -/* ACP defines a schema for a simple (experimental) JSON-RPC protocol that allows GUI applications to interact with agents. */ - -import * as schema from './schema.js'; -export * from './schema.js'; - -import type { WritableStream, ReadableStream } from 'node:stream/web'; -import { Connection, RequestError } from './connection.js'; -export { RequestError }; - -export class AgentSideConnection implements Client { - #connection: Connection; - - constructor( - toAgent: (conn: Client) => Agent, - input: WritableStream, - output: ReadableStream, - ) { - const agent = toAgent(this); - - const handler = async ( - method: string, - params: unknown, - ): Promise => { - switch (method) { - case schema.AGENT_METHODS.initialize: { - const validatedParams = schema.initializeRequestSchema.parse(params); - return agent.initialize(validatedParams); - } - case schema.AGENT_METHODS.session_new: { - const validatedParams = schema.newSessionRequestSchema.parse(params); - return agent.newSession(validatedParams); - } - case schema.AGENT_METHODS.session_load: { - if (!agent.loadSession) { - throw RequestError.methodNotFound(); - } - const validatedParams = schema.loadSessionRequestSchema.parse(params); - return agent.loadSession(validatedParams); - } - case schema.AGENT_METHODS.authenticate: { - const validatedParams = - schema.authenticateRequestSchema.parse(params); - return agent.authenticate(validatedParams); - } - case schema.AGENT_METHODS.session_prompt: { - const validatedParams = schema.promptRequestSchema.parse(params); - return agent.prompt(validatedParams); - } - case schema.AGENT_METHODS.session_cancel: { - const validatedParams = schema.cancelNotificationSchema.parse(params); - return agent.cancel(validatedParams); - } - default: - throw RequestError.methodNotFound(method); - } - }; - - this.#connection = new Connection(handler, input, output); - } - - /** - * Streams new content to the client including text, tool calls, etc. - */ - async sessionUpdate(params: schema.SessionNotification): Promise { - return this.#connection.sendNotification( - schema.CLIENT_METHODS.session_update, - params, - ); - } - - /** - * Request permission before running a tool - * - * The agent specifies a series of permission options with different granularity, - * and the client returns the chosen one. - */ - async requestPermission( - params: schema.RequestPermissionRequest, - ): Promise { - return this.#connection.sendRequest( - schema.CLIENT_METHODS.session_request_permission, - params, - ); - } - - async readTextFile( - params: schema.ReadTextFileRequest, - ): Promise { - return this.#connection.sendRequest( - schema.CLIENT_METHODS.fs_read_text_file, - params, - ); - } - - async writeTextFile( - params: schema.WriteTextFileRequest, - ): Promise { - return this.#connection.sendRequest( - schema.CLIENT_METHODS.fs_write_text_file, - params, - ); - } -} - -export interface Client { - requestPermission( - params: schema.RequestPermissionRequest, - ): Promise; - sessionUpdate(params: schema.SessionNotification): Promise; - writeTextFile( - params: schema.WriteTextFileRequest, - ): Promise; - readTextFile( - params: schema.ReadTextFileRequest, - ): Promise; -} - -export interface Agent { - initialize( - params: schema.InitializeRequest, - ): Promise; - newSession( - params: schema.NewSessionRequest, - ): Promise; - loadSession?( - params: schema.LoadSessionRequest, - ): Promise; - authenticate(params: schema.AuthenticateRequest): Promise; - prompt(params: schema.PromptRequest): Promise; - cancel(params: schema.CancelNotification): Promise; -} diff --git a/packages/cli/src/zed-integration/connection.test.ts b/packages/cli/src/zed-integration/connection.test.ts deleted file mode 100644 index 20bd709fca..0000000000 --- a/packages/cli/src/zed-integration/connection.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { Connection, RequestError } from './connection.js'; -import { ReadableStream, WritableStream } from 'node:stream/web'; - -describe('Connection', () => { - let toPeer: WritableStream; - let fromPeer: ReadableStream; - let peerController: ReadableStreamDefaultController; - let receivedChunks: string[] = []; - let connection: Connection; - let handler: ReturnType; - - beforeEach(() => { - receivedChunks = []; - toPeer = new WritableStream({ - write(chunk) { - const str = new TextDecoder().decode(chunk); - receivedChunks.push(str); - }, - }); - - fromPeer = new ReadableStream({ - start(controller) { - peerController = controller; - }, - }); - - handler = vi.fn(); - connection = new Connection(handler, toPeer, fromPeer); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should send a request and receive a response', async () => { - const responsePromise = connection.sendRequest('testMethod', { - key: 'value', - }); - - // Verify request was sent - await vi.waitFor(() => { - expect(receivedChunks.length).toBeGreaterThan(0); - }); - const request = JSON.parse(receivedChunks[0]); - expect(request).toMatchObject({ - jsonrpc: '2.0', - method: 'testMethod', - params: { key: 'value' }, - }); - expect(request.id).toBeDefined(); - - // Simulate response - const response = { - jsonrpc: '2.0', - id: request.id, - result: { success: true }, - }; - peerController.enqueue( - new TextEncoder().encode(JSON.stringify(response) + '\n'), - ); - - const result = await responsePromise; - expect(result).toEqual({ success: true }); - }); - - it('should send a notification', async () => { - await connection.sendNotification('notifyMethod', { key: 'value' }); - - await vi.waitFor(() => { - expect(receivedChunks.length).toBeGreaterThan(0); - }); - const notification = JSON.parse(receivedChunks[0]); - expect(notification).toMatchObject({ - jsonrpc: '2.0', - method: 'notifyMethod', - params: { key: 'value' }, - }); - expect(notification.id).toBeUndefined(); - }); - - it('should handle incoming requests', async () => { - handler.mockResolvedValue({ result: 'ok' }); - - const request = { - jsonrpc: '2.0', - id: 1, - method: 'incomingMethod', - params: { foo: 'bar' }, - }; - peerController.enqueue( - new TextEncoder().encode(JSON.stringify(request) + '\n'), - ); - - // Wait for handler to be called and response to be written - await vi.waitFor(() => { - expect(handler).toHaveBeenCalledWith('incomingMethod', { foo: 'bar' }); - expect(receivedChunks.length).toBeGreaterThan(0); - }); - - const response = JSON.parse(receivedChunks[receivedChunks.length - 1]); - expect(response).toMatchObject({ - jsonrpc: '2.0', - id: 1, - result: { result: 'ok' }, - }); - }); - - it('should handle incoming notifications', async () => { - const notification = { - jsonrpc: '2.0', - method: 'incomingNotify', - params: { foo: 'bar' }, - }; - peerController.enqueue( - new TextEncoder().encode(JSON.stringify(notification) + '\n'), - ); - - // Wait for handler to be called - await vi.waitFor(() => { - expect(handler).toHaveBeenCalledWith('incomingNotify', { foo: 'bar' }); - }); - // Notifications don't send responses - expect(receivedChunks.length).toBe(0); - }); - - it('should handle request errors from handler', async () => { - handler.mockRejectedValue(new Error('Handler failed')); - - const request = { - jsonrpc: '2.0', - id: 2, - method: 'failMethod', - }; - peerController.enqueue( - new TextEncoder().encode(JSON.stringify(request) + '\n'), - ); - - await vi.waitFor(() => { - expect(receivedChunks.length).toBeGreaterThan(0); - }); - - const response = JSON.parse(receivedChunks[receivedChunks.length - 1]); - expect(response).toMatchObject({ - jsonrpc: '2.0', - id: 2, - error: { - code: -32603, - message: 'Internal error', - data: { details: 'Handler failed' }, - }, - }); - }); - - it('should handle RequestError from handler', async () => { - handler.mockRejectedValue(RequestError.methodNotFound('Unknown method')); - - const request = { - jsonrpc: '2.0', - id: 3, - method: 'unknown', - }; - peerController.enqueue( - new TextEncoder().encode(JSON.stringify(request) + '\n'), - ); - - await vi.waitFor(() => { - expect(receivedChunks.length).toBeGreaterThan(0); - }); - - const response = JSON.parse(receivedChunks[receivedChunks.length - 1]); - expect(response).toMatchObject({ - jsonrpc: '2.0', - id: 3, - error: { - code: -32601, - message: 'Method not found', - data: { details: 'Unknown method' }, - }, - }); - }); - - it('should handle response errors', async () => { - const responsePromise = connection.sendRequest('testMethod'); - - // Verify request was sent - await vi.waitFor(() => { - expect(receivedChunks.length).toBeGreaterThan(0); - }); - const request = JSON.parse(receivedChunks[0]); - - // Simulate error response - const response = { - jsonrpc: '2.0', - id: request.id, - error: { - code: -32000, - message: 'Custom error', - }, - }; - peerController.enqueue( - new TextEncoder().encode(JSON.stringify(response) + '\n'), - ); - - await expect(responsePromise).rejects.toMatchObject({ - code: -32000, - message: 'Custom error', - }); - }); -}); diff --git a/packages/cli/src/zed-integration/connection.ts b/packages/cli/src/zed-integration/connection.ts deleted file mode 100644 index 84d0acf82f..0000000000 --- a/packages/cli/src/zed-integration/connection.ts +++ /dev/null @@ -1,231 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { z } from 'zod'; -import { coreEvents } from '@google/gemini-cli-core'; -import { type Result, type ErrorResponse } from './schema.js'; -import type { WritableStream, ReadableStream } from 'node:stream/web'; - -export class RequestError extends Error { - data?: { details?: string }; - - constructor( - public code: number, - message: string, - details?: string, - ) { - super(message); - this.name = 'RequestError'; - if (details) { - this.data = { details }; - } - } - - static parseError(details?: string): RequestError { - return new RequestError(-32700, 'Parse error', details); - } - - static invalidRequest(details?: string): RequestError { - return new RequestError(-32600, 'Invalid request', details); - } - - static methodNotFound(details?: string): RequestError { - return new RequestError(-32601, 'Method not found', details); - } - - static invalidParams(details?: string): RequestError { - return new RequestError(-32602, 'Invalid params', details); - } - - static internalError(details?: string): RequestError { - return new RequestError(-32603, 'Internal error', details); - } - - static authRequired(details?: string): RequestError { - return new RequestError(-32000, 'Authentication required', details); - } - - toResult(): Result { - return { - error: { - code: this.code, - message: this.message, - data: this.data, - }, - }; - } -} - -type AnyMessage = AnyRequest | AnyResponse | AnyNotification; - -type AnyRequest = { - jsonrpc: '2.0'; - id: string | number; - method: string; - params?: unknown; -}; - -type AnyResponse = { - jsonrpc: '2.0'; - id: string | number; -} & Result; - -type AnyNotification = { - jsonrpc: '2.0'; - method: string; - params?: unknown; -}; - -type PendingResponse = { - resolve: (response: unknown) => void; - reject: (error: ErrorResponse) => void; -}; - -export type MethodHandler = ( - method: string, - params: unknown, -) => Promise; - -export class Connection { - #pendingResponses: Map = new Map(); - #nextRequestId: number = 0; - #handler: MethodHandler; - #peerInput: WritableStream; - #writeQueue: Promise = Promise.resolve(); - #textEncoder: TextEncoder; - - constructor( - handler: MethodHandler, - peerInput: WritableStream, - peerOutput: ReadableStream, - ) { - this.#handler = handler; - this.#peerInput = peerInput; - this.#textEncoder = new TextEncoder(); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.#receive(peerOutput); - } - - async #receive(output: ReadableStream) { - let content = ''; - const decoder = new TextDecoder(); - for await (const chunk of output) { - content += decoder.decode(chunk, { stream: true }); - const lines = content.split('\n'); - content = lines.pop() || ''; - - for (const line of lines) { - const trimmedLine = line.trim(); - - if (trimmedLine) { - const message = JSON.parse(trimmedLine); - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.#processMessage(message); - } - } - } - } - - async #processMessage(message: AnyMessage) { - if ('method' in message && 'id' in message) { - // It's a request - const response = await this.#tryCallHandler( - message.method, - message.params, - ); - - await this.#sendMessage({ - jsonrpc: '2.0', - id: message.id, - ...response, - }); - } else if ('method' in message) { - // It's a notification - await this.#tryCallHandler(message.method, message.params); - } else if ('id' in message) { - // It's a response - this.#handleResponse(message); - } - } - - async #tryCallHandler( - method: string, - params?: unknown, - ): Promise> { - try { - const result = await this.#handler(method, params); - return { result: result ?? null }; - } catch (error: unknown) { - if (error instanceof RequestError) { - return error.toResult(); - } - - if (error instanceof z.ZodError) { - return RequestError.invalidParams( - JSON.stringify(error.format(), undefined, 2), - ).toResult(); - } - - let details; - - if (error instanceof Error) { - details = error.message; - } else if ( - typeof error === 'object' && - error != null && - 'message' in error && - typeof error.message === 'string' - ) { - details = error.message; - } - - return RequestError.internalError(details).toResult(); - } - } - - #handleResponse(response: AnyResponse) { - const pendingResponse = this.#pendingResponses.get(response.id); - if (pendingResponse) { - if ('result' in response) { - pendingResponse.resolve(response.result); - } else if ('error' in response) { - pendingResponse.reject(response.error); - } - this.#pendingResponses.delete(response.id); - } - } - - async sendRequest(method: string, params?: Req): Promise { - const id = this.#nextRequestId++; - const responsePromise = new Promise((resolve, reject) => { - this.#pendingResponses.set(id, { resolve, reject }); - }); - await this.#sendMessage({ jsonrpc: '2.0', id, method, params }); - return responsePromise as Promise; - } - - async sendNotification(method: string, params?: N): Promise { - await this.#sendMessage({ jsonrpc: '2.0', method, params }); - } - - async #sendMessage(json: AnyMessage) { - const content = JSON.stringify(json) + '\n'; - this.#writeQueue = this.#writeQueue - .then(async () => { - const writer = this.#peerInput.getWriter(); - try { - await writer.write(this.#textEncoder.encode(content)); - } finally { - writer.releaseLock(); - } - }) - .catch((error) => { - // Continue processing writes on error - coreEvents.emitFeedback('error', 'ACP write error.', error); - }); - return this.#writeQueue; - } -} diff --git a/packages/cli/src/zed-integration/fileSystemService.test.ts b/packages/cli/src/zed-integration/fileSystemService.test.ts index 60462458d3..66624d5449 100644 --- a/packages/cli/src/zed-integration/fileSystemService.test.ts +++ b/packages/cli/src/zed-integration/fileSystemService.test.ts @@ -6,21 +6,21 @@ import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest'; import { AcpFileSystemService } from './fileSystemService.js'; -import type { Client } from './acp.js'; +import type { AgentSideConnection } from '@agentclientprotocol/sdk'; import type { FileSystemService } from '@google/gemini-cli-core'; describe('AcpFileSystemService', () => { - let mockClient: Mocked; + let mockConnection: Mocked; let mockFallback: Mocked; let service: AcpFileSystemService; beforeEach(() => { - mockClient = { + mockConnection = { requestPermission: vi.fn(), sessionUpdate: vi.fn(), writeTextFile: vi.fn(), readTextFile: vi.fn(), - }; + } as unknown as Mocked; mockFallback = { readTextFile: vi.fn(), writeTextFile: vi.fn(), @@ -31,16 +31,14 @@ describe('AcpFileSystemService', () => { it.each([ { capability: true, - desc: 'client if capability exists', + desc: 'connection if capability exists', setup: () => { - mockClient.readTextFile.mockResolvedValue({ content: 'content' }); + mockConnection.readTextFile.mockResolvedValue({ content: 'content' }); }, verify: () => { - expect(mockClient.readTextFile).toHaveBeenCalledWith({ + expect(mockConnection.readTextFile).toHaveBeenCalledWith({ path: '/path/to/file', sessionId: 'session-1', - line: null, - limit: null, }); expect(mockFallback.readTextFile).not.toHaveBeenCalled(); }, @@ -55,12 +53,12 @@ describe('AcpFileSystemService', () => { expect(mockFallback.readTextFile).toHaveBeenCalledWith( '/path/to/file', ); - expect(mockClient.readTextFile).not.toHaveBeenCalled(); + expect(mockConnection.readTextFile).not.toHaveBeenCalled(); }, }, ])('should use $desc', async ({ capability, setup, verify }) => { service = new AcpFileSystemService( - mockClient, + mockConnection, 'session-1', { readTextFile: capability, writeTextFile: true }, mockFallback, @@ -78,9 +76,9 @@ describe('AcpFileSystemService', () => { it.each([ { capability: true, - desc: 'client if capability exists', + desc: 'connection if capability exists', verify: () => { - expect(mockClient.writeTextFile).toHaveBeenCalledWith({ + expect(mockConnection.writeTextFile).toHaveBeenCalledWith({ path: '/path/to/file', content: 'content', sessionId: 'session-1', @@ -96,12 +94,12 @@ describe('AcpFileSystemService', () => { '/path/to/file', 'content', ); - expect(mockClient.writeTextFile).not.toHaveBeenCalled(); + expect(mockConnection.writeTextFile).not.toHaveBeenCalled(); }, }, ])('should use $desc', async ({ capability, verify }) => { service = new AcpFileSystemService( - mockClient, + mockConnection, 'session-1', { writeTextFile: capability, readTextFile: true }, mockFallback, diff --git a/packages/cli/src/zed-integration/fileSystemService.ts b/packages/cli/src/zed-integration/fileSystemService.ts index a56593f8c3..51a32f2779 100644 --- a/packages/cli/src/zed-integration/fileSystemService.ts +++ b/packages/cli/src/zed-integration/fileSystemService.ts @@ -5,14 +5,14 @@ */ import type { FileSystemService } from '@google/gemini-cli-core'; -import type * as acp from './acp.js'; +import type * as acp from '@agentclientprotocol/sdk'; /** * ACP client-based implementation of FileSystemService */ export class AcpFileSystemService implements FileSystemService { constructor( - private readonly client: acp.Client, + private readonly connection: acp.AgentSideConnection, private readonly sessionId: string, private readonly capabilities: acp.FileSystemCapability, private readonly fallback: FileSystemService, @@ -23,11 +23,9 @@ export class AcpFileSystemService implements FileSystemService { return this.fallback.readTextFile(filePath); } - const response = await this.client.readTextFile({ + const response = await this.connection.readTextFile({ path: filePath, sessionId: this.sessionId, - line: null, - limit: null, }); return response.content; @@ -38,7 +36,7 @@ export class AcpFileSystemService implements FileSystemService { return this.fallback.writeTextFile(filePath, content); } - await this.client.writeTextFile({ + await this.connection.writeTextFile({ path: filePath, content, sessionId: this.sessionId, diff --git a/packages/cli/src/zed-integration/schema.ts b/packages/cli/src/zed-integration/schema.ts deleted file mode 100644 index 14e1a4c388..0000000000 --- a/packages/cli/src/zed-integration/schema.ts +++ /dev/null @@ -1,480 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { z } from 'zod'; - -export const AGENT_METHODS = { - authenticate: 'authenticate', - initialize: 'initialize', - session_cancel: 'session/cancel', - session_load: 'session/load', - session_new: 'session/new', - session_prompt: 'session/prompt', -}; - -export const CLIENT_METHODS = { - fs_read_text_file: 'fs/read_text_file', - fs_write_text_file: 'fs/write_text_file', - session_request_permission: 'session/request_permission', - session_update: 'session/update', -}; - -export const PROTOCOL_VERSION = 1; - -export const authMethodSchema = z.object({ - description: z.string().nullable(), - id: z.string(), - name: z.string(), -}); - -export type WriteTextFileRequest = z.infer; - -export type ReadTextFileRequest = z.infer; - -export type PermissionOptionKind = z.infer; - -export type Role = z.infer; - -export type TextResourceContents = z.infer; - -export type BlobResourceContents = z.infer; - -export type ToolKind = z.infer; - -export type ToolCallStatus = z.infer; - -export type WriteTextFileResponse = z.infer; - -export type ReadTextFileResponse = z.infer; - -export type RequestPermissionOutcome = z.infer< - typeof requestPermissionOutcomeSchema ->; - -export type CancelNotification = z.infer; - -export type AuthenticateRequest = z.infer; - -export type AuthenticateResponse = z.infer; - -export type NewSessionResponse = z.infer; - -export type LoadSessionResponse = z.infer; - -export type StopReason = z.infer; - -export type PromptResponse = z.infer; - -export type ToolCallLocation = z.infer; - -export type PlanEntry = z.infer; - -export type PermissionOption = z.infer; - -export type Annotations = z.infer; - -export type RequestPermissionResponse = z.infer< - typeof requestPermissionResponseSchema ->; - -export type FileSystemCapability = z.infer; - -export type EnvVariable = z.infer; - -export type McpServer = z.infer; - -export type AgentCapabilities = z.infer; - -export type AuthMethod = z.infer; - -export type PromptCapabilities = z.infer; - -export type ClientResponse = z.infer; - -export type ClientNotification = z.infer; - -export type EmbeddedResourceResource = z.infer< - typeof embeddedResourceResourceSchema ->; - -export type NewSessionRequest = z.infer; - -export type LoadSessionRequest = z.infer; - -export type InitializeResponse = z.infer; - -export type ContentBlock = z.infer; - -export type ToolCallContent = z.infer; - -export type ToolCall = z.infer; - -export type ClientCapabilities = z.infer; - -export type PromptRequest = z.infer; - -export type SessionUpdate = z.infer; - -export type AgentResponse = z.infer; - -export type RequestPermissionRequest = z.infer< - typeof requestPermissionRequestSchema ->; - -export type InitializeRequest = z.infer; - -export type SessionNotification = z.infer; - -export type ClientRequest = z.infer; - -export type AgentRequest = z.infer; - -export type AgentNotification = z.infer; - -export type Result = - | { - result: T; - } - | { - error: ErrorResponse; - }; - -export type ErrorResponse = { - code: number; - message: string; - data?: unknown; -}; - -export const writeTextFileRequestSchema = z.object({ - content: z.string(), - path: z.string(), - sessionId: z.string(), -}); - -export const readTextFileRequestSchema = z.object({ - limit: z.number().optional().nullable(), - line: z.number().optional().nullable(), - path: z.string(), - sessionId: z.string(), -}); - -export const permissionOptionKindSchema = z.union([ - z.literal('allow_once'), - z.literal('allow_always'), - z.literal('reject_once'), - z.literal('reject_always'), -]); - -export const roleSchema = z.union([z.literal('assistant'), z.literal('user')]); - -export const textResourceContentsSchema = z.object({ - mimeType: z.string().optional().nullable(), - text: z.string(), - uri: z.string(), -}); - -export const blobResourceContentsSchema = z.object({ - blob: z.string(), - mimeType: z.string().optional().nullable(), - uri: z.string(), -}); - -export const toolKindSchema = z.union([ - z.literal('read'), - z.literal('edit'), - z.literal('delete'), - z.literal('move'), - z.literal('search'), - z.literal('execute'), - z.literal('think'), - z.literal('fetch'), - z.literal('other'), -]); - -export const toolCallStatusSchema = z.union([ - z.literal('pending'), - z.literal('in_progress'), - z.literal('completed'), - z.literal('failed'), -]); - -export const writeTextFileResponseSchema = z.null(); - -export const readTextFileResponseSchema = z.object({ - content: z.string(), -}); - -export const requestPermissionOutcomeSchema = z.union([ - z.object({ - outcome: z.literal('cancelled'), - }), - z.object({ - optionId: z.string(), - outcome: z.literal('selected'), - }), -]); - -export const cancelNotificationSchema = z.object({ - sessionId: z.string(), -}); - -export const authenticateRequestSchema = z.object({ - methodId: z.string(), -}); - -export const authenticateResponseSchema = z.null(); - -export const newSessionResponseSchema = z.object({ - sessionId: z.string(), -}); - -export const loadSessionResponseSchema = z.null(); - -export const stopReasonSchema = z.union([ - z.literal('end_turn'), - z.literal('max_tokens'), - z.literal('refusal'), - z.literal('cancelled'), -]); - -export const promptResponseSchema = z.object({ - stopReason: stopReasonSchema, -}); - -export const toolCallLocationSchema = z.object({ - line: z.number().optional().nullable(), - path: z.string(), -}); - -export const planEntrySchema = z.object({ - content: z.string(), - priority: z.union([z.literal('high'), z.literal('medium'), z.literal('low')]), - status: z.union([ - z.literal('pending'), - z.literal('in_progress'), - z.literal('completed'), - ]), -}); - -export const permissionOptionSchema = z.object({ - kind: permissionOptionKindSchema, - name: z.string(), - optionId: z.string(), -}); - -export const annotationsSchema = z.object({ - audience: z.array(roleSchema).optional().nullable(), - lastModified: z.string().optional().nullable(), - priority: z.number().optional().nullable(), -}); - -export const requestPermissionResponseSchema = z.object({ - outcome: requestPermissionOutcomeSchema, -}); - -export const fileSystemCapabilitySchema = z.object({ - readTextFile: z.boolean(), - writeTextFile: z.boolean(), -}); - -export const envVariableSchema = z.object({ - name: z.string(), - value: z.string(), -}); - -export const mcpServerSchema = z.object({ - args: z.array(z.string()), - command: z.string(), - env: z.array(envVariableSchema), - name: z.string(), -}); - -export const promptCapabilitiesSchema = z.object({ - audio: z.boolean().optional(), - embeddedContext: z.boolean().optional(), - image: z.boolean().optional(), -}); - -export const agentCapabilitiesSchema = z.object({ - loadSession: z.boolean().optional(), - promptCapabilities: promptCapabilitiesSchema.optional(), -}); - -export const clientResponseSchema = z.union([ - writeTextFileResponseSchema, - readTextFileResponseSchema, - requestPermissionResponseSchema, -]); - -export const clientNotificationSchema = cancelNotificationSchema; - -export const embeddedResourceResourceSchema = z.union([ - textResourceContentsSchema, - blobResourceContentsSchema, -]); - -export const newSessionRequestSchema = z.object({ - cwd: z.string(), - mcpServers: z.array(mcpServerSchema), -}); - -export const loadSessionRequestSchema = z.object({ - cwd: z.string(), - mcpServers: z.array(mcpServerSchema), - sessionId: z.string(), -}); - -export const initializeResponseSchema = z.object({ - agentCapabilities: agentCapabilitiesSchema, - authMethods: z.array(authMethodSchema), - protocolVersion: z.number(), -}); - -export const contentBlockSchema = z.union([ - z.object({ - annotations: annotationsSchema.optional().nullable(), - text: z.string(), - type: z.literal('text'), - }), - z.object({ - annotations: annotationsSchema.optional().nullable(), - data: z.string(), - mimeType: z.string(), - type: z.literal('image'), - }), - z.object({ - annotations: annotationsSchema.optional().nullable(), - data: z.string(), - mimeType: z.string(), - type: z.literal('audio'), - }), - z.object({ - annotations: annotationsSchema.optional().nullable(), - description: z.string().optional().nullable(), - mimeType: z.string().optional().nullable(), - name: z.string(), - size: z.number().optional().nullable(), - title: z.string().optional().nullable(), - type: z.literal('resource_link'), - uri: z.string(), - }), - z.object({ - annotations: annotationsSchema.optional().nullable(), - resource: embeddedResourceResourceSchema, - type: z.literal('resource'), - }), -]); - -export const toolCallContentSchema = z.union([ - z.object({ - content: contentBlockSchema, - type: z.literal('content'), - }), - z.object({ - newText: z.string(), - oldText: z.string().nullable(), - path: z.string(), - type: z.literal('diff'), - }), -]); - -export const toolCallSchema = z.object({ - content: z.array(toolCallContentSchema).optional(), - kind: toolKindSchema, - locations: z.array(toolCallLocationSchema).optional(), - rawInput: z.unknown().optional(), - status: toolCallStatusSchema, - title: z.string(), - toolCallId: z.string(), -}); - -export const clientCapabilitiesSchema = z.object({ - fs: fileSystemCapabilitySchema, -}); - -export const promptRequestSchema = z.object({ - prompt: z.array(contentBlockSchema), - sessionId: z.string(), -}); - -export const sessionUpdateSchema = z.union([ - z.object({ - content: contentBlockSchema, - sessionUpdate: z.literal('user_message_chunk'), - }), - z.object({ - content: contentBlockSchema, - sessionUpdate: z.literal('agent_message_chunk'), - }), - z.object({ - content: contentBlockSchema, - sessionUpdate: z.literal('agent_thought_chunk'), - }), - z.object({ - content: z.array(toolCallContentSchema).optional(), - kind: toolKindSchema, - locations: z.array(toolCallLocationSchema).optional(), - rawInput: z.unknown().optional(), - sessionUpdate: z.literal('tool_call'), - status: toolCallStatusSchema, - title: z.string(), - toolCallId: z.string(), - }), - z.object({ - content: z.array(toolCallContentSchema).optional().nullable(), - kind: toolKindSchema.optional().nullable(), - locations: z.array(toolCallLocationSchema).optional().nullable(), - rawInput: z.unknown().optional(), - sessionUpdate: z.literal('tool_call_update'), - status: toolCallStatusSchema.optional().nullable(), - title: z.string().optional().nullable(), - toolCallId: z.string(), - }), - z.object({ - entries: z.array(planEntrySchema), - sessionUpdate: z.literal('plan'), - }), -]); - -export const agentResponseSchema = z.union([ - initializeResponseSchema, - authenticateResponseSchema, - newSessionResponseSchema, - loadSessionResponseSchema, - promptResponseSchema, -]); - -export const requestPermissionRequestSchema = z.object({ - options: z.array(permissionOptionSchema), - sessionId: z.string(), - toolCall: toolCallSchema, -}); - -export const initializeRequestSchema = z.object({ - clientCapabilities: clientCapabilitiesSchema, - protocolVersion: z.number(), -}); - -export const sessionNotificationSchema = z.object({ - sessionId: z.string(), - update: sessionUpdateSchema, -}); - -export const clientRequestSchema = z.union([ - writeTextFileRequestSchema, - readTextFileRequestSchema, - requestPermissionRequestSchema, -]); - -export const agentRequestSchema = z.union([ - initializeRequestSchema, - authenticateRequestSchema, - newSessionRequestSchema, - loadSessionRequestSchema, - promptRequestSchema, -]); - -export const agentNotificationSchema = sessionNotificationSchema; diff --git a/packages/cli/src/zed-integration/zedIntegration.test.ts b/packages/cli/src/zed-integration/zedIntegration.test.ts index c7017bb644..5cae1b37d0 100644 --- a/packages/cli/src/zed-integration/zedIntegration.test.ts +++ b/packages/cli/src/zed-integration/zedIntegration.test.ts @@ -15,7 +15,7 @@ import { type Mocked, } from 'vitest'; import { GeminiAgent, Session } from './zedIntegration.js'; -import * as acp from './acp.js'; +import * as acp from '@agentclientprotocol/sdk'; import { AuthType, ToolConfirmationOutcome, @@ -85,7 +85,7 @@ describe('GeminiAgent', () => { let mockConfig: Mocked>>; let mockSettings: Mocked; let mockArgv: CliArgs; - let mockClient: Mocked; + let mockConnection: Mocked; let agent: GeminiAgent; beforeEach(() => { @@ -106,13 +106,13 @@ describe('GeminiAgent', () => { setValue: vi.fn(), } as unknown as Mocked; mockArgv = {} as unknown as CliArgs; - mockClient = { + mockConnection = { sessionUpdate: vi.fn(), - } as unknown as Mocked; + } as unknown as Mocked; (loadCliConfig as unknown as Mock).mockResolvedValue(mockConfig); - agent = new GeminiAgent(mockConfig, mockSettings, mockArgv, mockClient); + agent = new GeminiAgent(mockConfig, mockSettings, mockArgv, mockConnection); }); it('should initialize correctly', async () => { @@ -123,7 +123,7 @@ describe('GeminiAgent', () => { expect(response.protocolVersion).toBe(acp.PROTOCOL_VERSION); expect(response.authMethods).toHaveLength(3); - expect(response.agentCapabilities.loadSession).toBe(false); + expect(response.agentCapabilities?.loadSession).toBe(false); }); it('should authenticate correctly', async () => { @@ -202,7 +202,7 @@ describe('GeminiAgent', () => { }); it('should initialize file system service if client supports it', async () => { - agent = new GeminiAgent(mockConfig, mockSettings, mockArgv, mockClient); + agent = new GeminiAgent(mockConfig, mockSettings, mockArgv, mockConnection); await agent.initialize({ clientCapabilities: { fs: { readTextFile: true, writeTextFile: true } }, protocolVersion: 1, @@ -257,7 +257,7 @@ describe('GeminiAgent', () => { describe('Session', () => { let mockChat: Mocked; let mockConfig: Mocked; - let mockClient: Mocked; + let mockConnection: Mocked; let session: Session; let mockToolRegistry: { getTool: Mock }; let mockTool: { kind: string; build: Mock }; @@ -292,13 +292,13 @@ describe('Session', () => { getEnableRecursiveFileSearch: vi.fn().mockReturnValue(false), getDebugMode: vi.fn().mockReturnValue(false), } as unknown as Mocked; - mockClient = { + mockConnection = { sessionUpdate: vi.fn(), requestPermission: vi.fn(), sendNotification: vi.fn(), - } as unknown as Mocked; + } as unknown as Mocked; - session = new Session('session-1', mockChat, mockConfig, mockClient); + session = new Session('session-1', mockChat, mockConfig, mockConnection); }); afterEach(() => { @@ -322,7 +322,7 @@ describe('Session', () => { }); expect(mockChat.sendMessageStream).toHaveBeenCalled(); - expect(mockClient.sessionUpdate).toHaveBeenCalledWith({ + expect(mockConnection.sessionUpdate).toHaveBeenCalledWith({ sessionId: 'session-1', update: { sessionUpdate: 'agent_message_chunk', @@ -361,7 +361,7 @@ describe('Session', () => { expect(mockToolRegistry.getTool).toHaveBeenCalledWith('test_tool'); expect(mockTool.build).toHaveBeenCalledWith({ foo: 'bar' }); - expect(mockClient.sessionUpdate).toHaveBeenCalledWith( + expect(mockConnection.sessionUpdate).toHaveBeenCalledWith( expect.objectContaining({ update: expect.objectContaining({ sessionUpdate: 'tool_call', @@ -369,7 +369,7 @@ describe('Session', () => { }), }), ); - expect(mockClient.sessionUpdate).toHaveBeenCalledWith( + expect(mockConnection.sessionUpdate).toHaveBeenCalledWith( expect.objectContaining({ update: expect.objectContaining({ sessionUpdate: 'tool_call_update', @@ -392,7 +392,7 @@ describe('Session', () => { execute: vi.fn().mockResolvedValue({ llmContent: 'Tool Result' }), }); - mockClient.requestPermission.mockResolvedValue({ + mockConnection.requestPermission.mockResolvedValue({ outcome: { outcome: 'selected', optionId: ToolConfirmationOutcome.ProceedOnce, @@ -423,7 +423,7 @@ describe('Session', () => { prompt: [{ type: 'text', text: 'Call tool' }], }); - expect(mockClient.requestPermission).toHaveBeenCalled(); + expect(mockConnection.requestPermission).toHaveBeenCalled(); expect(confirmationDetails.onConfirm).toHaveBeenCalledWith( ToolConfirmationOutcome.ProceedOnce, ); @@ -441,7 +441,7 @@ describe('Session', () => { execute: vi.fn().mockResolvedValue({ llmContent: 'Tool Result' }), }); - mockClient.requestPermission.mockResolvedValue({ + mockConnection.requestPermission.mockResolvedValue({ outcome: { outcome: 'cancelled' }, }); @@ -635,7 +635,7 @@ describe('Session', () => { prompt: [{ type: 'text', text: 'Call tool' }], }); - expect(mockClient.sessionUpdate).toHaveBeenCalledWith( + expect(mockConnection.sessionUpdate).toHaveBeenCalledWith( expect.objectContaining({ update: expect.objectContaining({ sessionUpdate: 'tool_call_update', diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts index abcfb62ccc..8d70907c25 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/zed-integration/zedIntegration.ts @@ -4,8 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { ReadableStream } from 'node:stream/web'; - import type { Config, GeminiChat, @@ -33,7 +31,7 @@ import { createWorkingStdio, startupProfiler, } from '@google/gemini-cli-core'; -import * as acp from './acp.js'; +import * as acp from '@agentclientprotocol/sdk'; import { AcpFileSystemService } from './fileSystemService.js'; import { Readable, Writable } from 'node:stream'; import type { Content, Part, FunctionCall } from '@google/genai'; @@ -53,13 +51,13 @@ export async function runZedIntegration( argv: CliArgs, ) { const { stdout: workingStdout } = createWorkingStdio(); - const stdout = Writable.toWeb(workingStdout); + const stdout = Writable.toWeb(workingStdout) as WritableStream; const stdin = Readable.toWeb(process.stdin) as ReadableStream; + const stream = acp.ndJsonStream(stdout, stdin); new acp.AgentSideConnection( - (client: acp.Client) => new GeminiAgent(config, settings, argv, client), - stdout, - stdin, + (connection) => new GeminiAgent(config, settings, argv, connection), + stream, ); } @@ -71,7 +69,7 @@ export class GeminiAgent { private config: Config, private settings: LoadedSettings, private argv: CliArgs, - private client: acp.Client, + private connection: acp.AgentSideConnection, ) {} async initialize( @@ -107,6 +105,10 @@ export class GeminiAgent { audio: true, embeddedContext: true, }, + mcpCapabilities: { + http: true, + sse: true, + }, }, }; } @@ -156,7 +158,7 @@ export class GeminiAgent { if (this.clientCapabilities?.fs) { const acpFileSystemService = new AcpFileSystemService( - this.client, + this.connection, sessionId, this.clientCapabilities.fs, config.getFileSystemService(), @@ -166,7 +168,7 @@ export class GeminiAgent { const geminiClient = config.getGeminiClient(); const chat = await geminiClient.startChat(); - const session = new Session(sessionId, chat, config, this.client); + const session = new Session(sessionId, chat, config, this.connection); this.sessions.set(sessionId, session); return { @@ -181,12 +183,37 @@ export class GeminiAgent { ): Promise { const mergedMcpServers = { ...this.settings.merged.mcpServers }; - for (const { command, args, env: rawEnv, name } of mcpServers) { - const env: Record = {}; - for (const { name: envName, value } of rawEnv) { - env[envName] = value; + for (const server of mcpServers) { + if ( + 'type' in server && + (server.type === 'sse' || server.type === 'http') + ) { + // HTTP or SSE MCP server + const headers = Object.fromEntries( + server.headers.map(({ name, value }) => [name, value]), + ); + mergedMcpServers[server.name] = new MCPServerConfig( + undefined, // command + undefined, // args + undefined, // env + undefined, // cwd + server.type === 'sse' ? server.url : undefined, // url (sse) + server.type === 'http' ? server.url : undefined, // httpUrl + headers, + ); + } else if ('command' in server) { + // Stdio MCP server + const env: Record = {}; + for (const { name: envName, value } of server.env) { + env[envName] = value; + } + mergedMcpServers[server.name] = new MCPServerConfig( + server.command, + server.args, + env, + cwd, + ); } - mergedMcpServers[name] = new MCPServerConfig(command, args, env, cwd); } const settings = { ...this.settings.merged, mcpServers: mergedMcpServers }; @@ -222,7 +249,7 @@ export class Session { private readonly id: string, private readonly chat: GeminiChat, private readonly config: Config, - private readonly client: acp.Client, + private readonly connection: acp.AgentSideConnection, ) {} async cancelPendingPrompt(): Promise { @@ -340,13 +367,15 @@ export class Session { return { stopReason: 'end_turn' }; } - private async sendUpdate(update: acp.SessionUpdate): Promise { + private async sendUpdate( + update: acp.SessionNotification['update'], + ): Promise { const params: acp.SessionNotification = { sessionId: this.id, update, }; - await this.client.sessionUpdate(params); + await this.connection.sessionUpdate(params); } private async runTool( @@ -432,7 +461,7 @@ export class Session { }, }; - const output = await this.client.requestPermission(params); + const output = await this.connection.requestPermission(params); const outcome = output.outcome.outcome === 'cancelled' ? ToolConfirmationOutcome.Cancel