Skip to main content

copilot_sdk/
types.rs

1// Copyright (c) 2026 Elias Bachaalany
2// SPDX-License-Identifier: MIT
3
4//! Core types for the Copilot SDK.
5
6use 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
15// =============================================================================
16// Protocol Version
17// =============================================================================
18
19/// SDK protocol version - must match copilot-agent-runtime server.
20pub const SDK_PROTOCOL_VERSION: u32 = 2;
21
22// =============================================================================
23// Enums
24// =============================================================================
25
26/// Connection state of the client.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum ConnectionState {
29    #[default]
30    Disconnected,
31    Connecting,
32    Connected,
33    Error,
34}
35
36/// System message mode for session configuration.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum SystemMessageMode {
40    Append,
41    Replace,
42}
43
44/// Attachment type for user messages.
45#[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/// Log level for the CLI.
54#[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// =============================================================================
79// Tool Types
80// =============================================================================
81
82/// Binary result from a tool execution.
83#[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/// Result object returned from tool execution.
95#[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    /// Create a success result with text.
117    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    /// Create an error result.
129    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
141/// Convenient alias for tool results.
142pub type ToolResult = ToolResultObject;
143
144/// Information about a tool invocation from the server.
145#[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    /// Get an argument by name, deserializing to the specified type.
157    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// =============================================================================
174// Permission Types
175// =============================================================================
176
177/// Permission request from the server.
178#[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/// Result of a permission request (response to CLI).
189#[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    /// Create an approved permission result.
199    pub fn approved() -> Self {
200        Self {
201            kind: "approved".to_string(),
202            rules: None,
203        }
204    }
205
206    /// Create a denied permission result.
207    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    /// Returns true if the permission was approved.
215    pub fn is_approved(&self) -> bool {
216        self.kind == "approved"
217    }
218
219    /// Returns true if the permission was denied.
220    pub fn is_denied(&self) -> bool {
221        self.kind.starts_with("denied")
222    }
223}
224
225// =============================================================================
226// Configuration Types
227// =============================================================================
228
229/// System message configuration.
230#[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/// Azure-specific provider options.
240#[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/// Provider configuration for BYOK (Bring Your Own Key).
248#[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
264// Environment variable names for BYOK configuration
265impl ProviderConfig {
266    /// Environment variable for API key
267    pub const ENV_API_KEY: &'static str = "COPILOT_SDK_BYOK_API_KEY";
268    /// Environment variable for base URL
269    pub const ENV_BASE_URL: &'static str = "COPILOT_SDK_BYOK_BASE_URL";
270    /// Environment variable for provider type
271    pub const ENV_PROVIDER_TYPE: &'static str = "COPILOT_SDK_BYOK_PROVIDER_TYPE";
272    /// Environment variable for model
273    pub const ENV_MODEL: &'static str = "COPILOT_SDK_BYOK_MODEL";
274
275    /// Check if BYOK environment variables are configured.
276    ///
277    /// Returns true if `COPILOT_SDK_BYOK_API_KEY` is set and non-empty.
278    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    /// Load ProviderConfig from `COPILOT_SDK_BYOK_*` environment variables.
285    ///
286    /// Returns `Some(ProviderConfig)` if API key is set, `None` otherwise.
287    ///
288    /// Environment variables:
289    /// - `COPILOT_SDK_BYOK_API_KEY` (required): API key for the provider
290    /// - `COPILOT_SDK_BYOK_BASE_URL` (optional): Base URL (defaults to OpenAI)
291    /// - `COPILOT_SDK_BYOK_PROVIDER_TYPE` (optional): Provider type (defaults to "openai")
292    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    /// Load model from `COPILOT_SDK_BYOK_MODEL` environment variable.
315    ///
316    /// Returns `Some(model)` if set and non-empty, `None` otherwise.
317    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// =============================================================================
325// MCP Server Configuration
326// =============================================================================
327
328/// Configuration for a local/stdio MCP server.
329#[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/// Configuration for a remote MCP server (HTTP or SSE).
347#[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/// MCP server configuration (either local or remote).
365#[derive(Debug, Clone, Serialize, Deserialize)]
366#[serde(untagged)]
367pub enum McpServerConfig {
368    Local(McpLocalServerConfig),
369    Remote(McpRemoteServerConfig),
370}
371
372// =============================================================================
373// Custom Agent Configuration
374// =============================================================================
375
376/// Configuration for a custom agent.
377#[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// =============================================================================
395// Attachment Types
396// =============================================================================
397
398/// Attachment item for user messages.
399#[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// =============================================================================
409// Tool Definition (SDK-side)
410// =============================================================================
411
412/// Tool definition for registration with a session.
413///
414/// Use the builder pattern to create tools:
415/// ```no_run
416/// use copilot_sdk::{Client, SessionConfig, Tool, ToolHandler, ToolResultObject};
417/// use std::sync::Arc;
418///
419/// #[tokio::main]
420/// async fn main() -> copilot_sdk::Result<()> {
421/// let client = Client::builder().build()?;
422/// client.start().await?;
423///
424/// let tool = Tool::new("get_weather")
425///     .description("Get weather for a city")
426///     .schema(serde_json::json!({
427///         "type": "object",
428///         "properties": { "city": { "type": "string" } },
429///         "required": ["city"]
430///     }));
431///
432/// let session = client.create_session(SessionConfig {
433///     tools: vec![tool.clone()],
434///     ..Default::default()
435/// }).await?;
436///
437/// let handler: ToolHandler = Arc::new(|_name, args| {
438///     let city = args.get("city").and_then(|v| v.as_str()).unwrap_or("unknown");
439///     ToolResultObject::text(format!("Weather in {}: sunny", city))
440/// });
441/// session.register_tool_with_handler(tool, Some(handler)).await;
442/// client.stop().await;
443/// # Ok(())
444/// # }
445/// ```
446#[derive(Clone)]
447pub struct Tool {
448    pub name: String,
449    pub description: String,
450    pub parameters_schema: serde_json::Value,
451    // Handler is stored separately in Session since it's not Clone-friendly
452}
453
454impl Tool {
455    /// Create a new tool with the given name.
456    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    /// Set the tool description.
465    pub fn description(mut self, desc: impl Into<String>) -> Self {
466        self.description = desc.into();
467        self
468    }
469
470    /// Set the parameters JSON schema.
471    pub fn schema(mut self, schema: serde_json::Value) -> Self {
472        self.parameters_schema = schema;
473        self
474    }
475
476    /// Add a parameter to the tool's JSON schema.
477    ///
478    /// Builds the schema incrementally using the builder pattern.
479    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        // Ensure schema has the right shape
489        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    /// Derive the parameters JSON schema from a Rust type (requires the `schemars` feature).
514    #[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
537// Serialization for sending tool definitions to the CLI
538impl 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// =============================================================================
553// Infinite Session Configuration
554// =============================================================================
555
556/// Configuration for infinite sessions (automatic context compaction).
557///
558/// When enabled, the SDK will automatically manage conversation context to prevent
559/// buffer exhaustion. Thresholds are expressed as fractions (0.0 to 1.0).
560#[derive(Debug, Clone, Default, Serialize, Deserialize)]
561#[serde(rename_all = "camelCase")]
562pub struct InfiniteSessionConfig {
563    /// Enable infinite sessions.
564    #[serde(skip_serializing_if = "Option::is_none")]
565    pub enabled: Option<bool>,
566    /// Threshold for background compaction (0.0 to 1.0).
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub background_compaction_threshold: Option<f64>,
569    /// Threshold for buffer exhaustion handling (0.0 to 1.0).
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub buffer_exhaustion_threshold: Option<f64>,
572}
573
574impl InfiniteSessionConfig {
575    /// Create an enabled infinite session config with default thresholds.
576    pub fn enabled() -> Self {
577        Self {
578            enabled: Some(true),
579            background_compaction_threshold: None,
580            buffer_exhaustion_threshold: None,
581        }
582    }
583
584    /// Create an infinite session config with custom thresholds.
585    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// =============================================================================
595// Session Hooks
596// =============================================================================
597
598/// Input for the pre-tool-use hook.
599#[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/// Output for the pre-tool-use hook.
609#[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/// Input for the post-tool-use hook.
625#[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/// Output for the post-tool-use hook.
636#[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/// Input for the user-prompt-submitted hook.
648#[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/// Output for the user-prompt-submitted hook.
657#[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/// Input for the session-start hook.
669#[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/// Output for the session-start hook.
680#[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/// Input for the session-end hook.
690#[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/// Output for the session-end hook.
703#[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/// Input for the error-occurred hook.
715#[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/// Output for the error-occurred hook.
726#[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
739/// Handler types for session hooks.
740pub 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/// Configuration for session hooks.
752///
753/// Hooks allow intercepting and modifying behavior at key points in the session lifecycle.
754#[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    /// Returns true if any hook handler is registered.
766    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// =============================================================================
793// Session Configuration
794// =============================================================================
795
796/// Configuration for creating a new session.
797#[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    /// Infinite session configuration for automatic context compaction.
829    #[serde(skip_serializing_if = "Option::is_none")]
830    pub infinite_sessions: Option<InfiniteSessionConfig>,
831
832    /// Whether to request user input forwarding from the server.
833    /// When true, `userInput.request` callbacks will be sent to the SDK.
834    #[serde(skip_serializing_if = "Option::is_none", rename = "requestUserInput")]
835    pub request_user_input: Option<bool>,
836
837    /// Reasoning effort level: "low", "medium", "high", or "xhigh".
838    #[serde(skip_serializing_if = "Option::is_none")]
839    pub reasoning_effort: Option<String>,
840
841    /// Working directory for the session.
842    #[serde(skip_serializing_if = "Option::is_none")]
843    pub working_directory: Option<String>,
844
845    /// Session hooks for pre/post tool use, session lifecycle, etc.
846    #[serde(skip)]
847    pub hooks: Option<SessionHooks>,
848
849    /// If true and provider/model not explicitly set, load from `COPILOT_SDK_BYOK_*` env vars.
850    ///
851    /// Default: false (explicit configuration preferred over environment variables)
852    #[serde(skip)]
853    pub auto_byok_from_env: bool,
854}
855
856/// Configuration for resuming an existing session.
857#[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    /// Whether to request user input forwarding from the server.
880    #[serde(skip_serializing_if = "Option::is_none", rename = "requestUserInput")]
881    pub request_user_input: Option<bool>,
882
883    /// Reasoning effort level: "low", "medium", "high", or "xhigh".
884    #[serde(skip_serializing_if = "Option::is_none")]
885    pub reasoning_effort: Option<String>,
886
887    /// Working directory for the session.
888    #[serde(skip_serializing_if = "Option::is_none")]
889    pub working_directory: Option<String>,
890
891    /// If true, skip resuming and create a new session instead.
892    #[serde(default, skip_serializing_if = "is_false")]
893    pub disable_resume: bool,
894
895    /// Infinite session configuration for resumed sessions
896    #[serde(skip_serializing_if = "Option::is_none")]
897    pub infinite_sessions: Option<InfiniteSessionConfig>,
898
899    /// Session hooks for pre/post tool use, session lifecycle, etc.
900    #[serde(skip)]
901    pub hooks: Option<SessionHooks>,
902
903    /// If true and provider not explicitly set, load from `COPILOT_SDK_BYOK_*` env vars.
904    ///
905    /// Default: false (explicit configuration preferred over environment variables)
906    #[serde(skip)]
907    pub auto_byok_from_env: bool,
908}
909
910/// Options for sending a message.
911#[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// =============================================================================
942// Client Options
943// =============================================================================
944
945/// Options for creating a CopilotClient.
946#[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    /// GitHub personal access token for authentication.
959    /// Cannot be used together with `cli_url`.
960    pub github_token: Option<String>,
961    /// Whether to use the logged-in user for auth.
962    /// Defaults to true when github_token is empty. Cannot be used with `cli_url`.
963    pub use_logged_in_user: Option<bool>,
964
965    /// Tool specifications to deny (passed as `--deny-tool` arguments to the CLI).
966    ///
967    /// Each entry follows the CLI's tool specification format:
968    /// - `"shell(git push)"` — deny a specific shell command
969    /// - `"shell(git)"` — deny all git commands
970    /// - `"shell(rm)"` — deny rm commands
971    /// - `"shell"` — deny all shell commands
972    /// - `"write"` — deny file write operations
973    /// - `"MCP_SERVER(tool_name)"` — deny a specific MCP tool
974    ///
975    /// `--deny-tool` takes precedence over `--allow-tool` and `--allow-all-tools`.
976    pub deny_tools: Option<Vec<String>>,
977
978    /// Tool specifications to allow without manual approval
979    /// (passed as `--allow-tool` arguments to the CLI).
980    ///
981    /// Each entry follows the same format as `deny_tools`.
982    pub allow_tools: Option<Vec<String>>,
983
984    /// If true, passes `--allow-all-tools` to the CLI.
985    ///
986    /// This allows Copilot to use any tool without asking for approval.
987    /// Use `deny_tools` in combination to create an allowlist with exceptions.
988    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// =============================================================================
1014// Response Types
1015// =============================================================================
1016
1017/// Metadata about a session.
1018#[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/// Response from a ping request.
1033#[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/// Response from status.get request.
1043#[derive(Debug, Clone, Deserialize)]
1044#[serde(rename_all = "camelCase")]
1045pub struct GetStatusResponse {
1046    pub version: String,
1047    pub protocol_version: u32,
1048}
1049
1050/// Response from auth.getStatus request.
1051#[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/// Model capabilities - what the model supports.
1066#[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/// What features a model supports.
1076#[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/// Vision limits for a model.
1086#[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/// Model limits.
1098#[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/// Model policy state.
1110#[derive(Debug, Clone, Deserialize)]
1111pub struct ModelPolicy {
1112    pub state: String,
1113    #[serde(default)]
1114    pub terms: String,
1115}
1116
1117/// Model billing information.
1118#[derive(Debug, Clone, Deserialize)]
1119pub struct ModelBilling {
1120    #[serde(default)]
1121    pub multiplier: f64,
1122}
1123
1124/// Information about an available model.
1125#[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// =============================================================================
1142// Selection Attachment Types
1143// =============================================================================
1144
1145/// Position in a text document (line + character).
1146#[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/// Range within a text document.
1155#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1156pub struct SelectionRange {
1157    pub start: SelectionPosition,
1158    pub end: SelectionPosition,
1159}
1160
1161/// Attachment representing a text selection in a file.
1162#[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// =============================================================================
1172// User Input Types
1173// =============================================================================
1174
1175/// Request for user input from the server.
1176#[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/// Response to a user input request.
1187#[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/// Context for a user input invocation.
1197#[derive(Debug, Clone, Serialize, Deserialize)]
1198#[serde(rename_all = "camelCase")]
1199pub struct UserInputInvocation {
1200    pub session_id: String,
1201}
1202
1203// =============================================================================
1204// Session Lifecycle Types
1205// =============================================================================
1206
1207/// Session lifecycle event type constants.
1208pub 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/// Metadata for session lifecycle events.
1217#[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/// Session lifecycle event notification.
1229#[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/// Response from session.getForeground.
1240#[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/// Response from session.setForeground.
1250#[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// =============================================================================
1260// Stop Error
1261// =============================================================================
1262
1263/// Error collected during client stop.
1264#[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        // hooks field should be skipped from serialization
1601        assert!(json.get("hooks").is_none());
1602    }
1603}