1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use parking_lot::Mutex as ParkingLotMutex;
7use serde_json::Value;
8use tokio::sync::oneshot;
9use tokio::task::JoinHandle;
10use tokio_util::sync::CancellationToken;
11use tracing::{Instrument, warn};
12
13use crate::canvas::CanvasHandler;
14use crate::generated::api_types::{
15 LogRequest, ModelSwitchToRequest, OpenCanvasInstance, RegisterEventInterestParams,
16 ToolsGetCurrentMetadataResult, rpc_methods,
17};
18use crate::generated::session_events::{
19 CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData,
20 SessionCanvasClosedData, SessionErrorData, SessionEventType,
21};
22use crate::handler::{
23 AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler,
24 McpAuthHandler, McpAuthRequest, McpAuthResult, PermissionHandler, PermissionResult,
25 UserInputHandler, UserInputResponse,
26};
27use crate::hooks::SessionHooks;
28use crate::provider_token::BearerTokenProvider;
29use crate::session_fs::SessionFsProvider;
30use crate::trace_context::inject_trace_context;
31use crate::transforms::SystemMessageTransform;
32use crate::types::{
33 CommandContext, CommandDefinition, CommandHandler, CreateSessionResult, ElicitationRequest,
34 ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions,
35 PermissionRequestData, RequestId, ResumeSessionConfig, ResumeSessionResult, SectionOverride,
36 SessionCapabilities, SessionConfig, SessionEvent, SessionId, SetModelOptions,
37 SystemMessageConfig, ToolInvocation, ToolResult, ToolResultExpanded, TraceContext,
38 UiInputOptions, ensure_attachment_display_names,
39};
40use crate::{
41 Client, Error, ErrorKind, JsonRpcResponse, SessionErrorKind, SessionEventNotification,
42 error_codes,
43};
44
45const TOOL_SEARCH_TOOL_NAME: &str = "tool_search_tool";
49
50#[derive(Clone)]
58pub(crate) struct SessionHandlers {
59 pub permission: Option<Arc<dyn PermissionHandler>>,
60 pub elicitation: Option<Arc<dyn ElicitationHandler>>,
61 pub mcp_auth: Option<Arc<dyn McpAuthHandler>>,
62 pub user_input: Option<Arc<dyn UserInputHandler>>,
63 pub exit_plan_mode: Option<Arc<dyn ExitPlanModeHandler>>,
64 pub auto_mode_switch: Option<Arc<dyn AutoModeSwitchHandler>>,
65 pub tools: Arc<HashMap<String, Arc<dyn crate::tool::ToolHandler>>>,
66}
67
68struct IdleWaiter {
70 tx: oneshot::Sender<Result<Option<SessionEvent>, Error>>,
71 last_assistant_message: Option<SessionEvent>,
72 started_at: Instant,
73 first_assistant_message_seen: bool,
74}
75
76struct WaiterGuard {
88 slot: Arc<ParkingLotMutex<Option<IdleWaiter>>>,
89}
90
91impl Drop for WaiterGuard {
92 fn drop(&mut self) {
93 self.slot.lock().take();
94 }
95}
96
97struct PendingSessionRegistration {
98 client: Client,
99 session_id: SessionId,
100 shutdown: CancellationToken,
101 disarmed: bool,
102}
103
104impl PendingSessionRegistration {
105 fn new(client: Client, session_id: SessionId, shutdown: CancellationToken) -> Self {
106 Self {
107 client,
108 session_id,
109 shutdown,
110 disarmed: false,
111 }
112 }
113
114 async fn cleanup(mut self, event_loop: JoinHandle<()>) {
115 self.shutdown.cancel();
116 let _ = event_loop.await;
117 self.client.unregister_session(&self.session_id);
118 self.disarmed = true;
119 }
120
121 fn disarm(&mut self) {
122 self.disarmed = true;
123 }
124}
125
126impl Drop for PendingSessionRegistration {
127 fn drop(&mut self) {
128 if !self.disarmed {
129 self.shutdown.cancel();
130 self.client.unregister_session(&self.session_id);
131 }
132 }
133}
134
135pub struct Session {
148 id: SessionId,
149 cwd: PathBuf,
150 workspace_path: Option<PathBuf>,
151 remote_url: Option<String>,
152 client: Client,
153 event_loop: ParkingLotMutex<Option<JoinHandle<()>>>,
158 shutdown: CancellationToken,
172 idle_waiter: Arc<ParkingLotMutex<Option<IdleWaiter>>>,
179 capabilities: Arc<parking_lot::RwLock<SessionCapabilities>>,
181 open_canvases: Arc<parking_lot::RwLock<Vec<OpenCanvasInstance>>>,
183 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
185}
186
187impl Session {
188 pub fn id(&self) -> &SessionId {
190 &self.id
191 }
192
193 pub fn cwd(&self) -> &PathBuf {
195 &self.cwd
196 }
197
198 pub fn workspace_path(&self) -> Option<&Path> {
200 self.workspace_path.as_deref()
201 }
202
203 pub fn remote_url(&self) -> Option<&str> {
205 self.remote_url.as_deref()
206 }
207
208 pub fn capabilities(&self) -> SessionCapabilities {
213 self.capabilities.read().clone()
214 }
215
216 pub fn open_canvases(&self) -> Vec<OpenCanvasInstance> {
219 self.open_canvases.read().clone()
220 }
221
222 pub fn cancellation_token(&self) -> CancellationToken {
249 self.shutdown.child_token()
250 }
251
252 pub fn subscribe(&self) -> crate::subscription::EventSubscription {
291 crate::subscription::EventSubscription::new(self.event_tx.subscribe())
292 }
293
294 pub fn client(&self) -> &Client {
296 &self.client
297 }
298
299 pub fn rpc(&self) -> crate::generated::rpc::SessionRpc<'_> {
310 crate::generated::rpc::SessionRpc { session: self }
311 }
312
313 pub async fn stop_event_loop(&self) {
321 self.shutdown.cancel();
322 let handle = self.event_loop.lock().take();
323 if let Some(handle) = handle {
324 let _ = handle.await;
325 }
326 if let Some(waiter) = self.idle_waiter.lock().take() {
328 let _ = waiter.tx.send(Err(
329 ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()
330 ));
331 }
332 }
333
334 pub async fn send(&self, opts: impl Into<MessageOptions>) -> Result<String, Error> {
359 if self.idle_waiter.lock().is_some() {
360 return Err(ErrorKind::Session(SessionErrorKind::SendWhileWaiting).into());
361 }
362 self.send_inner(opts.into()).await
363 }
364
365 async fn send_inner(&self, opts: MessageOptions) -> Result<String, Error> {
366 let mut params = serde_json::json!({
367 "sessionId": self.id,
368 "prompt": opts.prompt,
369 });
370 if let Some(m) = opts.mode {
371 params["mode"] = serde_json::to_value(m)?;
372 }
373 if let Some(am) = opts.agent_mode {
374 params["agentMode"] = serde_json::to_value(am)?;
375 }
376 if let Some(mut a) = opts.attachments {
377 ensure_attachment_display_names(&mut a);
378 params["attachments"] = serde_json::to_value(a)?;
379 }
380 if let Some(headers) = opts.request_headers
381 && !headers.is_empty()
382 {
383 params["requestHeaders"] = serde_json::to_value(headers)?;
384 }
385 if let Some(display_prompt) = opts.display_prompt {
386 params["displayPrompt"] = serde_json::to_value(display_prompt)?;
387 }
388 let trace_ctx = if opts.traceparent.is_some() || opts.tracestate.is_some() {
389 TraceContext {
390 traceparent: opts.traceparent,
391 tracestate: opts.tracestate,
392 }
393 } else {
394 self.client.resolve_trace_context().await
395 };
396 inject_trace_context(&mut params, &trace_ctx);
397 let rpc_start = Instant::now();
398 let result = self.client.call("session.send", Some(params)).await?;
399 let message_id = result
400 .get("messageId")
401 .and_then(|v| v.as_str())
402 .map(|s| s.to_string())
403 .unwrap_or_default();
404 tracing::debug!(
405 elapsed_ms = rpc_start.elapsed().as_millis(),
406 session_id = %self.id,
407 message_id = %message_id,
408 "Session::send completed successfully"
409 );
410 Ok(message_id)
411 }
412
413 pub async fn send_and_wait(
433 &self,
434 opts: impl Into<MessageOptions>,
435 ) -> Result<Option<SessionEvent>, Error> {
436 let total_start = Instant::now();
437 let opts = opts.into();
438 let timeout_duration = opts.wait_timeout.unwrap_or(Duration::from_secs(60));
439 let (tx, rx) = oneshot::channel();
440
441 {
442 let mut guard = self.idle_waiter.lock();
443 if guard.is_some() {
444 return Err(ErrorKind::Session(SessionErrorKind::SendWhileWaiting).into());
445 }
446 *guard = Some(IdleWaiter {
447 tx,
448 last_assistant_message: None,
449 started_at: total_start,
450 first_assistant_message_seen: false,
451 });
452 }
453
454 let _waiter_guard = WaiterGuard {
459 slot: self.idle_waiter.clone(),
460 };
461
462 let result = tokio::time::timeout(timeout_duration, async {
463 self.send_inner(opts).await?;
464 match rx.await {
465 Ok(result) => result,
466 Err(_) => Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()),
467 }
468 })
469 .await;
470
471 match result {
472 Ok(inner) => {
473 tracing::debug!(
474 elapsed_ms = total_start.elapsed().as_millis(),
475 session_id = %self.id,
476 completed_by = if inner.is_ok() { "idle" } else { "error" },
477 "Session::send_and_wait complete"
478 );
479 inner
480 }
481 Err(_) => {
482 tracing::warn!(
483 elapsed_ms = total_start.elapsed().as_millis(),
484 session_id = %self.id,
485 completed_by = "timeout",
486 "Session::send_and_wait failed"
487 );
488 Err(ErrorKind::Session(SessionErrorKind::Timeout(timeout_duration)).into())
489 }
490 }
491 }
492
493 pub async fn get_events(&self) -> Result<Vec<SessionEvent>, Error> {
495 let result = self
496 .client
497 .call(
498 "session.getMessages",
499 Some(serde_json::json!({ "sessionId": self.id })),
500 )
501 .await?;
502 let response: GetMessagesResponse = serde_json::from_value(result)?;
503 Ok(response.events)
504 }
505
506 #[deprecated(since = "0.1.0", note = "Use `get_events()` instead")]
508 pub async fn get_messages(&self) -> Result<Vec<SessionEvent>, Error> {
509 self.get_events().await
510 }
511
512 pub async fn abort(&self) -> Result<(), Error> {
520 self.client
521 .call(
522 "session.abort",
523 Some(serde_json::json!({ "sessionId": self.id })),
524 )
525 .await?;
526 Ok(())
527 }
528
529 pub async fn set_model(&self, model: &str, opts: Option<SetModelOptions>) -> Result<(), Error> {
533 let opts = opts.unwrap_or_default();
534 let request = ModelSwitchToRequest {
535 model_id: model.to_string(),
536 reasoning_effort: opts.reasoning_effort,
537 reasoning_summary: opts.reasoning_summary,
538 verbosity: None,
539 context_tier: opts.context_tier,
540 model_capabilities: opts.model_capabilities,
541 };
542 self.rpc().model().switch_to(request).await?;
543 Ok(())
544 }
545
546 pub async fn disconnect(&self) -> Result<(), Error> {
563 self.client
564 .call(
565 "session.destroy",
566 Some(serde_json::json!({ "sessionId": self.id })),
567 )
568 .await?;
569 self.stop_event_loop().await;
570 self.client.unregister_session(&self.id);
571 Ok(())
572 }
573
574 #[deprecated(since = "0.1.0", note = "Use `disconnect()` instead")]
579 pub async fn destroy(&self) -> Result<(), Error> {
580 self.disconnect().await
581 }
582
583 pub async fn log(
587 &self,
588 message: &str,
589 opts: Option<crate::types::LogOptions>,
590 ) -> Result<(), Error> {
591 let opts = opts.unwrap_or_default();
592 let level = match opts.level {
593 Some(level) => Some(serde_json::from_value(serde_json::to_value(level)?)?),
594 None => None,
595 };
596 let request = LogRequest {
597 message: message.to_string(),
598 level,
599 ephemeral: opts.ephemeral,
600 r#type: None,
601 tip: None,
602 url: None,
603 };
604 self.rpc().log(request).await?;
605 Ok(())
606 }
607
608 pub fn ui(&self) -> SessionUi<'_> {
614 SessionUi { session: self }
615 }
616
617 fn assert_elicitation(&self) -> Result<(), Error> {
619 if self
620 .capabilities
621 .read()
622 .ui
623 .as_ref()
624 .and_then(|u| u.elicitation)
625 != Some(true)
626 {
627 return Err(ErrorKind::Session(SessionErrorKind::ElicitationNotSupported).into());
628 }
629 Ok(())
630 }
631}
632
633impl Drop for Session {
634 fn drop(&mut self) {
635 self.shutdown.cancel();
647 self.client.unregister_session(&self.id);
648 }
649}
650
651pub struct SessionUi<'a> {
658 session: &'a Session,
659}
660
661impl<'a> SessionUi<'a> {
662 pub async fn elicitation(
670 &self,
671 message: &str,
672 schema: Value,
673 ) -> Result<ElicitationResult, Error> {
674 self.session.assert_elicitation()?;
675 let result = self
676 .session
677 .client
678 .call(
679 "session.ui.elicitation",
680 Some(serde_json::json!({
681 "sessionId": self.session.id,
682 "message": message,
683 "requestedSchema": schema,
684 })),
685 )
686 .await?;
687 let elicitation: ElicitationResult = serde_json::from_value(result)?;
688 Ok(elicitation)
689 }
690
691 pub async fn confirm(&self, message: &str) -> Result<bool, Error> {
695 self.session.assert_elicitation()?;
696 let schema = serde_json::json!({
697 "type": "object",
698 "properties": {
699 "confirmed": {
700 "type": "boolean",
701 "default": true,
702 }
703 },
704 "required": ["confirmed"]
705 });
706 let result = self.elicitation(message, schema).await?;
707 Ok(result.action == "accept"
708 && result
709 .content
710 .and_then(|c| c.get("confirmed").and_then(|v| v.as_bool()))
711 == Some(true))
712 }
713
714 pub async fn select(&self, message: &str, options: &[&str]) -> Result<Option<String>, Error> {
718 self.session.assert_elicitation()?;
719 let schema = serde_json::json!({
720 "type": "object",
721 "properties": {
722 "selection": {
723 "type": "string",
724 "enum": options,
725 }
726 },
727 "required": ["selection"]
728 });
729 let result = self.elicitation(message, schema).await?;
730 if result.action != "accept" {
731 return Ok(None);
732 }
733 let selection = result.content.and_then(|c| {
734 c.get("selection")
735 .and_then(|v| v.as_str())
736 .map(String::from)
737 });
738 Ok(selection)
739 }
740
741 pub async fn input(
746 &self,
747 message: &str,
748 options: Option<&UiInputOptions<'_>>,
749 ) -> Result<Option<String>, Error> {
750 self.session.assert_elicitation()?;
751 let mut field = serde_json::json!({ "type": "string" });
752 if let Some(opts) = options {
753 if let Some(title) = opts.title {
754 field["title"] = Value::String(title.to_string());
755 }
756 if let Some(desc) = opts.description {
757 field["description"] = Value::String(desc.to_string());
758 }
759 if let Some(min) = opts.min_length {
760 field["minLength"] = Value::Number(min.into());
761 }
762 if let Some(max) = opts.max_length {
763 field["maxLength"] = Value::Number(max.into());
764 }
765 if let Some(fmt) = &opts.format {
766 field["format"] = Value::String(fmt.as_str().to_string());
767 }
768 if let Some(default) = opts.default {
769 field["default"] = Value::String(default.to_string());
770 }
771 }
772 let schema = serde_json::json!({
773 "type": "object",
774 "properties": { "value": field },
775 "required": ["value"]
776 });
777 let result = self.elicitation(message, schema).await?;
778 if result.action != "accept" {
779 return Ok(None);
780 }
781 let value = result
782 .content
783 .and_then(|c| c.get("value").and_then(|v| v.as_str()).map(String::from));
784 Ok(value)
785 }
786}
787
788impl Client {
789 pub async fn create_session(&self, mut config: SessionConfig) -> Result<Session, Error> {
811 let total_start = Instant::now();
812 let caller_session_id = config.session_id.clone();
820 let use_server_generated_id = config.cloud.is_some() && caller_session_id.is_none();
821 let local_session_id: Option<SessionId> = if use_server_generated_id {
822 None
823 } else {
824 Some(
825 caller_session_id
826 .clone()
827 .unwrap_or_else(|| SessionId::new(uuid::Uuid::new_v4().to_string())),
828 )
829 };
830 if config.hooks_handler.is_some() && config.hooks.is_none() {
831 config.hooks = Some(true);
832 }
833 if let Some(transforms) = config.system_message_transform.clone() {
834 inject_transform_sections(&mut config, transforms.as_ref());
835 }
836 let mode = self.inner.mode;
837 if mode == crate::ClientMode::Empty && config.available_tools.is_none() {
838 return Err(Error::with_message(
839 ErrorKind::InvalidConfig,
840 "ClientMode::Empty requires available_tools to be set on the session config. \
841 Use ToolSet to specify which tools the session may use (e.g. \
842 ToolSet::new().add_builtin_many(BUILTIN_TOOLS_ISOLATED)).",
843 ));
844 }
845 crate::mode::validate_tool_filter_list(
846 "available_tools",
847 config.available_tools.as_deref(),
848 )?;
849 crate::mode::validate_tool_filter_list("excluded_tools", config.excluded_tools.as_deref())?;
850 config.system_message =
851 crate::mode::system_message_for_mode(mode, config.system_message.take());
852 config.memory = crate::mode::memory_for_mode(mode, config.memory.take());
853 if mode == crate::ClientMode::Empty {
854 if config.enable_session_telemetry.is_none() {
855 config.enable_session_telemetry = Some(false);
856 }
857 if config.skip_embedding_retrieval.is_none() {
858 config.skip_embedding_retrieval = Some(true);
859 }
860 if config.enable_on_demand_instruction_discovery.is_none() {
861 config.enable_on_demand_instruction_discovery = Some(false);
862 }
863 if config.enable_file_hooks.is_none() {
864 config.enable_file_hooks = Some(false);
865 }
866 if config.enable_host_git_operations.is_none() {
867 config.enable_host_git_operations = Some(false);
868 }
869 if config.enable_session_store.is_none() {
870 config.enable_session_store = Some(false);
871 }
872 if config.enable_skills.is_none() {
873 config.enable_skills = Some(false);
874 }
875 }
876 if mode == crate::ClientMode::Empty && config.mcp_oauth_token_storage.is_none() {
877 config.mcp_oauth_token_storage = Some("in-memory".into());
878 }
879 if mode == crate::ClientMode::Empty && config.embedding_cache_storage.is_none() {
880 config.embedding_cache_storage = Some("in-memory".into());
881 }
882 let opt_skip_custom_instructions = config.skip_custom_instructions;
883 let opt_custom_agents_local_only = config.custom_agents_local_only;
884 let opt_coauthor_enabled = config.coauthor_enabled;
885 let opt_manage_schedule_enabled = config.manage_schedule_enabled;
886 let (mut wire, mut runtime) = config.into_wire(local_session_id.clone())?;
887 wire.enable_github_telemetry_forwarding =
888 self.inner.on_github_telemetry.is_some().then_some(true);
889
890 let permission_handler = crate::permission::resolve_handler(
891 runtime.permission_handler.take(),
892 runtime.permission_policy.take(),
893 );
894 let handlers = SessionHandlers {
895 permission: permission_handler,
896 elicitation: runtime.elicitation_handler.take(),
897 mcp_auth: runtime.mcp_auth_handler.take(),
898 user_input: runtime.user_input_handler.take(),
899 exit_plan_mode: runtime.exit_plan_mode_handler.take(),
900 auto_mode_switch: runtime.auto_mode_switch_handler.take(),
901 tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)),
902 };
903 let hooks = runtime.hooks_handler.take();
904 let transforms = runtime.system_message_transform.take();
905 let tools_count = wire.tools.as_ref().map_or(0, Vec::len);
906 let commands_count = runtime.commands.as_ref().map_or(0, Vec::len);
907 let has_hooks = hooks.is_some();
908 let command_handlers = build_command_handler_map(runtime.commands.as_deref());
909 let canvas_handler = runtime.canvas_handler.take();
910 let session_fs_provider = runtime.session_fs_provider.take();
911 let bearer_token_providers = std::mem::take(&mut runtime.bearer_token_providers);
912 let has_mcp_auth_handler = handlers.mcp_auth.is_some();
913 if self.inner.session_fs_configured && session_fs_provider.is_none() {
914 return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into());
915 }
916 if self.inner.session_fs_sqlite_declared
917 && let Some(ref provider) = session_fs_provider
918 && provider.sqlite().is_none()
919 {
920 return Err(Error::with_message(
921 ErrorKind::InvalidConfig,
922 "SessionFs capabilities declare SQLite support but the provider \
923 does not implement SessionFsSqliteProvider",
924 ));
925 }
926
927 let mut params = serde_json::to_value(&wire)?;
928 let trace_ctx = self.resolve_trace_context().await;
929 inject_trace_context(&mut params, &trace_ctx);
930
931 let setup_start = Instant::now();
932 let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default()));
933 let idle_waiter = Arc::new(ParkingLotMutex::new(None));
934 let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new()));
935 let shutdown = CancellationToken::new();
936 let (event_tx, _) = tokio::sync::broadcast::channel(512);
937
938 let inline_stash: Arc<
944 ParkingLotMutex<Option<(SessionId, crate::router::SessionChannels)>>,
945 > = Arc::new(ParkingLotMutex::new(None));
946
947 let inline_callback: Option<crate::jsonrpc::InlineResponseCallback> = if let Some(ref sid) =
948 local_session_id
949 {
950 let channels = self.register_session(sid);
951 *inline_stash.lock() = Some((sid.clone(), channels));
952 None
953 } else {
954 let client = self.clone();
955 let stash = inline_stash.clone();
956 let expected = caller_session_id.clone();
957 Some(Box::new(move |response| {
958 let result = response.result.as_ref().ok_or_else(|| {
959 Error::with_message(ErrorKind::Json, "session.create response had no result")
960 })?;
961 let parsed: CreateSessionResult =
962 serde_json::from_value(result.clone()).map_err(Error::from)?;
963 if let Some(requested) = expected.as_ref()
964 && parsed.session_id != *requested
965 {
966 return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch {
967 requested: requested.clone(),
968 returned: parsed.session_id,
969 })
970 .into());
971 }
972 let channels = client.register_session(&parsed.session_id);
973 *stash.lock() = Some((parsed.session_id, channels));
974 Ok(())
975 }))
976 };
977
978 let rpc_start = Instant::now();
979 let result = match self
980 .call_with_inline_callback("session.create", Some(params), inline_callback)
981 .await
982 {
983 Ok(result) => result,
984 Err(error) => {
985 if let Some((id, _channels)) = inline_stash.lock().take() {
986 self.unregister_session(&id);
987 }
988 return Err(error);
989 }
990 };
991 tracing::debug!(
992 elapsed_ms = rpc_start.elapsed().as_millis(),
993 "Client::create_session session creation request completed successfully"
994 );
995 let create_result: CreateSessionResult = match serde_json::from_value(result) {
996 Ok(result) => result,
997 Err(error) => {
998 if let Some((id, _channels)) = inline_stash.lock().take() {
999 self.unregister_session(&id);
1000 }
1001 return Err(error.into());
1002 }
1003 };
1004
1005 if let Some(ref requested) = local_session_id
1006 && create_result.session_id != *requested
1007 {
1008 if let Some((id, _channels)) = inline_stash.lock().take() {
1009 self.unregister_session(&id);
1010 }
1011 return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch {
1012 requested: requested.clone(),
1013 returned: create_result.session_id.clone(),
1014 })
1015 .into());
1016 }
1017
1018 let (session_id, channels) = inline_stash
1019 .lock()
1020 .take()
1021 .expect("session registration must have populated stash on success");
1022 let event_loop = spawn_event_loop(
1023 session_id.clone(),
1024 self.clone(),
1025 handlers,
1026 hooks,
1027 transforms,
1028 command_handlers,
1029 canvas_handler,
1030 session_fs_provider,
1031 bearer_token_providers,
1032 channels,
1033 idle_waiter.clone(),
1034 capabilities.clone(),
1035 open_canvases.clone(),
1036 event_tx.clone(),
1037 shutdown.clone(),
1038 );
1039 tracing::debug!(
1040 elapsed_ms = setup_start.elapsed().as_millis(),
1041 session_id = %session_id,
1042 tools_count,
1043 commands_count,
1044 has_hooks,
1045 "Client::create_session local setup complete"
1046 );
1047 *capabilities.write() = create_result.capabilities.unwrap_or_default();
1048 if has_mcp_auth_handler {
1049 register_mcp_auth_interest(self, &session_id).await?;
1050 }
1051
1052 tracing::debug!(
1053 elapsed_ms = total_start.elapsed().as_millis(),
1054 session_id = %session_id,
1055 "Client::create_session complete"
1056 );
1057 let session = Session {
1058 id: session_id,
1059 cwd: self.cwd().clone(),
1060 workspace_path: create_result.workspace_path,
1061 remote_url: create_result.remote_url,
1062 client: self.clone(),
1063 event_loop: ParkingLotMutex::new(Some(event_loop)),
1064 shutdown,
1065 idle_waiter,
1066 capabilities,
1067 open_canvases,
1068 event_tx,
1069 };
1070 apply_mode_post_create_patch(
1071 &session,
1072 mode,
1073 opt_skip_custom_instructions,
1074 opt_custom_agents_local_only,
1075 opt_coauthor_enabled,
1076 opt_manage_schedule_enabled,
1077 )
1078 .await?;
1079 Ok(session)
1080 }
1081
1082 pub async fn resume_session(&self, mut config: ResumeSessionConfig) -> Result<Session, Error> {
1093 let total_start = Instant::now();
1094 let session_id = config.session_id.clone();
1095 if config.hooks_handler.is_some() && config.hooks.is_none() {
1096 config.hooks = Some(true);
1097 }
1098 if let Some(transforms) = config.system_message_transform.clone() {
1099 inject_transform_sections_resume(&mut config, transforms.as_ref());
1100 }
1101 let mode = self.inner.mode;
1102 if mode == crate::ClientMode::Empty && config.available_tools.is_none() {
1103 return Err(Error::with_message(
1104 ErrorKind::InvalidConfig,
1105 "ClientMode::Empty requires available_tools to be set on the session config. \
1106 Use ToolSet to specify which tools the session may use (e.g. \
1107 ToolSet::new().add_builtin_many(BUILTIN_TOOLS_ISOLATED)).",
1108 ));
1109 }
1110 crate::mode::validate_tool_filter_list(
1111 "available_tools",
1112 config.available_tools.as_deref(),
1113 )?;
1114 crate::mode::validate_tool_filter_list("excluded_tools", config.excluded_tools.as_deref())?;
1115 config.system_message =
1116 crate::mode::system_message_for_mode(mode, config.system_message.take());
1117 config.memory = crate::mode::memory_for_mode(mode, config.memory.take());
1118 if mode == crate::ClientMode::Empty {
1119 if config.enable_session_telemetry.is_none() {
1120 config.enable_session_telemetry = Some(false);
1121 }
1122 if config.skip_embedding_retrieval.is_none() {
1123 config.skip_embedding_retrieval = Some(true);
1124 }
1125 if config.enable_on_demand_instruction_discovery.is_none() {
1126 config.enable_on_demand_instruction_discovery = Some(false);
1127 }
1128 if config.enable_file_hooks.is_none() {
1129 config.enable_file_hooks = Some(false);
1130 }
1131 if config.enable_host_git_operations.is_none() {
1132 config.enable_host_git_operations = Some(false);
1133 }
1134 if config.enable_session_store.is_none() {
1135 config.enable_session_store = Some(false);
1136 }
1137 if config.enable_skills.is_none() {
1138 config.enable_skills = Some(false);
1139 }
1140 }
1141 if mode == crate::ClientMode::Empty && config.mcp_oauth_token_storage.is_none() {
1142 config.mcp_oauth_token_storage = Some("in-memory".into());
1143 }
1144 if mode == crate::ClientMode::Empty && config.embedding_cache_storage.is_none() {
1145 config.embedding_cache_storage = Some("in-memory".into());
1146 }
1147 let opt_skip_custom_instructions = config.skip_custom_instructions;
1148 let opt_custom_agents_local_only = config.custom_agents_local_only;
1149 let opt_coauthor_enabled = config.coauthor_enabled;
1150 let opt_manage_schedule_enabled = config.manage_schedule_enabled;
1151 let (mut wire, mut runtime) = config.into_wire()?;
1152 wire.enable_github_telemetry_forwarding =
1153 self.inner.on_github_telemetry.is_some().then_some(true);
1154
1155 let permission_handler = crate::permission::resolve_handler(
1156 runtime.permission_handler.take(),
1157 runtime.permission_policy.take(),
1158 );
1159 let handlers = SessionHandlers {
1160 permission: permission_handler,
1161 elicitation: runtime.elicitation_handler.take(),
1162 mcp_auth: runtime.mcp_auth_handler.take(),
1163 user_input: runtime.user_input_handler.take(),
1164 exit_plan_mode: runtime.exit_plan_mode_handler.take(),
1165 auto_mode_switch: runtime.auto_mode_switch_handler.take(),
1166 tools: Arc::new(std::mem::take(&mut runtime.tool_handlers)),
1167 };
1168 let hooks = runtime.hooks_handler.take();
1169 let transforms = runtime.system_message_transform.take();
1170 let tools_count = wire.tools.as_ref().map_or(0, Vec::len);
1171 let commands_count = runtime.commands.as_ref().map_or(0, Vec::len);
1172 let has_hooks = hooks.is_some();
1173 let command_handlers = build_command_handler_map(runtime.commands.as_deref());
1174 let canvas_handler = runtime.canvas_handler.take();
1175 let session_fs_provider = runtime.session_fs_provider.take();
1176 let bearer_token_providers = std::mem::take(&mut runtime.bearer_token_providers);
1177 let has_mcp_auth_handler = handlers.mcp_auth.is_some();
1178 if self.inner.session_fs_configured && session_fs_provider.is_none() {
1179 return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into());
1180 }
1181 if self.inner.session_fs_sqlite_declared
1182 && let Some(ref provider) = session_fs_provider
1183 && provider.sqlite().is_none()
1184 {
1185 return Err(Error::with_message(
1186 ErrorKind::InvalidConfig,
1187 "SessionFs capabilities declare SQLite support but the provider \
1188 does not implement SessionFsSqliteProvider",
1189 ));
1190 }
1191
1192 let mut params = serde_json::to_value(&wire)?;
1193 let trace_ctx = self.resolve_trace_context().await;
1194 inject_trace_context(&mut params, &trace_ctx);
1195
1196 let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default()));
1197 let setup_start = Instant::now();
1198 let channels = self.register_session(&session_id);
1199 let idle_waiter = Arc::new(ParkingLotMutex::new(None));
1200 let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new()));
1201 let shutdown = CancellationToken::new();
1202 let (event_tx, _) = tokio::sync::broadcast::channel(512);
1203 let event_loop = spawn_event_loop(
1204 session_id.clone(),
1205 self.clone(),
1206 handlers,
1207 hooks,
1208 transforms,
1209 command_handlers,
1210 canvas_handler,
1211 session_fs_provider,
1212 bearer_token_providers,
1213 channels,
1214 idle_waiter.clone(),
1215 capabilities.clone(),
1216 open_canvases.clone(),
1217 event_tx.clone(),
1218 shutdown.clone(),
1219 );
1220 let mut registration =
1221 PendingSessionRegistration::new(self.clone(), session_id.clone(), shutdown.clone());
1222 tracing::debug!(
1223 elapsed_ms = setup_start.elapsed().as_millis(),
1224 session_id = %session_id,
1225 tools_count,
1226 commands_count,
1227 has_hooks,
1228 "Client::resume_session local setup complete"
1229 );
1230
1231 let rpc_start = Instant::now();
1232 let result = match self.call("session.resume", Some(params)).await {
1233 Ok(result) => result,
1234 Err(error) => {
1235 registration.cleanup(event_loop).await;
1236 return Err(error);
1237 }
1238 };
1239 tracing::debug!(
1240 elapsed_ms = rpc_start.elapsed().as_millis(),
1241 session_id = %session_id,
1242 "Client::resume_session session resume request completed successfully"
1243 );
1244
1245 let resume_result: ResumeSessionResult = match serde_json::from_value(result) {
1246 Ok(result) => result,
1247 Err(error) => {
1248 registration.cleanup(event_loop).await;
1249 return Err(error.into());
1250 }
1251 };
1252 let cli_session_id = resume_result
1253 .session_id
1254 .clone()
1255 .unwrap_or_else(|| session_id.clone());
1256 if cli_session_id != session_id {
1257 registration.cleanup(event_loop).await;
1258 return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch {
1259 requested: session_id,
1260 returned: cli_session_id,
1261 })
1262 .into());
1263 }
1264 if has_mcp_auth_handler {
1265 register_mcp_auth_interest(self, &session_id).await?;
1266 }
1267
1268 let skills_reload_start = Instant::now();
1270 if let Err(e) = self
1271 .call(
1272 "session.skills.reload",
1273 Some(serde_json::json!({ "sessionId": session_id })),
1274 )
1275 .await
1276 {
1277 warn!(
1278 elapsed_ms = skills_reload_start.elapsed().as_millis(),
1279 session_id = %session_id,
1280 error = %e,
1281 "Client::resume_session skills reload request failed"
1282 );
1283 } else {
1284 tracing::debug!(
1285 elapsed_ms = skills_reload_start.elapsed().as_millis(),
1286 session_id = %session_id,
1287 "Client::resume_session skills reload request completed successfully"
1288 );
1289 }
1290
1291 *capabilities.write() = resume_result.capabilities.unwrap_or_default();
1292 {
1297 let mut snapshots = open_canvases.write();
1298 for snapshot in resume_result.open_canvases.unwrap_or_default() {
1299 upsert_open_canvas_snapshot(&mut snapshots, snapshot);
1300 }
1301 }
1302
1303 tracing::debug!(
1304 elapsed_ms = total_start.elapsed().as_millis(),
1305 session_id = %session_id,
1306 "Client::resume_session complete"
1307 );
1308 registration.disarm();
1309 let session = Session {
1310 id: session_id,
1311 cwd: self.cwd().clone(),
1312 workspace_path: resume_result.workspace_path,
1313 remote_url: resume_result.remote_url,
1314 client: self.clone(),
1315 event_loop: ParkingLotMutex::new(Some(event_loop)),
1316 shutdown,
1317 idle_waiter,
1318 capabilities,
1319 open_canvases,
1320 event_tx,
1321 };
1322 apply_mode_post_create_patch(
1323 &session,
1324 mode,
1325 opt_skip_custom_instructions,
1326 opt_custom_agents_local_only,
1327 opt_coauthor_enabled,
1328 opt_manage_schedule_enabled,
1329 )
1330 .await?;
1331 Ok(session)
1332 }
1333}
1334
1335type CommandHandlerMap = HashMap<String, Arc<dyn CommandHandler>>;
1336
1337async fn apply_mode_post_create_patch(
1338 session: &Session,
1339 mode: crate::ClientMode,
1340 opt_skip_custom_instructions: Option<bool>,
1341 opt_custom_agents_local_only: Option<bool>,
1342 opt_coauthor_enabled: Option<bool>,
1343 opt_manage_schedule_enabled: Option<bool>,
1344) -> Result<(), Error> {
1345 use crate::generated::api_types::SessionUpdateOptionsParams;
1346 let mut patch = SessionUpdateOptionsParams::default();
1347 let should_send = if mode == crate::ClientMode::Empty {
1348 patch.skip_custom_instructions = Some(opt_skip_custom_instructions.unwrap_or(true));
1349 patch.custom_agents_local_only = Some(opt_custom_agents_local_only.unwrap_or(true));
1350 patch.coauthor_enabled = Some(opt_coauthor_enabled.unwrap_or(false));
1351 patch.manage_schedule_enabled = Some(opt_manage_schedule_enabled.unwrap_or(false));
1352 patch.installed_plugins = Some(Vec::new());
1353 true
1354 } else {
1355 let mut any = false;
1356 if let Some(v) = opt_skip_custom_instructions {
1357 patch.skip_custom_instructions = Some(v);
1358 any = true;
1359 }
1360 if let Some(v) = opt_custom_agents_local_only {
1361 patch.custom_agents_local_only = Some(v);
1362 any = true;
1363 }
1364 if let Some(v) = opt_coauthor_enabled {
1365 patch.coauthor_enabled = Some(v);
1366 any = true;
1367 }
1368 if let Some(v) = opt_manage_schedule_enabled {
1369 patch.manage_schedule_enabled = Some(v);
1370 any = true;
1371 }
1372 any
1373 };
1374 if !should_send {
1375 return Ok(());
1376 }
1377 if let Err(error) = session.rpc().options().update(patch).await {
1378 let _ = session.disconnect().await;
1379 return Err(error);
1380 }
1381 Ok(())
1382}
1383
1384fn build_command_handler_map(commands: Option<&[CommandDefinition]>) -> Arc<CommandHandlerMap> {
1385 let map = match commands {
1386 Some(commands) => commands
1387 .iter()
1388 .filter(|cmd| !cmd.name.is_empty())
1389 .map(|cmd| (cmd.name.clone(), cmd.handler.clone()))
1390 .collect(),
1391 None => HashMap::new(),
1392 };
1393 Arc::new(map)
1394}
1395
1396fn upsert_open_canvas_snapshot(
1397 snapshots: &mut Vec<OpenCanvasInstance>,
1398 snapshot: OpenCanvasInstance,
1399) {
1400 if let Some(existing) = snapshots
1401 .iter_mut()
1402 .find(|open| open.instance_id == snapshot.instance_id)
1403 {
1404 *existing = snapshot;
1405 } else {
1406 snapshots.push(snapshot);
1407 }
1408}
1409
1410fn remove_open_canvas_snapshot(snapshots: &mut Vec<OpenCanvasInstance>, instance_id: &str) {
1411 snapshots.retain(|open| open.instance_id != instance_id);
1412}
1413
1414#[allow(clippy::too_many_arguments)]
1415fn spawn_event_loop(
1416 session_id: SessionId,
1417 client: Client,
1418 handlers: SessionHandlers,
1419 hooks: Option<Arc<dyn SessionHooks>>,
1420 transforms: Option<Arc<dyn SystemMessageTransform>>,
1421 command_handlers: Arc<CommandHandlerMap>,
1422 canvas_handler: Option<Arc<dyn CanvasHandler>>,
1423 session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1424 bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
1425 channels: crate::router::SessionChannels,
1426 idle_waiter: Arc<ParkingLotMutex<Option<IdleWaiter>>>,
1427 capabilities: Arc<parking_lot::RwLock<SessionCapabilities>>,
1428 open_canvases: Arc<parking_lot::RwLock<Vec<OpenCanvasInstance>>>,
1429 event_tx: tokio::sync::broadcast::Sender<SessionEvent>,
1430 shutdown: CancellationToken,
1431) -> JoinHandle<()> {
1432 let crate::router::SessionChannels {
1433 mut notifications,
1434 mut requests,
1435 } = channels;
1436
1437 let span = tracing::error_span!("session_event_loop", session_id = %session_id);
1438 tokio::spawn(
1439 async move {
1440 loop {
1441 tokio::select! {
1465 _ = shutdown.cancelled() => break,
1466 Some(notification) = notifications.recv() => {
1467 handle_notification(
1468 &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx,
1469 ).await;
1470 }
1471 Some(request) = requests.recv() => {
1472 let span = tracing::error_span!("session_request_handler", session_id = %session_id);
1476 let session_id = session_id.clone();
1477 let client = client.clone();
1478 let handlers = handlers.clone();
1479 let hooks = hooks.clone();
1480 let transforms = transforms.clone();
1481 let canvas_handler = canvas_handler.clone();
1482 let session_fs_provider = session_fs_provider.clone();
1483 let bearer_token_providers = bearer_token_providers.clone();
1484 tokio::spawn(
1485 async move {
1486 let ctx = RequestDispatchContext {
1487 client: &client,
1488 handlers: &handlers,
1489 hooks: hooks.as_deref(),
1490 transforms: transforms.as_deref(),
1491 canvas_handler: canvas_handler.as_ref(),
1492 session_fs_provider: session_fs_provider.as_ref(),
1493 bearer_token_providers: &bearer_token_providers,
1494 };
1495 handle_request(&session_id, ctx, request).await;
1496 }
1497 .instrument(span),
1498 );
1499 }
1500 else => break,
1501 }
1502 }
1503 if let Some(waiter) = idle_waiter.lock().take() {
1506 let _ = waiter
1507 .tx
1508 .send(Err(ErrorKind::Session(SessionErrorKind::EventLoopClosed).into()));
1509 }
1510 }
1511 .instrument(span),
1512 )
1513}
1514
1515fn extract_request_id(data: &Value) -> Option<RequestId> {
1516 data.get("requestId")
1517 .and_then(|v| v.as_str())
1518 .filter(|s| !s.is_empty())
1519 .map(RequestId::new)
1520}
1521
1522fn notification_permission_payload(result: &PermissionResult) -> Option<Value> {
1527 match result {
1528 PermissionResult::NoResult => None,
1529 PermissionResult::Decision(decision) => Some(
1530 serde_json::to_value(decision).expect("serializing permission decision should succeed"),
1531 ),
1532 }
1533}
1534
1535async fn register_mcp_auth_interest(client: &Client, session_id: &SessionId) -> Result<(), Error> {
1536 let mut params = serde_json::to_value(RegisterEventInterestParams {
1537 event_type: "mcp.oauth_required".to_string(),
1538 })?;
1539 params["sessionId"] = Value::String(session_id.to_string());
1540 client
1541 .call(rpc_methods::SESSION_EVENTLOG_REGISTERINTEREST, Some(params))
1542 .await?;
1543 Ok(())
1544}
1545
1546fn tool_failure_result(message: impl Into<String>) -> ToolResult {
1547 let message = message.into();
1548 ToolResult::Expanded(ToolResultExpanded {
1549 text_result_for_llm: message.clone(),
1550 result_type: "failure".to_string(),
1551 binary_results_for_llm: None,
1552 session_log: None,
1553 error: Some(message),
1554 tool_telemetry: None,
1555 tool_references: None,
1556 })
1557}
1558
1559#[allow(clippy::too_many_arguments)]
1561async fn handle_notification(
1562 session_id: &SessionId,
1563 client: &Client,
1564 handlers: &SessionHandlers,
1565 command_handlers: &Arc<CommandHandlerMap>,
1566 notification: SessionEventNotification,
1567 idle_waiter: &Arc<ParkingLotMutex<Option<IdleWaiter>>>,
1568 capabilities: &Arc<parking_lot::RwLock<SessionCapabilities>>,
1569 open_canvases: &Arc<parking_lot::RwLock<Vec<OpenCanvasInstance>>>,
1570 event_tx: &tokio::sync::broadcast::Sender<SessionEvent>,
1571) {
1572 let dispatch_start = Instant::now();
1573 let event = notification.event.clone();
1574 let event_type = event.parsed_type();
1575 if event_type == SessionEventType::PermissionRequested {
1576 tracing::debug!(
1577 session_id = %session_id,
1578 event_type = %event.event_type,
1579 "Session::handle_notification permission request received"
1580 );
1581 }
1582
1583 match event_type {
1586 SessionEventType::AssistantMessage
1587 | SessionEventType::SessionIdle
1588 | SessionEventType::SessionError => {
1589 let mut guard = idle_waiter.lock();
1590 if let Some(waiter) = guard.as_mut() {
1591 match event_type {
1592 SessionEventType::AssistantMessage => {
1593 if !waiter.first_assistant_message_seen {
1594 waiter.first_assistant_message_seen = true;
1595 tracing::debug!(
1596 elapsed_ms = waiter.started_at.elapsed().as_millis(),
1597 session_id = %session_id,
1598 "Session::send_and_wait first assistant message"
1599 );
1600 }
1601 waiter.last_assistant_message = Some(event.clone());
1602 }
1603 SessionEventType::SessionIdle | SessionEventType::SessionError => {
1604 if let Some(waiter) = guard.take() {
1605 if event_type == SessionEventType::SessionIdle {
1606 tracing::debug!(
1607 elapsed_ms = waiter.started_at.elapsed().as_millis(),
1608 session_id = %session_id,
1609 "Session::send_and_wait idle received"
1610 );
1611 let _ = waiter.tx.send(Ok(waiter.last_assistant_message));
1612 } else {
1613 let error_msg = event
1614 .typed_data::<SessionErrorData>()
1615 .map(|d| d.message)
1616 .or_else(|| {
1617 event
1618 .data
1619 .get("message")
1620 .and_then(|v| v.as_str())
1621 .map(|s| s.to_string())
1622 })
1623 .unwrap_or_else(|| "session error".to_string());
1624 let _ = waiter.tx.send(Err(Error::with_message(
1625 ErrorKind::Session(SessionErrorKind::AgentError),
1626 error_msg,
1627 )));
1628 }
1629 }
1630 }
1631 _ => {}
1632 }
1633 }
1634 }
1635 _ => {}
1636 }
1637
1638 if event_type == SessionEventType::CapabilitiesChanged {
1642 match serde_json::from_value::<SessionCapabilities>(notification.event.data.clone()) {
1643 Ok(changed) => *capabilities.write() = changed,
1644 Err(e) => warn!(error = %e, "failed to deserialize capabilities.changed payload"),
1645 }
1646 }
1647 if event_type == SessionEventType::SessionCanvasOpened {
1648 match serde_json::from_value::<OpenCanvasInstance>(notification.event.data.clone()) {
1649 Ok(open_canvas) => {
1650 upsert_open_canvas_snapshot(&mut open_canvases.write(), open_canvas);
1651 }
1652 Err(e) => warn!(error = %e, "failed to deserialize session.canvas.opened payload"),
1653 }
1654 }
1655 if event_type == SessionEventType::SessionCanvasClosed {
1656 match serde_json::from_value::<SessionCanvasClosedData>(notification.event.data.clone()) {
1657 Ok(closed) => {
1658 if closed.instance_id.is_empty() {
1659 warn!("failed to deserialize session.canvas.closed payload");
1660 } else {
1661 remove_open_canvas_snapshot(&mut open_canvases.write(), &closed.instance_id);
1662 }
1663 }
1664 Err(e) => warn!(error = %e, "failed to deserialize session.canvas.closed payload"),
1665 }
1666 }
1667
1668 let _ = event_tx.send(event.clone());
1672
1673 tracing::debug!(
1674 elapsed_ms = dispatch_start.elapsed().as_millis(),
1675 session_id = %session_id,
1676 event_type = %notification.event.event_type,
1677 "Session::handle_notification dispatch"
1678 );
1679
1680 match event_type {
1683 SessionEventType::PermissionRequested => {
1684 let Some(request_id) = extract_request_id(¬ification.event.data) else {
1685 return;
1686 };
1687 if notification
1691 .event
1692 .data
1693 .get("resolvedByHook")
1694 .and_then(|v| v.as_bool())
1695 .unwrap_or(false)
1696 {
1697 return;
1698 }
1699 let Some(permission_handler) = handlers.permission.clone() else {
1703 return;
1704 };
1705 let client = client.clone();
1706 let sid = session_id.clone();
1707 let data: PermissionRequestData =
1708 serde_json::from_value(notification.event.data.clone()).unwrap_or_else(|_| {
1709 PermissionRequestData {
1710 kind: None,
1711 tool_call_id: None,
1712 extra: notification.event.data.clone(),
1713 }
1714 });
1715 let span = tracing::error_span!(
1716 "permission_request_handler",
1717 session_id = %sid,
1718 request_id = %request_id
1719 );
1720 tokio::spawn(
1721 async move {
1722 let handler_start = Instant::now();
1723 let result = permission_handler
1724 .handle(sid.clone(), request_id.clone(), data)
1725 .await;
1726 tracing::debug!(
1727 elapsed_ms = handler_start.elapsed().as_millis(),
1728 session_id = %sid,
1729 request_id = %request_id,
1730 "PermissionHandler::handle dispatch"
1731 );
1732 let Some(result_value) = notification_permission_payload(&result) else {
1733 return;
1737 };
1738 let rpc_start = Instant::now();
1739 let _ = client
1740 .call(
1741 "session.permissions.handlePendingPermissionRequest",
1742 Some(serde_json::json!({
1743 "sessionId": sid,
1744 "requestId": request_id,
1745 "result": result_value,
1746 })),
1747 )
1748 .await;
1749 tracing::debug!(
1750 elapsed_ms = rpc_start.elapsed().as_millis(),
1751 session_id = %sid,
1752 request_id = %request_id,
1753 "Session::handle_notification response sent successfully"
1754 );
1755 }
1756 .instrument(span),
1757 );
1758 }
1759 SessionEventType::ExternalToolRequested => {
1760 let Some(request_id) = extract_request_id(¬ification.event.data) else {
1761 return;
1762 };
1763 let data: ExternalToolRequestedData =
1764 match serde_json::from_value(notification.event.data.clone()) {
1765 Ok(d) => d,
1766 Err(e) => {
1767 warn!(error = %e, "failed to deserialize external_tool.requested");
1768 let client = client.clone();
1769 let sid = session_id.clone();
1770 let span = tracing::error_span!(
1771 "external_tool_deserialize_error",
1772 session_id = %sid,
1773 request_id = %request_id
1774 );
1775 tokio::spawn(
1776 async move {
1777 let rpc_start = Instant::now();
1778 let _ = client
1779 .call(
1780 "session.tools.handlePendingToolCall",
1781 Some(serde_json::json!({
1782 "sessionId": sid,
1783 "requestId": request_id,
1784 "error": format!("Failed to deserialize tool request: {e}"),
1785 })),
1786 )
1787 .await;
1788 tracing::debug!(
1789 elapsed_ms = rpc_start.elapsed().as_millis(),
1790 session_id = %sid,
1791 request_id = %request_id,
1792 "Session::handle_notification response sent successfully"
1793 );
1794 }
1795 .instrument(span),
1796 );
1797 return;
1798 }
1799 };
1800 let tool_handler = if data.tool_name.is_empty() {
1804 None
1805 } else {
1806 handlers.tools.get(&data.tool_name).cloned()
1807 };
1808 let Some(tool_handler) = tool_handler else {
1809 return;
1810 };
1811 let client = client.clone();
1812 let sid = session_id.clone();
1813 let span = tracing::error_span!(
1814 "external_tool_handler",
1815 session_id = %sid,
1816 request_id = %request_id
1817 );
1818 tokio::spawn(
1819 async move {
1820 if data.tool_call_id.is_empty() {
1825 let error_msg = "Missing toolCallId";
1826 let rpc_start = Instant::now();
1827 let _ = client
1828 .call(
1829 "session.tools.handlePendingToolCall",
1830 Some(serde_json::json!({
1831 "sessionId": sid,
1832 "requestId": request_id,
1833 "error": error_msg,
1834 })),
1835 )
1836 .await;
1837 tracing::debug!(
1838 elapsed_ms = rpc_start.elapsed().as_millis(),
1839 session_id = %sid,
1840 request_id = %request_id,
1841 "Session::handle_notification response sent successfully"
1842 );
1843 return;
1844 }
1845 let tool_call_id = data.tool_call_id.clone();
1846 let tool_name = data.tool_name.clone();
1847 let available_tools = if tool_name == TOOL_SEARCH_TOOL_NAME {
1854 match client
1855 .call(
1856 rpc_methods::SESSION_TOOLS_GETCURRENTMETADATA,
1857 Some(serde_json::json!({ "sessionId": sid })),
1858 )
1859 .await
1860 {
1861 Ok(value) => {
1862 serde_json::from_value::<ToolsGetCurrentMetadataResult>(value)
1863 .ok()
1864 .and_then(|result| result.tools)
1865 }
1866 Err(_) => None,
1867 }
1868 } else {
1869 None
1870 };
1871 let invocation = ToolInvocation {
1872 session_id: sid.clone(),
1873 tool_call_id: data.tool_call_id,
1874 tool_name: data.tool_name,
1875 arguments: data
1876 .arguments
1877 .unwrap_or(Value::Object(serde_json::Map::new())),
1878 available_tools,
1879 traceparent: data.traceparent,
1880 tracestate: data.tracestate,
1881 };
1882 let handler_start = Instant::now();
1883 let tool_result = match tool_handler.call(invocation).await {
1884 Ok(r) => r,
1885 Err(e) => tool_failure_result(e.to_string()),
1886 };
1887 tracing::debug!(
1888 elapsed_ms = handler_start.elapsed().as_millis(),
1889 session_id = %sid,
1890 request_id = %request_id,
1891 tool_call_id = %tool_call_id,
1892 tool_name = %tool_name,
1893 "ToolHandler::call dispatch"
1894 );
1895 let result_value = serde_json::to_value(tool_result).unwrap_or(Value::Null);
1896 let rpc_start = Instant::now();
1897 let _ = client
1898 .call(
1899 "session.tools.handlePendingToolCall",
1900 Some(serde_json::json!({
1901 "sessionId": sid,
1902 "requestId": request_id,
1903 "result": result_value,
1904 })),
1905 )
1906 .await;
1907 tracing::debug!(
1908 elapsed_ms = rpc_start.elapsed().as_millis(),
1909 session_id = %sid,
1910 request_id = %request_id,
1911 tool_call_id = %tool_call_id,
1912 tool_name = %tool_name,
1913 "Session::handle_notification response sent successfully"
1914 );
1915 }
1916 .instrument(span),
1917 );
1918 }
1919 SessionEventType::UserInputRequested => {
1920 }
1927 SessionEventType::ElicitationRequested => {
1928 let Some(request_id) = extract_request_id(¬ification.event.data) else {
1929 return;
1930 };
1931 let Some(elicitation_handler) = handlers.elicitation.clone() else {
1935 return;
1936 };
1937 let elicitation_data: ElicitationRequestedData =
1938 match serde_json::from_value(notification.event.data.clone()) {
1939 Ok(d) => d,
1940 Err(e) => {
1941 warn!(error = %e, "failed to deserialize elicitation request");
1942 return;
1943 }
1944 };
1945 let request = ElicitationRequest {
1946 message: elicitation_data.message,
1947 requested_schema: elicitation_data
1948 .requested_schema
1949 .map(|s| serde_json::to_value(s).unwrap_or(Value::Null)),
1950 mode: elicitation_data.mode.map(|m| match m {
1951 crate::generated::session_events::ElicitationRequestedMode::Form => {
1952 crate::types::ElicitationMode::Form
1953 }
1954 crate::generated::session_events::ElicitationRequestedMode::Url => {
1955 crate::types::ElicitationMode::Url
1956 }
1957 _ => crate::types::ElicitationMode::Unknown,
1958 }),
1959 elicitation_source: elicitation_data.elicitation_source,
1960 url: elicitation_data.url,
1961 };
1962 let client = client.clone();
1963 let sid = session_id.clone();
1964 let span = tracing::error_span!(
1965 "elicitation_request_handler",
1966 session_id = %sid,
1967 request_id = %request_id
1968 );
1969 tokio::spawn(
1970 async move {
1971 let cancel = ElicitationResult {
1972 action: "cancel".to_string(),
1973 content: None,
1974 };
1975 let handler_task = tokio::spawn({
1977 let sid = sid.clone();
1978 let request_id = request_id.clone();
1979 let span = tracing::error_span!(
1980 "elicitation_callback",
1981 session_id = %sid,
1982 request_id = %request_id
1983 );
1984 async move {
1985 let handler_start = Instant::now();
1986 let response = elicitation_handler
1987 .handle(sid.clone(), request_id.clone(), request)
1988 .await;
1989 tracing::debug!(
1990 elapsed_ms = handler_start.elapsed().as_millis(),
1991 session_id = %sid,
1992 request_id = %request_id,
1993 "ElicitationHandler::handle dispatch"
1994 );
1995 response
1996 }
1997 .instrument(span)
1998 });
1999 let result = match handler_task.await {
2000 Ok(r) => r,
2001 Err(_) => cancel.clone(),
2002 };
2003 let rpc_start = Instant::now();
2004 if let Err(e) = client
2005 .call(
2006 "session.ui.handlePendingElicitation",
2007 Some(serde_json::json!({
2008 "sessionId": sid,
2009 "requestId": request_id,
2010 "result": result,
2011 })),
2012 )
2013 .await
2014 {
2015 warn!(error = %e, "handlePendingElicitation failed, sending cancel");
2017 let _ = client
2018 .call(
2019 "session.ui.handlePendingElicitation",
2020 Some(serde_json::json!({
2021 "sessionId": sid,
2022 "requestId": request_id,
2023 "result": cancel,
2024 })),
2025 )
2026 .await;
2027 } else {
2028 tracing::debug!(
2029 elapsed_ms = rpc_start.elapsed().as_millis(),
2030 session_id = %sid,
2031 request_id = %request_id,
2032 "Session::handle_notification response sent successfully"
2033 );
2034 }
2035 }
2036 .instrument(span),
2037 );
2038 }
2039 SessionEventType::McpOauthRequired => {
2040 let Some(request_id) = extract_request_id(¬ification.event.data) else {
2041 return;
2042 };
2043 let Some(mcp_auth_handler) = handlers.mcp_auth.clone() else {
2044 warn!(
2045 session_id = %session_id,
2046 request_id = %request_id,
2047 "received MCP OAuth request without a registered MCP auth handler"
2048 );
2049 return;
2050 };
2051 let data: McpOauthRequiredData =
2052 match serde_json::from_value(notification.event.data.clone()) {
2053 Ok(d) => d,
2054 Err(e) => {
2055 warn!(error = %e, "failed to deserialize MCP OAuth request");
2056 return;
2057 }
2058 };
2059 let request = McpAuthRequest {
2060 request_id: request_id.clone(),
2061 server_name: data.server_name,
2062 server_url: data.server_url,
2063 reason: data.reason,
2064 www_authenticate_params: data.www_authenticate_params,
2065 resource_metadata: data.resource_metadata,
2066 static_client_config: data.static_client_config,
2067 };
2068 let client = client.clone();
2069 let sid = session_id.clone();
2070 let span = tracing::error_span!(
2071 "mcp_auth_request_handler",
2072 session_id = %sid,
2073 request_id = %request_id
2074 );
2075 tokio::spawn(
2076 async move {
2077 let cancel = McpAuthResult::Cancelled;
2078 let handler_task = tokio::spawn({
2079 let sid = sid.clone();
2080 let request_id = request_id.clone();
2081 let span = tracing::error_span!(
2082 "mcp_auth_callback",
2083 session_id = %sid,
2084 request_id = %request_id
2085 );
2086 async move {
2087 let handler_start = Instant::now();
2088 let response = mcp_auth_handler
2089 .handle(sid.clone(), request_id.clone(), request)
2090 .await;
2091 tracing::debug!(
2092 elapsed_ms = handler_start.elapsed().as_millis(),
2093 session_id = %sid,
2094 request_id = %request_id,
2095 "McpAuthHandler::handle dispatch"
2096 );
2097 response
2098 }
2099 .instrument(span)
2100 });
2101 let result = match handler_task.await {
2102 Ok(result) => result,
2103 Err(_) => cancel,
2104 };
2105 let rpc_start = Instant::now();
2106 let _ = client
2107 .call(
2108 "session.mcp.oauth.handlePendingRequest",
2109 Some(serde_json::json!({
2110 "sessionId": sid,
2111 "requestId": request_id,
2112 "result": result.into_wire(),
2113 })),
2114 )
2115 .await;
2116 tracing::debug!(
2117 elapsed_ms = rpc_start.elapsed().as_millis(),
2118 "Session::handle_notification MCP auth response sent"
2119 );
2120 }
2121 .instrument(span),
2122 );
2123 }
2124 SessionEventType::CommandExecute => {
2125 let data: CommandExecuteData =
2126 match serde_json::from_value(notification.event.data.clone()) {
2127 Ok(d) => d,
2128 Err(e) => {
2129 warn!(error = %e, "failed to deserialize command.execute");
2130 return;
2131 }
2132 };
2133 let client = client.clone();
2134 let command_handlers = command_handlers.clone();
2135 let sid = session_id.clone();
2136 let span = tracing::error_span!("command_handler", session_id = %sid);
2137 tokio::spawn(
2138 async move {
2139 let request_id = data.request_id;
2140 let ack_error = match command_handlers.get(&data.command_name).cloned() {
2141 None => Some(format!("Unknown command: {}", data.command_name)),
2142 Some(handler) => {
2143 let command_name = data.command_name.clone();
2144 let ctx = CommandContext {
2145 session_id: sid.clone(),
2146 command: data.command,
2147 command_name: data.command_name,
2148 args: data.args,
2149 };
2150 let handler_start = Instant::now();
2151 let result = handler.on_command(ctx).await;
2152 tracing::debug!(
2153 elapsed_ms = handler_start.elapsed().as_millis(),
2154 session_id = %sid,
2155 request_id = %request_id,
2156 command_name = %command_name,
2157 "CommandHandler::call dispatch"
2158 );
2159 match result {
2160 Ok(()) => None,
2161 Err(e) => Some(e.to_string()),
2162 }
2163 }
2164 };
2165 let mut params = serde_json::json!({
2166 "sessionId": sid,
2167 "requestId": request_id,
2168 });
2169 if let Some(error_msg) = ack_error {
2170 params["error"] = serde_json::Value::String(error_msg);
2171 }
2172 let rpc_start = Instant::now();
2173 let _ = client
2174 .call("session.commands.handlePendingCommand", Some(params))
2175 .await;
2176 tracing::debug!(
2177 elapsed_ms = rpc_start.elapsed().as_millis(),
2178 session_id = %sid,
2179 request_id = %request_id,
2180 "Session::handle_notification response sent successfully"
2181 );
2182 }
2183 .instrument(span),
2184 );
2185 }
2186 _ => {}
2187 }
2188}
2189
2190struct RequestDispatchContext<'a> {
2191 client: &'a Client,
2192 handlers: &'a SessionHandlers,
2193 hooks: Option<&'a dyn SessionHooks>,
2194 transforms: Option<&'a dyn SystemMessageTransform>,
2195 canvas_handler: Option<&'a Arc<dyn CanvasHandler>>,
2196 session_fs_provider: Option<&'a Arc<dyn SessionFsProvider>>,
2197 bearer_token_providers: &'a HashMap<String, Arc<dyn BearerTokenProvider>>,
2198}
2199
2200async fn handle_request(
2202 session_id: &SessionId,
2203 ctx: RequestDispatchContext<'_>,
2204 request: crate::JsonRpcRequest,
2205) {
2206 let sid = session_id.clone();
2207 let client = ctx.client;
2208 let handlers = ctx.handlers;
2209 let hooks = ctx.hooks;
2210 let transforms = ctx.transforms;
2211 let canvas_handler = ctx.canvas_handler;
2212 let session_fs_provider = ctx.session_fs_provider;
2213 let bearer_token_providers = ctx.bearer_token_providers;
2214
2215 if request.method.starts_with("sessionFs.") {
2216 crate::session_fs_dispatch::dispatch(client, session_fs_provider, request).await;
2217 return;
2218 }
2219
2220 if request.method.starts_with("canvas.") {
2221 crate::canvas_dispatch::dispatch(client, canvas_handler, request).await;
2222 return;
2223 }
2224
2225 if request.method == crate::generated::api_types::rpc_methods::PROVIDERTOKEN_GETTOKEN {
2226 crate::provider_token_dispatch::dispatch(client, bearer_token_providers, request).await;
2227 return;
2228 }
2229
2230 match request.method.as_str() {
2231 "hooks.invoke" => {
2232 let params = request.params.as_ref();
2233 let hook_type = params
2234 .and_then(|p| p.get("hookType"))
2235 .and_then(|v| v.as_str())
2236 .unwrap_or("");
2237 let input = params
2238 .and_then(|p| p.get("input"))
2239 .cloned()
2240 .unwrap_or(Value::Object(Default::default()));
2241
2242 let rpc_result = if let Some(hooks) = hooks {
2243 match crate::hooks::dispatch_hook(hooks, &sid, hook_type, input).await {
2244 Ok(output) => output,
2245 Err(e) => {
2246 warn!(error = %e, hook_type = hook_type, "hook dispatch failed");
2247 serde_json::json!({ "output": {} })
2248 }
2249 }
2250 } else {
2251 serde_json::json!({ "output": {} })
2252 };
2253
2254 let rpc_response = JsonRpcResponse {
2255 jsonrpc: "2.0".to_string(),
2256 id: request.id,
2257 result: Some(rpc_result),
2258 error: None,
2259 };
2260 let _ = client.send_response(&rpc_response).await;
2261 }
2262
2263 "userInput.request" => {
2264 let params = request.params.as_ref();
2265 let Some(question) = params
2266 .and_then(|p| p.get("question"))
2267 .and_then(|v| v.as_str())
2268 else {
2269 warn!("userInput.request missing 'question' field");
2270 let rpc_response = JsonRpcResponse {
2271 jsonrpc: "2.0".to_string(),
2272 id: request.id,
2273 result: None,
2274 error: Some(crate::JsonRpcError {
2275 code: error_codes::INVALID_PARAMS,
2276 message: "missing required field: question".to_string(),
2277 data: None,
2278 }),
2279 };
2280 let _ = client.send_response(&rpc_response).await;
2281 return;
2282 };
2283 let question = question.to_string();
2284 let choices = params
2285 .and_then(|p| p.get("choices"))
2286 .and_then(|v| v.as_array())
2287 .map(|arr| {
2288 arr.iter()
2289 .filter_map(|v| v.as_str().map(|s| s.to_string()))
2290 .collect()
2291 });
2292 let allow_freeform = params
2293 .and_then(|p| p.get("allowFreeform"))
2294 .and_then(|v| v.as_bool());
2295
2296 let handler_start = Instant::now();
2297 let response = if let Some(user_input_handler) = handlers.user_input.as_ref() {
2298 user_input_handler
2299 .handle(sid.clone(), question, choices, allow_freeform)
2300 .await
2301 } else {
2302 None
2303 };
2304 tracing::debug!(
2305 elapsed_ms = handler_start.elapsed().as_millis(),
2306 session_id = %sid,
2307 "UserInputHandler::handle dispatch"
2308 );
2309
2310 let rpc_result = match response {
2311 Some(UserInputResponse {
2312 answer,
2313 was_freeform,
2314 }) => serde_json::json!({
2315 "answer": answer,
2316 "wasFreeform": was_freeform,
2317 }),
2318 None => serde_json::json!({ "noResponse": true }),
2319 };
2320 let rpc_response = JsonRpcResponse {
2321 jsonrpc: "2.0".to_string(),
2322 id: request.id,
2323 result: Some(rpc_result),
2324 error: None,
2325 };
2326 let _ = client.send_response(&rpc_response).await;
2327 }
2328
2329 "exitPlanMode.request" => {
2330 let params = request
2331 .params
2332 .as_ref()
2333 .cloned()
2334 .unwrap_or(Value::Object(serde_json::Map::new()));
2335 let data: ExitPlanModeData = match serde_json::from_value(params) {
2336 Ok(d) => d,
2337 Err(e) => {
2338 warn!(error = %e, "failed to deserialize exitPlanMode.request params, using defaults");
2339 ExitPlanModeData::default()
2340 }
2341 };
2342
2343 let rpc_result = if let Some(exit_plan_handler) = handlers.exit_plan_mode.as_ref() {
2344 let result = exit_plan_handler.handle(sid, data).await;
2345 serde_json::to_value(result).expect("ExitPlanModeResult serialization cannot fail")
2346 } else {
2347 serde_json::json!({ "approved": true })
2348 };
2349 let rpc_response = JsonRpcResponse {
2350 jsonrpc: "2.0".to_string(),
2351 id: request.id,
2352 result: Some(rpc_result),
2353 error: None,
2354 };
2355 let _ = client.send_response(&rpc_response).await;
2356 }
2357
2358 "autoModeSwitch.request" => {
2359 let error_code = request
2360 .params
2361 .as_ref()
2362 .and_then(|p| p.get("errorCode"))
2363 .and_then(|v| v.as_str())
2364 .map(|s| s.to_string());
2365 let retry_after_seconds = request
2366 .params
2367 .as_ref()
2368 .and_then(|p| p.get("retryAfterSeconds"))
2369 .and_then(|v| v.as_f64());
2370
2371 let answer = if let Some(auto_mode_handler) = handlers.auto_mode_switch.as_ref() {
2372 auto_mode_handler
2373 .handle(sid, error_code, retry_after_seconds)
2374 .await
2375 } else {
2376 AutoModeSwitchResponse::No
2377 };
2378 let rpc_response = JsonRpcResponse {
2379 jsonrpc: "2.0".to_string(),
2380 id: request.id,
2381 result: Some(serde_json::json!({ "response": answer })),
2382 error: None,
2383 };
2384 let _ = client.send_response(&rpc_response).await;
2385 }
2386
2387 "systemMessage.transform" => {
2388 let params = request.params.as_ref();
2389 let sections: HashMap<String, crate::transforms::TransformSection> =
2390 match params.and_then(|p| p.get("sections")) {
2391 Some(v) => match serde_json::from_value(v.clone()) {
2392 Ok(s) => s,
2393 Err(e) => {
2394 let _ = send_error_response(
2395 client,
2396 request.id,
2397 error_codes::INVALID_PARAMS,
2398 &format!("invalid sections: {e}"),
2399 )
2400 .await;
2401 return;
2402 }
2403 },
2404 None => {
2405 let _ = send_error_response(
2406 client,
2407 request.id,
2408 error_codes::INVALID_PARAMS,
2409 "missing sections parameter",
2410 )
2411 .await;
2412 return;
2413 }
2414 };
2415
2416 let rpc_result = if let Some(transforms) = transforms {
2417 let transform_start = Instant::now();
2418 let response =
2419 crate::transforms::dispatch_transform(transforms, &sid, sections).await;
2420 tracing::debug!(
2421 elapsed_ms = transform_start.elapsed().as_millis(),
2422 session_id = %sid,
2423 "SystemMessageTransform::transform_section dispatch"
2424 );
2425 match serde_json::to_value(response) {
2426 Ok(v) => v,
2427 Err(e) => {
2428 warn!(error = %e, "failed to serialize transform response");
2429 serde_json::json!({ "sections": {} })
2430 }
2431 }
2432 } else {
2433 let passthrough: HashMap<String, crate::transforms::TransformSection> = sections;
2435 serde_json::json!({ "sections": passthrough })
2436 };
2437
2438 let rpc_response = JsonRpcResponse {
2439 jsonrpc: "2.0".to_string(),
2440 id: request.id,
2441 result: Some(rpc_result),
2442 error: None,
2443 };
2444 let _ = client.send_response(&rpc_response).await;
2445 }
2446
2447 method => {
2448 warn!(
2449 method = method,
2450 "unhandled request method in session event loop"
2451 );
2452 let _ = send_error_response(
2453 client,
2454 request.id,
2455 error_codes::METHOD_NOT_FOUND,
2456 &format!("unknown method: {method}"),
2457 )
2458 .await;
2459 }
2460 }
2461}
2462
2463async fn send_error_response(
2464 client: &Client,
2465 id: u64,
2466 code: i32,
2467 message: &str,
2468) -> Result<(), Error> {
2469 let response = JsonRpcResponse {
2470 jsonrpc: "2.0".to_string(),
2471 id,
2472 result: None,
2473 error: Some(crate::JsonRpcError {
2474 code,
2475 message: message.to_string(),
2476 data: None,
2477 }),
2478 };
2479 client.send_response(&response).await
2480}
2481
2482fn apply_transform_sections(
2486 sys_msg: &mut SystemMessageConfig,
2487 transforms: &dyn SystemMessageTransform,
2488) {
2489 sys_msg.mode = Some("customize".to_string());
2490 let sections = sys_msg.sections.get_or_insert_with(HashMap::new);
2491 for id in transforms.section_ids() {
2492 sections.entry(id).or_insert_with(|| SectionOverride {
2493 action: Some("transform".to_string()),
2494 content: None,
2495 });
2496 }
2497}
2498
2499fn inject_transform_sections(config: &mut SessionConfig, transforms: &dyn SystemMessageTransform) {
2500 let sys_msg = config.system_message.get_or_insert_with(Default::default);
2501 apply_transform_sections(sys_msg, transforms);
2502}
2503
2504fn inject_transform_sections_resume(
2505 config: &mut ResumeSessionConfig,
2506 transforms: &dyn SystemMessageTransform,
2507) {
2508 let sys_msg = config.system_message.get_or_insert_with(Default::default);
2509 apply_transform_sections(sys_msg, transforms);
2510}
2511
2512#[cfg(test)]
2513mod tests {
2514 use serde_json::json;
2515
2516 use super::notification_permission_payload;
2517 use crate::handler::PermissionResult;
2518
2519 #[test]
2520 fn notification_payload_suppresses_no_result() {
2521 assert!(notification_permission_payload(&PermissionResult::NoResult).is_none());
2522 }
2523
2524 #[test]
2525 fn notification_payload_serializes_decisions() {
2526 assert_eq!(
2527 notification_permission_payload(&PermissionResult::approve_once()),
2528 Some(json!({ "kind": "approve-once" }))
2529 );
2530 assert_eq!(
2531 notification_permission_payload(&PermissionResult::reject(None)),
2532 Some(json!({ "kind": "reject" }))
2533 );
2534 assert_eq!(
2535 notification_permission_payload(&PermissionResult::reject(Some("bad".to_string()))),
2536 Some(json!({ "kind": "reject", "feedback": "bad" }))
2537 );
2538 assert_eq!(
2539 notification_permission_payload(&PermissionResult::user_not_available()),
2540 Some(json!({ "kind": "user-not-available" }))
2541 );
2542 }
2543}