Skip to content

Commit eda849c

Browse files
authored
feat: Expose request ids in handler contexts (#202)
1 parent f8bf32c commit eda849c

3 files changed

Lines changed: 177 additions & 29 deletions

File tree

src/acp.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,14 @@ describe("Connection", () => {
721721
const appAgent = createAgent({ name: "app-agent" })
722722
.onRequest(AGENT_METHODS.initialize, (c) => {
723723
events.push(`initialize:${c.params.protocolVersion}`);
724-
expect(Object.keys(c).sort()).toEqual(["client", "params", "signal"]);
724+
expect(Object.keys(c).sort()).toEqual([
725+
"client",
726+
"params",
727+
"requestId",
728+
"signal",
729+
]);
730+
expect(c.requestId).toBe(0);
731+
expect(c.client.requestId).toBe(0);
725732
expect(c.signal.aborted).toBe(false);
726733

727734
return {
@@ -732,6 +739,8 @@ describe("Connection", () => {
732739
})
733740
.onRequest(AGENT_METHODS.session_new, (c) => {
734741
events.push(`new:${c.params.cwd}`);
742+
expect(c.requestId).toBe(1);
743+
expect(c.client.requestId).toBe(1);
735744
return { sessionId: "app-session" };
736745
})
737746
.onNotification(
@@ -744,11 +753,28 @@ describe("Connection", () => {
744753
},
745754
(c) => {
746755
expect(Object.keys(c).sort()).toEqual(["client", "params", "signal"]);
756+
expect("requestId" in c).toBe(false);
757+
expect(c.client.requestId).toBeUndefined();
747758
events.push(`agent-route:${String(c.params.message)}`);
748759
},
749760
)
750761
.onRequest(AGENT_METHODS.session_prompt, async (c) => {
751762
events.push(`prompt:${c.params.sessionId}`);
763+
expect(c.requestId).toBe(2);
764+
expect(c.client.requestId).toBe(2);
765+
const elicitation = await c.client.request(
766+
methods.client.elicitation.create,
767+
{
768+
requestId: c.requestId,
769+
mode: "form",
770+
message: "Need input",
771+
requestedSchema: {
772+
type: "object",
773+
properties: { value: { type: "string" } },
774+
},
775+
},
776+
);
777+
events.push(`elicitation:${elicitation.action}`);
752778
const pong = await c.client.request<{ message: string }>(
753779
"vendor/ping",
754780
{
@@ -769,16 +795,42 @@ describe("Connection", () => {
769795

770796
const appClient = createClient({ name: "app-client" })
771797
.onNotification(CLIENT_METHODS.session_update, (c) => {
798+
expect("requestId" in c).toBe(false);
799+
expect(c.agent.requestId).toBeUndefined();
772800
events.push(`update:${c.params.sessionId}`);
773801
})
802+
.onRequest(CLIENT_METHODS.elicitation_create, (c) => {
803+
expect(Object.keys(c).sort()).toEqual([
804+
"agent",
805+
"params",
806+
"requestId",
807+
"signal",
808+
]);
809+
expect(c.requestId).toBe(0);
810+
expect(c.agent.requestId).toBe(0);
811+
if (!("requestId" in c.params)) {
812+
throw new Error("Expected request-scoped elicitation");
813+
}
814+
expect(c.params.requestId).toBe(2);
815+
events.push(`client-elicitation:${String(c.params.requestId)}`);
816+
817+
return { action: "decline" };
818+
})
774819
.onRequest(
775820
"vendor/ping",
776821
(params) => {
777822
const message = (params as Record<string, unknown>).message;
778823
return { message: String(message).toUpperCase() };
779824
},
780825
(c) => {
781-
expect(Object.keys(c).sort()).toEqual(["agent", "params", "signal"]);
826+
expect(Object.keys(c).sort()).toEqual([
827+
"agent",
828+
"params",
829+
"requestId",
830+
"signal",
831+
]);
832+
expect(c.requestId).toBe(1);
833+
expect(c.agent.requestId).toBe(1);
782834
expect(c.signal.aborted).toBe(false);
783835
events.push(`client-route:${String(c.params.message)}`);
784836

@@ -820,6 +872,8 @@ describe("Connection", () => {
820872
"new:/app",
821873
"agent-route:from-client:parsed",
822874
"prompt:app-session",
875+
"client-elicitation:2",
876+
"elicitation:decline",
823877
"client-route:HELLO",
824878
"pong:HELLO",
825879
"update:app-session",

src/acp.ts

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type {
1515
AnyRequest,
1616
AnyResponse,
1717
ErrorResponse,
18+
JsonRpcId,
1819
MaybePromise,
1920
Result,
2021
SendRequestOptions,
@@ -28,6 +29,7 @@ import type {
2829
ConnectionContext,
2930
HandleResult,
3031
IncomingMessage,
32+
JsonRpcId,
3133
JsonRpcHandler,
3234
MaybePromise,
3335
SendRequestOptions,
@@ -186,7 +188,20 @@ export interface ClientConnection extends AcpConnection {
186188

187189
class AcpContext {
188190
/** @internal */
189-
constructor(private readonly cx: ConnectionContext) {}
191+
constructor(
192+
private readonly cx: ConnectionContext,
193+
private readonly currentRequestId?: JsonRpcId,
194+
) {}
195+
196+
/**
197+
* JSON-RPC id of the request currently being handled.
198+
*
199+
* This is `undefined` for notification handlers and for contexts created
200+
* outside an inbound request, such as `connect(...)` and `connectWith(...)`.
201+
*/
202+
get requestId(): JsonRpcId | undefined {
203+
return this.currentRequestId;
204+
}
190205

191206
/** @internal */
192207
protected get connectionContext(): ConnectionContext {
@@ -221,13 +236,13 @@ class AcpContext {
221236
* requests such as `session/prompt`.
222237
*/
223238
export class AgentContext extends AcpContext {
224-
private constructor(cx: ConnectionContext) {
225-
super(cx);
239+
private constructor(cx: ConnectionContext, requestId?: JsonRpcId) {
240+
super(cx, requestId);
226241
}
227242

228243
/** @internal */
229-
static create(cx: ConnectionContext): AgentContext {
230-
return new AgentContext(cx);
244+
static create(cx: ConnectionContext, requestId?: JsonRpcId): AgentContext {
245+
return new AgentContext(cx, requestId);
231246
}
232247

233248
/**
@@ -280,13 +295,13 @@ export class AgentContext extends AcpContext {
280295
* receive one as `ctx.agent` when they need to call back into the agent.
281296
*/
282297
export class ClientContext extends AcpContext {
283-
private constructor(cx: ConnectionContext) {
284-
super(cx);
298+
private constructor(cx: ConnectionContext, requestId?: JsonRpcId) {
299+
super(cx, requestId);
285300
}
286301

287302
/** @internal */
288-
static create(cx: ConnectionContext): ClientContext {
289-
return new ClientContext(cx);
303+
static create(cx: ConnectionContext, requestId?: JsonRpcId): ClientContext {
304+
return new ClientContext(cx, requestId);
290305
}
291306

292307
/** @internal */
@@ -895,7 +910,7 @@ export type ParamsParser<Params> =
895910
| ((params: unknown) => Params);
896911

897912
/**
898-
* Context passed to agent-side request and notification handlers.
913+
* Common context passed to agent-side handlers.
899914
*/
900915
export type AgentHandlerContext<Params> = {
901916
/**
@@ -914,7 +929,24 @@ export type AgentHandlerContext<Params> = {
914929
};
915930

916931
/**
917-
* Context passed to client-side request and notification handlers.
932+
* Context passed to agent-side request handlers.
933+
*/
934+
export type AgentRequestContext<Params> = AgentHandlerContext<Params> & {
935+
/**
936+
* JSON-RPC id of the request currently being handled.
937+
*/
938+
requestId: JsonRpcId;
939+
};
940+
941+
/**
942+
* Context passed to agent-side notification handlers.
943+
*
944+
* Notifications do not have JSON-RPC request ids.
945+
*/
946+
export type AgentNotificationContext<Params> = AgentHandlerContext<Params>;
947+
948+
/**
949+
* Common context passed to client-side handlers.
918950
*/
919951
export type ClientHandlerContext<Params> = {
920952
/**
@@ -932,32 +964,49 @@ export type ClientHandlerContext<Params> = {
932964
agent: ClientContext;
933965
};
934966

967+
/**
968+
* Context passed to client-side request handlers.
969+
*/
970+
export type ClientRequestContext<Params> = ClientHandlerContext<Params> & {
971+
/**
972+
* JSON-RPC id of the request currently being handled.
973+
*/
974+
requestId: JsonRpcId;
975+
};
976+
977+
/**
978+
* Context passed to client-side notification handlers.
979+
*
980+
* Notifications do not have JSON-RPC request ids.
981+
*/
982+
export type ClientNotificationContext<Params> = ClientHandlerContext<Params>;
983+
935984
/**
936985
* Request handler registered on an `AgentApp`.
937986
*/
938987
export type AgentRequestHandler<Params, Response> = (
939-
context: AgentHandlerContext<Params>,
988+
context: AgentRequestContext<Params>,
940989
) => MaybePromise<Response>;
941990

942991
/**
943992
* Notification handler registered on an `AgentApp`.
944993
*/
945994
export type AgentNotificationHandler<Params> = (
946-
context: AgentHandlerContext<Params>,
995+
context: AgentNotificationContext<Params>,
947996
) => MaybePromise<void>;
948997

949998
/**
950999
* Request handler registered on a `ClientApp`.
9511000
*/
9521001
export type ClientRequestHandler<Params, Response> = (
953-
context: ClientHandlerContext<Params>,
1002+
context: ClientRequestContext<Params>,
9541003
) => MaybePromise<Response>;
9551004

9561005
/**
9571006
* Notification handler registered on a `ClientApp`.
9581007
*/
9591008
export type ClientNotificationHandler<Params> = (
960-
context: ClientHandlerContext<Params>,
1009+
context: ClientNotificationContext<Params>,
9611010
) => MaybePromise<void>;
9621011

9631012
/**
@@ -1022,14 +1071,17 @@ function registerAppRequest<Params, Response, WireResponse, Context>(
10221071
params: Params,
10231072
cx: ConnectionContext,
10241073
signal: AbortSignal,
1074+
requestId: JsonRpcId,
10251075
) => Context,
10261076
handler: (context: Context) => MaybePromise<Response>,
10271077
): void {
10281078
builder.onReceiveRequest<Params, WireResponse>(
10291079
spec.method,
10301080
(params) => parseParams(spec.params, params),
10311081
async (params, responder, cx) => {
1032-
const response = await handler(context(params, cx, responder.signal));
1082+
const response = await handler(
1083+
context(params, cx, responder.signal, responder.id),
1084+
);
10331085
await responder.respond(
10341086
(spec.mapResponse
10351087
? spec.mapResponse(response)
@@ -1561,23 +1613,51 @@ export type ClientNotificationParamsByMethod = {
15611613
: never;
15621614
};
15631615

1564-
function agentHandlerContext<Params>(
1616+
function agentRequestContext<Params>(
1617+
params: Params,
1618+
client: AgentContext,
1619+
signal: AbortSignal,
1620+
requestId: JsonRpcId,
1621+
): AgentRequestContext<Params> {
1622+
return {
1623+
params,
1624+
requestId,
1625+
signal,
1626+
client,
1627+
};
1628+
}
1629+
1630+
function agentNotificationContext<Params>(
15651631
params: Params,
15661632
client: AgentContext,
15671633
signal: AbortSignal,
1568-
): AgentHandlerContext<Params> {
1634+
): AgentNotificationContext<Params> {
15691635
return {
15701636
params,
15711637
signal,
15721638
client,
15731639
};
15741640
}
15751641

1576-
function clientHandlerContext<Params>(
1642+
function clientRequestContext<Params>(
15771643
params: Params,
15781644
agent: ClientContext,
15791645
signal: AbortSignal,
1580-
): ClientHandlerContext<Params> {
1646+
requestId: JsonRpcId,
1647+
): ClientRequestContext<Params> {
1648+
return {
1649+
params,
1650+
requestId,
1651+
signal,
1652+
agent,
1653+
};
1654+
}
1655+
1656+
function clientNotificationContext<Params>(
1657+
params: Params,
1658+
agent: ClientContext,
1659+
signal: AbortSignal,
1660+
): ClientNotificationContext<Params> {
15811661
return {
15821662
params,
15831663
signal,
@@ -1875,8 +1955,13 @@ export class AgentApp {
18751955
registerAppRequest(
18761956
this.builder,
18771957
spec,
1878-
(params, cx, signal) =>
1879-
agentHandlerContext(params, AgentContext.create(cx), signal),
1958+
(params, cx, signal, requestId) =>
1959+
agentRequestContext(
1960+
params,
1961+
AgentContext.create(cx, requestId),
1962+
signal,
1963+
requestId,
1964+
),
18801965
handler,
18811966
);
18821967
return this;
@@ -1890,7 +1975,7 @@ export class AgentApp {
18901975
this.builder,
18911976
spec,
18921977
(params, cx, signal) =>
1893-
agentHandlerContext(params, AgentContext.create(cx), signal),
1978+
agentNotificationContext(params, AgentContext.create(cx), signal),
18941979
handler,
18951980
);
18961981
return this;
@@ -2120,8 +2205,13 @@ export class ClientApp {
21202205
registerAppRequest(
21212206
this.builder,
21222207
spec,
2123-
(params, cx, signal) =>
2124-
clientHandlerContext(params, ClientContext.create(cx), signal),
2208+
(params, cx, signal, requestId) =>
2209+
clientRequestContext(
2210+
params,
2211+
ClientContext.create(cx, requestId),
2212+
signal,
2213+
requestId,
2214+
),
21252215
handler,
21262216
);
21272217
return this;
@@ -2135,7 +2225,7 @@ export class ClientApp {
21352225
this.builder,
21362226
spec,
21372227
(params, cx, signal) =>
2138-
clientHandlerContext(params, ClientContext.create(cx), signal),
2228+
clientNotificationContext(params, ClientContext.create(cx), signal),
21392229
handler,
21402230
);
21412231
return this;

0 commit comments

Comments
 (0)