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