diff --git a/src/jsonrpc.ts b/src/jsonrpc.ts index bf03afe2..3eedf42f 100644 --- a/src/jsonrpc.ts +++ b/src/jsonrpc.ts @@ -1792,6 +1792,16 @@ export class RequestError extends Error { ); } + static unsupportedProtocolVersion( + requested: number, + supported: readonly number[], + ): RequestError { + return new RequestError(-32022, "Unsupported protocol version", { + requested, + supported: [...supported], + }); + } + /** * Converts this error to a JSON-RPC result object. */ diff --git a/src/protocol-router.test.ts b/src/protocol-router.test.ts index 26e00a85..80744ebd 100644 --- a/src/protocol-router.test.ts +++ b/src/protocol-router.test.ts @@ -203,8 +203,9 @@ describe("AgentProtocolRouter", () => { expect(response).toMatchObject({ id: "initialize-id", error: { - code: -32600, - data: expect.stringContaining("supports ACP protocol version 2"), + code: -32022, + message: "Unsupported protocol version", + data: { requested: 1, supported: [2] }, }, }); expect(v2.connectionCount).toBe(0); @@ -231,8 +232,9 @@ describe("AgentProtocolRouter", () => { jsonrpc: "2.0", id: "initialize-id", error: { - code: -32600, - data: expect.stringContaining("supports ACP protocol version 2"), + code: -32022, + message: "Unsupported protocol version", + data: { requested: 1, supported: [2] }, }, }, ], diff --git a/src/protocol-router.ts b/src/protocol-router.ts index 65fd5363..0a1af39b 100644 --- a/src/protocol-router.ts +++ b/src/protocol-router.ts @@ -41,10 +41,10 @@ const MAX_PROTOCOL_VERSION = 0xffff; * including the experimental draft ACP v2 API. * * The router consumes the first wire item, which must be an `initialize` - * request or a single-entry batch containing one. It selects the highest - * configured protocol version that does not exceed the client's requested - * version. Only the initialize params and optional batch framing are - * normalized; every later wire item is forwarded unchanged. + * request or a single-entry batch containing one. Endpoints exposing v2 select + * the highest configured version at or below the requested version. V1-only + * endpoints retain the legacy fallback to v1. Only initialize params and + * optional batch framing are normalized. Later wire items are forwarded unchanged. * * @experimental */ @@ -201,15 +201,16 @@ export class AgentProtocolRouter implements AgentConnector { return; } - const selected = this.highestCompatible(requested); + const selected = this.selectProtocol(requested); if (!selected) { await rejectInitialize( reader, stream.writable, message.id, - RequestError.invalidRequest( - `unsupported ACP protocol version ${requested}; this endpoint supports ${this.supportedDescription()}`, - ), + RequestError.unsupportedProtocolVersion(requested, [ + ...(this.v1 ? [PROTOCOL_V1] : []), + ...(this.v2 ? [PROTOCOL_V2] : []), + ]), batchedInitialize, ); lifecycle.finish(); @@ -266,7 +267,10 @@ export class AgentProtocolRouter implements AgentConnector { } } - private highestCompatible(requested: number): AgentProtocol | undefined { + private selectProtocol(requested: number): AgentProtocol | undefined { + if (this.v1 && !this.v2) { + return PROTOCOL_V1; + } if (this.v2 && requested >= PROTOCOL_V2) { return PROTOCOL_V2; } @@ -275,19 +279,6 @@ export class AgentProtocolRouter implements AgentConnector { } return undefined; } - - private supportedDescription(): string { - if (this.v1 && this.v2) { - return "ACP protocol versions 1 and 2"; - } - if (this.v1) { - return "ACP protocol version 1"; - } - if (this.v2) { - return "ACP protocol version 2"; - } - return "no ACP protocol versions"; - } } function shouldCloseWithoutResponse(message: WireMessage): boolean { diff --git a/src/v2/acp.ts b/src/v2/acp.ts index b358496b..f06a89ea 100644 --- a/src/v2/acp.ts +++ b/src/v2/acp.ts @@ -408,19 +408,25 @@ function assertV2BatchMethods( } function parseV2InitializeRequest(params: unknown): schema.InitializeRequest { + const protocolVersion = received( + validate.zProtocolVersion, + ).parse( + typeof params === "object" && params !== null && !Array.isArray(params) + ? (params as Record)["protocolVersion"] + : undefined, + ); + if (protocolVersion < schema.PROTOCOL_VERSION) { + throw RequestError.unsupportedProtocolVersion(protocolVersion, [ + schema.PROTOCOL_VERSION, + ]); + } const request = received( validate.zInitializeRequest, ).parse(params); - if (request.protocolVersion !== schema.PROTOCOL_VERSION) { - throw RequestError.invalidParams( - { - expectedProtocolVersion: schema.PROTOCOL_VERSION, - receivedProtocolVersion: request.protocolVersion, - }, - `The v2 API only supports protocol version ${schema.PROTOCOL_VERSION}`, - ); - } - return structuredClone(request); + return structuredClone({ + ...request, + protocolVersion: schema.PROTOCOL_VERSION, + }); } function normalizeOutgoingV2InitializeRequest(params: unknown): {