diff --git a/crates/goose-sdk-types/src/custom_notifications.rs b/crates/goose-sdk-types/src/custom_notifications.rs index 44424fb3b3..3170e93ccb 100644 --- a/crates/goose-sdk-types/src/custom_notifications.rs +++ b/crates/goose-sdk-types/src/custom_notifications.rs @@ -25,14 +25,12 @@ pub struct GooseSessionNotification { "propertyName": "sessionUpdate", "mapping": { "usage_update": "#/$defs/SessionUsageUpdate", - "status_message": "#/$defs/StatusMessageUpdate", - "interaction_update": "#/$defs/InteractionUpdate" + "status_message": "#/$defs/StatusMessageUpdate" } }))] pub enum GooseSessionUpdate { UsageUpdate(SessionUsageUpdate), StatusMessage(StatusMessageUpdate), - InteractionUpdate(InteractionUpdate), } impl Default for GooseSessionUpdate { @@ -70,48 +68,6 @@ pub enum StatusMessage { Progress { message: String }, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct InteractionUpdate { - pub interaction: Interaction, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "_meta")] - pub meta: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum Interaction { - #[serde(rename_all = "camelCase")] - Elicitation { - id: String, - state: InteractionState, - #[serde(skip_serializing_if = "Option::is_none")] - message: Option, - #[serde(skip_serializing_if = "Option::is_none")] - requested_schema: Option, - }, -} - -impl Default for Interaction { - fn default() -> Self { - Self::Elicitation { - id: String::new(), - state: InteractionState::Pending, - message: None, - requested_schema: None, - } - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum InteractionState { - #[default] - Pending, - Submitted, -} - fn notification_schema(generator: &mut SchemaGenerator) -> CustomMethodSchema where T: Default + JsonRpcMessage + JsonSchema, diff --git a/crates/goose-sdk-types/src/custom_requests.rs b/crates/goose-sdk-types/src/custom_requests.rs index f9b0bd8924..1107f83330 100644 --- a/crates/goose-sdk-types/src/custom_requests.rs +++ b/crates/goose-sdk-types/src/custom_requests.rs @@ -556,17 +556,6 @@ pub struct ImportSessionResponse { pub message_count: u64, } -/// Submit a response for a pending MCP elicitation in an active session. -#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request(method = "_goose/unstable/elicitation/respond", response = EmptyResponse)] -#[serde(rename_all = "camelCase")] -pub struct ElicitationRespondRequest { - pub session_id: String, - pub elicitation_id: String, - #[serde(default)] - pub user_data: serde_json::Value, -} - #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct ProviderConfigKey { diff --git a/crates/goose/acp-meta.json b/crates/goose/acp-meta.json index 27294da66c..a713ed632d 100644 --- a/crates/goose/acp-meta.json +++ b/crates/goose/acp-meta.json @@ -200,11 +200,6 @@ "requestType": "GetSessionInfoRequest_unstable", "responseType": "GetSessionInfoResponse_unstable" }, - { - "method": "_goose/unstable/elicitation/respond", - "requestType": "ElicitationRespondRequest_unstable", - "responseType": "EmptyResponse" - }, { "method": "_goose/unstable/session/project/update", "requestType": "UpdateSessionProjectRequest_unstable", diff --git a/crates/goose/acp-schema.json b/crates/goose/acp-schema.json index 97732c5202..9a9bdcd323 100644 --- a/crates/goose/acp-schema.json +++ b/crates/goose/acp-schema.json @@ -2768,27 +2768,6 @@ "type": "string", "description": "A unique identifier for a conversation session between a client and agent.\n\nSessions maintain their own context, conversation history, and state,\nallowing multiple independent interactions with the same agent.\n\nSee protocol docs: [Session ID](https://agentclientprotocol.com/protocol/session-setup#session-id)" }, - "ElicitationRespondRequest_unstable": { - "type": "object", - "properties": { - "sessionId": { - "type": "string" - }, - "elicitationId": { - "type": "string" - }, - "userData": { - "default": null - } - }, - "required": [ - "sessionId", - "elicitationId" - ], - "description": "Submit a response for a pending MCP elicitation in an active session.", - "x-side": "agent", - "x-method": "_goose/unstable/elicitation/respond" - }, "UpdateSessionProjectRequest_unstable": { "type": "object", "properties": { @@ -3598,19 +3577,6 @@ "required": [ "sessionUpdate" ] - }, - { - "$ref": "#/$defs/InteractionUpdate", - "type": "object", - "properties": { - "sessionUpdate": { - "type": "string", - "const": "interaction_update" - } - }, - "required": [ - "sessionUpdate" - ] } ], "description": "Discriminated union of goose-specific session update payloads.\nVariant tag matches ACP's convention (`sessionUpdate: \"\"`).\n\n`discriminator.mapping` is what makes TS codegen (`@hey-api/openapi-ts`)\nemit the correct snake_case tag value even when this enum has a single\nvariant. Add a mapping entry per variant.", @@ -3618,8 +3584,7 @@ "propertyName": "sessionUpdate", "mapping": { "usage_update": "#/$defs/SessionUsageUpdate", - "status_message": "#/$defs/StatusMessageUpdate", - "interaction_update": "#/$defs/InteractionUpdate" + "status_message": "#/$defs/StatusMessageUpdate" } } }, @@ -3706,56 +3671,6 @@ ], "description": "Live UI/session status. This is not conversation transcript content, and\nshould not be persisted or replayed as history." }, - "Interaction": { - "oneOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "state": { - "$ref": "#/$defs/InteractionState" - }, - "message": { - "type": [ - "string", - "null" - ] - }, - "requestedSchema": {}, - "type": { - "type": "string", - "const": "elicitation" - } - }, - "required": [ - "type", - "id", - "state" - ] - } - ] - }, - "InteractionState": { - "type": "string", - "enum": [ - "pending", - "submitted" - ] - }, - "InteractionUpdate": { - "type": "object", - "properties": { - "interaction": { - "$ref": "#/$defs/Interaction" - }, - "_meta": {} - }, - "required": [ - "interaction" - ] - }, "ExtRequest": { "properties": { "id": { @@ -4128,15 +4043,6 @@ "description": "Params for _goose/unstable/session/info", "title": "GetSessionInfoRequest_unstable" }, - { - "allOf": [ - { - "$ref": "#/$defs/ElicitationRespondRequest_unstable" - } - ], - "description": "Params for _goose/unstable/elicitation/respond", - "title": "ElicitationRespondRequest_unstable" - }, { "allOf": [ { diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index 656ceedfd3..3ae3aa713f 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -8,7 +8,6 @@ pub(super) use crate::acp::response_builder::{ }; use crate::acp::tools::AcpAwareToolMeta; use crate::acp::{PermissionDecision, ACP_CURRENT_MODEL}; -use crate::action_required_manager::ActionRequiredManager; use crate::agents::extension::{Envs, PLATFORM_EXTENSIONS}; use crate::agents::extension_manager::TRUSTED_TOOL_UPDATE_META_KEY; use crate::agents::mcp_client::{GooseMcpHostInfo, McpClientTrait}; @@ -63,12 +62,10 @@ use agent_client_protocol::{ }; use anyhow::Result; use fs_err as fs; -use futures::future::BoxFuture; +use futures::future::{BoxFuture, FutureExt}; use futures::stream::{self, StreamExt}; -use futures::FutureExt; use rmcp::model::{ - AnnotateAble, CallToolResult, ElicitationAction, RawContent, RawTextContent, ResourceContents, - Role, + AnnotateAble, CallToolResult, RawContent, RawTextContent, ResourceContents, Role, }; use serde::Deserialize; use std::collections::{HashMap, HashSet}; @@ -86,6 +83,7 @@ mod config; mod custom_dispatch; mod dictation; mod dispatch; +mod elicitation; mod extensions; mod fork_session; mod list_sessions; @@ -205,6 +203,7 @@ pub struct GooseAcpAgent { client_fs_capabilities: OnceCell, client_terminal: OnceCell, client_mcp_host_info: OnceCell, + client_supports_acp_elicitation: OnceCell, client_supports_goose_custom_notifications: OnceCell, use_login_shell_path: OnceCell, client_cx: OnceCell>, @@ -847,6 +846,13 @@ impl GooseAcpAgent { .unwrap_or(false) } + fn supports_acp_elicitation(&self) -> bool { + self.client_supports_acp_elicitation + .get() + .copied() + .unwrap_or(false) + } + // TODO: goose reads Paths::in_state_dir globally (e.g. RequestLog), ignoring this data_dir. pub async fn new(options: GooseAcpAgentOptions) -> Result { let session_manager = Arc::new(SessionManager::new(options.data_dir)); @@ -877,6 +883,7 @@ impl GooseAcpAgent { client_fs_capabilities: OnceCell::new(), client_terminal: OnceCell::new(), client_mcp_host_info: OnceCell::new(), + client_supports_acp_elicitation: OnceCell::new(), client_supports_goose_custom_notifications: OnceCell::new(), use_login_shell_path: OnceCell::new(), client_cx: OnceCell::new(), @@ -1320,20 +1327,15 @@ impl GooseAcpAgent { message, requested_schema, } => { - send_elicitation_interaction_update( + self.handle_form_elicitation( cx, - self.supports_goose_custom_notifications(), - session_id.0.as_ref(), - InteractionUpdate { - interaction: Interaction::Elicitation { - id: id.clone(), - state: InteractionState::Pending, - message: Some(message.clone()), - requested_schema: Some(requested_schema.clone()), - }, - meta: Some(interaction_update_meta(message_id, message_created)), - }, - )?; + session_id, + id, + message, + requested_schema, + message_update_meta(message_id, message_created, false), + ) + .await?; } ActionRequiredData::ElicitationResponse { .. } => {} }, @@ -1944,25 +1946,6 @@ fn status_message_from_system_notification( } } -fn send_elicitation_interaction_update( - cx: &ConnectionTo, - supports_goose_custom_notifications: bool, - session_id: &str, - update: InteractionUpdate, -) -> Result<(), agent_client_protocol::Error> { - if supports_goose_custom_notifications { - cx.send_notification(GooseSessionNotification { - session_id: session_id.to_string(), - update: GooseSessionUpdate::InteractionUpdate(update), - })?; - } - Ok(()) -} - -fn interaction_update_meta(message_id: Option<&str>, created: i64) -> serde_json::Value { - serde_json::Value::Object(message_update_meta(message_id, created, false)) -} - fn message_update_meta(message_id: Option<&str>, created: i64, steer: bool) -> Meta { let mut goose = serde_json::Map::new(); goose.insert("created".to_string(), serde_json::json!(created)); @@ -2103,6 +2086,9 @@ impl GooseAcpAgent { let _ = self.client_supports_goose_custom_notifications.set( extract_client_supports_goose_custom_notifications(goose_client_capabilities.as_ref()), ); + let _ = self + .client_supports_acp_elicitation + .set(elicitation::client_supports_form_elicitation(&args)); let _ = self .use_login_shell_path .set(extract_use_login_shell_path(&args)); @@ -2655,55 +2641,6 @@ impl GooseAcpAgent { Ok(()) } - async fn on_elicitation_respond( - &self, - cx: &ConnectionTo, - req: ElicitationRespondRequest, - ) -> Result { - ActionRequiredManager::global() - .submit_response( - req.elicitation_id.clone(), - req.user_data.clone(), - ElicitationAction::Accept, - ) - .await - .invalid_params_err_ctx("Failed to submit elicitation response")?; - - let response_message = Message::user() - .with_generated_id() - .with_content(MessageContent::action_required_elicitation_response( - req.elicitation_id.clone(), - req.user_data, - ElicitationAction::Accept, - )) - .agent_only(); - - self.session_manager - .add_message(&req.session_id, &response_message) - .await - .internal_err_ctx("Failed to persist elicitation response")?; - - send_elicitation_interaction_update( - cx, - self.supports_goose_custom_notifications(), - &req.session_id, - InteractionUpdate { - interaction: Interaction::Elicitation { - id: req.elicitation_id, - state: InteractionState::Submitted, - message: None, - requested_schema: None, - }, - meta: Some(interaction_update_meta( - response_message.id.as_deref(), - response_message.created, - )), - }, - )?; - - Ok(EmptyResponse {}) - } - async fn on_set_model( &self, session_id: &str, diff --git a/crates/goose/src/acp/server/custom_dispatch.rs b/crates/goose/src/acp/server/custom_dispatch.rs index 943685b5d0..4a069b909d 100644 --- a/crates/goose/src/acp/server/custom_dispatch.rs +++ b/crates/goose/src/acp/server/custom_dispatch.rs @@ -329,15 +329,6 @@ impl GooseAcpAgent { self.on_get_session_info(req).await } - #[custom_method(ElicitationRespondRequest)] - async fn dispatch_elicitation_respond( - &self, - _req: ElicitationRespondRequest, - ) -> Result { - Err(agent_client_protocol::Error::invalid_params() - .data("_goose/unstable/elicitation/respond must be handled by the connection-scoped dispatcher")) - } - #[custom_method(UpdateSessionProjectRequest)] async fn dispatch_update_session_project( &self, diff --git a/crates/goose/src/acp/server/dispatch.rs b/crates/goose/src/acp/server/dispatch.rs index 2e622f3893..a384921880 100644 --- a/crates/goose/src/acp/server/dispatch.rs +++ b/crates/goose/src/acp/server/dispatch.rs @@ -95,19 +95,6 @@ impl HandleDispatchFrom for GooseAcpHandler { Ok(()) }) .await - .if_request({ - let agent = agent.clone(); - let cx = cx.clone(); - |req: ElicitationRespondRequest, responder: Responder| async move { - let cx_spawn = cx.clone(); - cx.spawn(async move { - responder.respond_with_result(agent.on_elicitation_respond(&cx_spawn, req).await)?; - Ok(()) - })?; - Ok(()) - } - }) - .await // set_config_option (SACP 11) and legacy set_mode/set_model; custom _goose/* in otherwise. .if_request({ let agent = agent.clone(); diff --git a/crates/goose/src/acp/server/elicitation.rs b/crates/goose/src/acp/server/elicitation.rs new file mode 100644 index 0000000000..10c395d444 --- /dev/null +++ b/crates/goose/src/acp/server/elicitation.rs @@ -0,0 +1,253 @@ +use std::sync::Arc; + +use agent_client_protocol::schema::{ + CreateElicitationRequest, CreateElicitationResponse, ElicitationAction as AcpElicitationAction, + ElicitationFormMode, ElicitationSchema, ElicitationSessionScope, Meta, SessionId, + CLIENT_METHOD_NAMES, +}; +use agent_client_protocol::{ + Client, ConnectionTo, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse, UntypedMessage, +}; +use tracing::warn; + +use crate::action_required_manager::ElicitationOutcome; +use crate::session::SessionManager; + +impl super::GooseAcpAgent { + pub(super) async fn handle_form_elicitation( + &self, + cx: &ConnectionTo, + session_id: &SessionId, + elicitation_id: &str, + message: &str, + requested_schema: &serde_json::Value, + meta: Meta, + ) -> Result<(), agent_client_protocol::Error> { + if self.supports_acp_elicitation() { + self.send_form_elicitation( + cx, + session_id, + elicitation_id, + message, + requested_schema, + meta, + ) + .await?; + } else { + warn!( + session_id = %session_id.0.as_ref(), + elicitation_id = %elicitation_id, + "ACP client does not support form elicitation" + ); + self.cancel_form_elicitation(session_id.0.as_ref(), elicitation_id) + .await; + } + + Ok(()) + } + + async fn send_form_elicitation( + &self, + cx: &ConnectionTo, + session_id: &SessionId, + elicitation_id: &str, + message: &str, + requested_schema: &serde_json::Value, + meta: Meta, + ) -> Result<(), agent_client_protocol::Error> { + let session_id = session_id.0.as_ref().to_string(); + let elicitation_id = elicitation_id.to_string(); + if requested_schema + .get("url") + .and_then(|url| url.as_str()) + .is_some() + { + warn!( + session_id = %session_id, + elicitation_id = %elicitation_id, + "ACP URL elicitation is not supported" + ); + record_acp_elicitation_response( + &self.session_manager, + &session_id, + &elicitation_id, + ElicitationOutcome::Cancel, + ) + .await; + return Ok(()); + } + + let requested_schema: ElicitationSchema = + match serde_json::from_value(requested_schema.clone()) { + Ok(schema) => schema, + Err(error) => { + record_acp_elicitation_response( + &self.session_manager, + &session_id, + &elicitation_id, + ElicitationOutcome::Cancel, + ) + .await; + return Err(agent_client_protocol::Error::internal_error() + .data(format!("Failed to parse ACP elicitation schema: {error}"))); + } + }; + let request = CreateElicitationRequest::new( + ElicitationFormMode::new( + ElicitationSessionScope::new(session_id.clone()), + requested_schema, + ), + message.to_string(), + ) + .meta(meta); + + let callback_session_manager = Arc::clone(&self.session_manager); + let callback_session_id = session_id.clone(); + let callback_elicitation_id = elicitation_id.clone(); + if let Err(error) = cx + .send_request(CreateElicitationRequestMessage(request)) + .on_receiving_result(move |result| async move { + let response = match result { + Ok(response) => elicitation_response_from_acp(response.0), + Err(error) => { + warn!( + error = %error, + session_id = %callback_session_id, + elicitation_id = %callback_elicitation_id, + "ACP elicitation request failed" + ); + ElicitationOutcome::Cancel + } + }; + + record_acp_elicitation_response( + &callback_session_manager, + &callback_session_id, + &callback_elicitation_id, + response, + ) + .await; + + Ok(()) + }) + { + record_acp_elicitation_response( + &self.session_manager, + &session_id, + &elicitation_id, + ElicitationOutcome::Cancel, + ) + .await; + return Err(error); + } + + Ok(()) + } + + async fn cancel_form_elicitation(&self, session_id: &str, elicitation_id: &str) { + record_acp_elicitation_response( + &self.session_manager, + session_id, + elicitation_id, + ElicitationOutcome::Cancel, + ) + .await; + } +} + +#[derive(Debug, Clone)] +struct CreateElicitationRequestMessage(CreateElicitationRequest); + +impl JsonRpcMessage for CreateElicitationRequestMessage { + fn matches_method(method: &str) -> bool { + method == CLIENT_METHOD_NAMES.elicitation_create + } + + fn method(&self) -> &str { + CLIENT_METHOD_NAMES.elicitation_create + } + + fn to_untyped_message(&self) -> Result { + UntypedMessage::new(CLIENT_METHOD_NAMES.elicitation_create, &self.0) + } + + fn parse_message( + method: &str, + params: &impl serde::Serialize, + ) -> Result { + if !Self::matches_method(method) { + return Err(agent_client_protocol::Error::method_not_found()); + } + + Ok(Self(agent_client_protocol::util::json_cast_params(params)?)) + } +} + +impl JsonRpcRequest for CreateElicitationRequestMessage { + type Response = CreateElicitationResponseMessage; +} + +#[derive(Debug, Clone)] +struct CreateElicitationResponseMessage(CreateElicitationResponse); + +impl JsonRpcResponse for CreateElicitationResponseMessage { + fn into_json(self, _method: &str) -> Result { + serde_json::to_value(self.0).map_err(agent_client_protocol::Error::into_internal_error) + } + + fn from_value( + _method: &str, + value: serde_json::Value, + ) -> Result { + Ok(Self(agent_client_protocol::util::json_cast(&value)?)) + } +} + +pub(super) fn client_supports_form_elicitation( + args: &agent_client_protocol::schema::InitializeRequest, +) -> bool { + args.client_capabilities + .elicitation + .as_ref() + .and_then(|elicitation| elicitation.form.as_ref()) + .is_some() +} + +fn elicitation_response_from_acp(response: CreateElicitationResponse) -> ElicitationOutcome { + match response.action { + AcpElicitationAction::Accept(action) => { + let content = serde_json::to_value(action.content.unwrap_or_default()) + .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new())); + ElicitationOutcome::Accept(content) + } + AcpElicitationAction::Decline => ElicitationOutcome::Decline, + AcpElicitationAction::Cancel => ElicitationOutcome::Cancel, + action => { + warn!(?action, "Unsupported ACP elicitation action"); + ElicitationOutcome::Cancel + } + } +} + +async fn record_acp_elicitation_response( + session_manager: &SessionManager, + session_id: &str, + elicitation_id: &str, + response: ElicitationOutcome, +) { + if let Err(error) = crate::elicitation::complete_elicitation_with_generated_message( + session_manager, + session_id, + elicitation_id, + response, + ) + .await + { + warn!( + error = %error, + session_id = %session_id, + elicitation_id = %elicitation_id, + "Failed to record ACP elicitation response" + ); + } +} diff --git a/crates/goose/src/acp/server/load_session.rs b/crates/goose/src/acp/server/load_session.rs index 94681a3bcf..b22ccabe6e 100644 --- a/crates/goose/src/acp/server/load_session.rs +++ b/crates/goose/src/acp/server/load_session.rs @@ -29,7 +29,6 @@ fn send_replay_content_chunk( fn replay_conversation_to_client( cx: &ConnectionTo, session: &Session, - supports_goose_custom_notifications: bool, ) -> Result, agent_client_protocol::Error> { let session_id = SessionId::new(session.id.clone()); @@ -49,7 +48,6 @@ fn replay_conversation_to_client( let mut replay_tool_requests = HashMap::::new(); - let submitted_elicitation_ids = collect_submitted_elicitation_ids(&messages); for message in &messages { if !message.metadata.user_visible { @@ -156,33 +154,6 @@ fn replay_conversation_to_client( ), ))?; } - MessageContent::ActionRequired(action_required) => { - if let ActionRequiredData::Elicitation { - id, - message: elicitation_message, - requested_schema, - } = &action_required.data - { - if !submitted_elicitation_ids.contains(id) { - send_elicitation_interaction_update( - cx, - supports_goose_custom_notifications, - session_id.0.as_ref(), - InteractionUpdate { - interaction: Interaction::Elicitation { - id: id.clone(), - state: InteractionState::Pending, - message: Some(elicitation_message.clone()), - requested_schema: Some(requested_schema.clone()), - }, - meta: Some(serde_json::Value::Object(replay_message_meta( - message, - ))), - }, - )?; - } - } - } MessageContent::SystemNotification(_) => {} _ => {} } @@ -192,22 +163,6 @@ fn replay_conversation_to_client( Ok(replay_tool_requests) } -fn collect_submitted_elicitation_ids(messages: &[Message]) -> HashSet { - let mut submitted_ids = HashSet::new(); - - for message in messages { - for content_item in &message.content { - if let MessageContent::ActionRequired(action_required) = content_item { - if let ActionRequiredData::ElicitationResponse { id, .. } = &action_required.data { - submitted_ids.insert(id.clone()); - } - } - } - } - - submitted_ids -} - impl GooseAcpAgent { pub(super) async fn handle_load_session( &self, @@ -234,11 +189,7 @@ impl GooseAcpAgent { .prepare_session_for_activation(session, args.cwd.clone(), args.mcp_servers, true) .await?; - let replay_tool_requests = replay_conversation_to_client( - cx, - &session, - self.supports_goose_custom_notifications(), - )?; + let replay_tool_requests = replay_conversation_to_client(cx, &session)?; let (agent, extension_results) = self.prepare_acp_session_agent(cx, &session).await?; self.register_acp_session(session_id_str.clone(), agent.clone(), replay_tool_requests) .await; diff --git a/crates/goose/src/action_required_manager.rs b/crates/goose/src/action_required_manager.rs index faab8addb1..b76e0eaf86 100644 --- a/crates/goose/src/action_required_manager.rs +++ b/crates/goose/src/action_required_manager.rs @@ -1,10 +1,9 @@ use anyhow::Result; -use rmcp::model::ElicitationAction; use serde_json::Value; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::sync::Arc; use std::time::Duration; -use tokio::sync::{mpsc, Mutex, RwLock}; +use tokio::sync::{Mutex, OwnedMutexGuard, RwLock}; use tokio::time::timeout; use tracing::warn; use uuid::Uuid; @@ -12,151 +11,415 @@ use uuid::Uuid; use crate::conversation::message::{Message, MessageContent}; #[derive(Debug, Clone, PartialEq)] -pub struct ActionRequiredResponse { - pub action: ElicitationAction, - pub user_data: Value, +pub(crate) enum ElicitationOutcome { + Accept(Value), + Decline, + Cancel, } struct PendingRequest { - response_tx: Option>, + session_id: String, + response_tx: Option>, } -pub struct ActionRequiredManager { - pending: Arc>>>>, - request_tx: mpsc::UnboundedSender, - pub request_rx: Mutex>, +pub(crate) struct PendingResponseClaim { + request_id: String, + pending: OwnedMutexGuard, } -impl ActionRequiredManager { - fn new() -> Self { - let (request_tx, request_rx) = mpsc::unbounded_channel(); - Self { - pending: Arc::new(RwLock::new(HashMap::new())), - request_tx, - request_rx: Mutex::new(request_rx), - } - } +impl PendingResponseClaim { + pub(crate) fn submit(mut self, response: ElicitationOutcome) -> Result<()> { + let tx = self + .pending + .response_tx + .take() + .ok_or_else(|| anyhow::anyhow!("Request already completed: {}", self.request_id))?; + drop(self.pending); - pub fn global() -> &'static Self { - static INSTANCE: once_cell::sync::Lazy = - once_cell::sync::Lazy::new(ActionRequiredManager::new); - &INSTANCE - } - - pub async fn request_and_wait( - &self, - message: String, - schema: Value, - timeout_duration: Duration, - ) -> Result { - let id = Uuid::new_v4().to_string(); - let (tx, rx) = tokio::sync::oneshot::channel(); - let pending_request = PendingRequest { - response_tx: Some(tx), - }; - - self.pending - .write() - .await - .insert(id.clone(), Arc::new(Mutex::new(pending_request))); - - let action_required_message = Message::assistant().with_content( - MessageContent::action_required_elicitation(id.clone(), message, schema), - ); - - if let Err(e) = self.request_tx.send(action_required_message) { - warn!("Failed to send action required message: {}", e); - } - - let result = match timeout(timeout_duration, rx).await { - Ok(Ok(response)) => Ok(response), - Ok(Err(_)) => { - warn!("Response channel closed for request: {}", id); - Err(anyhow::anyhow!("Response channel closed")) - } - Err(_) => { - warn!("Timeout waiting for response: {}", id); - Err(anyhow::anyhow!("Timeout waiting for user response")) - } - }; - - self.pending.write().await.remove(&id); - - result - } - - pub async fn submit_response( - &self, - request_id: String, - user_data: Value, - action: ElicitationAction, - ) -> Result<()> { - let pending_arc = { - let pending = self.pending.read().await; - pending - .get(&request_id) - .cloned() - .ok_or_else(|| anyhow::anyhow!("Request not found: {}", request_id))? - }; - - let mut pending = pending_arc.lock().await; - if let Some(tx) = pending.response_tx.take() { - if tx - .send(ActionRequiredResponse { action, user_data }) - .is_err() - { - warn!("Failed to send response through oneshot channel"); - } + if tx.send(response).is_err() { + return Err(anyhow::anyhow!("Response channel closed")); } Ok(()) } } +pub(crate) struct ActionRequiredManager { + pending: Arc>>>>, + queued_requests: Mutex>>, +} + +impl ActionRequiredManager { + fn new() -> Self { + Self { + pending: Arc::new(RwLock::new(HashMap::new())), + queued_requests: Mutex::new(HashMap::new()), + } + } + + pub(crate) fn global() -> &'static Self { + static INSTANCE: once_cell::sync::Lazy = + once_cell::sync::Lazy::new(ActionRequiredManager::new); + &INSTANCE + } + + pub(crate) async fn request_and_wait( + &self, + session_id: String, + message: String, + schema: Value, + timeout_duration: Duration, + ) -> Result { + let id = Uuid::new_v4().to_string(); + let (tx, rx) = tokio::sync::oneshot::channel(); + let pending_request = PendingRequest { + session_id: session_id.clone(), + response_tx: Some(tx), + }; + let pending_request = Arc::new(Mutex::new(pending_request)); + + self.pending + .write() + .await + .insert(id.clone(), Arc::clone(&pending_request)); + + let action_required_message = Message::assistant().with_content( + MessageContent::action_required_elicitation(id.clone(), message, schema), + ); + + self.queued_requests + .lock() + .await + .entry(session_id) + .or_default() + .push_back(action_required_message); + + let result = self + .wait_for_response(&id, pending_request, rx, timeout_duration) + .await; + + self.pending.write().await.remove(&id); + + result + } + + pub(crate) async fn claim_response( + &self, + session_id: &str, + request_id: &str, + ) -> Result { + let pending_arc = self.pending_request(request_id).await?; + let mut pending = pending_arc.lock_owned().await; + + if pending.session_id != session_id { + return Err(anyhow::anyhow!( + "Request {} belongs to session {}, not {}", + request_id, + pending.session_id, + session_id + )); + } + + let tx = pending + .response_tx + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Request already completed: {}", request_id))?; + if tx.is_closed() { + pending.response_tx.take(); + return Err(anyhow::anyhow!("Response channel closed")); + } + + Ok(PendingResponseClaim { + request_id: request_id.to_string(), + pending, + }) + } + + async fn pending_request(&self, request_id: &str) -> Result>> { + let pending = self.pending.read().await; + pending + .get(request_id) + .cloned() + .ok_or_else(|| anyhow::anyhow!("Request not found: {}", request_id)) + } + + async fn wait_for_response( + &self, + request_id: &str, + pending_request: Arc>, + mut rx: tokio::sync::oneshot::Receiver, + timeout_duration: Duration, + ) -> Result { + match timeout(timeout_duration, &mut rx).await { + Ok(response) => Self::finish_waiting(request_id, response), + Err(_) => { + let mut pending = pending_request.lock().await; + if pending.response_tx.is_some() { + pending.response_tx.take(); + warn!("Timeout waiting for response: {}", request_id); + return Err(anyhow::anyhow!("Timeout waiting for user response")); + } + drop(pending); + + Self::finish_waiting(request_id, rx.await) + } + } + } + + fn finish_waiting( + request_id: &str, + response: Result, + ) -> Result { + match response { + Ok(user_data) => Ok(user_data), + Err(_) => { + warn!("Response channel closed for request: {}", request_id); + Err(anyhow::anyhow!("Response channel closed")) + } + } + } + + pub(crate) async fn drain_requests_for_session(&self, session_id: &str) -> Vec { + self.queued_requests + .lock() + .await + .remove(session_id) + .map(|queue| queue.into_iter().collect()) + .unwrap_or_default() + } +} + #[cfg(test)] mod tests { use super::*; - use crate::conversation::message::{ActionRequiredData, MessageContent}; + use crate::conversation::message::ActionRequiredData; use serde_json::json; - use std::sync::Arc; - use std::time::Duration; + + fn elicitation_id(message: &Message) -> String { + match &message.content[0] { + MessageContent::ActionRequired(action_required) => match &action_required.data { + ActionRequiredData::Elicitation { id, .. } => id.clone(), + _ => panic!("expected elicitation action-required message"), + }, + _ => panic!("expected action-required message"), + } + } + + async fn wait_for_elicitation_messages( + manager: &ActionRequiredManager, + session_id: &str, + ) -> Vec { + tokio::time::timeout(Duration::from_secs(1), async { + loop { + let messages = manager.drain_requests_for_session(session_id).await; + if !messages.is_empty() { + return messages; + } + tokio::task::yield_now().await; + } + }) + .await + .unwrap_or_else(|_| panic!("timed out waiting for elicitation message for {session_id}")) + } #[tokio::test] - async fn request_and_wait_returns_submitted_action() { + async fn wrong_session_does_not_consume_pending_response() { let manager = Arc::new(ActionRequiredManager::new()); let waiter = { let manager = manager.clone(); tokio::spawn(async move { manager .request_and_wait( - "Need information".to_string(), + "session-a".to_string(), + "Need input".to_string(), json!({ "type": "object" }), Duration::from_secs(5), ) .await - .unwrap() }) }; - let message = manager.request_rx.lock().await.recv().await.unwrap(); - let MessageContent::ActionRequired(action_required) = &message.content[0] else { - panic!("Expected action required message"); - }; - let ActionRequiredData::Elicitation { id, .. } = &action_required.data else { - panic!("Expected elicitation request"); + let messages = wait_for_elicitation_messages(&manager, "session-a").await; + assert_eq!(messages.len(), 1); + let request_id = elicitation_id(&messages[0]); + + let err = match manager.claim_response("session-b", &request_id).await { + Ok(_) => panic!("wrong session should not claim pending response"), + Err(error) => error, }; + assert!(err.to_string().contains("belongs to session session-a")); manager - .submit_response( - id.clone(), - json!({ "reason": "not needed" }), - ElicitationAction::Decline, - ) + .claim_response("session-a", &request_id) + .await + .unwrap() + .submit(ElicitationOutcome::Accept(json!({ "answer": "right" }))) + .unwrap(); + + let response = waiter.await.unwrap().unwrap(); + assert_eq!( + response, + ElicitationOutcome::Accept(json!({ "answer": "right" })) + ); + } + + #[tokio::test] + async fn drains_only_requested_session() { + let manager = Arc::new(ActionRequiredManager::new()); + let waiter_a = { + let manager = manager.clone(); + tokio::spawn(async move { + manager + .request_and_wait( + "session-a".to_string(), + "Need input A".to_string(), + json!({ "type": "object" }), + Duration::from_secs(5), + ) + .await + }) + }; + let waiter_b = { + let manager = manager.clone(); + tokio::spawn(async move { + manager + .request_and_wait( + "session-b".to_string(), + "Need input B".to_string(), + json!({ "type": "object" }), + Duration::from_secs(5), + ) + .await + }) + }; + + let session_a_messages = wait_for_elicitation_messages(&manager, "session-a").await; + assert_eq!(session_a_messages.len(), 1); + let request_id_a = elicitation_id(&session_a_messages[0]); + + let empty_messages = manager.drain_requests_for_session("session-a").await; + assert!(empty_messages.is_empty()); + + let session_b_messages = wait_for_elicitation_messages(&manager, "session-b").await; + assert_eq!(session_b_messages.len(), 1); + let request_id_b = elicitation_id(&session_b_messages[0]); + + manager + .claim_response("session-a", &request_id_a) + .await + .unwrap() + .submit(ElicitationOutcome::Accept(json!({ "answer": "a" }))) + .unwrap(); + manager + .claim_response("session-b", &request_id_b) + .await + .unwrap() + .submit(ElicitationOutcome::Accept(json!({ "answer": "b" }))) + .unwrap(); + + assert_eq!( + waiter_a.await.unwrap().unwrap(), + ElicitationOutcome::Accept(json!({ "answer": "a" })) + ); + assert_eq!( + waiter_b.await.unwrap().unwrap(), + ElicitationOutcome::Accept(json!({ "answer": "b" })) + ); + } + + #[tokio::test] + async fn claimed_response_can_complete_after_timeout_deadline() { + let manager = Arc::new(ActionRequiredManager::new()); + let waiter = { + let manager = manager.clone(); + tokio::spawn(async move { + manager + .request_and_wait( + "session-a".to_string(), + "Need input".to_string(), + json!({ "type": "object" }), + Duration::from_millis(25), + ) + .await + }) + }; + + let messages = wait_for_elicitation_messages(&manager, "session-a").await; + assert_eq!(messages.len(), 1); + let request_id = elicitation_id(&messages[0]); + + let claim = manager + .claim_response("session-a", &request_id) .await .unwrap(); - let response = waiter.await.unwrap(); - assert_eq!(response.action, ElicitationAction::Decline); - assert_eq!(response.user_data, json!({ "reason": "not needed" })); + tokio::time::sleep(Duration::from_millis(50)).await; + + claim + .submit(ElicitationOutcome::Accept(json!({ "answer": "late" }))) + .unwrap(); + + assert_eq!( + waiter.await.unwrap().unwrap(), + ElicitationOutcome::Accept(json!({ "answer": "late" })) + ); + } + + #[tokio::test] + async fn request_and_wait_returns_decline_and_cancel_actions() { + let manager = Arc::new(ActionRequiredManager::new()); + let decline_waiter = { + let manager = manager.clone(); + tokio::spawn(async move { + manager + .request_and_wait( + "session-a".to_string(), + "Need input A".to_string(), + json!({ "type": "object" }), + Duration::from_secs(5), + ) + .await + }) + }; + let cancel_waiter = { + let manager = manager.clone(); + tokio::spawn(async move { + manager + .request_and_wait( + "session-b".to_string(), + "Need input B".to_string(), + json!({ "type": "object" }), + Duration::from_secs(5), + ) + .await + }) + }; + + let decline_messages = wait_for_elicitation_messages(&manager, "session-a").await; + let decline_request_id = elicitation_id(&decline_messages[0]); + let cancel_messages = wait_for_elicitation_messages(&manager, "session-b").await; + let cancel_request_id = elicitation_id(&cancel_messages[0]); + + manager + .claim_response("session-a", &decline_request_id) + .await + .unwrap() + .submit(ElicitationOutcome::Decline) + .unwrap(); + manager + .claim_response("session-b", &cancel_request_id) + .await + .unwrap() + .submit(ElicitationOutcome::Cancel) + .unwrap(); + + assert_eq!( + decline_waiter.await.unwrap().unwrap(), + ElicitationOutcome::Decline + ); + assert_eq!( + cancel_waiter.await.unwrap().unwrap(), + ElicitationOutcome::Cancel + ); } } diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 75719718f2..458877098c 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -16,7 +16,7 @@ use super::mcp_client::GooseMcpHostInfo; use super::platform_tools; use super::tool_confirmation_router::ToolConfirmationRouter; use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE}; -use crate::action_required_manager::ActionRequiredManager; +use crate::action_required_manager::{ActionRequiredManager, ElicitationOutcome}; use crate::agents::extension::{ExtensionConfig, ExtensionResult, ToolInfo}; use crate::agents::extension_manager::{ get_parameter_names, ExtensionManager, ExtensionManagerCapabilities, @@ -57,8 +57,8 @@ use goose_providers::errors::ProviderError; use goose_providers::thinking::ThinkingEffort; use regex::Regex; use rmcp::model::{ - CallToolRequestParams, CallToolResult, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, - ServerNotification, Tool, + CallToolRequestParams, CallToolResult, Content, ElicitationAction, ErrorCode, ErrorData, + GetPromptResult, Prompt, ServerNotification, Tool, }; use serde_json::Value; use tokio::sync::{mpsc, Mutex}; @@ -639,8 +639,10 @@ impl Agent { async fn drain_elicitation_messages(&self, session_id: &str) -> Vec { let mut messages = Vec::new(); let manager = self.config.session_manager.clone(); - let mut elicitation_rx = ActionRequiredManager::global().request_rx.lock().await; - while let Ok(mut elicitation_message) = elicitation_rx.try_recv() { + for mut elicitation_message in ActionRequiredManager::global() + .drain_requests_for_session(session_id) + .await + { if elicitation_message.id.is_none() { elicitation_message = elicitation_message.with_generated_id(); } @@ -1471,16 +1473,23 @@ impl Agent { // success while the blocked tool call stays unblocked. // The success path returns an empty stream after the MCP // server receives the user's accept/decline/cancel action. - ActionRequiredManager::global() - .submit_response(id.clone(), user_data.clone(), action.clone()) - .await - .map_err(|e| { - error!("Failed to submit elicitation response: {}", e); - anyhow!("Failed to submit elicitation response: {}", e) - })?; - session_manager - .add_message(&session_config.id, &user_message) - .await?; + let response = match action { + ElicitationAction::Accept => ElicitationOutcome::Accept(user_data.clone()), + ElicitationAction::Decline => ElicitationOutcome::Decline, + ElicitationAction::Cancel => ElicitationOutcome::Cancel, + }; + crate::elicitation::complete_elicitation_with_message( + &session_manager, + &session_config.id, + id, + response, + &user_message, + ) + .await + .map_err(|e| { + error!("Failed to submit elicitation response: {}", e); + anyhow!("Failed to submit elicitation response: {}", e) + })?; return Ok(Box::pin(futures::stream::empty())); } } diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index 2a3510d079..56567a0050 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -1,4 +1,4 @@ -use crate::action_required_manager::ActionRequiredManager; +use crate::action_required_manager::{ActionRequiredManager, ElicitationOutcome}; use crate::agents::tool_execution::ToolCallContext; use crate::agents::types::SharedProvider; use crate::session_context::{SESSION_ID_HEADER, WORKING_DIR_HEADER}; @@ -371,8 +371,19 @@ impl ClientHandler for GooseClient { async fn create_elicitation( &self, request: CreateElicitationRequestParams, - _context: RequestContext, + context: RequestContext, ) -> Result { + let session_id = self + .resolve_session_id(&context.extensions) + .await + .ok_or_else(|| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + "Could not resolve session id for elicitation request", + None, + ) + })?; + let (message, schema_value) = match &request { CreateElicitationRequestParams::FormElicitationParams { message, @@ -394,14 +405,17 @@ impl ClientHandler for GooseClient { }; ActionRequiredManager::global() - .request_and_wait(message, schema_value, Duration::from_secs(300)) + .request_and_wait(session_id, message, schema_value, Duration::from_secs(300)) .await - .map(|response| { - let result = CreateElicitationResult::new(response.action.clone()); - if response.action == ElicitationAction::Accept { - result.with_content(response.user_data) - } else { - result + .map(|response| match response { + ElicitationOutcome::Accept(user_data) => { + CreateElicitationResult::new(ElicitationAction::Accept).with_content(user_data) + } + ElicitationOutcome::Decline => { + CreateElicitationResult::new(ElicitationAction::Decline) + } + ElicitationOutcome::Cancel => { + CreateElicitationResult::new(ElicitationAction::Cancel) } }) .map_err(|e| { diff --git a/crates/goose/src/elicitation.rs b/crates/goose/src/elicitation.rs new file mode 100644 index 0000000000..271f437063 --- /dev/null +++ b/crates/goose/src/elicitation.rs @@ -0,0 +1,71 @@ +use anyhow::Result; +use rmcp::model::ElicitationAction; +use serde_json::Value; + +use crate::action_required_manager::{ActionRequiredManager, ElicitationOutcome}; +use crate::conversation::message::{Message, MessageContent}; +use crate::session::SessionManager; + +fn elicitation_response_user_data(response: &ElicitationOutcome) -> Value { + match response { + ElicitationOutcome::Accept(user_data) => user_data.clone(), + ElicitationOutcome::Decline | ElicitationOutcome::Cancel => serde_json::json!({}), + } +} + +fn elicitation_response_action(response: &ElicitationOutcome) -> ElicitationAction { + match response { + ElicitationOutcome::Accept(_) => ElicitationAction::Accept, + ElicitationOutcome::Decline => ElicitationAction::Decline, + ElicitationOutcome::Cancel => ElicitationAction::Cancel, + } +} + +fn generated_elicitation_response_message( + elicitation_id: &str, + response: &ElicitationOutcome, +) -> Message { + Message::user() + .with_generated_id() + .with_content(MessageContent::action_required_elicitation_response( + elicitation_id.to_string(), + elicitation_response_user_data(response), + elicitation_response_action(response), + )) + .agent_only() +} + +pub(crate) async fn complete_elicitation_with_message( + session_manager: &SessionManager, + session_id: &str, + elicitation_id: &str, + response: ElicitationOutcome, + response_message: &Message, +) -> Result<()> { + let claim = ActionRequiredManager::global() + .claim_response(session_id, elicitation_id) + .await?; + + session_manager + .add_message(session_id, response_message) + .await?; + + claim.submit(response) +} + +pub(crate) async fn complete_elicitation_with_generated_message( + session_manager: &SessionManager, + session_id: &str, + elicitation_id: &str, + response: ElicitationOutcome, +) -> Result<()> { + let response_message = generated_elicitation_response_message(elicitation_id, &response); + complete_elicitation_with_message( + session_manager, + session_id, + elicitation_id, + response, + &response_message, + ) + .await +} diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index c610d4a78e..8ac4520734 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -15,6 +15,7 @@ pub mod conversation { pub mod dictation; pub mod doctor; pub mod download_manager; +pub mod elicitation; pub mod execution; pub mod gateway; pub mod goose_apps; diff --git a/ui/sdk/src/generated/client.gen.ts b/ui/sdk/src/generated/client.gen.ts index 72a22590f7..bb5e4f44fd 100644 --- a/ui/sdk/src/generated/client.gen.ts +++ b/ui/sdk/src/generated/client.gen.ts @@ -41,7 +41,6 @@ import type { DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, - ElicitationRespondRequest_unstable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, @@ -576,12 +575,6 @@ export class GooseExtClient { ) as GetSessionInfoResponse_unstable; } - async elicitationRespond_unstable( - params: ElicitationRespondRequest_unstable, - ): Promise { - await this.conn.extMethod("_goose/unstable/elicitation/respond", params); - } - async sessionProjectUpdate_unstable( params: UpdateSessionProjectRequest_unstable, ): Promise { diff --git a/ui/sdk/src/generated/index.ts b/ui/sdk/src/generated/index.ts index 77d32c4375..bd7833ec2d 100644 --- a/ui/sdk/src/generated/index.ts +++ b/ui/sdk/src/generated/index.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { AddConfigExtensionRequest_unstable, AddExtensionRequest_unstable, Annotations, ArchiveSessionRequest_unstable, AudioContent, BlobResourceContents, ContentBlock, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, ElicitationRespondRequest_unstable, EmbeddedResource, EmbeddedResourceResource, EmptyResponse, EnvVariable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtNotification, ExtRequest, ExtResponse, GetAvailableExtensionsRequest_unstable, GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetSessionInfoRequest_unstable, GetSessionInfoResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseExtension, GooseExtensionEntry, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, HttpHeader, ImageContent, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, Interaction, InteractionState, InteractionUpdate, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, McpServer, McpServerHttp, McpServerSse, McpServerStdio, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveExtensionRequest_unstable, RenameSessionRequest_unstable, ResourceLink, Role, SessionId, SessionInfo, SessionSystemPromptMode, SessionUsageUpdate, SetConfigExtensionEnabledRequest_unstable, SetSessionSystemPromptRequest_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, SteerSessionRequest_unstable, SteerSessionResponse_unstable, TextContent, TextResourceContents, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js'; +export type { AddConfigExtensionRequest_unstable, AddExtensionRequest_unstable, Annotations, ArchiveSessionRequest_unstable, AudioContent, BlobResourceContents, ContentBlock, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, EmbeddedResource, EmbeddedResourceResource, EmptyResponse, EnvVariable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtNotification, ExtRequest, ExtResponse, GetAvailableExtensionsRequest_unstable, GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetSessionInfoRequest_unstable, GetSessionInfoResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseExtension, GooseExtensionEntry, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, HttpHeader, ImageContent, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, McpServer, McpServerHttp, McpServerSse, McpServerStdio, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveExtensionRequest_unstable, RenameSessionRequest_unstable, ResourceLink, Role, SessionId, SessionInfo, SessionSystemPromptMode, SessionUsageUpdate, SetConfigExtensionEnabledRequest_unstable, SetSessionSystemPromptRequest_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, SteerSessionRequest_unstable, SteerSessionResponse_unstable, TextContent, TextResourceContents, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js'; export const GOOSE_EXT_METHODS = [ { @@ -203,11 +203,6 @@ export const GOOSE_EXT_METHODS = [ requestType: "GetSessionInfoRequest_unstable", responseType: "GetSessionInfoResponse_unstable", }, - { - method: "_goose/unstable/elicitation/respond", - requestType: "ElicitationRespondRequest_unstable", - responseType: "EmptyResponse", - }, { method: "_goose/unstable/session/project/update", requestType: "UpdateSessionProjectRequest_unstable", diff --git a/ui/sdk/src/generated/types.gen.ts b/ui/sdk/src/generated/types.gen.ts index 1216f16fb4..c2ceb32e81 100644 --- a/ui/sdk/src/generated/types.gen.ts +++ b/ui/sdk/src/generated/types.gen.ts @@ -1197,15 +1197,6 @@ export type SessionInfo = { */ export type SessionId = string; -/** - * Submit a response for a pending MCP elicitation in an active session. - */ -export type ElicitationRespondRequest_unstable = { - sessionId: string; - elicitationId: string; - userData?: unknown; -}; - /** * Update the project association for a session. */ @@ -1571,9 +1562,7 @@ export type GooseSessionUpdate = ({ sessionUpdate: 'usage_update'; } & SessionUsageUpdate) | ({ sessionUpdate: 'status_message'; -} & StatusMessageUpdate) | ({ - sessionUpdate: 'interaction_update'; -} & InteractionUpdate); +} & StatusMessageUpdate); /** * Streaming context-window usage update for a session. @@ -1602,25 +1591,10 @@ export type StatusMessageUpdate = { status: StatusMessage; }; -export type Interaction = { - id: string; - state: InteractionState; - message?: string | null; - requestedSchema?: unknown; - type: 'elicitation'; -}; - -export type InteractionState = 'pending' | 'submitted'; - -export type InteractionUpdate = { - interaction: Interaction; - _meta?: unknown; -}; - export type ExtRequest = { id: string; method: string; - params?: AddExtensionRequest_unstable | RemoveExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | GetSessionInfoRequest_unstable | ElicitationRespondRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | { + params?: AddExtensionRequest_unstable | RemoveExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | SteerSessionRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | GetSessionInfoRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | { [key: string]: unknown; } | null; }; diff --git a/ui/sdk/src/generated/zod.gen.ts b/ui/sdk/src/generated/zod.gen.ts index b98f640e0e..614d7a47c4 100644 --- a/ui/sdk/src/generated/zod.gen.ts +++ b/ui/sdk/src/generated/zod.gen.ts @@ -1173,15 +1173,6 @@ export const zGetSessionInfoResponse_unstable = z.object({ session: zSessionInfo }); -/** - * Submit a response for a pending MCP elicitation in an active session. - */ -export const zElicitationRespondRequest_unstable = z.object({ - sessionId: z.string(), - elicitationId: z.string(), - userData: z.unknown().optional().default(null) -}); - /** * Update the project association for a session. */ @@ -1543,24 +1534,6 @@ export const zStatusMessageUpdate = z.object({ status: zStatusMessage }); -export const zInteractionState = z.enum(['pending', 'submitted']); - -export const zInteraction = z.object({ - id: z.string(), - state: zInteractionState, - message: z.union([ - z.string(), - z.null() - ]).optional(), - requestedSchema: z.unknown().optional(), - type: z.literal('elicitation') -}); - -export const zInteractionUpdate = z.object({ - interaction: zInteraction, - _meta: z.unknown().optional() -}); - /** * Discriminated union of goose-specific session update payloads. * Variant tag matches ACP's convention (`sessionUpdate: ""`). @@ -1575,10 +1548,7 @@ export const zGooseSessionUpdate = z.union([ }).and(zSessionUsageUpdate), z.object({ sessionUpdate: z.literal('status_message') - }).and(zStatusMessageUpdate), - z.object({ - sessionUpdate: z.literal('interaction_update') - }).and(zInteractionUpdate) + }).and(zStatusMessageUpdate) ]); /** @@ -1635,7 +1605,6 @@ export const zExtRequest = z.object({ zExportSessionRequest_unstable, zImportSessionRequest_unstable, zGetSessionInfoRequest_unstable, - zElicitationRespondRequest_unstable, zUpdateSessionProjectRequest_unstable, zRenameSessionRequest_unstable, zArchiveSessionRequest_unstable,