1use std::collections::HashMap;
2use std::panic::AssertUnwindSafe;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use futures_util::FutureExt;
8use parking_lot::Mutex as ParkingLotMutex;
9use serde_json::Value;
10use tokio::sync::oneshot;
11use tokio::task::JoinHandle;
12use tokio_util::sync::CancellationToken;
13use tracing::{Instrument, error, warn};
14
15use crate::canvas::CanvasHandler;
16use crate::generated::api_types::{
17 LogRequest, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, ModelSwitchToRequest,
18 OpenCanvasInstance, PermissionDecisionRequest, RegisterEventInterestParams,
19 ToolsGetCurrentMetadataResult, rpc_methods,
20};
21use crate::generated::session_events::{
22 CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData,
23 SessionCanvasClosedData, SessionErrorData, SessionEventType, SessionIdleData, SessionMode,
24};
25use crate::handler::{
26 AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler,
27 McpAuthHandler, McpAuthRequest, McpAuthResult, PermissionHandler, PermissionResult,
28 UserInputHandler, UserInputResponse,
29};
30use crate::hooks::SessionHooks;
31use crate::provider_token::BearerTokenProvider;
32use crate::session_fs::SessionFsProvider;
33use crate::trace_context::inject_trace_context;
34use crate::transforms::SystemMessageTransform;
35use crate::types::{
36 AutoTier, AutoTierPreference, CommandContext, CommandDefinition, CommandHandler,
37 CreateSessionResult, ElicitationRequest, ElicitationResult, ExitPlanModeData,
38 GetMessagesResponse, MessageOptions, PermissionRequestData, RequestId, ResumeSessionConfig,
39 ResumeSessionResult, SectionOverride, SessionCapabilities, SessionConfig, SessionEvent,
40 SessionId, SetModelOptions, SystemMessageConfig, ToolInvocation, ToolResult,
41 ToolResultExpanded, TraceContext, UiInputOptions, ensure_attachment_display_names,
42};
43use crate::{
44 Client, Error, ErrorKind, JsonRpcResponse, SessionErrorKind, SessionEventNotification,
45 error_codes,
46};
47
48const TOOL_SEARCH_TOOL_NAME: &str = "tool_search_tool";
52
53pub const DEFAULT_EVENT_BUFFER_CAPACITY: usize = 512;
61
62fn resolve_event_buffer_capacity(capacity: Option<usize>) -> Result<usize, Error> {
68 match capacity {
69 Some(0) => Err(Error::with_message(
70 ErrorKind::InvalidConfig,
71 "event_buffer_capacity must be greater than zero",
72 )),
73 Some(capacity) => Ok(capacity),
74 None => Ok(DEFAULT_EVENT_BUFFER_CAPACITY),
75 }
76}
77
78#[derive(Clone)]
86pub(crate) struct SessionHandlers {
87 pub permission: Option<Arc<dyn PermissionHandler>>,
88 pub managed_settings_enabled: bool,
89 pub elicitation: Option<Arc<dyn ElicitationHandler>>,
90 pub mcp_auth: Option<Arc<dyn McpAuthHandler>>,
91 pub user_input: Option<Arc<dyn UserInputHandler>>,
92 pub exit_plan_mode: Option<Arc<dyn ExitPlanModeHandler>>,
93 pub auto_mode_switch: Option<Arc<dyn AutoModeSwitchHandler>>,
94 pub tools: Arc<HashMap<String, Arc<dyn crate::tool::ToolHandler>>>,
95}
96
97type PendingExternalTools = Arc<ParkingLotMutex<HashMap<RequestId, Arc<CancellationToken>>>>;
98
99struct PendingExternalToolGuard {
100 request_id: RequestId,
101 token: Arc<CancellationToken>,
102 pending: PendingExternalTools,
103}
104
105impl Drop for PendingExternalToolGuard {
106 fn drop(&mut self) {
107 let mut pending = self.pending.lock();
108 if pending
109 .get(&self.request_id)
110 .is_some_and(|token| Arc::ptr_eq(token, &self.token))
111 {
112 pending.remove(&self.request_id);
113 }
114 }
115}
116
117impl PendingExternalToolGuard {
118 fn claim(&self) -> bool {
119 let mut pending = self.pending.lock();
120 if pending
121 .get(&self.request_id)
122 .is_some_and(|token| Arc::ptr_eq(token, &self.token))
123 {
124 pending.remove(&self.request_id);
125 true
126 } else {
127 false
128 }
129 }
130}
131
132fn has_managed_settings(
133 enable_managed_settings: Option<bool>,
134 managed_settings: Option<&crate::types::ManagedSettings>,
135) -> bool {
136 enable_managed_settings == Some(true) || managed_settings.is_some()
137}
138
139struct IdleWaiter {
141 tx: oneshot::Sender<Result<Option<SessionEvent>, Error>>,
142 last_assistant_message: Option<SessionEvent>,
143 started_at: Instant,
144 first_assistant_message_seen: bool,
145}
146
147struct WaiterGuard {
159 slot: Arc<ParkingLotMutex<Option<IdleWaiter>>>,
160}
161
162impl Drop for WaiterGuard {
163 fn drop(&mut self) {
164 self.slot.lock().take();
165 }
166}
167
168struct PendingSessionRegistration {
169 client: Client,
170 session_id: PendingSessionId,
171 shutdown: CancellationToken,
172 external_tools_shutdown: CancellationToken,
173 disarmed: bool,
174}
175
176enum PendingSessionId {
184 Known(SessionId, crate::router::RegistrationToken),
187 Deferred(Arc<ParkingLotMutex<Option<(SessionId, crate::router::SessionRegistration)>>>),
190}
191
192impl PendingSessionRegistration {
193 fn new(
194 client: Client,
195 session_id: SessionId,
196 token: crate::router::RegistrationToken,
197 shutdown: CancellationToken,
198 external_tools_shutdown: CancellationToken,
199 ) -> Self {
200 Self {
201 client,
202 session_id: PendingSessionId::Known(session_id, token),
203 shutdown,
204 external_tools_shutdown,
205 disarmed: false,
206 }
207 }
208
209 fn deferred(
211 client: Client,
212 stash: Arc<ParkingLotMutex<Option<(SessionId, crate::router::SessionRegistration)>>>,
213 shutdown: CancellationToken,
214 external_tools_shutdown: CancellationToken,
215 ) -> Self {
216 Self {
217 client,
218 session_id: PendingSessionId::Deferred(stash),
219 shutdown,
220 external_tools_shutdown,
221 disarmed: false,
222 }
223 }
224
225 fn registered_id(&self) -> Option<SessionId> {
226 match &self.session_id {
227 PendingSessionId::Known(id, _) => Some(id.clone()),
228 PendingSessionId::Deferred(stash) => stash.lock().as_ref().map(|(id, _)| id.clone()),
229 }
230 }
231
232 fn resolve_to(&mut self, session_id: SessionId, token: crate::router::RegistrationToken) {
236 self.session_id = PendingSessionId::Known(session_id, token);
237 }
238
239 async fn cleanup(mut self, event_loop: JoinHandle<()>) {
240 self.external_tools_shutdown.cancel();
241 self.shutdown.cancel();
242 let _ = event_loop.await;
243 if let Some(id) = self.registered_id() {
244 if let PendingSessionId::Known(_, token) = self.session_id {
245 self.client.unregister_session_owned(&id, token);
246 } else if let PendingSessionId::Deferred(stash) = &self.session_id
247 && let Some((id, registration)) = stash.lock().as_ref()
248 {
249 self.client.unregister_session_owned(id, registration.token);
250 }
251 }
252 self.disarmed = true;
253 }
254
255 fn disarm(&mut self) {
256 self.disarmed = true;
257 }
258}
259
260impl Drop for PendingSessionRegistration {
261 fn drop(&mut self) {
262 if !self.disarmed {
263 self.external_tools_shutdown.cancel();
264 self.shutdown.cancel();
265 if let Some(id) = self.registered_id() {
266 if let PendingSessionId::Known(_, token) = self.session_id {
267 self.client.unregister_session_owned(&id, token);
268 } else if let PendingSessionId::Deferred(stash) = &self.session_id
269 && let Some((id, registration)) = stash.lock().as_ref()
270 {
271 self.client.unregister_session_owned(id, registration.token);
272 }
273 }
274 }
275 }
276}
277
278pub struct Session {
291 id: SessionId,
292 cwd: PathBuf,
293 workspace_path: Option<PathBuf>,
294 remote_url: Option<String>,
295 client: Client,
296 event_loop: ParkingLotMutex<Option<JoinHandle<()>>>,
301 shutdown: CancellationToken,
315 external_tools_shutdown: CancellationToken,
318 idle_waiter: Arc<ParkingLotMutex<Option<IdleWaiter>>>,
325 capabilities: Arc<parking_lot::RwLock<SessionCapabilities>>,
327 open_canvases: Arc<parking_lot::RwLock<Vec<OpenCanvasInstance>>>,
329 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
331 github_token_registration:
332 ParkingLotMutex<Option<crate::github_token::GitHubTokenRegistration>>,
333 registration_token: crate::router::RegistrationToken,
335}
336
337impl Session {
338 pub fn id(&self) -> &SessionId {
340 &self.id
341 }
342
343 pub fn cwd(&self) -> &PathBuf {
345 &self.cwd
346 }
347
348 pub fn workspace_path(&self) -> Option<&Path> {
350 self.workspace_path.as_deref()
351 }
352
353 pub fn remote_url(&self) -> Option<&str> {
355 self.remote_url.as_deref()
356 }
357
358 pub fn capabilities(&self) -> SessionCapabilities {
363 self.capabilities.read().clone()
364 }
365
366 pub fn open_canvases(&self) -> Vec<OpenCanvasInstance> {
369 self.open_canvases.read().clone()
370 }
371
372 pub fn cancellation_token(&self) -> CancellationToken {
399 self.shutdown.child_token()
400 }
401
402 pub fn subscribe(&self) -> crate::subscription::EventSubscription {
441 crate::subscription::EventSubscription::new(self.event_tx.subscribe())
442 }
443
444 pub fn client(&self) -> &Client {
446 &self.client
447 }
448
449 pub fn rpc(&self) -> crate::generated::rpc::SessionRpc<'_> {
460 crate::generated::rpc::SessionRpc { session: self }
461 }
462
463 pub async fn stop_event_loop(&self) {
476 self.shutdown.cancel();
477 let handle = self.event_loop.lock().take();
478 if let Some(handle) = handle {
479 let _ = handle.await;
480 }
481 if let Some(waiter) = self.idle_waiter.lock().take() {
483 let _ = waiter.tx.send(Err(
484 ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()
485 ));
486 }
487 }
488
489 pub async fn send(&self, opts: impl Into<MessageOptions>) -> Result<String, Error> {
514 if self.idle_waiter.lock().is_some() {
515 return Err(ErrorKind::Session(SessionErrorKind::SendWhileWaiting).into());
516 }
517 self.send_inner(opts.into()).await
518 }
519
520 async fn send_inner(&self, opts: MessageOptions) -> Result<String, Error> {
521 let mut params = serde_json::json!({
522 "sessionId": self.id,
523 "prompt": opts.prompt,
524 });
525 if let Some(m) = opts.mode {
526 params["mode"] = serde_json::to_value(m)?;
527 }
528 if let Some(am) = opts.agent_mode {
529 params["agentMode"] = serde_json::to_value(am)?;
530 }
531 if let Some(mut a) = opts.attachments {
532 ensure_attachment_display_names(&mut a);
533 params["attachments"] = serde_json::to_value(a)?;
534 }
535 if let Some(headers) = opts.request_headers
536 && !headers.is_empty()
537 {
538 params["requestHeaders"] = serde_json::to_value(headers)?;
539 }
540 if let Some(display_prompt) = opts.display_prompt {
541 params["displayPrompt"] = serde_json::to_value(display_prompt)?;
542 }
543 let trace_ctx = if opts.traceparent.is_some() || opts.tracestate.is_some() {
544 TraceContext {
545 traceparent: opts.traceparent,
546 tracestate: opts.tracestate,
547 }
548 } else {
549 self.client.resolve_trace_context().await
550 };
551 inject_trace_context(&mut params, &trace_ctx);
552 let rpc_start = Instant::now();
553 let result = self.client.call("session.send", Some(params)).await?;
554 let message_id = result
555 .get("messageId")
556 .and_then(|v| v.as_str())
557 .map(|s| s.to_string())
558 .unwrap_or_default();
559 tracing::debug!(
560 elapsed_ms = rpc_start.elapsed().as_millis(),
561 session_id = %self.id,
562 message_id = %message_id,
563 "Session::send completed successfully"
564 );
565 Ok(message_id)
566 }
567
568 pub async fn send_and_wait(
588 &self,
589 opts: impl Into<MessageOptions>,
590 ) -> Result<Option<SessionEvent>, Error> {
591 let total_start = Instant::now();
592 let opts = opts.into();
593 let timeout_duration = opts.wait_timeout.unwrap_or(Duration::from_secs(60));
594 let (tx, rx) = oneshot::channel();
595
596 {
597 let mut guard = self.idle_waiter.lock();
598 if guard.is_some() {
599 return Err(ErrorKind::Session(SessionErrorKind::SendWhileWaiting).into());
600 }
601 *guard = Some(IdleWaiter {
602 tx,
603 last_assistant_message: None,
604 started_at: total_start,
605 first_assistant_message_seen: false,
606 });
607 }
608
609 let _waiter_guard = WaiterGuard {
614 slot: self.idle_waiter.clone(),
615 };
616
617 let result = tokio::time::timeout(timeout_duration, async {
618 self.send_inner(opts).await?;
619 match rx.await {
620 Ok(result) => result,
621 Err(_) => Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()),
622 }
623 })
624 .await;
625
626 match result {
627 Ok(inner) => {
628 tracing::debug!(
629 elapsed_ms = total_start.elapsed().as_millis(),
630 session_id = %self.id,
631 completed_by = if inner.is_ok() { "idle" } else { "error" },
632 "Session::send_and_wait complete"
633 );
634 inner
635 }
636 Err(_) => {
637 tracing::warn!(
638 elapsed_ms = total_start.elapsed().as_millis(),
639 session_id = %self.id,
640 completed_by = "timeout",
641 "Session::send_and_wait failed"
642 );
643 Err(ErrorKind::Session(SessionErrorKind::Timeout(timeout_duration)).into())
644 }
645 }
646 }
647
648 pub async fn get_events(&self) -> Result<Vec<SessionEvent>, Error> {
650 let result = self
651 .client
652 .call(
653 "session.getMessages",
654 Some(serde_json::json!({ "sessionId": self.id })),
655 )
656 .await?;
657 let response: GetMessagesResponse = serde_json::from_value(result)?;
658 Ok(response.events)
659 }
660
661 #[deprecated(since = "0.1.0", note = "Use `get_events()` instead")]
663 pub async fn get_messages(&self) -> Result<Vec<SessionEvent>, Error> {
664 self.get_events().await
665 }
666
667 pub async fn abort(&self) -> Result<(), Error> {
675 self.client
676 .call(
677 "session.abort",
678 Some(serde_json::json!({ "sessionId": self.id })),
679 )
680 .await?;
681 Ok(())
682 }
683
684 pub async fn set_model(&self, model: &str, opts: Option<SetModelOptions>) -> Result<(), Error> {
688 let opts = opts.unwrap_or_default();
689 let auto_tier = opts.auto_tier.clone();
690 let request = ModelSwitchToRequest {
691 auto_tier: match &auto_tier {
692 Some(AutoTierPreference::Tier(tier)) => Some(tier.clone()),
693 _ => None,
694 },
695 compaction_decision: None,
696 context_tier: opts.context_tier,
697 defer_if_model_change_queued: None,
698 model_capabilities: opts.model_capabilities,
699 model_change_scope: None,
700 model_id: model.to_string(),
701 picker_persistence: None,
702 reasoning_effort: opts.reasoning_effort,
703 reasoning_summary: opts.reasoning_summary,
704 repo_scope: None,
705 require_available: None,
706 run_compaction_preflight: None,
707 source: None,
708 verbosity: None,
709 };
710
711 if matches!(auto_tier, Some(AutoTierPreference::Reset)) {
712 let mut wire_params = serde_json::to_value(request)?;
716 wire_params["sessionId"] = serde_json::Value::String(self.id.to_string());
717 wire_params["autoTier"] = serde_json::Value::Null;
718 self.client
719 .call("session.model.switchTo", Some(wire_params))
720 .await?;
721 return Ok(());
722 }
723
724 self.rpc().model().switch_to(request).await?;
725 Ok(())
726 }
727
728 pub async fn set_auto_tier(
757 &self,
758 auto_tier: Option<AutoTier>,
759 ) -> Result<ModelSwitchAutoTierResult, Error> {
760 self.rpc()
761 .model()
762 .switch_auto_tier(ModelSwitchAutoTierRequest {
763 auto_tier,
764 source: None,
765 })
766 .await
767 }
768
769 pub async fn disconnect(&self) -> Result<(), Error> {
786 self.client.detach_session(&self.id).await?;
787 self.external_tools_shutdown.cancel();
788 self.stop_event_loop().await;
789 self.github_token_registration.lock().take();
790 self.client
791 .unregister_session_owned(&self.id, self.registration_token);
792 Ok(())
793 }
794
795 #[deprecated(since = "0.1.0", note = "Use `disconnect()` instead")]
798 pub async fn destroy(&self) -> Result<(), Error> {
799 self.disconnect().await
800 }
801
802 pub async fn log(
806 &self,
807 message: &str,
808 opts: Option<crate::types::LogOptions>,
809 ) -> Result<(), Error> {
810 let opts = opts.unwrap_or_default();
811 let level = match opts.level {
812 Some(level) => Some(serde_json::from_value(serde_json::to_value(level)?)?),
813 None => None,
814 };
815 let request = LogRequest {
816 message: message.to_string(),
817 level,
818 ephemeral: opts.ephemeral,
819 r#type: None,
820 tip: None,
821 url: None,
822 };
823 self.rpc().log(request).await?;
824 Ok(())
825 }
826
827 pub fn ui(&self) -> SessionUi<'_> {
833 SessionUi { session: self }
834 }
835
836 fn assert_elicitation(&self) -> Result<(), Error> {
838 if self
839 .capabilities
840 .read()
841 .ui
842 .as_ref()
843 .and_then(|u| u.elicitation)
844 != Some(true)
845 {
846 return Err(ErrorKind::Session(SessionErrorKind::ElicitationNotSupported).into());
847 }
848 Ok(())
849 }
850}
851
852impl Drop for Session {
853 fn drop(&mut self) {
854 self.shutdown.cancel();
866 self.external_tools_shutdown.cancel();
867 self.github_token_registration.lock().take();
868 self.client
869 .unregister_session_owned(&self.id, self.registration_token);
870 }
871}
872
873pub struct SessionUi<'a> {
880 session: &'a Session,
881}
882
883impl<'a> SessionUi<'a> {
884 pub async fn elicitation(
892 &self,
893 message: &str,
894 schema: Value,
895 ) -> Result<ElicitationResult, Error> {
896 self.session.assert_elicitation()?;
897 let result = self
898 .session
899 .client
900 .call(
901 "session.ui.elicitation",
902 Some(serde_json::json!({
903 "sessionId": self.session.id,
904 "message": message,
905 "requestedSchema": schema,
906 })),
907 )
908 .await?;
909 let elicitation: ElicitationResult = serde_json::from_value(result)?;
910 Ok(elicitation)
911 }
912
913 pub async fn confirm(&self, message: &str) -> Result<bool, Error> {
917 self.session.assert_elicitation()?;
918 let schema = serde_json::json!({
919 "type": "object",
920 "properties": {
921 "confirmed": {
922 "type": "boolean",
923 "default": true,
924 }
925 },
926 "required": ["confirmed"]
927 });
928 let result = self.elicitation(message, schema).await?;
929 Ok(result.action == "accept"
930 && result
931 .content
932 .and_then(|c| c.get("confirmed").and_then(|v| v.as_bool()))
933 == Some(true))
934 }
935
936 pub async fn select(&self, message: &str, options: &[&str]) -> Result<Option<String>, Error> {
940 self.session.assert_elicitation()?;
941 let schema = serde_json::json!({
942 "type": "object",
943 "properties": {
944 "selection": {
945 "type": "string",
946 "enum": options,
947 }
948 },
949 "required": ["selection"]
950 });
951 let result = self.elicitation(message, schema).await?;
952 if result.action != "accept" {
953 return Ok(None);
954 }
955 let selection = result.content.and_then(|c| {
956 c.get("selection")
957 .and_then(|v| v.as_str())
958 .map(String::from)
959 });
960 Ok(selection)
961 }
962
963 pub async fn input(
968 &self,
969 message: &str,
970 options: Option<&UiInputOptions<'_>>,
971 ) -> Result<Option<String>, Error> {
972 self.session.assert_elicitation()?;
973 let mut field = serde_json::json!({ "type": "string" });
974 if let Some(opts) = options {
975 if let Some(title) = opts.title {
976 field["title"] = Value::String(title.to_string());
977 }
978 if let Some(desc) = opts.description {
979 field["description"] = Value::String(desc.to_string());
980 }
981 if let Some(min) = opts.min_length {
982 field["minLength"] = Value::Number(min.into());
983 }
984 if let Some(max) = opts.max_length {
985 field["maxLength"] = Value::Number(max.into());
986 }
987 if let Some(fmt) = &opts.format {
988 field["format"] = Value::String(fmt.as_str().to_string());
989 }
990 if let Some(default) = opts.default {
991 field["default"] = Value::String(default.to_string());
992 }
993 }
994 let schema = serde_json::json!({
995 "type": "object",
996 "properties": { "value": field },
997 "required": ["value"]
998 });
999 let result = self.elicitation(message, schema).await?;
1000 if result.action != "accept" {
1001 return Ok(None);
1002 }
1003 let value = result
1004 .content
1005 .and_then(|c| c.get("value").and_then(|v| v.as_str()).map(String::from));
1006 Ok(value)
1007 }
1008}
1009
1010impl Client {
1011 pub fn prepare_session(&self, config: SessionConfig) -> Result<PreparedSession, Error> {
1073 let capacity = resolve_event_buffer_capacity(config.event_buffer_capacity)?;
1074 Ok(PreparedSession::new(
1075 self.clone(),
1076 PreparedKind::Create(Box::new(config)),
1077 capacity,
1078 ))
1079 }
1080
1081 pub fn prepare_resume_session(
1096 &self,
1097 config: ResumeSessionConfig,
1098 ) -> Result<PreparedSession, Error> {
1099 let capacity = resolve_event_buffer_capacity(config.event_buffer_capacity)?;
1100 Ok(PreparedSession::new(
1101 self.clone(),
1102 PreparedKind::Resume(Box::new(config)),
1103 capacity,
1104 ))
1105 }
1106
1107 pub async fn create_session(&self, config: SessionConfig) -> Result<Session, Error> {
1139 self.prepare_session(config)?.start().await
1140 }
1141
1142 pub async fn resume_session(&self, config: ResumeSessionConfig) -> Result<Session, Error> {
1161 self.prepare_resume_session(config)?.start().await
1162 }
1163
1164 async fn start_prepared_create(
1165 &self,
1166 mut config: SessionConfig,
1167 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
1168 shutdown: CancellationToken,
1169 ) -> Result<Session, Error> {
1170 let total_start = Instant::now();
1171 let caller_session_id = config.session_id.clone();
1179 let use_server_generated_id = config.cloud.is_some() && caller_session_id.is_none();
1180 let local_session_id: Option<SessionId> = if use_server_generated_id {
1181 None
1182 } else {
1183 Some(
1184 caller_session_id
1185 .clone()
1186 .unwrap_or_else(|| SessionId::new(uuid::Uuid::new_v4().to_string())),
1187 )
1188 };
1189 if config.hooks_handler.is_some() && config.hooks.is_none() {
1190 config.hooks = Some(true);
1191 }
1192 if let Some(transforms) = config.system_message_transform.clone() {
1193 inject_transform_sections(&mut config, transforms.as_ref());
1194 }
1195 let mode = self.inner.mode;
1196 if mode == crate::ClientMode::Empty && config.available_tools.is_none() {
1197 return Err(Error::with_message(
1198 ErrorKind::InvalidConfig,
1199 "ClientMode::Empty requires available_tools to be set on the session config. \
1200 Use ToolSet to specify which tools the session may use (e.g. \
1201 ToolSet::new().add_builtin_many(BUILTIN_TOOLS_ISOLATED)).",
1202 ));
1203 }
1204 crate::mode::validate_tool_filter_list(
1205 "available_tools",
1206 config.available_tools.as_deref(),
1207 )?;
1208 crate::mode::validate_tool_filter_list("excluded_tools", config.excluded_tools.as_deref())?;
1209 config.system_message =
1210 crate::mode::system_message_for_mode(mode, config.system_message.take());
1211 config.memory = crate::mode::memory_for_mode(mode, config.memory.take());
1212 config.enable_experimental_mode =
1213 crate::mode::experimental_mode_for_mode(mode, config.enable_experimental_mode);
1214 if mode == crate::ClientMode::Empty {
1215 if config.enable_session_telemetry.is_none() {
1216 config.enable_session_telemetry = Some(false);
1217 }
1218 if config.skip_embedding_retrieval.is_none() {
1219 config.skip_embedding_retrieval = Some(true);
1220 }
1221 if config.enable_on_demand_instruction_discovery.is_none() {
1222 config.enable_on_demand_instruction_discovery = Some(false);
1223 }
1224 if config.enable_file_hooks.is_none() {
1225 config.enable_file_hooks = Some(false);
1226 }
1227 if config.enable_host_git_operations.is_none() {
1228 config.enable_host_git_operations = Some(false);
1229 }
1230 if config.enable_session_store.is_none() {
1231 config.enable_session_store = Some(false);
1232 }
1233 if config.enable_skills.is_none() {
1234 config.enable_skills = Some(false);
1235 }
1236 }
1237 if mode == crate::ClientMode::Empty && config.mcp_oauth_token_storage.is_none() {
1238 config.mcp_oauth_token_storage = Some("in-memory".into());
1239 }
1240 if mode == crate::ClientMode::Empty && config.embedding_cache_storage.is_none() {
1241 config.embedding_cache_storage = Some("in-memory".into());
1242 }
1243 config.custom_agents_local_only =
1244 crate::mode::resolve_custom_agents_local_only(mode, config.custom_agents_local_only);
1245 let opt_skip_custom_instructions = config.skip_custom_instructions;
1246 let opt_custom_agents_local_only = config.custom_agents_local_only;
1247 let opt_coauthor_enabled = config.coauthor_enabled;
1248 let opt_manage_schedule_enabled = config.manage_schedule_enabled;
1249 let opt_included_builtin_skills = config.included_builtin_skills.take();
1250 let (mut wire, mut runtime) = config.into_wire(local_session_id.clone())?;
1251 wire.enable_github_telemetry_forwarding =
1252 self.inner.on_github_telemetry.is_some().then_some(true);
1253
1254 let permission_handler = crate::permission::resolve_handler(
1255 runtime.permission_handler.take(),
1256 runtime.permission_policy.take(),
1257 );
1258 let handlers = SessionHandlers {
1259 permission: permission_handler,
1260 managed_settings_enabled: has_managed_settings(
1261 wire.enable_managed_settings,
1262 wire.managed_settings.as_ref(),
1263 ),
1264 elicitation: runtime.elicitation_handler.take(),
1265 mcp_auth: runtime.mcp_auth_handler.take(),
1266 user_input: runtime.user_input_handler.take(),
1267 exit_plan_mode: runtime.exit_plan_mode_handler.take(),
1268 auto_mode_switch: runtime.auto_mode_switch_handler.take(),
1269 tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)),
1270 };
1271 let hooks = runtime.hooks_handler.take();
1272 let transforms = runtime.system_message_transform.take();
1273 let tools_count = wire.tools.as_ref().map_or(0, Vec::len);
1274 let commands_count = runtime.commands.as_ref().map_or(0, Vec::len);
1275 let has_hooks = hooks.is_some();
1276 let command_handlers = build_command_handler_map(runtime.commands.as_deref());
1277 let canvas_handler = runtime.canvas_handler.take();
1278 let session_fs_provider = runtime.session_fs_provider.take();
1279 let bearer_token_providers = std::mem::take(&mut runtime.bearer_token_providers);
1280 let github_token_registration = runtime
1281 .github_token_provider
1282 .take()
1283 .map(|provider| self.register_github_token_provider(provider));
1284 wire.github_token_provider_registration_id = github_token_registration
1285 .as_ref()
1286 .map(|registration| registration.id().to_string());
1287 let has_mcp_auth_handler = handlers.mcp_auth.is_some();
1288 if self.inner.session_fs_configured && session_fs_provider.is_none() {
1289 return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into());
1290 }
1291 if self.inner.session_fs_sqlite_declared
1292 && let Some(ref provider) = session_fs_provider
1293 && provider.sqlite().is_none()
1294 {
1295 return Err(Error::with_message(
1296 ErrorKind::InvalidConfig,
1297 "SessionFs capabilities declare SQLite support but the provider \
1298 does not implement SessionFsSqliteProvider",
1299 ));
1300 }
1301
1302 let mut params = serde_json::to_value(&wire)?;
1303 let trace_ctx = self.resolve_trace_context().await;
1304 inject_trace_context(&mut params, &trace_ctx);
1305
1306 let setup_start = Instant::now();
1307 let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default()));
1308 let idle_waiter = Arc::new(ParkingLotMutex::new(None));
1309 let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new()));
1310 let external_tools_shutdown = self.inner.rpc.connection_closed_token();
1311
1312 let inline_stash: Arc<
1318 ParkingLotMutex<Option<(SessionId, crate::router::SessionRegistration)>>,
1319 > = Arc::new(ParkingLotMutex::new(None));
1320
1321 let inline_callback: Option<crate::jsonrpc::InlineResponseCallback> = if let Some(ref sid) =
1322 local_session_id
1323 {
1324 let channels = self.register_session(sid);
1325 *inline_stash.lock() = Some((sid.clone(), channels));
1326 None
1327 } else {
1328 let client = self.clone();
1329 let stash = inline_stash.clone();
1330 let expected = caller_session_id.clone();
1331 Some(Box::new(move |response| {
1332 let result = response.result.as_ref().ok_or_else(|| {
1333 Error::with_message(ErrorKind::Json, "session.create response had no result")
1334 })?;
1335 let parsed: CreateSessionResult =
1336 serde_json::from_value(result.clone()).map_err(Error::from)?;
1337 if let Some(requested) = expected.as_ref()
1338 && parsed.session_id != *requested
1339 {
1340 return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch {
1341 requested: requested.clone(),
1342 returned: parsed.session_id,
1343 })
1344 .into());
1345 }
1346 let mut stashed = stash.lock();
1354 let registration = client.register_session(&parsed.session_id);
1355 *stashed = Some((parsed.session_id, registration));
1356 Ok(())
1357 }))
1358 };
1359
1360 let mut pending_registration = match local_session_id {
1366 Some(ref sid) => {
1367 let token = inline_stash
1368 .lock()
1369 .as_ref()
1370 .expect("session registration must exist")
1371 .1
1372 .token;
1373 PendingSessionRegistration::new(
1374 self.clone(),
1375 sid.clone(),
1376 token,
1377 shutdown.clone(),
1378 external_tools_shutdown.clone(),
1379 )
1380 }
1381 None => PendingSessionRegistration::deferred(
1382 self.clone(),
1383 inline_stash.clone(),
1384 shutdown.clone(),
1385 external_tools_shutdown.clone(),
1386 ),
1387 };
1388
1389 let rpc_start = Instant::now();
1390 let result = self
1391 .call_with_inline_callback("session.create", Some(params), inline_callback)
1392 .await?;
1393 tracing::debug!(
1394 elapsed_ms = rpc_start.elapsed().as_millis(),
1395 "Client::create_session session creation request completed successfully"
1396 );
1397 let create_result: CreateSessionResult = serde_json::from_value(result)?;
1398
1399 if let Some(ref requested) = local_session_id
1400 && create_result.session_id != *requested
1401 {
1402 return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch {
1403 requested: requested.clone(),
1404 returned: create_result.session_id.clone(),
1405 })
1406 .into());
1407 }
1408
1409 let (session_id, registration) = inline_stash
1410 .lock()
1411 .take()
1412 .expect("session registration must have populated stash on success");
1413 let channels = registration.channels;
1414 let registration_token = registration.token;
1415 pending_registration.resolve_to(session_id.clone(), registration_token);
1416 let event_loop = spawn_event_loop(
1417 session_id.clone(),
1418 self.clone(),
1419 handlers,
1420 hooks,
1421 transforms,
1422 command_handlers,
1423 canvas_handler,
1424 session_fs_provider,
1425 bearer_token_providers,
1426 channels,
1427 idle_waiter.clone(),
1428 capabilities.clone(),
1429 open_canvases.clone(),
1430 event_tx.clone(),
1431 shutdown.clone(),
1432 external_tools_shutdown.clone(),
1433 );
1434 tracing::debug!(
1435 elapsed_ms = setup_start.elapsed().as_millis(),
1436 session_id = %session_id,
1437 tools_count,
1438 commands_count,
1439 has_hooks,
1440 "Client::create_session local setup complete"
1441 );
1442 *capabilities.write() = create_result.capabilities.unwrap_or_default();
1443 if has_mcp_auth_handler
1444 && let Err(error) = register_mcp_auth_interest(self, &session_id).await
1445 {
1446 pending_registration.cleanup(event_loop).await;
1447 return Err(error);
1448 }
1449
1450 tracing::debug!(
1451 elapsed_ms = total_start.elapsed().as_millis(),
1452 session_id = %session_id,
1453 "Client::create_session complete"
1454 );
1455 pending_registration.disarm();
1456 let session = Session {
1457 id: session_id,
1458 cwd: self.cwd().clone(),
1459 workspace_path: create_result.workspace_path,
1460 remote_url: create_result.remote_url,
1461 client: self.clone(),
1462 event_loop: ParkingLotMutex::new(Some(event_loop)),
1463 shutdown,
1464 external_tools_shutdown,
1465 idle_waiter,
1466 capabilities,
1467 open_canvases,
1468 event_tx,
1469 github_token_registration: ParkingLotMutex::new(github_token_registration),
1470 registration_token,
1471 };
1472 apply_mode_post_create_patch(
1473 &session,
1474 mode,
1475 opt_skip_custom_instructions,
1476 opt_custom_agents_local_only,
1477 opt_coauthor_enabled,
1478 opt_manage_schedule_enabled,
1479 opt_included_builtin_skills,
1480 )
1481 .await?;
1482 if let Some(registration) = session.github_token_registration.lock().as_ref() {
1483 registration.claim(session.id.clone());
1484 } else {
1485 self.retire_github_token_provider(&session.id);
1486 }
1487 Ok(session)
1488 }
1489
1490 async fn start_prepared_resume(
1501 &self,
1502 mut config: ResumeSessionConfig,
1503 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
1504 shutdown: CancellationToken,
1505 ) -> Result<Session, Error> {
1506 let total_start = Instant::now();
1507 let session_id = config.session_id.clone();
1508 if config.hooks_handler.is_some() && config.hooks.is_none() {
1509 config.hooks = Some(true);
1510 }
1511 if let Some(transforms) = config.system_message_transform.clone() {
1512 inject_transform_sections_resume(&mut config, transforms.as_ref());
1513 }
1514 let mode = self.inner.mode;
1515 if mode == crate::ClientMode::Empty && config.available_tools.is_none() {
1516 return Err(Error::with_message(
1517 ErrorKind::InvalidConfig,
1518 "ClientMode::Empty requires available_tools to be set on the session config. \
1519 Use ToolSet to specify which tools the session may use (e.g. \
1520 ToolSet::new().add_builtin_many(BUILTIN_TOOLS_ISOLATED)).",
1521 ));
1522 }
1523 crate::mode::validate_tool_filter_list(
1524 "available_tools",
1525 config.available_tools.as_deref(),
1526 )?;
1527 crate::mode::validate_tool_filter_list("excluded_tools", config.excluded_tools.as_deref())?;
1528 config.system_message =
1529 crate::mode::system_message_for_mode(mode, config.system_message.take());
1530 config.memory = crate::mode::memory_for_mode(mode, config.memory.take());
1531 config.enable_experimental_mode =
1532 crate::mode::experimental_mode_for_mode(mode, config.enable_experimental_mode);
1533 if mode == crate::ClientMode::Empty {
1534 if config.enable_session_telemetry.is_none() {
1535 config.enable_session_telemetry = Some(false);
1536 }
1537 if config.skip_embedding_retrieval.is_none() {
1538 config.skip_embedding_retrieval = Some(true);
1539 }
1540 if config.enable_on_demand_instruction_discovery.is_none() {
1541 config.enable_on_demand_instruction_discovery = Some(false);
1542 }
1543 if config.enable_file_hooks.is_none() {
1544 config.enable_file_hooks = Some(false);
1545 }
1546 if config.enable_host_git_operations.is_none() {
1547 config.enable_host_git_operations = Some(false);
1548 }
1549 if config.enable_session_store.is_none() {
1550 config.enable_session_store = Some(false);
1551 }
1552 if config.enable_skills.is_none() {
1553 config.enable_skills = Some(false);
1554 }
1555 }
1556 if mode == crate::ClientMode::Empty && config.mcp_oauth_token_storage.is_none() {
1557 config.mcp_oauth_token_storage = Some("in-memory".into());
1558 }
1559 if mode == crate::ClientMode::Empty && config.embedding_cache_storage.is_none() {
1560 config.embedding_cache_storage = Some("in-memory".into());
1561 }
1562 config.custom_agents_local_only =
1563 crate::mode::resolve_custom_agents_local_only(mode, config.custom_agents_local_only);
1564 let opt_skip_custom_instructions = config.skip_custom_instructions;
1565 let opt_custom_agents_local_only = config.custom_agents_local_only;
1566 let opt_coauthor_enabled = config.coauthor_enabled;
1567 let opt_manage_schedule_enabled = config.manage_schedule_enabled;
1568 let opt_included_builtin_skills = config.included_builtin_skills.take();
1569 let (mut wire, mut runtime) = config.into_wire()?;
1570 wire.enable_github_telemetry_forwarding =
1571 self.inner.on_github_telemetry.is_some().then_some(true);
1572
1573 let permission_handler = crate::permission::resolve_handler(
1574 runtime.permission_handler.take(),
1575 runtime.permission_policy.take(),
1576 );
1577 let handlers = SessionHandlers {
1578 permission: permission_handler,
1579 managed_settings_enabled: has_managed_settings(
1580 wire.enable_managed_settings,
1581 wire.managed_settings.as_ref(),
1582 ),
1583 elicitation: runtime.elicitation_handler.take(),
1584 mcp_auth: runtime.mcp_auth_handler.take(),
1585 user_input: runtime.user_input_handler.take(),
1586 exit_plan_mode: runtime.exit_plan_mode_handler.take(),
1587 auto_mode_switch: runtime.auto_mode_switch_handler.take(),
1588 tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)),
1589 };
1590 let hooks = runtime.hooks_handler.take();
1591 let transforms = runtime.system_message_transform.take();
1592 let tools_count = wire.tools.as_ref().map_or(0, Vec::len);
1593 let commands_count = runtime.commands.as_ref().map_or(0, Vec::len);
1594 let has_hooks = hooks.is_some();
1595 let command_handlers = build_command_handler_map(runtime.commands.as_deref());
1596 let canvas_handler = runtime.canvas_handler.take();
1597 let session_fs_provider = runtime.session_fs_provider.take();
1598 let bearer_token_providers = std::mem::take(&mut runtime.bearer_token_providers);
1599 let github_token_registration = runtime
1600 .github_token_provider
1601 .take()
1602 .map(|provider| self.register_github_token_provider(provider));
1603 wire.github_token_provider_registration_id = github_token_registration
1604 .as_ref()
1605 .map(|registration| registration.id().to_string());
1606 let has_mcp_auth_handler = handlers.mcp_auth.is_some();
1607 if self.inner.session_fs_configured && session_fs_provider.is_none() {
1608 return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into());
1609 }
1610 if self.inner.session_fs_sqlite_declared
1611 && let Some(ref provider) = session_fs_provider
1612 && provider.sqlite().is_none()
1613 {
1614 return Err(Error::with_message(
1615 ErrorKind::InvalidConfig,
1616 "SessionFs capabilities declare SQLite support but the provider \
1617 does not implement SessionFsSqliteProvider",
1618 ));
1619 }
1620
1621 let mut params = serde_json::to_value(&wire)?;
1622 let trace_ctx = self.resolve_trace_context().await;
1623 inject_trace_context(&mut params, &trace_ctx);
1624
1625 let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default()));
1626 let setup_start = Instant::now();
1627 let registration = self.register_session(&session_id);
1628 let registration_token = registration.token;
1629 let channels = registration.channels;
1630 let idle_waiter = Arc::new(ParkingLotMutex::new(None));
1631 let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new()));
1632 let external_tools_shutdown = self.inner.rpc.connection_closed_token();
1633 let event_loop = spawn_event_loop(
1634 session_id.clone(),
1635 self.clone(),
1636 handlers,
1637 hooks,
1638 transforms,
1639 command_handlers,
1640 canvas_handler,
1641 session_fs_provider,
1642 bearer_token_providers,
1643 channels,
1644 idle_waiter.clone(),
1645 capabilities.clone(),
1646 open_canvases.clone(),
1647 event_tx.clone(),
1648 shutdown.clone(),
1649 external_tools_shutdown.clone(),
1650 );
1651 let mut registration = PendingSessionRegistration::new(
1652 self.clone(),
1653 session_id.clone(),
1654 registration_token,
1655 shutdown.clone(),
1656 external_tools_shutdown.clone(),
1657 );
1658 tracing::debug!(
1659 elapsed_ms = setup_start.elapsed().as_millis(),
1660 session_id = %session_id,
1661 tools_count,
1662 commands_count,
1663 has_hooks,
1664 "Client::resume_session local setup complete"
1665 );
1666
1667 let rpc_start = Instant::now();
1668 let result = match self.call("session.resume", Some(params)).await {
1669 Ok(result) => result,
1670 Err(error) => {
1671 registration.cleanup(event_loop).await;
1672 return Err(error);
1673 }
1674 };
1675 tracing::debug!(
1676 elapsed_ms = rpc_start.elapsed().as_millis(),
1677 session_id = %session_id,
1678 "Client::resume_session session resume request completed successfully"
1679 );
1680
1681 let resume_result: ResumeSessionResult = match serde_json::from_value(result) {
1682 Ok(result) => result,
1683 Err(error) => {
1684 registration.cleanup(event_loop).await;
1685 return Err(error.into());
1686 }
1687 };
1688 let cli_session_id = resume_result
1689 .session_id
1690 .clone()
1691 .unwrap_or_else(|| session_id.clone());
1692 if cli_session_id != session_id {
1693 registration.cleanup(event_loop).await;
1694 return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch {
1695 requested: session_id,
1696 returned: cli_session_id,
1697 })
1698 .into());
1699 }
1700 if has_mcp_auth_handler
1701 && let Err(error) = register_mcp_auth_interest(self, &session_id).await
1702 {
1703 registration.cleanup(event_loop).await;
1704 return Err(error);
1705 }
1706 let skills_reload_start = Instant::now();
1708 if let Err(e) = self
1709 .call(
1710 "session.skills.reload",
1711 Some(serde_json::json!({ "sessionId": session_id })),
1712 )
1713 .await
1714 {
1715 warn!(
1716 elapsed_ms = skills_reload_start.elapsed().as_millis(),
1717 session_id = %session_id,
1718 error = %e,
1719 "Client::resume_session skills reload request failed"
1720 );
1721 } else {
1722 tracing::debug!(
1723 elapsed_ms = skills_reload_start.elapsed().as_millis(),
1724 session_id = %session_id,
1725 "Client::resume_session skills reload request completed successfully"
1726 );
1727 }
1728
1729 *capabilities.write() = resume_result.capabilities.unwrap_or_default();
1730 {
1735 let mut snapshots = open_canvases.write();
1736 for snapshot in resume_result.open_canvases.unwrap_or_default() {
1737 upsert_open_canvas_snapshot(&mut snapshots, snapshot);
1738 }
1739 }
1740
1741 tracing::debug!(
1742 elapsed_ms = total_start.elapsed().as_millis(),
1743 session_id = %session_id,
1744 "Client::resume_session complete"
1745 );
1746 registration.disarm();
1747 let session = Session {
1748 id: session_id,
1749 cwd: self.cwd().clone(),
1750 workspace_path: resume_result.workspace_path,
1751 remote_url: resume_result.remote_url,
1752 client: self.clone(),
1753 event_loop: ParkingLotMutex::new(Some(event_loop)),
1754 shutdown,
1755 external_tools_shutdown,
1756 idle_waiter,
1757 capabilities,
1758 open_canvases,
1759 event_tx,
1760 github_token_registration: ParkingLotMutex::new(github_token_registration),
1761 registration_token,
1762 };
1763 apply_mode_post_create_patch(
1764 &session,
1765 mode,
1766 opt_skip_custom_instructions,
1767 opt_custom_agents_local_only,
1768 opt_coauthor_enabled,
1769 opt_manage_schedule_enabled,
1770 opt_included_builtin_skills,
1771 )
1772 .await?;
1773 if let Some(registration) = session.github_token_registration.lock().as_ref() {
1774 registration.claim(session.id.clone());
1775 } else {
1776 self.retire_github_token_provider(&session.id);
1777 }
1778 Ok(session)
1779 }
1780}
1781
1782#[must_use = "a PreparedSession does nothing until started"]
1840pub struct PreparedSession {
1841 client: Client,
1842 kind: PreparedKind,
1843 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
1844 shutdown: CancellationToken,
1845}
1846
1847enum PreparedKind {
1850 Create(Box<SessionConfig>),
1851 Resume(Box<ResumeSessionConfig>),
1852}
1853
1854impl PreparedSession {
1855 fn new(client: Client, kind: PreparedKind, event_buffer_capacity: usize) -> Self {
1856 let (event_tx, _) = tokio::sync::broadcast::channel(event_buffer_capacity);
1857 Self {
1858 client,
1859 kind,
1860 event_tx,
1861 shutdown: CancellationToken::new(),
1862 }
1863 }
1864
1865 pub fn subscribe(&self) -> crate::subscription::EventSubscription {
1883 crate::subscription::EventSubscription::new(self.event_tx.subscribe())
1884 }
1885
1886 pub async fn start(self) -> Result<Session, Error> {
1904 let Self {
1905 client,
1906 kind,
1907 event_tx,
1908 shutdown,
1909 } = self;
1910 match kind {
1911 PreparedKind::Create(config) => {
1912 client
1913 .start_prepared_create(*config, event_tx, shutdown)
1914 .await
1915 }
1916 PreparedKind::Resume(config) => {
1917 client
1918 .start_prepared_resume(*config, event_tx, shutdown)
1919 .await
1920 }
1921 }
1922 }
1923}
1924
1925type CommandHandlerMap = HashMap<String, Arc<dyn CommandHandler>>;
1926
1927async fn apply_mode_post_create_patch(
1928 session: &Session,
1929 mode: crate::ClientMode,
1930 opt_skip_custom_instructions: Option<bool>,
1931 opt_custom_agents_local_only: Option<bool>,
1932 opt_coauthor_enabled: Option<bool>,
1933 opt_manage_schedule_enabled: Option<bool>,
1934 opt_included_builtin_skills: Option<Vec<String>>,
1935) -> Result<(), Error> {
1936 let Some(patch) = build_mode_post_create_patch(
1937 mode,
1938 opt_skip_custom_instructions,
1939 opt_custom_agents_local_only,
1940 opt_coauthor_enabled,
1941 opt_manage_schedule_enabled,
1942 opt_included_builtin_skills,
1943 ) else {
1944 return Ok(());
1945 };
1946 if let Err(error) = session.rpc().options().update(patch).await {
1947 let _ = session.disconnect().await;
1948 return Err(error);
1949 }
1950 Ok(())
1951}
1952
1953fn build_mode_post_create_patch(
1963 mode: crate::ClientMode,
1964 opt_skip_custom_instructions: Option<bool>,
1965 opt_custom_agents_local_only: Option<bool>,
1966 opt_coauthor_enabled: Option<bool>,
1967 opt_manage_schedule_enabled: Option<bool>,
1968 opt_included_builtin_skills: Option<Vec<String>>,
1969) -> Option<crate::generated::api_types::SessionUpdateOptionsParams> {
1970 use crate::generated::api_types::SessionUpdateOptionsParams;
1971 let mut patch = SessionUpdateOptionsParams::default();
1972 let should_send = if mode == crate::ClientMode::Empty {
1973 patch.skip_custom_instructions = Some(opt_skip_custom_instructions.unwrap_or(true));
1974 patch.custom_agents_local_only = Some(opt_custom_agents_local_only.unwrap_or(true));
1975 patch.coauthor_enabled = Some(opt_coauthor_enabled.unwrap_or(false));
1976 patch.manage_schedule_enabled = Some(opt_manage_schedule_enabled.unwrap_or(false));
1977 patch.installed_plugins = Some(Vec::new());
1978 patch.included_builtin_skills = Some(opt_included_builtin_skills.unwrap_or_default());
1979 true
1980 } else {
1981 let mut any = false;
1982 if let Some(v) = opt_skip_custom_instructions {
1983 patch.skip_custom_instructions = Some(v);
1984 any = true;
1985 }
1986 if let Some(v) = opt_custom_agents_local_only {
1987 patch.custom_agents_local_only = Some(v);
1988 any = true;
1989 }
1990 if let Some(v) = opt_coauthor_enabled {
1991 patch.coauthor_enabled = Some(v);
1992 any = true;
1993 }
1994 if let Some(v) = opt_manage_schedule_enabled {
1995 patch.manage_schedule_enabled = Some(v);
1996 any = true;
1997 }
1998 if let Some(v) = opt_included_builtin_skills {
1999 patch.included_builtin_skills = Some(v);
2000 any = true;
2001 }
2002 any
2003 };
2004 if !should_send {
2005 return None;
2006 }
2007 Some(patch)
2008}
2009
2010fn build_command_handler_map(commands: Option<&[CommandDefinition]>) -> Arc<CommandHandlerMap> {
2011 let map = match commands {
2012 Some(commands) => commands
2013 .iter()
2014 .filter(|cmd| !cmd.name.is_empty())
2015 .map(|cmd| (cmd.name.clone(), cmd.handler.clone()))
2016 .collect(),
2017 None => HashMap::new(),
2018 };
2019 Arc::new(map)
2020}
2021
2022fn upsert_open_canvas_snapshot(
2023 snapshots: &mut Vec<OpenCanvasInstance>,
2024 snapshot: OpenCanvasInstance,
2025) {
2026 if let Some(existing) = snapshots
2027 .iter_mut()
2028 .find(|open| open.instance_id == snapshot.instance_id)
2029 {
2030 *existing = snapshot;
2031 } else {
2032 snapshots.push(snapshot);
2033 }
2034}
2035
2036fn remove_open_canvas_snapshot(snapshots: &mut Vec<OpenCanvasInstance>, instance_id: &str) {
2037 snapshots.retain(|open| open.instance_id != instance_id);
2038}
2039
2040#[allow(clippy::too_many_arguments)]
2041fn spawn_event_loop(
2042 session_id: SessionId,
2043 client: Client,
2044 handlers: SessionHandlers,
2045 hooks: Option<Arc<dyn SessionHooks>>,
2046 transforms: Option<Arc<dyn SystemMessageTransform>>,
2047 command_handlers: Arc<CommandHandlerMap>,
2048 canvas_handler: Option<Arc<dyn CanvasHandler>>,
2049 session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2050 bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2051 channels: crate::router::SessionChannels,
2052 idle_waiter: Arc<ParkingLotMutex<Option<IdleWaiter>>>,
2053 capabilities: Arc<parking_lot::RwLock<SessionCapabilities>>,
2054 open_canvases: Arc<parking_lot::RwLock<Vec<OpenCanvasInstance>>>,
2055 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
2056 shutdown: CancellationToken,
2057 external_tools_shutdown: CancellationToken,
2058) -> JoinHandle<()> {
2059 let crate::router::SessionChannels {
2060 mut notifications,
2061 mut requests,
2062 } = channels;
2063 let pending_external_tools: PendingExternalTools =
2064 Arc::new(ParkingLotMutex::new(HashMap::new()));
2065
2066 let span = tracing::error_span!("session_event_loop", session_id = %session_id);
2067 tokio::spawn(
2068 async move {
2069 loop {
2070 tokio::select! {
2094 _ = shutdown.cancelled() => break,
2095 Some(notification) = notifications.recv() => {
2096 handle_notification(
2097 &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx, &shutdown, &external_tools_shutdown, &pending_external_tools,
2098 ).await;
2099 }
2100 Some(request) = requests.recv() => {
2101 let span = tracing::error_span!("session_request_handler", session_id = %session_id);
2105 let session_id = session_id.clone();
2106 let client = client.clone();
2107 let handlers = handlers.clone();
2108 let hooks = hooks.clone();
2109 let transforms = transforms.clone();
2110 let canvas_handler = canvas_handler.clone();
2111 let session_fs_provider = session_fs_provider.clone();
2112 let bearer_token_providers = bearer_token_providers.clone();
2113 let request_id = request.id;
2114 let method = request.method.clone();
2115 tokio::spawn(
2116 async move {
2117 let ctx = RequestDispatchContext {
2118 client: &client,
2119 handlers: &handlers,
2120 hooks: hooks.as_deref(),
2121 transforms: transforms.as_deref(),
2122 canvas_handler: canvas_handler.as_ref(),
2123 session_fs_provider: session_fs_provider.as_ref(),
2124 bearer_token_providers: &bearer_token_providers,
2125 };
2126 let dispatch = handle_request(&session_id, ctx, request);
2127 if AssertUnwindSafe(dispatch).catch_unwind().await.is_err() {
2128 error!(method = %method, "request handler panicked");
2131 let _ = send_error_response(
2132 &client,
2133 request_id,
2134 error_codes::INTERNAL_ERROR,
2135 "request handler panicked",
2136 )
2137 .await;
2138 }
2139 }
2140 .instrument(span),
2141 );
2142 }
2143 else => break,
2144 }
2145 }
2146 if let Some(waiter) = idle_waiter.lock().take() {
2149 let _ = waiter
2150 .tx
2151 .send(Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()));
2152 }
2153 }
2154 .instrument(span),
2155 )
2156}
2157
2158fn extract_request_id(data: &Value) -> Option<RequestId> {
2159 data.get("requestId")
2160 .and_then(|v| v.as_str())
2161 .filter(|s| !s.is_empty())
2162 .map(RequestId::new)
2163}
2164
2165fn permission_request_data(
2166 event_data: &Value,
2167 managed_settings_enabled: bool,
2168) -> PermissionRequestData {
2169 let request_data = event_data
2170 .get("permissionRequest")
2171 .cloned()
2172 .unwrap_or_else(|| event_data.clone());
2173 let managed_approval_required = match request_data.get("managedApprovalRequired") {
2174 None => None,
2175 Some(Value::Bool(value)) => Some(*value),
2176 Some(_) => Some(true),
2177 };
2178 match serde_json::from_value::<PermissionRequestData>(request_data) {
2179 Ok(mut data) => {
2180 data.extra = event_data.clone();
2181 data.managed_settings_enabled = managed_settings_enabled;
2182 data
2183 }
2184 Err(_) => PermissionRequestData {
2185 kind: None,
2186 tool_call_id: None,
2187 managed_approval_required,
2188 managed_settings_enabled,
2189 extra: event_data.clone(),
2190 },
2191 }
2192}
2193
2194fn permission_response_params(
2202 session_id: &SessionId,
2203 request_id: &RequestId,
2204 result: &PermissionResult,
2205) -> Option<Value> {
2206 let (decision, decision_context) = match result {
2207 PermissionResult::Decision { decision, context } => (decision, context.clone()),
2208 PermissionResult::NoResult => return None,
2209 };
2210 let mut params = serde_json::to_value(PermissionDecisionRequest {
2211 decision_context,
2212 request_id: request_id.clone(),
2213 result: decision.clone(),
2214 })
2215 .expect("serializing permission response should succeed");
2216 params["sessionId"] =
2217 serde_json::to_value(session_id).expect("serializing session ID should succeed");
2218 Some(params)
2219}
2220
2221async fn register_mcp_auth_interest(client: &Client, session_id: &SessionId) -> Result<(), Error> {
2222 let mut params = serde_json::to_value(RegisterEventInterestParams {
2223 event_type: "mcp.oauth_required".to_string(),
2224 })?;
2225 params["sessionId"] = Value::String(session_id.to_string());
2226 client
2227 .call(rpc_methods::SESSION_EVENTLOG_REGISTERINTEREST, Some(params))
2228 .await?;
2229 Ok(())
2230}
2231
2232fn tool_failure_result(message: impl Into<String>) -> ToolResult {
2233 let message = message.into();
2234 ToolResult::Expanded(ToolResultExpanded {
2235 text_result_for_llm: message.clone(),
2236 result_type: "failure".to_string(),
2237 binary_results_for_llm: None,
2238 session_log: None,
2239 error: Some(message),
2240 tool_telemetry: None,
2241 tool_references: None,
2242 })
2243}
2244
2245fn is_autopilot_continuation_idle(event: &SessionEvent) -> bool {
2246 event
2247 .typed_data::<SessionIdleData>()
2248 .is_some_and(|data| data.mode == Some(SessionMode::Autopilot))
2249}
2250
2251#[allow(clippy::too_many_arguments)]
2253async fn handle_notification(
2254 session_id: &SessionId,
2255 client: &Client,
2256 handlers: &SessionHandlers,
2257 command_handlers: &Arc<CommandHandlerMap>,
2258 notification: SessionEventNotification,
2259 idle_waiter: &Arc<ParkingLotMutex<Option<IdleWaiter>>>,
2260 capabilities: &Arc<parking_lot::RwLock<SessionCapabilities>>,
2261 open_canvases: &Arc<parking_lot::RwLock<Vec<OpenCanvasInstance>>>,
2262 event_tx: &tokio::sync::broadcast::Sender<SessionEvent>,
2263 shutdown: &CancellationToken,
2264 external_tools_shutdown: &CancellationToken,
2265 pending_external_tools: &PendingExternalTools,
2266) {
2267 let dispatch_start = Instant::now();
2268 let event = notification.event.clone();
2269 let event_type = event.parsed_type();
2270 if event_type == SessionEventType::PermissionRequested {
2271 tracing::debug!(
2272 session_id = %session_id,
2273 event_type = %event.event_type,
2274 "Session::handle_notification permission request received"
2275 );
2276 }
2277
2278 match event_type {
2281 SessionEventType::AssistantMessage
2282 | SessionEventType::SessionIdle
2283 | SessionEventType::SessionError => {
2284 let mut guard = idle_waiter.lock();
2285 if let Some(waiter) = guard.as_mut() {
2286 match event_type {
2287 SessionEventType::AssistantMessage => {
2288 if !waiter.first_assistant_message_seen {
2289 waiter.first_assistant_message_seen = true;
2290 tracing::debug!(
2291 elapsed_ms = waiter.started_at.elapsed().as_millis(),
2292 session_id = %session_id,
2293 "Session::send_and_wait first assistant message"
2294 );
2295 }
2296 waiter.last_assistant_message = Some(event.clone());
2297 }
2298 SessionEventType::SessionIdle if is_autopilot_continuation_idle(&event) => {}
2299 SessionEventType::SessionIdle | SessionEventType::SessionError => {
2300 if let Some(waiter) = guard.take() {
2301 if event_type == SessionEventType::SessionIdle {
2302 tracing::debug!(
2303 elapsed_ms = waiter.started_at.elapsed().as_millis(),
2304 session_id = %session_id,
2305 "Session::send_and_wait idle received"
2306 );
2307 let _ = waiter.tx.send(Ok(waiter.last_assistant_message));
2308 } else {
2309 let error_msg = event
2310 .typed_data::<SessionErrorData>()
2311 .map(|d| d.message)
2312 .or_else(|| {
2313 event
2314 .data
2315 .get("message")
2316 .and_then(|v| v.as_str())
2317 .map(|s| s.to_string())
2318 })
2319 .unwrap_or_else(|| "session error".to_string());
2320 let _ = waiter.tx.send(Err(Error::with_message(
2321 ErrorKind::Session(SessionErrorKind::AgentError),
2322 error_msg,
2323 )));
2324 }
2325 }
2326 }
2327 _ => {}
2328 }
2329 }
2330 }
2331 _ => {}
2332 }
2333
2334 if event_type == SessionEventType::CapabilitiesChanged {
2338 match serde_json::from_value::<SessionCapabilities>(notification.event.data.clone()) {
2339 Ok(changed) => *capabilities.write() = changed,
2340 Err(e) => warn!(error = %e, "failed to deserialize capabilities.changed payload"),
2341 }
2342 }
2343 if event_type == SessionEventType::SessionCanvasOpened {
2344 match serde_json::from_value::<OpenCanvasInstance>(notification.event.data.clone()) {
2345 Ok(open_canvas) => {
2346 upsert_open_canvas_snapshot(&mut open_canvases.write(), open_canvas);
2347 }
2348 Err(e) => warn!(error = %e, "failed to deserialize session.canvas.opened payload"),
2349 }
2350 }
2351 if event_type == SessionEventType::SessionCanvasClosed {
2352 match serde_json::from_value::<SessionCanvasClosedData>(notification.event.data.clone()) {
2353 Ok(closed) => {
2354 if closed.instance_id.is_empty() {
2355 warn!("failed to deserialize session.canvas.closed payload");
2356 } else {
2357 remove_open_canvas_snapshot(&mut open_canvases.write(), &closed.instance_id);
2358 }
2359 }
2360 Err(e) => warn!(error = %e, "failed to deserialize session.canvas.closed payload"),
2361 }
2362 }
2363
2364 let _ = event_tx.send(event.clone());
2368
2369 tracing::debug!(
2370 elapsed_ms = dispatch_start.elapsed().as_millis(),
2371 session_id = %session_id,
2372 event_type = %notification.event.event_type,
2373 "Session::handle_notification dispatch"
2374 );
2375
2376 match event_type {
2379 SessionEventType::ExternalToolCompleted => {
2380 if let Some(request_id) = extract_request_id(¬ification.event.data)
2381 && let Some(token) = pending_external_tools.lock().remove(&request_id)
2382 {
2383 token.cancel();
2384 }
2385 }
2386 SessionEventType::PermissionRequested => {
2387 let Some(request_id) = extract_request_id(¬ification.event.data) else {
2388 return;
2389 };
2390 if notification
2394 .event
2395 .data
2396 .get("resolvedByHook")
2397 .and_then(|v| v.as_bool())
2398 .unwrap_or(false)
2399 {
2400 return;
2401 }
2402 let Some(permission_handler) = handlers.permission.clone() else {
2406 return;
2407 };
2408 let client = client.clone();
2409 let sid = session_id.clone();
2410 let shutdown = shutdown.clone();
2411 let data = permission_request_data(
2412 ¬ification.event.data,
2413 handlers.managed_settings_enabled,
2414 );
2415 let span = tracing::error_span!(
2416 "permission_request_handler",
2417 session_id = %sid,
2418 request_id = %request_id
2419 );
2420 tokio::spawn(
2421 async move {
2422 let handler_start = Instant::now();
2423 let result = permission_handler
2424 .handle(sid.clone(), request_id.clone(), data)
2425 .await;
2426 tracing::debug!(
2427 elapsed_ms = handler_start.elapsed().as_millis(),
2428 session_id = %sid,
2429 request_id = %request_id,
2430 "PermissionHandler::handle dispatch"
2431 );
2432 let Some(params) = permission_response_params(&sid, &request_id, &result)
2433 else {
2434 return;
2438 };
2439 let rpc_start = Instant::now();
2440 let method =
2441 rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST;
2442 tokio::select! {
2443 biased;
2444 response = client.call(method, Some(params)) => {
2445 match response {
2446 Ok(_) => tracing::debug!(
2447 elapsed_ms = rpc_start.elapsed().as_millis(),
2448 session_id = %sid,
2449 request_id = %request_id,
2450 method,
2451 "Session::handle_notification response sent successfully"
2452 ),
2453 Err(error) => warn!(
2454 error = %error,
2455 session_id = %sid,
2456 request_id = %request_id,
2457 method,
2458 "failed to deliver permission decision back to the runtime"
2459 ),
2460 }
2461 }
2462 _ = shutdown.cancelled() => {
2463 warn!(
2464 elapsed_ms = rpc_start.elapsed().as_millis(),
2465 session_id = %sid,
2466 request_id = %request_id,
2467 method,
2468 delivery_outcome = "unknown",
2469 "permission confirmation acknowledgement wait cancelled during session shutdown"
2470 );
2471 }
2472 }
2473 }
2474 .instrument(span),
2475 );
2476 }
2477 SessionEventType::ExternalToolRequested => {
2478 let Some(request_id) = extract_request_id(¬ification.event.data) else {
2479 return;
2480 };
2481 let data: ExternalToolRequestedData =
2482 match serde_json::from_value(notification.event.data.clone()) {
2483 Ok(d) => d,
2484 Err(e) => {
2485 warn!(error = %e, "failed to deserialize external_tool.requested");
2486 let client = client.clone();
2487 let sid = session_id.clone();
2488 let span = tracing::error_span!(
2489 "external_tool_deserialize_error",
2490 session_id = %sid,
2491 request_id = %request_id
2492 );
2493 tokio::spawn(
2494 async move {
2495 let rpc_start = Instant::now();
2496 let _ = client
2497 .call(
2498 "session.tools.handlePendingToolCall",
2499 Some(serde_json::json!({
2500 "sessionId": sid,
2501 "requestId": request_id,
2502 "error": format!("Failed to deserialize tool request: {e}"),
2503 })),
2504 )
2505 .await;
2506 tracing::debug!(
2507 elapsed_ms = rpc_start.elapsed().as_millis(),
2508 session_id = %sid,
2509 request_id = %request_id,
2510 "Session::handle_notification response sent successfully"
2511 );
2512 }
2513 .instrument(span),
2514 );
2515 return;
2516 }
2517 };
2518 let tool_handler = if data.tool_name.is_empty() {
2522 None
2523 } else {
2524 handlers.tools.get(&data.tool_name).cloned()
2525 };
2526 let Some(tool_handler) = tool_handler else {
2527 return;
2528 };
2529 let cancellation = Arc::new(external_tools_shutdown.child_token());
2530 {
2531 let mut pending = pending_external_tools.lock();
2532 if external_tools_shutdown.is_cancelled() || pending.contains_key(&request_id) {
2533 return;
2534 }
2535 pending.insert(request_id.clone(), cancellation.clone());
2536 }
2537 let client = client.clone();
2538 let sid = session_id.clone();
2539 let pending_external_tools = pending_external_tools.clone();
2540 let guard_request_id = request_id.clone();
2541 let guard_cancellation = cancellation.clone();
2542 let span = tracing::error_span!(
2543 "external_tool_handler",
2544 session_id = %sid,
2545 request_id = %request_id
2546 );
2547 tokio::spawn(
2548 async move {
2549 let guard = PendingExternalToolGuard {
2550 request_id: guard_request_id,
2551 token: guard_cancellation,
2552 pending: pending_external_tools,
2553 };
2554 if cancellation.is_cancelled() {
2555 return;
2556 }
2557 if data.tool_call_id.is_empty() {
2562 if !guard.claim() {
2563 return;
2564 }
2565 let error_msg = "Missing toolCallId";
2566 let rpc_start = Instant::now();
2567 let _ = client
2568 .call(
2569 "session.tools.handlePendingToolCall",
2570 Some(serde_json::json!({
2571 "sessionId": sid,
2572 "requestId": request_id,
2573 "error": error_msg,
2574 })),
2575 )
2576 .await;
2577 tracing::debug!(
2578 elapsed_ms = rpc_start.elapsed().as_millis(),
2579 session_id = %sid,
2580 request_id = %request_id,
2581 "Session::handle_notification response sent successfully"
2582 );
2583 return;
2584 }
2585 let tool_call_id = data.tool_call_id.clone();
2586 let tool_name = data.tool_name.clone();
2587 let available_tools = if tool_name == TOOL_SEARCH_TOOL_NAME {
2594 let metadata_result = tokio::select! {
2595 biased;
2596 _ = cancellation.cancelled() => return,
2597 result = client.call(
2598 rpc_methods::SESSION_TOOLS_GETCURRENTMETADATA,
2599 Some(serde_json::json!({ "sessionId": sid })),
2600 ) => result,
2601 };
2602 match metadata_result {
2603 Ok(value) => {
2604 serde_json::from_value::<ToolsGetCurrentMetadataResult>(value)
2605 .ok()
2606 .and_then(|result| result.tools)
2607 }
2608 Err(_) => None,
2609 }
2610 } else {
2611 None
2612 };
2613 let invocation = ToolInvocation {
2614 session_id: sid.clone(),
2615 tool_call_id: data.tool_call_id,
2616 tool_name: data.tool_name,
2617 arguments: data
2618 .arguments
2619 .unwrap_or(Value::Object(serde_json::Map::new())),
2620 available_tools,
2621 traceparent: data.traceparent,
2622 tracestate: data.tracestate,
2623 };
2624 let handler_start = Instant::now();
2625 let tool_result = tokio::select! {
2626 biased;
2627 _ = cancellation.cancelled() => return,
2628 result = tool_handler.call(invocation) => match result {
2629 Ok(r) => r,
2630 Err(e) => tool_failure_result(e.to_string()),
2631 },
2632 };
2633 tracing::debug!(
2634 elapsed_ms = handler_start.elapsed().as_millis(),
2635 session_id = %sid,
2636 request_id = %request_id,
2637 tool_call_id = %tool_call_id,
2638 tool_name = %tool_name,
2639 "ToolHandler::call dispatch"
2640 );
2641 if !guard.claim() {
2642 return;
2643 }
2644 let result_value = serde_json::to_value(tool_result).unwrap_or(Value::Null);
2645 let rpc_start = Instant::now();
2646 let _ = client
2647 .call(
2648 "session.tools.handlePendingToolCall",
2649 Some(serde_json::json!({
2650 "sessionId": sid,
2651 "requestId": request_id,
2652 "result": result_value,
2653 })),
2654 )
2655 .await;
2656 tracing::debug!(
2657 elapsed_ms = rpc_start.elapsed().as_millis(),
2658 session_id = %sid,
2659 request_id = %request_id,
2660 tool_call_id = %tool_call_id,
2661 tool_name = %tool_name,
2662 "Session::handle_notification response sent successfully"
2663 );
2664 }
2665 .instrument(span),
2666 );
2667 }
2668 SessionEventType::UserInputRequested => {
2669 }
2676 SessionEventType::ElicitationRequested => {
2677 let Some(request_id) = extract_request_id(¬ification.event.data) else {
2678 return;
2679 };
2680 let Some(elicitation_handler) = handlers.elicitation.clone() else {
2684 return;
2685 };
2686 let elicitation_data: ElicitationRequestedData =
2687 match serde_json::from_value(notification.event.data.clone()) {
2688 Ok(d) => d,
2689 Err(e) => {
2690 warn!(error = %e, "failed to deserialize elicitation request");
2691 return;
2692 }
2693 };
2694 let request = ElicitationRequest {
2695 message: elicitation_data.message,
2696 requested_schema: elicitation_data
2697 .requested_schema
2698 .map(|s| serde_json::to_value(s).unwrap_or(Value::Null)),
2699 mode: elicitation_data.mode.map(|m| match m {
2700 crate::generated::session_events::ElicitationRequestedMode::Form => {
2701 crate::types::ElicitationMode::Form
2702 }
2703 crate::generated::session_events::ElicitationRequestedMode::Url => {
2704 crate::types::ElicitationMode::Url
2705 }
2706 _ => crate::types::ElicitationMode::Unknown,
2707 }),
2708 elicitation_source: elicitation_data.elicitation_source,
2709 url: elicitation_data.url,
2710 };
2711 let client = client.clone();
2712 let sid = session_id.clone();
2713 let span = tracing::error_span!(
2714 "elicitation_request_handler",
2715 session_id = %sid,
2716 request_id = %request_id
2717 );
2718 tokio::spawn(
2719 async move {
2720 let cancel = ElicitationResult {
2721 action: "cancel".to_string(),
2722 content: None,
2723 };
2724 let handler_task = tokio::spawn({
2726 let sid = sid.clone();
2727 let request_id = request_id.clone();
2728 let span = tracing::error_span!(
2729 "elicitation_callback",
2730 session_id = %sid,
2731 request_id = %request_id
2732 );
2733 async move {
2734 let handler_start = Instant::now();
2735 let response = elicitation_handler
2736 .handle(sid.clone(), request_id.clone(), request)
2737 .await;
2738 tracing::debug!(
2739 elapsed_ms = handler_start.elapsed().as_millis(),
2740 session_id = %sid,
2741 request_id = %request_id,
2742 "ElicitationHandler::handle dispatch"
2743 );
2744 response
2745 }
2746 .instrument(span)
2747 });
2748 let result = match handler_task.await {
2749 Ok(r) => r,
2750 Err(_) => cancel.clone(),
2751 };
2752 let rpc_start = Instant::now();
2753 if let Err(e) = client
2754 .call(
2755 "session.ui.handlePendingElicitation",
2756 Some(serde_json::json!({
2757 "sessionId": sid,
2758 "requestId": request_id,
2759 "result": result,
2760 })),
2761 )
2762 .await
2763 {
2764 warn!(error = %e, "handlePendingElicitation failed, sending cancel");
2766 let _ = client
2767 .call(
2768 "session.ui.handlePendingElicitation",
2769 Some(serde_json::json!({
2770 "sessionId": sid,
2771 "requestId": request_id,
2772 "result": cancel,
2773 })),
2774 )
2775 .await;
2776 } else {
2777 tracing::debug!(
2778 elapsed_ms = rpc_start.elapsed().as_millis(),
2779 session_id = %sid,
2780 request_id = %request_id,
2781 "Session::handle_notification response sent successfully"
2782 );
2783 }
2784 }
2785 .instrument(span),
2786 );
2787 }
2788 SessionEventType::McpOauthRequired => {
2789 let Some(request_id) = extract_request_id(¬ification.event.data) else {
2790 return;
2791 };
2792 let Some(mcp_auth_handler) = handlers.mcp_auth.clone() else {
2793 warn!(
2794 session_id = %session_id,
2795 request_id = %request_id,
2796 "received MCP OAuth request without a registered MCP auth handler"
2797 );
2798 return;
2799 };
2800 let data: McpOauthRequiredData =
2801 match serde_json::from_value(notification.event.data.clone()) {
2802 Ok(d) => d,
2803 Err(e) => {
2804 warn!(error = %e, "failed to deserialize MCP OAuth request");
2805 return;
2806 }
2807 };
2808 let request = McpAuthRequest {
2809 request_id: request_id.clone(),
2810 server_name: data.server_name,
2811 server_url: data.server_url,
2812 reason: data.reason,
2813 www_authenticate_params: data.www_authenticate_params,
2814 resource_metadata: data.resource_metadata,
2815 static_client_config: data.static_client_config,
2816 };
2817 let client = client.clone();
2818 let sid = session_id.clone();
2819 let span = tracing::error_span!(
2820 "mcp_auth_request_handler",
2821 session_id = %sid,
2822 request_id = %request_id
2823 );
2824 tokio::spawn(
2825 async move {
2826 let cancel = McpAuthResult::Cancelled;
2827 let handler_task = tokio::spawn({
2828 let sid = sid.clone();
2829 let request_id = request_id.clone();
2830 let span = tracing::error_span!(
2831 "mcp_auth_callback",
2832 session_id = %sid,
2833 request_id = %request_id
2834 );
2835 async move {
2836 let handler_start = Instant::now();
2837 let response = mcp_auth_handler
2838 .handle(sid.clone(), request_id.clone(), request)
2839 .await;
2840 tracing::debug!(
2841 elapsed_ms = handler_start.elapsed().as_millis(),
2842 session_id = %sid,
2843 request_id = %request_id,
2844 "McpAuthHandler::handle dispatch"
2845 );
2846 response
2847 }
2848 .instrument(span)
2849 });
2850 let result = match handler_task.await {
2851 Ok(result) => result,
2852 Err(_) => cancel,
2853 };
2854 let rpc_start = Instant::now();
2855 let _ = client
2856 .call(
2857 "session.mcp.oauth.handlePendingRequest",
2858 Some(serde_json::json!({
2859 "sessionId": sid,
2860 "requestId": request_id,
2861 "result": result.into_wire(),
2862 })),
2863 )
2864 .await;
2865 tracing::debug!(
2866 elapsed_ms = rpc_start.elapsed().as_millis(),
2867 "Session::handle_notification MCP auth response sent"
2868 );
2869 }
2870 .instrument(span),
2871 );
2872 }
2873 SessionEventType::CommandExecute => {
2874 let data: CommandExecuteData =
2875 match serde_json::from_value(notification.event.data.clone()) {
2876 Ok(d) => d,
2877 Err(e) => {
2878 warn!(error = %e, "failed to deserialize command.execute");
2879 return;
2880 }
2881 };
2882 let client = client.clone();
2883 let command_handlers = command_handlers.clone();
2884 let sid = session_id.clone();
2885 let span = tracing::error_span!("command_handler", session_id = %sid);
2886 tokio::spawn(
2887 async move {
2888 let request_id = data.request_id;
2889 let ack_error = match command_handlers.get(&data.command_name).cloned() {
2890 None => Some(format!("Unknown command: {}", data.command_name)),
2891 Some(handler) => {
2892 let command_name = data.command_name.clone();
2893 let ctx = CommandContext {
2894 session_id: sid.clone(),
2895 command: data.command,
2896 command_name: data.command_name,
2897 args: data.args,
2898 };
2899 let handler_start = Instant::now();
2900 let result = handler.on_command(ctx).await;
2901 tracing::debug!(
2902 elapsed_ms = handler_start.elapsed().as_millis(),
2903 session_id = %sid,
2904 request_id = %request_id,
2905 command_name = %command_name,
2906 "CommandHandler::call dispatch"
2907 );
2908 match result {
2909 Ok(()) => None,
2910 Err(e) => Some(e.to_string()),
2911 }
2912 }
2913 };
2914 let mut params = serde_json::json!({
2915 "sessionId": sid,
2916 "requestId": request_id,
2917 });
2918 if let Some(error_msg) = ack_error {
2919 params["error"] = serde_json::Value::String(error_msg);
2920 }
2921 let rpc_start = Instant::now();
2922 let _ = client
2923 .call("session.commands.handlePendingCommand", Some(params))
2924 .await;
2925 tracing::debug!(
2926 elapsed_ms = rpc_start.elapsed().as_millis(),
2927 session_id = %sid,
2928 request_id = %request_id,
2929 "Session::handle_notification response sent successfully"
2930 );
2931 }
2932 .instrument(span),
2933 );
2934 }
2935 _ => {}
2936 }
2937}
2938
2939struct RequestDispatchContext<'a> {
2940 client: &'a Client,
2941 handlers: &'a SessionHandlers,
2942 hooks: Option<&'a dyn SessionHooks>,
2943 transforms: Option<&'a dyn SystemMessageTransform>,
2944 canvas_handler: Option<&'a Arc<dyn CanvasHandler>>,
2945 session_fs_provider: Option<&'a Arc<dyn SessionFsProvider>>,
2946 bearer_token_providers: &'a HashMap<String, Arc<dyn BearerTokenProvider>>,
2947}
2948
2949async fn handle_request(
2951 session_id: &SessionId,
2952 ctx: RequestDispatchContext<'_>,
2953 request: crate::JsonRpcRequest,
2954) {
2955 let sid = session_id.clone();
2956 let client = ctx.client;
2957 let handlers = ctx.handlers;
2958 let hooks = ctx.hooks;
2959 let transforms = ctx.transforms;
2960 let canvas_handler = ctx.canvas_handler;
2961 let session_fs_provider = ctx.session_fs_provider;
2962 let bearer_token_providers = ctx.bearer_token_providers;
2963
2964 if request.method.starts_with("sessionFs.") {
2965 crate::session_fs_dispatch::dispatch(client, session_fs_provider, request).await;
2966 return;
2967 }
2968
2969 if request.method.starts_with("canvas.") {
2970 crate::canvas_dispatch::dispatch(client, canvas_handler, request).await;
2971 return;
2972 }
2973
2974 if request.method == crate::generated::api_types::rpc_methods::PROVIDERTOKEN_GETTOKEN {
2975 crate::provider_token_dispatch::dispatch(client, bearer_token_providers, request).await;
2976 return;
2977 }
2978
2979 match request.method.as_str() {
2980 "hooks.invoke" => {
2981 let params = request.params.as_ref();
2982 let hook_type = params
2983 .and_then(|p| p.get("hookType"))
2984 .and_then(|v| v.as_str())
2985 .unwrap_or("");
2986 let input = params
2987 .and_then(|p| p.get("input"))
2988 .cloned()
2989 .unwrap_or(Value::Object(Default::default()));
2990
2991 let rpc_result = if let Some(hooks) = hooks {
2992 match crate::hooks::dispatch_hook(hooks, &sid, hook_type, input).await {
2993 Ok(output) => output,
2994 Err(e) => {
2995 warn!(error = %e, hook_type = hook_type, "hook dispatch failed");
2996 serde_json::json!({ "output": {} })
2997 }
2998 }
2999 } else {
3000 serde_json::json!({ "output": {} })
3001 };
3002
3003 let rpc_response = JsonRpcResponse {
3004 jsonrpc: "2.0".to_string(),
3005 id: request.id,
3006 result: Some(rpc_result),
3007 error: None,
3008 };
3009 let _ = client.send_response(&rpc_response).await;
3010 }
3011
3012 "userInput.request" => {
3013 let params = request.params.as_ref();
3014 let Some(question) = params
3015 .and_then(|p| p.get("question"))
3016 .and_then(|v| v.as_str())
3017 else {
3018 warn!("userInput.request missing 'question' field");
3019 let rpc_response = JsonRpcResponse {
3020 jsonrpc: "2.0".to_string(),
3021 id: request.id,
3022 result: None,
3023 error: Some(crate::JsonRpcError {
3024 code: error_codes::INVALID_PARAMS,
3025 message: "missing required field: question".to_string(),
3026 data: None,
3027 }),
3028 };
3029 let _ = client.send_response(&rpc_response).await;
3030 return;
3031 };
3032 let question = question.to_string();
3033 let choices = params
3034 .and_then(|p| p.get("choices"))
3035 .and_then(|v| v.as_array())
3036 .map(|arr| {
3037 arr.iter()
3038 .filter_map(|v| v.as_str().map(|s| s.to_string()))
3039 .collect()
3040 });
3041 let allow_freeform = params
3042 .and_then(|p| p.get("allowFreeform"))
3043 .and_then(|v| v.as_bool());
3044
3045 let handler_start = Instant::now();
3046 let response = if let Some(user_input_handler) = handlers.user_input.as_ref() {
3047 user_input_handler
3048 .handle(sid.clone(), question, choices, allow_freeform)
3049 .await
3050 } else {
3051 None
3052 };
3053 tracing::debug!(
3054 elapsed_ms = handler_start.elapsed().as_millis(),
3055 session_id = %sid,
3056 "UserInputHandler::handle dispatch"
3057 );
3058
3059 let rpc_result = match response {
3060 Some(UserInputResponse {
3061 answer,
3062 was_freeform,
3063 }) => serde_json::json!({
3064 "answer": answer,
3065 "wasFreeform": was_freeform,
3066 }),
3067 None => serde_json::json!({ "noResponse": true }),
3068 };
3069 let rpc_response = JsonRpcResponse {
3070 jsonrpc: "2.0".to_string(),
3071 id: request.id,
3072 result: Some(rpc_result),
3073 error: None,
3074 };
3075 let _ = client.send_response(&rpc_response).await;
3076 }
3077
3078 "exitPlanMode.request" => {
3079 let params = request
3080 .params
3081 .as_ref()
3082 .cloned()
3083 .unwrap_or(Value::Object(serde_json::Map::new()));
3084 let data: ExitPlanModeData = match serde_json::from_value(params) {
3085 Ok(d) => d,
3086 Err(e) => {
3087 warn!(error = %e, "failed to deserialize exitPlanMode.request params, using defaults");
3088 ExitPlanModeData::default()
3089 }
3090 };
3091
3092 let rpc_result = if let Some(exit_plan_handler) = handlers.exit_plan_mode.as_ref() {
3093 let result = exit_plan_handler.handle(sid, data).await;
3094 serde_json::to_value(result).expect("ExitPlanModeResult serialization cannot fail")
3095 } else {
3096 serde_json::json!({ "approved": true })
3097 };
3098 let rpc_response = JsonRpcResponse {
3099 jsonrpc: "2.0".to_string(),
3100 id: request.id,
3101 result: Some(rpc_result),
3102 error: None,
3103 };
3104 let _ = client.send_response(&rpc_response).await;
3105 }
3106
3107 "autoModeSwitch.request" => {
3108 let error_code = request
3109 .params
3110 .as_ref()
3111 .and_then(|p| p.get("errorCode"))
3112 .and_then(|v| v.as_str())
3113 .map(|s| s.to_string());
3114 let retry_after_seconds = request
3115 .params
3116 .as_ref()
3117 .and_then(|p| p.get("retryAfterSeconds"))
3118 .and_then(|v| v.as_f64());
3119
3120 let answer = if let Some(auto_mode_handler) = handlers.auto_mode_switch.as_ref() {
3121 auto_mode_handler
3122 .handle(sid, error_code, retry_after_seconds)
3123 .await
3124 } else {
3125 AutoModeSwitchResponse::No
3126 };
3127 let rpc_response = JsonRpcResponse {
3128 jsonrpc: "2.0".to_string(),
3129 id: request.id,
3130 result: Some(serde_json::json!({ "response": answer })),
3131 error: None,
3132 };
3133 let _ = client.send_response(&rpc_response).await;
3134 }
3135
3136 "systemMessage.transform" => {
3137 let params = request.params.as_ref();
3138 let sections: HashMap<String, crate::transforms::TransformSection> =
3139 match params.and_then(|p| p.get("sections")) {
3140 Some(v) => match serde_json::from_value(v.clone()) {
3141 Ok(s) => s,
3142 Err(e) => {
3143 let _ = send_error_response(
3144 client,
3145 request.id,
3146 error_codes::INVALID_PARAMS,
3147 &format!("invalid sections: {e}"),
3148 )
3149 .await;
3150 return;
3151 }
3152 },
3153 None => {
3154 let _ = send_error_response(
3155 client,
3156 request.id,
3157 error_codes::INVALID_PARAMS,
3158 "missing sections parameter",
3159 )
3160 .await;
3161 return;
3162 }
3163 };
3164
3165 let rpc_result = if let Some(transforms) = transforms {
3166 let transform_start = Instant::now();
3167 let response =
3168 crate::transforms::dispatch_transform(transforms, &sid, sections).await;
3169 tracing::debug!(
3170 elapsed_ms = transform_start.elapsed().as_millis(),
3171 session_id = %sid,
3172 "SystemMessageTransform::transform_section dispatch"
3173 );
3174 match serde_json::to_value(response) {
3175 Ok(v) => v,
3176 Err(e) => {
3177 warn!(error = %e, "failed to serialize transform response");
3178 serde_json::json!({ "sections": {} })
3179 }
3180 }
3181 } else {
3182 let passthrough: HashMap<String, crate::transforms::TransformSection> = sections;
3184 serde_json::json!({ "sections": passthrough })
3185 };
3186
3187 let rpc_response = JsonRpcResponse {
3188 jsonrpc: "2.0".to_string(),
3189 id: request.id,
3190 result: Some(rpc_result),
3191 error: None,
3192 };
3193 let _ = client.send_response(&rpc_response).await;
3194 }
3195
3196 method => {
3197 warn!(
3198 method = method,
3199 "unhandled request method in session event loop"
3200 );
3201 let _ = send_error_response(
3202 client,
3203 request.id,
3204 error_codes::METHOD_NOT_FOUND,
3205 &format!("unknown method: {method}"),
3206 )
3207 .await;
3208 }
3209 }
3210}
3211
3212async fn send_error_response(
3213 client: &Client,
3214 id: u64,
3215 code: i32,
3216 message: &str,
3217) -> Result<(), Error> {
3218 let response = JsonRpcResponse {
3219 jsonrpc: "2.0".to_string(),
3220 id,
3221 result: None,
3222 error: Some(crate::JsonRpcError {
3223 code,
3224 message: message.to_string(),
3225 data: None,
3226 }),
3227 };
3228 client.send_response(&response).await
3229}
3230
3231fn apply_transform_sections(
3235 sys_msg: &mut SystemMessageConfig,
3236 transforms: &dyn SystemMessageTransform,
3237) {
3238 sys_msg.mode = Some("customize".to_string());
3239 let sections = sys_msg.sections.get_or_insert_with(HashMap::new);
3240 for id in transforms.section_ids() {
3241 sections.entry(id).or_insert_with(|| SectionOverride {
3242 action: Some("transform".to_string()),
3243 content: None,
3244 });
3245 }
3246}
3247
3248fn inject_transform_sections(config: &mut SessionConfig, transforms: &dyn SystemMessageTransform) {
3249 let sys_msg = config.system_message.get_or_insert_with(Default::default);
3250 apply_transform_sections(sys_msg, transforms);
3251}
3252
3253fn inject_transform_sections_resume(
3254 config: &mut ResumeSessionConfig,
3255 transforms: &dyn SystemMessageTransform,
3256) {
3257 let sys_msg = config.system_message.get_or_insert_with(Default::default);
3258 apply_transform_sections(sys_msg, transforms);
3259}
3260
3261#[cfg(test)]
3262mod tests {
3263 use serde_json::json;
3264
3265 use super::{
3266 build_mode_post_create_patch, has_managed_settings, is_autopilot_continuation_idle,
3267 permission_request_data, permission_response_params,
3268 };
3269 use crate::handler::PermissionResult;
3270 use crate::types::{
3271 PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource,
3272 PermissionDecisionSurface, RequestId, SessionEvent, SessionId,
3273 };
3274
3275 #[test]
3276 fn identifies_only_autopilot_continuation_idles() {
3277 let mut event = SessionEvent {
3278 id: "event-1".to_string(),
3279 timestamp: "2026-01-01T00:00:00Z".to_string(),
3280 parent_id: None,
3281 ephemeral: None,
3282 agent_id: None,
3283 debug_cli_received_at_ms: None,
3284 debug_ws_forwarded_at_ms: None,
3285 event_type: "session.idle".to_string(),
3286 data: json!({ "mode": "autopilot" }),
3287 };
3288
3289 assert!(is_autopilot_continuation_idle(&event));
3290
3291 event.data = json!({ "mode": "interactive" });
3292 assert!(!is_autopilot_continuation_idle(&event));
3293
3294 event.data = json!({});
3295 assert!(!is_autopilot_continuation_idle(&event));
3296 }
3297
3298 #[test]
3299 fn empty_mode_post_patch_sets_empty_included_builtin_skills() {
3300 let patch =
3301 build_mode_post_create_patch(crate::ClientMode::Empty, None, None, None, None, None)
3302 .expect("empty mode always sends a patch");
3303 assert_eq!(
3304 patch.included_builtin_skills,
3305 Some(Vec::new()),
3306 "empty mode must fail closed with an empty includedBuiltinSkills list"
3307 );
3308 assert_eq!(patch.installed_plugins.as_ref().map(|p| p.len()), Some(0));
3309 let value = serde_json::to_value(&patch).expect("serialize patch");
3311 assert_eq!(value["includedBuiltinSkills"], serde_json::json!([]));
3312 }
3313
3314 #[test]
3315 fn empty_mode_post_patch_preserves_explicit_builtin_skill_allowlist() {
3316 let patch = build_mode_post_create_patch(
3317 crate::ClientMode::Empty,
3318 Some(false),
3319 Some(false),
3320 Some(true),
3321 Some(true),
3322 Some(vec!["code-review".to_string()]),
3323 )
3324 .expect("empty mode always sends a patch");
3325 assert_eq!(
3326 patch.included_builtin_skills,
3327 Some(vec!["code-review".to_string()])
3328 );
3329 }
3330
3331 #[test]
3332 fn copilot_cli_mode_does_not_inject_included_builtin_skills() {
3333 assert!(
3335 build_mode_post_create_patch(
3336 crate::ClientMode::CopilotCli,
3337 None,
3338 None,
3339 None,
3340 None,
3341 None
3342 )
3343 .is_none()
3344 );
3345 let patch = build_mode_post_create_patch(
3347 crate::ClientMode::CopilotCli,
3348 Some(true),
3349 None,
3350 None,
3351 None,
3352 None,
3353 )
3354 .expect("a set field triggers a patch");
3355 assert_eq!(patch.included_builtin_skills, None);
3356 assert!(patch.installed_plugins.is_none());
3357 let value = serde_json::to_value(&patch).expect("serialize patch");
3358 assert!(value.get("includedBuiltinSkills").is_none());
3359
3360 let patch = build_mode_post_create_patch(
3361 crate::ClientMode::CopilotCli,
3362 None,
3363 None,
3364 None,
3365 None,
3366 Some(vec!["code-review".to_string()]),
3367 )
3368 .expect("an explicit allowlist triggers a patch");
3369 assert_eq!(
3370 patch.included_builtin_skills,
3371 Some(vec!["code-review".to_string()])
3372 );
3373 }
3374
3375 #[test]
3376 fn direct_injection_enables_managed_safeguards() {
3377 let settings = crate::types::ManagedSettings::default();
3378 assert!(has_managed_settings(None, Some(&settings)));
3379 assert!(!has_managed_settings(None, None));
3380 }
3381
3382 fn attribution_context() -> PermissionDecisionContext {
3383 PermissionDecisionContext {
3384 outcome: PermissionDecisionOutcome::AutoApproved,
3385 response_capability: None,
3386 source: PermissionDecisionSource::AssistedApproval,
3387 surface: PermissionDecisionSurface::CopilotApp,
3388 }
3389 }
3390
3391 #[test]
3392 fn response_params_omit_decision_context_without_attribution() {
3393 for (result, expected) in [
3394 (
3395 PermissionResult::approve_once(),
3396 json!({ "kind": "approve-once" }),
3397 ),
3398 (PermissionResult::reject(None), json!({ "kind": "reject" })),
3399 (
3400 PermissionResult::reject(Some("bad".to_string())),
3401 json!({ "kind": "reject", "feedback": "bad" }),
3402 ),
3403 (
3404 PermissionResult::user_not_available(),
3405 json!({ "kind": "user-not-available" }),
3406 ),
3407 ] {
3408 let params = permission_response_params(
3409 &SessionId::from("session-1"),
3410 &RequestId::from("permission-1"),
3411 &result,
3412 )
3413 .unwrap();
3414 assert_eq!(
3415 params,
3416 json!({
3417 "sessionId": "session-1",
3418 "requestId": "permission-1",
3419 "result": expected,
3420 })
3421 );
3422 }
3423 }
3424
3425 #[test]
3426 fn response_params_forward_decision_context_alongside_result() {
3427 let params = permission_response_params(
3428 &SessionId::from("session-1"),
3429 &RequestId::from("permission-1"),
3430 &PermissionResult::approve_once().with_context(attribution_context()),
3431 )
3432 .unwrap();
3433 assert_eq!(
3434 params,
3435 json!({
3436 "sessionId": "session-1",
3437 "requestId": "permission-1",
3438 "result": { "kind": "approve-once" },
3439 "decisionContext": {
3440 "outcome": "auto_approved",
3441 "source": "assisted_approval",
3442 "surface": "copilot_app",
3443 },
3444 })
3445 );
3446 assert!(params["result"].get("decisionContext").is_none());
3448 }
3449
3450 #[test]
3451 fn response_params_suppressed_for_no_result() {
3452 assert!(
3453 permission_response_params(
3454 &SessionId::from("session-1"),
3455 &RequestId::from("permission-1"),
3456 &PermissionResult::NoResult,
3457 )
3458 .is_none()
3459 );
3460 }
3461
3462 #[test]
3463 fn with_context_is_a_no_op_on_no_result() {
3464 let result = PermissionResult::no_result().with_context(attribution_context());
3465 assert!(matches!(result, PermissionResult::NoResult));
3466 }
3467
3468 #[test]
3469 fn with_context_replaces_rather_than_nests() {
3470 let result = PermissionResult::approve_once()
3471 .with_context(attribution_context())
3472 .with_context(PermissionDecisionContext {
3473 outcome: PermissionDecisionOutcome::PromptedUser,
3474 response_capability: None,
3475 source: PermissionDecisionSource::HumanResponse,
3476 surface: PermissionDecisionSurface::Sdk,
3477 });
3478 let params = permission_response_params(
3479 &SessionId::from("session-1"),
3480 &RequestId::from("permission-1"),
3481 &result,
3482 )
3483 .unwrap();
3484 assert_eq!(
3485 params["decisionContext"],
3486 json!({
3487 "outcome": "prompted_user",
3488 "source": "human_response",
3489 "surface": "sdk",
3490 })
3491 );
3492 }
3493
3494 #[test]
3495 fn permission_request_data_reads_nested_managed_approval_metadata() {
3496 let data = permission_request_data(
3497 &json!({
3498 "requestId": "permission-1",
3499 "permissionRequest": {
3500 "kind": "read",
3501 "managedApprovalRequired": true,
3502 "path": "/workspace/file.txt"
3503 }
3504 }),
3505 false,
3506 );
3507
3508 assert_eq!(data.managed_approval_required, Some(true));
3509 assert_eq!(
3510 data.extra["permissionRequest"]["path"],
3511 "/workspace/file.txt"
3512 );
3513 }
3514
3515 #[test]
3516 fn permission_request_data_preserves_managed_flag_when_other_fields_are_malformed() {
3517 let data = permission_request_data(
3518 &json!({
3519 "requestId": "permission-1",
3520 "permissionRequest": {
3521 "kind": "read",
3522 "managedApprovalRequired": true,
3523 "toolCallId": 42
3524 }
3525 }),
3526 false,
3527 );
3528
3529 assert_eq!(data.managed_approval_required, Some(true));
3530 assert_eq!(data.extra["requestId"], "permission-1");
3531 }
3532
3533 #[test]
3534 fn permission_request_data_fails_closed_for_malformed_managed_flag() {
3535 let data = permission_request_data(
3536 &json!({
3537 "requestId": "permission-1",
3538 "permissionRequest": {
3539 "kind": "read",
3540 "managedApprovalRequired": "yes",
3541 "path": "/workspace/file.txt"
3542 }
3543 }),
3544 false,
3545 );
3546
3547 assert_eq!(data.managed_approval_required, Some(true));
3548 }
3549
3550 #[test]
3551 fn permission_request_data_preserves_valid_false_managed_flag() {
3552 let data = permission_request_data(
3553 &json!({
3554 "requestId": "permission-1",
3555 "permissionRequest": {
3556 "kind": "read",
3557 "managedApprovalRequired": false,
3558 "path": "/workspace/file.txt"
3559 }
3560 }),
3561 false,
3562 );
3563
3564 assert_eq!(data.managed_approval_required, Some(false));
3565 }
3566}