1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10
11fn is_false(value: &bool) -> bool {
12 !*value
13}
14
15pub const SDK_PROTOCOL_VERSION: u32 = 2;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum ConnectionState {
29 #[default]
30 Disconnected,
31 Connecting,
32 Connected,
33 Error,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum SystemMessageMode {
40 Append,
41 Replace,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "lowercase")]
47pub enum AttachmentType {
48 File,
49 Directory,
50 Selection,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
55pub enum LogLevel {
56 None,
57 Debug,
58 #[default]
59 Info,
60 Warn,
61 Error,
62 All,
63}
64
65impl std::fmt::Display for LogLevel {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 LogLevel::None => write!(f, "none"),
69 LogLevel::Debug => write!(f, "debug"),
70 LogLevel::Info => write!(f, "info"),
71 LogLevel::Warn => write!(f, "warn"),
72 LogLevel::Error => write!(f, "error"),
73 LogLevel::All => write!(f, "all"),
74 }
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ToolBinaryResult {
86 pub data: String,
87 pub mime_type: String,
88 #[serde(rename = "type")]
89 pub result_type: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub description: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96#[serde(rename_all = "camelCase")]
97pub struct ToolResultObject {
98 pub text_result_for_llm: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
101 #[serde(default = "default_result_type")]
102 pub result_type: String,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub error: Option<String>,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub session_log: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub tool_telemetry: Option<HashMap<String, serde_json::Value>>,
109}
110
111fn default_result_type() -> String {
112 "success".to_string()
113}
114
115impl ToolResultObject {
116 pub fn text(result: impl Into<String>) -> Self {
118 Self {
119 text_result_for_llm: result.into(),
120 binary_results_for_llm: None,
121 result_type: "success".to_string(),
122 error: None,
123 session_log: None,
124 tool_telemetry: None,
125 }
126 }
127
128 pub fn error(message: impl Into<String>) -> Self {
130 Self {
131 text_result_for_llm: String::new(),
132 binary_results_for_llm: None,
133 result_type: "error".to_string(),
134 error: Some(message.into()),
135 session_log: None,
136 tool_telemetry: None,
137 }
138 }
139}
140
141pub type ToolResult = ToolResultObject;
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct ToolInvocation {
148 pub session_id: String,
149 pub tool_call_id: String,
150 pub tool_name: String,
151 #[serde(default)]
152 pub arguments: Option<serde_json::Value>,
153}
154
155impl ToolInvocation {
156 pub fn arg<T: serde::de::DeserializeOwned>(&self, name: &str) -> crate::Result<T> {
158 let args = self
159 .arguments
160 .as_ref()
161 .ok_or_else(|| crate::CopilotError::ToolError("No arguments provided".into()))?;
162
163 let value = args
164 .get(name)
165 .ok_or_else(|| crate::CopilotError::ToolError(format!("Missing argument: {}", name)))?;
166
167 serde_json::from_value(value.clone()).map_err(|e| {
168 crate::CopilotError::ToolError(format!("Invalid argument '{}': {}", name, e))
169 })
170 }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct PermissionRequest {
181 pub kind: String,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub tool_call_id: Option<String>,
184 #[serde(flatten)]
185 pub extension_data: HashMap<String, serde_json::Value>,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct PermissionRequestResult {
192 pub kind: String,
193 #[serde(skip_serializing_if = "Option::is_none")]
194 pub rules: Option<Vec<serde_json::Value>>,
195}
196
197impl PermissionRequestResult {
198 pub fn approved() -> Self {
200 Self {
201 kind: "approved".to_string(),
202 rules: None,
203 }
204 }
205
206 pub fn denied() -> Self {
208 Self {
209 kind: "denied-no-approval-rule-and-could-not-request-from-user".to_string(),
210 rules: None,
211 }
212 }
213
214 pub fn is_approved(&self) -> bool {
216 self.kind == "approved"
217 }
218
219 pub fn is_denied(&self) -> bool {
221 self.kind.starts_with("denied")
222 }
223}
224
225#[derive(Debug, Clone, Default, Serialize, Deserialize)]
231#[serde(rename_all = "camelCase")]
232pub struct SystemMessageConfig {
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub mode: Option<SystemMessageMode>,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub content: Option<String>,
237}
238
239#[derive(Debug, Clone, Default, Serialize, Deserialize)]
241#[serde(rename_all = "camelCase")]
242pub struct AzureOptions {
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub api_version: Option<String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub struct ProviderConfig {
251 pub base_url: String,
252 #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
253 pub provider_type: Option<String>,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub wire_api: Option<String>,
256 #[serde(skip_serializing_if = "Option::is_none")]
257 pub api_key: Option<String>,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub bearer_token: Option<String>,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub azure: Option<AzureOptions>,
262}
263
264impl ProviderConfig {
266 pub const ENV_API_KEY: &'static str = "COPILOT_SDK_BYOK_API_KEY";
268 pub const ENV_BASE_URL: &'static str = "COPILOT_SDK_BYOK_BASE_URL";
270 pub const ENV_PROVIDER_TYPE: &'static str = "COPILOT_SDK_BYOK_PROVIDER_TYPE";
272 pub const ENV_MODEL: &'static str = "COPILOT_SDK_BYOK_MODEL";
274
275 pub fn is_env_configured() -> bool {
279 std::env::var(Self::ENV_API_KEY)
280 .map(|v| !v.is_empty())
281 .unwrap_or(false)
282 }
283
284 pub fn from_env() -> Option<Self> {
293 if !Self::is_env_configured() {
294 return None;
295 }
296
297 let api_key = std::env::var(Self::ENV_API_KEY).ok();
298 let base_url = std::env::var(Self::ENV_BASE_URL)
299 .unwrap_or_else(|_| "https://api.openai.com/v1".to_string());
300 let provider_type = std::env::var(Self::ENV_PROVIDER_TYPE)
301 .ok()
302 .or_else(|| Some("openai".to_string()));
303
304 Some(Self {
305 base_url,
306 provider_type,
307 api_key,
308 wire_api: None,
309 bearer_token: None,
310 azure: None,
311 })
312 }
313
314 pub fn model_from_env() -> Option<String> {
318 std::env::var(Self::ENV_MODEL)
319 .ok()
320 .filter(|v| !v.is_empty())
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct McpLocalServerConfig {
332 pub tools: Vec<String>,
333 pub command: String,
334 #[serde(default)]
335 pub args: Vec<String>,
336 #[serde(skip_serializing_if = "Option::is_none", rename = "type")]
337 pub server_type: Option<String>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub timeout: Option<i32>,
340 #[serde(skip_serializing_if = "Option::is_none")]
341 pub env: Option<HashMap<String, String>>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 pub cwd: Option<String>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct McpRemoteServerConfig {
350 pub tools: Vec<String>,
351 pub url: String,
352 #[serde(default = "default_mcp_type", rename = "type")]
353 pub server_type: String,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub timeout: Option<i32>,
356 #[serde(skip_serializing_if = "Option::is_none")]
357 pub headers: Option<HashMap<String, String>>,
358}
359
360fn default_mcp_type() -> String {
361 "http".to_string()
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
366#[serde(untagged)]
367pub enum McpServerConfig {
368 Local(McpLocalServerConfig),
369 Remote(McpRemoteServerConfig),
370}
371
372#[derive(Debug, Clone, Default, Serialize, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct CustomAgentConfig {
380 pub name: String,
381 pub prompt: String,
382 #[serde(skip_serializing_if = "Option::is_none")]
383 pub display_name: Option<String>,
384 #[serde(skip_serializing_if = "Option::is_none")]
385 pub description: Option<String>,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub tools: Option<Vec<String>>,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub infer: Option<bool>,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct UserMessageAttachment {
402 #[serde(rename = "type")]
403 pub attachment_type: AttachmentType,
404 pub path: String,
405 pub display_name: String,
406}
407
408#[derive(Clone)]
447pub struct Tool {
448 pub name: String,
449 pub description: String,
450 pub parameters_schema: serde_json::Value,
451 }
453
454impl Tool {
455 pub fn new(name: impl Into<String>) -> Self {
457 Self {
458 name: name.into(),
459 description: String::new(),
460 parameters_schema: serde_json::json!({}),
461 }
462 }
463
464 pub fn description(mut self, desc: impl Into<String>) -> Self {
466 self.description = desc.into();
467 self
468 }
469
470 pub fn schema(mut self, schema: serde_json::Value) -> Self {
472 self.parameters_schema = schema;
473 self
474 }
475
476 pub fn parameter(
480 mut self,
481 name: impl Into<String>,
482 param_type: impl Into<String>,
483 description: impl Into<String>,
484 required: bool,
485 ) -> Self {
486 let name = name.into();
487
488 if self.parameters_schema.get("type").is_none() {
490 self.parameters_schema["type"] = serde_json::json!("object");
491 }
492 if self.parameters_schema.get("properties").is_none() {
493 self.parameters_schema["properties"] = serde_json::json!({});
494 }
495
496 self.parameters_schema["properties"][&name] = serde_json::json!({
497 "type": param_type.into(),
498 "description": description.into(),
499 });
500
501 if required {
502 if self.parameters_schema.get("required").is_none() {
503 self.parameters_schema["required"] = serde_json::json!([]);
504 }
505 if let Some(arr) = self.parameters_schema["required"].as_array_mut() {
506 arr.push(serde_json::json!(name));
507 }
508 }
509
510 self
511 }
512
513 #[cfg(feature = "schemars")]
515 pub fn typed_schema<T: schemars::JsonSchema>(mut self) -> Self {
516 let schema = schemars::schema_for!(T);
517 match serde_json::to_value(&schema) {
518 Ok(value) => self.parameters_schema = value,
519 Err(err) => {
520 tracing::warn!("Failed to serialize schemars schema: {err}");
521 self.parameters_schema = serde_json::json!({});
522 }
523 }
524 self
525 }
526}
527
528impl std::fmt::Debug for Tool {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530 f.debug_struct("Tool")
531 .field("name", &self.name)
532 .field("description", &self.description)
533 .finish()
534 }
535}
536
537impl Serialize for Tool {
539 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
540 where
541 S: serde::Serializer,
542 {
543 use serde::ser::SerializeStruct;
544 let mut state = serializer.serialize_struct("Tool", 3)?;
545 state.serialize_field("name", &self.name)?;
546 state.serialize_field("description", &self.description)?;
547 state.serialize_field("parametersSchema", &self.parameters_schema)?;
548 state.end()
549 }
550}
551
552#[derive(Debug, Clone, Default, Serialize, Deserialize)]
561#[serde(rename_all = "camelCase")]
562pub struct InfiniteSessionConfig {
563 #[serde(skip_serializing_if = "Option::is_none")]
565 pub enabled: Option<bool>,
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub background_compaction_threshold: Option<f64>,
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub buffer_exhaustion_threshold: Option<f64>,
572}
573
574impl InfiniteSessionConfig {
575 pub fn enabled() -> Self {
577 Self {
578 enabled: Some(true),
579 background_compaction_threshold: None,
580 buffer_exhaustion_threshold: None,
581 }
582 }
583
584 pub fn with_thresholds(background: f64, exhaustion: f64) -> Self {
586 Self {
587 enabled: Some(true),
588 background_compaction_threshold: Some(background),
589 buffer_exhaustion_threshold: Some(exhaustion),
590 }
591 }
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
600#[serde(rename_all = "camelCase")]
601pub struct PreToolUseHookInput {
602 pub timestamp: i64,
603 pub cwd: String,
604 pub tool_name: String,
605 pub tool_args: serde_json::Value,
606}
607
608#[derive(Debug, Clone, Default, Serialize, Deserialize)]
610#[serde(rename_all = "camelCase")]
611pub struct PreToolUseHookOutput {
612 #[serde(skip_serializing_if = "Option::is_none")]
613 pub permission_decision: Option<String>,
614 #[serde(skip_serializing_if = "Option::is_none")]
615 pub permission_decision_reason: Option<String>,
616 #[serde(skip_serializing_if = "Option::is_none")]
617 pub modified_args: Option<serde_json::Value>,
618 #[serde(skip_serializing_if = "Option::is_none")]
619 pub additional_context: Option<String>,
620 #[serde(skip_serializing_if = "Option::is_none")]
621 pub suppress_output: Option<bool>,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
626#[serde(rename_all = "camelCase")]
627pub struct PostToolUseHookInput {
628 pub timestamp: i64,
629 pub cwd: String,
630 pub tool_name: String,
631 pub tool_args: serde_json::Value,
632 pub tool_result: serde_json::Value,
633}
634
635#[derive(Debug, Clone, Default, Serialize, Deserialize)]
637#[serde(rename_all = "camelCase")]
638pub struct PostToolUseHookOutput {
639 #[serde(skip_serializing_if = "Option::is_none")]
640 pub modified_result: Option<serde_json::Value>,
641 #[serde(skip_serializing_if = "Option::is_none")]
642 pub additional_context: Option<String>,
643 #[serde(skip_serializing_if = "Option::is_none")]
644 pub suppress_output: Option<bool>,
645}
646
647#[derive(Debug, Clone, Serialize, Deserialize)]
649#[serde(rename_all = "camelCase")]
650pub struct UserPromptSubmittedHookInput {
651 pub timestamp: i64,
652 pub cwd: String,
653 pub prompt: String,
654}
655
656#[derive(Debug, Clone, Default, Serialize, Deserialize)]
658#[serde(rename_all = "camelCase")]
659pub struct UserPromptSubmittedHookOutput {
660 #[serde(skip_serializing_if = "Option::is_none")]
661 pub modified_prompt: Option<String>,
662 #[serde(skip_serializing_if = "Option::is_none")]
663 pub additional_context: Option<String>,
664 #[serde(skip_serializing_if = "Option::is_none")]
665 pub suppress_output: Option<bool>,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize)]
670#[serde(rename_all = "camelCase")]
671pub struct SessionStartHookInput {
672 pub timestamp: i64,
673 pub cwd: String,
674 pub source: String,
675 #[serde(skip_serializing_if = "Option::is_none")]
676 pub initial_prompt: Option<String>,
677}
678
679#[derive(Debug, Clone, Default, Serialize, Deserialize)]
681#[serde(rename_all = "camelCase")]
682pub struct SessionStartHookOutput {
683 #[serde(skip_serializing_if = "Option::is_none")]
684 pub additional_context: Option<String>,
685 #[serde(skip_serializing_if = "Option::is_none")]
686 pub modified_config: Option<serde_json::Value>,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize)]
691#[serde(rename_all = "camelCase")]
692pub struct SessionEndHookInput {
693 pub timestamp: i64,
694 pub cwd: String,
695 pub reason: String,
696 #[serde(skip_serializing_if = "Option::is_none")]
697 pub final_message: Option<String>,
698 #[serde(skip_serializing_if = "Option::is_none")]
699 pub error: Option<String>,
700}
701
702#[derive(Debug, Clone, Default, Serialize, Deserialize)]
704#[serde(rename_all = "camelCase")]
705pub struct SessionEndHookOutput {
706 #[serde(skip_serializing_if = "Option::is_none")]
707 pub suppress_output: Option<bool>,
708 #[serde(skip_serializing_if = "Option::is_none")]
709 pub cleanup_actions: Option<Vec<String>>,
710 #[serde(skip_serializing_if = "Option::is_none")]
711 pub session_summary: Option<String>,
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
716#[serde(rename_all = "camelCase")]
717pub struct ErrorOccurredHookInput {
718 pub timestamp: i64,
719 pub cwd: String,
720 pub error: String,
721 pub error_context: String,
722 pub recoverable: bool,
723}
724
725#[derive(Debug, Clone, Default, Serialize, Deserialize)]
727#[serde(rename_all = "camelCase")]
728pub struct ErrorOccurredHookOutput {
729 #[serde(skip_serializing_if = "Option::is_none")]
730 pub suppress_output: Option<bool>,
731 #[serde(skip_serializing_if = "Option::is_none")]
732 pub error_handling: Option<String>,
733 #[serde(skip_serializing_if = "Option::is_none")]
734 pub retry_count: Option<i32>,
735 #[serde(skip_serializing_if = "Option::is_none")]
736 pub user_notification: Option<String>,
737}
738
739pub type PreToolUseHandler = Arc<dyn Fn(PreToolUseHookInput) -> PreToolUseHookOutput + Send + Sync>;
741pub type PostToolUseHandler =
742 Arc<dyn Fn(PostToolUseHookInput) -> PostToolUseHookOutput + Send + Sync>;
743pub type UserPromptSubmittedHandler =
744 Arc<dyn Fn(UserPromptSubmittedHookInput) -> UserPromptSubmittedHookOutput + Send + Sync>;
745pub type SessionStartHandler =
746 Arc<dyn Fn(SessionStartHookInput) -> SessionStartHookOutput + Send + Sync>;
747pub type SessionEndHandler = Arc<dyn Fn(SessionEndHookInput) -> SessionEndHookOutput + Send + Sync>;
748pub type ErrorOccurredHandler =
749 Arc<dyn Fn(ErrorOccurredHookInput) -> ErrorOccurredHookOutput + Send + Sync>;
750
751#[derive(Clone, Default)]
755pub struct SessionHooks {
756 pub on_pre_tool_use: Option<PreToolUseHandler>,
757 pub on_post_tool_use: Option<PostToolUseHandler>,
758 pub on_user_prompt_submitted: Option<UserPromptSubmittedHandler>,
759 pub on_session_start: Option<SessionStartHandler>,
760 pub on_session_end: Option<SessionEndHandler>,
761 pub on_error_occurred: Option<ErrorOccurredHandler>,
762}
763
764impl SessionHooks {
765 pub fn has_any(&self) -> bool {
767 self.on_pre_tool_use.is_some()
768 || self.on_post_tool_use.is_some()
769 || self.on_user_prompt_submitted.is_some()
770 || self.on_session_start.is_some()
771 || self.on_session_end.is_some()
772 || self.on_error_occurred.is_some()
773 }
774}
775
776impl std::fmt::Debug for SessionHooks {
777 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
778 f.debug_struct("SessionHooks")
779 .field("on_pre_tool_use", &self.on_pre_tool_use.is_some())
780 .field("on_post_tool_use", &self.on_post_tool_use.is_some())
781 .field(
782 "on_user_prompt_submitted",
783 &self.on_user_prompt_submitted.is_some(),
784 )
785 .field("on_session_start", &self.on_session_start.is_some())
786 .field("on_session_end", &self.on_session_end.is_some())
787 .field("on_error_occurred", &self.on_error_occurred.is_some())
788 .finish()
789 }
790}
791
792#[derive(Debug, Clone, Default, Serialize)]
798#[serde(rename_all = "camelCase")]
799pub struct SessionConfig {
800 #[serde(skip_serializing_if = "Option::is_none")]
801 pub session_id: Option<String>,
802 #[serde(skip_serializing_if = "Option::is_none")]
803 pub model: Option<String>,
804 #[serde(skip_serializing_if = "Option::is_none")]
805 pub config_dir: Option<PathBuf>,
806 #[serde(skip_serializing_if = "Vec::is_empty")]
807 pub tools: Vec<Tool>,
808 #[serde(skip_serializing_if = "Option::is_none")]
809 pub system_message: Option<SystemMessageConfig>,
810 #[serde(skip_serializing_if = "Option::is_none")]
811 pub available_tools: Option<Vec<String>>,
812 #[serde(skip_serializing_if = "Option::is_none")]
813 pub excluded_tools: Option<Vec<String>>,
814 #[serde(skip_serializing_if = "Option::is_none")]
815 pub provider: Option<ProviderConfig>,
816 #[serde(default, skip_serializing_if = "is_false")]
817 pub streaming: bool,
818 #[serde(skip_serializing_if = "Option::is_none")]
819 pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
820 #[serde(skip_serializing_if = "Option::is_none")]
821 pub custom_agents: Option<Vec<CustomAgentConfig>>,
822 #[serde(skip_serializing_if = "Option::is_none")]
823 pub skill_directories: Option<Vec<String>>,
824 #[serde(skip_serializing_if = "Option::is_none")]
825 pub disabled_skills: Option<Vec<String>>,
826 #[serde(skip_serializing_if = "Option::is_none", rename = "requestPermission")]
827 pub request_permission: Option<bool>,
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub infinite_sessions: Option<InfiniteSessionConfig>,
831
832 #[serde(skip_serializing_if = "Option::is_none", rename = "requestUserInput")]
835 pub request_user_input: Option<bool>,
836
837 #[serde(skip_serializing_if = "Option::is_none")]
839 pub reasoning_effort: Option<String>,
840
841 #[serde(skip_serializing_if = "Option::is_none")]
843 pub working_directory: Option<String>,
844
845 #[serde(skip)]
847 pub hooks: Option<SessionHooks>,
848
849 #[serde(skip)]
853 pub auto_byok_from_env: bool,
854}
855
856#[derive(Debug, Clone, Default, Serialize)]
858#[serde(rename_all = "camelCase")]
859pub struct ResumeSessionConfig {
860 #[serde(skip_serializing_if = "Option::is_none")]
861 pub model: Option<String>,
862 #[serde(skip_serializing_if = "Vec::is_empty")]
863 pub tools: Vec<Tool>,
864 #[serde(skip_serializing_if = "Option::is_none")]
865 pub provider: Option<ProviderConfig>,
866 #[serde(default, skip_serializing_if = "is_false")]
867 pub streaming: bool,
868 #[serde(skip_serializing_if = "Option::is_none")]
869 pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
870 #[serde(skip_serializing_if = "Option::is_none")]
871 pub custom_agents: Option<Vec<CustomAgentConfig>>,
872 #[serde(skip_serializing_if = "Option::is_none")]
873 pub skill_directories: Option<Vec<String>>,
874 #[serde(skip_serializing_if = "Option::is_none")]
875 pub disabled_skills: Option<Vec<String>>,
876 #[serde(skip_serializing_if = "Option::is_none", rename = "requestPermission")]
877 pub request_permission: Option<bool>,
878
879 #[serde(skip_serializing_if = "Option::is_none", rename = "requestUserInput")]
881 pub request_user_input: Option<bool>,
882
883 #[serde(skip_serializing_if = "Option::is_none")]
885 pub reasoning_effort: Option<String>,
886
887 #[serde(skip_serializing_if = "Option::is_none")]
889 pub working_directory: Option<String>,
890
891 #[serde(default, skip_serializing_if = "is_false")]
893 pub disable_resume: bool,
894
895 #[serde(skip_serializing_if = "Option::is_none")]
897 pub infinite_sessions: Option<InfiniteSessionConfig>,
898
899 #[serde(skip)]
901 pub hooks: Option<SessionHooks>,
902
903 #[serde(skip)]
907 pub auto_byok_from_env: bool,
908}
909
910#[derive(Debug, Clone, Default, Serialize)]
912#[serde(rename_all = "camelCase")]
913pub struct MessageOptions {
914 pub prompt: String,
915 #[serde(skip_serializing_if = "Option::is_none")]
916 pub attachments: Option<Vec<UserMessageAttachment>>,
917 #[serde(skip_serializing_if = "Option::is_none")]
918 pub mode: Option<String>,
919}
920
921impl From<&str> for MessageOptions {
922 fn from(prompt: &str) -> Self {
923 Self {
924 prompt: prompt.to_string(),
925 attachments: None,
926 mode: None,
927 }
928 }
929}
930
931impl From<String> for MessageOptions {
932 fn from(prompt: String) -> Self {
933 Self {
934 prompt,
935 attachments: None,
936 mode: None,
937 }
938 }
939}
940
941#[derive(Debug, Clone)]
947pub struct ClientOptions {
948 pub cli_path: Option<PathBuf>,
949 pub cli_args: Option<Vec<String>>,
950 pub cwd: Option<PathBuf>,
951 pub port: u16,
952 pub use_stdio: bool,
953 pub cli_url: Option<String>,
954 pub log_level: LogLevel,
955 pub auto_start: bool,
956 pub auto_restart: bool,
957 pub environment: Option<HashMap<String, String>>,
958 pub github_token: Option<String>,
961 pub use_logged_in_user: Option<bool>,
964
965 pub deny_tools: Option<Vec<String>>,
977
978 pub allow_tools: Option<Vec<String>>,
983
984 pub allow_all_tools: bool,
989}
990
991impl Default for ClientOptions {
992 fn default() -> Self {
993 Self {
994 cli_path: None,
995 cli_args: None,
996 cwd: None,
997 port: 0,
998 use_stdio: true,
999 cli_url: None,
1000 log_level: LogLevel::Info,
1001 auto_start: true,
1002 auto_restart: true,
1003 environment: None,
1004 github_token: None,
1005 use_logged_in_user: None,
1006 deny_tools: None,
1007 allow_tools: None,
1008 allow_all_tools: false,
1009 }
1010 }
1011}
1012
1013#[derive(Debug, Clone, Deserialize)]
1019#[serde(rename_all = "camelCase")]
1020pub struct SessionMetadata {
1021 pub session_id: String,
1022 #[serde(default)]
1023 pub start_time: Option<String>,
1024 #[serde(default)]
1025 pub modified_time: Option<String>,
1026 #[serde(default)]
1027 pub summary: Option<String>,
1028 #[serde(default)]
1029 pub is_remote: bool,
1030}
1031
1032#[derive(Debug, Clone, Deserialize)]
1034#[serde(rename_all = "camelCase")]
1035pub struct PingResponse {
1036 pub message: String,
1037 pub timestamp: i64,
1038 #[serde(default)]
1039 pub protocol_version: Option<u32>,
1040}
1041
1042#[derive(Debug, Clone, Deserialize)]
1044#[serde(rename_all = "camelCase")]
1045pub struct GetStatusResponse {
1046 pub version: String,
1047 pub protocol_version: u32,
1048}
1049
1050#[derive(Debug, Clone, Deserialize)]
1052#[serde(rename_all = "camelCase")]
1053pub struct GetAuthStatusResponse {
1054 pub is_authenticated: bool,
1055 #[serde(default)]
1056 pub auth_type: Option<String>,
1057 #[serde(default)]
1058 pub host: Option<String>,
1059 #[serde(default)]
1060 pub login: Option<String>,
1061 #[serde(default)]
1062 pub status_message: Option<String>,
1063}
1064
1065#[derive(Debug, Clone, Default, Deserialize)]
1067#[serde(rename_all = "camelCase")]
1068pub struct ModelCapabilities {
1069 #[serde(default)]
1070 pub supports: ModelSupports,
1071 #[serde(default)]
1072 pub limits: ModelLimits,
1073}
1074
1075#[derive(Debug, Clone, Default, Deserialize)]
1077#[serde(rename_all = "camelCase")]
1078pub struct ModelSupports {
1079 #[serde(default)]
1080 pub vision: bool,
1081 #[serde(default)]
1082 pub reasoning_effort: bool,
1083}
1084
1085#[derive(Debug, Clone, Default, Deserialize)]
1087#[serde(rename_all = "camelCase")]
1088pub struct ModelVisionLimits {
1089 #[serde(default)]
1090 pub supported_media_types: Vec<String>,
1091 #[serde(default)]
1092 pub max_prompt_images: u32,
1093 #[serde(default)]
1094 pub max_prompt_image_size: u64,
1095}
1096
1097#[derive(Debug, Clone, Default, Deserialize)]
1099#[serde(rename_all = "camelCase")]
1100pub struct ModelLimits {
1101 #[serde(default)]
1102 pub max_prompt_tokens: Option<u32>,
1103 #[serde(default)]
1104 pub max_context_window_tokens: u32,
1105 #[serde(default)]
1106 pub vision: Option<ModelVisionLimits>,
1107}
1108
1109#[derive(Debug, Clone, Deserialize)]
1111pub struct ModelPolicy {
1112 pub state: String,
1113 #[serde(default)]
1114 pub terms: String,
1115}
1116
1117#[derive(Debug, Clone, Deserialize)]
1119pub struct ModelBilling {
1120 #[serde(default)]
1121 pub multiplier: f64,
1122}
1123
1124#[derive(Debug, Clone, Deserialize)]
1126#[serde(rename_all = "camelCase")]
1127pub struct ModelInfo {
1128 pub id: String,
1129 pub name: String,
1130 pub capabilities: ModelCapabilities,
1131 #[serde(default)]
1132 pub policy: Option<ModelPolicy>,
1133 #[serde(default)]
1134 pub billing: Option<ModelBilling>,
1135 #[serde(default)]
1136 pub supported_reasoning_efforts: Option<Vec<String>>,
1137 #[serde(default)]
1138 pub default_reasoning_effort: Option<String>,
1139}
1140
1141#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1147pub struct SelectionPosition {
1148 #[serde(default)]
1149 pub line: f64,
1150 #[serde(default)]
1151 pub character: f64,
1152}
1153
1154#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1156pub struct SelectionRange {
1157 pub start: SelectionPosition,
1158 pub end: SelectionPosition,
1159}
1160
1161#[derive(Debug, Clone, Serialize, Deserialize)]
1163#[serde(rename_all = "camelCase")]
1164pub struct SelectionAttachment {
1165 pub file_path: String,
1166 pub display_name: String,
1167 pub text: String,
1168 pub selection: SelectionRange,
1169}
1170
1171#[derive(Debug, Clone, Serialize, Deserialize)]
1177#[serde(rename_all = "camelCase")]
1178pub struct UserInputRequest {
1179 pub question: String,
1180 #[serde(skip_serializing_if = "Option::is_none")]
1181 pub choices: Option<Vec<String>>,
1182 #[serde(skip_serializing_if = "Option::is_none")]
1183 pub allow_freeform: Option<bool>,
1184}
1185
1186#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1188#[serde(rename_all = "camelCase")]
1189pub struct UserInputResponse {
1190 #[serde(default)]
1191 pub answer: String,
1192 #[serde(default, skip_serializing_if = "Option::is_none")]
1193 pub was_freeform: Option<bool>,
1194}
1195
1196#[derive(Debug, Clone, Serialize, Deserialize)]
1198#[serde(rename_all = "camelCase")]
1199pub struct UserInputInvocation {
1200 pub session_id: String,
1201}
1202
1203pub mod session_lifecycle_event_types {
1209 pub const CREATED: &str = "session.created";
1210 pub const DELETED: &str = "session.deleted";
1211 pub const UPDATED: &str = "session.updated";
1212 pub const FOREGROUND: &str = "session.foreground";
1213 pub const BACKGROUND: &str = "session.background";
1214}
1215
1216#[derive(Debug, Clone, Deserialize)]
1218#[serde(rename_all = "camelCase")]
1219pub struct SessionLifecycleEventMetadata {
1220 #[serde(default)]
1221 pub start_time: Option<String>,
1222 #[serde(default)]
1223 pub modified_time: Option<String>,
1224 #[serde(default)]
1225 pub summary: Option<String>,
1226}
1227
1228#[derive(Debug, Clone, Deserialize)]
1230#[serde(rename_all = "camelCase")]
1231pub struct SessionLifecycleEvent {
1232 #[serde(rename = "type")]
1233 pub event_type: String,
1234 pub session_id: String,
1235 #[serde(default)]
1236 pub metadata: Option<SessionLifecycleEventMetadata>,
1237}
1238
1239#[derive(Debug, Clone, Default, Deserialize)]
1241#[serde(rename_all = "camelCase")]
1242pub struct GetForegroundSessionResponse {
1243 #[serde(default)]
1244 pub session_id: Option<String>,
1245 #[serde(default)]
1246 pub workspace_path: Option<String>,
1247}
1248
1249#[derive(Debug, Clone, Default, Deserialize)]
1251#[serde(rename_all = "camelCase")]
1252pub struct SetForegroundSessionResponse {
1253 #[serde(default)]
1254 pub success: bool,
1255 #[serde(default)]
1256 pub error: Option<String>,
1257}
1258
1259#[derive(Debug, Clone)]
1265pub struct StopError {
1266 pub message: String,
1267 pub source: Option<String>,
1268}
1269
1270impl std::fmt::Display for StopError {
1271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1272 write!(f, "{}", self.message)
1273 }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use super::*;
1279
1280 #[test]
1281 fn test_tool_result_text() {
1282 let result = ToolResult::text("Hello, world!");
1283 assert_eq!(result.text_result_for_llm, "Hello, world!");
1284 assert_eq!(result.result_type, "success");
1285 }
1286
1287 #[test]
1288 fn test_tool_result_error() {
1289 let result = ToolResult::error("Something went wrong");
1290 assert_eq!(result.result_type, "error");
1291 assert_eq!(result.error, Some("Something went wrong".to_string()));
1292 }
1293
1294 #[test]
1295 fn test_permission_result() {
1296 let approved = PermissionRequestResult::approved();
1297 assert_eq!(approved.kind, "approved");
1298 assert!(approved.is_approved());
1299 assert!(!approved.is_denied());
1300
1301 let denied = PermissionRequestResult::denied();
1302 assert!(denied.kind.starts_with("denied"));
1303 assert!(denied.is_denied());
1304 assert!(!denied.is_approved());
1305 }
1306
1307 #[test]
1308 fn test_message_options_from_str() {
1309 let opts: MessageOptions = "Hello".into();
1310 assert_eq!(opts.prompt, "Hello");
1311 }
1312
1313 #[test]
1314 fn test_session_config_default() {
1315 let config = SessionConfig::default();
1316 assert!(config.model.is_none());
1317 assert!(config.tools.is_empty());
1318 }
1319
1320 #[test]
1321 fn test_session_config_serialization_with_new_fields() {
1322 let config = SessionConfig {
1323 session_id: Some("sess-1".into()),
1324 model: Some("gpt-4.1".into()),
1325 config_dir: Some(PathBuf::from("/tmp/copilot")),
1326 streaming: true,
1327 skill_directories: Some(vec!["skills".into()]),
1328 disabled_skills: Some(vec!["legacy_skill".into()]),
1329 request_permission: Some(true),
1330 ..Default::default()
1331 };
1332
1333 let value = serde_json::to_value(&config).unwrap();
1334 assert_eq!(value["sessionId"], "sess-1");
1335 assert_eq!(value["model"], "gpt-4.1");
1336 assert_eq!(value["configDir"], "/tmp/copilot");
1337 assert_eq!(value["streaming"], true);
1338 assert_eq!(value["skillDirectories"][0], "skills");
1339 assert_eq!(value["disabledSkills"][0], "legacy_skill");
1340 assert_eq!(value["requestPermission"], true);
1341 }
1342
1343 #[test]
1344 fn test_tool_builder() {
1345 let tool = Tool::new("my_tool")
1346 .description("A test tool")
1347 .schema(serde_json::json!({"type": "object"}));
1348
1349 assert_eq!(tool.name, "my_tool");
1350 assert_eq!(tool.description, "A test tool");
1351 }
1352
1353 #[test]
1354 fn test_user_input_request_roundtrip() {
1355 let req = UserInputRequest {
1356 question: "What color?".into(),
1357 choices: Some(vec!["red".into(), "blue".into()]),
1358 allow_freeform: Some(true),
1359 };
1360 let j = serde_json::to_value(&req).unwrap();
1361 assert_eq!(j["question"], "What color?");
1362 assert_eq!(j["choices"][0], "red");
1363 assert_eq!(j["allowFreeform"], true);
1364
1365 let req2: UserInputRequest = serde_json::from_value(j).unwrap();
1366 assert_eq!(req2.question, "What color?");
1367 }
1368
1369 #[test]
1370 fn test_user_input_response_roundtrip() {
1371 let resp = UserInputResponse {
1372 answer: "blue".into(),
1373 was_freeform: Some(true),
1374 };
1375 let j = serde_json::to_value(&resp).unwrap();
1376 assert_eq!(j["answer"], "blue");
1377
1378 let resp2: UserInputResponse = serde_json::from_value(j).unwrap();
1379 assert_eq!(resp2.answer, "blue");
1380 assert_eq!(resp2.was_freeform, Some(true));
1381 }
1382
1383 #[test]
1384 fn test_user_input_request_minimal() {
1385 let j = serde_json::json!({"question": "Yes or no?"});
1386 let req: UserInputRequest = serde_json::from_value(j).unwrap();
1387 assert_eq!(req.question, "Yes or no?");
1388 assert!(req.choices.is_none());
1389 assert!(req.allow_freeform.is_none());
1390 }
1391
1392 #[test]
1393 fn test_session_lifecycle_event_from_json() {
1394 let j = serde_json::json!({
1395 "type": "session.created",
1396 "sessionId": "sess_123",
1397 "metadata": {
1398 "startTime": "2024-01-15T10:30:00Z",
1399 "modifiedTime": "2024-01-15T10:30:00Z",
1400 "summary": "Test session"
1401 }
1402 });
1403 let event: SessionLifecycleEvent = serde_json::from_value(j).unwrap();
1404 assert_eq!(event.event_type, session_lifecycle_event_types::CREATED);
1405 assert_eq!(event.session_id, "sess_123");
1406 assert_eq!(
1407 event.metadata.as_ref().unwrap().summary,
1408 Some("Test session".into())
1409 );
1410 }
1411
1412 #[test]
1413 fn test_get_foreground_session_response() {
1414 let j = serde_json::json!({"sessionId": "sess_123", "workspacePath": "/tmp"});
1415 let resp: GetForegroundSessionResponse = serde_json::from_value(j).unwrap();
1416 assert_eq!(resp.session_id, Some("sess_123".into()));
1417 assert_eq!(resp.workspace_path, Some("/tmp".into()));
1418 }
1419
1420 #[test]
1421 fn test_set_foreground_session_response() {
1422 let j = serde_json::json!({"success": true});
1423 let resp: SetForegroundSessionResponse = serde_json::from_value(j).unwrap();
1424 assert!(resp.success);
1425 assert!(resp.error.is_none());
1426 }
1427
1428 #[test]
1429 fn test_set_foreground_session_response_error() {
1430 let j = serde_json::json!({"success": false, "error": "not found"});
1431 let resp: SetForegroundSessionResponse = serde_json::from_value(j).unwrap();
1432 assert!(!resp.success);
1433 assert_eq!(resp.error, Some("not found".into()));
1434 }
1435
1436 #[test]
1437 fn test_selection_attachment_roundtrip() {
1438 let att = SelectionAttachment {
1439 file_path: "src/main.rs".into(),
1440 display_name: "main.rs".into(),
1441 text: "fn main()".into(),
1442 selection: SelectionRange {
1443 start: SelectionPosition {
1444 line: 1.0,
1445 character: 0.0,
1446 },
1447 end: SelectionPosition {
1448 line: 1.0,
1449 character: 9.0,
1450 },
1451 },
1452 };
1453 let j = serde_json::to_value(&att).unwrap();
1454 assert_eq!(j["filePath"], "src/main.rs");
1455 assert_eq!(j["selection"]["start"]["line"], 1.0);
1456 }
1457
1458 #[test]
1459 fn test_attachment_type_selection() {
1460 let j = serde_json::json!("selection");
1461 let at: AttachmentType = serde_json::from_value(j).unwrap();
1462 assert_eq!(at, AttachmentType::Selection);
1463 }
1464
1465 #[test]
1466 fn test_stop_error_display() {
1467 let err = StopError {
1468 message: "timeout".into(),
1469 source: Some("rpc".into()),
1470 };
1471 assert_eq!(format!("{err}"), "timeout");
1472 }
1473
1474 #[test]
1475 fn test_session_config_reasoning_effort() {
1476 let config = SessionConfig {
1477 reasoning_effort: Some("high".into()),
1478 ..Default::default()
1479 };
1480 let json = serde_json::to_value(&config).unwrap();
1481 assert_eq!(json["reasoningEffort"], "high");
1482 }
1483
1484 #[test]
1485 fn test_session_config_working_directory() {
1486 let config = SessionConfig {
1487 working_directory: Some("/home/user/project".into()),
1488 ..Default::default()
1489 };
1490 let json = serde_json::to_value(&config).unwrap();
1491 assert_eq!(json["workingDirectory"], "/home/user/project");
1492 }
1493
1494 #[test]
1495 fn test_resume_config_disable_resume() {
1496 let config = ResumeSessionConfig {
1497 disable_resume: true,
1498 ..Default::default()
1499 };
1500 let json = serde_json::to_value(&config).unwrap();
1501 assert_eq!(json["disableResume"], true);
1502 }
1503
1504 #[test]
1505 fn test_resume_config_model() {
1506 let config = ResumeSessionConfig {
1507 model: Some("gpt-4".into()),
1508 ..Default::default()
1509 };
1510 let json = serde_json::to_value(&config).unwrap();
1511 assert_eq!(json["model"], "gpt-4");
1512 }
1513
1514 #[test]
1515 fn test_session_hooks_has_any() {
1516 let hooks = SessionHooks::default();
1517 assert!(!hooks.has_any());
1518
1519 let hooks = SessionHooks {
1520 on_pre_tool_use: Some(Arc::new(|_| PreToolUseHookOutput::default())),
1521 ..Default::default()
1522 };
1523 assert!(hooks.has_any());
1524 }
1525
1526 #[test]
1527 fn test_session_hooks_debug() {
1528 let hooks = SessionHooks {
1529 on_pre_tool_use: Some(Arc::new(|_| PreToolUseHookOutput::default())),
1530 ..Default::default()
1531 };
1532 let debug = format!("{:?}", hooks);
1533 assert!(debug.contains("on_pre_tool_use: true"));
1534 assert!(debug.contains("on_post_tool_use: false"));
1535 }
1536
1537 #[test]
1538 fn test_pre_tool_use_hook_input_serde() {
1539 let json = serde_json::json!({
1540 "timestamp": 1234567890,
1541 "cwd": "/tmp",
1542 "toolName": "my_tool",
1543 "toolArgs": {"key": "value"}
1544 });
1545 let input: PreToolUseHookInput = serde_json::from_value(json).unwrap();
1546 assert_eq!(input.timestamp, 1234567890);
1547 assert_eq!(input.tool_name, "my_tool");
1548 }
1549
1550 #[test]
1551 fn test_pre_tool_use_hook_output_serde() {
1552 let output = PreToolUseHookOutput {
1553 permission_decision: Some("allow".into()),
1554 additional_context: Some("context".into()),
1555 ..Default::default()
1556 };
1557 let json = serde_json::to_value(&output).unwrap();
1558 assert_eq!(json["permissionDecision"], "allow");
1559 assert_eq!(json["additionalContext"], "context");
1560 assert!(json.get("suppressOutput").is_none());
1561 }
1562
1563 #[test]
1564 fn test_session_end_hook_input_serde() {
1565 let json = serde_json::json!({
1566 "timestamp": 1234567890,
1567 "cwd": "/tmp",
1568 "reason": "complete",
1569 "finalMessage": "Done"
1570 });
1571 let input: SessionEndHookInput = serde_json::from_value(json).unwrap();
1572 assert_eq!(input.reason, "complete");
1573 assert_eq!(input.final_message, Some("Done".into()));
1574 }
1575
1576 #[test]
1577 fn test_error_occurred_hook_input_serde() {
1578 let json = serde_json::json!({
1579 "timestamp": 1234567890,
1580 "cwd": "/tmp",
1581 "error": "connection failed",
1582 "errorContext": "model_call",
1583 "recoverable": true
1584 });
1585 let input: ErrorOccurredHookInput = serde_json::from_value(json).unwrap();
1586 assert_eq!(input.error_context, "model_call");
1587 assert!(input.recoverable);
1588 }
1589
1590 #[test]
1591 fn test_hooks_not_serialized_in_config() {
1592 let config = SessionConfig {
1593 hooks: Some(SessionHooks {
1594 on_pre_tool_use: Some(Arc::new(|_| PreToolUseHookOutput::default())),
1595 ..Default::default()
1596 }),
1597 ..Default::default()
1598 };
1599 let json = serde_json::to_value(&config).unwrap();
1600 assert!(json.get("hooks").is_none());
1602 }
1603}