diff --git a/Cargo.toml b/Cargo.toml index cce19d79..a1edcee7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ include = ["/src/**/*.rs", "/README.md", "/LICENSE", "/Cargo.toml"] unstable = [ "unstable_auth_methods", "unstable_cancel_request", + "unstable_elicitation", "unstable_logout", "unstable_session_fork", "unstable_session_model", @@ -28,6 +29,7 @@ unstable = [ ] unstable_auth_methods = [] unstable_cancel_request = [] +unstable_elicitation = [] unstable_logout = [] unstable_session_fork = [] unstable_session_model = [] diff --git a/docs/protocol/draft/schema.mdx b/docs/protocol/draft/schema.mdx index 06b879e7..390e52c4 100644 --- a/docs/protocol/draft/schema.mdx +++ b/docs/protocol/draft/schema.mdx @@ -1134,6 +1134,122 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte + +### 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. + +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. + +**Type:** Union + + +Form-based elicitation where the client renders a form from the provided schema. + + + + + The discriminator value. Must be `"form"`. + + + A JSON Schema describing the form fields to present to the user. + + + + + + +URL-based elicitation where the client directs the user to a URL. + + + +ElicitationId} + required +> + The unique identifier for this elicitation. + + + The discriminator value. Must be `"url"`. + + + The URL to direct the user to. + + + + + +#### ElicitationResponse + +**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. + +**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) + + +ElicitationAction} required> + The user's action in response to the elicitation. + + + +### session/elicitation/complete + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Notification that a URL-based elicitation has completed. + +#### ElicitationCompleteNotification + +**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. + +**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) + + +ElicitationId} required> + The ID of the elicitation that completed. + + ### session/request_permission @@ -2157,6 +2273,15 @@ in its `InitializeResponse`. - Default: `{"terminal":false}` + +ElicitationCapabilities | null} > + **UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Elicitation capabilities supported by the client. +Determines which elicitation modes the agent may use. + FileSystemCapabilities} > File system capabilities supported by the client. @@ -2484,6 +2609,134 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte The file path being modified. +## 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. + + + + + + +The user declined the elicitation. + + + + + The discriminator value. Must be `"decline"`. + + + + + + +The elicitation was cancelled. + + + + + The discriminator value. Must be `"cancel"`. + + + + + +## ElicitationCapabilities + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Elicitation capabilities supported by the client. + +**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) + + +ElicitationFormCapabilities | null} > + Whether the client supports form-based elicitation. + +ElicitationUrlCapabilities | null} > + Whether the client supports URL-based elicitation. + + +## ElicitationFormCapabilities + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Form-based elicitation capabilities. + +**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) + + + +## ElicitationId + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Unique identifier for an elicitation. + +**Type:** `string` + +## ElicitationUrlCapabilities + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +URL-based elicitation capabilities. + +**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) + + + ## EmbeddedResource The contents of a resource, embedded into a prompt or tool call result. @@ -2657,6 +2910,15 @@ because of resource constraints or shutdown. **Resource not found**: A given resource, such as a file, was not found. + +**URL elicitation required**: **UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +The agent requires user input via a URL-based elicitation before it can proceed. + + + Other undefined error code. diff --git a/schema/meta.unstable.json b/schema/meta.unstable.json index ae8e1b86..6198214a 100644 --- a/schema/meta.unstable.json +++ b/schema/meta.unstable.json @@ -18,6 +18,8 @@ "clientMethods": { "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 5af0bca6..72dfe94a 100644 --- a/schema/schema.unstable.json +++ b/schema/schema.unstable.json @@ -98,6 +98,15 @@ "description": "Handles session update notifications from the agent.\n\nThis is a notification endpoint (no response expected) that receives\nreal-time updates about session progress, including message chunks,\ntool calls, and execution plans.\n\nNote: Clients SHOULD continue accepting tool call updates even after\nsending a `session/cancel` notification, as the agent may send final\nupdates before responding with the cancelled stop reason.\n\nSee protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)", "title": "SessionNotification" }, + { + "allOf": [ + { + "$ref": "#/$defs/ElicitationCompleteNotification" + } + ], + "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" + }, { "allOf": [ { @@ -204,6 +213,15 @@ "description": "Kills the terminal command without releasing the terminal\n\nWhile `terminal/release` will also kill the command, this method will keep\nthe `TerminalId` valid so it can be used with other methods.\n\nThis method can be helpful when implementing command timeouts which terminate\nthe command as soon as elapsed, and then get the final output so it can be sent\nto the model.\n\nNote: Call `terminal/release` when `TerminalId` is no longer needed.\n\nSee protocol docs: [Terminals](https://agentclientprotocol.com/protocol/terminals)", "title": "KillTerminalRequest" }, + { + "allOf": [ + { + "$ref": "#/$defs/ElicitationRequest" + } + ], + "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" + }, { "allOf": [ { @@ -791,6 +809,17 @@ }, "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nAuthentication capabilities supported by the client.\nDetermines which authentication method types the agent may include\nin its `InitializeResponse`." }, + "elicitation": { + "anyOf": [ + { + "$ref": "#/$defs/ElicitationCapabilities" + }, + { + "type": "null" + } + ], + "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.\nDetermines which elicitation modes the agent may use." + }, "fs": { "allOf": [ { @@ -1075,6 +1104,14 @@ ], "title": "KillTerminalResponse" }, + { + "allOf": [ + { + "$ref": "#/$defs/ElicitationResponse" + } + ], + "title": "ElicitationResponse" + }, { "allOf": [ { @@ -1421,6 +1458,221 @@ "required": ["path", "newText"], "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": [ + { + "description": "The user accepted and provided content.", + "properties": { + "action": { + "const": "accept", + "type": "string" + }, + "content": { + "description": "The user-provided content, if any." + } + }, + "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": { + "_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"] + }, + "form": { + "anyOf": [ + { + "$ref": "#/$defs/ElicitationFormCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the client supports form-based elicitation." + }, + "url": { + "anyOf": [ + { + "$ref": "#/$defs/ElicitationUrlCapabilities" + }, + { + "type": "null" + } + ], + "description": "Whether the client supports URL-based elicitation." + } + }, + "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" + }, + "ElicitationFormCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nForm-based elicitation capabilities.", + "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" + }, + "ElicitationId": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nUnique identifier for an elicitation.", + "type": "string" + }, + "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": [ + { + "description": "Form-based elicitation where the client renders a form from the provided schema.", + "properties": { + "mode": { + "const": "form", + "type": "string" + }, + "requestedSchema": { + "description": "A JSON Schema describing the form fields to present to the user." + } + }, + "required": ["mode", "requestedSchema"], + "type": "object" + }, + { + "description": "URL-based elicitation where the client directs the user to a URL.", + "properties": { + "elicitationId": { + "allOf": [ + { + "$ref": "#/$defs/ElicitationId" + } + ], + "description": "The unique identifier for this elicitation." + }, + "mode": { + "const": "url", + "type": "string" + }, + "url": { + "description": "The URL to direct the user to.", + "type": "string" + } + }, + "required": ["mode", "elicitationId", "url"], + "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" + }, + "ElicitationUrlCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nURL-based elicitation capabilities.", + "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" + }, "EmbeddedResource": { "description": "The contents of a resource, embedded into a prompt or tool call result.", "properties": { @@ -1567,6 +1819,13 @@ "title": "Resource not found", "type": "integer" }, + { + "const": -32042, + "description": "**URL elicitation required**: **UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nThe agent requires user input via a URL-based elicitation before it can proceed.", + "format": "int32", + "title": "URL elicitation required", + "type": "integer" + }, { "description": "Other undefined error code.", "format": "int32", diff --git a/src/agent.rs b/src/agent.rs index 70e7b040..b8ce81de 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -3797,6 +3797,7 @@ pub(crate) const LOGOUT_METHOD_NAME: &str = "logout"; #[serde(untagged)] #[schemars(inline)] #[non_exhaustive] +#[allow(clippy::large_enum_variant)] pub enum ClientRequest { /// Establishes the connection with a client and negotiates protocol capabilities. /// diff --git a/src/bin/generate.rs b/src/bin/generate.rs index ccf6fb76..54dbff27 100644 --- a/src/bin/generate.rs +++ b/src/bin/generate.rs @@ -962,6 +962,12 @@ starting with '$/' it is free to ignore the notification." "terminal/release" => self.client.get("ReleaseTerminalRequest").unwrap(), "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(), + #[cfg(feature = "unstable_elicitation")] + "session/elicitation/complete" => { + self.client.get("ElicitationCompleteNotification").unwrap() + } _ => panic!("Introduced a method? Add it here :)"), } } diff --git a/src/client.rs b/src/client.rs index c22c82fa..6077415a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -746,6 +746,415 @@ impl SelectedPermissionOutcome { } } +// Elicitation + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Unique identifier for an elicitation. +#[cfg(feature = "unstable_elicitation")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Hash, Display, From)] +#[serde(transparent)] +#[from(Arc, String, &'static str)] +#[non_exhaustive] +pub struct ElicitationId(pub Arc); + +#[cfg(feature = "unstable_elicitation")] +impl ElicitationId { + #[must_use] + pub fn new(id: impl Into>) -> Self { + Self(id.into()) + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Elicitation capabilities supported by the client. +#[cfg(feature = "unstable_elicitation")] +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ElicitationCapabilities { + /// Whether the client supports form-based elicitation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub form: Option, + /// Whether the client supports URL-based elicitation. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub url: Option, + /// 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) + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +#[cfg(feature = "unstable_elicitation")] +impl ElicitationCapabilities { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Whether the client supports form-based elicitation. + #[must_use] + pub fn form(mut self, form: impl IntoOption) -> Self { + self.form = form.into_option(); + self + } + + /// Whether the client supports URL-based elicitation. + #[must_use] + pub fn url(mut self, url: impl IntoOption) -> Self { + self.url = url.into_option(); + self + } + + /// 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) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Form-based elicitation capabilities. +#[cfg(feature = "unstable_elicitation")] +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ElicitationFormCapabilities { + /// 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) + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +#[cfg(feature = "unstable_elicitation")] +impl ElicitationFormCapabilities { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// 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) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// URL-based elicitation capabilities. +#[cfg(feature = "unstable_elicitation")] +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ElicitationUrlCapabilities { + /// 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) + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +#[cfg(feature = "unstable_elicitation")] +impl ElicitationUrlCapabilities { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// 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) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// 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. +#[cfg(feature = "unstable_elicitation")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +#[schemars(extend("x-side" = "client", "x-method" = SESSION_ELICITATION_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ElicitationRequest { + /// The session ID for this request. + pub session_id: SessionId, + /// The elicitation mode and its mode-specific fields. + #[serde(flatten)] + pub mode: ElicitationMode, + /// A human-readable message describing what input is needed. + pub message: String, + /// 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) + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +#[cfg(feature = "unstable_elicitation")] +impl ElicitationRequest { + #[must_use] + pub fn new( + session_id: impl Into, + mode: ElicitationMode, + message: impl Into, + ) -> Self { + Self { + session_id: session_id.into(), + mode, + message: message.into(), + meta: None, + } + } + + /// 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) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// The mode of elicitation, determining how user input is collected. +#[cfg(feature = "unstable_elicitation")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(tag = "mode", rename_all = "snake_case")] +#[schemars(extend("discriminator" = {"propertyName": "mode"}))] +#[non_exhaustive] +pub enum ElicitationMode { + /// Form-based elicitation where the client renders a form from the provided schema. + #[serde(rename_all = "camelCase")] + Form { + /// A JSON Schema describing the form fields to present to the user. + requested_schema: serde_json::Value, + }, + /// URL-based elicitation where the client directs the user to a URL. + #[serde(rename_all = "camelCase")] + Url { + /// The unique identifier for this elicitation. + elicitation_id: ElicitationId, + /// The URL to direct the user to. + url: 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. +#[cfg(feature = "unstable_elicitation")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +#[schemars(extend("x-side" = "client", "x-method" = SESSION_ELICITATION_METHOD_NAME))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ElicitationResponse { + /// The user's action in response to the elicitation. + 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 + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +#[cfg(feature = "unstable_elicitation")] +impl ElicitationResponse { + #[must_use] + pub fn new(action: ElicitationAction) -> Self { + Self { action, meta: None } + } + + /// 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) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **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. +#[cfg(feature = "unstable_elicitation")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)] +#[serde(tag = "action", rename_all = "snake_case")] +#[schemars(extend("discriminator" = {"propertyName": "action"}))] +#[non_exhaustive] +pub enum ElicitationAction { + /// The user accepted and provided content. + Accept { + /// The user-provided content, if any. + #[serde(default, skip_serializing_if = "Option::is_none")] + content: Option, + }, + /// The user declined the elicitation. + Decline, + /// The elicitation was cancelled. + Cancel, +} + +/// **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. +#[cfg(feature = "unstable_elicitation")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[schemars(extend("x-side" = "client", "x-method" = SESSION_ELICITATION_COMPLETE))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ElicitationCompleteNotification { + /// 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 + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, +} + +#[cfg(feature = "unstable_elicitation")] +impl ElicitationCompleteNotification { + #[must_use] + pub fn new(elicitation_id: impl Into) -> Self { + Self { + elicitation_id: elicitation_id.into(), + meta: None, + } + } + + /// 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) + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Data payload for the `UrlElicitationRequired` error, describing the URL elicitations +/// the user must complete. +#[cfg(feature = "unstable_elicitation")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct UrlElicitationRequiredData { + /// The URL elicitations the user must complete. + pub elicitations: Vec, +} + +#[cfg(feature = "unstable_elicitation")] +impl UrlElicitationRequiredData { + #[must_use] + pub fn new(elicitations: Vec) -> Self { + Self { elicitations } + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// A single URL elicitation item within the `UrlElicitationRequired` error data. +#[cfg(feature = "unstable_elicitation")] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct UrlElicitationRequiredItem { + /// The elicitation mode (always `"url"` for this item type). + pub mode: String, + /// The unique identifier for this elicitation. + pub elicitation_id: ElicitationId, + /// The URL the user should be directed to. + pub url: String, + /// A human-readable message describing what input is needed. + pub message: String, +} + +#[cfg(feature = "unstable_elicitation")] +impl UrlElicitationRequiredItem { + #[must_use] + pub fn new( + elicitation_id: impl Into, + url: impl Into, + message: impl Into, + ) -> Self { + Self { + mode: "url".to_string(), + elicitation_id: elicitation_id.into(), + url: url.into(), + message: message.into(), + } + } +} + // Write text file /// Request to write content to a text file. @@ -1486,6 +1895,15 @@ pub struct ClientCapabilities { #[cfg(feature = "unstable_auth_methods")] #[serde(default)] pub auth: AuthCapabilities, + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// Elicitation capabilities supported by the client. + /// Determines which elicitation modes the agent may use. + #[cfg(feature = "unstable_elicitation")] + #[serde(default, skip_serializing_if = "Option::is_none")] + pub elicitation: Option, /// 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. @@ -1530,6 +1948,19 @@ impl ClientCapabilities { self } + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// Elicitation capabilities supported by the client. + /// Determines which elicitation modes the agent may use. + #[cfg(feature = "unstable_elicitation")] + #[must_use] + pub fn elicitation(mut self, elicitation: impl IntoOption) -> Self { + self.elicitation = elicitation.into_option(); + self + } + /// 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. @@ -1679,6 +2110,12 @@ pub struct ClientMethodNames { pub terminal_wait_for_exit: &'static str, /// Method for killing a terminal. pub terminal_kill: &'static str, + /// Method for session elicitation. + #[cfg(feature = "unstable_elicitation")] + pub session_elicitation: &'static str, + /// Notification for elicitation completion. + #[cfg(feature = "unstable_elicitation")] + pub session_elicitation_complete: &'static str, } /// Constant containing all client method names. @@ -1692,6 +2129,10 @@ pub const CLIENT_METHOD_NAMES: ClientMethodNames = ClientMethodNames { terminal_release: TERMINAL_RELEASE_METHOD_NAME, terminal_wait_for_exit: TERMINAL_WAIT_FOR_EXIT_METHOD_NAME, terminal_kill: TERMINAL_KILL_METHOD_NAME, + #[cfg(feature = "unstable_elicitation")] + session_elicitation: SESSION_ELICITATION_METHOD_NAME, + #[cfg(feature = "unstable_elicitation")] + session_elicitation_complete: SESSION_ELICITATION_COMPLETE, }; /// Notification name for session updates. @@ -1712,6 +2153,12 @@ pub(crate) const TERMINAL_RELEASE_METHOD_NAME: &str = "terminal/release"; pub(crate) const TERMINAL_WAIT_FOR_EXIT_METHOD_NAME: &str = "terminal/wait_for_exit"; /// Method for killing a terminal. pub(crate) const TERMINAL_KILL_METHOD_NAME: &str = "terminal/kill"; +/// Method name for session elicitation. +#[cfg(feature = "unstable_elicitation")] +pub(crate) const SESSION_ELICITATION_METHOD_NAME: &str = "session/elicitation"; +/// Notification name for elicitation completion. +#[cfg(feature = "unstable_elicitation")] +pub(crate) const SESSION_ELICITATION_COMPLETE: &str = "session/elicitation/complete"; /// All possible requests that an agent can send to a client. /// @@ -1801,6 +2248,13 @@ pub enum AgentRequest { /// /// See protocol docs: [Terminals](https://agentclientprotocol.com/protocol/terminals) KillTerminalRequest(KillTerminalRequest), + /// **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. + #[cfg(feature = "unstable_elicitation")] + ElicitationRequest(ElicitationRequest), /// Handles extension method requests from the agent. /// /// Allows the Agent to send an arbitrary request that is not part of the ACP spec. @@ -1824,6 +2278,8 @@ impl AgentRequest { Self::ReleaseTerminalRequest(_) => CLIENT_METHOD_NAMES.terminal_release, Self::WaitForTerminalExitRequest(_) => 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::ExtMethodRequest(ext_request) => &ext_request.method, } } @@ -1848,6 +2304,8 @@ pub enum ClientResponse { ReleaseTerminalResponse(#[serde(default)] ReleaseTerminalResponse), WaitForTerminalExitResponse(WaitForTerminalExitResponse), KillTerminalResponse(#[serde(default)] KillTerminalResponse), + #[cfg(feature = "unstable_elicitation")] + ElicitationResponse(ElicitationResponse), ExtMethodResponse(ExtResponse), } @@ -1875,6 +2333,13 @@ pub enum AgentNotification { /// /// See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output) SessionNotification(SessionNotification), + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// Notification that a URL-based elicitation has completed. + #[cfg(feature = "unstable_elicitation")] + ElicitationCompleteNotification(ElicitationCompleteNotification), /// Handles extension notifications from the agent. /// /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec. @@ -1891,6 +2356,10 @@ impl AgentNotification { pub fn method(&self) -> &str { match self { Self::SessionNotification(_) => CLIENT_METHOD_NAMES.session_update, + #[cfg(feature = "unstable_elicitation")] + Self::ElicitationCompleteNotification(_) => { + CLIENT_METHOD_NAMES.session_elicitation_complete + } Self::ExtNotification(ext_notification) => &ext_notification.method, } } @@ -1955,4 +2424,165 @@ mod tests { json!({}) ); } + + #[cfg(feature = "unstable_elicitation")] + mod elicitation_tests { + use super::*; + use serde_json::json; + + #[test] + fn form_mode_request_serialization() { + let req = ElicitationRequest::new( + "sess_1", + ElicitationMode::Form { + requested_schema: json!({"type": "object", "properties": {"name": {"type": "string"}}}), + }, + "Please enter your name", + ); + + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["sessionId"], "sess_1"); + assert_eq!(json["mode"], "form"); + assert_eq!(json["message"], "Please enter your name"); + assert!(json["requestedSchema"].is_object()); + + let roundtripped: ElicitationRequest = serde_json::from_value(json).unwrap(); + assert_eq!(roundtripped.session_id, SessionId::new("sess_1")); + 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", + ElicitationMode::Url { + elicitation_id: ElicitationId::new("elic_1"), + url: "https://example.com/auth".to_string(), + }, + "Please authenticate", + ); + + let json = serde_json::to_value(&req).unwrap(); + assert_eq!(json["sessionId"], "sess_2"); + 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")); + assert!(matches!(roundtripped.mode, ElicitationMode::Url { .. })); + } + + #[test] + fn response_accept_serialization() { + let resp = ElicitationResponse::new(ElicitationAction::Accept { + content: Some(json!({"name": "Alice"})), + }); + + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["action"]["action"], "accept"); + assert_eq!(json["action"]["content"]["name"], "Alice"); + + let roundtripped: ElicitationResponse = serde_json::from_value(json).unwrap(); + assert!(matches!( + roundtripped.action, + ElicitationAction::Accept { content: Some(_) } + )); + } + + #[test] + fn response_decline_serialization() { + let resp = ElicitationResponse::new(ElicitationAction::Decline); + + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["action"]["action"], "decline"); + + let roundtripped: ElicitationResponse = serde_json::from_value(json).unwrap(); + assert!(matches!(roundtripped.action, ElicitationAction::Decline)); + } + + #[test] + fn response_cancel_serialization() { + let resp = ElicitationResponse::new(ElicitationAction::Cancel); + + let json = serde_json::to_value(&resp).unwrap(); + assert_eq!(json["action"]["action"], "cancel"); + + let roundtripped: ElicitationResponse = serde_json::from_value(json).unwrap(); + assert!(matches!(roundtripped.action, ElicitationAction::Cancel)); + } + + #[test] + fn completion_notification_serialization() { + let notif = ElicitationCompleteNotification::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(); + assert_eq!(roundtripped.elicitation_id, ElicitationId::new("elic_1")); + } + + #[test] + fn capabilities_form_only() { + let caps = ElicitationCapabilities::new().form(ElicitationFormCapabilities::new()); + + let json = serde_json::to_value(&caps).unwrap(); + assert!(json["form"].is_object()); + assert!(json.get("url").is_none()); + + let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap(); + assert!(roundtripped.form.is_some()); + assert!(roundtripped.url.is_none()); + } + + #[test] + fn capabilities_url_only() { + let caps = ElicitationCapabilities::new().url(ElicitationUrlCapabilities::new()); + + let json = serde_json::to_value(&caps).unwrap(); + assert!(json.get("form").is_none()); + assert!(json["url"].is_object()); + + let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap(); + assert!(roundtripped.form.is_none()); + assert!(roundtripped.url.is_some()); + } + + #[test] + fn capabilities_both() { + let caps = ElicitationCapabilities::new() + .form(ElicitationFormCapabilities::new()) + .url(ElicitationUrlCapabilities::new()); + + let json = serde_json::to_value(&caps).unwrap(); + assert!(json["form"].is_object()); + assert!(json["url"].is_object()); + + let roundtripped: ElicitationCapabilities = serde_json::from_value(json).unwrap(); + assert!(roundtripped.form.is_some()); + assert!(roundtripped.url.is_some()); + } + + #[test] + fn url_elicitation_required_data_serialization() { + let data = UrlElicitationRequiredData::new(vec![UrlElicitationRequiredItem::new( + "elic_1", + "https://example.com/auth", + "Please authenticate", + )]); + + let json = serde_json::to_value(&data).unwrap(); + assert_eq!(json["elicitations"][0]["mode"], "url"); + assert_eq!(json["elicitations"][0]["elicitationId"], "elic_1"); + assert_eq!(json["elicitations"][0]["url"], "https://example.com/auth"); + + let roundtripped: UrlElicitationRequiredData = serde_json::from_value(json).unwrap(); + assert_eq!(roundtripped.elicitations.len(), 1); + assert_eq!(roundtripped.elicitations[0].mode, "url"); + } + } } diff --git a/src/error.rs b/src/error.rs index 70d8f374..8606d056 100644 --- a/src/error.rs +++ b/src/error.rs @@ -113,6 +113,17 @@ impl Error { ErrorCode::AuthRequired.into() } + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// The agent requires user input via a URL-based elicitation before it can proceed. + #[cfg(feature = "unstable_elicitation")] + #[must_use] + pub fn url_elicitation_required() -> Self { + ErrorCode::UrlElicitationRequired.into() + } + /// A given resource, such as a file, was not found. #[must_use] pub fn resource_not_found(uri: Option) -> Self { @@ -186,6 +197,15 @@ pub enum ErrorCode { #[schemars(transform = error_code_transform)] #[strum(to_string = "Resource not found")] ResourceNotFound, // -32002 + #[cfg(feature = "unstable_elicitation")] + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// The agent requires user input via a URL-based elicitation before it can proceed. + #[schemars(transform = error_code_transform)] + #[strum(to_string = "URL elicitation required")] + UrlElicitationRequired, // -32042 /// Other undefined error code. #[schemars(untagged)] @@ -205,6 +225,8 @@ impl From for ErrorCode { -32800 => ErrorCode::RequestCancelled, -32000 => ErrorCode::AuthRequired, -32002 => ErrorCode::ResourceNotFound, + #[cfg(feature = "unstable_elicitation")] + -32042 => ErrorCode::UrlElicitationRequired, _ => ErrorCode::Other(value), } } @@ -222,6 +244,8 @@ impl From for i32 { ErrorCode::RequestCancelled => -32800, ErrorCode::AuthRequired => -32000, ErrorCode::ResourceNotFound => -32002, + #[cfg(feature = "unstable_elicitation")] + ErrorCode::UrlElicitationRequired => -32042, ErrorCode::Other(value) => value, } } @@ -249,6 +273,8 @@ fn error_code_transform(schema: &mut Schema) { "RequestCancelled" => ErrorCode::RequestCancelled, "AuthRequired" => ErrorCode::AuthRequired, "ResourceNotFound" => ErrorCode::ResourceNotFound, + #[cfg(feature = "unstable_elicitation")] + "UrlElicitationRequired" => ErrorCode::UrlElicitationRequired, _ => panic!("Unexpected error code name {name}"), }; let mut description = schema diff --git a/src/rpc.rs b/src/rpc.rs index 865e5519..868087ee 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -215,6 +215,10 @@ impl Side for ClientSide { .map(AgentRequest::WaitForTerminalExitRequest) .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) + .map_err(Into::into), _ => { if let Some(custom_method) = method.strip_prefix('_') { Ok(AgentRequest::ExtMethodRequest(ExtRequest { @@ -235,6 +239,12 @@ impl Side for ClientSide { m if m == CLIENT_METHOD_NAMES.session_update => serde_json::from_str(params.get()) .map(AgentNotification::SessionNotification) .map_err(Into::into), + #[cfg(feature = "unstable_elicitation")] + m if m == CLIENT_METHOD_NAMES.session_elicitation_complete => { + serde_json::from_str(params.get()) + .map(AgentNotification::ElicitationCompleteNotification) + .map_err(Into::into) + } _ => { if let Some(custom_method) = method.strip_prefix('_') { Ok(AgentNotification::ExtNotification(ExtNotification {