mirror of
https://github.com/block/goose.git
synced 2026-07-09 16:09:22 +00:00
feat: implement acp method for elicitation and elicitation improvement (#9797)
This commit is contained in:
parent
1f0f5d90cb
commit
72da97204e
18 changed files with 775 additions and 521 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
253
crates/goose/src/acp/server/elicitation.rs
Normal file
253
crates/goose/src/acp/server/elicitation.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
71
crates/goose/src/elicitation.rs
Normal file
71
crates/goose/src/elicitation.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue