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