diff --git a/rust/src/types.rs b/rust/src/types.rs index 332a48d18c..fa61236109 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -2235,9 +2235,18 @@ pub struct SessionConfig { /// server OAuth requests with host-acquired token data or cancellation. pub mcp_auth_handler: Option>, /// Optional handler for the legacy question-and-answer `ask_user` variant. - /// When `None`, `requestUserInput: false` goes on the wire, so this client - /// cannot handle legacy user-input requests. + /// When `None`, + /// `requestUserInput: false` goes on the wire. Enable + /// [`observe_prompt_events`](Self::observe_prompt_events) to answer through events instead. pub user_input_handler: Option>, + /// Observe pending prompt events with their runtime request IDs. + /// + /// An event-driven host can answer `user_input.requested` through + /// `session.rpc().ui().handle_pending_user_input(...)` and retire its UI on + /// `user_input.completed`, including when another client answered. + /// Without a callback handler, the observer is responsible for answering; + /// otherwise the request can remain pending. + pub observe_prompt_events: Option, /// Optional exit-plan-mode handler. When `None`, /// `requestExitPlanMode: false` goes on the wire. pub exit_plan_mode_handler: Option>, @@ -2307,6 +2316,7 @@ impl std::fmt::Debug for SessionConfig { .field("reasoning_summary", &self.reasoning_summary) .field("context_tier", &self.context_tier) .field("streaming", &self.streaming) + .field("observe_prompt_events", &self.observe_prompt_events) .field("system_message", &self.system_message) .field("ask_user_variant", &self.ask_user_variant) .field("tools", &self.tools) @@ -2522,6 +2532,7 @@ impl Default for SessionConfig { elicitation_handler: None, mcp_auth_handler: None, user_input_handler: None, + observe_prompt_events: None, exit_plan_mode_handler: None, auto_mode_switch_handler: None, hooks_handler: None, @@ -2652,6 +2663,7 @@ impl SessionConfig { enable_session_store: self.enable_session_store, enable_skills: self.enable_skills, request_user_input, + observe_prompt_events: self.observe_prompt_events, request_permission: permission_active, request_exit_plan_mode, request_auto_mode_switch, @@ -2753,6 +2765,12 @@ impl SessionConfig { self } + /// Enable event-driven prompt handling. See [`Self::observe_prompt_events`]. + pub fn with_observe_prompt_events(mut self, enabled: bool) -> Self { + self.observe_prompt_events = Some(enabled); + self + } + /// Install an [`ExitPlanModeHandler`]. pub fn with_exit_plan_mode_handler(mut self, handler: Arc) -> Self { self.exit_plan_mode_handler = Some(handler); @@ -3638,6 +3656,8 @@ pub struct ResumeSessionConfig { /// Optional user-input handler. See /// [`SessionConfig::user_input_handler`]. pub user_input_handler: Option>, + /// See [`SessionConfig::observe_prompt_events`]. + pub observe_prompt_events: Option, /// Optional exit-plan-mode handler. See /// [`SessionConfig::exit_plan_mode_handler`]. pub exit_plan_mode_handler: Option>, @@ -3677,6 +3697,7 @@ impl std::fmt::Debug for ResumeSessionConfig { .field("reasoning_summary", &self.reasoning_summary) .field("context_tier", &self.context_tier) .field("streaming", &self.streaming) + .field("observe_prompt_events", &self.observe_prompt_events) .field("system_message", &self.system_message) .field("ask_user_variant", &self.ask_user_variant) .field("tools", &self.tools) @@ -3895,6 +3916,7 @@ impl ResumeSessionConfig { enable_session_store: self.enable_session_store, enable_skills: self.enable_skills, request_user_input, + observe_prompt_events: self.observe_prompt_events, request_permission: permission_active, request_exit_plan_mode, request_auto_mode_switch, @@ -4046,6 +4068,7 @@ impl ResumeSessionConfig { elicitation_handler: None, mcp_auth_handler: None, user_input_handler: None, + observe_prompt_events: None, exit_plan_mode_handler: None, auto_mode_switch_handler: None, hooks_handler: None, @@ -4090,6 +4113,12 @@ impl ResumeSessionConfig { self } + /// Enable event-driven prompt handling. See [`SessionConfig::observe_prompt_events`]. + pub fn with_observe_prompt_events(mut self, enabled: bool) -> Self { + self.observe_prompt_events = Some(enabled); + self + } + /// Install an [`ExitPlanModeHandler`] for the resumed session. pub fn with_exit_plan_mode_handler(mut self, handler: Arc) -> Self { self.exit_plan_mode_handler = Some(handler); @@ -6224,6 +6253,9 @@ impl Default for ExitPlanModeData { } } +#[cfg(test)] +mod prompt_observer_tests; + #[cfg(test)] mod tests { use std::collections::HashMap; diff --git a/rust/src/types/prompt_observer_tests.rs b/rust/src/types/prompt_observer_tests.rs new file mode 100644 index 0000000000..ad91d0252d --- /dev/null +++ b/rust/src/types/prompt_observer_tests.rs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +#![cfg(test)] + +use super::*; + +#[test] +fn omitted_prompt_observation_preserves_create_and_resume_wire_shapes() { + let (create, _) = SessionConfig::default().into_wire(None).unwrap(); + let (resume, _) = ResumeSessionConfig::new(SessionId::from("observer")) + .into_wire() + .unwrap(); + for wire in [ + serde_json::to_value(create).unwrap(), + serde_json::to_value(resume).unwrap(), + ] { + assert!(wire.get("observePromptEvents").is_none()); + assert_eq!(wire["requestUserInput"], false); + assert_eq!(wire["requestPermission"], false); + assert_eq!(wire["requestElicitation"], false); + } +} + +#[test] +fn prompt_observation_is_serialized_independently_of_callback_handlers() { + for enabled in [true, false] { + let (create, _) = SessionConfig::default() + .with_observe_prompt_events(enabled) + .into_wire(None) + .unwrap(); + let (resume, _) = ResumeSessionConfig::new(SessionId::from("observer")) + .with_observe_prompt_events(enabled) + .into_wire() + .unwrap(); + for wire in [ + serde_json::to_value(create).unwrap(), + serde_json::to_value(resume).unwrap(), + ] { + assert_eq!(wire["observePromptEvents"], enabled); + assert_eq!(wire["requestUserInput"], false); + assert_eq!(wire["requestPermission"], false); + assert_eq!(wire["requestElicitation"], false); + } + } +} diff --git a/rust/src/wire.rs b/rust/src/wire.rs index 325dfdaaf1..e11126a1df 100644 --- a/rust/src/wire.rs +++ b/rust/src/wire.rs @@ -115,6 +115,8 @@ pub(crate) struct SessionCreateWire { #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, pub request_user_input: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub observe_prompt_events: Option, pub request_permission: bool, pub request_exit_plan_mode: bool, pub request_auto_mode_switch: bool, @@ -276,6 +278,8 @@ pub(crate) struct SessionResumeWire { #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, pub request_user_input: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub observe_prompt_events: Option, pub request_permission: bool, pub request_exit_plan_mode: bool, pub request_auto_mode_switch: bool,