From 66f98104466094f521cf0ce656941c6538874e82 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 29 Jun 2026 16:32:31 +0200 Subject: [PATCH] feat: Expose request ids in handler contexts --- src/acp.test.ts | 58 +++++++++++++++++++- src/acp.ts | 142 +++++++++++++++++++++++++++++++++++++++--------- src/jsonrpc.ts | 6 +- 3 files changed, 177 insertions(+), 29 deletions(-) diff --git a/src/acp.test.ts b/src/acp.test.ts index 1bd6a47..e2ff060 100644 --- a/src/acp.test.ts +++ b/src/acp.test.ts @@ -721,7 +721,14 @@ describe("Connection", () => { const appAgent = createAgent({ name: "app-agent" }) .onRequest(AGENT_METHODS.initialize, (c) => { events.push(`initialize:${c.params.protocolVersion}`); - expect(Object.keys(c).sort()).toEqual(["client", "params", "signal"]); + expect(Object.keys(c).sort()).toEqual([ + "client", + "params", + "requestId", + "signal", + ]); + expect(c.requestId).toBe(0); + expect(c.client.requestId).toBe(0); expect(c.signal.aborted).toBe(false); return { @@ -732,6 +739,8 @@ describe("Connection", () => { }) .onRequest(AGENT_METHODS.session_new, (c) => { events.push(`new:${c.params.cwd}`); + expect(c.requestId).toBe(1); + expect(c.client.requestId).toBe(1); return { sessionId: "app-session" }; }) .onNotification( @@ -744,11 +753,28 @@ describe("Connection", () => { }, (c) => { expect(Object.keys(c).sort()).toEqual(["client", "params", "signal"]); + expect("requestId" in c).toBe(false); + expect(c.client.requestId).toBeUndefined(); events.push(`agent-route:${String(c.params.message)}`); }, ) .onRequest(AGENT_METHODS.session_prompt, async (c) => { events.push(`prompt:${c.params.sessionId}`); + expect(c.requestId).toBe(2); + expect(c.client.requestId).toBe(2); + const elicitation = await c.client.request( + methods.client.elicitation.create, + { + requestId: c.requestId, + mode: "form", + message: "Need input", + requestedSchema: { + type: "object", + properties: { value: { type: "string" } }, + }, + }, + ); + events.push(`elicitation:${elicitation.action}`); const pong = await c.client.request<{ message: string }>( "vendor/ping", { @@ -769,8 +795,27 @@ describe("Connection", () => { const appClient = createClient({ name: "app-client" }) .onNotification(CLIENT_METHODS.session_update, (c) => { + expect("requestId" in c).toBe(false); + expect(c.agent.requestId).toBeUndefined(); events.push(`update:${c.params.sessionId}`); }) + .onRequest(CLIENT_METHODS.elicitation_create, (c) => { + expect(Object.keys(c).sort()).toEqual([ + "agent", + "params", + "requestId", + "signal", + ]); + expect(c.requestId).toBe(0); + expect(c.agent.requestId).toBe(0); + if (!("requestId" in c.params)) { + throw new Error("Expected request-scoped elicitation"); + } + expect(c.params.requestId).toBe(2); + events.push(`client-elicitation:${String(c.params.requestId)}`); + + return { action: "decline" }; + }) .onRequest( "vendor/ping", (params) => { @@ -778,7 +823,14 @@ describe("Connection", () => { return { message: String(message).toUpperCase() }; }, (c) => { - expect(Object.keys(c).sort()).toEqual(["agent", "params", "signal"]); + expect(Object.keys(c).sort()).toEqual([ + "agent", + "params", + "requestId", + "signal", + ]); + expect(c.requestId).toBe(1); + expect(c.agent.requestId).toBe(1); expect(c.signal.aborted).toBe(false); events.push(`client-route:${String(c.params.message)}`); @@ -820,6 +872,8 @@ describe("Connection", () => { "new:/app", "agent-route:from-client:parsed", "prompt:app-session", + "client-elicitation:2", + "elicitation:decline", "client-route:HELLO", "pong:HELLO", "update:app-session", diff --git a/src/acp.ts b/src/acp.ts index ee7b7c0..f69685d 100644 --- a/src/acp.ts +++ b/src/acp.ts @@ -15,6 +15,7 @@ export type { AnyRequest, AnyResponse, ErrorResponse, + JsonRpcId, MaybePromise, Result, SendRequestOptions, @@ -28,6 +29,7 @@ import type { ConnectionContext, HandleResult, IncomingMessage, + JsonRpcId, JsonRpcHandler, MaybePromise, SendRequestOptions, @@ -186,7 +188,20 @@ export interface ClientConnection extends AcpConnection { class AcpContext { /** @internal */ - constructor(private readonly cx: ConnectionContext) {} + constructor( + private readonly cx: ConnectionContext, + private readonly currentRequestId?: JsonRpcId, + ) {} + + /** + * JSON-RPC id of the request currently being handled. + * + * This is `undefined` for notification handlers and for contexts created + * outside an inbound request, such as `connect(...)` and `connectWith(...)`. + */ + get requestId(): JsonRpcId | undefined { + return this.currentRequestId; + } /** @internal */ protected get connectionContext(): ConnectionContext { @@ -221,13 +236,13 @@ class AcpContext { * requests such as `session/prompt`. */ export class AgentContext extends AcpContext { - private constructor(cx: ConnectionContext) { - super(cx); + private constructor(cx: ConnectionContext, requestId?: JsonRpcId) { + super(cx, requestId); } /** @internal */ - static create(cx: ConnectionContext): AgentContext { - return new AgentContext(cx); + static create(cx: ConnectionContext, requestId?: JsonRpcId): AgentContext { + return new AgentContext(cx, requestId); } /** @@ -280,13 +295,13 @@ export class AgentContext extends AcpContext { * receive one as `ctx.agent` when they need to call back into the agent. */ export class ClientContext extends AcpContext { - private constructor(cx: ConnectionContext) { - super(cx); + private constructor(cx: ConnectionContext, requestId?: JsonRpcId) { + super(cx, requestId); } /** @internal */ - static create(cx: ConnectionContext): ClientContext { - return new ClientContext(cx); + static create(cx: ConnectionContext, requestId?: JsonRpcId): ClientContext { + return new ClientContext(cx, requestId); } /** @internal */ @@ -895,7 +910,7 @@ export type ParamsParser = | ((params: unknown) => Params); /** - * Context passed to agent-side request and notification handlers. + * Common context passed to agent-side handlers. */ export type AgentHandlerContext = { /** @@ -914,7 +929,24 @@ export type AgentHandlerContext = { }; /** - * Context passed to client-side request and notification handlers. + * Context passed to agent-side request handlers. + */ +export type AgentRequestContext = AgentHandlerContext & { + /** + * JSON-RPC id of the request currently being handled. + */ + requestId: JsonRpcId; +}; + +/** + * Context passed to agent-side notification handlers. + * + * Notifications do not have JSON-RPC request ids. + */ +export type AgentNotificationContext = AgentHandlerContext; + +/** + * Common context passed to client-side handlers. */ export type ClientHandlerContext = { /** @@ -932,32 +964,49 @@ export type ClientHandlerContext = { agent: ClientContext; }; +/** + * Context passed to client-side request handlers. + */ +export type ClientRequestContext = ClientHandlerContext & { + /** + * JSON-RPC id of the request currently being handled. + */ + requestId: JsonRpcId; +}; + +/** + * Context passed to client-side notification handlers. + * + * Notifications do not have JSON-RPC request ids. + */ +export type ClientNotificationContext = ClientHandlerContext; + /** * Request handler registered on an `AgentApp`. */ export type AgentRequestHandler = ( - context: AgentHandlerContext, + context: AgentRequestContext, ) => MaybePromise; /** * Notification handler registered on an `AgentApp`. */ export type AgentNotificationHandler = ( - context: AgentHandlerContext, + context: AgentNotificationContext, ) => MaybePromise; /** * Request handler registered on a `ClientApp`. */ export type ClientRequestHandler = ( - context: ClientHandlerContext, + context: ClientRequestContext, ) => MaybePromise; /** * Notification handler registered on a `ClientApp`. */ export type ClientNotificationHandler = ( - context: ClientHandlerContext, + context: ClientNotificationContext, ) => MaybePromise; /** @@ -1022,6 +1071,7 @@ function registerAppRequest( params: Params, cx: ConnectionContext, signal: AbortSignal, + requestId: JsonRpcId, ) => Context, handler: (context: Context) => MaybePromise, ): void { @@ -1029,7 +1079,9 @@ function registerAppRequest( spec.method, (params) => parseParams(spec.params, params), async (params, responder, cx) => { - const response = await handler(context(params, cx, responder.signal)); + const response = await handler( + context(params, cx, responder.signal, responder.id), + ); await responder.respond( (spec.mapResponse ? spec.mapResponse(response) @@ -1561,11 +1613,25 @@ export type ClientNotificationParamsByMethod = { : never; }; -function agentHandlerContext( +function agentRequestContext( + params: Params, + client: AgentContext, + signal: AbortSignal, + requestId: JsonRpcId, +): AgentRequestContext { + return { + params, + requestId, + signal, + client, + }; +} + +function agentNotificationContext( params: Params, client: AgentContext, signal: AbortSignal, -): AgentHandlerContext { +): AgentNotificationContext { return { params, signal, @@ -1573,11 +1639,25 @@ function agentHandlerContext( }; } -function clientHandlerContext( +function clientRequestContext( params: Params, agent: ClientContext, signal: AbortSignal, -): ClientHandlerContext { + requestId: JsonRpcId, +): ClientRequestContext { + return { + params, + requestId, + signal, + agent, + }; +} + +function clientNotificationContext( + params: Params, + agent: ClientContext, + signal: AbortSignal, +): ClientNotificationContext { return { params, signal, @@ -1875,8 +1955,13 @@ export class AgentApp { registerAppRequest( this.builder, spec, - (params, cx, signal) => - agentHandlerContext(params, AgentContext.create(cx), signal), + (params, cx, signal, requestId) => + agentRequestContext( + params, + AgentContext.create(cx, requestId), + signal, + requestId, + ), handler, ); return this; @@ -1890,7 +1975,7 @@ export class AgentApp { this.builder, spec, (params, cx, signal) => - agentHandlerContext(params, AgentContext.create(cx), signal), + agentNotificationContext(params, AgentContext.create(cx), signal), handler, ); return this; @@ -2120,8 +2205,13 @@ export class ClientApp { registerAppRequest( this.builder, spec, - (params, cx, signal) => - clientHandlerContext(params, ClientContext.create(cx), signal), + (params, cx, signal, requestId) => + clientRequestContext( + params, + ClientContext.create(cx, requestId), + signal, + requestId, + ), handler, ); return this; @@ -2135,7 +2225,7 @@ export class ClientApp { this.builder, spec, (params, cx, signal) => - clientHandlerContext(params, ClientContext.create(cx), signal), + clientNotificationContext(params, ClientContext.create(cx), signal), handler, ); return this; diff --git a/src/jsonrpc.ts b/src/jsonrpc.ts index 2d49bb9..7926b8c 100644 --- a/src/jsonrpc.ts +++ b/src/jsonrpc.ts @@ -60,7 +60,11 @@ export type AnyNotification = { }; const CANCEL_REQUEST_METHOD = "$/cancel_request"; -type JsonRpcId = string | number | null; + +/** + * JSON-RPC request identifier. + */ +export type JsonRpcId = string | number | null; /** * Options for sending a JSON-RPC request.