Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2235,9 +2235,18 @@ pub struct SessionConfig {
/// server OAuth requests with host-acquired token data or cancellation.
pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
/// 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<Arc<dyn UserInputHandler>>,
/// 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<bool>,
/// Optional exit-plan-mode handler. When `None`,
/// `requestExitPlanMode: false` goes on the wire.
pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<dyn ExitPlanModeHandler>) -> Self {
self.exit_plan_mode_handler = Some(handler);
Expand Down Expand Up @@ -3638,6 +3656,8 @@ pub struct ResumeSessionConfig {
/// Optional user-input handler. See
/// [`SessionConfig::user_input_handler`].
pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
/// See [`SessionConfig::observe_prompt_events`].
pub observe_prompt_events: Option<bool>,
/// Optional exit-plan-mode handler. See
/// [`SessionConfig::exit_plan_mode_handler`].
pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<dyn ExitPlanModeHandler>) -> Self {
self.exit_plan_mode_handler = Some(handler);
Expand Down Expand Up @@ -6224,6 +6253,9 @@ impl Default for ExitPlanModeData {
}
}

#[cfg(test)]
mod prompt_observer_tests;

#[cfg(test)]
mod tests {
use std::collections::HashMap;
Expand Down
45 changes: 45 additions & 0 deletions rust/src/types/prompt_observer_tests.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
4 changes: 4 additions & 0 deletions rust/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ pub(crate) struct SessionCreateWire {
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_skills: Option<bool>,
pub request_user_input: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub observe_prompt_events: Option<bool>,
pub request_permission: bool,
pub request_exit_plan_mode: bool,
pub request_auto_mode_switch: bool,
Expand Down Expand Up @@ -276,6 +278,8 @@ pub(crate) struct SessionResumeWire {
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_skills: Option<bool>,
pub request_user_input: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub observe_prompt_events: Option<bool>,
pub request_permission: bool,
pub request_exit_plan_mode: bool,
pub request_auto_mode_switch: bool,
Expand Down
Loading