Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions src/jsonrpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
10 changes: 6 additions & 4 deletions src/protocol-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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] },
},
},
],
Expand Down
35 changes: 13 additions & 22 deletions src/protocol-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
Expand All @@ -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 {
Expand Down
26 changes: 16 additions & 10 deletions src/v2/acp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,19 +408,25 @@ function assertV2BatchMethods(
}

function parseV2InitializeRequest(params: unknown): schema.InitializeRequest {
const protocolVersion = received<schema.ProtocolVersion>(
validate.zProtocolVersion,
).parse(
typeof params === "object" && params !== null && !Array.isArray(params)
? (params as Record<string, unknown>)["protocolVersion"]
: undefined,
);
if (protocolVersion < schema.PROTOCOL_VERSION) {
throw RequestError.unsupportedProtocolVersion(protocolVersion, [
schema.PROTOCOL_VERSION,
]);
}
const request = received<schema.InitializeRequest>(
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): {
Expand Down
Loading