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