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