diff --git a/docs/protocol/draft/schema.mdx b/docs/protocol/draft/schema.mdx index ebc6338c..ca2eb5b3 100644 --- a/docs/protocol/draft/schema.mdx +++ b/docs/protocol/draft/schema.mdx @@ -1306,6 +1306,25 @@ Request parameters for setting a session configuration option. **Type:** Union +**Shared properties:** + + + 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) + + +SessionConfigId} required> + The ID of the configuration option to set. + +SessionId} required> + The ID of the session to set the configuration option for. + + +**Variants:** + A boolean value (`type: "boolean"`). @@ -1321,7 +1340,7 @@ A boolean value (`type: "boolean"`). - + A `SessionConfigValueId` string value. This is the default when `type` is absent on the wire. Unknown `type` @@ -1483,21 +1502,22 @@ Clients are typically code editors (IDEs, text editors) that provide the interfa between users and AI agents. They manage the environment, handle user interactions, and control access to resources. - -### fs/read_text_file + +### elicitation/complete -Reads content from a text file in the client's file system. +**UNSTABLE** -Only available if the client advertises the `fs.readTextFile` capability. -Allows the agent to access file contents within the client's environment. +This capability is not part of the spec yet, and may be removed or changed at any point. -See protocol docs: [Client](https://agentclientprotocol.com/protocol/overview#client) +Notification that a URL-based elicitation has completed. -#### ReadTextFileRequest +#### CompleteElicitationNotification -Request to read content from a text file. +**UNSTABLE** -Only available if the client supports the `fs.readTextFile` capability. +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. **Type:** Object @@ -1511,63 +1531,34 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - - Maximum number of lines to read. - - - Minimum: `0` - - - - Line number to start reading from (1-based). - - - Minimum: `0` - - - - Absolute path to the file to read. - -SessionId} required> - The session ID for this request. +ElicitationId} required> + The ID of the elicitation that completed. -#### ReadTextFileResponse - -Response containing the contents of a text file. - -**Type:** Object - -**Properties:** - - - 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. + +### elicitation/create -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - - - - +**UNSTABLE** - -### fs/write_text_file +This capability is not part of the spec yet, and may be removed or changed at any point. -Writes content to a text file in the client's file system. +Requests structured user input via a form or URL. -Only available if the client advertises the `fs.writeTextFile` capability. -Allows the agent to create or modify files within the client's environment. +#### CreateElicitationRequest -See protocol docs: [Client](https://agentclientprotocol.com/protocol/overview#client) +**UNSTABLE** -#### WriteTextFileRequest +This capability is not part of the spec yet, and may be removed or changed at any point. -Request to write content to a text file. +Request from the agent to elicit structured user input. -Only available if the client supports the `fs.writeTextFile` capability. +The agent sends this to the client to request information from the user, +either via a form or by directing them to a URL. +Elicitations are tied to a session (optionally a tool call) or a request. -**Type:** Object +**Type:** Union -**Properties:** +**Shared properties:** The _meta property is reserved by ACP to allow clients and agents to attach additional @@ -1577,54 +1568,61 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) - - The text content to write to the file. - - - Absolute path to the file to write. - -SessionId} required> - The session ID for this request. + + A human-readable message describing what input is needed. -#### WriteTextFileResponse - -Response to `fs/write_text_file` - -**Type:** Object +**Variants:** -**Properties:** + +Tied to a session, optionally to a specific tool call within that session. - - 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. +When `tool_call_id` is set, the elicitation is tied to a specific tool call. +This is useful when an agent receives an elicitation from an MCP server +during a tool call and needs to redirect it to the user. -See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + +SessionId} + required +> + The session this elicitation is tied to. + + + + ToolCallId + + | null + + } +> + Optional tool call within the session. - -### session/elicitation - -**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. - -#### ElicitationRequest - -**UNSTABLE** + + -This capability is not part of the spec yet, and may be removed or changed at any point. + +Tied to a specific JSON-RPC request outside of a session +(e.g., during auth/configuration phases before any session is started). -Request from the agent to elicit structured user input. + -The agent sends this to the client to request information from the user, -either via a form or by directing them to a URL. +RequestId} + required +> + The request this elicitation is tied to. + -**Type:** Union + + Form-based elicitation where the client renders a form from the provided schema. @@ -1667,7 +1665,7 @@ URL-based elicitation where the client directs the user to a URL. -#### ElicitationResponse +#### CreateElicitationResponse **UNSTABLE** @@ -1675,6 +1673,76 @@ This capability is not part of the spec yet, and may be removed or changed at an Response from the client to an elicitation request. +**Type:** Union + +**Shared properties:** + + + 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) + + + +**Variants:** + + +The user accepted and provided content. + + + + + The discriminator value. Must be `"accept"`. + + + The user-provided content, if any, as an object matching the requested schema. + + + + + + +The user declined the elicitation. + + + + + The discriminator value. Must be `"decline"`. + + + + + + +The elicitation was cancelled. + + + + + The discriminator value. Must be `"cancel"`. + + + + + + +### fs/read_text_file + +Reads content from a text file in the client's file system. + +Only available if the client advertises the `fs.readTextFile` capability. +Allows the agent to access file contents within the client's environment. + +See protocol docs: [Client](https://agentclientprotocol.com/protocol/overview#client) + +#### ReadTextFileRequest + +Request to read content from a text file. + +Only available if the client supports the `fs.readTextFile` capability. + **Type:** Object **Properties:** @@ -1687,26 +1755,59 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) -ElicitationAction} required> - The user's action in response to the elicitation. + + Maximum number of lines to read. + + - Minimum: `0` + + + Line number to start reading from (1-based). - -### session/elicitation/complete + - Minimum: `0` -**UNSTABLE** + + + Absolute path to the file to read. + +SessionId} required> + The session ID for this request. + -This capability is not part of the spec yet, and may be removed or changed at any point. +#### ReadTextFileResponse -Notification that a URL-based elicitation has completed. +Response containing the contents of a text file. -#### ElicitationCompleteNotification +**Type:** Object -**UNSTABLE** +**Properties:** -This capability is not part of the spec yet, and may be removed or changed at any point. + + 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. -Notification sent by the agent when a URL-based elicitation is complete. +See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + + + + + + +### fs/write_text_file + +Writes content to a text file in the client's file system. + +Only available if the client advertises the `fs.writeTextFile` capability. +Allows the agent to create or modify files within the client's environment. + +See protocol docs: [Client](https://agentclientprotocol.com/protocol/overview#client) + +#### WriteTextFileRequest + +Request to write content to a text file. + +Only available if the client supports the `fs.writeTextFile` capability. **Type:** Object @@ -1720,8 +1821,31 @@ these keys. See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) -ElicitationId} required> - The ID of the elicitation that completed. + + The text content to write to the file. + + + Absolute path to the file to write. + +SessionId} required> + The session ID for this request. + + +#### WriteTextFileResponse + +Response to `fs/write_text_file` + +**Type:** Object + +**Properties:** + + + 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) + @@ -3175,55 +3299,6 @@ The user accepted the elicitation and provided content. The user-provided content, if any, as an object matching the requested schema. -## ElicitationAction - -**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. - -**Type:** Union - - -The user accepted and provided content. - - - - - The discriminator value. Must be `"accept"`. - - - The user-provided content, if any, as an object matching the requested schema. - - - - - - -The user declined the elicitation. - - - - - The discriminator value. Must be `"decline"`. - - - - - - -The elicitation was cancelled. - - - - - The discriminator value. Must be `"cancel"`. - - - - - ## ElicitationCapabilities **UNSTABLE** @@ -5754,6 +5829,31 @@ A session configuration option selector and its current state. **Type:** Union +**Shared properties:** + + + 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) + + +SessionConfigOptionCategory | null} > + Optional semantic category for this option (UX only). + + + Optional description for the Client to display to the user. + +SessionConfigId} required> + Unique identifier for the configuration option. + + + Human-readable label for the option. + + +**Variants:** + Single-value selector (dropdown). diff --git a/docs/rfds/elicitation.mdx b/docs/rfds/elicitation.mdx index 0987663e..ab69c108 100644 --- a/docs/rfds/elicitation.mdx +++ b/docs/rfds/elicitation.mdx @@ -1,5 +1,5 @@ --- -title: "Elicitation: Structured User Input During Sessions" +title: "Elicitation: Structured User Input" --- - Author(s): [@yordis](https://github.com/yordis) @@ -7,7 +7,7 @@ title: "Elicitation: Structured User Input During Sessions" ## Elevator pitch -Add support for agents to request structured information from users during a session through a standardized elicitation mechanism, aligned with [MCP's elicitation feature](https://modelcontextprotocol.io/specification/draft/client/elicitation). This allows agents to ask follow-up questions, collect authentication credentials, gather preferences, and request required information without side-channel communication or ad-hoc client UI implementations. +Add support for agents to request structured information from users through a standardized elicitation mechanism, aligned with [MCP's elicitation feature](https://modelcontextprotocol.io/specification/draft/client/elicitation). This allows agents to ask follow-up questions, collect authentication credentials, gather preferences, and request required information without side-channel communication or ad-hoc client UI implementations. ## Status quo @@ -41,7 +41,7 @@ The mechanism would: - **Form mode** (in-band): Structured data collection via JSON Schema forms - **URL mode** (out-of-band): Browser-based flows for sensitive operations like OAuth (addressing PR #330 authentication pain points) -3. **Request/response pattern**: Agents send elicitation requests via a `session/elicitation` method and receive responses. The agent controls when to send requests and whether to wait for responses before proceeding. Unlike Session Config Options (which are persistent), elicitation requests are transient. +3. **Request/response pattern**: Agents send elicitation requests via an `elicitation/create` method and receive responses. The agent controls when to send requests and whether to wait for responses before proceeding. Unlike Session Config Options (which are persistent), elicitation requests are transient. 4. **Support client capability negotiation**: Clients declare elicitation support via a structured capability object that distinguishes between `form`-based and `url`-based elicitation (following MCP's capability model). This allows clients to support one or both modalities, enables agents to pass capabilities along to MCP servers, and handles graceful degradation when clients have limited elicitation support. @@ -70,12 +70,13 @@ Clients can: ### Alignment with MCP -This proposal follows MCP's draft elicitation specification. See [MCP Elicitation Specification](https://modelcontextprotocol.io/specification/draft/client/elicitation) for detailed guidance. ACP uses the same JSON Schema constraint approach and capability model, adapted for our session/turn-based architecture. +This proposal follows MCP's draft elicitation specification. See [MCP Elicitation Specification](https://modelcontextprotocol.io/specification/draft/client/elicitation) for detailed guidance. ACP uses the same JSON Schema constraint approach and capability model, adapted for ACP interactions. Key differences from MCP: -- MCP elicitation is tool-call-scoped; ACP elicitation is session-scoped -- ACP uses `session/elicitation` method; MCP uses `elicitation/create` +- MCP elicitation is tool-call-scoped; ACP elicitation may be tool-call-scoped, session-scoped, or request-scoped +- ACP uses `elicitation/create` method (same as MCP) +- ACP includes an optional `toolCallId` field to support tool-call-scoped elicitations (e.g., when an agent receives an MCP elicitation during a tool call and needs to redirect it to the user) - ACP must integrate with existing Session Config Options (which also use schema constraints) ### Elicitation Request Structure @@ -346,7 +347,7 @@ Clients use this schema to generate appropriate input forms, validate user input ### Elicitation Request -The agent sends a `session/elicitation` request when it needs information from the user: +The agent sends an `elicitation/create` request when it needs information from the user: **Form mode example:** @@ -354,9 +355,10 @@ The agent sends a `session/elicitation` request when it needs information from t { "jsonrpc": "2.0", "id": 43, - "method": "session/elicitation", + "method": "elicitation/create", "params": { "sessionId": "...", + "toolCallId": "tc_123", "mode": "form", "message": "How would you like me to approach this refactoring?", "requestedSchema": { @@ -385,9 +387,9 @@ The agent sends a `session/elicitation` request when it needs information from t { "jsonrpc": "2.0", "id": 44, - "method": "session/elicitation", + "method": "elicitation/create", "params": { - "sessionId": "...", + "requestId": 12, "mode": "url", "elicitationId": "github-oauth-001", "url": "https://agent.example.com/connect?elicitationId=github-oauth-001", @@ -396,6 +398,37 @@ The agent sends a `session/elicitation` request when it needs information from t } ``` +The scope fields are flattened at the top level of the request. Elicitation supports two scoping variants: + +- **Session scope**: `sessionId` is set — tied to a specific session. Optionally includes `toolCallId` when tied to a specific tool call within that session (e.g., when an agent receives an elicitation from an MCP server during a tool call and needs to redirect it to the user). +- **Request scope**: `requestId` is set — tied to a specific JSON-RPC request outside of a session (e.g., auth/configuration phases before any session is started). + +**Request-scoped example:** + +```json +{ + "jsonrpc": "2.0", + "id": 45, + "method": "elicitation/create", + "params": { + "requestId": 12, + "mode": "form", + "message": "Please provide your workspace name to continue setup.", + "requestedSchema": { + "type": "object", + "properties": { + "workspaceName": { + "type": "string", + "title": "Workspace Name", + "description": "The name of your workspace" + } + }, + "required": ["workspaceName"] + } + } +} +``` + The client presents the elicitation UI to the user. For form mode, the client generates appropriate input UI based on the JSON Schema. For URL mode, the client opens the URL in a secure browser context. ### User Response @@ -460,7 +493,7 @@ sequenceDiagram participant Agent Note over Agent: Agent initiates elicitation - Agent->>Client: session/elicitation (mode: form) + Agent->>Client: elicitation/create (mode: form) Note over User,Client: Present elicitation UI User-->>Client: Provide requested information @@ -481,7 +514,7 @@ sequenceDiagram participant Agent Note over Agent: Agent initiates elicitation - Agent->>Client: session/elicitation (mode: url) + Agent->>Client: elicitation/create (mode: url) Client->>User: Present consent to open URL User-->>Client: Provide consent @@ -491,7 +524,7 @@ sequenceDiagram Note over User,UserAgent: User interaction UserAgent-->>Agent: Interaction complete - Agent-->>Client: notifications/elicitation/complete (optional) + Agent-->>Client: elicitation/complete (optional) Note over Agent: Continue processing with new information ``` @@ -519,19 +552,19 @@ sequenceDiagram Note over User,UserAgent: User interaction UserAgent-->>Agent: Interaction complete - Agent-->>Client: notifications/elicitation/complete (optional) + Agent-->>Client: elicitation/complete (optional) Client->>Agent: Retry original request (optional) ``` ### Completion Notifications for URL Mode -Following MCP, agents MAY send a `notifications/elicitation/complete` notification when an out-of-band interaction started by URL mode elicitation is completed: +Following MCP, agents MAY send an `elicitation/complete` notification when an out-of-band interaction started by URL mode elicitation is completed: ```json { "jsonrpc": "2.0", - "method": "notifications/elicitation/complete", + "method": "elicitation/complete", "params": { "elicitationId": "github-oauth-001" } @@ -584,7 +617,7 @@ Agents MUST return standard JSON-RPC errors for common failure cases: Clients MUST return standard JSON-RPC errors for common failure cases: -- When the agent sends a `session/elicitation` request with a mode not declared in client capabilities: `-32602` (Invalid params) +- When the agent sends an `elicitation/create` request with a mode not declared in client capabilities: `-32602` (Invalid params) ### Client Capabilities @@ -725,7 +758,7 @@ From PR #330: URL-mode elicitation allows agents to request authentication witho 7. User authenticates and grants permission 8. OAuth provider redirects back to the agent's redirect_uri 9. Agent exchanges the authorization code for tokens and stores them bound to the user's identity -10. Agent sends a `notifications/elicitation/complete` notification to inform the client +10. Agent sends an `elicitation/complete` notification to inform the client **Key guarantees**: diff --git a/schema/meta.unstable.json b/schema/meta.unstable.json index 167bba2f..24b26a94 100644 --- a/schema/meta.unstable.json +++ b/schema/meta.unstable.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.unstable.json b/schema/schema.unstable.json index 0a6fb847..2492de78 100644 --- a/schema/schema.unstable.json +++ b/schema/schema.unstable.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/src/bin/generate.rs b/src/bin/generate.rs index fd93616b..b461ec6e 100644 --- a/src/bin/generate.rs +++ b/src/bin/generate.rs @@ -339,12 +339,55 @@ starting with '$/' it is free to ignore the notification." writeln!(&mut self.output, "**Type:** Union").unwrap(); writeln!(&mut self.output).unwrap(); - let variants = definition - .get("oneOf") - .or_else(|| definition.get("anyOf")) - .and_then(|v| v.as_array()); + let discriminator_prop = definition + .get("discriminator") + .and_then(|d| d.get("propertyName")) + .and_then(|p| p.as_str()); + + let any_of = definition.get("anyOf").and_then(|v| v.as_array()); + let one_of = definition.get("oneOf").and_then(|v| v.as_array()); + + // Union types with top-level "properties" alongside "oneOf"/"anyOf" use them + // as shared properties that apply to all variants (e.g., _meta, message). + // The discriminator property (if any) is excluded since it's per-variant. + let has_shared_props = if let Some(shared_props) = + definition.get("properties").and_then(|v| v.as_object()) + { + let filtered_props: serde_json::Map = shared_props + .iter() + .filter(|(key, _)| Some(key.as_str()) != discriminator_prop) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + + if filtered_props.is_empty() { + false + } else { + writeln!(&mut self.output, "**Shared properties:**").unwrap(); + writeln!(&mut self.output).unwrap(); + self.document_properties_as_fields(&filtered_props, definition, 0); + writeln!(&mut self.output).unwrap(); + true + } + } else { + false + }; + + // Print a single "Variants:" label before all variant groups when + // there is surrounding context that benefits from a separator + // (shared properties above, or multiple variant groups). + if has_shared_props || (any_of.is_some() && one_of.is_some()) { + writeln!(&mut self.output, "**Variants:**").unwrap(); + writeln!(&mut self.output).unwrap(); + } + + if let Some(variants) = any_of { + for variant in variants { + self.document_variant_table_row(variant); + } + writeln!(&mut self.output).unwrap(); + } - if let Some(variants) = variants { + if let Some(variants) = one_of { for variant in variants { self.document_variant_table_row(variant); } @@ -377,6 +420,8 @@ starting with '$/' it is free to ignore the notification." if let Some(const_val) = discriminator { write!(&mut self.output, "{const_val}").unwrap(); + } else if let Some(title) = variant.get("title").and_then(|t| t.as_str()) { + write!(&mut self.output, "{title}").unwrap(); } else { write!(&mut self.output, "Object").unwrap(); } @@ -1061,10 +1106,10 @@ starting with '$/' it is free to ignore the notification." "terminal/wait_for_exit" => self.client.get("WaitForTerminalExitRequest").unwrap(), "terminal/kill" => self.client.get("KillTerminalRequest").unwrap(), #[cfg(feature = "unstable_elicitation")] - "session/elicitation" => self.client.get("ElicitationRequest").unwrap(), + "elicitation/create" => self.client.get("CreateElicitationRequest").unwrap(), #[cfg(feature = "unstable_elicitation")] - "session/elicitation/complete" => { - self.client.get("ElicitationCompleteNotification").unwrap() + "elicitation/complete" => { + self.client.get("CompleteElicitationNotification").unwrap() } _ => panic!("Introduced a method? Add it here :)"), } @@ -1190,4 +1235,166 @@ starting with '$/' it is free to ignore the notification." side_docs } + + #[cfg(test)] + mod tests { + use super::MarkdownGenerator; + use serde_json::json; + + #[test] + fn document_union_includes_shared_properties() { + let mut generator = MarkdownGenerator::new(); + let definition = json!({ + "description": "Example union.", + "discriminator": { + "propertyName": "mode" + }, + "properties": { + "message": { + "type": "string", + "description": "Shared message." + }, + "mode": { + "type": "string", + "description": "The discriminator." + } + }, + "required": ["message", "mode"], + "oneOf": [ + { + "description": "First variant.", + "properties": { + "mode": { + "const": "form", + "type": "string" + } + }, + "required": ["mode"], + "type": "object" + }, + { + "description": "Second variant.", + "properties": { + "mode": { + "const": "url", + "type": "string" + } + }, + "required": ["mode"], + "type": "object" + } + ] + }); + + generator.document_type(4, "ExampleUnion", &definition); + + assert!(generator.output.contains("**Shared properties:**")); + assert!( + generator + .output + .contains("") + ); + assert!(generator.output.contains("Shared message.")); + assert!(generator.output.contains("**Variants:**")); + assert!( + generator + .output + .contains("") + ); + assert!( + generator + .output + .contains(""), + ); + let shared_section = generator.output.split("**Variants:**").next().unwrap_or(""); + assert!( + !shared_section.contains(""), + "should use title 'Session' not 'Object'" + ); + assert!( + generator + .output + .contains(""), + "should use title 'Request' not 'Object'" + ); + + // oneOf mode variants rendered under Variants + assert!(generator.output.contains("**Variants:**")); + assert!(generator.output.contains(" CLIENT_METHOD_NAMES.terminal_wait_for_exit, Self::KillTerminalRequest(_) => CLIENT_METHOD_NAMES.terminal_kill, #[cfg(feature = "unstable_elicitation")] - Self::ElicitationRequest(_) => CLIENT_METHOD_NAMES.session_elicitation, + Self::CreateElicitationRequest(_) => CLIENT_METHOD_NAMES.elicitation_create, Self::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -1941,7 +1941,7 @@ pub enum ClientResponse { WaitForTerminalExitResponse(WaitForTerminalExitResponse), KillTerminalResponse(#[serde(default)] KillTerminalResponse), #[cfg(feature = "unstable_elicitation")] - ElicitationResponse(ElicitationResponse), + CreateElicitationResponse(CreateElicitationResponse), ExtMethodResponse(ExtResponse), } @@ -1975,7 +1975,7 @@ pub enum AgentNotification { /// /// Notification that a URL-based elicitation has completed. #[cfg(feature = "unstable_elicitation")] - ElicitationCompleteNotification(ElicitationCompleteNotification), + CompleteElicitationNotification(CompleteElicitationNotification), /// Handles extension notifications from the agent. /// /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec. @@ -1993,9 +1993,7 @@ impl AgentNotification { match self { Self::SessionNotification(_) => CLIENT_METHOD_NAMES.session_update, #[cfg(feature = "unstable_elicitation")] - Self::ElicitationCompleteNotification(_) => { - CLIENT_METHOD_NAMES.session_elicitation_complete - } + Self::CompleteElicitationNotification(_) => CLIENT_METHOD_NAMES.elicitation_complete, Self::ExtNotification(ext_notification) => &ext_notification.method, } } diff --git a/src/elicitation.rs b/src/elicitation.rs index 55b1d316..4ce13b9c 100644 --- a/src/elicitation.rs +++ b/src/elicitation.rs @@ -11,8 +11,9 @@ use derive_more::{Display, From}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::client::{SESSION_ELICITATION_COMPLETE, SESSION_ELICITATION_METHOD_NAME}; -use crate::{IntoOption, Meta, SessionId}; +use crate::client::{ELICITATION_COMPLETE_NOTIFICATION, ELICITATION_CREATE_METHOD_NAME}; +use crate::tool_call::ToolCallId; +use crate::{IntoOption, Meta, RequestId, SessionId}; /// **UNSTABLE** /// @@ -862,6 +863,37 @@ impl ElicitationUrlCapabilities { } } +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// The scope of an elicitation request, determining what context it's tied to. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(untagged)] +#[non_exhaustive] +pub enum ElicitationScope { + /// Tied to a session, optionally to a specific tool call within that session. + /// + /// When `tool_call_id` is set, the elicitation is tied to a specific tool call. + /// This is useful when an agent receives an elicitation from an MCP server + /// during a tool call and needs to redirect it to the user. + #[serde(rename_all = "camelCase")] + Session { + /// The session this elicitation is tied to. + session_id: SessionId, + /// Optional tool call within the session. + #[serde(skip_serializing_if = "Option::is_none")] + tool_call_id: Option, + }, + /// Tied to a specific JSON-RPC request outside of a session + /// (e.g., during auth/configuration phases before any session is started). + #[serde(rename_all = "camelCase")] + Request { + /// The request this elicitation is tied to. + request_id: RequestId, + }, +} + /// **UNSTABLE** /// /// This capability is not part of the spec yet, and may be removed or changed at any point. @@ -870,13 +902,15 @@ impl ElicitationUrlCapabilities { /// /// The agent sends this to the client to request information from the user, /// either via a form or by directing them to a URL. +/// Elicitations are tied to a session (optionally a tool call) or a request. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] -#[schemars(extend("x-side" = "client", "x-method" = SESSION_ELICITATION_METHOD_NAME))] +#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))] #[serde(rename_all = "camelCase")] #[non_exhaustive] -pub struct ElicitationRequest { - /// The session ID for this request. - pub session_id: SessionId, +pub struct CreateElicitationRequest { + /// The scope this elicitation is tied to. + #[serde(flatten)] + pub scope: ElicitationScope, /// The elicitation mode and its mode-specific fields. #[serde(flatten)] pub mode: ElicitationMode, @@ -891,15 +925,11 @@ pub struct ElicitationRequest { pub meta: Option, } -impl ElicitationRequest { +impl CreateElicitationRequest { #[must_use] - pub fn new( - session_id: impl Into, - mode: ElicitationMode, - message: impl Into, - ) -> Self { + pub fn new(scope: ElicitationScope, mode: ElicitationMode, message: impl Into) -> Self { Self { - session_id: session_id.into(), + scope, mode, message: message.into(), meta: None, @@ -986,11 +1016,12 @@ impl ElicitationUrlMode { /// /// Response from the client to an elicitation request. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] -#[schemars(extend("x-side" = "client", "x-method" = SESSION_ELICITATION_METHOD_NAME))] +#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_CREATE_METHOD_NAME))] #[serde(rename_all = "camelCase")] #[non_exhaustive] -pub struct ElicitationResponse { +pub struct CreateElicitationResponse { /// The user's action in response to the elicitation. + #[serde(flatten)] pub action: ElicitationAction, /// 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 @@ -1001,7 +1032,7 @@ pub struct ElicitationResponse { pub meta: Option, } -impl ElicitationResponse { +impl CreateElicitationResponse { #[must_use] pub fn new(action: ElicitationAction) -> Self { Self { action, meta: None } @@ -1139,10 +1170,10 @@ impl Default for ElicitationAcceptAction { /// /// Notification sent by the agent when a URL-based elicitation is complete. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[schemars(extend("x-side" = "client", "x-method" = SESSION_ELICITATION_COMPLETE))] +#[schemars(extend("x-side" = "client", "x-method" = ELICITATION_COMPLETE_NOTIFICATION))] #[serde(rename_all = "camelCase")] #[non_exhaustive] -pub struct ElicitationCompleteNotification { +pub struct CompleteElicitationNotification { /// The ID of the elicitation that completed. pub elicitation_id: ElicitationId, /// The _meta property is reserved by ACP to allow clients and agents to attach additional @@ -1154,7 +1185,7 @@ pub struct ElicitationCompleteNotification { pub meta: Option, } -impl ElicitationCompleteNotification { +impl CompleteElicitationNotification { #[must_use] pub fn new(elicitation_id: impl Into) -> Self { Self { @@ -1250,14 +1281,18 @@ mod tests { #[test] fn form_mode_request_serialization() { let schema = ElicitationSchema::new().string("name", true); - let req = ElicitationRequest::new( - "sess_1", + let req = CreateElicitationRequest::new( + ElicitationScope::Session { + session_id: SessionId::new("sess_1"), + tool_call_id: None, + }, ElicitationMode::Form(ElicitationFormMode::new(schema)), "Please enter your name", ); let json = serde_json::to_value(&req).unwrap(); assert_eq!(json["sessionId"], "sess_1"); + assert!(json.get("toolCallId").is_none()); assert_eq!(json["mode"], "form"); assert_eq!(json["message"], "Please enter your name"); assert!(json["requestedSchema"].is_object()); @@ -1267,16 +1302,25 @@ mod tests { "string" ); - let roundtripped: ElicitationRequest = serde_json::from_value(json).unwrap(); - assert_eq!(roundtripped.session_id, SessionId::new("sess_1")); + let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap(); + assert_eq!( + roundtripped.scope, + ElicitationScope::Session { + session_id: SessionId::new("sess_1"), + tool_call_id: None, + } + ); assert_eq!(roundtripped.message, "Please enter your name"); assert!(matches!(roundtripped.mode, ElicitationMode::Form(_))); } #[test] fn url_mode_request_serialization() { - let req = ElicitationRequest::new( - "sess_2", + let req = CreateElicitationRequest::new( + ElicitationScope::Session { + session_id: SessionId::new("sess_2"), + tool_call_id: Some(ToolCallId::new("tc_1")), + }, ElicitationMode::Url(ElicitationUrlMode::new( "elic_1", "https://example.com/auth", @@ -1286,19 +1330,26 @@ mod tests { let json = serde_json::to_value(&req).unwrap(); assert_eq!(json["sessionId"], "sess_2"); + assert_eq!(json["toolCallId"], "tc_1"); assert_eq!(json["mode"], "url"); assert_eq!(json["elicitationId"], "elic_1"); assert_eq!(json["url"], "https://example.com/auth"); assert_eq!(json["message"], "Please authenticate"); - let roundtripped: ElicitationRequest = serde_json::from_value(json).unwrap(); - assert_eq!(roundtripped.session_id, SessionId::new("sess_2")); + let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap(); + assert_eq!( + roundtripped.scope, + ElicitationScope::Session { + session_id: SessionId::new("sess_2"), + tool_call_id: Some(ToolCallId::new("tc_1")), + } + ); assert!(matches!(roundtripped.mode, ElicitationMode::Url(_))); } #[test] fn response_accept_serialization() { - let resp = ElicitationResponse::new(ElicitationAction::Accept( + let resp = CreateElicitationResponse::new(ElicitationAction::Accept( ElicitationAcceptAction::new().content(BTreeMap::from([( "name".to_string(), ElicitationContentValue::from("Alice"), @@ -1306,10 +1357,10 @@ mod tests { )); let json = serde_json::to_value(&resp).unwrap(); - assert_eq!(json["action"]["action"], "accept"); - assert_eq!(json["action"]["content"]["name"], "Alice"); + assert_eq!(json["action"], "accept"); + assert_eq!(json["content"]["name"], "Alice"); - let roundtripped: ElicitationResponse = serde_json::from_value(json).unwrap(); + let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap(); assert!(matches!( roundtripped.action, ElicitationAction::Accept(ElicitationAcceptAction { @@ -1321,34 +1372,170 @@ mod tests { #[test] fn response_decline_serialization() { - let resp = ElicitationResponse::new(ElicitationAction::Decline); + let resp = CreateElicitationResponse::new(ElicitationAction::Decline); let json = serde_json::to_value(&resp).unwrap(); - assert_eq!(json["action"]["action"], "decline"); + assert_eq!(json["action"], "decline"); - let roundtripped: ElicitationResponse = serde_json::from_value(json).unwrap(); + let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap(); assert!(matches!(roundtripped.action, ElicitationAction::Decline)); } #[test] fn response_cancel_serialization() { - let resp = ElicitationResponse::new(ElicitationAction::Cancel); + let resp = CreateElicitationResponse::new(ElicitationAction::Cancel); + + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["action"], "cancel"); + + let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap(); + assert!(matches!(roundtripped.action, ElicitationAction::Cancel)); + } + + #[test] + fn session_only_request_serialization() { + let req = CreateElicitationRequest::new( + ElicitationScope::Session { + session_id: SessionId::new("sess_1"), + tool_call_id: None, + }, + ElicitationMode::Form(ElicitationFormMode::new( + ElicitationSchema::new().string("name", true), + )), + "Enter your name", + ); + + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["sessionId"], "sess_1"); + assert!(json.get("toolCallId").is_none()); + + let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap(); + assert_eq!( + roundtripped.scope, + ElicitationScope::Session { + session_id: SessionId::new("sess_1"), + tool_call_id: None, + } + ); + } + #[test] + fn request_scope_request_serialization() { + let req = CreateElicitationRequest::new( + ElicitationScope::Request { + request_id: RequestId::Number(99), + }, + ElicitationMode::Form(ElicitationFormMode::new( + ElicitationSchema::new().string("workspace", true), + )), + "Enter workspace name", + ); + + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["requestId"], 99); + assert!(json.get("sessionId").is_none()); + + let roundtripped: CreateElicitationRequest = serde_json::from_value(json).unwrap(); + assert_eq!( + roundtripped.scope, + ElicitationScope::Request { + request_id: RequestId::Number(99), + } + ); + } + + /// `ClientResponse` is `#[serde(untagged)]` with `WriteTextFileResponse` (which has + /// `#[serde(default)]`) listed first, so standalone deserialization is ambiguous. + /// In practice, the RPC layer selects the correct variant based on the originating + /// request method. These tests verify that serialization through `ClientResponse` + /// produces the correct flattened wire format and round-trips back via the + /// concrete `CreateElicitationResponse` type. + #[test] + fn client_response_serialization_accept() { + use crate::ClientResponse; + + let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new( + ElicitationAction::Accept(ElicitationAcceptAction::new().content(BTreeMap::from([( + "name".to_string(), + ElicitationContentValue::from("Alice"), + )]))), + )); + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["action"], "accept"); + assert_eq!(json["content"]["name"], "Alice"); + + // Round-trip back through the concrete type + let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap(); + assert!(matches!(roundtripped.action, ElicitationAction::Accept(_))); + } + + #[test] + fn client_response_serialization_decline() { + use crate::ClientResponse; + + let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new( + ElicitationAction::Decline, + )); let json = serde_json::to_value(&resp).unwrap(); - assert_eq!(json["action"]["action"], "cancel"); + assert_eq!(json["action"], "decline"); + + let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap(); + assert!(matches!(roundtripped.action, ElicitationAction::Decline)); + } - let roundtripped: ElicitationResponse = serde_json::from_value(json).unwrap(); + #[test] + fn client_response_serialization_cancel() { + use crate::ClientResponse; + + let resp = ClientResponse::CreateElicitationResponse(CreateElicitationResponse::new( + ElicitationAction::Cancel, + )); + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["action"], "cancel"); + + let roundtripped: CreateElicitationResponse = serde_json::from_value(json).unwrap(); assert!(matches!(roundtripped.action, ElicitationAction::Cancel)); } + /// Guard against serde regressions with the `flatten` + `untagged` + `flatten` (tagged) + /// combination. Extra fields in the JSON must not cause deserialization failures. + #[test] + fn request_tolerates_extra_fields() { + let json = json!({ + "sessionId": "sess_1", + "mode": "form", + "message": "Enter your name", + "requestedSchema": { + "type": "object", + "properties": { + "name": { "type": "string", "title": "Name" } + }, + "required": ["name"] + }, + "unknownStringField": "hello", + "unknownNumberField": 42 + }); + + let req: CreateElicitationRequest = serde_json::from_value(json).unwrap(); + assert_eq!( + req.scope, + ElicitationScope::Session { + session_id: SessionId::new("sess_1"), + tool_call_id: None, + } + ); + assert_eq!(req.message, "Enter your name"); + assert!(matches!(req.mode, ElicitationMode::Form(_))); + } + #[test] fn completion_notification_serialization() { - let notif = ElicitationCompleteNotification::new("elic_1"); + let notif = CompleteElicitationNotification::new("elic_1"); let json = serde_json::to_value(¬if).unwrap(); assert_eq!(json["elicitationId"], "elic_1"); - let roundtripped: ElicitationCompleteNotification = serde_json::from_value(json).unwrap(); + let roundtripped: CompleteElicitationNotification = serde_json::from_value(json).unwrap(); assert_eq!(roundtripped.elicitation_id, ElicitationId::new("elic_1")); } @@ -1642,11 +1829,9 @@ mod tests { #[test] fn response_accept_rejects_non_object_content() { - let err = serde_json::from_value::(json!({ - "action": { - "action": "accept", - "content": "Alice" - } + let err = serde_json::from_value::(json!({ + "action": "accept", + "content": "Alice" })) .unwrap_err(); @@ -1655,13 +1840,11 @@ mod tests { #[test] fn response_accept_rejects_nested_object_content() { - let err = serde_json::from_value::(json!({ - "action": { - "action": "accept", - "content": { - "profile": { - "name": "Alice" - } + let err = serde_json::from_value::(json!({ + "action": "accept", + "content": { + "profile": { + "name": "Alice" } } })) @@ -1672,7 +1855,7 @@ mod tests { #[test] fn response_accept_allows_primitive_and_string_array_content() { - let response = ElicitationResponse::new(ElicitationAction::Accept( + let response = CreateElicitationResponse::new(ElicitationAction::Accept( ElicitationAcceptAction::new().content(BTreeMap::from([ ("name".to_string(), ElicitationContentValue::from("Alice")), ("age".to_string(), ElicitationContentValue::from(30_i32)), @@ -1689,11 +1872,12 @@ mod tests { )); let json = serde_json::to_value(&response).unwrap(); - assert_eq!(json["action"]["content"]["name"], "Alice"); - assert_eq!(json["action"]["content"]["age"], 30); - assert_eq!(json["action"]["content"]["score"], 9.5); - assert_eq!(json["action"]["content"]["subscribed"], true); - assert_eq!(json["action"]["content"]["tags"][0], "rust"); - assert_eq!(json["action"]["content"]["tags"][1], "acp"); + assert_eq!(json["action"], "accept"); + assert_eq!(json["content"]["name"], "Alice"); + assert_eq!(json["content"]["age"], 30); + assert_eq!(json["content"]["score"], 9.5); + assert_eq!(json["content"]["subscribed"], true); + assert_eq!(json["content"]["tags"][0], "rust"); + assert_eq!(json["content"]["tags"][1], "acp"); } } diff --git a/src/rpc.rs b/src/rpc.rs index 4df1d484..ae52d616 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -216,8 +216,8 @@ impl Side for ClientSide { .map_err(Into::into) } #[cfg(feature = "unstable_elicitation")] - m if m == CLIENT_METHOD_NAMES.session_elicitation => serde_json::from_str(params.get()) - .map(AgentRequest::ElicitationRequest) + m if m == CLIENT_METHOD_NAMES.elicitation_create => serde_json::from_str(params.get()) + .map(AgentRequest::CreateElicitationRequest) .map_err(Into::into), _ => { if let Some(custom_method) = method.strip_prefix('_') { @@ -240,9 +240,9 @@ impl Side for ClientSide { .map(AgentNotification::SessionNotification) .map_err(Into::into), #[cfg(feature = "unstable_elicitation")] - m if m == CLIENT_METHOD_NAMES.session_elicitation_complete => { + m if m == CLIENT_METHOD_NAMES.elicitation_complete => { serde_json::from_str(params.get()) - .map(AgentNotification::ElicitationCompleteNotification) + .map(AgentNotification::CompleteElicitationNotification) .map_err(Into::into) } _ => {