Skip to main content

github_copilot_sdk/
types.rs

1//! Protocol types shared between the SDK and the GitHub Copilot CLI.
2//!
3//! These types map directly to the JSON-RPC request/response payloads
4//! defined by the GitHub Copilot CLI protocol. They are used for session
5//! configuration, event handling, tool invocations, and model queries.
6
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use crate::handler::SessionHandler;
16use crate::hooks::SessionHooks;
17pub use crate::session_fs::{
18    DirEntry, DirEntryKind, FileInfo, FsError, SessionFsConfig, SessionFsConventions,
19    SessionFsProvider,
20};
21pub use crate::trace_context::{TraceContext, TraceContextProvider};
22use crate::transforms::SystemMessageTransform;
23
24/// Lifecycle state of a [`Client`](crate::Client) connection to the CLI.
25///
26/// The state advances from `Connecting` → `Connected` during construction,
27/// transitions to `Disconnected` after [`Client::stop`](crate::Client::stop) or
28/// [`Client::force_stop`](crate::Client::force_stop), and lands in
29/// `Error` if startup fails or the underlying transport tears down
30/// unexpectedly.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33#[non_exhaustive]
34pub enum ConnectionState {
35    /// No CLI process is attached or the process has exited cleanly.
36    Disconnected,
37    /// The client is starting up (spawning the CLI, negotiating protocol).
38    Connecting,
39    /// The client is connected and ready to handle RPC traffic.
40    Connected,
41    /// Startup failed or the connection encountered an unrecoverable error.
42    Error,
43}
44
45/// Type of [`SessionLifecycleEvent`] received via [`Client::subscribe_lifecycle`](crate::Client::subscribe_lifecycle).
46///
47/// Values serialize as the dotted JSON strings the CLI sends (e.g.
48/// `"session.created"`).
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50#[non_exhaustive]
51pub enum SessionLifecycleEventType {
52    /// A new session was created.
53    #[serde(rename = "session.created")]
54    Created,
55    /// A session was deleted.
56    #[serde(rename = "session.deleted")]
57    Deleted,
58    /// A session's metadata was updated (e.g. summary regenerated).
59    #[serde(rename = "session.updated")]
60    Updated,
61    /// A session moved into the foreground.
62    #[serde(rename = "session.foreground")]
63    Foreground,
64    /// A session moved into the background.
65    #[serde(rename = "session.background")]
66    Background,
67}
68
69/// Optional metadata attached to a [`SessionLifecycleEvent`].
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct SessionLifecycleEventMetadata {
72    /// ISO-8601 timestamp the session was created.
73    #[serde(rename = "startTime")]
74    pub start_time: String,
75    /// ISO-8601 timestamp the session was last modified.
76    #[serde(rename = "modifiedTime")]
77    pub modified_time: String,
78    /// Optional generated summary of the session conversation so far.
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub summary: Option<String>,
81}
82
83/// A `session.lifecycle` notification dispatched to subscribers obtained via
84/// [`Client::subscribe_lifecycle`](crate::Client::subscribe_lifecycle).
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct SessionLifecycleEvent {
87    /// The kind of lifecycle change this event represents.
88    #[serde(rename = "type")]
89    pub event_type: SessionLifecycleEventType,
90    /// Identifier of the session this event refers to.
91    #[serde(rename = "sessionId")]
92    pub session_id: SessionId,
93    /// Optional metadata describing the session at the time of the event.
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub metadata: Option<SessionLifecycleEventMetadata>,
96}
97
98/// Opaque session identifier assigned by the CLI.
99///
100/// A newtype wrapper around `String` that provides type safety — prevents
101/// accidentally passing a workspace ID or request ID where a session ID
102/// is expected. Derefs to `str` for zero-friction borrowing.
103#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
104#[serde(transparent)]
105pub struct SessionId(String);
106
107impl SessionId {
108    /// Create a new session ID from any string-like value.
109    pub fn new(id: impl Into<String>) -> Self {
110        Self(id.into())
111    }
112
113    /// Borrow the inner string.
114    pub fn as_str(&self) -> &str {
115        &self.0
116    }
117
118    /// Consume the wrapper, returning the inner string.
119    pub fn into_inner(self) -> String {
120        self.0
121    }
122}
123
124impl std::ops::Deref for SessionId {
125    type Target = str;
126
127    fn deref(&self) -> &str {
128        &self.0
129    }
130}
131
132impl std::fmt::Display for SessionId {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        f.write_str(&self.0)
135    }
136}
137
138impl From<String> for SessionId {
139    fn from(s: String) -> Self {
140        Self(s)
141    }
142}
143
144impl From<&str> for SessionId {
145    fn from(s: &str) -> Self {
146        Self(s.to_owned())
147    }
148}
149
150impl AsRef<str> for SessionId {
151    fn as_ref(&self) -> &str {
152        &self.0
153    }
154}
155
156impl std::borrow::Borrow<str> for SessionId {
157    fn borrow(&self) -> &str {
158        &self.0
159    }
160}
161
162impl From<SessionId> for String {
163    fn from(id: SessionId) -> String {
164        id.0
165    }
166}
167
168impl PartialEq<str> for SessionId {
169    fn eq(&self, other: &str) -> bool {
170        self.0 == other
171    }
172}
173
174impl PartialEq<String> for SessionId {
175    fn eq(&self, other: &String) -> bool {
176        &self.0 == other
177    }
178}
179
180impl PartialEq<SessionId> for String {
181    fn eq(&self, other: &SessionId) -> bool {
182        self == &other.0
183    }
184}
185
186impl PartialEq<&str> for SessionId {
187    fn eq(&self, other: &&str) -> bool {
188        self.0 == *other
189    }
190}
191
192impl PartialEq<&SessionId> for SessionId {
193    fn eq(&self, other: &&SessionId) -> bool {
194        self.0 == other.0
195    }
196}
197
198impl PartialEq<SessionId> for &SessionId {
199    fn eq(&self, other: &SessionId) -> bool {
200        self.0 == other.0
201    }
202}
203
204/// Opaque request identifier for pending CLI requests (permission, user-input, etc.).
205///
206/// A newtype wrapper around `String` that provides type safety — prevents
207/// accidentally passing a session ID or workspace ID where a request ID
208/// is expected. Derefs to `str` for zero-friction borrowing.
209#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
210#[serde(transparent)]
211pub struct RequestId(String);
212
213impl RequestId {
214    /// Create a new request ID from any string-like value.
215    pub fn new(id: impl Into<String>) -> Self {
216        Self(id.into())
217    }
218
219    /// Consume the wrapper, returning the inner string.
220    pub fn into_inner(self) -> String {
221        self.0
222    }
223}
224
225impl std::ops::Deref for RequestId {
226    type Target = str;
227
228    fn deref(&self) -> &str {
229        &self.0
230    }
231}
232
233impl std::fmt::Display for RequestId {
234    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235        f.write_str(&self.0)
236    }
237}
238
239impl From<String> for RequestId {
240    fn from(s: String) -> Self {
241        Self(s)
242    }
243}
244
245impl From<&str> for RequestId {
246    fn from(s: &str) -> Self {
247        Self(s.to_owned())
248    }
249}
250
251impl AsRef<str> for RequestId {
252    fn as_ref(&self) -> &str {
253        &self.0
254    }
255}
256
257impl std::borrow::Borrow<str> for RequestId {
258    fn borrow(&self) -> &str {
259        &self.0
260    }
261}
262
263impl From<RequestId> for String {
264    fn from(id: RequestId) -> String {
265        id.0
266    }
267}
268
269impl PartialEq<str> for RequestId {
270    fn eq(&self, other: &str) -> bool {
271        self.0 == other
272    }
273}
274
275impl PartialEq<String> for RequestId {
276    fn eq(&self, other: &String) -> bool {
277        &self.0 == other
278    }
279}
280
281impl PartialEq<RequestId> for String {
282    fn eq(&self, other: &RequestId) -> bool {
283        self == &other.0
284    }
285}
286
287impl PartialEq<&str> for RequestId {
288    fn eq(&self, other: &&str) -> bool {
289        self.0 == *other
290    }
291}
292
293/// A tool that the client exposes to the Copilot agent.
294///
295/// Sent to the CLI as part of [`SessionConfig::tools`] / [`ResumeSessionConfig::tools`]
296/// at session creation/resume time. The Rust SDK hand-authors this struct
297/// (rather than using the schema-generated form) so it can carry runtime
298/// hints — `overrides_built_in_tool`, `skip_permission` — that don't appear
299/// in the wire schema but are honored by the CLI.
300#[derive(Debug, Clone, Default, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302#[non_exhaustive]
303pub struct Tool {
304    /// Tool identifier (e.g., `"bash"`, `"grep"`, `"str_replace_editor"`).
305    pub name: String,
306    /// Optional namespaced name for declarative filtering (e.g., `"playwright/navigate"`
307    /// for MCP tools).
308    #[serde(default, skip_serializing_if = "Option::is_none")]
309    pub namespaced_name: Option<String>,
310    /// Description of what the tool does.
311    #[serde(default)]
312    pub description: String,
313    /// Optional instructions for how to use this tool effectively.
314    #[serde(default, skip_serializing_if = "Option::is_none")]
315    pub instructions: Option<String>,
316    /// JSON Schema for the tool's input parameters.
317    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
318    pub parameters: HashMap<String, Value>,
319    /// When `true`, this tool replaces a built-in tool of the same name
320    /// (e.g. supplying a custom `grep` that the agent uses in place of the
321    /// CLI's built-in implementation).
322    #[serde(default, skip_serializing_if = "is_false")]
323    pub overrides_built_in_tool: bool,
324    /// When `true`, the CLI does not request permission before invoking
325    /// this tool. Use with caution — the tool is responsible for any
326    /// access control.
327    #[serde(default, skip_serializing_if = "is_false")]
328    pub skip_permission: bool,
329}
330
331#[inline]
332fn is_false(b: &bool) -> bool {
333    !*b
334}
335
336impl Tool {
337    /// Construct a new [`Tool`] with the given name and otherwise default
338    /// values. The struct is `#[non_exhaustive]`, so external callers
339    /// cannot use struct-literal syntax — use this builder or
340    /// [`Default::default`] plus mut-let.
341    ///
342    /// # Example
343    ///
344    /// ```
345    /// # use github_copilot_sdk::types::Tool;
346    /// # use serde_json::json;
347    /// let tool = Tool::new("greet")
348    ///     .with_description("Say hello to a user")
349    ///     .with_parameters(json!({
350    ///         "type": "object",
351    ///         "properties": { "name": { "type": "string" } },
352    ///         "required": ["name"]
353    ///     }));
354    /// # let _ = tool;
355    /// ```
356    pub fn new(name: impl Into<String>) -> Self {
357        Self {
358            name: name.into(),
359            ..Default::default()
360        }
361    }
362
363    /// Set the namespaced name for declarative filtering (e.g.
364    /// `"playwright/navigate"` for MCP tools).
365    pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
366        self.namespaced_name = Some(namespaced_name.into());
367        self
368    }
369
370    /// Set the human-readable description of what the tool does.
371    pub fn with_description(mut self, description: impl Into<String>) -> Self {
372        self.description = description.into();
373        self
374    }
375
376    /// Set optional instructions for how to use this tool effectively.
377    pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
378        self.instructions = Some(instructions.into());
379        self
380    }
381
382    /// Set the JSON Schema for the tool's input parameters.
383    ///
384    /// Accepts anything that converts into a JSON object, including a
385    /// `serde_json::Value` produced by `json!({...})`. Non-object values
386    /// are stored as an empty parameter map; callers that need direct
387    /// control over the field can construct a `HashMap<String, Value>`
388    /// and assign it to [`Tool::parameters`] via [`Default::default`].
389    pub fn with_parameters(mut self, parameters: Value) -> Self {
390        self.parameters = parameters
391            .as_object()
392            .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
393            .unwrap_or_default();
394        self
395    }
396
397    /// Mark this tool as overriding a built-in tool of the same name.
398    /// E.g. supplying a custom `grep` that the agent uses in place of the
399    /// CLI's built-in implementation.
400    pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
401        self.overrides_built_in_tool = overrides;
402        self
403    }
404
405    /// When `true`, the CLI will not request permission before invoking
406    /// this tool. Use with caution — the tool is responsible for any
407    /// access control.
408    pub fn with_skip_permission(mut self, skip: bool) -> Self {
409        self.skip_permission = skip;
410        self
411    }
412}
413
414/// Context passed to a [`CommandHandler`] when a registered slash command
415/// is executed by the user.
416#[non_exhaustive]
417#[derive(Debug, Clone)]
418pub struct CommandContext {
419    /// Session ID where the command was invoked.
420    pub session_id: SessionId,
421    /// The full command text (e.g. `"/deploy production"`).
422    pub command: String,
423    /// Command name without the leading `/` (e.g. `"deploy"`).
424    pub command_name: String,
425    /// Raw argument string after the command name (e.g. `"production"`).
426    pub args: String,
427}
428
429/// Handler invoked when a registered slash command is executed.
430///
431/// Returning `Err(_)` causes the SDK to forward the error message back to
432/// the CLI via `session.commands.handlePendingCommand` so the TUI can
433/// surface it. Returning `Ok(())` reports success.
434#[async_trait::async_trait]
435pub trait CommandHandler: Send + Sync {
436    /// Called when the user invokes the command this handler is registered for.
437    async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
438}
439
440/// Definition of a slash command registered with the session.
441///
442/// When the CLI is running with a TUI, registered commands appear as
443/// `/name` for the user to invoke. Only `name` and `description` are sent
444/// over the wire — the handler is local to this SDK process.
445#[non_exhaustive]
446#[derive(Clone)]
447pub struct CommandDefinition {
448    /// Command name (without leading `/`).
449    pub name: String,
450    /// Human-readable description shown in command-completion UI.
451    pub description: Option<String>,
452    /// Handler invoked when the command is executed.
453    pub handler: Arc<dyn CommandHandler>,
454}
455
456impl CommandDefinition {
457    /// Construct a new command definition. Use [`with_description`](Self::with_description)
458    /// to add a description.
459    pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
460        Self {
461            name: name.into(),
462            description: None,
463            handler,
464        }
465    }
466
467    /// Set the human-readable description shown in the CLI's command-completion UI.
468    pub fn with_description(mut self, description: impl Into<String>) -> Self {
469        self.description = Some(description.into());
470        self
471    }
472}
473
474impl std::fmt::Debug for CommandDefinition {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        f.debug_struct("CommandDefinition")
477            .field("name", &self.name)
478            .field("description", &self.description)
479            .field("handler", &"<set>")
480            .finish()
481    }
482}
483
484impl Serialize for CommandDefinition {
485    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
486        use serde::ser::SerializeStruct;
487        let len = if self.description.is_some() { 2 } else { 1 };
488        let mut state = serializer.serialize_struct("CommandDefinition", len)?;
489        state.serialize_field("name", &self.name)?;
490        if let Some(description) = &self.description {
491            state.serialize_field("description", description)?;
492        }
493        state.end()
494    }
495}
496
497/// Configures a custom agent (sub-agent) for the session.
498///
499/// Custom agents have their own prompt, tool allowlist, and optionally
500/// their own MCP servers and skill set. The agent named in
501/// [`SessionConfig::agent`] (or the runtime default) is the active one
502/// when the session starts.
503#[derive(Debug, Clone, Default, Serialize, Deserialize)]
504#[serde(rename_all = "camelCase")]
505#[non_exhaustive]
506pub struct CustomAgentConfig {
507    /// Unique name of the custom agent.
508    pub name: String,
509    /// Display name for UI purposes.
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    pub display_name: Option<String>,
512    /// Description of what the agent does.
513    #[serde(default, skip_serializing_if = "Option::is_none")]
514    pub description: Option<String>,
515    /// List of tool names the agent can use. `None` means all tools.
516    #[serde(default, skip_serializing_if = "Option::is_none")]
517    pub tools: Option<Vec<String>>,
518    /// Prompt content for the agent.
519    pub prompt: String,
520    /// MCP servers specific to this agent.
521    #[serde(default, skip_serializing_if = "Option::is_none")]
522    pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
523    /// Whether the agent is available for model inference.
524    #[serde(default, skip_serializing_if = "Option::is_none")]
525    pub infer: Option<bool>,
526    /// Skill names to preload into this agent's context at startup.
527    #[serde(default, skip_serializing_if = "Option::is_none")]
528    pub skills: Option<Vec<String>>,
529}
530
531impl CustomAgentConfig {
532    /// Construct a custom agent configuration with the required `name`
533    /// and `prompt` fields populated.
534    ///
535    /// All other fields default to unset; use the `with_*` chain to
536    /// customize them. Fields are also `pub` if direct assignment is
537    /// preferred for `Option<T>` pass-through.
538    pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
539        Self {
540            name: name.into(),
541            prompt: prompt.into(),
542            ..Self::default()
543        }
544    }
545
546    /// Set the display name shown in the CLI's agent-selection UI.
547    pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
548        self.display_name = Some(display_name.into());
549        self
550    }
551
552    /// Set the description of what the agent does.
553    pub fn with_description(mut self, description: impl Into<String>) -> Self {
554        self.description = Some(description.into());
555        self
556    }
557
558    /// Restrict the agent to a specific tool allowlist. When unset, the
559    /// agent inherits the parent session's tool set.
560    pub fn with_tools<I, S>(mut self, tools: I) -> Self
561    where
562        I: IntoIterator<Item = S>,
563        S: Into<String>,
564    {
565        self.tools = Some(tools.into_iter().map(Into::into).collect());
566        self
567    }
568
569    /// Configure agent-specific MCP servers.
570    pub fn with_mcp_servers(mut self, mcp_servers: HashMap<String, McpServerConfig>) -> Self {
571        self.mcp_servers = Some(mcp_servers);
572        self
573    }
574
575    /// Whether the agent participates in model inference.
576    pub fn with_infer(mut self, infer: bool) -> Self {
577        self.infer = Some(infer);
578        self
579    }
580
581    /// Set the skills preloaded into the agent's context at startup.
582    pub fn with_skills<I, S>(mut self, skills: I) -> Self
583    where
584        I: IntoIterator<Item = S>,
585        S: Into<String>,
586    {
587        self.skills = Some(skills.into_iter().map(Into::into).collect());
588        self
589    }
590}
591
592/// Configures the default (built-in) agent that handles turns when no
593/// custom agent is selected.
594///
595/// Use [`Self::excluded_tools`] to hide tools from the default agent
596/// while keeping them available to custom sub-agents that list them in
597/// their [`CustomAgentConfig::tools`].
598#[derive(Debug, Clone, Default, Serialize, Deserialize)]
599#[serde(rename_all = "camelCase")]
600pub struct DefaultAgentConfig {
601    /// Tool names to exclude from the default agent.
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    pub excluded_tools: Option<Vec<String>>,
604}
605
606/// Configures infinite sessions: persistent workspaces with automatic
607/// context-window compaction.
608///
609/// When enabled (default), sessions automatically manage context limits
610/// through background compaction and persist state to a workspace
611/// directory.
612#[derive(Debug, Clone, Default, Serialize, Deserialize)]
613#[serde(rename_all = "camelCase")]
614#[non_exhaustive]
615pub struct InfiniteSessionConfig {
616    /// Whether infinite sessions are enabled. Defaults to `true` on the CLI.
617    #[serde(default, skip_serializing_if = "Option::is_none")]
618    pub enabled: Option<bool>,
619    /// Context utilization (0.0–1.0) at which background compaction starts.
620    /// Default: 0.80.
621    #[serde(default, skip_serializing_if = "Option::is_none")]
622    pub background_compaction_threshold: Option<f64>,
623    /// Context utilization (0.0–1.0) at which the session blocks until
624    /// compaction completes. Default: 0.95.
625    #[serde(default, skip_serializing_if = "Option::is_none")]
626    pub buffer_exhaustion_threshold: Option<f64>,
627}
628
629impl InfiniteSessionConfig {
630    /// Construct an empty [`InfiniteSessionConfig`]; all fields default to
631    /// unset (the CLI applies its own defaults).
632    pub fn new() -> Self {
633        Self::default()
634    }
635
636    /// Toggle infinite sessions on or off. Defaults to `true` on the CLI
637    /// when unset.
638    pub fn with_enabled(mut self, enabled: bool) -> Self {
639        self.enabled = Some(enabled);
640        self
641    }
642
643    /// Set the context utilization (0.0–1.0) at which background
644    /// compaction starts.
645    pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
646        self.background_compaction_threshold = Some(threshold);
647        self
648    }
649
650    /// Set the context utilization (0.0–1.0) at which the session blocks
651    /// until compaction completes.
652    pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
653        self.buffer_exhaustion_threshold = Some(threshold);
654        self
655    }
656}
657
658/// Configuration for a single MCP server.
659///
660/// MCP (Model Context Protocol) servers expose external tools to the
661/// agent. Local servers run as a subprocess over stdio; remote servers
662/// speak HTTP or Server-Sent Events.
663///
664/// Serialized as a JSON object with a `type` discriminator (`"stdio"` |
665/// `"http"` | `"sse"`).
666///
667/// # Example
668///
669/// ```
670/// # use github_copilot_sdk::types::{McpServerConfig, McpStdioServerConfig, McpHttpServerConfig};
671/// # use std::collections::HashMap;
672/// let mut servers = HashMap::new();
673/// servers.insert(
674///     "playwright".to_string(),
675///     McpServerConfig::Stdio(McpStdioServerConfig {
676///         tools: vec!["*".to_string()],
677///         command: "npx".to_string(),
678///         args: vec!["-y".to_string(), "@playwright/mcp".to_string()],
679///         ..Default::default()
680///     }),
681/// );
682/// servers.insert(
683///     "weather".to_string(),
684///     McpServerConfig::Http(McpHttpServerConfig {
685///         tools: vec!["forecast".to_string()],
686///         url: "https://example.com/mcp".to_string(),
687///         ..Default::default()
688///     }),
689/// );
690/// ```
691#[derive(Debug, Clone, Serialize, Deserialize)]
692#[serde(tag = "type", rename_all = "lowercase")]
693#[non_exhaustive]
694pub enum McpServerConfig {
695    /// Local MCP server launched as a subprocess and addressed over stdio.
696    /// On the wire this serializes as `{"type": "stdio", ...}`. The CLI
697    /// also accepts `"local"` as an alias on input.
698    #[serde(alias = "local")]
699    Stdio(McpStdioServerConfig),
700    /// Remote MCP server addressed over HTTP.
701    Http(McpHttpServerConfig),
702    /// Remote MCP server addressed over Server-Sent Events.
703    Sse(McpHttpServerConfig),
704}
705
706/// Configuration for a local/stdio MCP server.
707///
708/// See [`McpServerConfig::Stdio`].
709#[derive(Debug, Clone, Default, Serialize, Deserialize)]
710#[serde(rename_all = "camelCase")]
711pub struct McpStdioServerConfig {
712    /// Tools to expose from this server. `["*"]` exposes all; `[]` exposes none.
713    #[serde(default)]
714    pub tools: Vec<String>,
715    /// Optional timeout in milliseconds for tool calls to this server.
716    #[serde(default, skip_serializing_if = "Option::is_none")]
717    pub timeout: Option<i64>,
718    /// Subprocess executable.
719    pub command: String,
720    /// Arguments to pass to the subprocess.
721    #[serde(default)]
722    pub args: Vec<String>,
723    /// Environment variables to set on the subprocess.
724    ///
725    /// Interpretation depends on the parent session's
726    /// `env_value_mode`: `"direct"` (default) treats values as literals;
727    /// `"indirect"` treats them as env-var names to look up at start time.
728    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
729    pub env: HashMap<String, String>,
730    /// Working directory for the subprocess.
731    #[serde(default, skip_serializing_if = "Option::is_none")]
732    pub cwd: Option<String>,
733}
734
735/// Configuration for a remote MCP server (HTTP or SSE).
736///
737/// See [`McpServerConfig::Http`] and [`McpServerConfig::Sse`].
738#[derive(Debug, Clone, Default, Serialize, Deserialize)]
739#[serde(rename_all = "camelCase")]
740pub struct McpHttpServerConfig {
741    /// Tools to expose from this server. `["*"]` exposes all; `[]` exposes none.
742    #[serde(default)]
743    pub tools: Vec<String>,
744    /// Optional timeout in milliseconds for tool calls to this server.
745    #[serde(default, skip_serializing_if = "Option::is_none")]
746    pub timeout: Option<i64>,
747    /// Server URL.
748    pub url: String,
749    /// Optional HTTP headers to include on every request.
750    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
751    pub headers: HashMap<String, String>,
752}
753
754/// Configures a custom inference provider (BYOK — Bring Your Own Key).
755///
756/// Routes session requests through an alternative model provider
757/// (OpenAI-compatible, Azure, Anthropic, or local) instead of GitHub
758/// Copilot's default routing.
759#[derive(Debug, Clone, Default, Serialize, Deserialize)]
760#[serde(rename_all = "camelCase")]
761#[non_exhaustive]
762pub struct ProviderConfig {
763    /// Provider type: `"openai"`, `"azure"`, or `"anthropic"`. Defaults to
764    /// `"openai"` on the CLI.
765    #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
766    pub provider_type: Option<String>,
767    /// API format (openai/azure only): `"completions"` or `"responses"`.
768    /// Defaults to `"completions"`.
769    #[serde(default, skip_serializing_if = "Option::is_none")]
770    pub wire_api: Option<String>,
771    /// API endpoint URL.
772    pub base_url: String,
773    /// API key. Optional for local providers like Ollama.
774    #[serde(default, skip_serializing_if = "Option::is_none")]
775    pub api_key: Option<String>,
776    /// Bearer token for authentication. Sets the `Authorization` header
777    /// directly. Use for services requiring bearer-token auth instead of
778    /// API key. Takes precedence over `api_key` when both are set.
779    #[serde(default, skip_serializing_if = "Option::is_none")]
780    pub bearer_token: Option<String>,
781    /// Azure-specific options.
782    #[serde(default, skip_serializing_if = "Option::is_none")]
783    pub azure: Option<AzureProviderOptions>,
784    /// Custom HTTP headers included in outbound provider requests.
785    #[serde(default, skip_serializing_if = "Option::is_none")]
786    pub headers: Option<HashMap<String, String>>,
787    /// Well-known model ID used to look up agent config and default token
788    /// limits. Also used as the wire model when [`wire_model`](Self::wire_model)
789    /// is unset. Falls back to [`SessionConfig::model`](crate::SessionConfig::model).
790    #[serde(default, skip_serializing_if = "Option::is_none")]
791    pub model_id: Option<String>,
792    /// Model name sent to the provider API for inference. Use this when
793    /// the provider's model name (e.g. an Azure deployment name or a
794    /// custom fine-tune name) differs from
795    /// [`model_id`](Self::model_id). Falls back to
796    /// [`model_id`](Self::model_id), then to
797    /// [`SessionConfig::model`](crate::SessionConfig::model).
798    #[serde(default, skip_serializing_if = "Option::is_none")]
799    pub wire_model: Option<String>,
800    /// Overrides the resolved model's default max prompt tokens. The
801    /// runtime triggers conversation compaction before sending a request
802    /// when the prompt (system message, history, tool definitions, user
803    /// message) would exceed this limit.
804    #[serde(default, skip_serializing_if = "Option::is_none")]
805    pub max_prompt_tokens: Option<i64>,
806    /// Overrides the resolved model's default max output tokens. When
807    /// hit, the model stops generating and returns a truncated response.
808    #[serde(default, skip_serializing_if = "Option::is_none")]
809    pub max_output_tokens: Option<i64>,
810}
811
812impl ProviderConfig {
813    /// Construct a [`ProviderConfig`] with the required `base_url` set;
814    /// all other fields default to unset.
815    pub fn new(base_url: impl Into<String>) -> Self {
816        Self {
817            base_url: base_url.into(),
818            ..Self::default()
819        }
820    }
821
822    /// Set the provider type (`"openai"`, `"azure"`, or `"anthropic"`).
823    pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
824        self.provider_type = Some(provider_type.into());
825        self
826    }
827
828    /// Set the API format (`"completions"` or `"responses"`; openai/azure only).
829    pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
830        self.wire_api = Some(wire_api.into());
831        self
832    }
833
834    /// Set the API key. Optional for local providers like Ollama.
835    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
836        self.api_key = Some(api_key.into());
837        self
838    }
839
840    /// Set the bearer token used to populate the `Authorization` header.
841    /// Takes precedence over `api_key` when both are set.
842    pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
843        self.bearer_token = Some(bearer_token.into());
844        self
845    }
846
847    /// Set Azure-specific options.
848    pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
849        self.azure = Some(azure);
850        self
851    }
852
853    /// Set the custom HTTP headers attached to outbound provider requests.
854    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
855        self.headers = Some(headers);
856        self
857    }
858
859    /// Set the well-known model ID used to look up agent config and default
860    /// token limits. Falls back to the session's configured model when unset.
861    pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
862        self.model_id = Some(model_id.into());
863        self
864    }
865
866    /// Set the model name sent to the provider API for inference. Use this
867    /// when the provider's model name (e.g. an Azure deployment name or a
868    /// custom fine-tune name) differs from
869    /// [`model_id`](Self::model_id).
870    pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
871        self.wire_model = Some(wire_model.into());
872        self
873    }
874
875    /// Override the resolved model's default max prompt tokens. The
876    /// runtime triggers conversation compaction when the prompt would
877    /// exceed this limit.
878    pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
879        self.max_prompt_tokens = Some(max);
880        self
881    }
882
883    /// Override the resolved model's default max output tokens. When
884    /// hit, the model stops generating and returns a truncated response.
885    pub fn with_max_output_tokens(mut self, max: i64) -> Self {
886        self.max_output_tokens = Some(max);
887        self
888    }
889}
890
891/// Azure-specific provider options.
892#[derive(Debug, Clone, Default, Serialize, Deserialize)]
893#[serde(rename_all = "camelCase")]
894pub struct AzureProviderOptions {
895    /// Azure API version. Defaults to `"2024-10-21"`.
896    #[serde(default, skip_serializing_if = "Option::is_none")]
897    pub api_version: Option<String>,
898}
899
900/// Configuration for creating a new session via the `session.create` RPC.
901///
902/// All fields are optional — the CLI applies sensible defaults.
903///
904/// # Construction
905///
906/// Two equivalent shapes are supported:
907///
908/// 1. **Chained builder** (preferred for compile-time-known values):
909///
910///    ```
911///    # use github_copilot_sdk::types::SessionConfig;
912///    let cfg = SessionConfig::default()
913///        .with_client_name("my-app")
914///        .with_streaming(true)
915///        .with_enable_config_discovery(true);
916///    ```
917///
918/// 2. **Direct field assignment** (preferred when forwarding `Option<T>`
919///    from upstream code, since `with_<field>` setters take the inner
920///    `T`, not `Option<T>`):
921///
922///    ```
923///    # use github_copilot_sdk::types::SessionConfig;
924///    # let upstream_model: Option<String> = None;
925///    # let upstream_system_message: Option<github_copilot_sdk::types::SystemMessageConfig> = None;
926///    let mut cfg = SessionConfig::default()
927///        .with_client_name("my-app")
928///        .with_streaming(true);
929///    cfg.model = upstream_model;
930///    cfg.system_message = upstream_system_message;
931///    ```
932///
933///    Mixing the two is fine: chain the fields you know at compile time,
934///    then assign the `Option<T>` pass-through fields directly. All
935///    fields on this struct are `pub`. This pattern matches the
936///    `http::request::Parts` / `hyper::Body::Builder` convention in the
937///    wider Rust ecosystem.
938///
939/// # Field naming across SDKs
940///
941/// Rust field names are snake_case (`available_tools`, `system_message`);
942/// they round-trip to the camelCase wire protocol via `#[serde(rename_all =
943/// "camelCase")]`. When porting code from the TypeScript, Go, Python, or
944/// .NET SDKs — or reading the raw JSON-RPC traces — fields appear as
945/// `availableTools`, `systemMessage`, etc.
946#[derive(Clone, Serialize, Deserialize)]
947#[serde(rename_all = "camelCase")]
948#[non_exhaustive]
949pub struct SessionConfig {
950    /// Custom session ID. When unset, the CLI generates one.
951    #[serde(skip_serializing_if = "Option::is_none")]
952    pub session_id: Option<SessionId>,
953    /// Model to use (e.g. `"gpt-4"`, `"claude-sonnet-4"`).
954    #[serde(skip_serializing_if = "Option::is_none")]
955    pub model: Option<String>,
956    /// Application name sent as `User-Agent` context.
957    #[serde(skip_serializing_if = "Option::is_none")]
958    pub client_name: Option<String>,
959    /// Reasoning effort level (e.g. `"low"`, `"medium"`, `"high"`).
960    #[serde(skip_serializing_if = "Option::is_none")]
961    pub reasoning_effort: Option<String>,
962    /// Enable streaming token deltas via `assistant.message_delta` events.
963    #[serde(skip_serializing_if = "Option::is_none")]
964    pub streaming: Option<bool>,
965    /// Custom system message configuration.
966    #[serde(skip_serializing_if = "Option::is_none")]
967    pub system_message: Option<SystemMessageConfig>,
968    /// Client-defined tools to expose to the agent.
969    #[serde(skip_serializing_if = "Option::is_none")]
970    pub tools: Option<Vec<Tool>>,
971    /// Allowlist of built-in tool names the agent may use.
972    #[serde(skip_serializing_if = "Option::is_none")]
973    pub available_tools: Option<Vec<String>>,
974    /// Blocklist of built-in tool names the agent must not use.
975    #[serde(skip_serializing_if = "Option::is_none")]
976    pub excluded_tools: Option<Vec<String>>,
977    /// MCP server configurations passed through to the CLI.
978    #[serde(skip_serializing_if = "Option::is_none")]
979    pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
980    /// How the CLI interprets env values in MCP server configs.
981    /// `"direct"` = literal values; `"indirect"` = env var names to look up.
982    #[serde(skip_serializing_if = "Option::is_none")]
983    pub env_value_mode: Option<String>,
984    /// When true, the CLI runs config discovery (MCP config files, skills, plugins).
985    #[serde(skip_serializing_if = "Option::is_none")]
986    pub enable_config_discovery: Option<bool>,
987    /// Enable the `ask_user` tool for interactive user input. Defaults to
988    /// `Some(true)` via [`SessionConfig::default`].
989    #[serde(skip_serializing_if = "Option::is_none")]
990    pub request_user_input: Option<bool>,
991    /// Enable `permission.request` JSON-RPC calls from the CLI. Defaults
992    /// to `Some(true)` via [`SessionConfig::default`]; the default
993    /// [`DenyAllHandler`](crate::handler::DenyAllHandler) refuses all
994    /// requests so the wire surface is safe out-of-the-box.
995    #[serde(skip_serializing_if = "Option::is_none")]
996    pub request_permission: Option<bool>,
997    /// Enable `exitPlanMode.request` JSON-RPC calls for plan approval.
998    /// Defaults to `Some(true)` via [`SessionConfig::default`].
999    #[serde(skip_serializing_if = "Option::is_none")]
1000    pub request_exit_plan_mode: Option<bool>,
1001    /// Enable `autoModeSwitch.request` JSON-RPC calls. When `true`, the CLI
1002    /// asks the handler whether to switch to auto model when an eligible
1003    /// rate limit is hit. Defaults to `Some(true)` via
1004    /// [`SessionConfig::default`]. Without this flag, the CLI surfaces the
1005    /// rate-limit error directly without offering the auto-mode switch.
1006    ///
1007    /// Currently a Rust-only typed handler; cross-SDK parity (Node /
1008    /// Python / Go / .NET) is post-release follow-up work — see
1009    /// [`SessionHandler::on_auto_mode_switch`].
1010    ///
1011    /// [`SessionHandler::on_auto_mode_switch`]: crate::handler::SessionHandler::on_auto_mode_switch
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    pub request_auto_mode_switch: Option<bool>,
1014    /// Advertise elicitation provider capability. When true, the CLI sends
1015    /// `elicitation.requested` events that the handler can respond to.
1016    /// Defaults to `Some(true)` via [`SessionConfig::default`].
1017    #[serde(skip_serializing_if = "Option::is_none")]
1018    pub request_elicitation: Option<bool>,
1019    /// Skill directory paths passed through to the GitHub Copilot CLI.
1020    #[serde(skip_serializing_if = "Option::is_none")]
1021    pub skill_directories: Option<Vec<PathBuf>>,
1022    /// Additional directories to search for custom instruction files.
1023    /// Forwarded to the CLI; not the same as [`skill_directories`](Self::skill_directories).
1024    #[serde(skip_serializing_if = "Option::is_none")]
1025    pub instruction_directories: Option<Vec<PathBuf>>,
1026    /// Skill names to disable. Skills in this set will not be available
1027    /// even if found in skill directories.
1028    #[serde(skip_serializing_if = "Option::is_none")]
1029    pub disabled_skills: Option<Vec<String>>,
1030    /// MCP server names to disable. Servers in this set will not be
1031    /// started or connected.
1032    #[serde(skip_serializing_if = "Option::is_none")]
1033    pub disabled_mcp_servers: Option<Vec<String>>,
1034    /// Enable session hooks. When `true`, the CLI sends `hooks.invoke`
1035    /// RPC requests at key lifecycle points (pre/post tool use, prompt
1036    /// submission, session start/end, errors).
1037    #[serde(skip_serializing_if = "Option::is_none")]
1038    pub hooks: Option<bool>,
1039    /// Custom agents (sub-agents) configured for this session.
1040    #[serde(skip_serializing_if = "Option::is_none")]
1041    pub custom_agents: Option<Vec<CustomAgentConfig>>,
1042    /// Configures the built-in default agent. Use `excluded_tools` to
1043    /// hide tools from the default agent while keeping them available
1044    /// to custom sub-agents that reference them in their `tools` list.
1045    #[serde(skip_serializing_if = "Option::is_none")]
1046    pub default_agent: Option<DefaultAgentConfig>,
1047    /// Name of the custom agent to activate when the session starts.
1048    /// Must match the `name` of one of the agents in [`Self::custom_agents`].
1049    #[serde(skip_serializing_if = "Option::is_none")]
1050    pub agent: Option<String>,
1051    /// Configures infinite sessions: persistent workspace + automatic
1052    /// context-window compaction. Enabled by default on the CLI.
1053    #[serde(skip_serializing_if = "Option::is_none")]
1054    pub infinite_sessions: Option<InfiniteSessionConfig>,
1055    /// Custom model provider (BYOK). When set, the session routes
1056    /// requests through this provider instead of the default Copilot
1057    /// routing.
1058    #[serde(skip_serializing_if = "Option::is_none")]
1059    pub provider: Option<ProviderConfig>,
1060    /// Per-property overrides for model capabilities, deep-merged over
1061    /// runtime defaults.
1062    #[serde(skip_serializing_if = "Option::is_none")]
1063    pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1064    /// Override the default configuration directory location. When set,
1065    /// the session uses this directory for storing config and state.
1066    #[serde(skip_serializing_if = "Option::is_none")]
1067    pub config_dir: Option<PathBuf>,
1068    /// Working directory for the session. Tool operations resolve
1069    /// relative paths against this directory.
1070    #[serde(skip_serializing_if = "Option::is_none")]
1071    pub working_directory: Option<PathBuf>,
1072    /// Per-session GitHub token. Distinct from
1073    /// [`ClientOptions::github_token`](crate::ClientOptions::github_token),
1074    /// which authenticates the CLI process itself; this token determines
1075    /// the GitHub identity used for content exclusion, model routing, and
1076    /// quota checks for *this session*.
1077    #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")]
1078    pub github_token: Option<String>,
1079    /// Forward sub-agent streaming events to this connection. When false,
1080    /// only non-streaming sub-agent events and `subagent.*` lifecycle events
1081    /// are delivered. Defaults to true on the CLI.
1082    #[serde(skip_serializing_if = "Option::is_none")]
1083    pub include_sub_agent_streaming_events: Option<bool>,
1084    /// Slash commands registered for this session. When the CLI has a TUI,
1085    /// each command appears as `/name` for the user to invoke and the
1086    /// associated [`CommandHandler`] is called when executed.
1087    #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1088    pub commands: Option<Vec<CommandDefinition>>,
1089    /// Custom session filesystem provider for this session. Required when
1090    /// the [`Client`](crate::Client) was started with
1091    /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs) set.
1092    /// See [`SessionFsProvider`].
1093    #[serde(skip)]
1094    pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1095    /// Session-level event handler. The default is
1096    /// [`DenyAllHandler`](crate::handler::DenyAllHandler) — permission
1097    /// requests are denied; other events are no-ops. Use
1098    /// [`with_handler`](Self::with_handler) to install a custom handler.
1099    #[serde(skip)]
1100    pub handler: Option<Arc<dyn SessionHandler>>,
1101    /// Session lifecycle hook handler (pre/post tool use, session
1102    /// start/end, etc.). When set, the SDK auto-enables the wire-level
1103    /// `hooks` flag. Use [`with_hooks`](Self::with_hooks) to install one.
1104    #[serde(skip)]
1105    pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1106    /// System-message transform. When set, the SDK injects the matching
1107    /// `action: "transform"` sections into the system message and routes
1108    /// `systemMessage.transform` RPC callbacks to it during the session.
1109    /// Use [`with_transform`](Self::with_transform) to install one.
1110    #[serde(skip)]
1111    pub transform: Option<Arc<dyn SystemMessageTransform>>,
1112}
1113
1114impl std::fmt::Debug for SessionConfig {
1115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1116        f.debug_struct("SessionConfig")
1117            .field("session_id", &self.session_id)
1118            .field("model", &self.model)
1119            .field("client_name", &self.client_name)
1120            .field("reasoning_effort", &self.reasoning_effort)
1121            .field("streaming", &self.streaming)
1122            .field("system_message", &self.system_message)
1123            .field("tools", &self.tools)
1124            .field("available_tools", &self.available_tools)
1125            .field("excluded_tools", &self.excluded_tools)
1126            .field("mcp_servers", &self.mcp_servers)
1127            .field("env_value_mode", &self.env_value_mode)
1128            .field("enable_config_discovery", &self.enable_config_discovery)
1129            .field("request_user_input", &self.request_user_input)
1130            .field("request_permission", &self.request_permission)
1131            .field("request_exit_plan_mode", &self.request_exit_plan_mode)
1132            .field("request_auto_mode_switch", &self.request_auto_mode_switch)
1133            .field("request_elicitation", &self.request_elicitation)
1134            .field("skill_directories", &self.skill_directories)
1135            .field("instruction_directories", &self.instruction_directories)
1136            .field("disabled_skills", &self.disabled_skills)
1137            .field("disabled_mcp_servers", &self.disabled_mcp_servers)
1138            .field("hooks", &self.hooks)
1139            .field("custom_agents", &self.custom_agents)
1140            .field("default_agent", &self.default_agent)
1141            .field("agent", &self.agent)
1142            .field("infinite_sessions", &self.infinite_sessions)
1143            .field("provider", &self.provider)
1144            .field("model_capabilities", &self.model_capabilities)
1145            .field("config_dir", &self.config_dir)
1146            .field("working_directory", &self.working_directory)
1147            .field(
1148                "github_token",
1149                &self.github_token.as_ref().map(|_| "<redacted>"),
1150            )
1151            .field(
1152                "include_sub_agent_streaming_events",
1153                &self.include_sub_agent_streaming_events,
1154            )
1155            .field("commands", &self.commands)
1156            .field(
1157                "session_fs_provider",
1158                &self.session_fs_provider.as_ref().map(|_| "<set>"),
1159            )
1160            .field("handler", &self.handler.as_ref().map(|_| "<set>"))
1161            .field(
1162                "hooks_handler",
1163                &self.hooks_handler.as_ref().map(|_| "<set>"),
1164            )
1165            .field("transform", &self.transform.as_ref().map(|_| "<set>"))
1166            .finish()
1167    }
1168}
1169
1170impl Default for SessionConfig {
1171    /// Permission and elicitation flows are enabled by default. With
1172    /// Rust's trait-based handlers, the SDK installs `DenyAllHandler` when
1173    /// no handler is provided, so these flags being `Some(true)` means the
1174    /// wire surface advertises the capabilities — and the default handler
1175    /// safely refuses requests. Callers that want the wire surface fully
1176    /// disabled set these explicitly to `Some(false)`.
1177    fn default() -> Self {
1178        Self {
1179            session_id: None,
1180            model: None,
1181            client_name: None,
1182            reasoning_effort: None,
1183            streaming: None,
1184            system_message: None,
1185            tools: None,
1186            available_tools: None,
1187            excluded_tools: None,
1188            mcp_servers: None,
1189            env_value_mode: None,
1190            enable_config_discovery: None,
1191            request_user_input: Some(true),
1192            request_permission: Some(true),
1193            request_exit_plan_mode: Some(true),
1194            request_auto_mode_switch: Some(true),
1195            request_elicitation: Some(true),
1196            skill_directories: None,
1197            instruction_directories: None,
1198            disabled_skills: None,
1199            disabled_mcp_servers: None,
1200            hooks: None,
1201            custom_agents: None,
1202            default_agent: None,
1203            agent: None,
1204            infinite_sessions: None,
1205            provider: None,
1206            model_capabilities: None,
1207            config_dir: None,
1208            working_directory: None,
1209            github_token: None,
1210            include_sub_agent_streaming_events: None,
1211            commands: None,
1212            session_fs_provider: None,
1213            handler: None,
1214            hooks_handler: None,
1215            transform: None,
1216        }
1217    }
1218}
1219
1220impl SessionConfig {
1221    /// Install a custom [`SessionHandler`] for this session.
1222    pub fn with_handler(mut self, handler: Arc<dyn SessionHandler>) -> Self {
1223        self.handler = Some(handler);
1224        self
1225    }
1226
1227    /// Register slash commands for this session. Each command appears as
1228    /// `/name` in the CLI's TUI; the handler is invoked when the user
1229    /// executes the command. Replaces any commands previously set on this
1230    /// config. See [`CommandDefinition`].
1231    pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
1232        self.commands = Some(commands);
1233        self
1234    }
1235
1236    /// Install a [`SessionFsProvider`] backing the session's filesystem.
1237    /// Required when the [`Client`](crate::Client) was started with
1238    /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs).
1239    pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
1240        self.session_fs_provider = Some(provider);
1241        self
1242    }
1243
1244    /// Install a [`SessionHooks`] handler. Automatically enables the
1245    /// wire-level `hooks` flag on session creation.
1246    pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
1247        self.hooks_handler = Some(hooks);
1248        self
1249    }
1250
1251    /// Install a [`SystemMessageTransform`]. The SDK injects the matching
1252    /// `action: "transform"` sections into the system message and routes
1253    /// `systemMessage.transform` RPC callbacks to it during the session.
1254    pub fn with_transform(mut self, transform: Arc<dyn SystemMessageTransform>) -> Self {
1255        self.transform = Some(transform);
1256        self
1257    }
1258
1259    /// Wrap the configured handler so every permission request is
1260    /// auto-approved. Forwards every non-permission event to the inner
1261    /// handler unchanged.
1262    ///
1263    /// If no handler has been installed via [`with_handler`](Self::with_handler),
1264    /// wraps a [`DenyAllHandler`](crate::handler::DenyAllHandler) — useful
1265    /// when you only care about permission policy and want the trait
1266    /// fallback responses for everything else.
1267    ///
1268    /// Order-independent: `with_handler(...).approve_all_permissions()` and
1269    /// `approve_all_permissions().with_handler(...)` are NOT equivalent —
1270    /// the second form discards the wrap because `with_handler` overwrites
1271    /// the handler field. Always call `approve_all_permissions` *after*
1272    /// `with_handler`.
1273    pub fn approve_all_permissions(mut self) -> Self {
1274        let inner = self
1275            .handler
1276            .take()
1277            .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1278        self.handler = Some(crate::permission::approve_all(inner));
1279        self
1280    }
1281
1282    /// Wrap the configured handler so every permission request is
1283    /// auto-denied. See [`approve_all_permissions`](Self::approve_all_permissions)
1284    /// for ordering and default-handler semantics.
1285    pub fn deny_all_permissions(mut self) -> Self {
1286        let inner = self
1287            .handler
1288            .take()
1289            .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1290        self.handler = Some(crate::permission::deny_all(inner));
1291        self
1292    }
1293
1294    /// Wrap the configured handler with a closure-based permission policy:
1295    /// `predicate` is called for each permission request; `true` approves,
1296    /// `false` denies. See
1297    /// [`approve_all_permissions`](Self::approve_all_permissions) for
1298    /// ordering and default-handler semantics.
1299    pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
1300    where
1301        F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
1302    {
1303        let inner = self
1304            .handler
1305            .take()
1306            .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1307        self.handler = Some(crate::permission::approve_if(inner, predicate));
1308        self
1309    }
1310
1311    /// Set a custom session ID (when unset, the CLI generates one).
1312    pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
1313        self.session_id = Some(id.into());
1314        self
1315    }
1316
1317    /// Set the model identifier (e.g. `"claude-sonnet-4"`).
1318    pub fn with_model(mut self, model: impl Into<String>) -> Self {
1319        self.model = Some(model.into());
1320        self
1321    }
1322
1323    /// Set the application name sent as `User-Agent` context.
1324    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
1325        self.client_name = Some(name.into());
1326        self
1327    }
1328
1329    /// Set the reasoning effort level (e.g. `"low"`, `"medium"`, `"high"`).
1330    pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
1331        self.reasoning_effort = Some(effort.into());
1332        self
1333    }
1334
1335    /// Enable streaming token deltas via `assistant.message_delta` events.
1336    pub fn with_streaming(mut self, streaming: bool) -> Self {
1337        self.streaming = Some(streaming);
1338        self
1339    }
1340
1341    /// Set a custom system message configuration.
1342    pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
1343        self.system_message = Some(system_message);
1344        self
1345    }
1346
1347    /// Set the client-defined tools to expose to the agent.
1348    pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
1349        self.tools = Some(tools.into_iter().collect());
1350        self
1351    }
1352
1353    /// Set the allowlist of built-in tool names the agent may use.
1354    pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
1355    where
1356        I: IntoIterator<Item = S>,
1357        S: Into<String>,
1358    {
1359        self.available_tools = Some(tools.into_iter().map(Into::into).collect());
1360        self
1361    }
1362
1363    /// Set the blocklist of built-in tool names the agent must not use.
1364    pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
1365    where
1366        I: IntoIterator<Item = S>,
1367        S: Into<String>,
1368    {
1369        self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
1370        self
1371    }
1372
1373    /// Set MCP server configurations passed through to the CLI.
1374    pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
1375        self.mcp_servers = Some(servers);
1376        self
1377    }
1378
1379    /// Set how the CLI interprets env values in MCP server configs
1380    /// (`"direct"` literal vs `"indirect"` env var name lookup).
1381    pub fn with_env_value_mode(mut self, mode: impl Into<String>) -> Self {
1382        self.env_value_mode = Some(mode.into());
1383        self
1384    }
1385
1386    /// Enable or disable CLI config discovery (MCP config files, skills, plugins).
1387    pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
1388        self.enable_config_discovery = Some(enable);
1389        self
1390    }
1391
1392    /// Enable the `ask_user` tool. Defaults to `Some(true)` via [`Self::default`].
1393    pub fn with_request_user_input(mut self, enable: bool) -> Self {
1394        self.request_user_input = Some(enable);
1395        self
1396    }
1397
1398    /// Enable `permission.request` JSON-RPC calls. Defaults to `Some(true)`.
1399    pub fn with_request_permission(mut self, enable: bool) -> Self {
1400        self.request_permission = Some(enable);
1401        self
1402    }
1403
1404    /// Enable `exitPlanMode.request` JSON-RPC calls. Defaults to `Some(true)`.
1405    pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self {
1406        self.request_exit_plan_mode = Some(enable);
1407        self
1408    }
1409
1410    /// Enable `autoModeSwitch.request` JSON-RPC calls. Defaults to `Some(true)`.
1411    pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self {
1412        self.request_auto_mode_switch = Some(enable);
1413        self
1414    }
1415
1416    /// Advertise elicitation provider capability. Defaults to `Some(true)`.
1417    pub fn with_request_elicitation(mut self, enable: bool) -> Self {
1418        self.request_elicitation = Some(enable);
1419        self
1420    }
1421
1422    /// Set skill directory paths passed through to the CLI.
1423    pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
1424    where
1425        I: IntoIterator<Item = P>,
1426        P: Into<PathBuf>,
1427    {
1428        self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
1429        self
1430    }
1431
1432    /// Set additional directories to search for custom instruction files.
1433    /// Forwarded to the CLI on session create; not the same as
1434    /// [`with_skill_directories`](Self::with_skill_directories).
1435    pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
1436    where
1437        I: IntoIterator<Item = P>,
1438        P: Into<PathBuf>,
1439    {
1440        self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
1441        self
1442    }
1443
1444    /// Set the names of skills to disable (overrides skill discovery).
1445    pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
1446    where
1447        I: IntoIterator<Item = S>,
1448        S: Into<String>,
1449    {
1450        self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
1451        self
1452    }
1453
1454    /// Set the names of MCP servers to disable.
1455    pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
1456    where
1457        I: IntoIterator<Item = S>,
1458        S: Into<String>,
1459    {
1460        self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
1461        self
1462    }
1463
1464    /// Set the custom agents (sub-agents) configured for this session.
1465    pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
1466        mut self,
1467        agents: I,
1468    ) -> Self {
1469        self.custom_agents = Some(agents.into_iter().collect());
1470        self
1471    }
1472
1473    /// Configure the built-in default agent.
1474    pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
1475        self.default_agent = Some(agent);
1476        self
1477    }
1478
1479    /// Activate a named custom agent on session start. Must match the
1480    /// `name` of one of the agents in [`Self::custom_agents`].
1481    pub fn with_agent(mut self, name: impl Into<String>) -> Self {
1482        self.agent = Some(name.into());
1483        self
1484    }
1485
1486    /// Configure infinite sessions (persistent workspace + automatic
1487    /// context-window compaction).
1488    pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
1489        self.infinite_sessions = Some(config);
1490        self
1491    }
1492
1493    /// Configure a custom model provider (BYOK).
1494    pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
1495        self.provider = Some(provider);
1496        self
1497    }
1498
1499    /// Set per-property overrides for model capabilities.
1500    pub fn with_model_capabilities(
1501        mut self,
1502        capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1503    ) -> Self {
1504        self.model_capabilities = Some(capabilities);
1505        self
1506    }
1507
1508    /// Override the default configuration directory location.
1509    pub fn with_config_dir(mut self, dir: impl Into<PathBuf>) -> Self {
1510        self.config_dir = Some(dir.into());
1511        self
1512    }
1513
1514    /// Set the per-session working directory. Tool operations resolve
1515    /// relative paths against this directory.
1516    pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
1517        self.working_directory = Some(dir.into());
1518        self
1519    }
1520
1521    /// Set the per-session GitHub token. Distinct from
1522    /// [`ClientOptions::github_token`](crate::ClientOptions::github_token);
1523    /// this token determines the GitHub identity used for content exclusion,
1524    /// model routing, and quota checks for this session only.
1525    pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
1526        self.github_token = Some(token.into());
1527        self
1528    }
1529
1530    /// Forward sub-agent streaming events to this connection. Defaults
1531    /// to true on the CLI when unset.
1532    pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
1533        self.include_sub_agent_streaming_events = Some(include);
1534        self
1535    }
1536}
1537
1538/// Configuration for resuming an existing session via the `session.resume` RPC.
1539///
1540/// See [`SessionConfig`] for the construction patterns (chained `with_*`
1541/// builder vs. direct field assignment for `Option<T>` pass-through) and
1542/// the note on snake_case vs. camelCase field naming.
1543#[derive(Clone, Serialize, Deserialize)]
1544#[serde(rename_all = "camelCase")]
1545#[non_exhaustive]
1546pub struct ResumeSessionConfig {
1547    /// ID of the session to resume.
1548    pub session_id: SessionId,
1549    /// Application name sent as User-Agent context.
1550    #[serde(skip_serializing_if = "Option::is_none")]
1551    pub client_name: Option<String>,
1552    /// Enable streaming token deltas.
1553    #[serde(skip_serializing_if = "Option::is_none")]
1554    pub streaming: Option<bool>,
1555    /// Re-supply the system message so the agent retains workspace context
1556    /// across CLI process restarts.
1557    #[serde(skip_serializing_if = "Option::is_none")]
1558    pub system_message: Option<SystemMessageConfig>,
1559    /// Client-defined tools to re-supply on resume.
1560    #[serde(skip_serializing_if = "Option::is_none")]
1561    pub tools: Option<Vec<Tool>>,
1562    /// Blocklist of built-in tool names.
1563    #[serde(skip_serializing_if = "Option::is_none")]
1564    pub excluded_tools: Option<Vec<String>>,
1565    /// Re-supply MCP servers so they remain available after app restart.
1566    #[serde(skip_serializing_if = "Option::is_none")]
1567    pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
1568    /// How the CLI interprets env values in MCP configs.
1569    #[serde(skip_serializing_if = "Option::is_none")]
1570    pub env_value_mode: Option<String>,
1571    /// Enable config discovery on resume.
1572    #[serde(skip_serializing_if = "Option::is_none")]
1573    pub enable_config_discovery: Option<bool>,
1574    /// Enable the ask_user tool.
1575    #[serde(skip_serializing_if = "Option::is_none")]
1576    pub request_user_input: Option<bool>,
1577    /// Enable permission request RPCs.
1578    #[serde(skip_serializing_if = "Option::is_none")]
1579    pub request_permission: Option<bool>,
1580    /// Enable exit-plan-mode request RPCs.
1581    #[serde(skip_serializing_if = "Option::is_none")]
1582    pub request_exit_plan_mode: Option<bool>,
1583    /// Enable auto-mode-switch request RPCs on resume. Defaults to
1584    /// `Some(true)` via [`ResumeSessionConfig::new`]. See
1585    /// [`SessionConfig::request_auto_mode_switch`] for details.
1586    #[serde(skip_serializing_if = "Option::is_none")]
1587    pub request_auto_mode_switch: Option<bool>,
1588    /// Advertise elicitation provider capability on resume.
1589    #[serde(skip_serializing_if = "Option::is_none")]
1590    pub request_elicitation: Option<bool>,
1591    /// Skill directory paths passed through to the GitHub Copilot CLI on resume.
1592    #[serde(skip_serializing_if = "Option::is_none")]
1593    pub skill_directories: Option<Vec<PathBuf>>,
1594    /// Additional directories to search for custom instruction files on
1595    /// resume. Forwarded to the CLI; not the same as [`skill_directories`](Self::skill_directories).
1596    #[serde(skip_serializing_if = "Option::is_none")]
1597    pub instruction_directories: Option<Vec<PathBuf>>,
1598    /// Enable session hooks on resume.
1599    #[serde(skip_serializing_if = "Option::is_none")]
1600    pub hooks: Option<bool>,
1601    /// Custom agents to re-supply on resume.
1602    #[serde(skip_serializing_if = "Option::is_none")]
1603    pub custom_agents: Option<Vec<CustomAgentConfig>>,
1604    /// Configures the built-in default agent on resume.
1605    #[serde(skip_serializing_if = "Option::is_none")]
1606    pub default_agent: Option<DefaultAgentConfig>,
1607    /// Name of the custom agent to activate.
1608    #[serde(skip_serializing_if = "Option::is_none")]
1609    pub agent: Option<String>,
1610    /// Re-supply infinite session configuration on resume.
1611    #[serde(skip_serializing_if = "Option::is_none")]
1612    pub infinite_sessions: Option<InfiniteSessionConfig>,
1613    /// Re-supply BYOK provider configuration on resume.
1614    #[serde(skip_serializing_if = "Option::is_none")]
1615    pub provider: Option<ProviderConfig>,
1616    /// Per-property model capability overrides on resume.
1617    #[serde(skip_serializing_if = "Option::is_none")]
1618    pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1619    /// Override the default configuration directory location on resume.
1620    #[serde(skip_serializing_if = "Option::is_none")]
1621    pub config_dir: Option<PathBuf>,
1622    /// Per-session working directory on resume.
1623    #[serde(skip_serializing_if = "Option::is_none")]
1624    pub working_directory: Option<PathBuf>,
1625    /// Per-session GitHub token on resume. See
1626    /// [`SessionConfig::github_token`].
1627    #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")]
1628    pub github_token: Option<String>,
1629    /// Forward sub-agent streaming events to this connection on resume.
1630    #[serde(skip_serializing_if = "Option::is_none")]
1631    pub include_sub_agent_streaming_events: Option<bool>,
1632    /// Slash commands registered for this session on resume. See
1633    /// [`SessionConfig::commands`] — commands are not persisted server-side,
1634    /// so the resume payload re-supplies the registration.
1635    #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1636    pub commands: Option<Vec<CommandDefinition>>,
1637    /// Custom session filesystem provider. Required on resume when the
1638    /// [`Client`](crate::Client) was started with
1639    /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs).
1640    /// See [`SessionConfig::session_fs_provider`].
1641    #[serde(skip)]
1642    pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1643    /// Force-fail resume if the session does not exist on disk, instead of
1644    /// silently starting a new session.
1645    #[serde(skip_serializing_if = "Option::is_none")]
1646    pub disable_resume: Option<bool>,
1647    /// When `true`, instructs the runtime to continue any tool calls or
1648    /// permission requests that were pending when the previous connection
1649    /// was dropped. Use this together with [`Client::force_stop`] to hand
1650    /// off a session from one process to another without losing in-flight
1651    /// work.
1652    ///
1653    /// [`Client::force_stop`]: crate::Client::force_stop
1654    #[serde(skip_serializing_if = "Option::is_none")]
1655    pub continue_pending_work: Option<bool>,
1656    /// Session-level event handler. See [`SessionConfig::handler`].
1657    #[serde(skip)]
1658    pub handler: Option<Arc<dyn SessionHandler>>,
1659    /// Session hook handler. See [`SessionConfig::hooks_handler`].
1660    #[serde(skip)]
1661    pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1662    /// System-message transform. See [`SessionConfig::transform`].
1663    #[serde(skip)]
1664    pub transform: Option<Arc<dyn SystemMessageTransform>>,
1665}
1666
1667impl std::fmt::Debug for ResumeSessionConfig {
1668    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1669        f.debug_struct("ResumeSessionConfig")
1670            .field("session_id", &self.session_id)
1671            .field("client_name", &self.client_name)
1672            .field("streaming", &self.streaming)
1673            .field("system_message", &self.system_message)
1674            .field("tools", &self.tools)
1675            .field("excluded_tools", &self.excluded_tools)
1676            .field("mcp_servers", &self.mcp_servers)
1677            .field("env_value_mode", &self.env_value_mode)
1678            .field("enable_config_discovery", &self.enable_config_discovery)
1679            .field("request_user_input", &self.request_user_input)
1680            .field("request_permission", &self.request_permission)
1681            .field("request_exit_plan_mode", &self.request_exit_plan_mode)
1682            .field("request_auto_mode_switch", &self.request_auto_mode_switch)
1683            .field("request_elicitation", &self.request_elicitation)
1684            .field("skill_directories", &self.skill_directories)
1685            .field("instruction_directories", &self.instruction_directories)
1686            .field("hooks", &self.hooks)
1687            .field("custom_agents", &self.custom_agents)
1688            .field("default_agent", &self.default_agent)
1689            .field("agent", &self.agent)
1690            .field("infinite_sessions", &self.infinite_sessions)
1691            .field("provider", &self.provider)
1692            .field("model_capabilities", &self.model_capabilities)
1693            .field("config_dir", &self.config_dir)
1694            .field("working_directory", &self.working_directory)
1695            .field(
1696                "github_token",
1697                &self.github_token.as_ref().map(|_| "<redacted>"),
1698            )
1699            .field(
1700                "include_sub_agent_streaming_events",
1701                &self.include_sub_agent_streaming_events,
1702            )
1703            .field("commands", &self.commands)
1704            .field(
1705                "session_fs_provider",
1706                &self.session_fs_provider.as_ref().map(|_| "<set>"),
1707            )
1708            .field("handler", &self.handler.as_ref().map(|_| "<set>"))
1709            .field(
1710                "hooks_handler",
1711                &self.hooks_handler.as_ref().map(|_| "<set>"),
1712            )
1713            .field("transform", &self.transform.as_ref().map(|_| "<set>"))
1714            .field("disable_resume", &self.disable_resume)
1715            .field("continue_pending_work", &self.continue_pending_work)
1716            .finish()
1717    }
1718}
1719
1720impl ResumeSessionConfig {
1721    /// Construct a `ResumeSessionConfig` with the given session ID and all
1722    /// other fields left unset. Combine with `.with_*` builders or struct
1723    /// update syntax (`..ResumeSessionConfig::new(id)`) to populate the
1724    /// fields you need.
1725    pub fn new(session_id: SessionId) -> Self {
1726        Self {
1727            session_id,
1728            client_name: None,
1729            streaming: None,
1730            system_message: None,
1731            tools: None,
1732            excluded_tools: None,
1733            mcp_servers: None,
1734            env_value_mode: None,
1735            enable_config_discovery: None,
1736            request_user_input: Some(true),
1737            request_permission: Some(true),
1738            request_exit_plan_mode: Some(true),
1739            request_auto_mode_switch: Some(true),
1740            request_elicitation: Some(true),
1741            skill_directories: None,
1742            instruction_directories: None,
1743            hooks: None,
1744            custom_agents: None,
1745            default_agent: None,
1746            agent: None,
1747            infinite_sessions: None,
1748            provider: None,
1749            model_capabilities: None,
1750            config_dir: None,
1751            working_directory: None,
1752            github_token: None,
1753            include_sub_agent_streaming_events: None,
1754            commands: None,
1755            session_fs_provider: None,
1756            disable_resume: None,
1757            continue_pending_work: None,
1758            handler: None,
1759            hooks_handler: None,
1760            transform: None,
1761        }
1762    }
1763
1764    /// Install a custom [`SessionHandler`] for this session.
1765    pub fn with_handler(mut self, handler: Arc<dyn SessionHandler>) -> Self {
1766        self.handler = Some(handler);
1767        self
1768    }
1769
1770    /// Install a [`SessionHooks`] handler. Automatically enables the
1771    /// wire-level `hooks` flag on session resumption.
1772    pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
1773        self.hooks_handler = Some(hooks);
1774        self
1775    }
1776
1777    /// Install a [`SystemMessageTransform`].
1778    pub fn with_transform(mut self, transform: Arc<dyn SystemMessageTransform>) -> Self {
1779        self.transform = Some(transform);
1780        self
1781    }
1782
1783    /// Register slash commands for the resumed session. See
1784    /// [`SessionConfig::with_commands`] — commands are not persisted
1785    /// server-side, so the resume payload re-supplies the registration.
1786    pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
1787        self.commands = Some(commands);
1788        self
1789    }
1790
1791    /// Install a [`SessionFsProvider`] backing the resumed session's
1792    /// filesystem. See [`SessionConfig::with_session_fs_provider`].
1793    pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
1794        self.session_fs_provider = Some(provider);
1795        self
1796    }
1797
1798    /// Wrap the configured handler so every permission request is
1799    /// auto-approved. See
1800    /// [`SessionConfig::approve_all_permissions`] for semantics.
1801    pub fn approve_all_permissions(mut self) -> Self {
1802        let inner = self
1803            .handler
1804            .take()
1805            .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1806        self.handler = Some(crate::permission::approve_all(inner));
1807        self
1808    }
1809
1810    /// Wrap the configured handler so every permission request is
1811    /// auto-denied. See
1812    /// [`SessionConfig::deny_all_permissions`] for semantics.
1813    pub fn deny_all_permissions(mut self) -> Self {
1814        let inner = self
1815            .handler
1816            .take()
1817            .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1818        self.handler = Some(crate::permission::deny_all(inner));
1819        self
1820    }
1821
1822    /// Wrap the configured handler with a predicate-based permission policy.
1823    /// See [`SessionConfig::approve_permissions_if`] for semantics.
1824    pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
1825    where
1826        F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
1827    {
1828        let inner = self
1829            .handler
1830            .take()
1831            .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1832        self.handler = Some(crate::permission::approve_if(inner, predicate));
1833        self
1834    }
1835
1836    /// Set the application name sent as `User-Agent` context.
1837    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
1838        self.client_name = Some(name.into());
1839        self
1840    }
1841
1842    /// Enable streaming token deltas via `assistant.message_delta` events.
1843    pub fn with_streaming(mut self, streaming: bool) -> Self {
1844        self.streaming = Some(streaming);
1845        self
1846    }
1847
1848    /// Re-supply the system message so the agent retains workspace context
1849    /// across CLI process restarts.
1850    pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
1851        self.system_message = Some(system_message);
1852        self
1853    }
1854
1855    /// Re-supply client-defined tools on resume.
1856    pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
1857        self.tools = Some(tools.into_iter().collect());
1858        self
1859    }
1860
1861    /// Set the blocklist of built-in tool names the agent must not use.
1862    pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
1863    where
1864        I: IntoIterator<Item = S>,
1865        S: Into<String>,
1866    {
1867        self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
1868        self
1869    }
1870
1871    /// Re-supply MCP server configurations on resume.
1872    pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
1873        self.mcp_servers = Some(servers);
1874        self
1875    }
1876
1877    /// Set how the CLI interprets env values in MCP configs (`"direct"` /
1878    /// `"indirect"`).
1879    pub fn with_env_value_mode(mut self, mode: impl Into<String>) -> Self {
1880        self.env_value_mode = Some(mode.into());
1881        self
1882    }
1883
1884    /// Enable or disable CLI config discovery on resume.
1885    pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
1886        self.enable_config_discovery = Some(enable);
1887        self
1888    }
1889
1890    /// Enable the `ask_user` tool. Defaults to `Some(true)` via [`Self::new`].
1891    pub fn with_request_user_input(mut self, enable: bool) -> Self {
1892        self.request_user_input = Some(enable);
1893        self
1894    }
1895
1896    /// Enable `permission.request` JSON-RPC calls. Defaults to `Some(true)`.
1897    pub fn with_request_permission(mut self, enable: bool) -> Self {
1898        self.request_permission = Some(enable);
1899        self
1900    }
1901
1902    /// Enable `exitPlanMode.request` JSON-RPC calls. Defaults to `Some(true)`.
1903    pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self {
1904        self.request_exit_plan_mode = Some(enable);
1905        self
1906    }
1907
1908    /// Enable `autoModeSwitch.request` JSON-RPC calls. Defaults to `Some(true)`.
1909    pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self {
1910        self.request_auto_mode_switch = Some(enable);
1911        self
1912    }
1913
1914    /// Advertise elicitation provider capability on resume. Defaults to `Some(true)`.
1915    pub fn with_request_elicitation(mut self, enable: bool) -> Self {
1916        self.request_elicitation = Some(enable);
1917        self
1918    }
1919
1920    /// Set skill directory paths passed through to the CLI on resume.
1921    pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
1922    where
1923        I: IntoIterator<Item = P>,
1924        P: Into<PathBuf>,
1925    {
1926        self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
1927        self
1928    }
1929
1930    /// Set additional directories to search for custom instruction files
1931    /// on resume. Forwarded to the CLI; not the same as
1932    /// [`with_skill_directories`](Self::with_skill_directories).
1933    pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
1934    where
1935        I: IntoIterator<Item = P>,
1936        P: Into<PathBuf>,
1937    {
1938        self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
1939        self
1940    }
1941
1942    /// Re-supply custom agents on resume.
1943    pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
1944        mut self,
1945        agents: I,
1946    ) -> Self {
1947        self.custom_agents = Some(agents.into_iter().collect());
1948        self
1949    }
1950
1951    /// Configure the built-in default agent on resume.
1952    pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
1953        self.default_agent = Some(agent);
1954        self
1955    }
1956
1957    /// Activate a named custom agent on resume.
1958    pub fn with_agent(mut self, name: impl Into<String>) -> Self {
1959        self.agent = Some(name.into());
1960        self
1961    }
1962
1963    /// Re-supply infinite session configuration on resume.
1964    pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
1965        self.infinite_sessions = Some(config);
1966        self
1967    }
1968
1969    /// Re-supply BYOK provider configuration on resume.
1970    pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
1971        self.provider = Some(provider);
1972        self
1973    }
1974
1975    /// Set per-property model capability overrides on resume.
1976    pub fn with_model_capabilities(
1977        mut self,
1978        capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1979    ) -> Self {
1980        self.model_capabilities = Some(capabilities);
1981        self
1982    }
1983
1984    /// Override the default configuration directory location on resume.
1985    pub fn with_config_dir(mut self, dir: impl Into<PathBuf>) -> Self {
1986        self.config_dir = Some(dir.into());
1987        self
1988    }
1989
1990    /// Set the per-session working directory on resume.
1991    pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
1992        self.working_directory = Some(dir.into());
1993        self
1994    }
1995
1996    /// Set the per-session GitHub token on resume. See
1997    /// [`SessionConfig::github_token`] for distinction from the
1998    /// client-level token.
1999    pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2000        self.github_token = Some(token.into());
2001        self
2002    }
2003
2004    /// Forward sub-agent streaming events to this connection on resume.
2005    pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2006        self.include_sub_agent_streaming_events = Some(include);
2007        self
2008    }
2009
2010    /// Force-fail resume if the session does not exist on disk, instead
2011    /// of silently starting a new session.
2012    pub fn with_disable_resume(mut self, disable: bool) -> Self {
2013        self.disable_resume = Some(disable);
2014        self
2015    }
2016
2017    /// When `true`, instructs the runtime to continue any tool calls or
2018    /// permission requests that were pending when the previous connection
2019    /// was dropped. Use this together with
2020    /// [`Client::force_stop`](crate::Client::force_stop) to hand off a
2021    /// session from one process to another without losing in-flight work.
2022    pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
2023        self.continue_pending_work = Some(continue_pending);
2024        self
2025    }
2026}
2027
2028/// Controls how the system message is constructed.
2029///
2030/// Use `mode: "append"` (default) to add content after the built-in system
2031/// message, `"replace"` to substitute it entirely, or `"customize"` for
2032/// section-level overrides.
2033#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2034#[serde(rename_all = "camelCase")]
2035#[non_exhaustive]
2036pub struct SystemMessageConfig {
2037    /// How content is applied: `"append"` (default), `"replace"`, or `"customize"`.
2038    #[serde(skip_serializing_if = "Option::is_none")]
2039    pub mode: Option<String>,
2040    /// Content string to append or replace.
2041    #[serde(skip_serializing_if = "Option::is_none")]
2042    pub content: Option<String>,
2043    /// Section-level overrides (used with `mode: "customize"`).
2044    #[serde(skip_serializing_if = "Option::is_none")]
2045    pub sections: Option<HashMap<String, SectionOverride>>,
2046}
2047
2048impl SystemMessageConfig {
2049    /// Construct an empty [`SystemMessageConfig`]; all fields default to
2050    /// unset.
2051    pub fn new() -> Self {
2052        Self::default()
2053    }
2054
2055    /// Set the application mode: `"append"` (default), `"replace"`, or
2056    /// `"customize"`.
2057    pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
2058        self.mode = Some(mode.into());
2059        self
2060    }
2061
2062    /// Set the system message content (used by `"append"` and `"replace"`
2063    /// modes).
2064    pub fn with_content(mut self, content: impl Into<String>) -> Self {
2065        self.content = Some(content.into());
2066        self
2067    }
2068
2069    /// Set the section-level overrides (used with `mode: "customize"`).
2070    pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
2071        self.sections = Some(sections);
2072        self
2073    }
2074}
2075
2076/// An override operation for a single system prompt section.
2077///
2078/// Used within [`SystemMessageConfig::sections`] when `mode` is `"customize"`.
2079/// The `action` field determines the operation: `"replace"`, `"remove"`,
2080/// `"append"`, `"prepend"`, or `"transform"`.
2081#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2082#[serde(rename_all = "camelCase")]
2083pub struct SectionOverride {
2084    /// Override action: `"replace"`, `"remove"`, `"append"`, `"prepend"`, or `"transform"`.
2085    #[serde(skip_serializing_if = "Option::is_none")]
2086    pub action: Option<String>,
2087    /// Content for the override operation.
2088    #[serde(skip_serializing_if = "Option::is_none")]
2089    pub content: Option<String>,
2090}
2091
2092/// Response from `session.create`.
2093#[derive(Debug, Clone, Serialize, Deserialize)]
2094#[serde(rename_all = "camelCase")]
2095pub struct CreateSessionResult {
2096    /// The CLI-assigned session ID.
2097    pub session_id: SessionId,
2098    /// Workspace directory for the session (infinite sessions).
2099    #[serde(skip_serializing_if = "Option::is_none")]
2100    pub workspace_path: Option<PathBuf>,
2101    /// Remote session URL, if the session is running remotely.
2102    #[serde(default, alias = "remote_url")]
2103    pub remote_url: Option<String>,
2104    /// Capabilities negotiated with the CLI for this session.
2105    #[serde(skip_serializing_if = "Option::is_none")]
2106    pub capabilities: Option<SessionCapabilities>,
2107}
2108
2109/// Severity level for [`Session::log`](crate::session::Session::log) messages.
2110#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2111#[serde(rename_all = "lowercase")]
2112pub enum LogLevel {
2113    /// Informational message (default).
2114    #[default]
2115    Info,
2116    /// Warning message.
2117    Warning,
2118    /// Error message.
2119    Error,
2120}
2121
2122/// Options for [`Session::log`](crate::session::Session::log).
2123///
2124/// Pass `None` to `log` for defaults (info level, persisted to the session
2125/// event log on disk).
2126#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
2127#[serde(rename_all = "camelCase")]
2128pub struct LogOptions {
2129    /// Log severity. `None` lets the server pick (defaults to `info`).
2130    #[serde(skip_serializing_if = "Option::is_none")]
2131    pub level: Option<LogLevel>,
2132    /// When `Some(true)`, the message is transient and not persisted to the
2133    /// session event log on disk. `None` lets the server pick.
2134    #[serde(skip_serializing_if = "Option::is_none")]
2135    pub ephemeral: Option<bool>,
2136}
2137
2138impl LogOptions {
2139    /// Set [`level`](Self::level).
2140    pub fn with_level(mut self, level: LogLevel) -> Self {
2141        self.level = Some(level);
2142        self
2143    }
2144
2145    /// Set [`ephemeral`](Self::ephemeral).
2146    pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
2147        self.ephemeral = Some(ephemeral);
2148        self
2149    }
2150}
2151
2152/// Options for [`Session::set_model`](crate::session::Session::set_model).
2153///
2154/// Pass `None` to `set_model` to switch model without any overrides.
2155#[derive(Debug, Clone, Default)]
2156pub struct SetModelOptions {
2157    /// Reasoning effort for the new model (e.g. `"low"`, `"medium"`,
2158    /// `"high"`, `"xhigh"`).
2159    pub reasoning_effort: Option<String>,
2160    /// Override individual model capabilities resolved by the runtime. Only
2161    /// fields set on the override are applied; the rest fall back to the
2162    /// runtime-resolved values for the model.
2163    pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2164}
2165
2166impl SetModelOptions {
2167    /// Set [`reasoning_effort`](Self::reasoning_effort).
2168    pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2169        self.reasoning_effort = Some(effort.into());
2170        self
2171    }
2172
2173    /// Set [`model_capabilities`](Self::model_capabilities).
2174    pub fn with_model_capabilities(
2175        mut self,
2176        caps: crate::generated::api_types::ModelCapabilitiesOverride,
2177    ) -> Self {
2178        self.model_capabilities = Some(caps);
2179        self
2180    }
2181}
2182
2183/// Response from the top-level `ping` RPC.
2184///
2185/// The `protocol_version` field is the most commonly-inspected piece —
2186/// see [`Client::verify_protocol_version`].
2187///
2188/// [`Client::verify_protocol_version`]: crate::Client::verify_protocol_version
2189#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2190#[serde(rename_all = "camelCase")]
2191pub struct PingResponse {
2192    /// The message echoed back by the CLI.
2193    #[serde(default)]
2194    pub message: String,
2195    /// Server-side timestamp (Unix epoch milliseconds).
2196    #[serde(default)]
2197    pub timestamp: i64,
2198    /// The protocol version negotiated by the CLI, if reported.
2199    #[serde(skip_serializing_if = "Option::is_none")]
2200    pub protocol_version: Option<u32>,
2201}
2202
2203/// Line range for file attachments.
2204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2205#[serde(rename_all = "camelCase")]
2206pub struct AttachmentLineRange {
2207    /// First line (1-based).
2208    pub start: u32,
2209    /// Last line (inclusive).
2210    pub end: u32,
2211}
2212
2213/// Cursor position within a file selection.
2214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2215#[serde(rename_all = "camelCase")]
2216pub struct AttachmentSelectionPosition {
2217    /// Line number (0-based).
2218    pub line: u32,
2219    /// Character offset (0-based).
2220    pub character: u32,
2221}
2222
2223/// Range of selected text within a file.
2224#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2225#[serde(rename_all = "camelCase")]
2226pub struct AttachmentSelectionRange {
2227    /// Start position.
2228    pub start: AttachmentSelectionPosition,
2229    /// End position.
2230    pub end: AttachmentSelectionPosition,
2231}
2232
2233/// Type of GitHub reference attachment.
2234#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2235#[serde(rename_all = "snake_case")]
2236#[non_exhaustive]
2237pub enum GitHubReferenceType {
2238    /// GitHub issue.
2239    Issue,
2240    /// GitHub pull request.
2241    Pr,
2242    /// GitHub discussion.
2243    Discussion,
2244}
2245
2246/// An attachment included with a user message.
2247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2248#[serde(
2249    tag = "type",
2250    rename_all = "camelCase",
2251    rename_all_fields = "camelCase"
2252)]
2253#[non_exhaustive]
2254pub enum Attachment {
2255    /// A file path, optionally with a line range.
2256    File {
2257        /// Absolute path to the file.
2258        path: PathBuf,
2259        /// Label shown in the UI.
2260        #[serde(skip_serializing_if = "Option::is_none")]
2261        display_name: Option<String>,
2262        /// Optional line range to focus on.
2263        #[serde(skip_serializing_if = "Option::is_none")]
2264        line_range: Option<AttachmentLineRange>,
2265    },
2266    /// A directory path.
2267    Directory {
2268        /// Absolute path to the directory.
2269        path: PathBuf,
2270        /// Label shown in the UI.
2271        #[serde(skip_serializing_if = "Option::is_none")]
2272        display_name: Option<String>,
2273    },
2274    /// A text selection within a file.
2275    Selection {
2276        /// Path to the file containing the selection.
2277        file_path: PathBuf,
2278        /// The selected text content.
2279        text: String,
2280        /// Label shown in the UI.
2281        #[serde(skip_serializing_if = "Option::is_none")]
2282        display_name: Option<String>,
2283        /// Character range of the selection.
2284        selection: AttachmentSelectionRange,
2285    },
2286    /// Raw binary data (e.g. an image).
2287    Blob {
2288        /// Base64-encoded data.
2289        data: String,
2290        /// MIME type of the data.
2291        mime_type: String,
2292        /// Label shown in the UI.
2293        #[serde(skip_serializing_if = "Option::is_none")]
2294        display_name: Option<String>,
2295    },
2296    /// A reference to a GitHub issue, PR, or discussion.
2297    #[serde(rename = "github_reference")]
2298    GitHubReference {
2299        /// Issue/PR/discussion number.
2300        number: u64,
2301        /// Title of the referenced item.
2302        title: String,
2303        /// Kind of reference.
2304        reference_type: GitHubReferenceType,
2305        /// Current state (e.g. "open", "closed").
2306        state: String,
2307        /// URL to the referenced item.
2308        url: String,
2309    },
2310}
2311
2312impl Attachment {
2313    /// Returns the display name, if set.
2314    pub fn display_name(&self) -> Option<&str> {
2315        match self {
2316            Self::File { display_name, .. }
2317            | Self::Directory { display_name, .. }
2318            | Self::Selection { display_name, .. }
2319            | Self::Blob { display_name, .. } => display_name.as_deref(),
2320            Self::GitHubReference { .. } => None,
2321        }
2322    }
2323
2324    /// Returns a human-readable label, deriving one from the path if needed.
2325    pub fn label(&self) -> Option<String> {
2326        if let Some(display_name) = self
2327            .display_name()
2328            .map(str::trim)
2329            .filter(|name| !name.is_empty())
2330        {
2331            return Some(display_name.to_string());
2332        }
2333
2334        match self {
2335            Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
2336                format!("#{}", number)
2337            } else {
2338                title.trim().to_string()
2339            }),
2340            _ => self.derived_display_name(),
2341        }
2342    }
2343
2344    /// Ensure `display_name` is populated when the variant supports one.
2345    pub fn ensure_display_name(&mut self) {
2346        if self
2347            .display_name()
2348            .map(str::trim)
2349            .is_some_and(|name| !name.is_empty())
2350        {
2351            return;
2352        }
2353
2354        let Some(derived_display_name) = self.derived_display_name() else {
2355            return;
2356        };
2357
2358        match self {
2359            Self::File { display_name, .. }
2360            | Self::Directory { display_name, .. }
2361            | Self::Selection { display_name, .. }
2362            | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
2363            Self::GitHubReference { .. } => {}
2364        }
2365    }
2366
2367    fn derived_display_name(&self) -> Option<String> {
2368        match self {
2369            Self::File { path, .. } | Self::Directory { path, .. } => {
2370                Some(attachment_name_from_path(path))
2371            }
2372            Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
2373            Self::Blob { .. } => Some("attachment".to_string()),
2374            Self::GitHubReference { .. } => None,
2375        }
2376    }
2377}
2378
2379fn attachment_name_from_path(path: &Path) -> String {
2380    path.file_name()
2381        .map(|name| name.to_string_lossy().into_owned())
2382        .filter(|name| !name.is_empty())
2383        .unwrap_or_else(|| {
2384            let full = path.to_string_lossy();
2385            if full.is_empty() {
2386                "attachment".to_string()
2387            } else {
2388                full.into_owned()
2389            }
2390        })
2391}
2392
2393/// Normalize a list of attachments so every entry has a `display_name`.
2394pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
2395    for attachment in attachments {
2396        attachment.ensure_display_name();
2397    }
2398}
2399
2400/// Message delivery mode for [`MessageOptions::mode`].
2401///
2402/// Controls how a prompt is delivered relative to in-flight session work.
2403/// Wire values: `"enqueue"` and `"immediate"`.
2404#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2405#[serde(rename_all = "lowercase")]
2406#[non_exhaustive]
2407pub enum DeliveryMode {
2408    /// Queue the prompt behind any in-flight work (default).
2409    Enqueue,
2410    /// Interrupt the session and run the prompt immediately.
2411    Immediate,
2412}
2413
2414/// Options for sending a user message to the agent.
2415///
2416/// Used by both [`Session::send`](crate::session::Session::send) and
2417/// [`Session::send_and_wait`](crate::session::Session::send_and_wait); the
2418/// `wait_timeout` field is honored only by `send_and_wait` and is ignored by
2419/// `send`.
2420///
2421/// `MessageOptions` is `#[non_exhaustive]` and constructed via [`MessageOptions::new`]
2422/// plus the `with_*` chain so future fields can land without breaking callers.
2423/// For the trivial case, both `&str` and `String` implement `Into<MessageOptions>`,
2424/// so:
2425///
2426/// ```no_run
2427/// # use github_copilot_sdk::session::Session;
2428/// # async fn run(session: Session) -> Result<(), github_copilot_sdk::Error> {
2429/// session.send("hello").await?;
2430/// # Ok(()) }
2431/// ```
2432///
2433/// is equivalent to:
2434///
2435/// ```no_run
2436/// # use github_copilot_sdk::session::Session;
2437/// # use github_copilot_sdk::types::MessageOptions;
2438/// # async fn run(session: Session) -> Result<(), github_copilot_sdk::Error> {
2439/// session.send(MessageOptions::new("hello")).await?;
2440/// # Ok(()) }
2441/// ```
2442#[derive(Debug, Clone)]
2443#[non_exhaustive]
2444pub struct MessageOptions {
2445    /// The user prompt to send.
2446    pub prompt: String,
2447    /// Optional message delivery mode for this turn.
2448    ///
2449    /// Controls whether the prompt is queued behind in-flight work
2450    /// ([`DeliveryMode::Enqueue`], default) or interrupts the session and
2451    /// runs immediately ([`DeliveryMode::Immediate`]).
2452    pub mode: Option<DeliveryMode>,
2453    /// Optional attachments to include with the message.
2454    pub attachments: Option<Vec<Attachment>>,
2455    /// Maximum time to wait for the session to go idle. Honored only by
2456    /// `send_and_wait`. Defaults to 60 seconds when unset.
2457    pub wait_timeout: Option<Duration>,
2458    /// Custom HTTP headers to include in outbound model requests for this
2459    /// turn. When `None` or empty, no `requestHeaders` field is sent on
2460    /// the wire.
2461    pub request_headers: Option<HashMap<String, String>>,
2462    /// W3C Trace Context `traceparent` header for this turn.
2463    ///
2464    /// Per-turn override that takes precedence over
2465    /// [`ClientOptions::on_get_trace_context`](crate::ClientOptions::on_get_trace_context).
2466    /// When `None`, the SDK falls back to the provider (if configured)
2467    /// before omitting the field.
2468    pub traceparent: Option<String>,
2469    /// W3C Trace Context `tracestate` header for this turn.
2470    ///
2471    /// Per-turn override paired with [`traceparent`](Self::traceparent).
2472    pub tracestate: Option<String>,
2473}
2474
2475impl MessageOptions {
2476    /// Build a new `MessageOptions` with just a prompt.
2477    pub fn new(prompt: impl Into<String>) -> Self {
2478        Self {
2479            prompt: prompt.into(),
2480            mode: None,
2481            attachments: None,
2482            wait_timeout: None,
2483            request_headers: None,
2484            traceparent: None,
2485            tracestate: None,
2486        }
2487    }
2488
2489    /// Set the message delivery mode for this turn.
2490    ///
2491    /// Pass [`DeliveryMode::Immediate`] to interrupt the session and run
2492    /// the prompt now; the default ([`DeliveryMode::Enqueue`]) queues the
2493    /// prompt behind in-flight work.
2494    pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
2495        self.mode = Some(mode);
2496        self
2497    }
2498
2499    /// Attach files / selections / blobs to the message.
2500    pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
2501        self.attachments = Some(attachments);
2502        self
2503    }
2504
2505    /// Override the default 60-second wait timeout for `send_and_wait`.
2506    pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
2507        self.wait_timeout = Some(timeout);
2508        self
2509    }
2510
2511    /// Set custom HTTP headers for outbound model requests for this turn.
2512    pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
2513        self.request_headers = Some(headers);
2514        self
2515    }
2516
2517    /// Set both `traceparent` and `tracestate` from a [`TraceContext`].
2518    /// Either field may remain `None` if the [`TraceContext`] has no value
2519    /// for it. Use [`with_traceparent`](Self::with_traceparent) or
2520    /// [`with_tracestate`](Self::with_tracestate) to set them individually.
2521    pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
2522        self.traceparent = ctx.traceparent;
2523        self.tracestate = ctx.tracestate;
2524        self
2525    }
2526
2527    /// Set the W3C `traceparent` header for this turn.
2528    pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
2529        self.traceparent = Some(traceparent.into());
2530        self
2531    }
2532
2533    /// Set the W3C `tracestate` header for this turn.
2534    pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
2535        self.tracestate = Some(tracestate.into());
2536        self
2537    }
2538}
2539
2540impl From<&str> for MessageOptions {
2541    fn from(prompt: &str) -> Self {
2542        Self::new(prompt)
2543    }
2544}
2545
2546impl From<String> for MessageOptions {
2547    fn from(prompt: String) -> Self {
2548        Self::new(prompt)
2549    }
2550}
2551
2552impl From<&String> for MessageOptions {
2553    fn from(prompt: &String) -> Self {
2554        Self::new(prompt.clone())
2555    }
2556}
2557
2558/// Response from [`Client::get_status`](crate::Client::get_status).
2559#[derive(Debug, Clone, Serialize, Deserialize)]
2560#[serde(rename_all = "camelCase")]
2561#[non_exhaustive]
2562pub struct GetStatusResponse {
2563    /// Package version (e.g. `"1.0.0"`).
2564    pub version: String,
2565    /// Protocol version for SDK compatibility.
2566    pub protocol_version: u32,
2567}
2568
2569/// Response from [`Client::get_auth_status`](crate::Client::get_auth_status).
2570#[derive(Debug, Clone, Serialize, Deserialize)]
2571#[serde(rename_all = "camelCase")]
2572#[non_exhaustive]
2573pub struct GetAuthStatusResponse {
2574    /// Whether the user is authenticated.
2575    pub is_authenticated: bool,
2576    /// Authentication type (e.g. `"user"`, `"env"`, `"gh-cli"`, `"hmac"`,
2577    /// `"api-key"`, `"token"`).
2578    #[serde(skip_serializing_if = "Option::is_none")]
2579    pub auth_type: Option<String>,
2580    /// GitHub host URL.
2581    #[serde(skip_serializing_if = "Option::is_none")]
2582    pub host: Option<String>,
2583    /// User login name.
2584    #[serde(skip_serializing_if = "Option::is_none")]
2585    pub login: Option<String>,
2586    /// Human-readable status message.
2587    #[serde(skip_serializing_if = "Option::is_none")]
2588    pub status_message: Option<String>,
2589}
2590
2591/// Wrapper for session event notifications received from the CLI.
2592///
2593/// The CLI sends these as JSON-RPC notifications on the `session.event` method.
2594#[derive(Debug, Clone, Serialize, Deserialize)]
2595#[serde(rename_all = "camelCase")]
2596pub struct SessionEventNotification {
2597    /// The session this event belongs to.
2598    pub session_id: SessionId,
2599    /// The event payload.
2600    pub event: SessionEvent,
2601}
2602
2603/// A single event in a session's timeline.
2604///
2605/// Events form a linked chain via `parent_id`. The `event_type` string
2606/// identifies the kind (e.g. `"assistant.message_delta"`, `"session.idle"`,
2607/// `"tool.execution_start"`). Event-specific payload is in `data` as
2608/// untyped JSON.
2609#[derive(Debug, Clone, Serialize, Deserialize)]
2610#[serde(rename_all = "camelCase")]
2611pub struct SessionEvent {
2612    /// Unique event ID (UUID v4).
2613    pub id: String,
2614    /// ISO 8601 timestamp.
2615    pub timestamp: String,
2616    /// ID of the preceding event in the chain.
2617    pub parent_id: Option<String>,
2618    /// Transient events that are not persisted to disk.
2619    #[serde(skip_serializing_if = "Option::is_none")]
2620    pub ephemeral: Option<bool>,
2621    /// Sub-agent instance identifier. Absent for events emitted by the
2622    /// root/main agent and for session-level events.
2623    #[serde(skip_serializing_if = "Option::is_none")]
2624    pub agent_id: Option<String>,
2625    /// Debug timestamp: when the CLI received this event (ms since epoch).
2626    #[serde(skip_serializing_if = "Option::is_none")]
2627    pub debug_cli_received_at_ms: Option<i64>,
2628    /// Debug timestamp: when the event was forwarded over WebSocket.
2629    #[serde(skip_serializing_if = "Option::is_none")]
2630    pub debug_ws_forwarded_at_ms: Option<i64>,
2631    /// Event type string (e.g. `"assistant.message"`, `"session.idle"`).
2632    #[serde(rename = "type")]
2633    pub event_type: String,
2634    /// Event-specific data. Structure depends on `event_type`.
2635    pub data: Value,
2636}
2637
2638impl SessionEvent {
2639    /// Parse the string `event_type` into a typed [`SessionEventType`](crate::generated::SessionEventType) enum.
2640    ///
2641    /// Returns `SessionEventType::Unknown` for unrecognized event types,
2642    /// ensuring forward compatibility with newer CLI versions.
2643    pub fn parsed_type(&self) -> crate::generated::SessionEventType {
2644        use serde::de::IntoDeserializer;
2645        let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
2646            self.event_type.as_str().into_deserializer();
2647        crate::generated::SessionEventType::deserialize(deserializer)
2648            .unwrap_or(crate::generated::SessionEventType::Unknown)
2649    }
2650
2651    /// Deserialize the event `data` field into a typed struct.
2652    ///
2653    /// Returns `None` if deserialization fails (e.g. unknown event type
2654    /// or schema mismatch). Prefer typed data accessors for specific
2655    /// event types where you need strongly-typed field access.
2656    pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
2657        serde_json::from_value(self.data.clone()).ok()
2658    }
2659
2660    /// `model_call` errors are transient — the CLI agent loop continues
2661    /// after them and may succeed on the next turn. These should not be
2662    /// treated as session-ending errors.
2663    pub fn is_transient_error(&self) -> bool {
2664        self.event_type == "session.error"
2665            && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
2666    }
2667}
2668
2669/// A request from the CLI to invoke a client-defined tool.
2670///
2671/// Received as a JSON-RPC request on the `tool.call` method. The client
2672/// must respond with a [`ToolResultResponse`].
2673#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2674#[serde(rename_all = "camelCase")]
2675#[non_exhaustive]
2676pub struct ToolInvocation {
2677    /// Session that owns this tool call.
2678    pub session_id: SessionId,
2679    /// Unique ID for this tool call, used to correlate the response.
2680    pub tool_call_id: String,
2681    /// Name of the tool being invoked.
2682    pub tool_name: String,
2683    /// Tool arguments as JSON.
2684    pub arguments: Value,
2685    /// W3C Trace Context `traceparent` header propagated from the CLI's
2686    /// `execute_tool` span. Pass through to OpenTelemetry-aware code so
2687    /// child spans created inside the handler are parented to the CLI
2688    /// span. `None` when the CLI has no trace context for this call.
2689    #[serde(default, skip_serializing_if = "Option::is_none")]
2690    pub traceparent: Option<String>,
2691    /// W3C Trace Context `tracestate` paired with
2692    /// [`traceparent`](Self::traceparent).
2693    #[serde(default, skip_serializing_if = "Option::is_none")]
2694    pub tracestate: Option<String>,
2695}
2696
2697impl ToolInvocation {
2698    /// Deserialize this invocation's [`arguments`](Self::arguments) into a
2699    /// strongly-typed parameter struct.
2700    ///
2701    /// Idiomatic way to extract typed parameters when implementing
2702    /// [`ToolHandler`](crate::tool::ToolHandler) directly. Equivalent to
2703    /// `serde_json::from_value(invocation.arguments.clone())` with the SDK's
2704    /// error type.
2705    ///
2706    /// # Example
2707    ///
2708    /// ```rust,no_run
2709    /// # use github_copilot_sdk::{Error, types::ToolInvocation, ToolResult};
2710    /// # use serde::Deserialize;
2711    /// # #[derive(Deserialize)] struct MyParams { city: String }
2712    /// # async fn example(inv: ToolInvocation) -> Result<ToolResult, Error> {
2713    /// let params: MyParams = inv.params()?;
2714    /// // …use `inv.session_id` / `inv.tool_call_id` alongside `params`…
2715    /// # let _ = params; Ok(ToolResult::Text(String::new()))
2716    /// # }
2717    /// ```
2718    pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
2719        serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
2720    }
2721
2722    /// Returns the propagated [`TraceContext`] for this invocation, or
2723    /// [`TraceContext::default()`] when the CLI sent no headers.
2724    pub fn trace_context(&self) -> TraceContext {
2725        TraceContext {
2726            traceparent: self.traceparent.clone(),
2727            tracestate: self.tracestate.clone(),
2728        }
2729    }
2730}
2731
2732/// Expanded tool result with metadata for the LLM and session log.
2733#[derive(Debug, Clone, Serialize, Deserialize)]
2734#[serde(rename_all = "camelCase")]
2735pub struct ToolResultExpanded {
2736    /// Result text sent back to the LLM.
2737    pub text_result_for_llm: String,
2738    /// `"success"` or `"failure"`.
2739    pub result_type: String,
2740    /// Optional log message for the session timeline.
2741    #[serde(skip_serializing_if = "Option::is_none")]
2742    pub session_log: Option<String>,
2743    /// Error message, if the tool failed.
2744    #[serde(skip_serializing_if = "Option::is_none")]
2745    pub error: Option<String>,
2746}
2747
2748/// Result of a tool invocation — either a plain text string or an expanded result.
2749#[derive(Debug, Clone, Serialize, Deserialize)]
2750#[serde(untagged)]
2751#[non_exhaustive]
2752pub enum ToolResult {
2753    /// Simple text result passed directly to the LLM.
2754    Text(String),
2755    /// Structured result with metadata.
2756    Expanded(ToolResultExpanded),
2757}
2758
2759/// JSON-RPC response wrapper for a tool result, sent back to the CLI.
2760#[derive(Debug, Clone, Serialize, Deserialize)]
2761#[serde(rename_all = "camelCase")]
2762pub struct ToolResultResponse {
2763    /// The tool result payload.
2764    pub result: ToolResult,
2765}
2766
2767/// Metadata for a persisted session, returned by `session.list`.
2768#[derive(Debug, Clone, Serialize, Deserialize)]
2769#[serde(rename_all = "camelCase")]
2770pub struct SessionMetadata {
2771    /// The session's unique identifier.
2772    pub session_id: SessionId,
2773    /// ISO 8601 timestamp when the session was created.
2774    pub start_time: String,
2775    /// ISO 8601 timestamp of the last modification.
2776    pub modified_time: String,
2777    /// Agent-generated session summary.
2778    #[serde(skip_serializing_if = "Option::is_none")]
2779    pub summary: Option<String>,
2780    /// Whether the session is running remotely.
2781    pub is_remote: bool,
2782}
2783
2784/// Response from `session.list`.
2785#[derive(Debug, Clone, Serialize, Deserialize)]
2786#[serde(rename_all = "camelCase")]
2787pub struct ListSessionsResponse {
2788    /// The list of session metadata entries.
2789    pub sessions: Vec<SessionMetadata>,
2790}
2791
2792/// Filter options for [`Client::list_sessions`](crate::Client::list_sessions).
2793///
2794/// All fields are optional; unset fields don't constrain the result.
2795#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2796#[serde(rename_all = "camelCase")]
2797pub struct SessionListFilter {
2798    /// Filter by exact `cwd` match.
2799    #[serde(default, skip_serializing_if = "Option::is_none")]
2800    pub cwd: Option<String>,
2801    /// Filter by git root path.
2802    #[serde(default, skip_serializing_if = "Option::is_none")]
2803    pub git_root: Option<String>,
2804    /// Filter by repository in `owner/repo` form.
2805    #[serde(default, skip_serializing_if = "Option::is_none")]
2806    pub repository: Option<String>,
2807    /// Filter by git branch name.
2808    #[serde(default, skip_serializing_if = "Option::is_none")]
2809    pub branch: Option<String>,
2810}
2811
2812/// Response from `session.getMetadata`.
2813#[derive(Debug, Clone, Serialize, Deserialize)]
2814#[serde(rename_all = "camelCase")]
2815pub struct GetSessionMetadataResponse {
2816    /// The session metadata, or `None` if the session was not found.
2817    #[serde(skip_serializing_if = "Option::is_none")]
2818    pub session: Option<SessionMetadata>,
2819}
2820
2821/// Response from `session.getLastId`.
2822#[derive(Debug, Clone, Serialize, Deserialize)]
2823#[serde(rename_all = "camelCase")]
2824pub struct GetLastSessionIdResponse {
2825    /// The most recently updated session ID, or `None` if no sessions exist.
2826    #[serde(skip_serializing_if = "Option::is_none")]
2827    pub session_id: Option<SessionId>,
2828}
2829
2830/// Response from `session.getForeground`.
2831#[derive(Debug, Clone, Serialize, Deserialize)]
2832#[serde(rename_all = "camelCase")]
2833pub struct GetForegroundSessionResponse {
2834    /// The current foreground session ID, or `None` if no foreground session.
2835    #[serde(skip_serializing_if = "Option::is_none")]
2836    pub session_id: Option<SessionId>,
2837}
2838
2839/// Response from `session.getMessages`.
2840#[derive(Debug, Clone, Serialize, Deserialize)]
2841#[serde(rename_all = "camelCase")]
2842pub struct GetMessagesResponse {
2843    /// Timeline events for the session.
2844    pub events: Vec<SessionEvent>,
2845}
2846
2847/// Result of an elicitation (interactive UI form) request.
2848#[derive(Debug, Clone, Serialize, Deserialize)]
2849#[serde(rename_all = "camelCase")]
2850pub struct ElicitationResult {
2851    /// User's action: `"accept"`, `"decline"`, or `"cancel"`.
2852    pub action: String,
2853    /// Form data submitted by the user (present when action is `"accept"`).
2854    #[serde(skip_serializing_if = "Option::is_none")]
2855    pub content: Option<Value>,
2856}
2857
2858/// Elicitation display mode.
2859///
2860/// New modes may be added by the CLI in future protocol versions; the
2861/// `Unknown` variant keeps deserialization from failing on unrecognised
2862/// values so the SDK can still surface the request to callers.
2863#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2864#[serde(rename_all = "camelCase")]
2865#[non_exhaustive]
2866pub enum ElicitationMode {
2867    /// Structured form input rendered by the host.
2868    Form,
2869    /// Browser redirect to a URL.
2870    Url,
2871    /// A mode not yet known to this SDK version.
2872    #[serde(other)]
2873    Unknown,
2874}
2875
2876/// An incoming elicitation request from the CLI (provider side).
2877///
2878/// Received via `elicitation.requested` session event when the session was
2879/// created with `request_elicitation: true`. The provider should render a
2880/// form or dialog and return an [`ElicitationResult`].
2881#[derive(Debug, Clone, Serialize, Deserialize)]
2882#[serde(rename_all = "camelCase")]
2883pub struct ElicitationRequest {
2884    /// Message describing what information is needed from the user.
2885    pub message: String,
2886    /// JSON Schema describing the form fields to present.
2887    #[serde(skip_serializing_if = "Option::is_none")]
2888    pub requested_schema: Option<Value>,
2889    /// Elicitation display mode.
2890    #[serde(skip_serializing_if = "Option::is_none")]
2891    pub mode: Option<ElicitationMode>,
2892    /// The source that initiated the request (e.g. MCP server name).
2893    #[serde(skip_serializing_if = "Option::is_none")]
2894    pub elicitation_source: Option<String>,
2895    /// URL to open in the user's browser (url mode only).
2896    #[serde(skip_serializing_if = "Option::is_none")]
2897    pub url: Option<String>,
2898}
2899
2900/// Session-level capabilities reported by the CLI after session creation.
2901///
2902/// Capabilities indicate which features the CLI host supports for this session.
2903/// Updated at runtime via `capabilities.changed` events.
2904#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2905#[serde(rename_all = "camelCase")]
2906pub struct SessionCapabilities {
2907    /// UI capabilities (elicitation support, etc.).
2908    #[serde(skip_serializing_if = "Option::is_none")]
2909    pub ui: Option<UiCapabilities>,
2910}
2911
2912/// UI-specific capabilities for a session.
2913#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2914#[serde(rename_all = "camelCase")]
2915pub struct UiCapabilities {
2916    /// Whether the host supports interactive elicitation dialogs.
2917    #[serde(skip_serializing_if = "Option::is_none")]
2918    pub elicitation: Option<bool>,
2919}
2920
2921/// Options for the [`SessionUi::input`](crate::session::SessionUi::input) convenience method.
2922#[derive(Debug, Clone, Default)]
2923pub struct InputOptions<'a> {
2924    /// Title label for the input field.
2925    pub title: Option<&'a str>,
2926    /// Descriptive text shown below the field.
2927    pub description: Option<&'a str>,
2928    /// Minimum character length.
2929    pub min_length: Option<u64>,
2930    /// Maximum character length.
2931    pub max_length: Option<u64>,
2932    /// Semantic format hint.
2933    pub format: Option<InputFormat>,
2934    /// Default value pre-populated in the field.
2935    pub default: Option<&'a str>,
2936}
2937
2938/// Semantic format hints for text input fields.
2939#[derive(Debug, Clone, Copy)]
2940#[non_exhaustive]
2941pub enum InputFormat {
2942    /// Email address.
2943    Email,
2944    /// URI.
2945    Uri,
2946    /// Calendar date.
2947    Date,
2948    /// Date and time.
2949    DateTime,
2950}
2951
2952impl InputFormat {
2953    /// Returns the JSON Schema format string for this variant.
2954    pub fn as_str(&self) -> &'static str {
2955        match self {
2956            Self::Email => "email",
2957            Self::Uri => "uri",
2958            Self::Date => "date",
2959            Self::DateTime => "date-time",
2960        }
2961    }
2962}
2963
2964/// Re-exports of generated protocol types that are part of the SDK's
2965/// public API surface. The canonical definitions live in
2966/// [`crate::generated::api_types`]; they live here so the crate-root
2967/// `pub use types::*` surfaces them alongside hand-written SDK types.
2968pub use crate::generated::api_types::{
2969    Model, ModelBilling, ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
2970    ModelCapabilitiesSupports, ModelList, ModelPolicy,
2971};
2972
2973/// Permission categories the CLI may request approval for.
2974///
2975/// Wire values are the lower-kebab strings the CLI sends as the `kind`
2976/// discriminator on a permission request. Marked `#[non_exhaustive]`
2977/// because the CLI may add new kinds; matches must include a `_` arm.
2978#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2979#[serde(rename_all = "kebab-case")]
2980#[non_exhaustive]
2981pub enum PermissionRequestKind {
2982    /// Run a shell command.
2983    Shell,
2984    /// Write to a file.
2985    Write,
2986    /// Read a file.
2987    Read,
2988    /// Open a URL.
2989    Url,
2990    /// Invoke an MCP server tool.
2991    Mcp,
2992    /// Invoke a client-defined custom tool.
2993    CustomTool,
2994    /// Update agent memory.
2995    Memory,
2996    /// Run a hook callback.
2997    Hook,
2998    /// Unrecognized kind. The original wire string is available in
2999    /// [`PermissionRequestData::extra`] under the `kind` key.
3000    #[serde(other)]
3001    Unknown,
3002}
3003
3004/// Data sent by the CLI for permission-related events.
3005///
3006/// Used for both the `permission.request` RPC call (which expects a response)
3007/// and `permission.requested` notifications (fire-and-forget). Contains the
3008/// full params object. Note that `requestId` is also available as a separate
3009/// field on `HandlerEvent::PermissionRequest`.
3010#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3011#[serde(rename_all = "camelCase")]
3012pub struct PermissionRequestData {
3013    /// The permission category being requested. `None` means the CLI did
3014    /// not include a `kind` field. Use this to branch on common cases
3015    /// (shell, write, etc.) without parsing [`extra`](Self::extra).
3016    #[serde(default, skip_serializing_if = "Option::is_none")]
3017    pub kind: Option<PermissionRequestKind>,
3018    /// The originating tool-call ID, if this permission request is tied
3019    /// to a specific tool invocation.
3020    #[serde(default, skip_serializing_if = "Option::is_none")]
3021    pub tool_call_id: Option<String>,
3022    /// The full permission request params from the CLI. The shape varies by
3023    /// permission type and CLI version, so we preserve it as `Value`.
3024    #[serde(flatten)]
3025    pub extra: Value,
3026}
3027
3028/// Data sent by the CLI with an `exitPlanMode.request` RPC call.
3029#[derive(Debug, Clone, Serialize, Deserialize)]
3030#[serde(rename_all = "camelCase")]
3031pub struct ExitPlanModeData {
3032    /// Markdown summary of the plan presented to the user.
3033    #[serde(default)]
3034    pub summary: String,
3035    /// Full plan content (e.g. the plan.md body), if available.
3036    #[serde(default, skip_serializing_if = "Option::is_none")]
3037    pub plan_content: Option<String>,
3038    /// Allowed exit actions (e.g. "interactive", "autopilot", "autopilot_fleet").
3039    #[serde(default)]
3040    pub actions: Vec<String>,
3041    /// Which action the CLI recommends, defaults to "autopilot".
3042    #[serde(default = "default_recommended_action")]
3043    pub recommended_action: String,
3044}
3045
3046fn default_recommended_action() -> String {
3047    "autopilot".to_string()
3048}
3049
3050impl Default for ExitPlanModeData {
3051    fn default() -> Self {
3052        Self {
3053            summary: String::new(),
3054            plan_content: None,
3055            actions: Vec::new(),
3056            recommended_action: default_recommended_action(),
3057        }
3058    }
3059}
3060
3061#[cfg(test)]
3062mod tests {
3063    use std::path::PathBuf;
3064
3065    use serde_json::json;
3066
3067    use super::{
3068        Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange,
3069        ConnectionState, CustomAgentConfig, DeliveryMode, GitHubReferenceType,
3070        InfiniteSessionConfig, ProviderConfig, ResumeSessionConfig, SessionConfig, SessionEvent,
3071        SessionId, SystemMessageConfig, Tool, ensure_attachment_display_names,
3072    };
3073    use crate::generated::session_events::TypedSessionEvent;
3074
3075    #[test]
3076    fn tool_builder_composes() {
3077        let tool = Tool::new("greet")
3078            .with_description("Say hello")
3079            .with_namespaced_name("hello/greet")
3080            .with_instructions("Pass the user's name")
3081            .with_parameters(json!({
3082                "type": "object",
3083                "properties": { "name": { "type": "string" } },
3084                "required": ["name"]
3085            }))
3086            .with_overrides_built_in_tool(true)
3087            .with_skip_permission(true);
3088        assert_eq!(tool.name, "greet");
3089        assert_eq!(tool.description, "Say hello");
3090        assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
3091        assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
3092        assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
3093        assert!(tool.overrides_built_in_tool);
3094        assert!(tool.skip_permission);
3095    }
3096
3097    #[test]
3098    fn tool_with_parameters_handles_non_object_value() {
3099        let tool = Tool::new("noop").with_parameters(json!(null));
3100        assert!(tool.parameters.is_empty());
3101    }
3102
3103    #[test]
3104    fn session_config_default_enables_permission_flow_flags() {
3105        let cfg = SessionConfig::default();
3106        assert_eq!(cfg.request_user_input, Some(true));
3107        assert_eq!(cfg.request_permission, Some(true));
3108        assert_eq!(cfg.request_exit_plan_mode, Some(true));
3109        assert_eq!(cfg.request_auto_mode_switch, Some(true));
3110        assert_eq!(cfg.request_elicitation, Some(true));
3111    }
3112
3113    #[test]
3114    fn resume_session_config_new_enables_permission_flow_flags() {
3115        let cfg = ResumeSessionConfig::new(SessionId::from("test-id"));
3116        assert_eq!(cfg.request_user_input, Some(true));
3117        assert_eq!(cfg.request_permission, Some(true));
3118        assert_eq!(cfg.request_exit_plan_mode, Some(true));
3119        assert_eq!(cfg.request_auto_mode_switch, Some(true));
3120        assert_eq!(cfg.request_elicitation, Some(true));
3121    }
3122
3123    #[test]
3124    fn session_config_builder_composes() {
3125        use std::collections::HashMap;
3126
3127        let cfg = SessionConfig::default()
3128            .with_session_id(SessionId::from("sess-1"))
3129            .with_model("claude-sonnet-4")
3130            .with_client_name("test-app")
3131            .with_reasoning_effort("medium")
3132            .with_streaming(true)
3133            .with_tools([Tool::new("greet")])
3134            .with_available_tools(["bash", "view"])
3135            .with_excluded_tools(["dangerous"])
3136            .with_mcp_servers(HashMap::new())
3137            .with_env_value_mode("direct")
3138            .with_enable_config_discovery(true)
3139            .with_request_user_input(false)
3140            .with_skill_directories([PathBuf::from("/tmp/skills")])
3141            .with_disabled_skills(["broken-skill"])
3142            .with_disabled_mcp_servers(["broken-server"])
3143            .with_agent("researcher")
3144            .with_config_dir(PathBuf::from("/tmp/config"))
3145            .with_working_directory(PathBuf::from("/tmp/work"))
3146            .with_github_token("ghp_test")
3147            .with_include_sub_agent_streaming_events(false);
3148
3149        assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
3150        assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
3151        assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
3152        assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
3153        assert_eq!(cfg.streaming, Some(true));
3154        assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
3155        assert_eq!(
3156            cfg.available_tools.as_deref(),
3157            Some(&["bash".to_string(), "view".to_string()][..])
3158        );
3159        assert_eq!(
3160            cfg.excluded_tools.as_deref(),
3161            Some(&["dangerous".to_string()][..])
3162        );
3163        assert!(cfg.mcp_servers.is_some());
3164        assert_eq!(cfg.env_value_mode.as_deref(), Some("direct"));
3165        assert_eq!(cfg.enable_config_discovery, Some(true));
3166        assert_eq!(cfg.request_user_input, Some(false)); // overrode default
3167        assert_eq!(cfg.request_permission, Some(true)); // default preserved
3168        assert_eq!(
3169            cfg.skill_directories.as_deref(),
3170            Some(&[PathBuf::from("/tmp/skills")][..])
3171        );
3172        assert_eq!(
3173            cfg.disabled_skills.as_deref(),
3174            Some(&["broken-skill".to_string()][..])
3175        );
3176        assert_eq!(cfg.agent.as_deref(), Some("researcher"));
3177        assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config")));
3178        assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
3179        assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
3180        assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
3181    }
3182
3183    #[test]
3184    fn resume_session_config_builder_composes() {
3185        use std::collections::HashMap;
3186
3187        let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
3188            .with_client_name("test-app")
3189            .with_streaming(true)
3190            .with_tools([Tool::new("greet")])
3191            .with_excluded_tools(["dangerous"])
3192            .with_mcp_servers(HashMap::new())
3193            .with_env_value_mode("indirect")
3194            .with_enable_config_discovery(true)
3195            .with_request_user_input(false)
3196            .with_skill_directories([PathBuf::from("/tmp/skills")])
3197            .with_agent("researcher")
3198            .with_config_dir(PathBuf::from("/tmp/config"))
3199            .with_working_directory(PathBuf::from("/tmp/work"))
3200            .with_github_token("ghp_test")
3201            .with_include_sub_agent_streaming_events(true)
3202            .with_disable_resume(true)
3203            .with_continue_pending_work(true);
3204
3205        assert_eq!(cfg.session_id.as_str(), "sess-2");
3206        assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
3207        assert_eq!(cfg.streaming, Some(true));
3208        assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
3209        assert_eq!(
3210            cfg.excluded_tools.as_deref(),
3211            Some(&["dangerous".to_string()][..])
3212        );
3213        assert!(cfg.mcp_servers.is_some());
3214        assert_eq!(cfg.env_value_mode.as_deref(), Some("indirect"));
3215        assert_eq!(cfg.enable_config_discovery, Some(true));
3216        assert_eq!(cfg.request_user_input, Some(false)); // overrode default
3217        assert_eq!(cfg.request_permission, Some(true)); // default preserved
3218        assert_eq!(
3219            cfg.skill_directories.as_deref(),
3220            Some(&[PathBuf::from("/tmp/skills")][..])
3221        );
3222        assert_eq!(cfg.agent.as_deref(), Some("researcher"));
3223        assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config")));
3224        assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
3225        assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
3226        assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
3227        assert_eq!(cfg.disable_resume, Some(true));
3228        assert_eq!(cfg.continue_pending_work, Some(true));
3229    }
3230
3231    /// `continue_pending_work` must serialize to wire as `continuePendingWork`
3232    /// — the runtime keys off this exact field name to opt into the
3233    /// pending-work-handoff pattern.
3234    #[test]
3235    fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
3236        let cfg =
3237            ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
3238        let wire = serde_json::to_value(&cfg).unwrap();
3239        assert_eq!(wire["continuePendingWork"], true);
3240
3241        // Unset case — skip_serializing_if must omit the field.
3242        let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"));
3243        let wire = serde_json::to_value(&cfg).unwrap();
3244        assert!(wire.get("continuePendingWork").is_none());
3245    }
3246
3247    /// `instruction_directories` must serialize to wire as
3248    /// `instructionDirectories` on `SessionConfig`. Cross-SDK parity field
3249    /// (Node/Python pass it through to the CLI verbatim).
3250    #[test]
3251    fn session_config_serializes_instruction_directories_to_camel_case() {
3252        let cfg =
3253            SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
3254        let wire = serde_json::to_value(&cfg).unwrap();
3255        assert_eq!(
3256            wire["instructionDirectories"],
3257            serde_json::json!(["/tmp/instr"])
3258        );
3259
3260        // Unset case — skip_serializing_if must omit the field.
3261        let cfg = SessionConfig::default();
3262        let wire = serde_json::to_value(&cfg).unwrap();
3263        assert!(wire.get("instructionDirectories").is_none());
3264    }
3265
3266    /// Same check on the resume path. Forwarded to the CLI on
3267    /// `session.resume`.
3268    #[test]
3269    fn resume_session_config_serializes_instruction_directories_to_camel_case() {
3270        let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
3271            .with_instruction_directories([PathBuf::from("/tmp/instr")]);
3272        let wire = serde_json::to_value(&cfg).unwrap();
3273        assert_eq!(
3274            wire["instructionDirectories"],
3275            serde_json::json!(["/tmp/instr"])
3276        );
3277
3278        let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"));
3279        let wire = serde_json::to_value(&cfg).unwrap();
3280        assert!(wire.get("instructionDirectories").is_none());
3281    }
3282
3283    #[test]
3284    fn custom_agent_config_builder_composes() {
3285        use std::collections::HashMap;
3286
3287        let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
3288            .with_display_name("Research Assistant")
3289            .with_description("Investigates technical questions.")
3290            .with_tools(["bash", "view"])
3291            .with_mcp_servers(HashMap::new())
3292            .with_infer(true)
3293            .with_skills(["rust-coding-skill"]);
3294
3295        assert_eq!(cfg.name, "researcher");
3296        assert_eq!(cfg.prompt, "You are a research assistant.");
3297        assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
3298        assert_eq!(
3299            cfg.description.as_deref(),
3300            Some("Investigates technical questions.")
3301        );
3302        assert_eq!(
3303            cfg.tools.as_deref(),
3304            Some(&["bash".to_string(), "view".to_string()][..])
3305        );
3306        assert!(cfg.mcp_servers.is_some());
3307        assert_eq!(cfg.infer, Some(true));
3308        assert_eq!(
3309            cfg.skills.as_deref(),
3310            Some(&["rust-coding-skill".to_string()][..])
3311        );
3312    }
3313
3314    #[test]
3315    fn infinite_session_config_builder_composes() {
3316        let cfg = InfiniteSessionConfig::new()
3317            .with_enabled(true)
3318            .with_background_compaction_threshold(0.75)
3319            .with_buffer_exhaustion_threshold(0.92);
3320
3321        assert_eq!(cfg.enabled, Some(true));
3322        assert_eq!(cfg.background_compaction_threshold, Some(0.75));
3323        assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
3324    }
3325
3326    #[test]
3327    fn provider_config_builder_composes() {
3328        use std::collections::HashMap;
3329
3330        let mut headers = HashMap::new();
3331        headers.insert("X-Custom".to_string(), "value".to_string());
3332
3333        let cfg = ProviderConfig::new("https://api.example.com")
3334            .with_provider_type("openai")
3335            .with_wire_api("completions")
3336            .with_api_key("sk-test")
3337            .with_bearer_token("bearer-test")
3338            .with_headers(headers)
3339            .with_model_id("gpt-4")
3340            .with_wire_model("azure-gpt-4-deployment")
3341            .with_max_prompt_tokens(8192)
3342            .with_max_output_tokens(2048);
3343
3344        assert_eq!(cfg.base_url, "https://api.example.com");
3345        assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
3346        assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
3347        assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
3348        assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
3349        assert_eq!(
3350            cfg.headers
3351                .as_ref()
3352                .and_then(|h| h.get("X-Custom"))
3353                .map(String::as_str),
3354            Some("value"),
3355        );
3356        assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
3357        assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
3358        assert_eq!(cfg.max_prompt_tokens, Some(8192));
3359        assert_eq!(cfg.max_output_tokens, Some(2048));
3360
3361        // Wire-shape: camelCase, skip_serializing_if when unset.
3362        let wire = serde_json::to_value(&cfg).unwrap();
3363        assert_eq!(wire["modelId"], "gpt-4");
3364        assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
3365        assert_eq!(wire["maxPromptTokens"], 8192);
3366        assert_eq!(wire["maxOutputTokens"], 2048);
3367
3368        let unset = ProviderConfig::new("https://api.example.com");
3369        let wire_unset = serde_json::to_value(&unset).unwrap();
3370        assert!(wire_unset.get("modelId").is_none());
3371        assert!(wire_unset.get("wireModel").is_none());
3372        assert!(wire_unset.get("maxPromptTokens").is_none());
3373        assert!(wire_unset.get("maxOutputTokens").is_none());
3374    }
3375
3376    #[test]
3377    fn system_message_config_builder_composes() {
3378        use std::collections::HashMap;
3379
3380        let cfg = SystemMessageConfig::new()
3381            .with_mode("replace")
3382            .with_content("Custom system message.")
3383            .with_sections(HashMap::new());
3384
3385        assert_eq!(cfg.mode.as_deref(), Some("replace"));
3386        assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
3387        assert!(cfg.sections.is_some());
3388    }
3389
3390    #[test]
3391    fn delivery_mode_serializes_to_kebab_case_strings() {
3392        assert_eq!(
3393            serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
3394            "\"enqueue\""
3395        );
3396        assert_eq!(
3397            serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
3398            "\"immediate\""
3399        );
3400        let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
3401        assert_eq!(parsed, DeliveryMode::Immediate);
3402    }
3403
3404    #[test]
3405    fn connection_state_error_serializes_to_match_go() {
3406        let json = serde_json::to_string(&ConnectionState::Error).unwrap();
3407        assert_eq!(json, "\"error\"");
3408        let parsed: ConnectionState = serde_json::from_str("\"error\"").unwrap();
3409        assert_eq!(parsed, ConnectionState::Error);
3410    }
3411
3412    /// `agentId` is the sub-agent attribution field added in copilot-sdk
3413    /// commit f8cf846 ("Derive session event envelopes from schema").
3414    /// Every other SDK (Node, Python, Go, .NET) carries it on the event
3415    /// envelope; Rust must too or sub-agent events lose attribution at
3416    /// the deserialization boundary. Cross-SDK parity test.
3417    #[test]
3418    fn session_event_round_trips_agent_id_on_envelope() {
3419        let wire = json!({
3420            "id": "evt-1",
3421            "timestamp": "2026-04-30T12:00:00Z",
3422            "parentId": null,
3423            "agentId": "sub-agent-42",
3424            "type": "assistant.message",
3425            "data": { "message": "hi" }
3426        });
3427
3428        let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
3429        assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
3430
3431        // Round-trip preserves the field on the wire.
3432        let roundtripped = serde_json::to_value(&event).unwrap();
3433        assert_eq!(roundtripped["agentId"], "sub-agent-42");
3434
3435        // Absent agentId remains absent (skip_serializing_if).
3436        let main_agent_event: SessionEvent = serde_json::from_value(json!({
3437            "id": "evt-2",
3438            "timestamp": "2026-04-30T12:00:01Z",
3439            "parentId": null,
3440            "type": "session.idle",
3441            "data": {}
3442        }))
3443        .unwrap();
3444        assert!(main_agent_event.agent_id.is_none());
3445        let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
3446        assert!(roundtripped.get("agentId").is_none());
3447    }
3448
3449    /// Same parity for the typed event envelope produced by the codegen.
3450    #[test]
3451    fn typed_session_event_round_trips_agent_id_on_envelope() {
3452        let wire = json!({
3453            "id": "evt-1",
3454            "timestamp": "2026-04-30T12:00:00Z",
3455            "parentId": null,
3456            "agentId": "sub-agent-42",
3457            "type": "session.idle",
3458            "data": {}
3459        });
3460
3461        let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
3462        assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
3463
3464        let roundtripped = serde_json::to_value(&event).unwrap();
3465        assert_eq!(roundtripped["agentId"], "sub-agent-42");
3466    }
3467
3468    #[test]
3469    fn connection_state_other_variants_serialize_as_lowercase() {
3470        assert_eq!(
3471            serde_json::to_string(&ConnectionState::Disconnected).unwrap(),
3472            "\"disconnected\""
3473        );
3474        assert_eq!(
3475            serde_json::to_string(&ConnectionState::Connecting).unwrap(),
3476            "\"connecting\""
3477        );
3478        assert_eq!(
3479            serde_json::to_string(&ConnectionState::Connected).unwrap(),
3480            "\"connected\""
3481        );
3482    }
3483
3484    #[test]
3485    fn deserializes_runtime_attachment_variants() {
3486        let attachments: Vec<Attachment> = serde_json::from_value(json!([
3487            {
3488                "type": "file",
3489                "path": "/tmp/file.rs",
3490                "displayName": "file.rs",
3491                "lineRange": { "start": 7, "end": 12 }
3492            },
3493            {
3494                "type": "directory",
3495                "path": "/tmp/project",
3496                "displayName": "project"
3497            },
3498            {
3499                "type": "selection",
3500                "filePath": "/tmp/lib.rs",
3501                "displayName": "lib.rs",
3502                "text": "fn main() {}",
3503                "selection": {
3504                    "start": { "line": 1, "character": 2 },
3505                    "end": { "line": 3, "character": 4 }
3506                }
3507            },
3508            {
3509                "type": "blob",
3510                "data": "Zm9v",
3511                "mimeType": "image/png",
3512                "displayName": "image.png"
3513            },
3514            {
3515                "type": "github_reference",
3516                "number": 42,
3517                "title": "Fix rendering",
3518                "referenceType": "issue",
3519                "state": "open",
3520                "url": "https://github.com/example/repo/issues/42"
3521            }
3522        ]))
3523        .expect("attachments should deserialize");
3524
3525        assert_eq!(attachments.len(), 5);
3526        assert!(matches!(
3527            &attachments[0],
3528            Attachment::File {
3529                path,
3530                display_name,
3531                line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
3532            } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
3533        ));
3534        assert!(matches!(
3535            &attachments[1],
3536            Attachment::Directory { path, display_name }
3537                if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
3538        ));
3539        assert!(matches!(
3540            &attachments[2],
3541            Attachment::Selection {
3542                file_path,
3543                display_name,
3544                selection:
3545                    AttachmentSelectionRange {
3546                        start: AttachmentSelectionPosition { line: 1, character: 2 },
3547                        end: AttachmentSelectionPosition { line: 3, character: 4 },
3548                    },
3549                ..
3550            } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
3551        ));
3552        assert!(matches!(
3553            &attachments[3],
3554            Attachment::Blob {
3555                data,
3556                mime_type,
3557                display_name,
3558            } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
3559        ));
3560        assert!(matches!(
3561            &attachments[4],
3562            Attachment::GitHubReference {
3563                number: 42,
3564                title,
3565                reference_type: GitHubReferenceType::Issue,
3566                state,
3567                url,
3568            } if title == "Fix rendering"
3569                && state == "open"
3570                && url == "https://github.com/example/repo/issues/42"
3571        ));
3572    }
3573
3574    #[test]
3575    fn ensures_display_names_for_variants_that_support_them() {
3576        let mut attachments = vec![
3577            Attachment::File {
3578                path: PathBuf::from("/tmp/file.rs"),
3579                display_name: None,
3580                line_range: None,
3581            },
3582            Attachment::Selection {
3583                file_path: PathBuf::from("/tmp/src/lib.rs"),
3584                display_name: None,
3585                text: "fn main() {}".to_string(),
3586                selection: AttachmentSelectionRange {
3587                    start: AttachmentSelectionPosition {
3588                        line: 0,
3589                        character: 0,
3590                    },
3591                    end: AttachmentSelectionPosition {
3592                        line: 0,
3593                        character: 10,
3594                    },
3595                },
3596            },
3597            Attachment::Blob {
3598                data: "Zm9v".to_string(),
3599                mime_type: "image/png".to_string(),
3600                display_name: None,
3601            },
3602            Attachment::GitHubReference {
3603                number: 7,
3604                title: "Track regressions".to_string(),
3605                reference_type: GitHubReferenceType::Issue,
3606                state: "open".to_string(),
3607                url: "https://example.com/issues/7".to_string(),
3608            },
3609        ];
3610
3611        ensure_attachment_display_names(&mut attachments);
3612
3613        assert_eq!(attachments[0].display_name(), Some("file.rs"));
3614        assert_eq!(attachments[1].display_name(), Some("lib.rs"));
3615        assert_eq!(attachments[2].display_name(), Some("attachment"));
3616        assert_eq!(attachments[3].display_name(), None);
3617        assert_eq!(
3618            attachments[3].label(),
3619            Some("Track regressions".to_string())
3620        );
3621    }
3622}
3623
3624#[cfg(test)]
3625mod permission_builder_tests {
3626    use std::sync::Arc;
3627
3628    use crate::handler::{
3629        ApproveAllHandler, HandlerEvent, HandlerResponse, PermissionResult, SessionHandler,
3630    };
3631    use crate::types::{
3632        PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, SessionId,
3633    };
3634
3635    fn permission_event() -> HandlerEvent {
3636        HandlerEvent::PermissionRequest {
3637            session_id: SessionId::from("s1"),
3638            request_id: RequestId::new("1"),
3639            data: PermissionRequestData {
3640                extra: serde_json::json!({"tool": "shell"}),
3641                ..Default::default()
3642            },
3643        }
3644    }
3645
3646    async fn dispatch(handler: &Arc<dyn SessionHandler>) -> HandlerResponse {
3647        handler.on_event(permission_event()).await
3648    }
3649
3650    #[tokio::test]
3651    async fn session_config_approve_all_wraps_existing_handler() {
3652        let cfg = SessionConfig::default()
3653            .with_handler(Arc::new(ApproveAllHandler))
3654            .approve_all_permissions();
3655        let handler = cfg.handler.expect("handler should be set");
3656        match dispatch(&handler).await {
3657            HandlerResponse::Permission(PermissionResult::Approved) => {}
3658            other => panic!("expected Approved, got {other:?}"),
3659        }
3660    }
3661
3662    #[tokio::test]
3663    async fn session_config_approve_all_defaults_to_deny_inner() {
3664        // Without with_handler, the wrap defaults to DenyAllHandler. The
3665        // approve-all wrap intercepts permission events, so they're still
3666        // approved -- the inner handler is consulted only for other events.
3667        let cfg = SessionConfig::default().approve_all_permissions();
3668        let handler = cfg.handler.expect("handler should be set");
3669        match dispatch(&handler).await {
3670            HandlerResponse::Permission(PermissionResult::Approved) => {}
3671            other => panic!("expected Approved, got {other:?}"),
3672        }
3673    }
3674
3675    #[tokio::test]
3676    async fn session_config_deny_all_denies() {
3677        let cfg = SessionConfig::default()
3678            .with_handler(Arc::new(ApproveAllHandler))
3679            .deny_all_permissions();
3680        let handler = cfg.handler.expect("handler should be set");
3681        match dispatch(&handler).await {
3682            HandlerResponse::Permission(PermissionResult::Denied) => {}
3683            other => panic!("expected Denied, got {other:?}"),
3684        }
3685    }
3686
3687    #[tokio::test]
3688    async fn session_config_approve_permissions_if_consults_predicate() {
3689        let cfg = SessionConfig::default()
3690            .with_handler(Arc::new(ApproveAllHandler))
3691            .approve_permissions_if(|data| {
3692                data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
3693            });
3694        let handler = cfg.handler.expect("handler should be set");
3695        match dispatch(&handler).await {
3696            HandlerResponse::Permission(PermissionResult::Denied) => {}
3697            other => panic!("expected Denied for shell, got {other:?}"),
3698        }
3699    }
3700
3701    #[tokio::test]
3702    async fn resume_session_config_approve_all_wraps_existing_handler() {
3703        let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
3704            .with_handler(Arc::new(ApproveAllHandler))
3705            .approve_all_permissions();
3706        let handler = cfg.handler.expect("handler should be set");
3707        match dispatch(&handler).await {
3708            HandlerResponse::Permission(PermissionResult::Approved) => {}
3709            other => panic!("expected Approved, got {other:?}"),
3710        }
3711    }
3712}