From 4e16260cbdb8f8ac2bbc1a2702b7ab1337f9a57e Mon Sep 17 00:00:00 2001 From: "Anna.Zhdan" Date: Fri, 10 Apr 2026 08:26:44 +0200 Subject: [PATCH 1/3] Support for unstable elicitation methods --- schema/meta.json | 4 +- schema/schema.json | 343 +++++++++++++++++++++------------------- scripts/generate.js | 53 ++++++- src/acp.test.ts | 162 +++++++++++++++++++ src/acp.ts | 76 +++++++++ src/schema/index.ts | 11 +- src/schema/types.gen.ts | 211 ++++++++++++------------ src/schema/zod.gen.ts | 103 ++++++------ 8 files changed, 640 insertions(+), 323 deletions(-) diff --git a/schema/meta.json b/schema/meta.json index 167bba2..24b26a9 100644 --- a/schema/meta.json +++ b/schema/meta.json @@ -26,10 +26,10 @@ "session_set_model": "session/set_model" }, "clientMethods": { + "elicitation_complete": "elicitation/complete", + "elicitation_create": "elicitation/create", "fs_read_text_file": "fs/read_text_file", "fs_write_text_file": "fs/write_text_file", - "session_elicitation": "session/elicitation", - "session_elicitation_complete": "session/elicitation/complete", "session_request_permission": "session/request_permission", "session_update": "session/update", "terminal_create": "terminal/create", diff --git a/schema/schema.json b/schema/schema.json index 0a6fb84..2492de7 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -149,11 +149,11 @@ { "allOf": [ { - "$ref": "#/$defs/ElicitationCompleteNotification" + "$ref": "#/$defs/CompleteElicitationNotification" } ], "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification that a URL-based elicitation has completed.", - "title": "ElicitationCompleteNotification" + "title": "CompleteElicitationNotification" }, { "allOf": [ @@ -264,11 +264,11 @@ { "allOf": [ { - "$ref": "#/$defs/ElicitationRequest" + "$ref": "#/$defs/CreateElicitationRequest" } ], "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequests structured user input via a form or URL.", - "title": "ElicitationRequest" + "title": "CreateElicitationRequest" }, { "allOf": [ @@ -1349,10 +1349,10 @@ { "allOf": [ { - "$ref": "#/$defs/ElicitationResponse" + "$ref": "#/$defs/CreateElicitationResponse" } ], - "title": "ElicitationResponse" + "title": "CreateElicitationResponse" }, { "allOf": [ @@ -1456,6 +1456,28 @@ "x-method": "session/close", "x-side": "agent" }, + "CompleteElicitationNotification": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification sent by the agent when a URL-based elicitation is complete.", + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"] + }, + "elicitationId": { + "allOf": [ + { + "$ref": "#/$defs/ElicitationId" + } + ], + "description": "The ID of the elicitation that completed." + } + }, + "required": ["elicitationId"], + "type": "object", + "x-method": "elicitation/complete", + "x-side": "client" + }, "ConfigOptionUpdate": { "description": "Session configuration options have been updated.", "properties": { @@ -1623,6 +1645,162 @@ "required": ["amount", "currency"], "type": "object" }, + "CreateElicitationRequest": { + "anyOf": [ + { + "description": "Tied to a session, optionally to a specific tool call within that session.\n\nWhen `tool_call_id` is set, the elicitation is tied to a specific tool call.\nThis is useful when an agent receives an elicitation from an MCP server\nduring a tool call and needs to redirect it to the user.", + "properties": { + "sessionId": { + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ], + "description": "The session this elicitation is tied to." + }, + "toolCallId": { + "anyOf": [ + { + "$ref": "#/$defs/ToolCallId" + }, + { + "type": "null" + } + ], + "description": "Optional tool call within the session." + } + }, + "required": ["sessionId"], + "title": "Session", + "type": "object" + }, + { + "description": "Tied to a specific JSON-RPC request outside of a session\n(e.g., during auth/configuration phases before any session is started).", + "properties": { + "requestId": { + "allOf": [ + { + "$ref": "#/$defs/RequestId" + } + ], + "description": "The request this elicitation is tied to." + } + }, + "required": ["requestId"], + "title": "Request", + "type": "object" + } + ], + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest from the agent to elicit structured user input.\n\nThe agent sends this to the client to request information from the user,\neither via a form or by directing them to a URL.\nElicitations are tied to a session (optionally a tool call) or a request.", + "discriminator": { + "propertyName": "mode" + }, + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/ElicitationFormMode" + } + ], + "description": "Form-based elicitation where the client renders a form from the provided schema.", + "properties": { + "mode": { + "const": "form", + "type": "string" + } + }, + "required": ["mode"], + "type": "object" + }, + { + "allOf": [ + { + "$ref": "#/$defs/ElicitationUrlMode" + } + ], + "description": "URL-based elicitation where the client directs the user to a URL.", + "properties": { + "mode": { + "const": "url", + "type": "string" + } + }, + "required": ["mode"], + "type": "object" + } + ], + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"] + }, + "message": { + "description": "A human-readable message describing what input is needed.", + "type": "string" + } + }, + "required": ["message"], + "type": "object", + "x-method": "elicitation/create", + "x-side": "client" + }, + "CreateElicitationResponse": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse from the client to an elicitation request.", + "discriminator": { + "propertyName": "action" + }, + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/$defs/ElicitationAcceptAction" + } + ], + "description": "The user accepted and provided content.", + "properties": { + "action": { + "const": "accept", + "type": "string" + } + }, + "required": ["action"], + "type": "object" + }, + { + "description": "The user declined the elicitation.", + "properties": { + "action": { + "const": "decline", + "type": "string" + } + }, + "required": ["action"], + "type": "object" + }, + { + "description": "The elicitation was cancelled.", + "properties": { + "action": { + "const": "cancel", + "type": "string" + } + }, + "required": ["action"], + "type": "object" + } + ], + "properties": { + "_meta": { + "additionalProperties": true, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", + "type": ["object", "null"] + } + }, + "type": "object", + "x-method": "elicitation/create", + "x-side": "client" + }, "CreateTerminalRequest": { "description": "Request to create a new terminal and execute a command.", "properties": { @@ -1924,52 +2102,6 @@ }, "type": "object" }, - "ElicitationAction": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe user's action in response to an elicitation.", - "discriminator": { - "propertyName": "action" - }, - "oneOf": [ - { - "allOf": [ - { - "$ref": "#/$defs/ElicitationAcceptAction" - } - ], - "description": "The user accepted and provided content.", - "properties": { - "action": { - "const": "accept", - "type": "string" - } - }, - "required": ["action"], - "type": "object" - }, - { - "description": "The user declined the elicitation.", - "properties": { - "action": { - "const": "decline", - "type": "string" - } - }, - "required": ["action"], - "type": "object" - }, - { - "description": "The elicitation was cancelled.", - "properties": { - "action": { - "const": "cancel", - "type": "string" - } - }, - "required": ["action"], - "type": "object" - } - ] - }, "ElicitationCapabilities": { "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nElicitation capabilities supported by the client.", "properties": { @@ -2003,28 +2135,6 @@ }, "type": "object" }, - "ElicitationCompleteNotification": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nNotification sent by the agent when a URL-based elicitation is complete.", - "properties": { - "_meta": { - "additionalProperties": true, - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"] - }, - "elicitationId": { - "allOf": [ - { - "$ref": "#/$defs/ElicitationId" - } - ], - "description": "The ID of the elicitation that completed." - } - }, - "required": ["elicitationId"], - "type": "object", - "x-method": "session/elicitation/complete", - "x-side": "client" - }, "ElicitationContentValue": { "anyOf": [ { @@ -2172,91 +2282,6 @@ } ] }, - "ElicitationRequest": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest from the agent to elicit structured user input.\n\nThe agent sends this to the client to request information from the user,\neither via a form or by directing them to a URL.", - "discriminator": { - "propertyName": "mode" - }, - "oneOf": [ - { - "allOf": [ - { - "$ref": "#/$defs/ElicitationFormMode" - } - ], - "description": "Form-based elicitation where the client renders a form from the provided schema.", - "properties": { - "mode": { - "const": "form", - "type": "string" - } - }, - "required": ["mode"], - "type": "object" - }, - { - "allOf": [ - { - "$ref": "#/$defs/ElicitationUrlMode" - } - ], - "description": "URL-based elicitation where the client directs the user to a URL.", - "properties": { - "mode": { - "const": "url", - "type": "string" - } - }, - "required": ["mode"], - "type": "object" - } - ], - "properties": { - "_meta": { - "additionalProperties": true, - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"] - }, - "message": { - "description": "A human-readable message describing what input is needed.", - "type": "string" - }, - "sessionId": { - "allOf": [ - { - "$ref": "#/$defs/SessionId" - } - ], - "description": "The session ID for this request." - } - }, - "required": ["sessionId", "message"], - "type": "object", - "x-method": "session/elicitation", - "x-side": "client" - }, - "ElicitationResponse": { - "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse from the client to an elicitation request.", - "properties": { - "_meta": { - "additionalProperties": true, - "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)", - "type": ["object", "null"] - }, - "action": { - "allOf": [ - { - "$ref": "#/$defs/ElicitationAction" - } - ], - "description": "The user's action in response to the elicitation." - } - }, - "required": ["action"], - "type": "object", - "x-method": "session/elicitation", - "x-side": "client" - }, "ElicitationSchema": { "description": "Type-safe elicitation schema for requesting structured user input.\n\nThis represents a JSON Schema object with primitive-typed properties,\nas required by the elicitation specification.", "properties": { diff --git a/scripts/generate.js b/scripts/generate.js index b905a43..1b722bb 100644 --- a/scripts/generate.js +++ b/scripts/generate.js @@ -5,7 +5,7 @@ import * as fs from "fs/promises"; import { dirname } from "path"; import * as prettier from "prettier"; -const CURRENT_SCHEMA_RELEASE = "v0.11.4"; +const CURRENT_SCHEMA_RELEASE = "v0.11.5"; await main(); @@ -61,6 +61,33 @@ async function main() { .replaceAll( /z\.coerce\s*\.bigint\(\)\s*\.gte\(BigInt\(0\)\)\s*\.max\(BigInt\("18446744073709551615"\),\s*\{\s*message:\s*"Invalid value: Expected uint64 to be <= 18446744073709551615",\s*\}\s*\)/gm, "z.number()", + ) + // Add missing JSDoc for zCreateElicitationResponse + .replace( + "\nexport const zCreateElicitationResponse =", + "\n/**\n * **UNSTABLE**\n *\n * This capability is not part of the spec yet, and may be removed or changed at any point.\n *\n * Response from the client to an elicitation request.\n */\nexport const zCreateElicitationResponse =", + ) + // Fix zCreateElicitationRequest: add mode discriminated union lost by codegen + // Uses z.lazy() because zElicitationFormMode is declared later in the file + .replace( + /export const zCreateElicitationRequest = z\.intersection\(\s*z\.union\(\[([\s\S]*?)\]\),\s*z\.looseObject\(\{([\s\S]*?)\}\),\s*\);/, + `/** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Requests structured user input via a form or URL. + */ +export const zCreateElicitationRequest = z.intersection( + z.union([$1]), + z.intersection( + z.lazy(() => z.discriminatedUnion("mode", [ + z.looseObject({ mode: z.literal("form"), ...zElicitationFormMode.shape }), + z.looseObject({ mode: z.literal("url"), ...zElicitationUrlMode.shape }), + ])), + z.looseObject({$2}), + ), +);`, ), ), { parser: "typescript" }, @@ -71,10 +98,26 @@ async function main() { const tsSrc = await fs.readFile(tsPath, "utf8"); const ts = await prettier.format( updateDocs( - tsSrc.replace( - `export type ClientOptions`, - `// eslint-disable-next-line @typescript-eslint/no-unused-vars\ntype ClientOptions`, - ), + tsSrc + .replace( + `export type ClientOptions`, + `// eslint-disable-next-line @typescript-eslint/no-unused-vars\ntype ClientOptions`, + ) + // Fix CreateElicitationRequest: add mode discriminator (oneOf) lost by codegen + .replace( + /(\nexport type CreateElicitationRequest = \([\s\S]*?\n\)) & \{/, + `$1 & (\n | (ElicitationFormMode & { mode: "form" })\n | (ElicitationUrlMode & { mode: "url" })\n) & {`, + ) + // Add missing JSDoc for CreateElicitationRequest (codegen drops it on anyOf+oneOf schemas) + .replace( + "\nexport type CreateElicitationRequest =", + "\n/**\n * **UNSTABLE**\n *\n * This capability is not part of the spec yet, and may be removed or changed at any point.\n *\n * Requests structured user input via a form or URL.\n */\nexport type CreateElicitationRequest =", + ) + // Add missing JSDoc for CreateElicitationResponse (codegen drops it on discriminator schemas) + .replace( + "\nexport type CreateElicitationResponse =", + "\n/**\n * **UNSTABLE**\n *\n * This capability is not part of the spec yet, and may be removed or changed at any point.\n *\n * Response from the client to an elicitation request.\n */\nexport type CreateElicitationResponse =", + ), ), { parser: "typescript" }, ); diff --git a/src/acp.test.ts b/src/acp.test.ts index b1ac9e4..a90a886 100644 --- a/src/acp.test.ts +++ b/src/acp.test.ts @@ -43,6 +43,9 @@ import { ListSessionsResponse, ResumeSessionRequest, ResumeSessionResponse, + CreateElicitationRequest, + CreateElicitationResponse, + CompleteElicitationNotification, } from "./acp.js"; import type { AnyMessage } from "./acp.js"; @@ -1911,4 +1914,163 @@ describe("Connection", () => { "/extra/root2", ]); }); + + it("handles elicitation request lifecycle", async () => { + let receivedRequest: CreateElicitationRequest | undefined; + let receivedNotification: CompleteElicitationNotification | undefined; + + class TestClient implements Client { + async writeTextFile( + _: WriteTextFileRequest, + ): Promise { + return {}; + } + async readTextFile( + _: ReadTextFileRequest, + ): Promise { + return { content: "" }; + } + async requestPermission( + _: RequestPermissionRequest, + ): Promise { + return { outcome: { outcome: "selected", optionId: "allow" } }; + } + async sessionUpdate(_: SessionNotification): Promise {} + + async unstable_createElicitation( + params: CreateElicitationRequest, + ): Promise { + receivedRequest = params; + return { + action: "accept", + content: { name: "Alice" }, + }; + } + async unstable_completeElicitation( + params: CompleteElicitationNotification, + ): Promise { + receivedNotification = params; + } + } + + class TestAgent implements Agent { + async initialize(_: InitializeRequest): Promise { + return { + protocolVersion: 1, + agentCapabilities: { loadSession: false }, + authMethods: [], + }; + } + async newSession(_: NewSessionRequest): Promise { + return { sessionId: "test-session" }; + } + async authenticate(_: AuthenticateRequest): Promise {} + async prompt(_: PromptRequest): Promise { + return { stopReason: "end_turn" }; + } + async cancel(_: CancelNotification): Promise {} + } + + new ClientSideConnection( + () => new TestClient(), + ndJsonStream(clientToAgent.writable, agentToClient.readable), + ); + const clientConnection = new AgentSideConnection( + () => new TestAgent(), + ndJsonStream(agentToClient.writable, clientToAgent.readable), + ); + + // Test form-mode elicitation request + const response = await clientConnection.unstable_createElicitation({ + sessionId: "test-session", + mode: "form", + message: "Please enter your name", + requestedSchema: { + type: "object", + properties: { + name: { type: "string", description: "Your name" }, + }, + }, + }); + + expect(response.action).toBe("accept"); + expect(receivedRequest?.message).toBe("Please enter your name"); + expect((receivedRequest as any)?.sessionId).toBe("test-session"); + expect((receivedRequest as any)?.mode).toBe("form"); + + // Test elicitation complete notification + await clientConnection.unstable_completeElicitation({ + elicitationId: "elic-1", + }); + + await vi.waitFor(() => { + expect(receivedNotification?.elicitationId).toBe("elic-1"); + }); + }); + + it("rejects elicitation request when client does not implement handler", async () => { + // Client WITHOUT unstable_createElicitation + class TestClient implements Client { + async writeTextFile( + _: WriteTextFileRequest, + ): Promise { + return {}; + } + async readTextFile( + _: ReadTextFileRequest, + ): Promise { + return { content: "" }; + } + async requestPermission( + _: RequestPermissionRequest, + ): Promise { + return { outcome: { outcome: "selected", optionId: "allow" } }; + } + async sessionUpdate(_: SessionNotification): Promise {} + } + + class TestAgent implements Agent { + async initialize(_: InitializeRequest): Promise { + return { + protocolVersion: 1, + agentCapabilities: { loadSession: false }, + authMethods: [], + }; + } + async newSession(_: NewSessionRequest): Promise { + return { sessionId: "test-session" }; + } + async authenticate(_: AuthenticateRequest): Promise {} + async prompt(_: PromptRequest): Promise { + return { stopReason: "end_turn" }; + } + async cancel(_: CancelNotification): Promise {} + } + + new ClientSideConnection( + () => new TestClient(), + ndJsonStream(clientToAgent.writable, agentToClient.readable), + ); + const clientConnection = new AgentSideConnection( + () => new TestAgent(), + ndJsonStream(agentToClient.writable, clientToAgent.readable), + ); + + try { + await clientConnection.unstable_createElicitation({ + sessionId: "test-session", + mode: "form", + message: "Enter your name", + requestedSchema: { + type: "object", + properties: { + name: { type: "string" }, + }, + }, + }); + expect.fail("Should have thrown method not found error"); + } catch (error: any) { + expect(error.code).toBe(-32601); // Method not found + } + }); }); diff --git a/src/acp.ts b/src/acp.ts index be13162..a79d870 100644 --- a/src/acp.ts +++ b/src/acp.ts @@ -339,6 +339,42 @@ export class AgentSideConnection { ); } + /** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Creates an elicitation to request input from the user. + * + * @experimental + */ + async unstable_createElicitation( + params: schema.CreateElicitationRequest, + ): Promise { + return await this.#connection.sendRequest( + schema.CLIENT_METHODS.elicitation_create, + params, + ); + } + + /** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Notifies the client that a URL-based elicitation is complete. + * + * @experimental + */ + async unstable_completeElicitation( + params: schema.CompleteElicitationNotification, + ): Promise { + return await this.#connection.sendNotification( + schema.CLIENT_METHODS.elicitation_complete, + params, + ); + } + /** * Extension method * @@ -588,6 +624,14 @@ export class ClientSideConnection implements Agent { const result = await client.killTerminal?.(validatedParams); return result ?? {}; } + case schema.CLIENT_METHODS.elicitation_create: { + if (!client.unstable_createElicitation) { + throw RequestError.methodNotFound(method); + } + const validatedParams = + validate.zCreateElicitationRequest.parse(params); + return client.unstable_createElicitation(validatedParams); + } default: if (client.extMethod) { return client.extMethod(method, params as Record); @@ -605,6 +649,12 @@ export class ClientSideConnection implements Agent { const validatedParams = validate.zSessionNotification.parse(params); return client.sessionUpdate(validatedParams); } + case schema.CLIENT_METHODS.elicitation_complete: { + if (!client.unstable_completeElicitation) return; + const validatedParams = + validate.zCompleteElicitationNotification.parse(params); + return client.unstable_completeElicitation(validatedParams); + } default: if (client.extNotification) { return client.extNotification( @@ -1708,6 +1758,32 @@ export interface Client { params: schema.KillTerminalRequest, ): Promise; + /** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Creates an elicitation to request input from the user. + * + * @experimental + */ + unstable_createElicitation?( + params: schema.CreateElicitationRequest, + ): Promise; + + /** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Called when a URL-based elicitation is complete. + * + * @experimental + */ + unstable_completeElicitation?( + params: schema.CompleteElicitationNotification, + ): Promise; + /** * Extension method * diff --git a/src/schema/index.ts b/src/schema/index.ts index 27799f4..fff7098 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -33,11 +33,14 @@ export type { CloseNesResponse, CloseSessionRequest, CloseSessionResponse, + CompleteElicitationNotification, ConfigOptionUpdate, Content, ContentBlock, ContentChunk, Cost, + CreateElicitationRequest, + CreateElicitationResponse, CreateTerminalRequest, CreateTerminalResponse, CurrentModeUpdate, @@ -48,16 +51,12 @@ export type { DidSaveDocumentNotification, Diff, ElicitationAcceptAction, - ElicitationAction, ElicitationCapabilities, - ElicitationCompleteNotification, ElicitationContentValue, ElicitationFormCapabilities, ElicitationFormMode, ElicitationId, ElicitationPropertySchema, - ElicitationRequest, - ElicitationResponse, ElicitationSchema, ElicitationSchemaType, ElicitationStringType, @@ -259,10 +258,10 @@ export const AGENT_METHODS = { } as const; export const CLIENT_METHODS = { + elicitation_complete: "elicitation/complete", + elicitation_create: "elicitation/create", fs_read_text_file: "fs/read_text_file", fs_write_text_file: "fs/write_text_file", - session_elicitation: "session/elicitation", - session_elicitation_complete: "session/elicitation/complete", session_request_permission: "session/request_permission", session_update: "session/update", terminal_create: "terminal/create", diff --git a/src/schema/types.gen.ts b/src/schema/types.gen.ts index 70ccd2c..6d54a1f 100644 --- a/src/schema/types.gen.ts +++ b/src/schema/types.gen.ts @@ -125,7 +125,7 @@ export type AgentNotification = { method: string; params?: | SessionNotification - | ElicitationCompleteNotification + | CompleteElicitationNotification | ExtNotification | null; }; @@ -142,7 +142,7 @@ export type AgentRequest = { | ReleaseTerminalRequest | WaitForTerminalExitRequest | KillTerminalRequest - | ElicitationRequest + | CreateElicitationRequest | ExtRequest | null; }; @@ -771,7 +771,7 @@ export type ClientResponse = | ReleaseTerminalResponse | WaitForTerminalExitResponse | KillTerminalResponse - | ElicitationResponse + | CreateElicitationResponse | ExtResponse; } | { @@ -872,6 +872,32 @@ export type CloseSessionResponse = { } | null; }; +/** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Notification sent by the agent when a URL-based elicitation is complete. + * + * @experimental + */ +export type CompleteElicitationNotification = { + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; + /** + * The ID of the elicitation that completed. + */ + elicitationId: ElicitationId; +}; + /** * Session configuration options have been updated. */ @@ -999,6 +1025,85 @@ export type Cost = { currency: string; }; +/** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Requests structured user input via a form or URL. + * + * @experimental + */ +export type CreateElicitationRequest = ( + | { + /** + * The session this elicitation is tied to. + */ + sessionId: SessionId; + /** + * Optional tool call within the session. + */ + toolCallId?: ToolCallId | null; + } + | { + /** + * The request this elicitation is tied to. + */ + requestId: RequestId; + } +) & + ( + | (ElicitationFormMode & { mode: "form" }) + | (ElicitationUrlMode & { mode: "url" }) + ) & { + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; + /** + * A human-readable message describing what input is needed. + */ + message: string; + }; + +/** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Response from the client to an elicitation request. + * + * @experimental + */ +export type CreateElicitationResponse = ( + | (ElicitationAcceptAction & { + action: "accept"; + }) + | { + action: "decline"; + } + | { + action: "cancel"; + } +) & { + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + /** * Request to create a new terminal and execute a command. */ @@ -1290,26 +1395,6 @@ export type ElicitationAcceptAction = { } | null; }; -/** - * **UNSTABLE** - * - * This capability is not part of the spec yet, and may be removed or changed at any point. - * - * The user's action in response to an elicitation. - * - * @experimental - */ -export type ElicitationAction = - | (ElicitationAcceptAction & { - action: "accept"; - }) - | { - action: "decline"; - } - | { - action: "cancel"; - }; - /** * **UNSTABLE** * @@ -1340,32 +1425,6 @@ export type ElicitationCapabilities = { url?: ElicitationUrlCapabilities | null; }; -/** - * **UNSTABLE** - * - * This capability is not part of the spec yet, and may be removed or changed at any point. - * - * Notification sent by the agent when a URL-based elicitation is complete. - * - * @experimental - */ -export type ElicitationCompleteNotification = { - /** - * The _meta property is reserved by ACP to allow clients and agents to attach additional - * metadata to their interactions. Implementations MUST NOT make assumptions about values at - * these keys. - * - * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - */ - _meta?: { - [key: string]: unknown; - } | null; - /** - * The ID of the elicitation that completed. - */ - elicitationId: ElicitationId; -}; - export type ElicitationContentValue = | string | number @@ -1446,60 +1505,6 @@ export type ElicitationPropertySchema = type: "array"; }); -export type ElicitationRequest = ( - | (ElicitationFormMode & { - mode: "form"; - }) - | (ElicitationUrlMode & { - mode: "url"; - }) -) & { - /** - * The _meta property is reserved by ACP to allow clients and agents to attach additional - * metadata to their interactions. Implementations MUST NOT make assumptions about values at - * these keys. - * - * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - */ - _meta?: { - [key: string]: unknown; - } | null; - /** - * A human-readable message describing what input is needed. - */ - message: string; - /** - * The session ID for this request. - */ - sessionId: SessionId; -}; - -/** - * **UNSTABLE** - * - * This capability is not part of the spec yet, and may be removed or changed at any point. - * - * Response from the client to an elicitation request. - * - * @experimental - */ -export type ElicitationResponse = { - /** - * The _meta property is reserved by ACP to allow clients and agents to attach additional - * metadata to their interactions. Implementations MUST NOT make assumptions about values at - * these keys. - * - * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - */ - _meta?: { - [key: string]: unknown; - } | null; - /** - * The user's action in response to the elicitation. - */ - action: ElicitationAction; -}; - /** * Type-safe elicitation schema for requesting structured user input. * diff --git a/src/schema/zod.gen.ts b/src/schema/zod.gen.ts index 01a92c6..11558bc 100644 --- a/src/schema/zod.gen.ts +++ b/src/schema/zod.gen.ts @@ -227,23 +227,28 @@ export const zElicitationAcceptAction = z.looseObject({ * * This capability is not part of the spec yet, and may be removed or changed at any point. * - * The user's action in response to an elicitation. + * Response from the client to an elicitation request. * * @experimental */ -export const zElicitationAction = z.union([ - zElicitationAcceptAction.and( +export const zCreateElicitationResponse = z.intersection( + z.union([ + zElicitationAcceptAction.and( + z.looseObject({ + action: z.literal("accept"), + }), + ), z.looseObject({ - action: z.literal("accept"), + action: z.literal("decline"), }), - ), - z.looseObject({ - action: z.literal("decline"), - }), + z.looseObject({ + action: z.literal("cancel"), + }), + ]), z.looseObject({ - action: z.literal("cancel"), + _meta: z.record(z.string(), z.unknown()).nullish(), }), -]); +); /** * **UNSTABLE** @@ -278,25 +283,11 @@ export const zElicitationId = z.string(); * * @experimental */ -export const zElicitationCompleteNotification = z.looseObject({ +export const zCompleteElicitationNotification = z.looseObject({ _meta: z.record(z.string(), z.unknown()).nullish(), elicitationId: zElicitationId, }); -/** - * **UNSTABLE** - * - * This capability is not part of the spec yet, and may be removed or changed at any point. - * - * Response from the client to an elicitation request. - * - * @experimental - */ -export const zElicitationResponse = z.looseObject({ - _meta: z.record(z.string(), z.unknown()).nullish(), - action: zElicitationAction, -}); - /** * Object schema type. */ @@ -2355,6 +2346,42 @@ export const zToolCallContent = z.union([ */ export const zToolCallId = z.string(); +/** + * **UNSTABLE** + * + * This capability is not part of the spec yet, and may be removed or changed at any point. + * + * Requests structured user input via a form or URL. + * + * @experimental + */ +export const zCreateElicitationRequest = z.intersection( + z.union([ + z.looseObject({ + sessionId: zSessionId, + toolCallId: zToolCallId.nullish(), + }), + z.looseObject({ + requestId: zRequestId, + }), + ]), + z.intersection( + z.lazy(() => + z.discriminatedUnion("mode", [ + z.looseObject({ + mode: z.literal("form"), + ...zElicitationFormMode.shape, + }), + z.looseObject({ mode: z.literal("url"), ...zElicitationUrlMode.shape }), + ]), + ), + z.looseObject({ + _meta: z.record(z.string(), z.unknown()).nullish(), + message: z.string(), + }), + ), +); + /** * A file location being accessed or modified by a tool. * @@ -2591,26 +2618,6 @@ export const zElicitationFormMode = z.looseObject({ requestedSchema: zElicitationSchema, }); -export const zElicitationRequest = z.intersection( - z.union([ - zElicitationFormMode.and( - z.looseObject({ - mode: z.literal("form"), - }), - ), - zElicitationUrlMode.and( - z.looseObject({ - mode: z.literal("url"), - }), - ), - ]), - z.looseObject({ - _meta: z.record(z.string(), z.unknown()).nullish(), - message: z.string(), - sessionId: zSessionId, - }), -); - /** * **UNSTABLE** * @@ -2769,7 +2776,7 @@ export const zAgentNotification = z.looseObject({ params: z .union([ zSessionNotification, - zElicitationCompleteNotification, + zCompleteElicitationNotification, zExtNotification, ]) .nullish(), @@ -2869,7 +2876,7 @@ export const zAgentRequest = z.looseObject({ zReleaseTerminalRequest, zWaitForTerminalExitRequest, zKillTerminalRequest, - zElicitationRequest, + zCreateElicitationRequest, zExtRequest, ]) .nullish(), @@ -2894,7 +2901,7 @@ export const zClientResponse = z.union([ zReleaseTerminalResponse, zWaitForTerminalExitResponse, zKillTerminalResponse, - zElicitationResponse, + zCreateElicitationResponse, zExtResponse, ]), }), From ce50fb4d7c60e32ccd2e5b9bc263d4cf77604ac2 Mon Sep 17 00:00:00 2001 From: "Anna.Zhdan" Date: Fri, 10 Apr 2026 11:10:03 +0200 Subject: [PATCH 2/3] Support for unstable elicitation methods -- tests --- src/acp.test.ts | 149 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/src/acp.test.ts b/src/acp.test.ts index a90a886..461ec3c 100644 --- a/src/acp.test.ts +++ b/src/acp.test.ts @@ -1,4 +1,8 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; +import { + zCreateElicitationRequest, + zCreateElicitationResponse, +} from "./schema/zod.gen.js"; import { Agent, ClientSideConnection, @@ -2074,3 +2078,148 @@ describe("Connection", () => { } }); }); + +describe("CreateElicitationRequest schema", () => { + // These tests verify the post-processed zod schema correctly enforces + // both the scope union (session vs request) and mode discriminator (form vs url). + // If the generate.js patches stop applying, these will fail. + + + const formSessionRequest = { + sessionId: "sess-1", + mode: "form" as const, + message: "Enter your name", + requestedSchema: { type: "object" as const, properties: {} }, + }; + + it("accepts form-mode request scoped to a session", () => { + const result = zCreateElicitationRequest.safeParse(formSessionRequest); + expect(result.success).toBe(true); + }); + + it("accepts form-mode request with optional toolCallId", () => { + const result = zCreateElicitationRequest.safeParse({ + ...formSessionRequest, + toolCallId: "tc-1", + }); + expect(result.success).toBe(true); + }); + + it("accepts form-mode request scoped to a request", () => { + const result = zCreateElicitationRequest.safeParse({ + requestId: "req-1", + mode: "form", + message: "Enter your name", + requestedSchema: { type: "object", properties: {} }, + }); + expect(result.success).toBe(true); + }); + + it("accepts url-mode request scoped to a session", () => { + const result = zCreateElicitationRequest.safeParse({ + sessionId: "sess-1", + mode: "url", + message: "Please authenticate", + elicitationId: "elic-1", + url: "https://example.com/auth", + }); + expect(result.success).toBe(true); + }); + + it("accepts url-mode request scoped to a request", () => { + const result = zCreateElicitationRequest.safeParse({ + requestId: "req-1", + mode: "url", + message: "Please authenticate", + elicitationId: "elic-1", + url: "https://example.com/auth", + }); + expect(result.success).toBe(true); + }); + + it("rejects request without mode", () => { + const result = zCreateElicitationRequest.safeParse({ + sessionId: "sess-1", + message: "Enter your name", + requestedSchema: { type: "object", properties: {} }, + }); + expect(result.success).toBe(false); + }); + + it("rejects request with invalid mode", () => { + const result = zCreateElicitationRequest.safeParse({ + sessionId: "sess-1", + mode: "invalid", + message: "Enter your name", + }); + expect(result.success).toBe(false); + }); + + it("rejects request without message", () => { + const result = zCreateElicitationRequest.safeParse({ + sessionId: "sess-1", + mode: "form", + requestedSchema: { type: "object", properties: {} }, + }); + expect(result.success).toBe(false); + }); + + it("rejects request without scope (no sessionId or requestId)", () => { + const result = zCreateElicitationRequest.safeParse({ + mode: "form", + message: "Enter your name", + requestedSchema: { type: "object", properties: {} }, + }); + expect(result.success).toBe(false); + }); + + it("preserves unknown properties (looseObject)", () => { + const result = zCreateElicitationRequest.safeParse({ + ...formSessionRequest, + customField: "custom-value", + }); + expect(result.success).toBe(true); + if (result.success) { + expect((result.data as any).customField).toBe("custom-value"); + } + }); +}); + +describe("CreateElicitationResponse schema", () => { + + it("accepts accept action with content", () => { + const result = zCreateElicitationResponse.safeParse({ + action: "accept", + content: { name: "Alice" }, + }); + expect(result.success).toBe(true); + }); + + it("accepts decline action", () => { + const result = zCreateElicitationResponse.safeParse({ + action: "decline", + }); + expect(result.success).toBe(true); + }); + + it("accepts cancel action", () => { + const result = zCreateElicitationResponse.safeParse({ + action: "cancel", + }); + expect(result.success).toBe(true); + }); + + it("rejects response without action", () => { + const result = zCreateElicitationResponse.safeParse({ + content: { name: "Alice" }, + }); + expect(result.success).toBe(false); + }); + + it("rejects response with invalid action", () => { + const result = zCreateElicitationResponse.safeParse({ + action: "invalid", + }); + expect(result.success).toBe(false); + }); +}); From 5980b9c5f25e7cc453b33cddcd776bcbb39e2dfb Mon Sep 17 00:00:00 2001 From: "Anna.Zhdan" Date: Fri, 10 Apr 2026 11:13:09 +0200 Subject: [PATCH 3/3] Support for unstable elicitation methods -- tests --- src/acp.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/acp.test.ts b/src/acp.test.ts index 461ec3c..04a393f 100644 --- a/src/acp.test.ts +++ b/src/acp.test.ts @@ -2084,7 +2084,6 @@ describe("CreateElicitationRequest schema", () => { // both the scope union (session vs request) and mode discriminator (form vs url). // If the generate.js patches stop applying, these will fail. - const formSessionRequest = { sessionId: "sess-1", mode: "form" as const, @@ -2186,7 +2185,6 @@ describe("CreateElicitationRequest schema", () => { }); describe("CreateElicitationResponse schema", () => { - it("accepts accept action with content", () => { const result = zCreateElicitationResponse.safeParse({ action: "accept",