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