feat: implement acp method for elicitation and elicitation improvement (#9797)

This commit is contained in:
Lifei Zhou 2026-06-17 13:40:10 +10:00 committed by GitHub
parent 1f0f5d90cb
commit 72da97204e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 775 additions and 521 deletions

View file

@ -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<serde_json::Value>,
}
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
requested_schema: Option<serde_json::Value>,
},
}
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<T>(generator: &mut SchemaGenerator) -> CustomMethodSchema
where
T: Default + JsonRpcMessage + JsonSchema,

View file

@ -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 {

View file

@ -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",

View file

@ -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: \"<snake_case>\"`).\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": [
{

View file

@ -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<FileSystemCapabilities>,
client_terminal: OnceCell<bool>,
client_mcp_host_info: OnceCell<GooseMcpHostInfo>,
client_supports_acp_elicitation: OnceCell<bool>,
client_supports_goose_custom_notifications: OnceCell<bool>,
use_login_shell_path: OnceCell<bool>,
client_cx: OnceCell<ConnectionTo<Client>>,
@ -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<Self> {
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<Client>,
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<Client>,
req: ElicitationRespondRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
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,

View file

@ -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<EmptyResponse, agent_client_protocol::Error> {
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,

View file

@ -95,19 +95,6 @@ impl HandleDispatchFrom<Client> for GooseAcpHandler {
Ok(())
})
.await
.if_request({
let agent = agent.clone();
let cx = cx.clone();
|req: ElicitationRespondRequest, responder: Responder<EmptyResponse>| 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();

View file

@ -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<Client>,
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<Client>,
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, agent_client_protocol::Error> {
UntypedMessage::new(CLIENT_METHOD_NAMES.elicitation_create, &self.0)
}
fn parse_message(
method: &str,
params: &impl serde::Serialize,
) -> Result<Self, agent_client_protocol::Error> {
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::Value, agent_client_protocol::Error> {
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<Self, agent_client_protocol::Error> {
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"
);
}
}

View file

@ -29,7 +29,6 @@ fn send_replay_content_chunk(
fn replay_conversation_to_client(
cx: &ConnectionTo<Client>,
session: &Session,
supports_goose_custom_notifications: bool,
) -> Result<HashMap<String, crate::conversation::message::ToolRequest>, 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::<String, crate::conversation::message::ToolRequest>::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<String> {
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;

View file

@ -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<tokio::sync::oneshot::Sender<ActionRequiredResponse>>,
session_id: String,
response_tx: Option<tokio::sync::oneshot::Sender<ElicitationOutcome>>,
}
pub struct ActionRequiredManager {
pending: Arc<RwLock<HashMap<String, Arc<Mutex<PendingRequest>>>>>,
request_tx: mpsc::UnboundedSender<Message>,
pub request_rx: Mutex<mpsc::UnboundedReceiver<Message>>,
pub(crate) struct PendingResponseClaim {
request_id: String,
pending: OwnedMutexGuard<PendingRequest>,
}
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<ActionRequiredManager> =
once_cell::sync::Lazy::new(ActionRequiredManager::new);
&INSTANCE
}
pub async fn request_and_wait(
&self,
message: String,
schema: Value,
timeout_duration: Duration,
) -> Result<ActionRequiredResponse> {
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<RwLock<HashMap<String, Arc<Mutex<PendingRequest>>>>>,
queued_requests: Mutex<HashMap<String, VecDeque<Message>>>,
}
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<ActionRequiredManager> =
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<ElicitationOutcome> {
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<PendingResponseClaim> {
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<Arc<Mutex<PendingRequest>>> {
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<Mutex<PendingRequest>>,
mut rx: tokio::sync::oneshot::Receiver<ElicitationOutcome>,
timeout_duration: Duration,
) -> Result<ElicitationOutcome> {
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<ElicitationOutcome, tokio::sync::oneshot::error::RecvError>,
) -> Result<ElicitationOutcome> {
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<Message> {
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<Message> {
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
);
}
}

View file

@ -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<Message> {
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()));
}
}

View file

@ -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<RoleClient>,
context: RequestContext<RoleClient>,
) -> Result<CreateElicitationResult, ErrorData> {
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| {

View file

@ -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
}

View file

@ -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;

View file

@ -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<void> {
await this.conn.extMethod("_goose/unstable/elicitation/respond", params);
}
async sessionProjectUpdate_unstable(
params: UpdateSessionProjectRequest_unstable,
): Promise<void> {

View file

@ -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",

View file

@ -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;
};

View file

@ -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: "<snake_case>"`).
@ -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,