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 indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::canvas::{CanvasDeclaration, CanvasHandler};
17pub use crate::copilot_request_handler::{
18    CopilotHttpRequest, CopilotHttpResponse, CopilotHttpResponseBody, CopilotRequestContext,
19    CopilotRequestError, CopilotRequestHandler, CopilotRequestTransport, CopilotWebSocketForwarder,
20    CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage,
21    CopilotWebSocketResponse, WebSocketTransform, forward_http,
22};
23use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance};
24use crate::generated::session_events::ReasoningSummary;
25/// Context window tier for models that support tiered context windows.
26pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
27use crate::handler::{
28    AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
29    PermissionHandler, UserInputHandler,
30};
31use crate::hooks::SessionHooks;
32use crate::provider_token::BearerTokenProvider;
33pub use crate::session_fs::{
34    DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
35    SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
36    SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
37    SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
38};
39pub use crate::trace_context::{TraceContext, TraceContextProvider};
40use crate::transforms::SystemMessageTransform;
41
42/// Lifecycle state of a [`Client`](crate::Client) connection. Internal —
43/// not part of the public API.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[allow(dead_code)]
46#[non_exhaustive]
47pub(crate) enum ConnectionState {
48    /// No CLI process is attached or the process has exited cleanly.
49    Disconnected,
50    /// The client is starting up (spawning the CLI, negotiating protocol).
51    Connecting,
52    /// The client is connected and ready to handle RPC traffic.
53    Connected,
54    /// Startup failed or the connection encountered an unrecoverable error.
55    Error,
56}
57
58/// Type of [`SessionLifecycleEvent`] received via [`Client::subscribe_lifecycle`](crate::Client::subscribe_lifecycle).
59///
60/// Values serialize as the dotted JSON strings the CLI sends (e.g.
61/// `"session.created"`).
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[non_exhaustive]
64pub enum SessionLifecycleEventType {
65    /// A new session was created.
66    #[serde(rename = "session.created")]
67    Created,
68    /// A session was deleted.
69    #[serde(rename = "session.deleted")]
70    Deleted,
71    /// A session's metadata was updated (e.g. summary regenerated).
72    #[serde(rename = "session.updated")]
73    Updated,
74    /// A session moved into the foreground.
75    #[serde(rename = "session.foreground")]
76    Foreground,
77    /// A session moved into the background.
78    #[serde(rename = "session.background")]
79    Background,
80}
81
82/// Optional metadata attached to a [`SessionLifecycleEvent`].
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct SessionLifecycleEventMetadata {
85    /// ISO-8601 timestamp the session was created.
86    #[serde(rename = "startTime")]
87    pub start_time: String,
88    /// ISO-8601 timestamp the session was last modified.
89    #[serde(rename = "modifiedTime")]
90    pub modified_time: String,
91    /// Optional generated summary of the session conversation so far.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub summary: Option<String>,
94}
95
96/// A `session.lifecycle` notification dispatched to subscribers obtained via
97/// [`Client::subscribe_lifecycle`](crate::Client::subscribe_lifecycle).
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct SessionLifecycleEvent {
100    /// The kind of lifecycle change this event represents.
101    #[serde(rename = "type")]
102    pub event_type: SessionLifecycleEventType,
103    /// Identifier of the session this event refers to.
104    #[serde(rename = "sessionId")]
105    pub session_id: SessionId,
106    /// Optional metadata describing the session at the time of the event.
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub metadata: Option<SessionLifecycleEventMetadata>,
109}
110
111/// Opaque session identifier assigned by the CLI.
112///
113/// A newtype wrapper around `String` that provides type safety — prevents
114/// accidentally passing a workspace ID or request ID where a session ID
115/// is expected. Derefs to `str` for zero-friction borrowing.
116#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
117#[serde(transparent)]
118pub struct SessionId(String);
119
120impl SessionId {
121    /// Create a new session ID from any string-like value.
122    pub fn new(id: impl Into<String>) -> Self {
123        Self(id.into())
124    }
125
126    /// Borrow the inner string.
127    pub fn as_str(&self) -> &str {
128        &self.0
129    }
130
131    /// Consume the wrapper, returning the inner string.
132    pub fn into_inner(self) -> String {
133        self.0
134    }
135}
136
137impl std::ops::Deref for SessionId {
138    type Target = str;
139
140    fn deref(&self) -> &str {
141        &self.0
142    }
143}
144
145impl std::fmt::Display for SessionId {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.write_str(&self.0)
148    }
149}
150
151impl From<String> for SessionId {
152    fn from(s: String) -> Self {
153        Self(s)
154    }
155}
156
157impl From<&str> for SessionId {
158    fn from(s: &str) -> Self {
159        Self(s.to_owned())
160    }
161}
162
163impl AsRef<str> for SessionId {
164    fn as_ref(&self) -> &str {
165        &self.0
166    }
167}
168
169impl std::borrow::Borrow<str> for SessionId {
170    fn borrow(&self) -> &str {
171        &self.0
172    }
173}
174
175impl From<SessionId> for String {
176    fn from(id: SessionId) -> String {
177        id.0
178    }
179}
180
181impl PartialEq<str> for SessionId {
182    fn eq(&self, other: &str) -> bool {
183        self.0 == other
184    }
185}
186
187impl PartialEq<String> for SessionId {
188    fn eq(&self, other: &String) -> bool {
189        &self.0 == other
190    }
191}
192
193impl PartialEq<SessionId> for String {
194    fn eq(&self, other: &SessionId) -> bool {
195        self == &other.0
196    }
197}
198
199impl PartialEq<&str> for SessionId {
200    fn eq(&self, other: &&str) -> bool {
201        self.0 == *other
202    }
203}
204
205impl PartialEq<&SessionId> for SessionId {
206    fn eq(&self, other: &&SessionId) -> bool {
207        self.0 == other.0
208    }
209}
210
211impl PartialEq<SessionId> for &SessionId {
212    fn eq(&self, other: &SessionId) -> bool {
213        self.0 == other.0
214    }
215}
216
217/// Opaque request identifier for pending CLI requests (permission, user-input, etc.).
218///
219/// A newtype wrapper around `String` that provides type safety — prevents
220/// accidentally passing a session ID or workspace ID where a request ID
221/// is expected. Derefs to `str` for zero-friction borrowing.
222#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
223#[serde(transparent)]
224pub struct RequestId(String);
225
226impl RequestId {
227    /// Create a new request ID from any string-like value.
228    pub fn new(id: impl Into<String>) -> Self {
229        Self(id.into())
230    }
231
232    /// Consume the wrapper, returning the inner string.
233    pub fn into_inner(self) -> String {
234        self.0
235    }
236}
237
238impl std::ops::Deref for RequestId {
239    type Target = str;
240
241    fn deref(&self) -> &str {
242        &self.0
243    }
244}
245
246impl std::fmt::Display for RequestId {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        f.write_str(&self.0)
249    }
250}
251
252impl From<String> for RequestId {
253    fn from(s: String) -> Self {
254        Self(s)
255    }
256}
257
258impl From<&str> for RequestId {
259    fn from(s: &str) -> Self {
260        Self(s.to_owned())
261    }
262}
263
264impl AsRef<str> for RequestId {
265    fn as_ref(&self) -> &str {
266        &self.0
267    }
268}
269
270impl std::borrow::Borrow<str> for RequestId {
271    fn borrow(&self) -> &str {
272        &self.0
273    }
274}
275
276impl From<RequestId> for String {
277    fn from(id: RequestId) -> String {
278        id.0
279    }
280}
281
282impl PartialEq<str> for RequestId {
283    fn eq(&self, other: &str) -> bool {
284        self.0 == other
285    }
286}
287
288impl PartialEq<String> for RequestId {
289    fn eq(&self, other: &String) -> bool {
290        &self.0 == other
291    }
292}
293
294impl PartialEq<RequestId> for String {
295    fn eq(&self, other: &RequestId) -> bool {
296        self == &other.0
297    }
298}
299
300impl PartialEq<&str> for RequestId {
301    fn eq(&self, other: &&str) -> bool {
302        self.0 == *other
303    }
304}
305
306/// A tool that the client exposes to the Copilot agent.
307///
308/// Sent to the CLI as part of [`SessionConfig::tools`] / [`ResumeSessionConfig::tools`]
309/// at session creation/resume time. The Rust SDK hand-authors this struct
310/// (rather than using the schema-generated form) so it can carry runtime
311/// hints — `overrides_built_in_tool`, `skip_permission` — that don't appear
312/// in the wire schema but are honored by the CLI.
313///
314/// A `Tool` may optionally carry a [`handler`](Self::handler): an
315/// `Arc<dyn ToolHandler>` that implements the tool's runtime behavior.
316/// When present, the SDK dispatches matching `external_tool.requested`
317/// broadcasts to it automatically. When absent (`None`), the tool is
318/// declaration-only — another connected client must service incoming
319/// invocations.
320#[derive(Clone, Default, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322#[non_exhaustive]
323pub struct Tool {
324    /// Tool identifier (e.g., `"bash"`, `"grep"`, `"str_replace_editor"`).
325    pub name: String,
326    /// Optional namespaced name for declarative filtering (e.g., `"playwright/navigate"`
327    /// for MCP tools).
328    #[serde(default, skip_serializing_if = "Option::is_none")]
329    pub namespaced_name: Option<String>,
330    /// Description of what the tool does.
331    #[serde(default)]
332    pub description: String,
333    /// Optional instructions for how to use this tool effectively.
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub instructions: Option<String>,
336    /// JSON Schema for the tool's input parameters.
337    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
338    pub parameters: IndexMap<String, Value>,
339    /// When `true`, this tool replaces a built-in tool of the same name
340    /// (e.g. supplying a custom `grep` that the agent uses in place of the
341    /// CLI's built-in implementation).
342    #[serde(default, skip_serializing_if = "is_false")]
343    pub overrides_built_in_tool: bool,
344    /// When `true`, the CLI does not request permission before invoking
345    /// this tool. Use with caution — the tool is responsible for any
346    /// access control.
347    #[serde(default, skip_serializing_if = "is_false")]
348    pub skip_permission: bool,
349    /// When `true`, a successful call to this tool ends the agent turn: the
350    /// runtime's tool phase halts instead of feeding the result back to the
351    /// model for another round. A failed call leaves the loop running so the
352    /// model can read the error and retry.
353    #[serde(default, skip_serializing_if = "is_false")]
354    pub is_terminal: bool,
355    /// Controls whether the tool may be deferred (loaded lazily via tool
356    /// search) rather than always pre-loaded. When [`DeferMode::Auto`], the
357    /// tool can be deferred and surfaced through tool search. When
358    /// [`DeferMode::Never`], the tool is always pre-loaded. `None` lets the
359    /// runtime decide.
360    #[serde(default, skip_serializing_if = "Option::is_none")]
361    pub defer: Option<DeferMode>,
362    /// Opaque, host-defined metadata associated with the tool definition.
363    /// Keys are namespaced and not part of the stable public API; values are
364    /// not interpreted and may be recognized to inform host-specific behavior.
365    /// Unknown keys are preserved and round-tripped untouched.
366    #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
367    pub metadata: IndexMap<String, Value>,
368    /// Optional runtime implementation. When `Some`, the SDK dispatches
369    /// matching `external_tool.requested` broadcasts to this handler.
370    /// When `None`, the tool is declaration-only.
371    ///
372    /// Skipped during serialization — the handler is runtime behavior,
373    /// not part of the wire representation.
374    ///
375    /// Crate-private to enforce builder semantics: external callers must
376    /// install a handler through [`Tool::with_handler`] and inspect via
377    /// [`Tool::handler`], so an already-attached handler cannot be
378    /// silently overwritten by direct field assignment.
379    #[serde(skip)]
380    pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
381}
382
383#[inline]
384fn is_false(b: &bool) -> bool {
385    !*b
386}
387
388/// Controls whether a [`Tool`] may be deferred (loaded lazily via tool search)
389/// rather than always pre-loaded.
390#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(rename_all = "lowercase")]
392pub enum DeferMode {
393    /// The tool can be deferred and surfaced through tool search.
394    Auto,
395    /// The tool is always pre-loaded.
396    Never,
397}
398
399impl Tool {
400    /// Construct a new [`Tool`] with the given name and otherwise default
401    /// values. The struct is `#[non_exhaustive]`, so external callers
402    /// cannot use struct-literal syntax — use this builder or
403    /// [`Default::default`] plus mut-let.
404    ///
405    /// # Example
406    ///
407    /// ```
408    /// # use github_copilot_sdk::types::Tool;
409    /// # use serde_json::json;
410    /// let tool = Tool::new("greet")
411    ///     .with_description("Say hello to a user")
412    ///     .with_parameters(json!({
413    ///         "type": "object",
414    ///         "properties": { "name": { "type": "string" } },
415    ///         "required": ["name"]
416    ///     }));
417    /// # let _ = tool;
418    /// ```
419    pub fn new(name: impl Into<String>) -> Self {
420        Self {
421            name: name.into(),
422            ..Default::default()
423        }
424    }
425
426    /// Set the namespaced name for declarative filtering (e.g.
427    /// `"playwright/navigate"` for MCP tools).
428    pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
429        self.namespaced_name = Some(namespaced_name.into());
430        self
431    }
432
433    /// Set the human-readable description of what the tool does.
434    pub fn with_description(mut self, description: impl Into<String>) -> Self {
435        self.description = description.into();
436        self
437    }
438
439    /// Set optional instructions for how to use this tool effectively.
440    pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
441        self.instructions = Some(instructions.into());
442        self
443    }
444
445    /// Set the JSON Schema for the tool's input parameters.
446    ///
447    /// Accepts a JSON Schema as a `serde_json::Value`, typically built with
448    /// `serde_json::json!({...})` or returned by `schema_for` (available
449    /// with the `derive` feature). Tool parameter schemas are always
450    /// top-level JSON objects (`{"type": "object", ...}`).
451    ///
452    /// # Panics
453    ///
454    /// Panics if `parameters` is not a JSON object. Use
455    /// [`crate::tool::try_tool_parameters`] and assign to
456    /// [`Tool::parameters`] directly when the schema comes from dynamic
457    /// input and should produce a recoverable error instead.
458    pub fn with_parameters(mut self, parameters: Value) -> Self {
459        self.parameters = crate::tool::tool_parameters(parameters);
460        self
461    }
462
463    /// Mark this tool as overriding a built-in tool of the same name.
464    /// E.g. supplying a custom `grep` that the agent uses in place of the
465    /// CLI's built-in implementation.
466    pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
467        self.overrides_built_in_tool = overrides;
468        self
469    }
470
471    /// When `true`, the CLI will not request permission before invoking
472    /// this tool. Use with caution — the tool is responsible for any
473    /// access control.
474    pub fn with_skip_permission(mut self, skip: bool) -> Self {
475        self.skip_permission = skip;
476        self
477    }
478
479    /// Sets whether a successful call to this tool ends the agent turn.
480    ///
481    /// When `true`, the runtime's tool phase halts after a successful call
482    /// instead of feeding the result back to the model for another round. A
483    /// failed call leaves the loop running so the model can read the error and
484    /// retry.
485    #[must_use]
486    pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
487        self.is_terminal = is_terminal;
488        self
489    }
490
491    /// Set the deferral mode controlling whether the tool may be loaded
492    /// lazily via tool search ([`DeferMode::Auto`]) or always pre-loaded
493    /// ([`DeferMode::Never`]).
494    pub fn with_defer(mut self, defer: DeferMode) -> Self {
495        self.defer = Some(defer);
496        self
497    }
498
499    /// Set opaque, host-defined metadata for the tool. Keys are namespaced and
500    /// not part of the stable public API. Replaces any previously-set metadata.
501    pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
502        self.metadata = metadata;
503        self
504    }
505
506    /// Attach a runtime implementation. The SDK will dispatch matching
507    /// `external_tool.requested` broadcasts to `handler` for this tool's
508    /// name. Without a handler the tool is declaration-only.
509    pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
510        self.handler = Some(handler);
511        self
512    }
513
514    /// Returns the attached runtime handler, if any.
515    ///
516    /// Read-only inspection — to install or replace a handler, use
517    /// [`Tool::with_handler`].
518    pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
519        self.handler.as_ref()
520    }
521}
522
523impl std::fmt::Debug for Tool {
524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525        f.debug_struct("Tool")
526            .field("name", &self.name)
527            .field("namespaced_name", &self.namespaced_name)
528            .field("description", &self.description)
529            .field("instructions", &self.instructions)
530            .field("parameters", &self.parameters)
531            .field("overrides_built_in_tool", &self.overrides_built_in_tool)
532            .field("skip_permission", &self.skip_permission)
533            .field("is_terminal", &self.is_terminal)
534            .field("defer", &self.defer)
535            .field("metadata", &self.metadata)
536            .field(
537                "handler",
538                &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
539            )
540            .finish()
541    }
542}
543
544/// Context passed to a [`CommandHandler`] when a registered slash command
545/// is executed by the user.
546#[non_exhaustive]
547#[derive(Debug, Clone)]
548pub struct CommandContext {
549    /// Session ID where the command was invoked.
550    pub session_id: SessionId,
551    /// The full command text (e.g. `"/deploy production"`).
552    pub command: String,
553    /// Command name without the leading `/` (e.g. `"deploy"`).
554    pub command_name: String,
555    /// Raw argument string after the command name (e.g. `"production"`).
556    pub args: String,
557}
558
559/// Handler invoked when a registered slash command is executed.
560///
561/// Returning `Err(_)` causes the SDK to forward the error message back to
562/// the CLI via `session.commands.handlePendingCommand` so the TUI can
563/// surface it. Returning `Ok(())` reports success.
564#[async_trait::async_trait]
565pub trait CommandHandler: Send + Sync {
566    /// Called when the user invokes the command this handler is registered for.
567    async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
568}
569
570/// Definition of a slash command registered with the session.
571///
572/// When the CLI is running with a TUI, registered commands appear as
573/// `/name` for the user to invoke. Only `name` and `description` are sent
574/// over the wire — the handler is local to this SDK process.
575#[non_exhaustive]
576#[derive(Clone)]
577pub struct CommandDefinition {
578    /// Command name (without leading `/`).
579    pub name: String,
580    /// Human-readable description shown in command-completion UI.
581    pub description: Option<String>,
582    /// Handler invoked when the command is executed.
583    pub handler: Arc<dyn CommandHandler>,
584}
585
586impl CommandDefinition {
587    /// Construct a new command definition. Use [`with_description`](Self::with_description)
588    /// to add a description.
589    pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
590        Self {
591            name: name.into(),
592            description: None,
593            handler,
594        }
595    }
596
597    /// Set the human-readable description shown in the CLI's command-completion UI.
598    pub fn with_description(mut self, description: impl Into<String>) -> Self {
599        self.description = Some(description.into());
600        self
601    }
602}
603
604impl std::fmt::Debug for CommandDefinition {
605    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606        f.debug_struct("CommandDefinition")
607            .field("name", &self.name)
608            .field("description", &self.description)
609            .field("handler", &"<set>")
610            .finish()
611    }
612}
613
614impl Serialize for CommandDefinition {
615    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
616        use serde::ser::SerializeStruct;
617        let mut state = serializer.serialize_struct("CommandDefinition", 2)?;
618        state.serialize_field("name", &self.name)?;
619        state.serialize_field("description", self.description.as_deref().unwrap_or(""))?;
620        state.end()
621    }
622}
623
624/// Configures a custom agent (sub-agent) for the session.
625///
626/// Custom agents have their own prompt, tool allowlist, and optionally
627/// their own MCP servers and skill set. The agent named in
628/// [`SessionConfig::agent`] (or the runtime default) is the active one
629/// when the session starts.
630#[derive(Debug, Clone, Default, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632#[non_exhaustive]
633pub struct CustomAgentConfig {
634    /// Unique name of the custom agent.
635    pub name: String,
636    /// Display name for UI purposes.
637    #[serde(default, skip_serializing_if = "Option::is_none")]
638    pub display_name: Option<String>,
639    /// Description of what the agent does.
640    #[serde(default, skip_serializing_if = "Option::is_none")]
641    pub description: Option<String>,
642    /// List of tool names the agent can use. `None` means all tools.
643    #[serde(default, skip_serializing_if = "Option::is_none")]
644    pub tools: Option<Vec<String>>,
645    /// Prompt content for the agent.
646    pub prompt: String,
647    /// MCP servers specific to this agent.
648    #[serde(default, skip_serializing_if = "Option::is_none")]
649    pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
650    /// Whether the agent is available for model inference.
651    #[serde(default, skip_serializing_if = "Option::is_none")]
652    pub infer: Option<bool>,
653    /// Skill names to preload into this agent's context at startup.
654    #[serde(default, skip_serializing_if = "Option::is_none")]
655    pub skills: Option<Vec<String>>,
656    /// Model identifier for this agent (e.g. `"claude-haiku-4.5"`).
657    ///
658    /// When set, the runtime will attempt to use this model for the agent,
659    /// falling back to the parent session model if unavailable.
660    #[serde(default, skip_serializing_if = "Option::is_none")]
661    pub model: Option<String>,
662    /// Reasoning effort level for this agent's model.
663    ///
664    /// When unset, the runtime resolves model configuration, then inherits the
665    /// parent effort only for the same model.
666    #[serde(default, skip_serializing_if = "Option::is_none")]
667    pub reasoning_effort: Option<String>,
668}
669
670impl CustomAgentConfig {
671    /// Construct a custom agent configuration with the required `name`
672    /// and `prompt` fields populated.
673    ///
674    /// All other fields default to unset; use the `with_*` chain to
675    /// customize them. Fields are also `pub` if direct assignment is
676    /// preferred for `Option<T>` pass-through.
677    pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
678        Self {
679            name: name.into(),
680            prompt: prompt.into(),
681            ..Self::default()
682        }
683    }
684
685    /// Set the display name shown in the CLI's agent-selection UI.
686    pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
687        self.display_name = Some(display_name.into());
688        self
689    }
690
691    /// Set the description of what the agent does.
692    pub fn with_description(mut self, description: impl Into<String>) -> Self {
693        self.description = Some(description.into());
694        self
695    }
696
697    /// Restrict the agent to a specific tool allowlist. When unset, the
698    /// agent inherits the parent session's tool set.
699    pub fn with_tools<I, S>(mut self, tools: I) -> Self
700    where
701        I: IntoIterator<Item = S>,
702        S: Into<String>,
703    {
704        self.tools = Some(tools.into_iter().map(Into::into).collect());
705        self
706    }
707
708    /// Configure agent-specific MCP servers.
709    pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
710        self.mcp_servers = Some(mcp_servers);
711        self
712    }
713
714    /// Whether the agent participates in model inference.
715    pub fn with_infer(mut self, infer: bool) -> Self {
716        self.infer = Some(infer);
717        self
718    }
719
720    /// Set the skills preloaded into the agent's context at startup.
721    pub fn with_skills<I, S>(mut self, skills: I) -> Self
722    where
723        I: IntoIterator<Item = S>,
724        S: Into<String>,
725    {
726        self.skills = Some(skills.into_iter().map(Into::into).collect());
727        self
728    }
729
730    /// Set the model identifier for this agent.
731    pub fn with_model(mut self, model: impl Into<String>) -> Self {
732        self.model = Some(model.into());
733        self
734    }
735
736    /// Set the reasoning effort level for this agent's model.
737    pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
738        self.reasoning_effort = Some(reasoning_effort.into());
739        self
740    }
741}
742
743/// Configures the default (built-in) agent that handles turns when no
744/// custom agent is selected.
745///
746/// Use [`Self::excluded_tools`] to hide tools from the default agent
747/// while keeping them available to custom sub-agents that list them in
748/// their [`CustomAgentConfig::tools`].
749#[derive(Debug, Clone, Default, Serialize, Deserialize)]
750#[serde(rename_all = "camelCase")]
751pub struct DefaultAgentConfig {
752    /// Tool names to exclude from the default agent.
753    #[serde(default, skip_serializing_if = "Option::is_none")]
754    pub excluded_tools: Option<Vec<String>>,
755}
756
757/// Configuration for large tool output handling.
758///
759/// When a tool produces output exceeding [`max_size_bytes`](Self::max_size_bytes),
760/// the SDK writes the full output to a file in [`output_directory`](Self::output_directory)
761/// and returns a truncated preview to the model.
762#[derive(Debug, Clone, Default, Serialize, Deserialize)]
763#[serde(rename_all = "camelCase")]
764#[non_exhaustive]
765pub struct LargeToolOutputConfig {
766    /// Whether large tool output handling is enabled. Defaults to `true` on the CLI.
767    #[serde(default, skip_serializing_if = "Option::is_none")]
768    pub enabled: Option<bool>,
769    /// Maximum tool output size in bytes before it is redirected to a file.
770    /// Defaults to 50KB on the CLI.
771    #[serde(default, skip_serializing_if = "Option::is_none")]
772    pub max_size_bytes: Option<u64>,
773    /// Directory where large tool output files are written. Defaults to
774    /// the OS temp directory on the CLI.
775    #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
776    pub output_directory: Option<PathBuf>,
777}
778
779impl LargeToolOutputConfig {
780    /// Construct an empty [`LargeToolOutputConfig`]; all fields default to
781    /// unset (the CLI applies its own defaults).
782    pub fn new() -> Self {
783        Self::default()
784    }
785
786    /// Toggle large tool output handling on or off.
787    pub fn with_enabled(mut self, enabled: bool) -> Self {
788        self.enabled = Some(enabled);
789        self
790    }
791
792    /// Set the maximum tool output size in bytes before it is redirected to a file.
793    pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
794        self.max_size_bytes = Some(max_size_bytes);
795        self
796    }
797
798    /// Set the directory where large tool output files are written.
799    pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
800        self.output_directory = Some(output_directory.into());
801        self
802    }
803}
804
805/// Overrides the runtime's built-in tool-search behavior.
806///
807/// Tool search defers tools to keep the model's active tool set small.
808/// To override the tool-search tool's implementation, register a [`Tool`]
809/// named `"tool_search_tool"` with [`Tool::overrides_built_in_tool`] set to `true`.
810#[derive(Debug, Clone, Default, Serialize, Deserialize)]
811#[serde(rename_all = "camelCase")]
812#[non_exhaustive]
813pub struct ToolSearchConfig {
814    /// Toggle to enable/disable tool search.
815    #[serde(default, skip_serializing_if = "Option::is_none")]
816    pub enabled: Option<bool>,
817    /// The tool count above which MCP and external tools are deferred behind
818    /// tool search. When unset, the runtime default (30) applies.
819    #[serde(default, skip_serializing_if = "Option::is_none")]
820    pub defer_threshold: Option<u32>,
821}
822
823impl ToolSearchConfig {
824    /// Construct an empty [`ToolSearchConfig`]; all fields default to unset
825    /// (the runtime applies its own defaults).
826    pub fn new() -> Self {
827        Self::default()
828    }
829
830    /// Toggle that enables or disables tool search.
831    pub fn with_enabled(mut self, enabled: bool) -> Self {
832        self.enabled = Some(enabled);
833        self
834    }
835
836    /// Set the tool count above which MCP and external tools are deferred
837    /// behind tool search.
838    pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
839        self.defer_threshold = Some(defer_threshold);
840        self
841    }
842}
843
844/// Configuration for the built-in GitHub MCP server.
845///
846/// `disable_form_deferral` only applies to the built-in GitHub MCP server and
847/// only has an effect when MCP Apps and form-backed GitHub tools are enabled.
848#[derive(Debug, Clone, Default, Serialize, Deserialize)]
849#[serde(rename_all = "camelCase")]
850#[non_exhaustive]
851pub struct GitHubMcpToolConfig {
852    /// Whether all GitHub MCP tools are enabled.
853    #[serde(default, skip_serializing_if = "Option::is_none")]
854    pub enable_all_tools: Option<bool>,
855    /// Additional GitHub MCP toolsets to enable.
856    #[serde(default, skip_serializing_if = "Option::is_none")]
857    pub additional_toolsets: Option<Vec<String>>,
858    /// Additional GitHub MCP tools to enable.
859    #[serde(default, skip_serializing_if = "Option::is_none")]
860    pub additional_tools: Option<Vec<String>>,
861    /// Whether GitHub MCP insiders mode is enabled.
862    #[serde(default, skip_serializing_if = "Option::is_none")]
863    pub enable_insiders_mode: Option<bool>,
864    /// Disables form deferral for GitHub MCP tools. This only applies to the
865    /// built-in GitHub MCP server and only has an effect when MCP Apps and
866    /// form-backed GitHub tools are enabled.
867    #[serde(default, skip_serializing_if = "Option::is_none")]
868    pub disable_form_deferral: Option<bool>,
869}
870
871impl GitHubMcpToolConfig {
872    /// Construct an empty GitHub MCP tool configuration.
873    pub fn new() -> Self {
874        Self::default()
875    }
876
877    /// Set whether all GitHub MCP tools are enabled.
878    pub fn with_enable_all_tools(mut self, value: bool) -> Self {
879        self.enable_all_tools = Some(value);
880        self
881    }
882
883    /// Set the additional GitHub MCP toolsets to enable.
884    pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
885    where
886        I: IntoIterator<Item = S>,
887        S: Into<String>,
888    {
889        self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
890        self
891    }
892
893    /// Set the additional GitHub MCP tools to enable.
894    pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
895    where
896        I: IntoIterator<Item = S>,
897        S: Into<String>,
898    {
899        self.additional_tools = Some(values.into_iter().map(Into::into).collect());
900        self
901    }
902
903    /// Set whether GitHub MCP insiders mode is enabled.
904    pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
905        self.enable_insiders_mode = Some(value);
906        self
907    }
908
909    /// Disable form deferral for GitHub MCP tools. This only applies to the
910    /// built-in GitHub MCP server and only has an effect when MCP Apps and
911    /// form-backed GitHub tools are enabled.
912    pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
913        self.disable_form_deferral = Some(value);
914        self
915    }
916}
917
918/// Configures infinite sessions: persistent workspaces with automatic
919/// context-window compaction.
920///
921/// When enabled (default), sessions automatically manage context limits
922/// through background compaction and persist state to a workspace
923/// directory.
924#[derive(Debug, Clone, Default, Serialize, Deserialize)]
925#[serde(rename_all = "camelCase")]
926#[non_exhaustive]
927pub struct InfiniteSessionConfig {
928    /// Whether infinite sessions are enabled. Defaults to `true` on the CLI.
929    #[serde(default, skip_serializing_if = "Option::is_none")]
930    pub enabled: Option<bool>,
931    /// Context utilization (0.0–1.0) at which background compaction starts.
932    /// Default: 0.80.
933    #[serde(default, skip_serializing_if = "Option::is_none")]
934    pub background_compaction_threshold: Option<f64>,
935    /// Context utilization (0.0–1.0) at which the session blocks until
936    /// compaction completes. Default: 0.95.
937    #[serde(default, skip_serializing_if = "Option::is_none")]
938    pub buffer_exhaustion_threshold: Option<f64>,
939}
940
941impl InfiniteSessionConfig {
942    /// Construct an empty [`InfiniteSessionConfig`]; all fields default to
943    /// unset (the CLI applies its own defaults).
944    pub fn new() -> Self {
945        Self::default()
946    }
947
948    /// Toggle infinite sessions on or off. Defaults to `true` on the CLI
949    /// when unset.
950    pub fn with_enabled(mut self, enabled: bool) -> Self {
951        self.enabled = Some(enabled);
952        self
953    }
954
955    /// Set the context utilization (0.0–1.0) at which background
956    /// compaction starts.
957    pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
958        self.background_compaction_threshold = Some(threshold);
959        self
960    }
961
962    /// Set the context utilization (0.0–1.0) at which the session blocks
963    /// until compaction completes.
964    pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
965        self.buffer_exhaustion_threshold = Some(threshold);
966        self
967    }
968}
969
970/// Per-session configuration for the runtime memory feature.
971///
972/// Supplied via [`SessionConfig::with_memory`] /
973/// [`ResumeSessionConfig::with_memory`]. When a session is created or resumed
974/// without a memory configuration, the runtime applies its own default for the
975/// memory feature.
976///
977/// The type is extensible: today it carries [`enabled`](Self::enabled), and
978/// further tuning knobs can be added as optional fields without a breaking
979/// change.
980#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
981#[serde(rename_all = "camelCase")]
982#[non_exhaustive]
983pub struct MemoryConfiguration {
984    /// Whether the memory feature is enabled for this session.
985    pub enabled: bool,
986}
987
988impl MemoryConfiguration {
989    /// A configuration with the memory feature enabled.
990    pub fn enabled() -> Self {
991        Self { enabled: true }
992    }
993
994    /// A configuration with the memory feature disabled.
995    pub fn disabled() -> Self {
996        Self { enabled: false }
997    }
998
999    /// Set whether the memory feature is enabled.
1000    pub fn with_enabled(mut self, enabled: bool) -> Self {
1001        self.enabled = enabled;
1002        self
1003    }
1004}
1005
1006/// GitHub repository metadata to associate with a cloud session.
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1008#[serde(rename_all = "camelCase")]
1009#[non_exhaustive]
1010pub struct CloudSessionRepository {
1011    /// Repository owner.
1012    pub owner: String,
1013    /// Repository name.
1014    pub name: String,
1015    /// Optional branch name.
1016    #[serde(skip_serializing_if = "Option::is_none")]
1017    pub branch: Option<String>,
1018}
1019
1020impl CloudSessionRepository {
1021    /// Create repository metadata for a cloud session.
1022    pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1023        Self {
1024            owner: owner.into(),
1025            name: name.into(),
1026            branch: None,
1027        }
1028    }
1029
1030    /// Set the branch associated with the repository.
1031    pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1032        self.branch = Some(branch.into());
1033        self
1034    }
1035}
1036
1037/// Options for creating a remote session in the cloud.
1038#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1039#[serde(rename_all = "camelCase")]
1040#[non_exhaustive]
1041pub struct CloudSessionOptions {
1042    /// Optional GitHub repository metadata to associate with the cloud session.
1043    #[serde(skip_serializing_if = "Option::is_none")]
1044    pub repository: Option<CloudSessionRepository>,
1045}
1046
1047impl CloudSessionOptions {
1048    /// Create cloud session options with repository metadata.
1049    pub fn with_repository(repository: CloudSessionRepository) -> Self {
1050        Self {
1051            repository: Some(repository),
1052        }
1053    }
1054}
1055
1056/// Stable extension identity for session participants that provide canvases.
1057#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1058#[serde(rename_all = "camelCase")]
1059pub struct ExtensionInfo {
1060    /// Extension namespace/source, e.g. `"github-app"`.
1061    pub source: String,
1062    /// Stable provider name within the source namespace.
1063    pub name: String,
1064}
1065
1066impl ExtensionInfo {
1067    /// Create stable extension identity metadata.
1068    pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1069        Self {
1070            source: source.into(),
1071            name: name.into(),
1072        }
1073    }
1074}
1075
1076/// Stable identity for a host/SDK connection that supplies built-in canvases.
1077///
1078/// When set on session create or resume, the runtime uses [`id`] verbatim as
1079/// the agent-facing canvas extension id, so canvases declared on a control
1080/// connection survive stdio reconnect and CLI process restart instead of being
1081/// re-keyed to a per-connection id. The id is opaque to the runtime; a
1082/// per-window-stable value such as `app:builtin:<windowId>` is recommended. An
1083/// id beginning with `connection:` is reserved and ignored by the runtime.
1084///
1085/// [`id`]: CanvasProviderIdentity::id
1086#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1087#[serde(rename_all = "camelCase")]
1088pub struct CanvasProviderIdentity {
1089    /// Opaque, stable provider id used verbatim as the canvas extension id.
1090    pub id: String,
1091    /// Optional display name surfaced as the canvas extension name.
1092    #[serde(skip_serializing_if = "Option::is_none")]
1093    pub name: Option<String>,
1094}
1095
1096impl CanvasProviderIdentity {
1097    /// Create a canvas provider identity from a stable opaque id.
1098    pub fn new(id: impl Into<String>) -> Self {
1099        Self {
1100            id: id.into(),
1101            name: None,
1102        }
1103    }
1104
1105    /// Set the optional display name surfaced as the canvas extension name.
1106    pub fn with_name(mut self, name: impl Into<String>) -> Self {
1107        self.name = Some(name.into());
1108        self
1109    }
1110}
1111
1112/// Configuration for a single MCP server.
1113///
1114/// MCP (Model Context Protocol) servers expose external tools to the
1115/// agent. Local servers run as a subprocess over stdio; remote servers
1116/// speak HTTP or Server-Sent Events.
1117///
1118/// Serialized as a JSON object with a `type` discriminator (`"stdio"` |
1119/// `"http"` | `"sse"`).
1120///
1121/// # Example
1122///
1123/// ```
1124/// # use github_copilot_sdk::types::{McpServerConfig, McpStdioServerConfig, McpHttpServerConfig};
1125/// # use github_copilot_sdk::IndexMap;
1126/// let mut servers = IndexMap::new();
1127/// servers.insert(
1128///     "playwright".to_string(),
1129///     McpServerConfig::Stdio(McpStdioServerConfig {
1130///         tools: Some(vec!["*".to_string()]),
1131///         command: "npx".to_string(),
1132///         args: vec!["-y".to_string(), "@playwright/mcp".to_string()],
1133///         ..Default::default()
1134///     }),
1135/// );
1136/// servers.insert(
1137///     "weather".to_string(),
1138///     McpServerConfig::Http(McpHttpServerConfig {
1139///         tools: Some(vec!["forecast".to_string()]),
1140///         url: "https://example.com/mcp".to_string(),
1141///         ..Default::default()
1142///     }),
1143/// );
1144/// ```
1145#[derive(Debug, Clone, Serialize, Deserialize)]
1146#[serde(tag = "type", rename_all = "lowercase")]
1147#[non_exhaustive]
1148pub enum McpServerConfig {
1149    /// Local MCP server launched as a subprocess and addressed over stdio.
1150    /// On the wire this serializes as `{"type": "stdio", ...}`. The CLI
1151    /// also accepts `"local"` as an alias on input.
1152    #[serde(alias = "local")]
1153    Stdio(McpStdioServerConfig),
1154    /// Remote MCP server addressed over HTTP.
1155    Http(McpHttpServerConfig),
1156    /// Remote MCP server addressed over Server-Sent Events.
1157    Sse(McpHttpServerConfig),
1158}
1159
1160/// Configuration for a local/stdio MCP server.
1161///
1162/// See [`McpServerConfig::Stdio`].
1163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1164#[serde(rename_all = "camelCase")]
1165pub struct McpStdioServerConfig {
1166    /// Tools to expose from this server.
1167    ///
1168    /// - `None` (field omitted on the wire) — expose **all** tools.
1169    /// - `Some(vec![])` — expose **no** tools.
1170    /// - `Some(vec!["a", ...])` — expose only the listed tools.
1171    #[serde(default, skip_serializing_if = "Option::is_none")]
1172    pub tools: Option<Vec<String>>,
1173    /// Optional timeout in milliseconds for tool calls to this server.
1174    #[serde(default, skip_serializing_if = "Option::is_none")]
1175    pub timeout: Option<i64>,
1176    /// Subprocess executable.
1177    pub command: String,
1178    /// Arguments to pass to the subprocess.
1179    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1180    pub args: Vec<String>,
1181    /// Environment variables to set on the subprocess. Values are passed
1182    /// through literally to the child process.
1183    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1184    pub env: HashMap<String, String>,
1185    /// Working directory for the subprocess.
1186    #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1187    pub working_directory: Option<String>,
1188}
1189
1190/// Configuration for a remote MCP server (HTTP or SSE).
1191///
1192/// See [`McpServerConfig::Http`] and [`McpServerConfig::Sse`].
1193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1194#[serde(rename_all = "camelCase")]
1195pub struct McpHttpServerConfig {
1196    /// Tools to expose from this server.
1197    ///
1198    /// - `None` (field omitted on the wire) — expose **all** tools.
1199    /// - `Some(vec![])` — expose **no** tools.
1200    /// - `Some(vec!["a", ...])` — expose only the listed tools.
1201    #[serde(default, skip_serializing_if = "Option::is_none")]
1202    pub tools: Option<Vec<String>>,
1203    /// Optional timeout in milliseconds for tool calls to this server.
1204    #[serde(default, skip_serializing_if = "Option::is_none")]
1205    pub timeout: Option<i64>,
1206    /// Server URL.
1207    pub url: String,
1208    /// Optional HTTP headers to include on every request.
1209    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1210    pub headers: HashMap<String, String>,
1211}
1212
1213/// Configures a custom inference provider (BYOK — Bring Your Own Key).
1214///
1215/// Routes session requests through an alternative model provider
1216/// (OpenAI-compatible, Azure, Anthropic, or local) instead of GitHub
1217/// Copilot's default routing.
1218#[derive(Clone, Default, Serialize, Deserialize)]
1219#[serde(rename_all = "camelCase")]
1220#[non_exhaustive]
1221pub struct ProviderConfig {
1222    /// Provider type: `"openai"`, `"azure"`, or `"anthropic"`. Defaults to
1223    /// `"openai"` on the CLI.
1224    #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1225    pub provider_type: Option<String>,
1226    /// API format (openai/azure only): `"completions"` or `"responses"`.
1227    /// Defaults to `"completions"`.
1228    #[serde(default, skip_serializing_if = "Option::is_none")]
1229    pub wire_api: Option<String>,
1230    /// Transport for OpenAI Responses requests: `"http"` or `"websockets"`.
1231    /// Defaults to `"http"`. Set `"websockets"` to deliver Responses API
1232    /// requests over a persistent WebSocket connection instead of HTTP.
1233    /// Applies to OpenAI-compatible providers using `wire_api` `"responses"`.
1234    #[serde(default, skip_serializing_if = "Option::is_none")]
1235    pub transport: Option<String>,
1236    /// API endpoint URL.
1237    pub base_url: String,
1238    /// API key. Optional for local providers like Ollama.
1239    #[serde(default, skip_serializing_if = "Option::is_none")]
1240    pub api_key: Option<String>,
1241    /// Bearer token for authentication. Sets the `Authorization` header
1242    /// directly. Use for services requiring bearer-token auth instead of
1243    /// API key. Takes precedence over `api_key` when both are set.
1244    #[serde(default, skip_serializing_if = "Option::is_none")]
1245    pub bearer_token: Option<String>,
1246    /// **Experimental.** Callback used to acquire a bearer token before each
1247    /// outbound request to this provider.
1248    #[serde(skip)]
1249    pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1250    #[serde(default, skip_serializing_if = "Option::is_none")]
1251    pub(crate) has_bearer_token_provider: Option<bool>,
1252    /// Azure-specific options.
1253    #[serde(default, skip_serializing_if = "Option::is_none")]
1254    pub azure: Option<AzureProviderOptions>,
1255    /// Custom HTTP headers included in outbound provider requests.
1256    #[serde(default, skip_serializing_if = "Option::is_none")]
1257    pub headers: Option<HashMap<String, String>>,
1258    /// Well-known model ID used to look up agent config and default token
1259    /// limits. Also used as the wire model when [`wire_model`](Self::wire_model)
1260    /// is unset. Falls back to [`SessionConfig::model`](crate::SessionConfig::model).
1261    #[serde(default, skip_serializing_if = "Option::is_none")]
1262    pub model_id: Option<String>,
1263    /// Model name sent to the provider API for inference. Use this when
1264    /// the provider's model name (e.g. an Azure deployment name or a
1265    /// custom fine-tune name) differs from
1266    /// [`model_id`](Self::model_id). Falls back to
1267    /// [`model_id`](Self::model_id), then to
1268    /// [`SessionConfig::model`](crate::SessionConfig::model).
1269    #[serde(default, skip_serializing_if = "Option::is_none")]
1270    pub wire_model: Option<String>,
1271    /// Overrides the resolved model's default max prompt tokens. The
1272    /// runtime triggers conversation compaction before sending a request
1273    /// when the prompt (system message, history, tool definitions, user
1274    /// message) would exceed this limit.
1275    #[serde(default, skip_serializing_if = "Option::is_none")]
1276    pub max_prompt_tokens: Option<i64>,
1277    /// Overrides the resolved model's default max output tokens. When
1278    /// hit, the model stops generating and returns a truncated response.
1279    #[serde(default, skip_serializing_if = "Option::is_none")]
1280    pub max_output_tokens: Option<i64>,
1281}
1282
1283impl std::fmt::Debug for ProviderConfig {
1284    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1285        f.debug_struct("ProviderConfig")
1286            .field("provider_type", &self.provider_type)
1287            .field("wire_api", &self.wire_api)
1288            .field("transport", &self.transport)
1289            .field("base_url", &self.base_url)
1290            .field("api_key", &self.api_key)
1291            .field("bearer_token", &self.bearer_token)
1292            .field(
1293                "bearer_token_provider",
1294                &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1295            )
1296            .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1297            .field("azure", &self.azure)
1298            .field("headers", &self.headers)
1299            .field("model_id", &self.model_id)
1300            .field("wire_model", &self.wire_model)
1301            .field("max_prompt_tokens", &self.max_prompt_tokens)
1302            .field("max_output_tokens", &self.max_output_tokens)
1303            .finish()
1304    }
1305}
1306
1307impl ProviderConfig {
1308    /// Construct a [`ProviderConfig`] with the required `base_url` set;
1309    /// all other fields default to unset.
1310    pub fn new(base_url: impl Into<String>) -> Self {
1311        Self {
1312            base_url: base_url.into(),
1313            ..Self::default()
1314        }
1315    }
1316
1317    /// Set the provider type (`"openai"`, `"azure"`, or `"anthropic"`).
1318    pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1319        self.provider_type = Some(provider_type.into());
1320        self
1321    }
1322
1323    /// Set the API format (`"completions"` or `"responses"`; openai/azure only).
1324    pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1325        self.wire_api = Some(wire_api.into());
1326        self
1327    }
1328
1329    /// Set the transport (`"http"` or `"websockets"`) for OpenAI Responses
1330    /// requests. Defaults to `"http"`.
1331    pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1332        self.transport = Some(transport.into());
1333        self
1334    }
1335
1336    /// Set the API key. Optional for local providers like Ollama.
1337    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1338        self.api_key = Some(api_key.into());
1339        self
1340    }
1341
1342    /// Set the bearer token used to populate the `Authorization` header.
1343    /// Takes precedence over `api_key` when both are set.
1344    pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1345        self.bearer_token = Some(bearer_token.into());
1346        self
1347    }
1348
1349    /// Set the callback used to acquire a bearer token before each outbound
1350    /// request to this provider.
1351    ///
1352    /// **Experimental.** This method is part of an experimental wire-protocol
1353    /// surface and may change or be removed in a future release.
1354    pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1355        self.bearer_token_provider = Some(provider);
1356        self
1357    }
1358
1359    /// Set Azure-specific options.
1360    pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1361        self.azure = Some(azure);
1362        self
1363    }
1364
1365    /// Set the custom HTTP headers attached to outbound provider requests.
1366    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1367        self.headers = Some(headers);
1368        self
1369    }
1370
1371    /// Set the well-known model ID used to look up agent config and default
1372    /// token limits. Falls back to the session's configured model when unset.
1373    pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1374        self.model_id = Some(model_id.into());
1375        self
1376    }
1377
1378    /// Set the model name sent to the provider API for inference. Use this
1379    /// when the provider's model name (e.g. an Azure deployment name or a
1380    /// custom fine-tune name) differs from
1381    /// [`model_id`](Self::model_id).
1382    pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1383        self.wire_model = Some(wire_model.into());
1384        self
1385    }
1386
1387    /// Override the resolved model's default max prompt tokens. The
1388    /// runtime triggers conversation compaction when the prompt would
1389    /// exceed this limit.
1390    pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1391        self.max_prompt_tokens = Some(max);
1392        self
1393    }
1394
1395    /// Override the resolved model's default max output tokens. When
1396    /// hit, the model stops generating and returns a truncated response.
1397    pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1398        self.max_output_tokens = Some(max);
1399        self
1400    }
1401}
1402
1403/// Provider-scoped Copilot API (CAPI) session options.
1404///
1405/// WebSocket transport is the default for the CAPI Responses API whenever
1406/// the model advertises the `ws:/responses` endpoint. Set
1407/// [`enable_web_socket_responses`](Self::enable_web_socket_responses) to
1408/// `false` to force the HTTP Responses transport instead, which is useful
1409/// for users behind proxies where WebSockets fail.
1410///
1411/// Setting it to `false` is equivalent to setting the
1412/// `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. The option
1413/// is scoped under the `capi` namespace because a single session can host
1414/// multiple providers, so transport choice is provider-level.
1415#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1416#[serde(rename_all = "camelCase")]
1417#[non_exhaustive]
1418pub struct CapiSessionOptions {
1419    /// Whether to use WebSocket transport for CAPI Responses API calls.
1420    ///
1421    /// When `Some(false)`, the runtime uses HTTP Responses transport even if
1422    /// the selected model advertises `ws:/responses`. When unset, the runtime
1423    /// default applies (WebSocket transport when advertised).
1424    #[serde(default, skip_serializing_if = "Option::is_none")]
1425    pub enable_web_socket_responses: Option<bool>,
1426}
1427
1428impl CapiSessionOptions {
1429    /// Construct CAPI session options with all fields unset.
1430    pub fn new() -> Self {
1431        Self::default()
1432    }
1433
1434    /// Set whether to use WebSocket transport for CAPI Responses API calls.
1435    pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1436        self.enable_web_socket_responses = Some(enable);
1437        self
1438    }
1439}
1440
1441/// Azure-specific provider options.
1442#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1443#[serde(rename_all = "camelCase")]
1444pub struct AzureProviderOptions {
1445    /// Azure API version. When omitted, the runtime uses the GA versionless v1 route.
1446    #[serde(default, skip_serializing_if = "Option::is_none")]
1447    pub api_version: Option<String>,
1448}
1449
1450/// A named BYOK provider connection in the multi-provider registry.
1451///
1452/// **Experimental.** Multi-provider BYOK configuration is part of an
1453/// experimental surface and may change or be removed in a future release.
1454///
1455/// Unlike [`ProviderConfig`], which routes the whole session through a
1456/// single provider, named providers are additive: the session keeps its
1457/// default Copilot routing and exposes these providers' models alongside
1458/// it. Models are attached via [`ProviderModelConfig`], which references a
1459/// provider by [`name`](Self::name).
1460#[derive(Clone, Default, Serialize, Deserialize)]
1461#[serde(rename_all = "camelCase")]
1462#[non_exhaustive]
1463pub struct NamedProviderConfig {
1464    /// Unique name used by [`ProviderModelConfig::provider`] to reference
1465    /// this connection.
1466    pub name: String,
1467    /// Provider type: `"openai"`, `"azure"`, or `"anthropic"`. Defaults to
1468    /// `"openai"` on the CLI.
1469    #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1470    pub provider_type: Option<String>,
1471    /// API format (openai/azure only): `"completions"` or `"responses"`.
1472    /// Defaults to `"completions"`.
1473    #[serde(default, skip_serializing_if = "Option::is_none")]
1474    pub wire_api: Option<String>,
1475    /// API endpoint URL.
1476    pub base_url: String,
1477    /// API key. Optional for local providers like Ollama.
1478    #[serde(default, skip_serializing_if = "Option::is_none")]
1479    pub api_key: Option<String>,
1480    /// Bearer token for authentication. Sets the `Authorization` header
1481    /// directly. Takes precedence over `api_key` when both are set.
1482    #[serde(default, skip_serializing_if = "Option::is_none")]
1483    pub bearer_token: Option<String>,
1484    /// **Experimental.** Callback used to acquire a bearer token before each
1485    /// outbound request to this provider.
1486    #[serde(skip)]
1487    pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1488    #[serde(default, skip_serializing_if = "Option::is_none")]
1489    pub(crate) has_bearer_token_provider: Option<bool>,
1490    /// Azure-specific options.
1491    #[serde(default, skip_serializing_if = "Option::is_none")]
1492    pub azure: Option<AzureProviderOptions>,
1493    /// Custom HTTP headers included in outbound provider requests.
1494    #[serde(default, skip_serializing_if = "Option::is_none")]
1495    pub headers: Option<HashMap<String, String>>,
1496}
1497
1498impl std::fmt::Debug for NamedProviderConfig {
1499    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1500        f.debug_struct("NamedProviderConfig")
1501            .field("name", &self.name)
1502            .field("provider_type", &self.provider_type)
1503            .field("wire_api", &self.wire_api)
1504            .field("base_url", &self.base_url)
1505            .field("api_key", &self.api_key)
1506            .field("bearer_token", &self.bearer_token)
1507            .field(
1508                "bearer_token_provider",
1509                &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1510            )
1511            .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1512            .field("azure", &self.azure)
1513            .field("headers", &self.headers)
1514            .finish()
1515    }
1516}
1517
1518impl NamedProviderConfig {
1519    /// Construct a [`NamedProviderConfig`] with the required `name` and
1520    /// `base_url` set; all other fields default to unset.
1521    pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1522        Self {
1523            name: name.into(),
1524            base_url: base_url.into(),
1525            ..Self::default()
1526        }
1527    }
1528
1529    /// Set the provider type (`"openai"`, `"azure"`, or `"anthropic"`).
1530    pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1531        self.provider_type = Some(provider_type.into());
1532        self
1533    }
1534
1535    /// Set the API format (`"completions"` or `"responses"`; openai/azure only).
1536    pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1537        self.wire_api = Some(wire_api.into());
1538        self
1539    }
1540
1541    /// Set the API key. Optional for local providers like Ollama.
1542    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1543        self.api_key = Some(api_key.into());
1544        self
1545    }
1546
1547    /// Set the bearer token used to populate the `Authorization` header.
1548    /// Takes precedence over `api_key` when both are set.
1549    pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1550        self.bearer_token = Some(bearer_token.into());
1551        self
1552    }
1553
1554    /// Set the callback used to acquire a bearer token before each outbound
1555    /// request to this provider.
1556    ///
1557    /// **Experimental.** This method is part of an experimental wire-protocol
1558    /// surface and may change or be removed in a future release.
1559    pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1560        self.bearer_token_provider = Some(provider);
1561        self
1562    }
1563
1564    /// Set Azure-specific options.
1565    pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1566        self.azure = Some(azure);
1567        self
1568    }
1569
1570    /// Set the custom HTTP headers attached to outbound provider requests.
1571    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1572        self.headers = Some(headers);
1573        self
1574    }
1575}
1576
1577fn prepare_bearer_token_providers(
1578    provider: &mut Option<ProviderConfig>,
1579    providers: &mut Option<Vec<NamedProviderConfig>>,
1580) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1581    let mut bearer_token_providers = HashMap::new();
1582
1583    if let Some(provider) = provider.as_mut()
1584        && let Some(token_provider) = provider.bearer_token_provider.take()
1585    {
1586        provider.has_bearer_token_provider = Some(true);
1587        bearer_token_providers.insert("default".to_string(), token_provider);
1588    }
1589
1590    if let Some(providers) = providers.as_mut() {
1591        for provider in providers {
1592            if let Some(token_provider) = provider.bearer_token_provider.take() {
1593                provider.has_bearer_token_provider = Some(true);
1594                bearer_token_providers.insert(provider.name.clone(), token_provider);
1595            }
1596        }
1597    }
1598
1599    bearer_token_providers
1600}
1601
1602/// A BYOK model definition in the multi-provider registry.
1603///
1604/// **Experimental.** Multi-provider BYOK configuration is part of an
1605/// experimental surface and may change or be removed in a future release.
1606///
1607/// References a [`NamedProviderConfig`] by [`provider`](Self::provider) and
1608/// becomes selectable under the provider-qualified id `provider/id`.
1609#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1610#[serde(rename_all = "camelCase")]
1611#[non_exhaustive]
1612pub struct ProviderModelConfig {
1613    /// Model identifier, unique within its provider. Combined with
1614    /// [`provider`](Self::provider) to form the selection id `provider/id`.
1615    pub id: String,
1616    /// Name of the [`NamedProviderConfig`] this model is served by.
1617    pub provider: String,
1618    /// Model name sent to the provider API for inference. Use when the
1619    /// provider's model name differs from [`id`](Self::id).
1620    #[serde(default, skip_serializing_if = "Option::is_none")]
1621    pub wire_model: Option<String>,
1622    /// Well-known model ID used to look up agent config and default token
1623    /// limits.
1624    #[serde(default, skip_serializing_if = "Option::is_none")]
1625    pub model_id: Option<String>,
1626    /// Human-readable display name.
1627    #[serde(default, skip_serializing_if = "Option::is_none")]
1628    pub name: Option<String>,
1629    /// Overrides the resolved model's default max prompt tokens.
1630    #[serde(default, skip_serializing_if = "Option::is_none")]
1631    pub max_prompt_tokens: Option<i64>,
1632    /// Overrides the resolved model's default max context window tokens.
1633    #[serde(default, skip_serializing_if = "Option::is_none")]
1634    pub max_context_window_tokens: Option<i64>,
1635    /// Overrides the resolved model's default max output tokens.
1636    #[serde(default, skip_serializing_if = "Option::is_none")]
1637    pub max_output_tokens: Option<i64>,
1638    /// Per-property overrides for model capabilities, deep-merged over
1639    /// runtime defaults.
1640    #[serde(default, skip_serializing_if = "Option::is_none")]
1641    pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1642}
1643
1644impl ProviderModelConfig {
1645    /// Construct a [`ProviderModelConfig`] with the required `id` and
1646    /// `provider` set; all other fields default to unset.
1647    pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1648        Self {
1649            id: id.into(),
1650            provider: provider.into(),
1651            ..Self::default()
1652        }
1653    }
1654
1655    /// Set the model name sent to the provider API for inference.
1656    pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1657        self.wire_model = Some(wire_model.into());
1658        self
1659    }
1660
1661    /// Set the well-known model ID used to look up agent config and default
1662    /// token limits.
1663    pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1664        self.model_id = Some(model_id.into());
1665        self
1666    }
1667
1668    /// Set the human-readable display name.
1669    pub fn with_name(mut self, name: impl Into<String>) -> Self {
1670        self.name = Some(name.into());
1671        self
1672    }
1673
1674    /// Override the resolved model's default max prompt tokens.
1675    pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1676        self.max_prompt_tokens = Some(max);
1677        self
1678    }
1679
1680    /// Override the resolved model's default max context window tokens.
1681    pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1682        self.max_context_window_tokens = Some(max);
1683        self
1684    }
1685
1686    /// Override the resolved model's default max output tokens.
1687    pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1688        self.max_output_tokens = Some(max);
1689        self
1690    }
1691
1692    /// Set per-property model capability overrides.
1693    pub fn with_capabilities(
1694        mut self,
1695        capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1696    ) -> Self {
1697        self.capabilities = Some(capabilities);
1698        self
1699    }
1700}
1701
1702/// A single ExP (Experiment Platform) flag value.
1703///
1704/// ExP assignments resolve to a string, number, boolean, or null.
1705#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1706#[serde(untagged)]
1707pub enum ExpFlagValue {
1708    /// A boolean flag value.
1709    Bool(bool),
1710    /// An integer flag value.
1711    Integer(i64),
1712    /// A floating-point flag value.
1713    Float(f64),
1714    /// A string flag value.
1715    String(String),
1716    /// A null flag value.
1717    Null,
1718}
1719
1720/// A single configuration entry in a [`CopilotExpAssignmentResponse`].
1721///
1722/// Each entry carries an identifier and a bag of typed parameter values.
1723#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1724#[serde(rename_all = "PascalCase")]
1725pub struct ExpConfigEntry {
1726    /// Identifier of the configuration entry.
1727    pub id: String,
1728    /// Parameter values keyed by parameter name.
1729    pub parameters: HashMap<String, ExpFlagValue>,
1730}
1731
1732/// ExP ("flight") assignment data, in the same JSON shape the Copilot CLI
1733/// fetches from the experimentation service.
1734///
1735/// Field names serialize as PascalCase (`Features`, `Flights`, ...) to match
1736/// the on-the-wire contract consumed by the runtime.
1737#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1738#[serde(rename_all = "PascalCase")]
1739pub struct CopilotExpAssignmentResponse {
1740    /// Enabled feature names.
1741    #[serde(default)]
1742    pub features: Vec<String>,
1743    /// Assigned flights keyed by flight name.
1744    #[serde(default)]
1745    pub flights: HashMap<String, String>,
1746    /// Configuration entries carrying typed parameter values.
1747    #[serde(default)]
1748    pub configs: Vec<ExpConfigEntry>,
1749    /// Opaque parameter-group payload passed through untouched. Optional.
1750    #[serde(default, skip_serializing_if = "Option::is_none")]
1751    pub parameter_groups: Option<Value>,
1752    /// Version of the flighting configuration. Optional.
1753    #[serde(default, skip_serializing_if = "Option::is_none")]
1754    pub flighting_version: Option<i64>,
1755    /// Impression identifier for the assignment. Optional.
1756    #[serde(default, skip_serializing_if = "Option::is_none")]
1757    pub impression_id: Option<String>,
1758    /// Assignment context string forwarded to CAPI and telemetry.
1759    #[serde(default)]
1760    pub assignment_context: String,
1761}
1762
1763/// Controls whether bypass-permissions mode is available in a managed session.
1764#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1765#[serde(rename_all = "lowercase")]
1766#[non_exhaustive]
1767pub enum DisableBypassPermissionsMode {
1768    /// Turn off bypass-permissions mode.
1769    Disable,
1770}
1771
1772/// Permission rules injected as a managed-settings layer at session bootstrap.
1773///
1774/// All fields are optional; an omitted field imposes no constraint from this
1775/// layer. This layer composes restrictively with any server- or device-level
1776/// managed settings: [`deny`](Self::deny) and [`ask`](Self::ask) rules are
1777/// unioned across layers, every present [`allow`](Self::allow) list must admit a
1778/// tool for it to be allowed, and
1779/// [`disable_bypass_permissions_mode`](Self::disable_bypass_permissions_mode) is
1780/// honored if any layer sets it (deny-wins).
1781#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1782#[serde(rename_all = "camelCase")]
1783#[non_exhaustive]
1784pub struct ManagedSettingsPermissions {
1785    /// When set to `"disable"`, bypass-permissions mode is turned off for the
1786    /// session regardless of other layers. Serialized as
1787    /// `disableBypassPermissionsMode`.
1788    #[serde(default, skip_serializing_if = "Option::is_none")]
1789    pub disable_bypass_permissions_mode: Option<DisableBypassPermissionsMode>,
1790    /// Tool-permission patterns that are always denied.
1791    #[serde(default, skip_serializing_if = "Option::is_none")]
1792    pub deny: Option<Vec<String>>,
1793    /// Tool-permission patterns that require an explicit ask.
1794    #[serde(default, skip_serializing_if = "Option::is_none")]
1795    pub ask: Option<Vec<String>>,
1796    /// Tool-permission patterns that are allowed without prompting.
1797    #[serde(default, skip_serializing_if = "Option::is_none")]
1798    pub allow: Option<Vec<String>>,
1799}
1800
1801impl ManagedSettingsPermissions {
1802    /// Sets the bypass-permissions policy for this managed layer.
1803    pub fn with_disable_bypass_permissions_mode(
1804        mut self,
1805        value: DisableBypassPermissionsMode,
1806    ) -> Self {
1807        self.disable_bypass_permissions_mode = Some(value);
1808        self
1809    }
1810
1811    /// Sets the rules that are always denied.
1812    pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1813        self.deny = Some(rules);
1814        self
1815    }
1816
1817    /// Sets the rules that require explicit approval.
1818    pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1819        self.ask = Some(rules);
1820        self
1821    }
1822
1823    /// Sets the rules that are allowed without prompting.
1824    pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1825        self.allow = Some(rules);
1826        self
1827    }
1828}
1829
1830/// Managed-settings layer injected at session startup. Currently carries only a
1831/// [`permissions`](Self::permissions) object.
1832///
1833/// This layer is startup-only and is not persisted with the session. It must be
1834/// re-supplied on resume to remain in effect; omitting it on resume clears the
1835/// previously injected layer. It can be combined with
1836/// [`SessionConfig::enable_managed_settings`]. Older runtimes may ignore this
1837/// additive field, so hosts must not rely on injected policy until they ship a
1838/// compatible runtime.
1839#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1840#[serde(rename_all = "camelCase")]
1841#[non_exhaustive]
1842pub struct ManagedSettings {
1843    /// Permission rules for this managed-settings layer.
1844    #[serde(default, skip_serializing_if = "Option::is_none")]
1845    pub permissions: Option<ManagedSettingsPermissions>,
1846}
1847
1848impl ManagedSettings {
1849    /// Sets the permissions-only managed policy.
1850    pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1851        self.permissions = Some(permissions);
1852        self
1853    }
1854}
1855
1856/// Configuration for creating a new session via the `session.create` RPC.
1857///
1858/// All fields are optional — the CLI applies sensible defaults.
1859///
1860/// # Construction
1861///
1862/// Two equivalent shapes are supported:
1863///
1864/// 1. **Chained builder** (preferred for compile-time-known values):
1865///
1866///    ```
1867///    # use github_copilot_sdk::types::SessionConfig;
1868///    let cfg = SessionConfig::default()
1869///        .with_client_name("my-app")
1870///        .with_streaming(true)
1871///        .with_enable_config_discovery(true);
1872///    ```
1873///
1874/// 2. **Direct field assignment** (preferred when forwarding `Option<T>`
1875///    from upstream code, since `with_<field>` setters take the inner
1876///    `T`, not `Option<T>`):
1877///
1878///    ```
1879///    # use github_copilot_sdk::types::SessionConfig;
1880///    # let upstream_model: Option<String> = None;
1881///    # let upstream_system_message: Option<github_copilot_sdk::types::SystemMessageConfig> = None;
1882///    let mut cfg = SessionConfig::default()
1883///        .with_client_name("my-app")
1884///        .with_streaming(true);
1885///    cfg.model = upstream_model;
1886///    cfg.system_message = upstream_system_message;
1887///    ```
1888///
1889///    Mixing the two is fine: chain the fields you know at compile time,
1890///    then assign the `Option<T>` pass-through fields directly. All
1891///    fields on this struct are `pub`. This pattern matches the
1892///    `http::request::Parts` / `hyper::Body::Builder` convention in the
1893///    wider Rust ecosystem.
1894///
1895/// # Field naming across SDKs
1896///
1897/// Rust field names are snake_case (`available_tools`, `system_message`);
1898/// the wire protocol uses camelCase (`availableTools`, `systemMessage`).
1899/// The mapping happens inside `SessionConfig::into_wire` (crate-private),
1900/// which builds a separate `SessionCreateWire` payload. This config
1901/// struct is no longer itself serializable — the trait-object handler
1902/// fields (e.g. [`permission_handler`](Self::permission_handler)) could
1903/// never round-trip through serde, so the only legitimate serialization
1904/// path is now `into_wire`. When porting code from the TypeScript, Go,
1905/// Python, or .NET SDKs — or reading the raw JSON-RPC traces — fields
1906/// appear as `availableTools`, `systemMessage`, etc.
1907#[derive(Clone)]
1908#[non_exhaustive]
1909pub struct SessionConfig {
1910    /// Custom session ID. When unset, the CLI generates one.
1911    pub session_id: Option<SessionId>,
1912    /// Model to use (e.g. `"gpt-4"`, `"claude-sonnet-4"`).
1913    pub model: Option<String>,
1914    /// Application name sent as `User-Agent` context.
1915    pub client_name: Option<String>,
1916    /// Reasoning effort level (e.g. `"low"`, `"medium"`, `"high"`).
1917    pub reasoning_effort: Option<String>,
1918    /// Reasoning summary mode for models that support configurable
1919    /// reasoning summaries. Use [`ReasoningSummary::None`] to suppress
1920    /// summary output regardless of whether reasoning is enabled.
1921    pub reasoning_summary: Option<ReasoningSummary>,
1922    /// Context window tier for models that support it. Use `"long_context"`
1923    /// to pin the session to the long-context tier.
1924    pub context_tier: Option<String>,
1925    /// Enable streaming token deltas via `assistant.message_delta` events.
1926    pub streaming: Option<bool>,
1927    /// Custom system message configuration.
1928    pub system_message: Option<SystemMessageConfig>,
1929    /// Client-defined tool declarations to expose to the agent.
1930    pub tools: Option<Vec<Tool>>,
1931    /// Canvas declarations this connection provides to the runtime.
1932    pub canvases: Option<Vec<CanvasDeclaration>>,
1933    /// Provider-side canvas lifecycle handler. The SDK routes inbound
1934    /// `canvas.open` / `canvas.close` / `canvas.action.invoke` requests to
1935    /// this handler. Use [`with_canvas_handler`](Self::with_canvas_handler)
1936    /// to install one.
1937    pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1938    /// Request canvas renderer tools for this connection.
1939    pub request_canvas_renderer: Option<bool>,
1940    /// Request extension tools and dispatch for this connection.
1941    pub request_extensions: Option<bool>,
1942    /// Optional override path to a `copilot-sdk/` folder to inject into
1943    /// extension subprocesses for this session. Invalid paths fall back
1944    /// to the bundled SDK; takes precedence over the host's default.
1945    pub extension_sdk_path: Option<String>,
1946    /// Stable extension identity for canvas/tool providers on this connection.
1947    pub extension_info: Option<ExtensionInfo>,
1948    /// Stable identity for a host/SDK connection that supplies built-in
1949    /// canvases, so they survive reconnect and CLI restart.
1950    pub canvas_provider: Option<CanvasProviderIdentity>,
1951    /// Allowlist of built-in tool names the agent may use.
1952    pub available_tools: Option<Vec<String>>,
1953    /// Blocklist of built-in tool names the agent must not use.
1954    pub excluded_tools: Option<Vec<String>>,
1955    /// Names of built-in agents to exclude from the session.
1956    ///
1957    /// Excluded built-in agents are hidden from discovery and cannot be
1958    /// selected or invoked unless a custom agent with the same name is
1959    /// configured.
1960    pub excluded_builtin_agents: Option<Vec<String>>,
1961    /// MCP server configurations passed through to the CLI.
1962    pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1963    /// Controls how MCP OAuth tokens are stored for this session.
1964    ///
1965    /// - `"persistent"` — tokens are stored in the OS keychain (shared across sessions).
1966    /// - `"in-memory"` — tokens are stored in memory and discarded when the session ends.
1967    ///
1968    /// Defaults to `"in-memory"` when the client is in [`crate::ClientMode::Empty`],
1969    /// applied automatically at session creation/resume time. `None` means no
1970    /// explicit value is set and the runtime default takes effect.
1971    pub mcp_oauth_token_storage: Option<String>,
1972    /// Enables runtime discovery of supported configuration. Explicitly supplied
1973    /// configuration takes precedence over discovered values.
1974    pub enable_config_discovery: Option<bool>,
1975    /// When true, skips embedding retrieval for this session.
1976    pub skip_embedding_retrieval: Option<bool>,
1977    /// Controls how the embedding cache is stored for this session.
1978    /// `"persistent"` caches on disk; `"in-memory"` discards when session ends.
1979    pub embedding_cache_storage: Option<String>,
1980    /// Organization-level custom instructions to apply to this session.
1981    pub organization_custom_instructions: Option<String>,
1982    /// When true, enables on-demand instruction discovery for this session.
1983    pub enable_on_demand_instruction_discovery: Option<bool>,
1984    /// When true, enables file hooks for this session.
1985    pub enable_file_hooks: Option<bool>,
1986    /// When true, allows host Git operations for this session.
1987    pub enable_host_git_operations: Option<bool>,
1988    /// When true, enables the session store for this session.
1989    pub enable_session_store: Option<bool>,
1990    /// When true, enables skills for this session.
1991    pub enable_skills: Option<bool>,
1992    /// **Experimental.** This option is part of an experimental wire-protocol
1993    /// surface (SEP-1865) and may change or be removed in a future release.
1994    ///
1995    /// Enable MCP Apps (SEP-1865) UI passthrough on this session.
1996    ///
1997    /// When `true` **and** the runtime has MCP Apps enabled (via the
1998    /// `MCP_APPS` feature flag or `COPILOT_MCP_APPS=true` environment
1999    /// override), the runtime adds the `mcp-apps` capability to the
2000    /// session, which causes it to advertise the
2001    /// `extensions.io.modelcontextprotocol/ui` extension to MCP servers (so
2002    /// they expose `_meta.ui.resourceUri` on tools) and to expose the
2003    /// `session.rpc.mcp.apps.{listTools,callTool,readResource,setHostContext,
2004    /// getHostContext,diagnose}` JSON-RPC methods.
2005    ///
2006    /// If the runtime gate is off, the opt-in is silently dropped
2007    /// server-side (the runtime logs a warning); the session is created
2008    /// normally but the MCP Apps surface is unavailable. Inspect the
2009    /// runtime's `capabilities.ui.mcpApps` on the create/resume response to
2010    /// detect this.
2011    ///
2012    /// SDK consumers MUST set this to `true` only when they have an iframe
2013    /// renderer that can display `ui://` MCP App bundles. Setting it
2014    /// without a renderer will cause MCP servers to register UI-enabled
2015    /// tool variants the consumer cannot display.
2016    ///
2017    /// Defaults to `None` (treated as `false`).
2018    pub enable_mcp_apps: Option<bool>,
2019    /// Configuration for the built-in GitHub MCP server.
2020    ///
2021    /// `disable_form_deferral` only applies to that server and only has an
2022    /// effect when MCP Apps and form-backed GitHub tools are enabled.
2023    pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2024    /// Skill directory paths passed through to the GitHub Copilot CLI.
2025    pub skill_directories: Option<Vec<PathBuf>>,
2026    /// Additional directories to search for custom instruction files.
2027    /// Forwarded to the CLI; not the same as [`skill_directories`](Self::skill_directories).
2028    pub instruction_directories: Option<Vec<PathBuf>>,
2029    /// Open Plugin directory paths passed through to the CLI.
2030    pub plugin_directories: Option<Vec<PathBuf>>,
2031    /// Configuration for large tool output handling, forwarded to the CLI.
2032    pub large_output: Option<LargeToolOutputConfig>,
2033    /// Overrides the runtime's built-in tool-search behavior, which defers
2034    /// rarely used tools behind a searchable index. When unset, the runtime
2035    /// default applies.
2036    pub tool_search: Option<ToolSearchConfig>,
2037    /// Skill names to disable. Skills in this set will not be available
2038    /// even if found in skill directories.
2039    pub disabled_skills: Option<Vec<String>>,
2040    /// Exact MCP server names to disable for this session. Disabled servers are
2041    /// not started or authenticated on create or cold resume; a resident resume
2042    /// cannot stop servers that are already running.
2043    pub disabled_mcp_servers: Option<Vec<String>>,
2044    /// Enable session hooks. When `true`, the CLI sends `hooks.invoke`
2045    /// RPC requests at key lifecycle points (pre/post tool use, prompt
2046    /// submission, session start/end, errors).
2047    pub hooks: Option<bool>,
2048    /// Custom agents (sub-agents) configured for this session.
2049    pub custom_agents: Option<Vec<CustomAgentConfig>>,
2050    /// Configures the built-in default agent. Use `excluded_tools` to
2051    /// hide tools from the default agent while keeping them available
2052    /// to custom sub-agents that reference them in their `tools` list.
2053    pub default_agent: Option<DefaultAgentConfig>,
2054    /// Name of the custom agent to activate when the session starts.
2055    /// Must match the `name` of one of the agents in [`Self::custom_agents`].
2056    pub agent: Option<String>,
2057    /// Configures infinite sessions: persistent workspace + automatic
2058    /// context-window compaction. Enabled by default on the CLI.
2059    pub infinite_sessions: Option<InfiniteSessionConfig>,
2060    /// Custom model provider (BYOK). When set, the session routes
2061    /// requests through this provider instead of the default Copilot
2062    /// routing.
2063    pub provider: Option<ProviderConfig>,
2064    /// Provider-scoped CAPI session options.
2065    ///
2066    /// Use this to opt out of the default WebSocket transport for CAPI
2067    /// Responses API calls, equivalent to setting
2068    /// `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES`.
2069    pub capi: Option<CapiSessionOptions>,
2070    /// **Experimental.** This field is part of an experimental multi-provider
2071    /// BYOK surface and may change or be removed in a future release.
2072    ///
2073    /// Named BYOK provider connections. Additive to the default Copilot
2074    /// routing — unlike [`provider`](Self::provider), these do not switch
2075    /// the whole session to BYOK. Referenced by [`models`](Self::models).
2076    pub providers: Option<Vec<NamedProviderConfig>>,
2077    /// **Experimental.** This field is part of an experimental multi-provider
2078    /// BYOK surface and may change or be removed in a future release.
2079    ///
2080    /// BYOK model definitions, each referencing a [`providers`](Self::providers)
2081    /// entry by name. Selectable under the id `provider/id`.
2082    pub models: Option<Vec<ProviderModelConfig>>,
2083    /// Enables or disables internal session telemetry for this session.
2084    ///
2085    /// When `Some(false)`, disables session telemetry. When `None` or
2086    /// `Some(true)`, telemetry is enabled for GitHub-authenticated sessions.
2087    /// When a custom [`provider`](Self::provider) is configured, session
2088    /// telemetry is always disabled regardless of this setting. This is
2089    /// independent of [`ClientOptions::telemetry`](crate::ClientOptions::telemetry).
2090    pub enable_session_telemetry: Option<bool>,
2091    /// **Experimental.** Enables native model citations for supported providers.
2092    pub enable_citations: Option<bool>,
2093    /// Opts in to capturing file changes from the first turn for session rewind
2094    /// and cumulative session diff.
2095    pub enable_file_change_tracking: Option<bool>,
2096    /// **Experimental.** Limits applied to this session's current accounting window.
2097    pub session_limits: Option<SessionLimitsConfig>,
2098    /// Per-property overrides for model capabilities, deep-merged over
2099    /// runtime defaults.
2100    pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2101    /// Per-session configuration for the runtime memory feature.
2102    pub memory: Option<MemoryConfiguration>,
2103    /// Override the default configuration directory location. When set,
2104    /// the session uses this directory for storing config and state.
2105    pub config_directory: Option<PathBuf>,
2106    /// Working directory for the session. Tool operations resolve
2107    /// relative paths against this directory.
2108    pub working_directory: Option<PathBuf>,
2109    /// Additional directories the agent may access beyond the working directory.
2110    /// Relative paths resolve against the session working directory. Re-supply
2111    /// them when resuming a session.
2112    pub additional_directories: Option<Vec<PathBuf>>,
2113    /// Per-session GitHub token. Distinct from
2114    /// [`ClientOptions::github_token`](crate::ClientOptions::github_token),
2115    /// which authenticates the CLI process itself; this token determines
2116    /// the GitHub identity used for content exclusion, model routing, and
2117    /// quota checks for *this session*.
2118    pub github_token: Option<String>,
2119    /// Per-session remote behavior control:
2120    /// - `Off` — local only, no remote export (default)
2121    /// - `Export` — export session events to GitHub without
2122    ///   enabling remote steering
2123    /// - `On` — export to GitHub AND enable remote steering
2124    pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2125    /// Creates a remote session in the cloud instead of a local session.
2126    /// The optional repository is associated with the cloud session.
2127    pub cloud: Option<CloudSessionOptions>,
2128    /// Forward sub-agent streaming events to this connection. When false,
2129    /// only non-streaming sub-agent events and `subagent.*` lifecycle events
2130    /// are delivered. Defaults to true on the CLI.
2131    pub include_sub_agent_streaming_events: Option<bool>,
2132    /// Slash commands registered for this session. When the CLI has a TUI,
2133    /// each command appears as `/name` for the user to invoke and the
2134    /// associated [`CommandHandler`] is called when executed.
2135    pub commands: Option<Vec<CommandDefinition>>,
2136    /// ExP assignment ("flight") data injected by a trusted integrator, in
2137    /// the same JSON shape the Copilot CLI fetches from the experimentation
2138    /// service (`CopilotExpAssignmentResponse`). When supplied, the runtime
2139    /// feeds it into the same feature-flag path as CLI-fetched assignments.
2140    /// When absent, the session does not block on ExP. Set via
2141    /// [`with_exp_assignments`](Self::with_exp_assignments).
2142    #[doc(hidden)]
2143    pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2144    /// Opt-in: when `Some(true)`, the runtime self-fetches enterprise managed
2145    /// settings (bypass-permissions policy) at session bootstrap using the
2146    /// session's [`github_token`](Self::github_token). Requires `github_token`
2147    /// to be set; if omitted, the runtime is expected to reject session creation
2148    /// (fail-closed). When `None`, behaves exactly as before. Set via
2149    /// [`with_enable_managed_settings`](Self::with_enable_managed_settings).
2150    pub enable_managed_settings: Option<bool>,
2151    /// Optional managed-settings layer injected at session bootstrap. Currently
2152    /// carries a [`permissions`](ManagedSettingsPermissions) object that composes
2153    /// restrictively with any server- or device-level managed settings. This
2154    /// layer is startup-only and is not persisted: it must be re-supplied on
2155    /// resume to remain in effect. Can be combined with
2156    /// [`enable_managed_settings`](Self::enable_managed_settings). Serialized on
2157    /// the wire as `managedSettings`. Set via
2158    /// [`with_managed_settings`](Self::with_managed_settings).
2159    pub managed_settings: Option<ManagedSettings>,
2160    /// Custom session filesystem provider for this session. Required when
2161    /// the [`Client`](crate::Client) was started with
2162    /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs) set.
2163    /// See [`SessionFsProvider`].
2164    pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2165    /// Optional permission-request handler. When `None`, the SDK sends
2166    /// `requestPermission: false` on the wire so the runtime does not
2167    /// emit `permission.requested` broadcasts to this client.
2168    pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2169    /// Optional elicitation-request handler. When `None`,
2170    /// `requestElicitation: false` goes on the wire.
2171    pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2172    /// Optional MCP OAuth request handler. When set, the SDK can satisfy MCP
2173    /// server OAuth requests with host-acquired token data or cancellation.
2174    pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2175    /// Optional user-input handler. When `None`,
2176    /// `requestUserInput: false` goes on the wire and the `ask_user`
2177    /// tool is disabled.
2178    pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2179    /// Optional exit-plan-mode handler. When `None`,
2180    /// `requestExitPlanMode: false` goes on the wire.
2181    pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2182    /// Optional auto-mode-switch handler. When `None`,
2183    /// `requestAutoModeSwitch: false` goes on the wire.
2184    pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2185    /// Session lifecycle hook handler (pre/post tool use, session
2186    /// start/end, etc.). When set, the SDK auto-enables the wire-level
2187    /// `hooks` flag. Use [`with_hooks`](Self::with_hooks) to install one.
2188    pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2189    /// Permission policy applied to the handler. Stored separately from
2190    /// `permission_handler` so the order of `with_permission_handler` and
2191    /// `approve_all_permissions` (and friends) is irrelevant.
2192    pub(crate) permission_policy: Option<crate::permission::Policy>,
2193    /// System-message transform. When set, the SDK injects the matching
2194    /// `action: "transform"` sections into the system message and routes
2195    /// `systemMessage.transform` RPC callbacks to it during the session.
2196    /// Use [`with_system_message_transform`](Self::with_system_message_transform) to install one.
2197    pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2198    /// Whether to skip loading custom-instruction sources for this session.
2199    /// Applied via `session.options.update` after create/resume. Defaults to
2200    /// `true` in [`crate::ClientMode::Empty`] when unset.
2201    pub skip_custom_instructions: Option<bool>,
2202    /// Whether to constrain custom agents to local-only execution. Sent with
2203    /// the initial create request and maintained via `session.options.update`.
2204    /// Defaults to `true` in [`crate::ClientMode::Empty`] when unset.
2205    pub custom_agents_local_only: Option<bool>,
2206    /// Controls whether the session enables experimental features.
2207    ///
2208    /// Defaults to `false` in [`crate::ClientMode::Empty`] when unset;
2209    /// in `copilot-cli` mode, leaving this unset lets the runtime decide.
2210    pub enable_experimental_mode: Option<bool>,
2211    /// Whether to include the `Co-authored-by` trailer in commit messages.
2212    /// Applied via `session.options.update` after create/resume. Defaults to
2213    /// `false` in [`crate::ClientMode::Empty`] when unset.
2214    pub coauthor_enabled: Option<bool>,
2215    /// Whether to expose the `manage_schedule` tool. Applied via
2216    /// `session.options.update` after create/resume. Defaults to `false` in
2217    /// [`crate::ClientMode::Empty`] when unset.
2218    pub manage_schedule_enabled: Option<bool>,
2219}
2220
2221impl std::fmt::Debug for SessionConfig {
2222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2223        f.debug_struct("SessionConfig")
2224            .field("session_id", &self.session_id)
2225            .field("model", &self.model)
2226            .field("client_name", &self.client_name)
2227            .field("reasoning_effort", &self.reasoning_effort)
2228            .field("reasoning_summary", &self.reasoning_summary)
2229            .field("context_tier", &self.context_tier)
2230            .field("streaming", &self.streaming)
2231            .field("system_message", &self.system_message)
2232            .field("tools", &self.tools)
2233            .field("canvases", &self.canvases)
2234            .field(
2235                "canvas_handler",
2236                &self.canvas_handler.as_ref().map(|_| "<set>"),
2237            )
2238            .field("request_canvas_renderer", &self.request_canvas_renderer)
2239            .field("request_extensions", &self.request_extensions)
2240            .field("extension_sdk_path", &self.extension_sdk_path)
2241            .field("extension_info", &self.extension_info)
2242            .field("canvas_provider", &self.canvas_provider)
2243            .field("available_tools", &self.available_tools)
2244            .field("excluded_tools", &self.excluded_tools)
2245            .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2246            .field("mcp_servers", &self.mcp_servers)
2247            .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2248            .field("embedding_cache_storage", &self.embedding_cache_storage)
2249            .field("enable_config_discovery", &self.enable_config_discovery)
2250            .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2251            .field(
2252                "organization_custom_instructions",
2253                &self
2254                    .organization_custom_instructions
2255                    .as_ref()
2256                    .map(|_| "<redacted>"),
2257            )
2258            .field(
2259                "enable_on_demand_instruction_discovery",
2260                &self.enable_on_demand_instruction_discovery,
2261            )
2262            .field("enable_file_hooks", &self.enable_file_hooks)
2263            .field(
2264                "enable_host_git_operations",
2265                &self.enable_host_git_operations,
2266            )
2267            .field("enable_session_store", &self.enable_session_store)
2268            .field("enable_skills", &self.enable_skills)
2269            .field("enable_mcp_apps", &self.enable_mcp_apps)
2270            .field("skill_directories", &self.skill_directories)
2271            .field("instruction_directories", &self.instruction_directories)
2272            .field("plugin_directories", &self.plugin_directories)
2273            .field("large_output", &self.large_output)
2274            .field("tool_search", &self.tool_search)
2275            .field("disabled_skills", &self.disabled_skills)
2276            .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2277            .field("hooks", &self.hooks)
2278            .field("custom_agents", &self.custom_agents)
2279            .field("default_agent", &self.default_agent)
2280            .field("agent", &self.agent)
2281            .field("infinite_sessions", &self.infinite_sessions)
2282            .field("provider", &self.provider)
2283            .field("capi", &self.capi)
2284            .field("enable_session_telemetry", &self.enable_session_telemetry)
2285            .field("enable_citations", &self.enable_citations)
2286            .field(
2287                "enable_file_change_tracking",
2288                &self.enable_file_change_tracking,
2289            )
2290            .field("session_limits", &self.session_limits)
2291            .field("model_capabilities", &self.model_capabilities)
2292            .field("memory", &self.memory)
2293            .field("config_directory", &self.config_directory)
2294            .field("working_directory", &self.working_directory)
2295            .field("additional_directories", &self.additional_directories)
2296            .field(
2297                "github_token",
2298                &self.github_token.as_ref().map(|_| "<redacted>"),
2299            )
2300            .field("remote_session", &self.remote_session)
2301            .field("cloud", &self.cloud)
2302            .field(
2303                "include_sub_agent_streaming_events",
2304                &self.include_sub_agent_streaming_events,
2305            )
2306            .field("commands", &self.commands)
2307            .field("exp_assignments", &self.exp_assignments)
2308            .field("enable_managed_settings", &self.enable_managed_settings)
2309            .field("enable_experimental_mode", &self.enable_experimental_mode)
2310            .field("managed_settings", &self.managed_settings)
2311            .field(
2312                "session_fs_provider",
2313                &self.session_fs_provider.as_ref().map(|_| "<set>"),
2314            )
2315            .field(
2316                "permission_handler",
2317                &self.permission_handler.as_ref().map(|_| "<set>"),
2318            )
2319            .field(
2320                "elicitation_handler",
2321                &self.elicitation_handler.as_ref().map(|_| "<set>"),
2322            )
2323            .field(
2324                "mcp_auth_handler",
2325                &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2326            )
2327            .field(
2328                "user_input_handler",
2329                &self.user_input_handler.as_ref().map(|_| "<set>"),
2330            )
2331            .field(
2332                "exit_plan_mode_handler",
2333                &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2334            )
2335            .field(
2336                "auto_mode_switch_handler",
2337                &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2338            )
2339            .field(
2340                "hooks_handler",
2341                &self.hooks_handler.as_ref().map(|_| "<set>"),
2342            )
2343            .field(
2344                "system_message_transform",
2345                &self.system_message_transform.as_ref().map(|_| "<set>"),
2346            )
2347            .finish()
2348    }
2349}
2350
2351impl Default for SessionConfig {
2352    /// All wire-level "request" flags and handler fields start unset.
2353    /// Install a [`PermissionHandler`] via
2354    /// [`with_permission_handler`](Self::with_permission_handler) and
2355    /// the SDK derives `requestPermission: true` on the wire at
2356    /// [`Client::create_session`](crate::Client::create_session) time.
2357    fn default() -> Self {
2358        Self {
2359            session_id: None,
2360            model: None,
2361            client_name: None,
2362            reasoning_effort: None,
2363            reasoning_summary: None,
2364            context_tier: None,
2365            streaming: None,
2366            system_message: None,
2367            tools: None,
2368            canvases: None,
2369            canvas_handler: None,
2370            request_canvas_renderer: None,
2371            request_extensions: None,
2372            extension_sdk_path: None,
2373            extension_info: None,
2374            canvas_provider: None,
2375            available_tools: None,
2376            excluded_tools: None,
2377            excluded_builtin_agents: None,
2378            mcp_servers: None,
2379            mcp_oauth_token_storage: None,
2380            enable_config_discovery: None,
2381            skip_embedding_retrieval: None,
2382            organization_custom_instructions: None,
2383            enable_on_demand_instruction_discovery: None,
2384            enable_file_hooks: None,
2385            enable_host_git_operations: None,
2386            enable_session_store: None,
2387            enable_skills: None,
2388            embedding_cache_storage: None,
2389            enable_mcp_apps: None,
2390            github_mcp_tool_config: None,
2391            skill_directories: None,
2392            instruction_directories: None,
2393            plugin_directories: None,
2394            large_output: None,
2395            tool_search: None,
2396            disabled_skills: None,
2397            disabled_mcp_servers: None,
2398            hooks: None,
2399            custom_agents: None,
2400            default_agent: None,
2401            agent: None,
2402            infinite_sessions: None,
2403            provider: None,
2404            capi: None,
2405            providers: None,
2406            models: None,
2407            enable_session_telemetry: None,
2408            enable_citations: None,
2409            enable_file_change_tracking: None,
2410            session_limits: None,
2411            model_capabilities: None,
2412            memory: None,
2413            config_directory: None,
2414            working_directory: None,
2415            additional_directories: None,
2416            github_token: None,
2417            remote_session: None,
2418            cloud: None,
2419            include_sub_agent_streaming_events: None,
2420            commands: None,
2421            exp_assignments: None,
2422            enable_managed_settings: None,
2423            managed_settings: None,
2424            session_fs_provider: None,
2425            permission_handler: None,
2426            elicitation_handler: None,
2427            mcp_auth_handler: None,
2428            user_input_handler: None,
2429            exit_plan_mode_handler: None,
2430            auto_mode_switch_handler: None,
2431            hooks_handler: None,
2432            permission_policy: None,
2433            system_message_transform: None,
2434            skip_custom_instructions: None,
2435            custom_agents_local_only: None,
2436            enable_experimental_mode: None,
2437            coauthor_enabled: None,
2438            manage_schedule_enabled: None,
2439        }
2440    }
2441}
2442
2443/// Runtime-only bundle drained out of a [`SessionConfig`] or
2444/// [`ResumeSessionConfig`] by [`SessionConfig::into_wire`] /
2445/// [`ResumeSessionConfig::into_wire`]. Holds the trait-object handlers,
2446/// session-fs provider, and slash commands so the wire payload struct
2447/// stays a pure data shape.
2448pub(crate) struct SessionConfigRuntime {
2449    pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2450    pub permission_policy: Option<crate::permission::Policy>,
2451    pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2452    pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2453    pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2454    pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2455    pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2456    pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2457    pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2458    pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2459    pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2460    pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2461    pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2462    pub commands: Option<Vec<CommandDefinition>>,
2463}
2464
2465impl SessionConfig {
2466    /// Consume this config to produce the [`SessionCreateWire`] payload
2467    /// for `session.create` and a [`SessionConfigRuntime`] bundle holding
2468    /// the runtime-only fields (handlers, transforms, providers).
2469    ///
2470    /// Wire-format flags are derived from handler presence and the policy
2471    /// field; runtime fields are moved out into the returned runtime so
2472    /// the deep `Vec<Tool>` / `IndexMap<String, Value>` clones the previous
2473    /// `&self`-based shape required are eliminated, and the order of
2474    /// reading-vs-moving is enforced at compile time.
2475    ///
2476    /// [`SessionCreateWire`]: crate::wire::SessionCreateWire
2477    pub(crate) fn into_wire(
2478        mut self,
2479        session_id: Option<SessionId>,
2480    ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2481        let permission_active =
2482            self.permission_handler.is_some() || self.permission_policy.is_some();
2483        let request_user_input = self.user_input_handler.is_some();
2484        let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2485        let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2486        let request_elicitation = self.elicitation_handler.is_some();
2487        let hooks_flag = self.hooks_handler.is_some();
2488
2489        let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2490        if let Some(tools) = self.tools.as_mut() {
2491            for tool in tools.iter_mut() {
2492                if let Some(handler) = tool.handler.take()
2493                    && tool_handlers.insert(tool.name.clone(), handler).is_some()
2494                {
2495                    return Err(crate::Error::with_message(
2496                        crate::ErrorKind::InvalidConfig,
2497                        format!("duplicate tool handler registered for name {:?}", tool.name),
2498                    ));
2499                }
2500            }
2501        }
2502
2503        let wire_commands = self.commands.as_ref().map(|cmds| {
2504            cmds.iter()
2505                .map(|c| crate::wire::CommandWireDefinition {
2506                    name: c.name.clone(),
2507                    description: c.description.clone().unwrap_or_default(),
2508                })
2509                .collect()
2510        });
2511        let wire_canvases = self.canvases.clone();
2512        let canvas_handler = self.canvas_handler.clone();
2513        let bearer_token_providers =
2514            prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2515
2516        let wire = crate::wire::SessionCreateWire {
2517            session_id,
2518            model: self.model,
2519            client_name: self.client_name,
2520            reasoning_effort: self.reasoning_effort,
2521            reasoning_summary: self.reasoning_summary,
2522            context_tier: self.context_tier,
2523            streaming: self.streaming,
2524            system_message: self.system_message,
2525            tools: self.tools,
2526            canvases: wire_canvases,
2527            request_canvas_renderer: self.request_canvas_renderer,
2528            request_extensions: self.request_extensions,
2529            extension_sdk_path: self.extension_sdk_path,
2530            extension_info: self.extension_info,
2531            canvas_provider: self.canvas_provider,
2532            available_tools: self.available_tools,
2533            excluded_tools: self.excluded_tools,
2534            excluded_builtin_agents: self.excluded_builtin_agents,
2535            tool_filter_precedence: "excluded",
2536            mcp_servers: self.mcp_servers,
2537            mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2538            embedding_cache_storage: self.embedding_cache_storage,
2539            env_value_mode: "direct",
2540            enable_config_discovery: self.enable_config_discovery,
2541            skip_embedding_retrieval: self.skip_embedding_retrieval,
2542            organization_custom_instructions: self.organization_custom_instructions,
2543            enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2544            enable_file_hooks: self.enable_file_hooks,
2545            enable_host_git_operations: self.enable_host_git_operations,
2546            enable_session_store: self.enable_session_store,
2547            enable_skills: self.enable_skills,
2548            request_user_input,
2549            request_permission: permission_active,
2550            request_exit_plan_mode,
2551            request_auto_mode_switch,
2552            request_elicitation,
2553            request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2554            github_mcp_tool_config: self.github_mcp_tool_config,
2555            hooks: hooks_flag,
2556            skill_directories: self.skill_directories,
2557            instruction_directories: self.instruction_directories,
2558            plugin_directories: self.plugin_directories,
2559            large_output: self.large_output,
2560            tool_search: self.tool_search,
2561            disabled_skills: self.disabled_skills,
2562            disabled_mcp_servers: self.disabled_mcp_servers,
2563            custom_agents: self.custom_agents,
2564            custom_agents_local_only: self.custom_agents_local_only,
2565            default_agent: self.default_agent,
2566            agent: self.agent,
2567            infinite_sessions: self.infinite_sessions,
2568            provider: self.provider,
2569            capi: self.capi,
2570            providers: self.providers,
2571            models: self.models,
2572            enable_session_telemetry: self.enable_session_telemetry,
2573            enable_citations: self.enable_citations,
2574            enable_file_change_tracking: self.enable_file_change_tracking,
2575            session_limits: self.session_limits,
2576            model_capabilities: self.model_capabilities,
2577            memory: self.memory,
2578            config_dir: self.config_directory,
2579            working_directory: self.working_directory,
2580            additional_directories: self.additional_directories,
2581            github_token: self.github_token,
2582            remote_session: self.remote_session,
2583            cloud: self.cloud,
2584            include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2585            enable_github_telemetry_forwarding: None,
2586            commands: wire_commands,
2587            exp_assignments: self.exp_assignments,
2588            enable_managed_settings: self.enable_managed_settings,
2589            is_experimental_mode: self.enable_experimental_mode,
2590            managed_settings: self.managed_settings,
2591        };
2592
2593        let runtime = SessionConfigRuntime {
2594            permission_handler: self.permission_handler,
2595            permission_policy: self.permission_policy,
2596            elicitation_handler: self.elicitation_handler,
2597            mcp_auth_handler: self.mcp_auth_handler,
2598            user_input_handler: self.user_input_handler,
2599            exit_plan_mode_handler: self.exit_plan_mode_handler,
2600            auto_mode_switch_handler: self.auto_mode_switch_handler,
2601            hooks_handler: self.hooks_handler,
2602            system_message_transform: self.system_message_transform,
2603            tool_handlers,
2604            canvas_handler,
2605            session_fs_provider: self.session_fs_provider,
2606            bearer_token_providers,
2607            commands: self.commands,
2608        };
2609
2610        Ok((wire, runtime))
2611    }
2612
2613    /// Install a [`PermissionHandler`] for this session. When omitted, the
2614    /// SDK sends `requestPermission: false` on the wire and the runtime
2615    /// short-circuits permission prompts for this client.
2616    pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2617        self.permission_handler = Some(handler);
2618        self
2619    }
2620
2621    /// Install an [`ElicitationHandler`]. When omitted, the SDK sends
2622    /// `requestElicitation: false` on the wire.
2623    pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2624        self.elicitation_handler = Some(handler);
2625        self
2626    }
2627
2628    /// Install an [`McpAuthHandler`] for host-provided MCP OAuth tokens.
2629    pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2630        self.mcp_auth_handler = Some(handler);
2631        self
2632    }
2633
2634    /// Install a [`UserInputHandler`]. Required for the `ask_user` tool
2635    /// to be enabled.
2636    pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2637        self.user_input_handler = Some(handler);
2638        self
2639    }
2640
2641    /// Install an [`ExitPlanModeHandler`].
2642    pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2643        self.exit_plan_mode_handler = Some(handler);
2644        self
2645    }
2646
2647    /// Install an [`AutoModeSwitchHandler`].
2648    pub fn with_auto_mode_switch_handler(
2649        mut self,
2650        handler: Arc<dyn AutoModeSwitchHandler>,
2651    ) -> Self {
2652        self.auto_mode_switch_handler = Some(handler);
2653        self
2654    }
2655
2656    /// Register slash commands for this session. Each command appears as
2657    /// `/name` in the CLI's TUI; the handler is invoked when the user
2658    /// executes the command. Replaces any commands previously set on this
2659    /// config. See [`CommandDefinition`].
2660    pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2661        self.commands = Some(commands);
2662        self
2663    }
2664
2665    /// Install a [`SessionFsProvider`] backing the session's filesystem.
2666    /// Required when the [`Client`](crate::Client) was started with
2667    /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs).
2668    pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2669        self.session_fs_provider = Some(provider);
2670        self
2671    }
2672
2673    /// Install a [`SessionHooks`] handler. Automatically enables the
2674    /// wire-level `hooks` flag on session creation.
2675    pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2676        self.hooks_handler = Some(hooks);
2677        self
2678    }
2679
2680    /// Install a [`SystemMessageTransform`]. The SDK injects the matching
2681    /// `action: "transform"` sections into the system message and routes
2682    /// `systemMessage.transform` RPC callbacks to it during the session.
2683    pub fn with_system_message_transform(
2684        mut self,
2685        transform: Arc<dyn SystemMessageTransform>,
2686    ) -> Self {
2687        self.system_message_transform = Some(transform);
2688        self
2689    }
2690
2691    /// Auto-approve every permission request on this session. Stored as a
2692    /// policy that's applied at
2693    /// [`Client::create_session`](crate::Client::create_session) time, so
2694    /// order with [`with_permission_handler`](Self::with_permission_handler)
2695    /// is irrelevant.
2696    pub fn approve_all_permissions(mut self) -> Self {
2697        self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2698        self
2699    }
2700
2701    /// Auto-deny every permission request on this session. See
2702    /// [`approve_all_permissions`](Self::approve_all_permissions).
2703    pub fn deny_all_permissions(mut self) -> Self {
2704        self.permission_policy = Some(crate::permission::Policy::DenyAll);
2705        self
2706    }
2707
2708    /// Apply a closure-based permission policy: `predicate` returns `true`
2709    /// to approve, `false` to deny. See
2710    /// [`approve_all_permissions`](Self::approve_all_permissions) for
2711    /// ordering semantics.
2712    pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2713    where
2714        F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2715    {
2716        self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2717        self
2718    }
2719
2720    /// Set a custom session ID (when unset, the CLI generates one).
2721    pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2722        self.session_id = Some(id.into());
2723        self
2724    }
2725
2726    /// Set the model identifier (e.g. `"claude-sonnet-4"`).
2727    pub fn with_model(mut self, model: impl Into<String>) -> Self {
2728        self.model = Some(model.into());
2729        self
2730    }
2731
2732    /// Set the application name sent as `User-Agent` context.
2733    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2734        self.client_name = Some(name.into());
2735        self
2736    }
2737
2738    /// Set the reasoning effort level (e.g. `"low"`, `"medium"`, `"high"`).
2739    pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2740        self.reasoning_effort = Some(effort.into());
2741        self
2742    }
2743
2744    /// Set [`reasoning_summary`](Self::reasoning_summary).
2745    pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2746        self.reasoning_summary = Some(summary);
2747        self
2748    }
2749
2750    /// Set the context window tier (e.g. `"default"`, `"long_context"`).
2751    pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2752        self.context_tier = Some(tier.into());
2753        self
2754    }
2755
2756    /// Enable streaming token deltas via `assistant.message_delta` events.
2757    pub fn with_streaming(mut self, streaming: bool) -> Self {
2758        self.streaming = Some(streaming);
2759        self
2760    }
2761
2762    /// Set a custom system message configuration.
2763    pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2764        self.system_message = Some(system_message);
2765        self
2766    }
2767
2768    /// Set the client-defined tools to expose to the agent.
2769    pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2770        self.tools = Some(tools.into_iter().collect());
2771        self
2772    }
2773
2774    /// Set canvas declarations for this connection. The runtime advertises
2775    /// these to the agent; install a [`CanvasHandler`] via
2776    /// [`with_canvas_handler`](Self::with_canvas_handler) to receive the
2777    /// resulting provider callbacks.
2778    pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2779        self.canvases = Some(canvases.into_iter().collect());
2780        self
2781    }
2782
2783    /// Install the provider-side [`CanvasHandler`] for this session.
2784    pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2785        self.canvas_handler = Some(handler);
2786        self
2787    }
2788
2789    /// Request host canvas renderer tools for this connection.
2790    pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2791        self.request_canvas_renderer = Some(request);
2792        self
2793    }
2794
2795    /// Request extension tools and dispatch for this connection.
2796    pub fn with_request_extensions(mut self, request: bool) -> Self {
2797        self.request_extensions = Some(request);
2798        self
2799    }
2800
2801    /// Override the bundled `@github/copilot-sdk` drop injected into extension
2802    /// subprocesses for this session. Invalid paths fall back to the bundled
2803    /// SDK silently.
2804    pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2805        self.extension_sdk_path = Some(path.into());
2806        self
2807    }
2808
2809    /// Set stable extension identity metadata for this connection.
2810    pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2811        self.extension_info = Some(extension_info);
2812        self
2813    }
2814
2815    /// Set the canvas provider identity for this connection so host-supplied
2816    /// canvases survive reconnect and CLI restart.
2817    pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2818        self.canvas_provider = Some(canvas_provider);
2819        self
2820    }
2821
2822    /// Set the allowlist of built-in tool names the agent may use.
2823    pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2824    where
2825        I: IntoIterator<Item = S>,
2826        S: Into<String>,
2827    {
2828        self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2829        self
2830    }
2831
2832    /// Set the blocklist of built-in tool names the agent must not use.
2833    pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2834    where
2835        I: IntoIterator<Item = S>,
2836        S: Into<String>,
2837    {
2838        self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2839        self
2840    }
2841
2842    /// Set the built-in agent names to exclude from the session.
2843    pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2844    where
2845        I: IntoIterator<Item = S>,
2846        S: Into<String>,
2847    {
2848        self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2849        self
2850    }
2851
2852    /// Set MCP server configurations passed through to the CLI.
2853    pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2854        self.mcp_servers = Some(servers);
2855        self
2856    }
2857
2858    /// Set MCP OAuth token storage mode.
2859    ///
2860    /// - `"persistent"` — tokens stored in the OS keychain.
2861    /// - `"in-memory"` — tokens discarded when the session ends.
2862    ///
2863    /// Defaults to `"in-memory"` when the client is in [`crate::ClientMode::Empty`],
2864    /// applied automatically at session creation/resume time.
2865    pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2866        self.mcp_oauth_token_storage = Some(mode.into());
2867        self
2868    }
2869
2870    /// Set embedding cache storage mode.
2871    pub fn with_embedding_cache_storage(
2872        mut self,
2873        embedding_cache_storage: impl Into<String>,
2874    ) -> Self {
2875        self.embedding_cache_storage = Some(embedding_cache_storage.into());
2876        self
2877    }
2878
2879    /// Enables runtime discovery of supported configuration. Explicitly supplied
2880    /// configuration takes precedence over discovered values.
2881    pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2882        self.enable_config_discovery = Some(enable);
2883        self
2884    }
2885
2886    /// Set [`Self::skip_embedding_retrieval`].
2887    pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2888        self.skip_embedding_retrieval = Some(value);
2889        self
2890    }
2891
2892    /// Set [`Self::organization_custom_instructions`].
2893    pub fn with_organization_custom_instructions(
2894        mut self,
2895        instructions: impl Into<String>,
2896    ) -> Self {
2897        self.organization_custom_instructions = Some(instructions.into());
2898        self
2899    }
2900
2901    /// Set [`Self::enable_on_demand_instruction_discovery`].
2902    pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2903        self.enable_on_demand_instruction_discovery = Some(value);
2904        self
2905    }
2906
2907    /// Set [`Self::enable_file_hooks`].
2908    pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2909        self.enable_file_hooks = Some(value);
2910        self
2911    }
2912
2913    /// Set [`Self::enable_host_git_operations`].
2914    pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2915        self.enable_host_git_operations = Some(value);
2916        self
2917    }
2918
2919    /// Set [`Self::enable_session_store`].
2920    pub fn with_enable_session_store(mut self, value: bool) -> Self {
2921        self.enable_session_store = Some(value);
2922        self
2923    }
2924
2925    /// Set [`Self::enable_skills`].
2926    pub fn with_enable_skills(mut self, value: bool) -> Self {
2927        self.enable_skills = Some(value);
2928        self
2929    }
2930
2931    /// **Experimental.** This method is part of an experimental wire-protocol
2932    /// surface (SEP-1865) and may change or be removed in a future release.
2933    ///
2934    /// Enable MCP Apps (SEP-1865) UI passthrough on this session. Defaults
2935    /// to `None` (treated as `false`). See [`SessionConfig::enable_mcp_apps`].
2936    pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2937        self.enable_mcp_apps = Some(enable);
2938        self
2939    }
2940
2941    /// Set the built-in GitHub MCP server configuration.
2942    pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
2943        self.github_mcp_tool_config = Some(config);
2944        self
2945    }
2946
2947    /// Set skill directory paths passed through to the CLI.
2948    pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2949    where
2950        I: IntoIterator<Item = P>,
2951        P: Into<PathBuf>,
2952    {
2953        self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2954        self
2955    }
2956
2957    /// Set additional directories to search for custom instruction files.
2958    /// Forwarded to the CLI on session create; not the same as
2959    /// [`with_skill_directories`](Self::with_skill_directories).
2960    pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2961    where
2962        I: IntoIterator<Item = P>,
2963        P: Into<PathBuf>,
2964    {
2965        self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2966        self
2967    }
2968
2969    /// Set Open Plugin directory paths passed through to the CLI on session create.
2970    pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2971    where
2972        I: IntoIterator<Item = P>,
2973        P: Into<PathBuf>,
2974    {
2975        self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2976        self
2977    }
2978
2979    /// Set the [`LargeToolOutputConfig`] forwarded to the CLI on session create.
2980    pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2981        self.large_output = Some(config);
2982        self
2983    }
2984
2985    /// Set the [`ToolSearchConfig`] overriding the runtime's built-in
2986    /// tool-search behavior on session create.
2987    pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2988        self.tool_search = Some(config);
2989        self
2990    }
2991
2992    /// Set the names of skills to disable (overrides skill discovery).
2993    pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2994    where
2995        I: IntoIterator<Item = S>,
2996        S: Into<String>,
2997    {
2998        self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2999        self
3000    }
3001
3002    /// Set exact MCP server names to disable for this session.
3003    pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3004    where
3005        I: IntoIterator<Item = S>,
3006        S: Into<String>,
3007    {
3008        self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3009        self
3010    }
3011
3012    /// Set the custom agents (sub-agents) configured for this session.
3013    pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3014        mut self,
3015        agents: I,
3016    ) -> Self {
3017        self.custom_agents = Some(agents.into_iter().collect());
3018        self
3019    }
3020
3021    /// Configure the built-in default agent.
3022    pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3023        self.default_agent = Some(agent);
3024        self
3025    }
3026
3027    /// Activate a named custom agent on session start. Must match the
3028    /// `name` of one of the agents in [`Self::custom_agents`].
3029    pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3030        self.agent = Some(name.into());
3031        self
3032    }
3033
3034    /// Configure infinite sessions (persistent workspace + automatic
3035    /// context-window compaction).
3036    pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3037        self.infinite_sessions = Some(config);
3038        self
3039    }
3040
3041    /// Configure a custom model provider (BYOK).
3042    pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3043        self.provider = Some(provider);
3044        self
3045    }
3046
3047    /// Configure provider-scoped CAPI session options.
3048    pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3049        self.capi = Some(capi);
3050        self
3051    }
3052
3053    /// **Experimental.** This method is part of an experimental multi-provider
3054    /// BYOK surface and may change or be removed in a future release.
3055    ///
3056    /// Set the named BYOK provider connections (additive multi-provider
3057    /// registry). Attach models referencing these with [`Self::with_models`].
3058    pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3059        self.providers = Some(providers);
3060        self
3061    }
3062
3063    /// **Experimental.** This method is part of an experimental multi-provider
3064    /// BYOK surface and may change or be removed in a future release.
3065    ///
3066    /// Set the BYOK model definitions, each referencing a named provider
3067    /// supplied via [`Self::with_providers`].
3068    pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3069        self.models = Some(models);
3070        self
3071    }
3072
3073    /// Enable or disable internal session telemetry.
3074    ///
3075    /// See [`Self::enable_session_telemetry`] for default and BYOK behavior.
3076    pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3077        self.enable_session_telemetry = Some(enable);
3078        self
3079    }
3080
3081    /// **Experimental.** Enable native model citations for supported providers.
3082    pub fn with_enable_citations(mut self, enable: bool) -> Self {
3083        self.enable_citations = Some(enable);
3084        self
3085    }
3086
3087    /// Opt in to capturing file changes from the first turn for session rewind
3088    /// and cumulative session diff.
3089    pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3090        self.enable_file_change_tracking = Some(enable);
3091        self
3092    }
3093
3094    /// **Experimental.** Set limits for this session's current accounting window.
3095    pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3096        self.session_limits = Some(limits);
3097        self
3098    }
3099
3100    /// Set per-property overrides for model capabilities.
3101    pub fn with_model_capabilities(
3102        mut self,
3103        capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3104    ) -> Self {
3105        self.model_capabilities = Some(capabilities);
3106        self
3107    }
3108
3109    /// Configure the runtime memory feature for this session.
3110    pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3111        self.memory = Some(memory);
3112        self
3113    }
3114
3115    /// Override the default configuration directory location.
3116    pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3117        self.config_directory = Some(dir.into());
3118        self
3119    }
3120
3121    /// Set the per-session working directory. Tool operations resolve
3122    /// relative paths against this directory.
3123    pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3124        self.working_directory = Some(dir.into());
3125        self
3126    }
3127
3128    /// Set directories the agent may access beyond the working directory.
3129    pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3130    where
3131        I: IntoIterator<Item = P>,
3132        P: Into<PathBuf>,
3133    {
3134        self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3135        self
3136    }
3137
3138    /// Set the per-session GitHub token. Distinct from
3139    /// [`ClientOptions::github_token`](crate::ClientOptions::github_token);
3140    /// this token determines the GitHub identity used for content exclusion,
3141    /// model routing, and quota checks for this session only.
3142    pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3143        self.github_token = Some(token.into());
3144        self
3145    }
3146
3147    /// Forward sub-agent streaming events to this connection. Defaults
3148    /// to true on the CLI when unset.
3149    pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3150        self.include_sub_agent_streaming_events = Some(include);
3151        self
3152    }
3153
3154    /// Set per-session remote behavior.
3155    pub fn with_remote_session(
3156        mut self,
3157        mode: crate::generated::api_types::RemoteSessionMode,
3158    ) -> Self {
3159        self.remote_session = Some(mode);
3160        self
3161    }
3162
3163    /// Create a remote session in the cloud instead of a local session.
3164    pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3165        self.cloud = Some(cloud);
3166        self
3167    }
3168
3169    /// Set [`Self::skip_custom_instructions`].
3170    pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3171        self.skip_custom_instructions = Some(value);
3172        self
3173    }
3174
3175    /// Set [`Self::custom_agents_local_only`].
3176    pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3177        self.custom_agents_local_only = Some(value);
3178        self
3179    }
3180
3181    /// Set [`enable_experimental_mode`](Self::enable_experimental_mode).
3182    pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3183        self.enable_experimental_mode = Some(enable_experimental_mode);
3184        self
3185    }
3186
3187    /// Set [`Self::coauthor_enabled`].
3188    pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3189        self.coauthor_enabled = Some(value);
3190        self
3191    }
3192
3193    /// Set [`Self::manage_schedule_enabled`].
3194    pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3195        self.manage_schedule_enabled = Some(value);
3196        self
3197    }
3198
3199    /// Inject ExP assignment ("flight") data for this session, in the same
3200    /// JSON shape the Copilot CLI fetches from the experimentation service
3201    /// (`CopilotExpAssignmentResponse`). The runtime feeds it into the same
3202    /// feature-flag path as CLI-fetched assignments and stamps it onto
3203    /// telemetry and the CAPI request header. Intended for trusted
3204    /// integrators that fetch ExP data out of process; malformed payloads
3205    /// are dropped by the runtime (fail-open).
3206    #[doc(hidden)]
3207    pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3208        self.exp_assignments = Some(assignments);
3209        self
3210    }
3211
3212    /// Opt the runtime into self-fetching enterprise managed settings
3213    /// (bypass-permissions policy) at session bootstrap using the session's
3214    /// [`github_token`](Self::github_token). Requires `github_token` to be set;
3215    /// if omitted, the runtime is expected to reject session creation
3216    /// (fail-closed).
3217    pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3218        self.enable_managed_settings = Some(enabled);
3219        self
3220    }
3221
3222    /// Inject a managed-settings layer (currently permission rules) at session
3223    /// bootstrap. This layer is startup-only and is not persisted, so it must be
3224    /// re-supplied on resume to remain in effect. Can be combined with
3225    /// [`with_enable_managed_settings`](Self::with_enable_managed_settings).
3226    pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3227        self.managed_settings = Some(managed_settings);
3228        self
3229    }
3230}
3231///
3232/// See [`SessionConfig`] for the construction patterns (chained `with_*`
3233/// builder vs. direct field assignment for `Option<T>` pass-through) and
3234/// the note on snake_case vs. camelCase field naming. This config is not
3235/// itself serializable — call `ResumeSessionConfig::into_wire`
3236/// (crate-private) to produce the wire payload.
3237#[derive(Clone)]
3238#[non_exhaustive]
3239pub struct ResumeSessionConfig {
3240    /// ID of the session to resume.
3241    pub session_id: SessionId,
3242    /// Model to use for this session (e.g. `"gpt-4"`, `"claude-sonnet-4"`).
3243    /// Can change the model when resuming.
3244    pub model: Option<String>,
3245    /// Application name sent as User-Agent context.
3246    pub client_name: Option<String>,
3247    /// Desired reasoning effort to apply after resuming the session.
3248    pub reasoning_effort: Option<String>,
3249    /// Reasoning summary mode to apply after resuming the session. Use
3250    /// [`ReasoningSummary::None`] to suppress summary output regardless of
3251    /// whether reasoning is enabled.
3252    pub reasoning_summary: Option<ReasoningSummary>,
3253    /// Context window tier to apply after resuming the session. Use
3254    /// `"long_context"` to pin the session to the long-context tier.
3255    pub context_tier: Option<String>,
3256    /// Enable streaming token deltas.
3257    pub streaming: Option<bool>,
3258    /// Re-supply the system message so the agent retains workspace context
3259    /// across CLI process restarts.
3260    pub system_message: Option<SystemMessageConfig>,
3261    /// Client-defined tool declarations to re-supply on resume.
3262    pub tools: Option<Vec<Tool>>,
3263    /// Canvas declarations this connection provides to the runtime.
3264    pub canvases: Option<Vec<CanvasDeclaration>>,
3265    /// Provider-side canvas lifecycle handler. See
3266    /// [`SessionConfig::canvas_handler`].
3267    pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3268    /// Open canvas instances the caller knows were open before this resume.
3269    pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3270    /// Request canvas renderer tools for this connection.
3271    pub request_canvas_renderer: Option<bool>,
3272    /// Request extension tools and dispatch for this connection.
3273    pub request_extensions: Option<bool>,
3274    /// Optional override path to a `copilot-sdk/` folder to inject into
3275    /// extension subprocesses for this session on resume. See
3276    /// `SessionConfig::extension_sdk_path`.
3277    pub extension_sdk_path: Option<String>,
3278    /// Stable extension identity for canvas/tool providers on this connection.
3279    pub extension_info: Option<ExtensionInfo>,
3280    /// Stable identity for a host/SDK connection that supplies built-in
3281    /// canvases, so they rehydrate against a stable extension id on resume.
3282    pub canvas_provider: Option<CanvasProviderIdentity>,
3283    /// Allowlist of tool names the agent may use.
3284    pub available_tools: Option<Vec<String>>,
3285    /// Blocklist of built-in tool names.
3286    pub excluded_tools: Option<Vec<String>>,
3287    /// Names of built-in agents to exclude from the resumed session.
3288    ///
3289    /// Excluded built-in agents are hidden from discovery and cannot be
3290    /// selected or invoked unless a custom agent with the same name is
3291    /// configured.
3292    pub excluded_builtin_agents: Option<Vec<String>>,
3293    /// Re-supply MCP servers so they remain available after app restart.
3294    pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3295    /// Controls how MCP OAuth tokens are stored for this session.
3296    /// See [`SessionConfig::mcp_oauth_token_storage`] for details.
3297    pub mcp_oauth_token_storage: Option<String>,
3298    /// Enables runtime discovery of supported configuration. Explicitly supplied
3299    /// configuration takes precedence over discovered values.
3300    pub enable_config_discovery: Option<bool>,
3301    /// When true, skips embedding retrieval on resume.
3302    pub skip_embedding_retrieval: Option<bool>,
3303    /// Controls how the embedding cache is stored for this session.
3304    pub embedding_cache_storage: Option<String>,
3305    /// Organization-level custom instructions to apply on resume.
3306    pub organization_custom_instructions: Option<String>,
3307    /// When true, enables on-demand instruction discovery on resume.
3308    pub enable_on_demand_instruction_discovery: Option<bool>,
3309    /// When true, enables file hooks on resume.
3310    pub enable_file_hooks: Option<bool>,
3311    /// When true, allows host Git operations on resume.
3312    pub enable_host_git_operations: Option<bool>,
3313    /// When true, enables the session store on resume.
3314    pub enable_session_store: Option<bool>,
3315    /// When true, enables skills on resume.
3316    pub enable_skills: Option<bool>,
3317    /// **Experimental.** This option is part of an experimental wire-protocol
3318    /// surface (SEP-1865) and may change or be removed in a future release.
3319    ///
3320    /// Enable MCP Apps (SEP-1865) UI passthrough on resume. See
3321    /// [`SessionConfig::enable_mcp_apps`]. Defaults to `None` (treated as `false`).
3322    pub enable_mcp_apps: Option<bool>,
3323    /// Configuration for the built-in GitHub MCP server.
3324    ///
3325    /// `disable_form_deferral` only applies to that server and only has an
3326    /// effect when MCP Apps and form-backed GitHub tools are enabled.
3327    pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3328    /// Skill directory paths passed through to the GitHub Copilot CLI on resume.
3329    pub skill_directories: Option<Vec<PathBuf>>,
3330    /// Additional directories to search for custom instruction files on
3331    /// resume. Forwarded to the CLI; not the same as [`skill_directories`](Self::skill_directories).
3332    pub instruction_directories: Option<Vec<PathBuf>>,
3333    /// Open Plugin directory paths passed through to the CLI on resume.
3334    pub plugin_directories: Option<Vec<PathBuf>>,
3335    /// Configuration for large tool output handling, forwarded to the CLI on resume.
3336    pub large_output: Option<LargeToolOutputConfig>,
3337    /// Overrides the runtime's built-in tool-search behavior on resume. When
3338    /// unset, the runtime default applies.
3339    pub tool_search: Option<ToolSearchConfig>,
3340    /// Skill names to disable on resume.
3341    pub disabled_skills: Option<Vec<String>>,
3342    /// Exact MCP server names to disable on resume. This prevents startup and
3343    /// authentication during a cold resume, but cannot stop resident servers.
3344    pub disabled_mcp_servers: Option<Vec<String>>,
3345    /// Enable session hooks on resume.
3346    pub hooks: Option<bool>,
3347    /// Custom agents to re-supply on resume.
3348    pub custom_agents: Option<Vec<CustomAgentConfig>>,
3349    /// Configures the built-in default agent on resume.
3350    pub default_agent: Option<DefaultAgentConfig>,
3351    /// Name of the custom agent to activate.
3352    pub agent: Option<String>,
3353    /// Re-supply infinite session configuration on resume.
3354    pub infinite_sessions: Option<InfiniteSessionConfig>,
3355    /// Re-supply BYOK provider configuration on resume.
3356    pub provider: Option<ProviderConfig>,
3357    /// Re-supply provider-scoped CAPI session options on resume.
3358    ///
3359    /// Use this to opt out of the default WebSocket transport for CAPI
3360    /// Responses API calls, equivalent to setting
3361    /// `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES`.
3362    pub capi: Option<CapiSessionOptions>,
3363    /// **Experimental.** This field is part of an experimental multi-provider
3364    /// BYOK surface and may change or be removed in a future release.
3365    ///
3366    /// Re-supply named BYOK provider connections on resume. Additive to
3367    /// the default Copilot routing. Referenced by [`models`](Self::models).
3368    pub providers: Option<Vec<NamedProviderConfig>>,
3369    /// **Experimental.** This field is part of an experimental multi-provider
3370    /// BYOK surface and may change or be removed in a future release.
3371    ///
3372    /// Re-supply BYOK model definitions on resume, each referencing a
3373    /// [`providers`](Self::providers) entry by name.
3374    pub models: Option<Vec<ProviderModelConfig>>,
3375    /// Enables or disables internal session telemetry for this session.
3376    ///
3377    /// When `Some(false)`, disables session telemetry. When `None` or
3378    /// `Some(true)`, telemetry is enabled for GitHub-authenticated sessions.
3379    /// When a custom [`provider`](Self::provider) is configured, session
3380    /// telemetry is always disabled regardless of this setting. This is
3381    /// independent of [`ClientOptions::telemetry`](crate::ClientOptions::telemetry).
3382    pub enable_session_telemetry: Option<bool>,
3383    /// **Experimental.** Enables native model citations for supported providers.
3384    pub enable_citations: Option<bool>,
3385    /// Opts in to capturing file changes for session rewind and cumulative
3386    /// session diff when the resumed session has a valid baseline. Earlier
3387    /// untracked changes cannot be reconstructed.
3388    pub enable_file_change_tracking: Option<bool>,
3389    /// **Experimental.** Limits applied to this session's current accounting window.
3390    pub session_limits: Option<SessionLimitsConfig>,
3391    /// Per-property model capability overrides on resume.
3392    pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3393    /// Per-session configuration for the runtime memory feature on resume.
3394    pub memory: Option<MemoryConfiguration>,
3395    /// Override the default configuration directory location on resume.
3396    pub config_directory: Option<PathBuf>,
3397    /// Per-session working directory on resume.
3398    pub working_directory: Option<PathBuf>,
3399    /// Additional directories the agent may access on resume. Relative paths
3400    /// resolve against the session working directory.
3401    pub additional_directories: Option<Vec<PathBuf>>,
3402    /// Per-session GitHub token on resume. See
3403    /// [`SessionConfig::github_token`].
3404    pub github_token: Option<String>,
3405    /// Per-session remote behavior control on resume. See
3406    /// [`SessionConfig::remote_session`].
3407    pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3408    /// Forward sub-agent streaming events to this connection on resume.
3409    pub include_sub_agent_streaming_events: Option<bool>,
3410    /// Slash commands registered for this session on resume. See
3411    /// [`SessionConfig::commands`] — commands are not persisted server-side,
3412    /// so the resume payload re-supplies the registration.
3413    pub commands: Option<Vec<CommandDefinition>>,
3414    /// ExP assignment ("flight") data injected on resume. See
3415    /// [`SessionConfig::exp_assignments`]. Re-supply on resume so the runtime
3416    /// re-applies the assignments after a CLI process restart. Set via
3417    /// [`with_exp_assignments`](Self::with_exp_assignments).
3418    #[doc(hidden)]
3419    pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3420    /// Opt-in flag injected on resume. See
3421    /// [`SessionConfig::enable_managed_settings`]. Re-supply on resume so
3422    /// the runtime re-applies the managed-settings self-fetch after a CLI
3423    /// process restart. Set via
3424    /// [`with_enable_managed_settings`](Self::with_enable_managed_settings).
3425    pub enable_managed_settings: Option<bool>,
3426    /// Optional managed-settings layer injected on resume. See
3427    /// [`SessionConfig::managed_settings`]. This layer is not persisted, so it
3428    /// must be re-supplied on resume to remain in effect; omitting it clears the
3429    /// previously injected layer. Serialized on the wire as `managedSettings`.
3430    /// Set via [`with_managed_settings`](Self::with_managed_settings).
3431    pub managed_settings: Option<ManagedSettings>,
3432    /// Custom session filesystem provider. Required on resume when the
3433    /// [`Client`](crate::Client) was started with
3434    /// [`ClientOptions::session_fs`](crate::ClientOptions::session_fs).
3435    /// See [`SessionConfig::session_fs_provider`].
3436    pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3437    /// Force-fail resume if the session does not exist on disk, instead of
3438    /// silently starting a new session. Wire field name stays `disableResume`.
3439    pub suppress_resume_event: Option<bool>,
3440    /// When `true`, instructs the runtime to continue any tool calls or
3441    /// permission requests that were pending when the previous connection
3442    /// was dropped. Use this together with [`Client::force_stop`] to hand
3443    /// off a session from one process to another without losing in-flight
3444    /// work.
3445    ///
3446    /// [`Client::force_stop`]: crate::Client::force_stop
3447    pub continue_pending_work: Option<bool>,
3448    /// Optional permission-request handler. See
3449    /// [`SessionConfig::permission_handler`].
3450    pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3451    /// Optional elicitation handler. See
3452    /// [`SessionConfig::elicitation_handler`].
3453    pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3454    /// Optional MCP OAuth handler. See [`SessionConfig::mcp_auth_handler`].
3455    pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3456    /// Optional user-input handler. See
3457    /// [`SessionConfig::user_input_handler`].
3458    pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3459    /// Optional exit-plan-mode handler. See
3460    /// [`SessionConfig::exit_plan_mode_handler`].
3461    pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3462    /// Optional auto-mode-switch handler. See
3463    /// [`SessionConfig::auto_mode_switch_handler`].
3464    pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3465    /// Session hook handler. See [`SessionConfig::hooks_handler`].
3466    pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3467    /// Permission policy. See `SessionConfig::permission_policy`.
3468    pub(crate) permission_policy: Option<crate::permission::Policy>,
3469    /// System-message transform. See [`SessionConfig::system_message_transform`].
3470    pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3471    /// See [`SessionConfig::skip_custom_instructions`].
3472    pub skip_custom_instructions: Option<bool>,
3473    /// See [`SessionConfig::custom_agents_local_only`].
3474    pub custom_agents_local_only: Option<bool>,
3475    /// Controls whether the session enables experimental features.
3476    ///
3477    /// Defaults to `false` in [`crate::ClientMode::Empty`] when unset;
3478    /// in `copilot-cli` mode, leaving this unset lets the runtime decide.
3479    pub enable_experimental_mode: Option<bool>,
3480    /// See [`SessionConfig::coauthor_enabled`].
3481    pub coauthor_enabled: Option<bool>,
3482    /// See [`SessionConfig::manage_schedule_enabled`].
3483    pub manage_schedule_enabled: Option<bool>,
3484}
3485
3486impl std::fmt::Debug for ResumeSessionConfig {
3487    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3488        f.debug_struct("ResumeSessionConfig")
3489            .field("session_id", &self.session_id)
3490            .field("model", &self.model)
3491            .field("client_name", &self.client_name)
3492            .field("reasoning_effort", &self.reasoning_effort)
3493            .field("reasoning_summary", &self.reasoning_summary)
3494            .field("context_tier", &self.context_tier)
3495            .field("streaming", &self.streaming)
3496            .field("system_message", &self.system_message)
3497            .field("tools", &self.tools)
3498            .field("canvases", &self.canvases)
3499            .field(
3500                "canvas_handler",
3501                &self.canvas_handler.as_ref().map(|_| "<set>"),
3502            )
3503            .field("open_canvases", &self.open_canvases)
3504            .field("request_canvas_renderer", &self.request_canvas_renderer)
3505            .field("request_extensions", &self.request_extensions)
3506            .field("extension_sdk_path", &self.extension_sdk_path)
3507            .field("extension_info", &self.extension_info)
3508            .field("canvas_provider", &self.canvas_provider)
3509            .field("available_tools", &self.available_tools)
3510            .field("excluded_tools", &self.excluded_tools)
3511            .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3512            .field("mcp_servers", &self.mcp_servers)
3513            .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3514            .field("embedding_cache_storage", &self.embedding_cache_storage)
3515            .field("enable_config_discovery", &self.enable_config_discovery)
3516            .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3517            .field(
3518                "organization_custom_instructions",
3519                &self
3520                    .organization_custom_instructions
3521                    .as_ref()
3522                    .map(|_| "<redacted>"),
3523            )
3524            .field(
3525                "enable_on_demand_instruction_discovery",
3526                &self.enable_on_demand_instruction_discovery,
3527            )
3528            .field("enable_file_hooks", &self.enable_file_hooks)
3529            .field(
3530                "enable_host_git_operations",
3531                &self.enable_host_git_operations,
3532            )
3533            .field("enable_session_store", &self.enable_session_store)
3534            .field("enable_skills", &self.enable_skills)
3535            .field("enable_mcp_apps", &self.enable_mcp_apps)
3536            .field("skill_directories", &self.skill_directories)
3537            .field("instruction_directories", &self.instruction_directories)
3538            .field("plugin_directories", &self.plugin_directories)
3539            .field("large_output", &self.large_output)
3540            .field("tool_search", &self.tool_search)
3541            .field("disabled_skills", &self.disabled_skills)
3542            .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3543            .field("hooks", &self.hooks)
3544            .field("custom_agents", &self.custom_agents)
3545            .field("default_agent", &self.default_agent)
3546            .field("agent", &self.agent)
3547            .field("infinite_sessions", &self.infinite_sessions)
3548            .field("provider", &self.provider)
3549            .field("capi", &self.capi)
3550            .field("enable_session_telemetry", &self.enable_session_telemetry)
3551            .field("enable_citations", &self.enable_citations)
3552            .field(
3553                "enable_file_change_tracking",
3554                &self.enable_file_change_tracking,
3555            )
3556            .field("session_limits", &self.session_limits)
3557            .field("model_capabilities", &self.model_capabilities)
3558            .field("memory", &self.memory)
3559            .field("config_directory", &self.config_directory)
3560            .field("working_directory", &self.working_directory)
3561            .field("additional_directories", &self.additional_directories)
3562            .field(
3563                "github_token",
3564                &self.github_token.as_ref().map(|_| "<redacted>"),
3565            )
3566            .field("remote_session", &self.remote_session)
3567            .field(
3568                "include_sub_agent_streaming_events",
3569                &self.include_sub_agent_streaming_events,
3570            )
3571            .field("commands", &self.commands)
3572            .field("exp_assignments", &self.exp_assignments)
3573            .field("enable_managed_settings", &self.enable_managed_settings)
3574            .field("enable_experimental_mode", &self.enable_experimental_mode)
3575            .field("managed_settings", &self.managed_settings)
3576            .field(
3577                "session_fs_provider",
3578                &self.session_fs_provider.as_ref().map(|_| "<set>"),
3579            )
3580            .field(
3581                "permission_handler",
3582                &self.permission_handler.as_ref().map(|_| "<set>"),
3583            )
3584            .field(
3585                "elicitation_handler",
3586                &self.elicitation_handler.as_ref().map(|_| "<set>"),
3587            )
3588            .field(
3589                "user_input_handler",
3590                &self.user_input_handler.as_ref().map(|_| "<set>"),
3591            )
3592            .field(
3593                "exit_plan_mode_handler",
3594                &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3595            )
3596            .field(
3597                "auto_mode_switch_handler",
3598                &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3599            )
3600            .field(
3601                "hooks_handler",
3602                &self.hooks_handler.as_ref().map(|_| "<set>"),
3603            )
3604            .field(
3605                "system_message_transform",
3606                &self.system_message_transform.as_ref().map(|_| "<set>"),
3607            )
3608            .field("suppress_resume_event", &self.suppress_resume_event)
3609            .field("continue_pending_work", &self.continue_pending_work)
3610            .finish()
3611    }
3612}
3613
3614impl ResumeSessionConfig {
3615    /// Consume this config to produce the [`SessionResumeWire`] payload
3616    /// for `session.resume` and a [`SessionConfigRuntime`] bundle holding
3617    /// the runtime-only fields (handlers, transforms, providers).
3618    ///
3619    /// See [`SessionConfig::into_wire`] for the design rationale.
3620    ///
3621    /// [`SessionResumeWire`]: crate::wire::SessionResumeWire
3622    pub(crate) fn into_wire(
3623        mut self,
3624    ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3625        let permission_active =
3626            self.permission_handler.is_some() || self.permission_policy.is_some();
3627        let request_user_input = self.user_input_handler.is_some();
3628        let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3629        let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3630        let request_elicitation = self.elicitation_handler.is_some();
3631        let hooks_flag = self.hooks_handler.is_some();
3632
3633        let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3634        if let Some(tools) = self.tools.as_mut() {
3635            for tool in tools.iter_mut() {
3636                if let Some(handler) = tool.handler.take()
3637                    && tool_handlers.insert(tool.name.clone(), handler).is_some()
3638                {
3639                    return Err(crate::Error::with_message(
3640                        crate::ErrorKind::InvalidConfig,
3641                        format!("duplicate tool handler registered for name {:?}", tool.name),
3642                    ));
3643                }
3644            }
3645        }
3646
3647        let wire_commands = self.commands.as_ref().map(|cmds| {
3648            cmds.iter()
3649                .map(|c| crate::wire::CommandWireDefinition {
3650                    name: c.name.clone(),
3651                    description: c.description.clone().unwrap_or_default(),
3652                })
3653                .collect()
3654        });
3655        let wire_canvases = self.canvases.clone();
3656        let canvas_handler = self.canvas_handler.clone();
3657        let bearer_token_providers =
3658            prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3659
3660        let wire = crate::wire::SessionResumeWire {
3661            session_id: self.session_id,
3662            model: self.model,
3663            client_name: self.client_name,
3664            reasoning_effort: self.reasoning_effort,
3665            reasoning_summary: self.reasoning_summary,
3666            context_tier: self.context_tier,
3667            streaming: self.streaming,
3668            system_message: self.system_message,
3669            tools: self.tools,
3670            canvases: wire_canvases,
3671            open_canvases: self.open_canvases,
3672            request_canvas_renderer: self.request_canvas_renderer,
3673            request_extensions: self.request_extensions,
3674            extension_sdk_path: self.extension_sdk_path,
3675            extension_info: self.extension_info,
3676            canvas_provider: self.canvas_provider,
3677            available_tools: self.available_tools,
3678            excluded_tools: self.excluded_tools,
3679            excluded_builtin_agents: self.excluded_builtin_agents,
3680            tool_filter_precedence: "excluded",
3681            mcp_servers: self.mcp_servers,
3682            mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3683            embedding_cache_storage: self.embedding_cache_storage,
3684            env_value_mode: "direct",
3685            enable_config_discovery: self.enable_config_discovery,
3686            skip_embedding_retrieval: self.skip_embedding_retrieval,
3687            organization_custom_instructions: self.organization_custom_instructions,
3688            enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3689            enable_file_hooks: self.enable_file_hooks,
3690            enable_host_git_operations: self.enable_host_git_operations,
3691            enable_session_store: self.enable_session_store,
3692            enable_skills: self.enable_skills,
3693            request_user_input,
3694            request_permission: permission_active,
3695            request_exit_plan_mode,
3696            request_auto_mode_switch,
3697            request_elicitation,
3698            request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3699            github_mcp_tool_config: self.github_mcp_tool_config,
3700            hooks: hooks_flag,
3701            skill_directories: self.skill_directories,
3702            instruction_directories: self.instruction_directories,
3703            plugin_directories: self.plugin_directories,
3704            large_output: self.large_output,
3705            tool_search: self.tool_search,
3706            disabled_skills: self.disabled_skills,
3707            disabled_mcp_servers: self.disabled_mcp_servers,
3708            custom_agents: self.custom_agents,
3709            custom_agents_local_only: self.custom_agents_local_only,
3710            default_agent: self.default_agent,
3711            agent: self.agent,
3712            infinite_sessions: self.infinite_sessions,
3713            provider: self.provider,
3714            capi: self.capi,
3715            providers: self.providers,
3716            models: self.models,
3717            enable_session_telemetry: self.enable_session_telemetry,
3718            enable_citations: self.enable_citations,
3719            enable_file_change_tracking: self.enable_file_change_tracking,
3720            session_limits: self.session_limits,
3721            model_capabilities: self.model_capabilities,
3722            memory: self.memory,
3723            config_dir: self.config_directory,
3724            working_directory: self.working_directory,
3725            additional_directories: self.additional_directories,
3726            github_token: self.github_token,
3727            remote_session: self.remote_session,
3728            include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3729            enable_github_telemetry_forwarding: None,
3730            commands: wire_commands,
3731            exp_assignments: self.exp_assignments,
3732            enable_managed_settings: self.enable_managed_settings,
3733            is_experimental_mode: self.enable_experimental_mode,
3734            managed_settings: self.managed_settings,
3735            suppress_resume_event: self.suppress_resume_event,
3736            continue_pending_work: self.continue_pending_work,
3737        };
3738
3739        let runtime = SessionConfigRuntime {
3740            permission_handler: self.permission_handler,
3741            permission_policy: self.permission_policy,
3742            elicitation_handler: self.elicitation_handler,
3743            mcp_auth_handler: self.mcp_auth_handler,
3744            user_input_handler: self.user_input_handler,
3745            exit_plan_mode_handler: self.exit_plan_mode_handler,
3746            auto_mode_switch_handler: self.auto_mode_switch_handler,
3747            hooks_handler: self.hooks_handler,
3748            system_message_transform: self.system_message_transform,
3749            tool_handlers,
3750            canvas_handler,
3751            session_fs_provider: self.session_fs_provider,
3752            bearer_token_providers,
3753            commands: self.commands,
3754        };
3755
3756        Ok((wire, runtime))
3757    }
3758
3759    /// Construct a `ResumeSessionConfig` with the given session ID and all
3760    /// other fields left unset. Combine with `.with_*` builders or struct
3761    /// update syntax (`..ResumeSessionConfig::new(id)`) to populate the
3762    /// fields you need.
3763    pub fn new(session_id: SessionId) -> Self {
3764        Self {
3765            session_id,
3766            model: None,
3767            client_name: None,
3768            reasoning_effort: None,
3769            reasoning_summary: None,
3770            context_tier: None,
3771            streaming: None,
3772            system_message: None,
3773            tools: None,
3774            canvases: None,
3775            canvas_handler: None,
3776            open_canvases: None,
3777            request_canvas_renderer: None,
3778            request_extensions: None,
3779            extension_sdk_path: None,
3780            extension_info: None,
3781            canvas_provider: None,
3782            available_tools: None,
3783            excluded_tools: None,
3784            excluded_builtin_agents: None,
3785            mcp_servers: None,
3786            mcp_oauth_token_storage: None,
3787            enable_config_discovery: None,
3788            skip_embedding_retrieval: None,
3789            organization_custom_instructions: None,
3790            enable_on_demand_instruction_discovery: None,
3791            enable_file_hooks: None,
3792            enable_host_git_operations: None,
3793            enable_session_store: None,
3794            enable_skills: None,
3795            embedding_cache_storage: None,
3796            enable_mcp_apps: None,
3797            github_mcp_tool_config: None,
3798            skill_directories: None,
3799            instruction_directories: None,
3800            plugin_directories: None,
3801            large_output: None,
3802            tool_search: None,
3803            disabled_skills: None,
3804            disabled_mcp_servers: None,
3805            hooks: None,
3806            custom_agents: None,
3807            default_agent: None,
3808            agent: None,
3809            infinite_sessions: None,
3810            provider: None,
3811            capi: None,
3812            providers: None,
3813            models: None,
3814            enable_session_telemetry: None,
3815            enable_citations: None,
3816            enable_file_change_tracking: None,
3817            session_limits: None,
3818            model_capabilities: None,
3819            memory: None,
3820            config_directory: None,
3821            working_directory: None,
3822            additional_directories: None,
3823            github_token: None,
3824            remote_session: None,
3825            include_sub_agent_streaming_events: None,
3826            commands: None,
3827            exp_assignments: None,
3828            enable_managed_settings: None,
3829            managed_settings: None,
3830            session_fs_provider: None,
3831            suppress_resume_event: None,
3832            continue_pending_work: None,
3833            permission_handler: None,
3834            elicitation_handler: None,
3835            mcp_auth_handler: None,
3836            user_input_handler: None,
3837            exit_plan_mode_handler: None,
3838            auto_mode_switch_handler: None,
3839            hooks_handler: None,
3840            permission_policy: None,
3841            system_message_transform: None,
3842            skip_custom_instructions: None,
3843            custom_agents_local_only: None,
3844            enable_experimental_mode: None,
3845            coauthor_enabled: None,
3846            manage_schedule_enabled: None,
3847        }
3848    }
3849
3850    /// Install a [`PermissionHandler`] for the resumed session.
3851    pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3852        self.permission_handler = Some(handler);
3853        self
3854    }
3855
3856    /// Install an [`ElicitationHandler`] for the resumed session.
3857    pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3858        self.elicitation_handler = Some(handler);
3859        self
3860    }
3861
3862    /// Install an [`McpAuthHandler`] for host-provided MCP OAuth tokens.
3863    pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3864        self.mcp_auth_handler = Some(handler);
3865        self
3866    }
3867
3868    /// Install a [`UserInputHandler`] for the resumed session.
3869    pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3870        self.user_input_handler = Some(handler);
3871        self
3872    }
3873
3874    /// Install an [`ExitPlanModeHandler`] for the resumed session.
3875    pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3876        self.exit_plan_mode_handler = Some(handler);
3877        self
3878    }
3879
3880    /// Install an [`AutoModeSwitchHandler`] for the resumed session.
3881    pub fn with_auto_mode_switch_handler(
3882        mut self,
3883        handler: Arc<dyn AutoModeSwitchHandler>,
3884    ) -> Self {
3885        self.auto_mode_switch_handler = Some(handler);
3886        self
3887    }
3888
3889    /// Install a [`SessionHooks`] handler. Automatically enables the
3890    /// wire-level `hooks` flag on session resumption.
3891    pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3892        self.hooks_handler = Some(hooks);
3893        self
3894    }
3895
3896    /// Install a [`SystemMessageTransform`].
3897    pub fn with_system_message_transform(
3898        mut self,
3899        transform: Arc<dyn SystemMessageTransform>,
3900    ) -> Self {
3901        self.system_message_transform = Some(transform);
3902        self
3903    }
3904
3905    /// Register slash commands for the resumed session. See
3906    /// [`SessionConfig::with_commands`] — commands are not persisted
3907    /// server-side, so the resume payload re-supplies the registration.
3908    pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3909        self.commands = Some(commands);
3910        self
3911    }
3912
3913    /// Install a [`SessionFsProvider`] backing the resumed session's
3914    /// filesystem. See [`SessionConfig::with_session_fs_provider`].
3915    pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3916        self.session_fs_provider = Some(provider);
3917        self
3918    }
3919
3920    /// Auto-approve every permission request on the resumed session. See
3921    /// [`SessionConfig::approve_all_permissions`].
3922    pub fn approve_all_permissions(mut self) -> Self {
3923        self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3924        self
3925    }
3926
3927    /// Auto-deny every permission request on the resumed session. See
3928    /// [`SessionConfig::deny_all_permissions`].
3929    pub fn deny_all_permissions(mut self) -> Self {
3930        self.permission_policy = Some(crate::permission::Policy::DenyAll);
3931        self
3932    }
3933
3934    /// Apply a closure-based permission policy on the resumed session.
3935    /// See [`SessionConfig::approve_permissions_if`].
3936    pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3937    where
3938        F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3939    {
3940        self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3941        self
3942    }
3943
3944    /// Set the model identifier to switch to on resume (e.g. `"claude-sonnet-4"`).
3945    pub fn with_model(mut self, model: impl Into<String>) -> Self {
3946        self.model = Some(model.into());
3947        self
3948    }
3949
3950    /// Set the application name sent as `User-Agent` context.
3951    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3952        self.client_name = Some(name.into());
3953        self
3954    }
3955
3956    /// Set the reasoning effort to apply on resume.
3957    pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3958        self.reasoning_effort = Some(effort.into());
3959        self
3960    }
3961
3962    /// Set [`reasoning_summary`](Self::reasoning_summary).
3963    pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3964        self.reasoning_summary = Some(summary);
3965        self
3966    }
3967
3968    /// Set the context window tier to apply on resume (e.g. `"default"`,
3969    /// `"long_context"`).
3970    pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3971        self.context_tier = Some(tier.into());
3972        self
3973    }
3974
3975    /// Enable streaming token deltas via `assistant.message_delta` events.
3976    pub fn with_streaming(mut self, streaming: bool) -> Self {
3977        self.streaming = Some(streaming);
3978        self
3979    }
3980
3981    /// Re-supply the system message so the agent retains workspace context
3982    /// across CLI process restarts.
3983    pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3984        self.system_message = Some(system_message);
3985        self
3986    }
3987
3988    /// Re-supply client-defined tools on resume.
3989    pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3990        self.tools = Some(tools.into_iter().collect());
3991        self
3992    }
3993
3994    /// Re-supply canvas declarations on resume.
3995    pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3996        self.canvases = Some(canvases.into_iter().collect());
3997        self
3998    }
3999
4000    /// Install the provider-side [`CanvasHandler`] for the resumed session.
4001    pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4002        self.canvas_handler = Some(handler);
4003        self
4004    }
4005
4006    /// Seed open canvas instances that were visible before resuming.
4007    pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4008        mut self,
4009        open_canvases: I,
4010    ) -> Self {
4011        self.open_canvases = Some(open_canvases.into_iter().collect());
4012        self
4013    }
4014
4015    /// Request host canvas renderer tools for this connection on resume.
4016    pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4017        self.request_canvas_renderer = Some(request);
4018        self
4019    }
4020
4021    /// Request extension tools and dispatch for this connection on resume.
4022    pub fn with_request_extensions(mut self, request: bool) -> Self {
4023        self.request_extensions = Some(request);
4024        self
4025    }
4026
4027    /// Override the bundled `@github/copilot-sdk` drop injected into extension
4028    /// subprocesses for this resumed session. Invalid paths fall back to the
4029    /// bundled SDK silently.
4030    pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4031        self.extension_sdk_path = Some(path.into());
4032        self
4033    }
4034
4035    /// Set stable extension identity metadata for this connection on resume.
4036    pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4037        self.extension_info = Some(extension_info);
4038        self
4039    }
4040
4041    /// Set the canvas provider identity for this connection on resume so
4042    /// host-supplied canvases rehydrate against a stable extension id.
4043    pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4044        self.canvas_provider = Some(canvas_provider);
4045        self
4046    }
4047
4048    /// Set the allowlist of tool names the agent may use.
4049    pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4050    where
4051        I: IntoIterator<Item = S>,
4052        S: Into<String>,
4053    {
4054        self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4055        self
4056    }
4057
4058    /// Set the blocklist of built-in tool names the agent must not use.
4059    pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4060    where
4061        I: IntoIterator<Item = S>,
4062        S: Into<String>,
4063    {
4064        self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4065        self
4066    }
4067
4068    /// Set the built-in agent names to exclude from the resumed session.
4069    pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4070    where
4071        I: IntoIterator<Item = S>,
4072        S: Into<String>,
4073    {
4074        self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4075        self
4076    }
4077
4078    /// Re-supply MCP server configurations on resume.
4079    pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4080        self.mcp_servers = Some(servers);
4081        self
4082    }
4083
4084    /// Set MCP OAuth token storage mode on resume.
4085    /// See [`SessionConfig::with_mcp_oauth_token_storage`] for details.
4086    pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4087        self.mcp_oauth_token_storage = Some(mode.into());
4088        self
4089    }
4090
4091    /// Set embedding cache storage mode on resume.
4092    pub fn with_embedding_cache_storage(
4093        mut self,
4094        embedding_cache_storage: impl Into<String>,
4095    ) -> Self {
4096        self.embedding_cache_storage = Some(embedding_cache_storage.into());
4097        self
4098    }
4099
4100    /// Enables runtime discovery of supported configuration. Explicitly supplied
4101    /// configuration takes precedence over discovered values.
4102    pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4103        self.enable_config_discovery = Some(enable);
4104        self
4105    }
4106
4107    /// Set [`Self::skip_embedding_retrieval`].
4108    pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4109        self.skip_embedding_retrieval = Some(value);
4110        self
4111    }
4112
4113    /// Set [`Self::organization_custom_instructions`].
4114    pub fn with_organization_custom_instructions(
4115        mut self,
4116        instructions: impl Into<String>,
4117    ) -> Self {
4118        self.organization_custom_instructions = Some(instructions.into());
4119        self
4120    }
4121
4122    /// Set [`Self::enable_on_demand_instruction_discovery`].
4123    pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4124        self.enable_on_demand_instruction_discovery = Some(value);
4125        self
4126    }
4127
4128    /// Set [`Self::enable_file_hooks`].
4129    pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4130        self.enable_file_hooks = Some(value);
4131        self
4132    }
4133
4134    /// Set [`Self::enable_host_git_operations`].
4135    pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4136        self.enable_host_git_operations = Some(value);
4137        self
4138    }
4139
4140    /// Set [`Self::enable_session_store`].
4141    pub fn with_enable_session_store(mut self, value: bool) -> Self {
4142        self.enable_session_store = Some(value);
4143        self
4144    }
4145
4146    /// Set [`Self::enable_skills`].
4147    pub fn with_enable_skills(mut self, value: bool) -> Self {
4148        self.enable_skills = Some(value);
4149        self
4150    }
4151
4152    /// **Experimental.** This method is part of an experimental wire-protocol
4153    /// surface (SEP-1865) and may change or be removed in a future release.
4154    ///
4155    /// Enable MCP Apps (SEP-1865) UI passthrough on resume. Defaults to
4156    /// `None` (treated as `false`). See [`SessionConfig::enable_mcp_apps`].
4157    pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4158        self.enable_mcp_apps = Some(enable);
4159        self
4160    }
4161
4162    /// Set the built-in GitHub MCP server configuration.
4163    pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4164        self.github_mcp_tool_config = Some(config);
4165        self
4166    }
4167
4168    /// Set skill directory paths passed through to the CLI on resume.
4169    pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4170    where
4171        I: IntoIterator<Item = P>,
4172        P: Into<PathBuf>,
4173    {
4174        self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4175        self
4176    }
4177
4178    /// Set additional directories to search for custom instruction files
4179    /// on resume. Forwarded to the CLI; not the same as
4180    /// [`with_skill_directories`](Self::with_skill_directories).
4181    pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4182    where
4183        I: IntoIterator<Item = P>,
4184        P: Into<PathBuf>,
4185    {
4186        self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4187        self
4188    }
4189
4190    /// Set Open Plugin directory paths passed through to the CLI on resume.
4191    pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4192    where
4193        I: IntoIterator<Item = P>,
4194        P: Into<PathBuf>,
4195    {
4196        self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4197        self
4198    }
4199
4200    /// Set the [`LargeToolOutputConfig`] forwarded to the CLI on resume.
4201    pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4202        self.large_output = Some(config);
4203        self
4204    }
4205
4206    /// Set the [`ToolSearchConfig`] overriding the runtime's built-in
4207    /// tool-search behavior on resume.
4208    pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4209        self.tool_search = Some(config);
4210        self
4211    }
4212
4213    /// Set the names of skills to disable on resume.
4214    pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4215    where
4216        I: IntoIterator<Item = S>,
4217        S: Into<String>,
4218    {
4219        self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4220        self
4221    }
4222
4223    /// Set exact MCP server names to disable for this session.
4224    pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4225    where
4226        I: IntoIterator<Item = S>,
4227        S: Into<String>,
4228    {
4229        self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4230        self
4231    }
4232
4233    /// Re-supply custom agents on resume.
4234    pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4235        mut self,
4236        agents: I,
4237    ) -> Self {
4238        self.custom_agents = Some(agents.into_iter().collect());
4239        self
4240    }
4241
4242    /// Configure the built-in default agent on resume.
4243    pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4244        self.default_agent = Some(agent);
4245        self
4246    }
4247
4248    /// Activate a named custom agent on resume.
4249    pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4250        self.agent = Some(name.into());
4251        self
4252    }
4253
4254    /// Re-supply infinite session configuration on resume.
4255    pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4256        self.infinite_sessions = Some(config);
4257        self
4258    }
4259
4260    /// Re-supply BYOK provider configuration on resume.
4261    pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4262        self.provider = Some(provider);
4263        self
4264    }
4265
4266    /// Re-supply provider-scoped CAPI session options on resume.
4267    pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4268        self.capi = Some(capi);
4269        self
4270    }
4271
4272    /// **Experimental.** This method is part of an experimental multi-provider
4273    /// BYOK surface and may change or be removed in a future release.
4274    ///
4275    /// Re-supply the named BYOK provider connections on resume. Attach
4276    /// models referencing these with [`Self::with_models`].
4277    pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4278        self.providers = Some(providers);
4279        self
4280    }
4281
4282    /// **Experimental.** This method is part of an experimental multi-provider
4283    /// BYOK surface and may change or be removed in a future release.
4284    ///
4285    /// Re-supply the BYOK model definitions on resume, each referencing a
4286    /// named provider supplied via [`Self::with_providers`].
4287    pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4288        self.models = Some(models);
4289        self
4290    }
4291
4292    /// Enable or disable internal session telemetry on resume.
4293    ///
4294    /// See [`Self::enable_session_telemetry`] for default and BYOK behavior.
4295    pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4296        self.enable_session_telemetry = Some(enable);
4297        self
4298    }
4299
4300    /// **Experimental.** Enable native model citations for supported providers on resume.
4301    pub fn with_enable_citations(mut self, enable: bool) -> Self {
4302        self.enable_citations = Some(enable);
4303        self
4304    }
4305
4306    /// Opt in to capturing file changes for session rewind and cumulative
4307    /// session diff when the resumed session has a valid baseline.
4308    pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4309        self.enable_file_change_tracking = Some(enable);
4310        self
4311    }
4312
4313    /// **Experimental.** Set limits for this session's current accounting window.
4314    pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4315        self.session_limits = Some(limits);
4316        self
4317    }
4318
4319    /// Set per-property model capability overrides on resume.
4320    pub fn with_model_capabilities(
4321        mut self,
4322        capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4323    ) -> Self {
4324        self.model_capabilities = Some(capabilities);
4325        self
4326    }
4327
4328    /// Configure the runtime memory feature for the resumed session.
4329    pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4330        self.memory = Some(memory);
4331        self
4332    }
4333
4334    /// Override the default configuration directory location on resume.
4335    pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4336        self.config_directory = Some(dir.into());
4337        self
4338    }
4339
4340    /// Set the per-session working directory on resume.
4341    pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4342        self.working_directory = Some(dir.into());
4343        self
4344    }
4345
4346    /// Set directories the agent may access beyond the working directory on resume.
4347    pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4348    where
4349        I: IntoIterator<Item = P>,
4350        P: Into<PathBuf>,
4351    {
4352        self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4353        self
4354    }
4355
4356    /// Set the per-session GitHub token on resume. See
4357    /// [`SessionConfig::github_token`] for distinction from the
4358    /// client-level token.
4359    pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4360        self.github_token = Some(token.into());
4361        self
4362    }
4363
4364    /// Forward sub-agent streaming events to this connection on resume.
4365    pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4366        self.include_sub_agent_streaming_events = Some(include);
4367        self
4368    }
4369
4370    /// Set per-session remote behavior on resume.
4371    pub fn with_remote_session(
4372        mut self,
4373        mode: crate::generated::api_types::RemoteSessionMode,
4374    ) -> Self {
4375        self.remote_session = Some(mode);
4376        self
4377    }
4378
4379    /// Force-fail resume if the session does not exist on disk, instead
4380    /// of silently starting a new session.
4381    pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4382        self.suppress_resume_event = Some(suppress);
4383        self
4384    }
4385
4386    /// When `true`, instructs the runtime to continue any tool calls or
4387    /// permission requests that were pending when the previous connection
4388    /// was dropped. Use this together with
4389    /// [`Client::force_stop`](crate::Client::force_stop) to hand off a
4390    /// session from one process to another without losing in-flight work.
4391    pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4392        self.continue_pending_work = Some(continue_pending);
4393        self
4394    }
4395
4396    /// Set [`Self::skip_custom_instructions`].
4397    pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4398        self.skip_custom_instructions = Some(value);
4399        self
4400    }
4401
4402    /// Set [`Self::custom_agents_local_only`].
4403    pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4404        self.custom_agents_local_only = Some(value);
4405        self
4406    }
4407
4408    /// Set [`enable_experimental_mode`](Self::enable_experimental_mode).
4409    pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4410        self.enable_experimental_mode = Some(enable_experimental_mode);
4411        self
4412    }
4413
4414    /// Set [`Self::coauthor_enabled`].
4415    pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4416        self.coauthor_enabled = Some(value);
4417        self
4418    }
4419
4420    /// Set [`Self::manage_schedule_enabled`].
4421    pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4422        self.manage_schedule_enabled = Some(value);
4423        self
4424    }
4425
4426    /// Inject ExP assignment ("flight") data on resume. See
4427    /// [`SessionConfig::with_exp_assignments`]. Re-supply the assignments on
4428    /// resume so the runtime re-applies them after a CLI process restart.
4429    #[doc(hidden)]
4430    pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4431        self.exp_assignments = Some(assignments);
4432        self
4433    }
4434
4435    /// Opt the runtime into self-fetching enterprise managed settings on resume.
4436    /// See [`SessionConfig::with_enable_managed_settings`].
4437    pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4438        self.enable_managed_settings = Some(enabled);
4439        self
4440    }
4441
4442    /// Inject a managed-settings layer (currently permission rules) on resume.
4443    /// See [`SessionConfig::with_managed_settings`]. Must be re-supplied on
4444    /// resume; omitting it clears the previously injected layer.
4445    pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4446        self.managed_settings = Some(managed_settings);
4447        self
4448    }
4449}
4450
4451/// Controls how the system message is constructed.
4452///
4453/// Use `mode: "append"` (default) to add content after the built-in system
4454/// message, `"replace"` to substitute it entirely, or `"customize"` for
4455/// section-level overrides.
4456#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4457#[serde(rename_all = "camelCase")]
4458#[non_exhaustive]
4459pub struct SystemMessageConfig {
4460    /// How content is applied: `"append"` (default), `"replace"`, or `"customize"`.
4461    #[serde(skip_serializing_if = "Option::is_none")]
4462    pub mode: Option<String>,
4463    /// Content string to append or replace.
4464    #[serde(skip_serializing_if = "Option::is_none")]
4465    pub content: Option<String>,
4466    /// Section-level overrides (used with `mode: "customize"`).
4467    #[serde(skip_serializing_if = "Option::is_none")]
4468    pub sections: Option<HashMap<String, SectionOverride>>,
4469}
4470
4471impl SystemMessageConfig {
4472    /// Construct an empty [`SystemMessageConfig`]; all fields default to
4473    /// unset.
4474    pub fn new() -> Self {
4475        Self::default()
4476    }
4477
4478    /// Set the application mode: `"append"` (default), `"replace"`, or
4479    /// `"customize"`.
4480    pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4481        self.mode = Some(mode.into());
4482        self
4483    }
4484
4485    /// Set the system message content (used by `"append"` and `"replace"`
4486    /// modes).
4487    pub fn with_content(mut self, content: impl Into<String>) -> Self {
4488        self.content = Some(content.into());
4489        self
4490    }
4491
4492    /// Set the section-level overrides (used with `mode: "customize"`).
4493    pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4494        self.sections = Some(sections);
4495        self
4496    }
4497}
4498
4499/// An override operation for a single system message section.
4500///
4501/// Used within [`SystemMessageConfig::sections`] when `mode` is `"customize"`.
4502/// The `action` field determines the operation: `"replace"`, `"remove"`,
4503/// `"append"`, `"prepend"`, `"preserve"`, or `"transform"`.
4504#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4505#[serde(rename_all = "camelCase")]
4506pub struct SectionOverride {
4507    /// Override action: `"replace"`, `"remove"`, `"append"`, `"prepend"`,
4508    /// `"preserve"`, or `"transform"`.
4509    #[serde(skip_serializing_if = "Option::is_none")]
4510    pub action: Option<String>,
4511    /// Content for the override operation.
4512    #[serde(skip_serializing_if = "Option::is_none")]
4513    pub content: Option<String>,
4514}
4515
4516/// Response from `session.create`.
4517#[derive(Debug, Clone, Serialize, Deserialize)]
4518#[serde(rename_all = "camelCase")]
4519pub struct CreateSessionResult {
4520    /// The CLI-assigned session ID.
4521    pub session_id: SessionId,
4522    /// Workspace directory for the session (infinite sessions).
4523    #[serde(skip_serializing_if = "Option::is_none")]
4524    pub workspace_path: Option<PathBuf>,
4525    /// Remote session URL, if the session is running remotely.
4526    #[serde(default, alias = "remote_url")]
4527    pub remote_url: Option<String>,
4528    /// Capabilities negotiated with the CLI for this session.
4529    #[serde(skip_serializing_if = "Option::is_none")]
4530    pub capabilities: Option<SessionCapabilities>,
4531}
4532
4533/// Response from `session.resume`.
4534#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4535#[serde(rename_all = "camelCase")]
4536pub(crate) struct ResumeSessionResult {
4537    /// The CLI-assigned session ID. Older runtimes may omit this on resume.
4538    #[serde(default)]
4539    pub session_id: Option<SessionId>,
4540    /// Workspace directory for the session (infinite sessions).
4541    #[serde(default, skip_serializing_if = "Option::is_none")]
4542    pub workspace_path: Option<PathBuf>,
4543    /// Remote session URL, if the session is running remotely.
4544    #[serde(default, alias = "remote_url")]
4545    pub remote_url: Option<String>,
4546    /// Capabilities negotiated with the CLI for this session.
4547    #[serde(default, skip_serializing_if = "Option::is_none")]
4548    pub capabilities: Option<SessionCapabilities>,
4549    /// Canvas instances already open when the session was resumed.
4550    #[serde(
4551        default,
4552        alias = "openCanvasInstances",
4553        skip_serializing_if = "Option::is_none"
4554    )]
4555    pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4556}
4557
4558/// Severity level for [`Session::log`](crate::session::Session::log) messages.
4559#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4560#[serde(rename_all = "lowercase")]
4561pub enum LogLevel {
4562    /// Informational message (default).
4563    #[default]
4564    Info,
4565    /// Warning message.
4566    Warning,
4567    /// Error message.
4568    Error,
4569}
4570
4571/// Options for [`Session::log`](crate::session::Session::log).
4572///
4573/// Pass `None` to `log` for defaults (info level, persisted to the session
4574/// event log on disk).
4575#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4576#[serde(rename_all = "camelCase")]
4577pub struct LogOptions {
4578    /// Log severity. `None` lets the server pick (defaults to `info`).
4579    #[serde(skip_serializing_if = "Option::is_none")]
4580    pub level: Option<LogLevel>,
4581    /// When `Some(true)`, the message is transient and not persisted to the
4582    /// session event log on disk. `None` lets the server pick.
4583    #[serde(skip_serializing_if = "Option::is_none")]
4584    pub ephemeral: Option<bool>,
4585}
4586
4587impl LogOptions {
4588    /// Set [`level`](Self::level).
4589    pub fn with_level(mut self, level: LogLevel) -> Self {
4590        self.level = Some(level);
4591        self
4592    }
4593
4594    /// Set [`ephemeral`](Self::ephemeral).
4595    pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4596        self.ephemeral = Some(ephemeral);
4597        self
4598    }
4599}
4600
4601/// Options for [`Session::set_model`](crate::session::Session::set_model).
4602///
4603/// Pass `None` to `set_model` to switch model without any overrides.
4604#[derive(Debug, Clone, Default)]
4605pub struct SetModelOptions {
4606    /// Reasoning effort for the new model (e.g. `"low"`, `"medium"`,
4607    /// `"high"`, `"xhigh"`, `"max"`).
4608    pub reasoning_effort: Option<String>,
4609    /// Reasoning summary mode for the new model. Use
4610    /// [`ReasoningSummary::None`] to suppress summary output regardless of
4611    /// whether reasoning is enabled.
4612    pub reasoning_summary: Option<ReasoningSummary>,
4613    /// Explicit context window tier for the new model. Leave unset to use
4614    /// normal model behavior with no explicit tier.
4615    pub context_tier: Option<ContextTier>,
4616    /// Override individual model capabilities resolved by the runtime. Only
4617    /// fields set on the override are applied; the rest fall back to the
4618    /// runtime-resolved values for the model.
4619    pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4620}
4621
4622impl SetModelOptions {
4623    /// Set [`reasoning_effort`](Self::reasoning_effort).
4624    pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4625        self.reasoning_effort = Some(effort.into());
4626        self
4627    }
4628
4629    /// Set [`reasoning_summary`](Self::reasoning_summary).
4630    pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4631        self.reasoning_summary = Some(summary);
4632        self
4633    }
4634
4635    /// Set [`context_tier`](Self::context_tier).
4636    pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4637        self.context_tier = Some(tier);
4638        self
4639    }
4640
4641    /// Set [`model_capabilities`](Self::model_capabilities).
4642    pub fn with_model_capabilities(
4643        mut self,
4644        caps: crate::generated::api_types::ModelCapabilitiesOverride,
4645    ) -> Self {
4646        self.model_capabilities = Some(caps);
4647        self
4648    }
4649}
4650
4651/// Response from the top-level `ping` RPC.
4652///
4653/// The `protocol_version` field is the most commonly-inspected piece —
4654/// see [`Client::verify_protocol_version`].
4655///
4656/// [`Client::verify_protocol_version`]: crate::Client::verify_protocol_version
4657#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4658#[serde(rename_all = "camelCase")]
4659pub struct PingResponse {
4660    /// The message echoed back by the CLI.
4661    #[serde(default)]
4662    pub message: String,
4663    /// ISO 8601 timestamp when the ping was processed.
4664    #[serde(default)]
4665    pub timestamp: String,
4666    /// The protocol version negotiated by the CLI, if reported.
4667    #[serde(skip_serializing_if = "Option::is_none")]
4668    pub protocol_version: Option<u32>,
4669}
4670
4671/// Line range for file attachments.
4672#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4673#[serde(rename_all = "camelCase")]
4674pub struct AttachmentLineRange {
4675    /// First line (1-based).
4676    pub start: u32,
4677    /// Last line (inclusive).
4678    pub end: u32,
4679}
4680
4681/// Cursor position within a file selection.
4682#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4683#[serde(rename_all = "camelCase")]
4684pub struct AttachmentSelectionPosition {
4685    /// Line number (0-based).
4686    pub line: u32,
4687    /// Character offset (0-based).
4688    pub character: u32,
4689}
4690
4691/// Range of selected text within a file.
4692#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4693#[serde(rename_all = "camelCase")]
4694pub struct AttachmentSelectionRange {
4695    /// Start position.
4696    pub start: AttachmentSelectionPosition,
4697    /// End position.
4698    pub end: AttachmentSelectionPosition,
4699}
4700
4701/// Type of GitHub reference attachment.
4702#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4703#[serde(rename_all = "snake_case")]
4704#[non_exhaustive]
4705pub enum GitHubReferenceType {
4706    /// GitHub issue.
4707    Issue,
4708    /// GitHub pull request.
4709    Pr,
4710    /// GitHub discussion.
4711    Discussion,
4712}
4713
4714/// Pointer to a GitHub repository (owner/name plus optional numeric id).
4715///
4716/// Used by the GitHub-anchored [`Attachment`] variants. Mirrors the field
4717/// shape of the generated `GitHubRepoRef`, but defined locally so it can
4718/// derive `Eq` for use inside the `Attachment` enum.
4719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4720#[serde(rename_all = "camelCase")]
4721pub struct GitHubRepoPointer {
4722    /// Numeric GitHub repository id.
4723    #[serde(skip_serializing_if = "Option::is_none")]
4724    pub id: Option<i64>,
4725    /// Repository name (without owner).
4726    pub name: String,
4727    /// Repository owner login (user or organization).
4728    pub owner: String,
4729}
4730
4731/// One side (head or base) of a GitHub single-file diff.
4732#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4733#[serde(rename_all = "camelCase")]
4734pub struct GitHubFileDiffSide {
4735    /// Repository-relative path to the file.
4736    pub path: String,
4737    /// Git ref (branch, tag, or commit SHA) the file is read at.
4738    pub r#ref: String,
4739    /// Repository the file lives in.
4740    pub repo: GitHubRepoPointer,
4741}
4742
4743/// One side (head or base) of a GitHub tree comparison.
4744#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4745#[serde(rename_all = "camelCase")]
4746pub struct GitHubTreeComparisonSide {
4747    /// Repository the revision belongs to.
4748    pub repo: GitHubRepoPointer,
4749    /// Git revision (branch, tag, or commit SHA).
4750    pub revision: String,
4751}
4752
4753/// Line range covered by a GitHub snippet attachment (1-based, inclusive end).
4754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4755#[serde(rename_all = "camelCase")]
4756pub struct GitHubSnippetLineRange {
4757    /// Start line number (1-based).
4758    pub start: i64,
4759    /// End line number (1-based, inclusive).
4760    pub end: i64,
4761}
4762
4763/// An attachment included with a user message.
4764#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4765#[serde(
4766    tag = "type",
4767    rename_all = "camelCase",
4768    rename_all_fields = "camelCase"
4769)]
4770#[non_exhaustive]
4771pub enum Attachment {
4772    /// A file path, optionally with a line range.
4773    File {
4774        /// Absolute path to the file.
4775        path: PathBuf,
4776        /// Label shown in the UI.
4777        #[serde(skip_serializing_if = "Option::is_none")]
4778        display_name: Option<String>,
4779        /// Optional line range to focus on.
4780        #[serde(skip_serializing_if = "Option::is_none")]
4781        line_range: Option<AttachmentLineRange>,
4782    },
4783    /// A directory path.
4784    Directory {
4785        /// Absolute path to the directory.
4786        path: PathBuf,
4787        /// Label shown in the UI.
4788        #[serde(skip_serializing_if = "Option::is_none")]
4789        display_name: Option<String>,
4790    },
4791    /// A text selection within a file.
4792    Selection {
4793        /// Path to the file containing the selection.
4794        file_path: PathBuf,
4795        /// The selected text content.
4796        text: String,
4797        /// Label shown in the UI.
4798        #[serde(skip_serializing_if = "Option::is_none")]
4799        display_name: Option<String>,
4800        /// Character range of the selection.
4801        selection: AttachmentSelectionRange,
4802    },
4803    /// Raw binary data (e.g. an image).
4804    Blob {
4805        /// Base64-encoded data.
4806        data: String,
4807        /// MIME type of the data.
4808        mime_type: String,
4809        /// Label shown in the UI.
4810        #[serde(skip_serializing_if = "Option::is_none")]
4811        display_name: Option<String>,
4812    },
4813    /// A reference to a GitHub issue, PR, or discussion.
4814    #[serde(rename = "github_reference")]
4815    GitHubReference {
4816        /// Issue/PR/discussion number.
4817        number: u64,
4818        /// Title of the referenced item.
4819        title: String,
4820        /// Kind of reference.
4821        reference_type: GitHubReferenceType,
4822        /// Current state (e.g. "open", "closed").
4823        state: String,
4824        /// URL to the referenced item.
4825        url: String,
4826    },
4827    /// A pointer to a GitHub commit.
4828    #[serde(rename = "github_commit")]
4829    GitHubCommit {
4830        /// First line of the commit message.
4831        message: String,
4832        /// Full commit SHA.
4833        oid: String,
4834        /// Repository the commit belongs to.
4835        repo: GitHubRepoPointer,
4836        /// URL to the commit on GitHub.
4837        url: String,
4838    },
4839    /// A pointer to a GitHub release.
4840    #[serde(rename = "github_release")]
4841    GitHubRelease {
4842        /// Human-readable release name.
4843        name: String,
4844        /// Repository the release belongs to.
4845        repo: GitHubRepoPointer,
4846        /// Git tag the release is anchored to.
4847        tag_name: String,
4848        /// URL to the release on GitHub.
4849        url: String,
4850    },
4851    /// A pointer to a GitHub Actions job.
4852    #[serde(rename = "github_actions_job")]
4853    GitHubActionsJob {
4854        /// Terminal conclusion of the job when finished (e.g. "success",
4855        /// "failure", "cancelled"). Absent for in-progress jobs.
4856        #[serde(skip_serializing_if = "Option::is_none")]
4857        conclusion: Option<String>,
4858        /// Job id within the workflow run.
4859        job_id: i64,
4860        /// Display name of the job.
4861        job_name: String,
4862        /// Repository the workflow run belongs to.
4863        repo: GitHubRepoPointer,
4864        /// URL to the job on GitHub.
4865        url: String,
4866        /// Display name of the workflow the job ran in.
4867        workflow_name: String,
4868    },
4869    /// A pointer to a GitHub repository.
4870    #[serde(rename = "github_repository")]
4871    GitHubRepository {
4872        /// Short description of the repository.
4873        #[serde(skip_serializing_if = "Option::is_none")]
4874        description: Option<String>,
4875        /// Git ref this attachment is anchored at (branch, tag, or commit).
4876        /// When absent the default branch is implied.
4877        #[serde(skip_serializing_if = "Option::is_none")]
4878        r#ref: Option<String>,
4879        /// Repository pointer.
4880        repo: GitHubRepoPointer,
4881        /// URL to the repository on GitHub.
4882        url: String,
4883    },
4884    /// A pointer to a single-file diff. At least one of `head` and `base` is present.
4885    #[serde(rename = "github_file_diff")]
4886    GitHubFileDiff {
4887        /// File location on the base side of the diff. Absent for additions.
4888        #[serde(skip_serializing_if = "Option::is_none")]
4889        base: Option<GitHubFileDiffSide>,
4890        /// File location on the head side of the diff. Absent for deletions.
4891        #[serde(skip_serializing_if = "Option::is_none")]
4892        head: Option<GitHubFileDiffSide>,
4893        /// URL to the diff on GitHub (e.g. a commit, compare, or PR-file URL).
4894        url: String,
4895    },
4896    /// A pointer to a comparison between two git revisions.
4897    #[serde(rename = "github_tree_comparison")]
4898    GitHubTreeComparison {
4899        /// Base side of the comparison.
4900        base: GitHubTreeComparisonSide,
4901        /// Head side of the comparison.
4902        head: GitHubTreeComparisonSide,
4903        /// URL to the comparison on GitHub.
4904        url: String,
4905    },
4906    /// A generic GitHub URL reference.
4907    #[serde(rename = "github_url")]
4908    GitHubUrl {
4909        /// URL to the GitHub resource.
4910        url: String,
4911    },
4912    /// A pointer to a file in a GitHub repository at a specific ref.
4913    #[serde(rename = "github_file")]
4914    GitHubFile {
4915        /// Repository-relative path to the file.
4916        path: String,
4917        /// Git ref the file is read at (branch, tag, or commit SHA).
4918        r#ref: String,
4919        /// Repository the file lives in.
4920        repo: GitHubRepoPointer,
4921        /// URL to the file on GitHub.
4922        url: String,
4923    },
4924    /// A pointer to a line range inside a file in a GitHub repository.
4925    #[serde(rename = "github_snippet")]
4926    GitHubSnippet {
4927        /// Line range the snippet covers.
4928        line_range: GitHubSnippetLineRange,
4929        /// Repository-relative path to the file.
4930        path: String,
4931        /// Git ref the file is read at (branch, tag, or commit SHA).
4932        r#ref: String,
4933        /// Repository the file lives in.
4934        repo: GitHubRepoPointer,
4935        /// URL to the snippet on GitHub (with line anchor).
4936        url: String,
4937    },
4938}
4939
4940impl Attachment {
4941    /// Returns the display name, if set.
4942    pub fn display_name(&self) -> Option<&str> {
4943        match self {
4944            Self::File { display_name, .. }
4945            | Self::Directory { display_name, .. }
4946            | Self::Selection { display_name, .. }
4947            | Self::Blob { display_name, .. } => display_name.as_deref(),
4948            Self::GitHubReference { .. }
4949            | Self::GitHubCommit { .. }
4950            | Self::GitHubRelease { .. }
4951            | Self::GitHubActionsJob { .. }
4952            | Self::GitHubRepository { .. }
4953            | Self::GitHubFileDiff { .. }
4954            | Self::GitHubTreeComparison { .. }
4955            | Self::GitHubUrl { .. }
4956            | Self::GitHubFile { .. }
4957            | Self::GitHubSnippet { .. } => None,
4958        }
4959    }
4960
4961    /// Returns a human-readable label, deriving one from the path if needed.
4962    pub fn label(&self) -> Option<String> {
4963        if let Some(display_name) = self
4964            .display_name()
4965            .map(str::trim)
4966            .filter(|name| !name.is_empty())
4967        {
4968            return Some(display_name.to_string());
4969        }
4970
4971        match self {
4972            Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4973                format!("#{}", number)
4974            } else {
4975                title.trim().to_string()
4976            }),
4977            _ => self.derived_display_name(),
4978        }
4979    }
4980
4981    /// Ensure `display_name` is populated when the variant supports one.
4982    pub fn ensure_display_name(&mut self) {
4983        if self
4984            .display_name()
4985            .map(str::trim)
4986            .is_some_and(|name| !name.is_empty())
4987        {
4988            return;
4989        }
4990
4991        let Some(derived_display_name) = self.derived_display_name() else {
4992            return;
4993        };
4994
4995        match self {
4996            Self::File { display_name, .. }
4997            | Self::Directory { display_name, .. }
4998            | Self::Selection { display_name, .. }
4999            | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5000            Self::GitHubReference { .. }
5001            | Self::GitHubCommit { .. }
5002            | Self::GitHubRelease { .. }
5003            | Self::GitHubActionsJob { .. }
5004            | Self::GitHubRepository { .. }
5005            | Self::GitHubFileDiff { .. }
5006            | Self::GitHubTreeComparison { .. }
5007            | Self::GitHubUrl { .. }
5008            | Self::GitHubFile { .. }
5009            | Self::GitHubSnippet { .. } => {}
5010        }
5011    }
5012
5013    fn derived_display_name(&self) -> Option<String> {
5014        match self {
5015            Self::File { path, .. } | Self::Directory { path, .. } => {
5016                Some(attachment_name_from_path(path))
5017            }
5018            Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5019            Self::Blob { .. } => Some("attachment".to_string()),
5020            Self::GitHubReference { .. }
5021            | Self::GitHubCommit { .. }
5022            | Self::GitHubRelease { .. }
5023            | Self::GitHubActionsJob { .. }
5024            | Self::GitHubRepository { .. }
5025            | Self::GitHubFileDiff { .. }
5026            | Self::GitHubTreeComparison { .. }
5027            | Self::GitHubUrl { .. }
5028            | Self::GitHubFile { .. }
5029            | Self::GitHubSnippet { .. } => None,
5030        }
5031    }
5032}
5033
5034fn attachment_name_from_path(path: &Path) -> String {
5035    path.file_name()
5036        .map(|name| name.to_string_lossy().into_owned())
5037        .filter(|name| !name.is_empty())
5038        .unwrap_or_else(|| {
5039            let full = path.to_string_lossy();
5040            if full.is_empty() {
5041                "attachment".to_string()
5042            } else {
5043                full.into_owned()
5044            }
5045        })
5046}
5047
5048/// Normalize a list of attachments so every entry has a `display_name`.
5049pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5050    for attachment in attachments {
5051        attachment.ensure_display_name();
5052    }
5053}
5054
5055/// Message delivery mode for [`MessageOptions::mode`].
5056///
5057/// Controls how a prompt is delivered relative to in-flight session work.
5058/// Wire values: `"enqueue"` and `"immediate"`.
5059#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5060#[serde(rename_all = "lowercase")]
5061#[non_exhaustive]
5062pub enum DeliveryMode {
5063    /// Queue the prompt behind any in-flight work (default).
5064    Enqueue,
5065    /// Interrupt the session and run the prompt immediately.
5066    Immediate,
5067}
5068
5069/// The UI mode the agent is in for a given turn, used by
5070/// [`MessageOptions::agent_mode`].
5071///
5072/// Wire values: `"interactive"`, `"plan"`, `"autopilot"`, `"shell"`.
5073#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5074#[serde(rename_all = "lowercase")]
5075#[non_exhaustive]
5076pub enum AgentMode {
5077    /// The agent is responding interactively to the user.
5078    Interactive,
5079    /// The agent is preparing a plan before making changes.
5080    Plan,
5081    /// The agent is working autonomously toward task completion.
5082    Autopilot,
5083    /// The agent is in shell-focused UI mode.
5084    Shell,
5085}
5086
5087/// Options for sending a user message to the agent.
5088///
5089/// Used by both [`Session::send`](crate::session::Session::send) and
5090/// [`Session::send_and_wait`](crate::session::Session::send_and_wait); the
5091/// `wait_timeout` field is honored only by `send_and_wait` and is ignored by
5092/// `send`.
5093///
5094/// `MessageOptions` is `#[non_exhaustive]` and constructed via [`MessageOptions::new`]
5095/// plus the `with_*` chain so future fields can land without breaking callers.
5096/// For the trivial case, both `&str` and `String` implement `Into<MessageOptions>`,
5097/// so:
5098///
5099/// ```no_run
5100/// # use github_copilot_sdk::session::Session;
5101/// # async fn run(session: Session) -> Result<(), github_copilot_sdk::Error> {
5102/// session.send("hello").await?;
5103/// # Ok(()) }
5104/// ```
5105///
5106/// is equivalent to:
5107///
5108/// ```no_run
5109/// # use github_copilot_sdk::session::Session;
5110/// # use github_copilot_sdk::types::MessageOptions;
5111/// # async fn run(session: Session) -> Result<(), github_copilot_sdk::Error> {
5112/// session.send(MessageOptions::new("hello")).await?;
5113/// # Ok(()) }
5114/// ```
5115#[derive(Debug, Clone)]
5116#[non_exhaustive]
5117pub struct MessageOptions {
5118    /// The user prompt to send.
5119    pub prompt: String,
5120    /// Optional message delivery mode for this turn.
5121    ///
5122    /// Controls whether the prompt is queued behind in-flight work
5123    /// ([`DeliveryMode::Enqueue`], default) or interrupts the session and
5124    /// runs immediately ([`DeliveryMode::Immediate`]).
5125    pub mode: Option<DeliveryMode>,
5126    /// Optional UI mode the agent was in when this message was sent
5127    /// (for example [`AgentMode::Plan`] or [`AgentMode::Autopilot`]).
5128    /// Defaults to the session's current mode when `None`.
5129    pub agent_mode: Option<AgentMode>,
5130    /// Optional attachments to include with the message.
5131    pub attachments: Option<Vec<Attachment>>,
5132    /// Maximum time to wait for the session to go idle. Honored only by
5133    /// `send_and_wait`. Defaults to 60 seconds when unset.
5134    pub wait_timeout: Option<Duration>,
5135    /// Custom HTTP headers to include in outbound model requests for this
5136    /// turn. When `None` or empty, no `requestHeaders` field is sent on
5137    /// the wire.
5138    pub request_headers: Option<HashMap<String, String>>,
5139    /// W3C Trace Context `traceparent` header for this turn.
5140    ///
5141    /// Per-turn override that takes precedence over
5142    /// [`ClientOptions::on_get_trace_context`](crate::ClientOptions::on_get_trace_context).
5143    /// When `None`, the SDK falls back to the provider (if configured)
5144    /// before omitting the field.
5145    pub traceparent: Option<String>,
5146    /// W3C Trace Context `tracestate` header for this turn.
5147    ///
5148    /// Per-turn override paired with [`traceparent`](Self::traceparent).
5149    pub tracestate: Option<String>,
5150    /// If provided, this is shown in the timeline instead of `prompt`.
5151    pub display_prompt: Option<String>,
5152}
5153
5154impl MessageOptions {
5155    /// Build a new `MessageOptions` with just a prompt.
5156    pub fn new(prompt: impl Into<String>) -> Self {
5157        Self {
5158            prompt: prompt.into(),
5159            mode: None,
5160            agent_mode: None,
5161            attachments: None,
5162            wait_timeout: None,
5163            request_headers: None,
5164            traceparent: None,
5165            tracestate: None,
5166            display_prompt: None,
5167        }
5168    }
5169
5170    /// Set the message delivery mode for this turn.
5171    ///
5172    /// Pass [`DeliveryMode::Immediate`] to interrupt the session and run
5173    /// the prompt now; the default ([`DeliveryMode::Enqueue`]) queues the
5174    /// prompt behind in-flight work.
5175    pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5176        self.mode = Some(mode);
5177        self
5178    }
5179
5180    /// Set the per-message agent UI mode for this turn.
5181    ///
5182    /// When `None`, the session's current mode is used.
5183    pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5184        self.agent_mode = Some(agent_mode);
5185        self
5186    }
5187
5188    /// Attach files / selections / blobs to the message.
5189    pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5190        self.attachments = Some(attachments);
5191        self
5192    }
5193
5194    /// Override the default 60-second wait timeout for `send_and_wait`.
5195    pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5196        self.wait_timeout = Some(timeout);
5197        self
5198    }
5199
5200    /// Set custom HTTP headers for outbound model requests for this turn.
5201    pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5202        self.request_headers = Some(headers);
5203        self
5204    }
5205
5206    /// Set both `traceparent` and `tracestate` from a [`TraceContext`].
5207    /// Either field may remain `None` if the [`TraceContext`] has no value
5208    /// for it. Use [`with_traceparent`](Self::with_traceparent) or
5209    /// [`with_tracestate`](Self::with_tracestate) to set them individually.
5210    pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5211        self.traceparent = ctx.traceparent;
5212        self.tracestate = ctx.tracestate;
5213        self
5214    }
5215
5216    /// Set the W3C `traceparent` header for this turn.
5217    pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5218        self.traceparent = Some(traceparent.into());
5219        self
5220    }
5221
5222    /// Set the W3C `tracestate` header for this turn.
5223    pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5224        self.tracestate = Some(tracestate.into());
5225        self
5226    }
5227
5228    /// Set the display prompt shown in the timeline instead of `prompt`.
5229    pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5230        self.display_prompt = Some(display_prompt.into());
5231        self
5232    }
5233}
5234
5235impl From<&str> for MessageOptions {
5236    fn from(prompt: &str) -> Self {
5237        Self::new(prompt)
5238    }
5239}
5240
5241impl From<String> for MessageOptions {
5242    fn from(prompt: String) -> Self {
5243        Self::new(prompt)
5244    }
5245}
5246
5247impl From<&String> for MessageOptions {
5248    fn from(prompt: &String) -> Self {
5249        Self::new(prompt.clone())
5250    }
5251}
5252
5253/// Response from [`Client::get_status`](crate::Client::get_status).
5254#[derive(Debug, Clone, Serialize, Deserialize)]
5255#[serde(rename_all = "camelCase")]
5256#[non_exhaustive]
5257pub struct GetStatusResponse {
5258    /// Package version (e.g. `"1.0.0"`).
5259    pub version: String,
5260    /// Protocol version for SDK compatibility.
5261    pub protocol_version: u32,
5262}
5263
5264/// Response from [`Client::get_auth_status`](crate::Client::get_auth_status).
5265#[derive(Debug, Clone, Serialize, Deserialize)]
5266#[serde(rename_all = "camelCase")]
5267#[non_exhaustive]
5268pub struct GetAuthStatusResponse {
5269    /// Whether the user is authenticated.
5270    pub is_authenticated: bool,
5271    /// Authentication type (e.g. `"user"`, `"env"`, `"gh-cli"`, `"hmac"`,
5272    /// `"api-key"`, `"token"`).
5273    #[serde(skip_serializing_if = "Option::is_none")]
5274    pub auth_type: Option<String>,
5275    /// GitHub host URL.
5276    #[serde(skip_serializing_if = "Option::is_none")]
5277    pub host: Option<String>,
5278    /// User login name.
5279    #[serde(skip_serializing_if = "Option::is_none")]
5280    pub login: Option<String>,
5281    /// Human-readable status message.
5282    #[serde(skip_serializing_if = "Option::is_none")]
5283    pub status_message: Option<String>,
5284}
5285
5286/// Wrapper for session event notifications received from the CLI.
5287///
5288/// The CLI sends these as JSON-RPC notifications on the `session.event` method.
5289#[derive(Debug, Clone, Serialize, Deserialize)]
5290#[serde(rename_all = "camelCase")]
5291pub struct SessionEventNotification {
5292    /// The session this event belongs to.
5293    pub session_id: SessionId,
5294    /// The event payload.
5295    pub event: SessionEvent,
5296}
5297
5298/// A single event in a session's timeline.
5299///
5300/// Events form a linked chain via `parent_id`. The `event_type` string
5301/// identifies the kind (e.g. `"assistant.message_delta"`, `"session.idle"`,
5302/// `"tool.execution_start"`). Event-specific payload is in `data` as
5303/// untyped JSON.
5304#[derive(Debug, Clone, Serialize, Deserialize)]
5305#[serde(rename_all = "camelCase")]
5306pub struct SessionEvent {
5307    /// Unique event ID (UUID v4).
5308    pub id: String,
5309    /// ISO 8601 timestamp.
5310    pub timestamp: String,
5311    /// ID of the preceding event in the chain.
5312    pub parent_id: Option<String>,
5313    /// Transient events that are not persisted to disk.
5314    #[serde(skip_serializing_if = "Option::is_none")]
5315    pub ephemeral: Option<bool>,
5316    /// Sub-agent instance identifier. Absent for events emitted by the
5317    /// root/main agent and for session-level events.
5318    #[serde(skip_serializing_if = "Option::is_none")]
5319    pub agent_id: Option<String>,
5320    /// Debug timestamp: when the CLI received this event (ms since epoch).
5321    #[serde(skip_serializing_if = "Option::is_none")]
5322    pub debug_cli_received_at_ms: Option<i64>,
5323    /// Debug timestamp: when the event was forwarded over WebSocket.
5324    #[serde(skip_serializing_if = "Option::is_none")]
5325    pub debug_ws_forwarded_at_ms: Option<i64>,
5326    /// Event type string (e.g. `"assistant.message"`, `"session.idle"`).
5327    #[serde(rename = "type")]
5328    pub event_type: String,
5329    /// Event-specific data. Structure depends on `event_type`.
5330    pub data: Value,
5331}
5332
5333impl SessionEvent {
5334    /// Parse the string `event_type` into a typed [`SessionEventType`](crate::session_events::SessionEventType) enum.
5335    ///
5336    /// Returns `SessionEventType::Unknown` for unrecognized event types,
5337    /// ensuring forward compatibility with newer CLI versions.
5338    pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5339        use serde::de::IntoDeserializer;
5340        let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5341            self.event_type.as_str().into_deserializer();
5342        crate::generated::SessionEventType::deserialize(deserializer)
5343            .unwrap_or(crate::generated::SessionEventType::Unknown)
5344    }
5345
5346    /// Deserialize the event `data` field into a typed struct.
5347    ///
5348    /// Returns `None` if deserialization fails (e.g. unknown event type
5349    /// or schema mismatch). Prefer typed data accessors for specific
5350    /// event types where you need strongly-typed field access.
5351    pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5352        serde_json::from_value(self.data.clone()).ok()
5353    }
5354
5355    /// `model_call` errors are transient — the CLI agent loop continues
5356    /// after them and may succeed on the next turn. These should not be
5357    /// treated as session-ending errors.
5358    pub fn is_transient_error(&self) -> bool {
5359        self.event_type == "session.error"
5360            && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5361    }
5362}
5363
5364/// A request from the CLI to invoke a client-defined tool.
5365///
5366/// Received as a JSON-RPC request on the `tool.call` method. The client
5367/// must respond with a [`ToolResultResponse`].
5368#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5369#[serde(rename_all = "camelCase")]
5370#[non_exhaustive]
5371pub struct ToolInvocation {
5372    /// Session that owns this tool call.
5373    pub session_id: SessionId,
5374    /// Unique ID for this tool call, used to correlate the response.
5375    pub tool_call_id: String,
5376    /// Name of the tool being invoked.
5377    pub tool_name: String,
5378    /// Tool arguments as JSON.
5379    pub arguments: Value,
5380    /// Snapshot of the session's currently initialized tools.
5381    ///
5382    /// The SDK populates this only when the invocation targets the built-in
5383    /// tool-search tool (`tool_search_tool`), so a tool-search override can
5384    /// rank/filter the live catalog — including MCP tools configured in
5385    /// settings — without issuing its own RPC. `None` for every other tool
5386    /// invocation. This field is not part of the wire protocol.
5387    #[serde(skip)]
5388    pub available_tools: Option<Vec<CurrentToolMetadata>>,
5389    /// W3C Trace Context `traceparent` header propagated from the CLI's
5390    /// `execute_tool` span. Pass through to OpenTelemetry-aware code so
5391    /// child spans created inside the handler are parented to the CLI
5392    /// span. `None` when the CLI has no trace context for this call.
5393    #[serde(default, skip_serializing_if = "Option::is_none")]
5394    pub traceparent: Option<String>,
5395    /// W3C Trace Context `tracestate` paired with
5396    /// [`traceparent`](Self::traceparent).
5397    #[serde(default, skip_serializing_if = "Option::is_none")]
5398    pub tracestate: Option<String>,
5399}
5400
5401impl ToolInvocation {
5402    /// Deserialize this invocation's [`arguments`](Self::arguments) into a
5403    /// strongly-typed parameter struct.
5404    ///
5405    /// Idiomatic way to extract typed parameters when implementing
5406    /// [`ToolHandler`](crate::tool::ToolHandler) directly. Equivalent to
5407    /// `serde_json::from_value(invocation.arguments.clone())` with the SDK's
5408    /// error type.
5409    ///
5410    /// # Example
5411    ///
5412    /// ```rust,no_run
5413    /// # use github_copilot_sdk::{Error, types::ToolInvocation, ToolResult};
5414    /// # use serde::Deserialize;
5415    /// # #[derive(Deserialize)] struct MyParams { city: String }
5416    /// # async fn example(inv: ToolInvocation) -> Result<ToolResult, Error> {
5417    /// let params: MyParams = inv.params()?;
5418    /// // …use `inv.session_id` / `inv.tool_call_id` alongside `params`…
5419    /// # let _ = params; Ok(ToolResult::Text(String::new()))
5420    /// # }
5421    /// ```
5422    pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5423        serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5424    }
5425
5426    /// Returns the propagated [`TraceContext`] for this invocation, or
5427    /// [`TraceContext::default()`] when the CLI sent no headers.
5428    pub fn trace_context(&self) -> TraceContext {
5429        TraceContext {
5430            traceparent: self.traceparent.clone(),
5431            tracestate: self.tracestate.clone(),
5432        }
5433    }
5434}
5435
5436/// Binary content returned by a tool.
5437#[derive(Debug, Clone, Serialize, Deserialize)]
5438#[serde(rename_all = "camelCase")]
5439pub struct ToolBinaryResult {
5440    /// Base64-encoded binary data.
5441    pub data: String,
5442    /// MIME type for the binary data.
5443    pub mime_type: String,
5444    /// Type identifier for the binary result.
5445    pub r#type: String,
5446    /// Optional description shown alongside the binary result.
5447    #[serde(default, skip_serializing_if = "Option::is_none")]
5448    pub description: Option<String>,
5449}
5450
5451/// Expanded tool result with metadata for the LLM and session log.
5452///
5453/// This type is `#[non_exhaustive]`: it mirrors a growing wire shape, so
5454/// construct it via [`ToolResultExpanded::new`] plus the `with_*` chain
5455/// rather than a struct literal, allowing new fields to land without
5456/// breaking callers.
5457#[derive(Debug, Clone, Serialize, Deserialize)]
5458#[serde(rename_all = "camelCase")]
5459#[non_exhaustive]
5460pub struct ToolResultExpanded {
5461    /// Result text sent back to the LLM.
5462    pub text_result_for_llm: String,
5463    /// `"success"` or `"failure"`.
5464    pub result_type: String,
5465    /// Binary payloads sent back to the LLM.
5466    #[serde(default, skip_serializing_if = "Option::is_none")]
5467    pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5468    /// Optional log message for the session timeline.
5469    #[serde(skip_serializing_if = "Option::is_none")]
5470    pub session_log: Option<String>,
5471    /// Error message, if the tool failed.
5472    #[serde(skip_serializing_if = "Option::is_none")]
5473    pub error: Option<String>,
5474    /// Tool-specific telemetry emitted with the result.
5475    #[serde(default, skip_serializing_if = "Option::is_none")]
5476    pub tool_telemetry: Option<HashMap<String, Value>>,
5477    /// Names of tools returned by a tool-search tool.
5478    #[serde(default, skip_serializing_if = "Option::is_none")]
5479    pub tool_references: Option<Vec<String>>,
5480}
5481
5482impl ToolResultExpanded {
5483    /// Construct an expanded result with the required `text_result_for_llm`
5484    /// and `result_type` (`"success"` or `"failure"`). All optional metadata
5485    /// fields start unset; populate them with the `with_*` builders.
5486    pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5487        Self {
5488            text_result_for_llm: text_result_for_llm.into(),
5489            result_type: result_type.into(),
5490            binary_results_for_llm: None,
5491            session_log: None,
5492            error: None,
5493            tool_telemetry: None,
5494            tool_references: None,
5495        }
5496    }
5497
5498    /// Set the binary payloads returned to the LLM.
5499    pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5500        self.binary_results_for_llm = Some(results);
5501        self
5502    }
5503
5504    /// Set the log message for the session timeline.
5505    pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5506        self.session_log = Some(session_log.into());
5507        self
5508    }
5509
5510    /// Set the error message, marking the tool as failed.
5511    pub fn with_error(mut self, error: impl Into<String>) -> Self {
5512        self.error = Some(error.into());
5513        self
5514    }
5515
5516    /// Set the tool-specific telemetry emitted with the result.
5517    pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5518        self.tool_telemetry = Some(telemetry);
5519        self
5520    }
5521
5522    /// Set the names of tools returned by a tool-search tool.
5523    pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5524    where
5525        I: IntoIterator<Item = S>,
5526        S: Into<String>,
5527    {
5528        self.tool_references = Some(references.into_iter().map(Into::into).collect());
5529        self
5530    }
5531}
5532
5533/// Result of a tool invocation — either a plain text string or an expanded result.
5534#[derive(Debug, Clone, Serialize, Deserialize)]
5535#[serde(untagged)]
5536#[non_exhaustive]
5537pub enum ToolResult {
5538    /// Simple text result passed directly to the LLM.
5539    Text(String),
5540    /// Structured result with metadata.
5541    Expanded(ToolResultExpanded),
5542}
5543
5544/// JSON-RPC response wrapper for a tool result, sent back to the CLI.
5545#[derive(Debug, Clone, Serialize, Deserialize)]
5546#[serde(rename_all = "camelCase")]
5547pub struct ToolResultResponse {
5548    /// The tool result payload.
5549    pub result: ToolResult,
5550}
5551
5552/// Metadata for a persisted session, returned by `session.list`.
5553#[derive(Debug, Clone, Serialize, Deserialize)]
5554#[serde(rename_all = "camelCase")]
5555pub struct SessionMetadata {
5556    /// The session's unique identifier.
5557    pub session_id: SessionId,
5558    /// ISO 8601 timestamp when the session was created.
5559    pub start_time: String,
5560    /// ISO 8601 timestamp of the last modification.
5561    pub modified_time: String,
5562    /// Agent-generated session summary.
5563    #[serde(skip_serializing_if = "Option::is_none")]
5564    pub summary: Option<String>,
5565    /// Whether the session is running remotely.
5566    pub is_remote: bool,
5567}
5568
5569/// Response from `session.list`.
5570#[derive(Debug, Clone, Serialize, Deserialize)]
5571#[serde(rename_all = "camelCase")]
5572pub struct ListSessionsResponse {
5573    /// The list of session metadata entries.
5574    pub sessions: Vec<SessionMetadata>,
5575}
5576
5577/// Filter options for [`Client::list_sessions`](crate::Client::list_sessions).
5578///
5579/// All fields are optional; unset fields don't constrain the result.
5580#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5581#[serde(rename_all = "camelCase")]
5582pub struct SessionListFilter {
5583    /// Filter by exact `cwd` match.
5584    #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5585    pub working_directory: Option<String>,
5586    /// Filter by git root path.
5587    #[serde(default, skip_serializing_if = "Option::is_none")]
5588    pub git_root: Option<String>,
5589    /// Filter by repository in `owner/repo` form.
5590    #[serde(default, skip_serializing_if = "Option::is_none")]
5591    pub repository: Option<String>,
5592    /// Filter by git branch name.
5593    #[serde(default, skip_serializing_if = "Option::is_none")]
5594    pub branch: Option<String>,
5595}
5596
5597/// Response from `session.getMetadata`.
5598#[derive(Debug, Clone, Serialize, Deserialize)]
5599#[serde(rename_all = "camelCase")]
5600pub struct GetSessionMetadataResponse {
5601    /// The session metadata, or `None` if the session was not found.
5602    #[serde(skip_serializing_if = "Option::is_none")]
5603    pub session: Option<SessionMetadata>,
5604}
5605
5606/// Response from `session.getLastId`.
5607#[derive(Debug, Clone, Serialize, Deserialize)]
5608#[serde(rename_all = "camelCase")]
5609pub struct GetLastSessionIdResponse {
5610    /// The most recently updated session ID, or `None` if no sessions exist.
5611    #[serde(skip_serializing_if = "Option::is_none")]
5612    pub session_id: Option<SessionId>,
5613}
5614
5615/// Response from `session.getForeground`.
5616#[derive(Debug, Clone, Serialize, Deserialize)]
5617#[serde(rename_all = "camelCase")]
5618pub struct GetForegroundSessionResponse {
5619    /// The current foreground session ID, or `None` if no foreground session.
5620    #[serde(skip_serializing_if = "Option::is_none")]
5621    pub session_id: Option<SessionId>,
5622}
5623
5624/// Response from `session.getMessages`.
5625#[derive(Debug, Clone, Serialize, Deserialize)]
5626#[serde(rename_all = "camelCase")]
5627pub struct GetMessagesResponse {
5628    /// Timeline events for the session.
5629    pub events: Vec<SessionEvent>,
5630}
5631
5632/// Result of an elicitation (interactive UI form) request.
5633#[derive(Debug, Clone, Serialize, Deserialize)]
5634#[serde(rename_all = "camelCase")]
5635pub struct ElicitationResult {
5636    /// User's action: `"accept"`, `"decline"`, or `"cancel"`.
5637    pub action: String,
5638    /// Form data submitted by the user (present when action is `"accept"`).
5639    #[serde(skip_serializing_if = "Option::is_none")]
5640    pub content: Option<Value>,
5641}
5642
5643/// Elicitation display mode.
5644///
5645/// New modes may be added by the CLI in future protocol versions; the
5646/// `Unknown` variant keeps deserialization from failing on unrecognised
5647/// values so the SDK can still surface the request to callers.
5648#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5649#[serde(rename_all = "camelCase")]
5650#[non_exhaustive]
5651pub enum ElicitationMode {
5652    /// Structured form input rendered by the host.
5653    Form,
5654    /// Browser redirect to a URL.
5655    Url,
5656    /// A mode not yet known to this SDK version.
5657    #[serde(other)]
5658    Unknown,
5659}
5660
5661/// An incoming elicitation request from the CLI (provider side).
5662///
5663/// Received via `elicitation.requested` session event when the session has
5664/// an [`ElicitationHandler`] installed.
5665/// The provider should render a form or dialog and return an
5666/// [`ElicitationResult`].
5667#[derive(Debug, Clone, Serialize, Deserialize)]
5668#[serde(rename_all = "camelCase")]
5669pub struct ElicitationRequest {
5670    /// Message describing what information is needed from the user.
5671    pub message: String,
5672    /// JSON Schema describing the form fields to present.
5673    #[serde(skip_serializing_if = "Option::is_none")]
5674    pub requested_schema: Option<Value>,
5675    /// Elicitation display mode.
5676    #[serde(skip_serializing_if = "Option::is_none")]
5677    pub mode: Option<ElicitationMode>,
5678    /// The source that initiated the request (e.g. MCP server name).
5679    #[serde(skip_serializing_if = "Option::is_none")]
5680    pub elicitation_source: Option<String>,
5681    /// URL to open in the user's browser (url mode only).
5682    #[serde(skip_serializing_if = "Option::is_none")]
5683    pub url: Option<String>,
5684}
5685
5686/// Session-level capabilities reported by the CLI after session creation.
5687///
5688/// Capabilities indicate which features the CLI host supports for this session.
5689/// Updated at runtime via `capabilities.changed` events.
5690#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5691#[serde(rename_all = "camelCase")]
5692pub struct SessionCapabilities {
5693    /// UI capabilities (elicitation support, etc.).
5694    #[serde(skip_serializing_if = "Option::is_none")]
5695    pub ui: Option<UiCapabilities>,
5696}
5697
5698/// UI-specific capabilities for a session.
5699#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5700#[serde(rename_all = "camelCase")]
5701pub struct UiCapabilities {
5702    /// Whether the host supports interactive elicitation dialogs.
5703    #[serde(skip_serializing_if = "Option::is_none")]
5704    pub elicitation: Option<bool>,
5705    /// **Experimental.** This field is part of an experimental wire-protocol
5706    /// surface (SEP-1865) and may change or be removed in a future release.
5707    ///
5708    /// Whether the runtime has accepted the session's MCP Apps (SEP-1865)
5709    /// opt-in. `Some(true)` when the consumer set
5710    /// [`SessionConfig::enable_mcp_apps`] / [`ResumeSessionConfig::enable_mcp_apps`]
5711    /// to `true` on create/resume **and** the runtime's `MCP_APPS` feature
5712    /// flag (or `COPILOT_MCP_APPS=true` env override) is on. Otherwise
5713    /// absent or `Some(false)`, indicating the runtime silently dropped the
5714    /// opt-in.
5715    #[serde(skip_serializing_if = "Option::is_none")]
5716    pub mcp_apps: Option<bool>,
5717    /// Host-specific canvas capabilities.
5718    #[serde(skip_serializing_if = "Option::is_none")]
5719    pub canvases: Option<bool>,
5720}
5721
5722/// Options for the [`SessionUi::input`](crate::session::SessionUi::input) convenience method.
5723#[derive(Debug, Clone, Default)]
5724pub struct UiInputOptions<'a> {
5725    /// Title label for the input field.
5726    pub title: Option<&'a str>,
5727    /// Descriptive text shown below the field.
5728    pub description: Option<&'a str>,
5729    /// Minimum character length.
5730    pub min_length: Option<u64>,
5731    /// Maximum character length.
5732    pub max_length: Option<u64>,
5733    /// Semantic format hint.
5734    pub format: Option<InputFormat>,
5735    /// Default value pre-populated in the field.
5736    pub default: Option<&'a str>,
5737}
5738
5739/// Semantic format hints for text input fields.
5740#[derive(Debug, Clone, Copy)]
5741#[non_exhaustive]
5742pub enum InputFormat {
5743    /// Email address.
5744    Email,
5745    /// URI.
5746    Uri,
5747    /// Calendar date.
5748    Date,
5749    /// Date and time.
5750    DateTime,
5751}
5752
5753impl InputFormat {
5754    /// Returns the JSON Schema format string for this variant.
5755    pub fn as_str(&self) -> &'static str {
5756        match self {
5757            Self::Email => "email",
5758            Self::Uri => "uri",
5759            Self::Date => "date",
5760            Self::DateTime => "date-time",
5761        }
5762    }
5763}
5764
5765/// Re-exports of generated protocol types that are part of the SDK's
5766/// public API surface. The canonical definitions live in
5767/// [`crate::rpc`]; they live here so the crate-root
5768/// `pub use types::*` surfaces them alongside hand-written SDK types.
5769pub use crate::generated::api_types::{
5770    Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5771    ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5772    ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5773    PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
5774    PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
5775    PermissionDecisionUserNotAvailable,
5776};
5777
5778/// Permission categories the CLI may request approval for.
5779///
5780/// Wire values are the lower-kebab strings the CLI sends as the `kind`
5781/// discriminator on a permission request. Marked `#[non_exhaustive]`
5782/// because the CLI may add new kinds; matches must include a `_` arm.
5783#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5784#[serde(rename_all = "kebab-case")]
5785#[non_exhaustive]
5786pub enum PermissionRequestKind {
5787    /// Run a shell command.
5788    Shell,
5789    /// Write to a file.
5790    Write,
5791    /// Read a file.
5792    Read,
5793    /// Open a URL.
5794    Url,
5795    /// Invoke an MCP server tool.
5796    Mcp,
5797    /// Invoke a client-defined custom tool.
5798    CustomTool,
5799    /// Update agent memory.
5800    Memory,
5801    /// Run a hook callback.
5802    Hook,
5803    /// Unrecognized kind. The original wire string is available in
5804    /// [`PermissionRequestData::extra`] under the `kind` key.
5805    #[serde(other)]
5806    Unknown,
5807}
5808
5809/// Data sent by the CLI for permission-related events.
5810///
5811/// Used for both the `permission.request` RPC call (which expects a response)
5812/// and `permission.requested` notifications (fire-and-forget). Contains the
5813/// full params object.
5814#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5815#[serde(rename_all = "camelCase")]
5816pub struct PermissionRequestData {
5817    /// The permission category being requested. `None` means the CLI did
5818    /// not include a `kind` field. Use this to branch on common cases
5819    /// (shell, write, etc.) without parsing [`extra`](Self::extra).
5820    #[serde(default, skip_serializing_if = "Option::is_none")]
5821    pub kind: Option<PermissionRequestKind>,
5822    /// The originating tool-call ID, if this permission request is tied
5823    /// to a specific tool invocation.
5824    #[serde(default, skip_serializing_if = "Option::is_none")]
5825    pub tool_call_id: Option<String>,
5826    /// Whether managed policy requires an explicit human decision.
5827    #[serde(default, skip_serializing_if = "Option::is_none")]
5828    pub managed_approval_required: Option<bool>,
5829    /// Whether managed settings are enabled for this session.
5830    #[serde(default, skip_serializing_if = "is_false")]
5831    pub managed_settings_enabled: bool,
5832    /// The full permission event params from the CLI, including the request ID
5833    /// and nested permission request. The shape varies by permission type and
5834    /// CLI version, so we preserve it as `Value`.
5835    #[serde(flatten)]
5836    pub extra: Value,
5837}
5838
5839/// Data sent by the CLI with an `exitPlanMode.request` RPC call.
5840#[derive(Debug, Clone, Serialize, Deserialize)]
5841#[serde(rename_all = "camelCase")]
5842pub struct ExitPlanModeData {
5843    /// Markdown summary of the plan presented to the user.
5844    #[serde(default)]
5845    pub summary: String,
5846    /// Full plan content (e.g. the plan.md body), if available.
5847    #[serde(default, skip_serializing_if = "Option::is_none")]
5848    pub plan_content: Option<String>,
5849    /// Allowed exit actions (e.g. "interactive", "autopilot", "autopilot_fleet").
5850    #[serde(default)]
5851    pub actions: Vec<String>,
5852    /// Which action the CLI recommends, defaults to "autopilot".
5853    #[serde(default = "default_recommended_action")]
5854    pub recommended_action: String,
5855}
5856
5857fn default_recommended_action() -> String {
5858    "autopilot".to_string()
5859}
5860
5861impl Default for ExitPlanModeData {
5862    fn default() -> Self {
5863        Self {
5864            summary: String::new(),
5865            plan_content: None,
5866            actions: Vec::new(),
5867            recommended_action: default_recommended_action(),
5868        }
5869    }
5870}
5871
5872#[cfg(test)]
5873mod tests {
5874    use std::collections::HashMap;
5875    use std::path::PathBuf;
5876
5877    use serde_json::json;
5878
5879    use super::{
5880        AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5881        AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5882        CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5883        ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
5884        InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
5885        MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig,
5886        ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId,
5887        SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5888        ToolResultResponse, ensure_attachment_display_names,
5889    };
5890    use crate::generated::session_events::TypedSessionEvent;
5891
5892    #[test]
5893    fn tool_builder_composes() {
5894        let tool = Tool::new("greet")
5895            .with_description("Say hello")
5896            .with_namespaced_name("hello/greet")
5897            .with_instructions("Pass the user's name")
5898            .with_parameters(json!({
5899                "type": "object",
5900                "properties": { "name": { "type": "string" } },
5901                "required": ["name"]
5902            }))
5903            .with_overrides_built_in_tool(true)
5904            .with_skip_permission(true);
5905        assert_eq!(tool.name, "greet");
5906        assert_eq!(tool.description, "Say hello");
5907        assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5908        assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5909        assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5910        assert!(tool.overrides_built_in_tool);
5911        assert!(tool.skip_permission);
5912    }
5913
5914    #[test]
5915    fn tool_defer_serialization() {
5916        let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5917        assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5918        let value = serde_json::to_value(&tool).unwrap();
5919        assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5920
5921        let plain = Tool::new("plain");
5922        let value = serde_json::to_value(&plain).unwrap();
5923        assert!(value.get("defer").is_none());
5924    }
5925
5926    #[test]
5927    fn tool_metadata_serialization() {
5928        use indexmap::IndexMap;
5929
5930        let mut metadata = IndexMap::new();
5931        metadata.insert(
5932            "github.com/copilot:safeForTelemetry".to_string(),
5933            json!({ "name": true, "inputsNames": false }),
5934        );
5935        let tool = Tool::new("lookup").with_metadata(metadata);
5936        let value = serde_json::to_value(&tool).unwrap();
5937        assert_eq!(
5938            value
5939                .get("metadata")
5940                .unwrap()
5941                .get("github.com/copilot:safeForTelemetry")
5942                .unwrap(),
5943            &json!({ "name": true, "inputsNames": false })
5944        );
5945
5946        // Empty metadata is omitted on the wire.
5947        let plain = Tool::new("plain");
5948        let value = serde_json::to_value(&plain).unwrap();
5949        assert!(value.get("metadata").is_none());
5950    }
5951
5952    #[test]
5953    fn custom_agent_config_builder_with_model() {
5954        let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5955            .with_model("claude-haiku-4.5")
5956            .with_display_name("My Agent");
5957        assert_eq!(agent.name, "my-agent");
5958        assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5959        assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5960    }
5961
5962    #[test]
5963    fn custom_agent_config_serializes_model() {
5964        let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5965        let wire = serde_json::to_value(&agent).unwrap();
5966        assert_eq!(wire["model"], "claude-haiku-4.5");
5967        assert_eq!(wire["name"], "model-agent");
5968    }
5969
5970    #[test]
5971    fn custom_agent_config_omits_model_when_none() {
5972        let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5973        let wire = serde_json::to_value(&agent).unwrap();
5974        assert!(wire.get("model").is_none());
5975    }
5976
5977    #[test]
5978    fn custom_agent_config_builder_with_reasoning_effort() {
5979        let agent =
5980            CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5981        assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
5982    }
5983
5984    #[test]
5985    fn custom_agent_config_serializes_reasoning_effort() {
5986        let agent =
5987            CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5988        let wire = serde_json::to_value(&agent).unwrap();
5989        assert_eq!(wire["reasoningEffort"], "high");
5990    }
5991
5992    #[test]
5993    fn custom_agent_config_omits_reasoning_effort_when_none() {
5994        let agent = CustomAgentConfig::new("default-agent", "prompt");
5995        let wire = serde_json::to_value(&agent).unwrap();
5996        assert!(wire.get("reasoningEffort").is_none());
5997    }
5998
5999    #[test]
6000    #[should_panic(expected = "tool parameter schema must be a JSON object")]
6001    fn tool_with_parameters_panics_on_non_object_value() {
6002        let _ = Tool::new("noop").with_parameters(json!(null));
6003    }
6004
6005    #[test]
6006    fn tool_result_expanded_serializes_binary_results_for_llm() {
6007        let response = ToolResultResponse {
6008            result: ToolResult::Expanded(ToolResultExpanded {
6009                text_result_for_llm: "rendered chart".to_string(),
6010                result_type: "success".to_string(),
6011                binary_results_for_llm: Some(vec![ToolBinaryResult {
6012                    data: "aW1n".to_string(),
6013                    mime_type: "image/png".to_string(),
6014                    r#type: "image".to_string(),
6015                    description: Some("chart preview".to_string()),
6016                }]),
6017                session_log: None,
6018                error: None,
6019                tool_telemetry: None,
6020                tool_references: None,
6021            }),
6022        };
6023
6024        let wire = serde_json::to_value(&response).unwrap();
6025
6026        assert_eq!(
6027            wire,
6028            json!({
6029                "result": {
6030                    "textResultForLlm": "rendered chart",
6031                    "resultType": "success",
6032                    "binaryResultsForLlm": [
6033                        {
6034                            "data": "aW1n",
6035                            "mimeType": "image/png",
6036                            "type": "image",
6037                            "description": "chart preview"
6038                        }
6039                    ]
6040                }
6041            })
6042        );
6043    }
6044
6045    #[test]
6046    fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6047        let response = ToolResultResponse {
6048            result: ToolResult::Expanded(ToolResultExpanded {
6049                text_result_for_llm: "ok".to_string(),
6050                result_type: "success".to_string(),
6051                binary_results_for_llm: None,
6052                session_log: None,
6053                error: None,
6054                tool_telemetry: None,
6055                tool_references: None,
6056            }),
6057        };
6058
6059        let wire = serde_json::to_value(&response).unwrap();
6060
6061        assert_eq!(wire["result"]["textResultForLlm"], "ok");
6062        assert!(wire["result"].get("binaryResultsForLlm").is_none());
6063    }
6064
6065    #[test]
6066    fn tool_result_expanded_serializes_tool_references() {
6067        let response = ToolResultResponse {
6068            result: ToolResult::Expanded(
6069                ToolResultExpanded::new("found 2 tools", "success")
6070                    .with_tool_references(["get_weather", "check_status"]),
6071            ),
6072        };
6073
6074        let wire = serde_json::to_value(&response).unwrap();
6075
6076        assert_eq!(
6077            wire,
6078            json!({
6079                "result": {
6080                    "textResultForLlm": "found 2 tools",
6081                    "resultType": "success",
6082                    "toolReferences": ["get_weather", "check_status"]
6083                }
6084            })
6085        );
6086    }
6087
6088    #[test]
6089    fn tool_result_expanded_omits_tool_references_when_none() {
6090        let response = ToolResultResponse {
6091            result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6092        };
6093
6094        let wire = serde_json::to_value(&response).unwrap();
6095
6096        assert_eq!(wire["result"]["textResultForLlm"], "ok");
6097        assert!(wire["result"].get("toolReferences").is_none());
6098    }
6099
6100    #[test]
6101    fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6102        // The builder is generic over `Into<String>`, so an owned `Vec<String>`
6103        // must compile and populate the field just like a `&str` array.
6104        let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6105        let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6106
6107        assert_eq!(
6108            expanded.tool_references.as_deref(),
6109            Some(["alpha".to_string(), "beta".to_string()].as_slice())
6110        );
6111    }
6112
6113    #[test]
6114    fn tool_result_expanded_deserializes_tool_references() {
6115        let wire = json!({
6116            "textResultForLlm": "found tools",
6117            "resultType": "success",
6118            "toolReferences": ["alpha", "beta"]
6119        });
6120
6121        let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6122
6123        assert_eq!(
6124            expanded.tool_references.as_deref(),
6125            Some(["alpha".to_string(), "beta".to_string()].as_slice())
6126        );
6127    }
6128
6129    #[test]
6130    fn session_config_default_wire_flags_off_without_handlers() {
6131        let cfg = SessionConfig::default();
6132        assert_eq!(cfg.mcp_oauth_token_storage, None);
6133        // Wire flags are derived from handler presence at create_session
6134        // time, not stored on the config. With no handlers installed, every
6135        // request_* flag should serialize as false.
6136        let (wire, _runtime) = cfg
6137            .into_wire(Some(SessionId::from("default-flags")))
6138            .expect("default config has no duplicate handlers");
6139        assert!(!wire.request_user_input);
6140        assert!(!wire.request_permission);
6141        assert!(!wire.request_elicitation);
6142        assert!(!wire.request_exit_plan_mode);
6143        assert!(!wire.request_auto_mode_switch);
6144        assert!(!wire.hooks);
6145        assert!(!wire.request_mcp_apps);
6146    }
6147
6148    #[test]
6149    fn resume_session_config_new_wire_flags_off_without_handlers() {
6150        let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6151        assert_eq!(cfg.mcp_oauth_token_storage, None);
6152        let (wire, _runtime) = cfg
6153            .into_wire()
6154            .expect("default resume config has no duplicate handlers");
6155        assert!(!wire.request_user_input);
6156        assert!(!wire.request_permission);
6157        assert!(!wire.request_elicitation);
6158        assert!(!wire.request_exit_plan_mode);
6159        assert!(!wire.request_auto_mode_switch);
6160        assert!(!wire.hooks);
6161        assert!(!wire.request_mcp_apps);
6162    }
6163
6164    #[test]
6165    fn custom_agents_local_only_serializes_on_create_and_resume() {
6166        let (create_wire, _) = SessionConfig::default()
6167            .with_custom_agents_local_only(false)
6168            .into_wire(Some(SessionId::from("create-locality")))
6169            .expect("create config has no duplicate handlers");
6170        let create_json = serde_json::to_value(&create_wire).unwrap();
6171        assert_eq!(create_json["customAgentsLocalOnly"], false);
6172
6173        let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6174            .with_custom_agents_local_only(false)
6175            .into_wire()
6176            .expect("resume config has no duplicate handlers");
6177        let resume_json = serde_json::to_value(&resume_wire).unwrap();
6178        assert_eq!(resume_json["customAgentsLocalOnly"], false);
6179
6180        let (unset_create_wire, _) = SessionConfig::default()
6181            .into_wire(Some(SessionId::from("create-unset")))
6182            .expect("create config has no duplicate handlers");
6183        let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6184        assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6185
6186        let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6187            .into_wire()
6188            .expect("resume config has no duplicate handlers");
6189        let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6190        assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6191    }
6192
6193    #[test]
6194    fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6195        let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6196        assert_eq!(cfg.enable_mcp_apps, Some(true));
6197
6198        let (wire, _runtime) = cfg
6199            .into_wire(Some(SessionId::from("enable-mcp-apps")))
6200            .expect("enable_mcp_apps config has no duplicate handlers");
6201        assert!(wire.request_mcp_apps);
6202
6203        let json = serde_json::to_value(&wire).unwrap();
6204        assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6205    }
6206
6207    #[test]
6208    fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6209        let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6210            .with_enable_mcp_apps(true);
6211        assert_eq!(cfg.enable_mcp_apps, Some(true));
6212
6213        let (wire, _runtime) = cfg
6214            .into_wire()
6215            .expect("resume enable_mcp_apps config has no duplicate handlers");
6216        assert!(wire.request_mcp_apps);
6217
6218        let json = serde_json::to_value(&wire).unwrap();
6219        assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6220    }
6221
6222    #[test]
6223    fn github_mcp_tool_config_serializes_for_create_and_resume() {
6224        let github_config = GitHubMcpToolConfig::new()
6225            .with_enable_all_tools(true)
6226            .with_additional_toolsets(["repos"])
6227            .with_additional_tools(["get_issue"])
6228            .with_enable_insiders_mode(true)
6229            .with_disable_form_deferral(true);
6230
6231        let (create_wire, _) = SessionConfig::default()
6232            .with_github_mcp_tool_config(github_config.clone())
6233            .into_wire(Some(SessionId::from("github-mcp")))
6234            .expect("create config has no duplicate handlers");
6235        assert_eq!(
6236            serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6237            serde_json::json!({
6238                "enableAllTools": true,
6239                "additionalToolsets": ["repos"],
6240                "additionalTools": ["get_issue"],
6241                "enableInsidersMode": true,
6242                "disableFormDeferral": true,
6243            })
6244        );
6245
6246        let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6247            .with_github_mcp_tool_config(github_config)
6248            .into_wire()
6249            .expect("resume config has no duplicate handlers");
6250        assert!(resume_wire.github_mcp_tool_config.is_some());
6251
6252        let (unset_wire, _) = SessionConfig::default()
6253            .into_wire(Some(SessionId::from("github-mcp-unset")))
6254            .expect("default config has no duplicate handlers");
6255        assert!(
6256            serde_json::to_value(&unset_wire)
6257                .unwrap()
6258                .get("githubMcpToolConfig")
6259                .is_none()
6260        );
6261    }
6262
6263    #[test]
6264    fn memory_configuration_constructors_and_serde() {
6265        assert!(MemoryConfiguration::enabled().enabled);
6266        assert!(!MemoryConfiguration::disabled().enabled);
6267        assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6268
6269        let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6270        assert_eq!(json, serde_json::json!({ "enabled": true }));
6271    }
6272
6273    #[test]
6274    fn session_config_with_memory_serializes() {
6275        let (wire, _runtime) = SessionConfig::default()
6276            .with_memory(MemoryConfiguration::enabled())
6277            .into_wire(Some(SessionId::from("memory-on")))
6278            .expect("no duplicate handlers");
6279        let json = serde_json::to_value(&wire).unwrap();
6280        assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6281
6282        let (wire_off, _) = SessionConfig::default()
6283            .with_memory(MemoryConfiguration::disabled())
6284            .into_wire(Some(SessionId::from("memory-off")))
6285            .expect("no duplicate handlers");
6286        let json_off = serde_json::to_value(&wire_off).unwrap();
6287        assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6288
6289        // Unset memory is omitted on the wire.
6290        let (empty_wire, _) = SessionConfig::default()
6291            .into_wire(Some(SessionId::from("memory-unset")))
6292            .expect("no duplicate handlers");
6293        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6294        assert!(empty_json.get("memory").is_none());
6295    }
6296
6297    #[test]
6298    fn resume_session_config_with_memory_serializes() {
6299        let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6300            .with_memory(MemoryConfiguration::enabled())
6301            .into_wire()
6302            .expect("no duplicate handlers");
6303        let json = serde_json::to_value(&wire).unwrap();
6304        assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6305
6306        // Unset memory is omitted on the wire.
6307        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6308            .into_wire()
6309            .expect("no duplicate handlers");
6310        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6311        assert!(empty_json.get("memory").is_none());
6312    }
6313
6314    fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6315        CopilotExpAssignmentResponse {
6316            features: vec!["copilot_exp_flag".to_string()],
6317            flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6318            configs: vec![ExpConfigEntry {
6319                id: "cfg-1".to_string(),
6320                parameters: HashMap::from([
6321                    ("threshold".to_string(), ExpFlagValue::Integer(5)),
6322                    ("enabled".to_string(), ExpFlagValue::Bool(true)),
6323                ]),
6324            }],
6325            assignment_context: context.to_string(),
6326            ..Default::default()
6327        }
6328    }
6329
6330    #[test]
6331    fn exp_flag_value_round_trips_all_variants() {
6332        let values = serde_json::json!({
6333            "s": "text",
6334            "i": 7,
6335            "f": 1.5,
6336            "b": true,
6337            "n": null,
6338        });
6339        let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6340        assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6341        assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6342        assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6343        assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6344        assert_eq!(parsed["n"], ExpFlagValue::Null);
6345        assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6346    }
6347
6348    #[test]
6349    fn session_config_with_exp_assignments_serializes() {
6350        let assignments = sample_exp_assignments("ctx-123");
6351        let expected = serde_json::to_value(&assignments).unwrap();
6352        let (wire, _runtime) = SessionConfig::default()
6353            .with_exp_assignments(assignments)
6354            .into_wire(Some(SessionId::from("exp-on")))
6355            .expect("no duplicate handlers");
6356        let json = serde_json::to_value(&wire).unwrap();
6357        assert_eq!(json["expAssignments"], expected);
6358        assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6359        assert_eq!(
6360            json["expAssignments"]["Flights"]["copilot_exp_flag"],
6361            "treatment"
6362        );
6363
6364        // Unset exp assignments are omitted on the wire.
6365        let (empty_wire, _) = SessionConfig::default()
6366            .into_wire(Some(SessionId::from("exp-unset")))
6367            .expect("no duplicate handlers");
6368        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6369        assert!(empty_json.get("expAssignments").is_none());
6370    }
6371
6372    #[test]
6373    fn resume_session_config_with_exp_assignments_serializes() {
6374        let assignments = sample_exp_assignments("ctx-456");
6375        let expected = serde_json::to_value(&assignments).unwrap();
6376        let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6377            .with_exp_assignments(assignments)
6378            .into_wire()
6379            .expect("no duplicate handlers");
6380        let json = serde_json::to_value(&wire).unwrap();
6381        assert_eq!(json["expAssignments"], expected);
6382
6383        // Unset exp assignments are omitted on the wire.
6384        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6385            .into_wire()
6386            .expect("no duplicate handlers");
6387        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6388        assert!(empty_json.get("expAssignments").is_none());
6389    }
6390
6391    #[test]
6392    fn session_config_clone_preserves_exp_assignments() {
6393        let assignments = sample_exp_assignments("ctx-clone");
6394        let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6395        let cloned = config.clone();
6396
6397        assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6398
6399        let (wire, _runtime) = cloned
6400            .into_wire(Some(SessionId::from("exp-clone")))
6401            .expect("no duplicate handlers");
6402        let json = serde_json::to_value(&wire).unwrap();
6403        assert_eq!(
6404            json["expAssignments"],
6405            serde_json::to_value(&assignments).unwrap()
6406        );
6407    }
6408
6409    #[test]
6410    fn resume_session_config_clone_preserves_exp_assignments() {
6411        let assignments = sample_exp_assignments("ctx-clone-resume");
6412        let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6413            .with_exp_assignments(assignments.clone());
6414        let cloned = config.clone();
6415
6416        assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6417
6418        let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6419        let json = serde_json::to_value(&wire).unwrap();
6420        assert_eq!(
6421            json["expAssignments"],
6422            serde_json::to_value(&assignments).unwrap()
6423        );
6424    }
6425
6426    #[test]
6427    #[allow(clippy::field_reassign_with_default)]
6428    fn session_config_into_wire_serializes_bucket_b_fields() {
6429        use std::path::PathBuf;
6430
6431        use super::{CloudSessionOptions, CloudSessionRepository};
6432
6433        let mut cfg = SessionConfig::default();
6434        cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6435        cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6436        cfg.github_token = Some("ghs_secret".to_string());
6437        cfg.include_sub_agent_streaming_events = Some(false);
6438        cfg.enable_session_telemetry = Some(false);
6439        cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6440        cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6441        cfg.enable_on_demand_instruction_discovery = Some(false);
6442        cfg.cloud = Some(CloudSessionOptions::with_repository(
6443            CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6444        ));
6445
6446        let (wire, _runtime) = cfg
6447            .into_wire(Some(SessionId::from("custom-id")))
6448            .expect("no duplicate handlers");
6449        let wire_json = serde_json::to_value(&wire).unwrap();
6450        assert_eq!(wire_json["sessionId"], "custom-id");
6451        assert_eq!(wire_json["configDir"], "/tmp/cfg");
6452        assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6453        assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6454        assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6455        assert_eq!(wire_json["enableSessionTelemetry"], false);
6456        assert_eq!(wire_json["reasoningSummary"], "concise");
6457        assert_eq!(wire_json["remoteSession"], "export");
6458        assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6459        assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6460        assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6461        assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6462
6463        // Unset fields are omitted on the wire.
6464        let (empty_wire, _) = SessionConfig::default()
6465            .into_wire(Some(SessionId::from("empty")))
6466            .expect("default has no duplicate handlers");
6467        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6468        assert!(empty_json.get("gitHubToken").is_none());
6469        assert!(empty_json.get("enableSessionTelemetry").is_none());
6470        assert!(empty_json.get("reasoningSummary").is_none());
6471        assert!(empty_json.get("remoteSession").is_none());
6472        assert!(
6473            empty_json
6474                .get("enableOnDemandInstructionDiscovery")
6475                .is_none()
6476        );
6477        assert!(empty_json.get("cloud").is_none());
6478    }
6479
6480    #[test]
6481    fn session_config_into_wire_serializes_named_providers_and_models() {
6482        let cfg = SessionConfig::default()
6483            .with_providers(vec![
6484                NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6485                    .with_provider_type("openai")
6486                    .with_wire_api("responses")
6487                    .with_api_key("sk-test"),
6488            ])
6489            .with_models(vec![
6490                ProviderModelConfig::new("gpt-x", "my-openai")
6491                    .with_wire_model("gpt-x-2025")
6492                    .with_max_output_tokens(2048),
6493            ]);
6494
6495        let (wire, _) = cfg
6496            .into_wire(Some(SessionId::from("sess-providers")))
6497            .expect("no duplicate handlers");
6498        let wire_json = serde_json::to_value(&wire).unwrap();
6499        assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6500        assert_eq!(
6501            wire_json["providers"][0]["baseUrl"],
6502            "https://api.example.com/v1"
6503        );
6504        assert_eq!(wire_json["providers"][0]["type"], "openai");
6505        assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6506        assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6507        assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6508        assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6509        assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6510        assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6511
6512        let (empty_wire, _) = SessionConfig::default()
6513            .into_wire(Some(SessionId::from("empty")))
6514            .expect("default has no duplicate handlers");
6515        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6516        assert!(empty_json.get("providers").is_none());
6517        assert!(empty_json.get("models").is_none());
6518    }
6519
6520    #[test]
6521    fn resume_config_into_wire_serializes_named_providers_and_models() {
6522        let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6523            .with_providers(vec![
6524                NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6525                    .with_provider_type("azure")
6526                    .with_azure(AzureProviderOptions {
6527                        api_version: Some("2024-10-21".to_string()),
6528                    }),
6529            ])
6530            .with_models(vec![
6531                ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6532            ]);
6533
6534        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6535        let wire_json = serde_json::to_value(&wire).unwrap();
6536        assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6537        assert_eq!(wire_json["providers"][0]["type"], "azure");
6538        assert_eq!(
6539            wire_json["providers"][0]["azure"]["apiVersion"],
6540            "2024-10-21"
6541        );
6542        assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6543        assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6544        assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6545
6546        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6547            .into_wire()
6548            .expect("default has no duplicate handlers");
6549        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6550        assert!(empty_json.get("providers").is_none());
6551        assert!(empty_json.get("models").is_none());
6552    }
6553
6554    #[test]
6555    fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6556        use std::path::PathBuf;
6557
6558        let cfg = SessionConfig {
6559            plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6560            disabled_mcp_servers: Some(vec![
6561                "local-files".to_string(),
6562                "remote-github".to_string(),
6563            ]),
6564            large_output: Some(
6565                LargeToolOutputConfig::new()
6566                    .with_enabled(true)
6567                    .with_max_size_bytes(1024)
6568                    .with_output_directory(PathBuf::from("/tmp/large-output")),
6569            ),
6570            ..Default::default()
6571        };
6572
6573        let (wire, _) = cfg
6574            .into_wire(Some(SessionId::from("sess-1")))
6575            .expect("no duplicate handlers");
6576        let wire_json = serde_json::to_value(&wire).unwrap();
6577        assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6578        assert_eq!(
6579            wire_json["disabledMcpServers"],
6580            serde_json::json!(["local-files", "remote-github"])
6581        );
6582        assert_eq!(wire_json["largeOutput"]["enabled"], true);
6583        assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6584        assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6585
6586        let (empty_wire, _) = SessionConfig::default()
6587            .into_wire(Some(SessionId::from("empty")))
6588            .expect("default has no duplicate handlers");
6589        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6590        assert!(empty_json.get("pluginDirectories").is_none());
6591        assert!(empty_json.get("disabledMcpServers").is_none());
6592        assert!(empty_json.get("largeOutput").is_none());
6593    }
6594
6595    #[test]
6596    fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6597        use std::path::PathBuf;
6598
6599        let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6600        cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6601        cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6602        cfg.github_token = Some("ghs_secret".to_string());
6603        cfg.include_sub_agent_streaming_events = Some(true);
6604        cfg.enable_session_telemetry = Some(false);
6605        cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6606        cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6607        cfg.enable_on_demand_instruction_discovery = Some(false);
6608
6609        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6610        let wire_json = serde_json::to_value(&wire).unwrap();
6611        assert_eq!(wire_json["sessionId"], "sess-1");
6612        assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6613        assert_eq!(wire_json["configDir"], "/tmp/cfg");
6614        assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6615        assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6616        assert_eq!(wire_json["enableSessionTelemetry"], false);
6617        assert_eq!(wire_json["reasoningSummary"], "detailed");
6618        assert_eq!(wire_json["remoteSession"], "on");
6619        assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6620
6621        // Unset remote_session is omitted on the wire.
6622        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6623            .into_wire()
6624            .expect("default resume has no duplicate handlers");
6625        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6626        assert!(empty_json.get("reasoningSummary").is_none());
6627        assert!(empty_json.get("remoteSession").is_none());
6628        assert!(
6629            empty_json
6630                .get("enableOnDemandInstructionDiscovery")
6631                .is_none()
6632        );
6633    }
6634
6635    #[test]
6636    fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6637        use std::path::PathBuf;
6638
6639        let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6640        cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6641        cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6642        cfg.large_output = Some(
6643            LargeToolOutputConfig::new()
6644                .with_enabled(false)
6645                .with_max_size_bytes(2048)
6646                .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6647        );
6648
6649        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6650        let wire_json = serde_json::to_value(&wire).unwrap();
6651        assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6652        assert_eq!(
6653            wire_json["disabledMcpServers"],
6654            serde_json::json!(["local-files-r"])
6655        );
6656        assert_eq!(wire_json["largeOutput"]["enabled"], false);
6657        assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6658        assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6659
6660        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6661            .into_wire()
6662            .expect("default resume has no duplicate handlers");
6663        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6664        assert!(empty_json.get("pluginDirectories").is_none());
6665        assert!(empty_json.get("disabledMcpServers").is_none());
6666        assert!(empty_json.get("largeOutput").is_none());
6667    }
6668
6669    #[test]
6670    fn session_config_clones_disabled_mcp_servers() {
6671        let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6672        let mut create_clone = create.clone();
6673        create_clone
6674            .disabled_mcp_servers
6675            .as_mut()
6676            .expect("configured disabled MCP servers")
6677            .push("remote-github".to_string());
6678        assert_eq!(
6679            create.disabled_mcp_servers.as_deref(),
6680            Some(&["local-files".to_string()][..])
6681        );
6682
6683        let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6684            .with_disabled_mcp_servers(["local-files"]);
6685        let mut resume_clone = resume.clone();
6686        resume_clone
6687            .disabled_mcp_servers
6688            .as_mut()
6689            .expect("configured disabled MCP servers")
6690            .push("remote-github".to_string());
6691        assert_eq!(
6692            resume.disabled_mcp_servers.as_deref(),
6693            Some(&["local-files".to_string()][..])
6694        );
6695    }
6696
6697    #[test]
6698    fn session_config_builder_composes() {
6699        use indexmap::IndexMap;
6700
6701        let cfg = SessionConfig::default()
6702            .with_session_id(SessionId::from("sess-1"))
6703            .with_model("claude-sonnet-4")
6704            .with_client_name("test-app")
6705            .with_reasoning_effort("medium")
6706            .with_reasoning_summary(ReasoningSummary::Concise)
6707            .with_context_tier("long_context")
6708            .with_streaming(true)
6709            .with_tools([Tool::new("greet")])
6710            .with_available_tools(["bash", "view"])
6711            .with_excluded_tools(["dangerous"])
6712            .with_mcp_servers(IndexMap::new())
6713            .with_mcp_oauth_token_storage("persistent")
6714            .with_enable_config_discovery(true)
6715            .with_enable_on_demand_instruction_discovery(true)
6716            .with_skill_directories([PathBuf::from("/tmp/skills")])
6717            .with_disabled_skills(["broken-skill"])
6718            .with_disabled_mcp_servers(["local-files"])
6719            .with_agent("researcher")
6720            .with_config_directory(PathBuf::from("/tmp/config"))
6721            .with_working_directory(PathBuf::from("/tmp/work"))
6722            .with_additional_directories([PathBuf::from("/tmp/shared")])
6723            .with_github_token("ghp_test")
6724            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6725            .with_enable_session_telemetry(false)
6726            .with_include_sub_agent_streaming_events(false)
6727            .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6728
6729        assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6730        assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6731        assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6732        assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6733        assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6734        assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6735        assert_eq!(cfg.streaming, Some(true));
6736        assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6737        assert_eq!(
6738            cfg.available_tools.as_deref(),
6739            Some(&["bash".to_string(), "view".to_string()][..])
6740        );
6741        assert_eq!(
6742            cfg.excluded_tools.as_deref(),
6743            Some(&["dangerous".to_string()][..])
6744        );
6745        assert!(cfg.mcp_servers.is_some());
6746        assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6747        assert_eq!(cfg.enable_config_discovery, Some(true));
6748        assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6749        assert_eq!(
6750            cfg.skill_directories.as_deref(),
6751            Some(&[PathBuf::from("/tmp/skills")][..])
6752        );
6753        assert_eq!(
6754            cfg.disabled_skills.as_deref(),
6755            Some(&["broken-skill".to_string()][..])
6756        );
6757        assert_eq!(
6758            cfg.disabled_mcp_servers.as_deref(),
6759            Some(&["local-files".to_string()][..])
6760        );
6761        assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6762        assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6763        assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6764        assert_eq!(
6765            cfg.additional_directories.as_deref(),
6766            Some(&[PathBuf::from("/tmp/shared")][..])
6767        );
6768        assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6769        assert_eq!(
6770            cfg.capi,
6771            Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6772        );
6773        assert_eq!(cfg.enable_session_telemetry, Some(false));
6774        assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6775        assert_eq!(
6776            cfg.extension_info,
6777            Some(ExtensionInfo::new("github-app", "counter"))
6778        );
6779    }
6780
6781    #[test]
6782    fn resume_session_config_builder_composes() {
6783        use indexmap::IndexMap;
6784
6785        let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6786            .with_client_name("test-app")
6787            .with_reasoning_summary(ReasoningSummary::None)
6788            .with_context_tier("default")
6789            .with_streaming(true)
6790            .with_tools([Tool::new("greet")])
6791            .with_available_tools(["bash", "view"])
6792            .with_excluded_tools(["dangerous"])
6793            .with_mcp_servers(IndexMap::new())
6794            .with_mcp_oauth_token_storage("persistent")
6795            .with_enable_config_discovery(true)
6796            .with_enable_on_demand_instruction_discovery(false)
6797            .with_skill_directories([PathBuf::from("/tmp/skills")])
6798            .with_disabled_skills(["broken-skill"])
6799            .with_disabled_mcp_servers(["local-files"])
6800            .with_agent("researcher")
6801            .with_config_directory(PathBuf::from("/tmp/config"))
6802            .with_working_directory(PathBuf::from("/tmp/work"))
6803            .with_additional_directories([PathBuf::from("/tmp/shared")])
6804            .with_github_token("ghp_test")
6805            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6806            .with_enable_session_telemetry(false)
6807            .with_include_sub_agent_streaming_events(true)
6808            .with_suppress_resume_event(true)
6809            .with_continue_pending_work(true)
6810            .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6811
6812        assert_eq!(cfg.session_id.as_str(), "sess-2");
6813        assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6814        assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6815        assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6816        assert_eq!(cfg.streaming, Some(true));
6817        assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6818        assert_eq!(
6819            cfg.available_tools.as_deref(),
6820            Some(&["bash".to_string(), "view".to_string()][..])
6821        );
6822        assert_eq!(
6823            cfg.excluded_tools.as_deref(),
6824            Some(&["dangerous".to_string()][..])
6825        );
6826        assert!(cfg.mcp_servers.is_some());
6827        assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6828        assert_eq!(cfg.enable_config_discovery, Some(true));
6829        assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6830        assert_eq!(
6831            cfg.skill_directories.as_deref(),
6832            Some(&[PathBuf::from("/tmp/skills")][..])
6833        );
6834        assert_eq!(
6835            cfg.disabled_skills.as_deref(),
6836            Some(&["broken-skill".to_string()][..])
6837        );
6838        assert_eq!(
6839            cfg.disabled_mcp_servers.as_deref(),
6840            Some(&["local-files".to_string()][..])
6841        );
6842        assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6843        assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6844        assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6845        assert_eq!(
6846            cfg.additional_directories.as_deref(),
6847            Some(&[PathBuf::from("/tmp/shared")][..])
6848        );
6849        assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6850        assert_eq!(
6851            cfg.capi,
6852            Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6853        );
6854        assert_eq!(cfg.enable_session_telemetry, Some(false));
6855        assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6856        assert_eq!(cfg.suppress_resume_event, Some(true));
6857        assert_eq!(cfg.continue_pending_work, Some(true));
6858        assert_eq!(
6859            cfg.extension_info,
6860            Some(ExtensionInfo::new("github-app", "counter"))
6861        );
6862    }
6863
6864    /// `continue_pending_work` must serialize to wire as `continuePendingWork`
6865    /// — the runtime keys off this exact field name to opt into the
6866    /// pending-work-handoff pattern.
6867    #[test]
6868    fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6869        let cfg =
6870            ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6871        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6872        let json = serde_json::to_value(&wire).unwrap();
6873        assert_eq!(json["continuePendingWork"], true);
6874
6875        // Unset case — skip_serializing_if must omit the field.
6876        let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6877            .into_wire()
6878            .expect("no duplicate handlers");
6879        let json = serde_json::to_value(&wire).unwrap();
6880        assert!(json.get("continuePendingWork").is_none());
6881    }
6882
6883    #[test]
6884    fn session_configs_serialize_additional_directories() {
6885        let create = SessionConfig::default().with_additional_directories([
6886            PathBuf::from("/tmp/shared"),
6887            PathBuf::from("/tmp/generated"),
6888        ]);
6889        let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
6890        let create_json = serde_json::to_value(&create_wire).unwrap();
6891        assert_eq!(
6892            create_json["additionalDirectories"],
6893            serde_json::json!(["/tmp/shared", "/tmp/generated"])
6894        );
6895
6896        let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6897            .with_additional_directories([PathBuf::from("/tmp/resumed")]);
6898        let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
6899        let resume_json = serde_json::to_value(&resume_wire).unwrap();
6900        assert_eq!(
6901            resume_json["additionalDirectories"],
6902            serde_json::json!(["/tmp/resumed"])
6903        );
6904    }
6905
6906    /// The Rust field is `suppress_resume_event`, but the wire field stays
6907    /// `disableResume` to preserve compatibility with the runtime and other
6908    /// SDKs.
6909    #[test]
6910    fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6911        let cfg =
6912            ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6913        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6914        let json = serde_json::to_value(&wire).unwrap();
6915        assert_eq!(json["disableResume"], true);
6916        assert!(json.get("suppressResumeEvent").is_none());
6917    }
6918
6919    /// `instruction_directories` must serialize to wire as
6920    /// `instructionDirectories` on `SessionConfig`.
6921    #[test]
6922    fn session_config_serializes_instruction_directories_to_camel_case() {
6923        let cfg =
6924            SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6925        let (wire, _) = cfg
6926            .into_wire(Some(SessionId::from("instr-on")))
6927            .expect("no duplicate handlers");
6928        let json = serde_json::to_value(&wire).unwrap();
6929        assert_eq!(
6930            json["instructionDirectories"],
6931            serde_json::json!(["/tmp/instr"])
6932        );
6933
6934        // Unset case — skip_serializing_if must omit the field.
6935        let (wire, _) = SessionConfig::default()
6936            .into_wire(Some(SessionId::from("instr-off")))
6937            .expect("no duplicate handlers");
6938        let json = serde_json::to_value(&wire).unwrap();
6939        assert!(json.get("instructionDirectories").is_none());
6940    }
6941
6942    /// Same check on the resume path. Forwarded to the CLI on
6943    /// `session.resume`.
6944    #[test]
6945    fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6946        let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6947            .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6948        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6949        let json = serde_json::to_value(&wire).unwrap();
6950        assert_eq!(
6951            json["instructionDirectories"],
6952            serde_json::json!(["/tmp/instr"])
6953        );
6954
6955        let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6956            .into_wire()
6957            .expect("no duplicate handlers");
6958        let json = serde_json::to_value(&wire).unwrap();
6959        assert!(json.get("instructionDirectories").is_none());
6960    }
6961
6962    #[test]
6963    fn custom_agent_config_builder_composes() {
6964        use indexmap::IndexMap;
6965
6966        let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6967            .with_display_name("Research Assistant")
6968            .with_description("Investigates technical questions.")
6969            .with_tools(["bash", "view"])
6970            .with_mcp_servers(IndexMap::new())
6971            .with_infer(true)
6972            .with_skills(["rust-coding-skill"]);
6973
6974        assert_eq!(cfg.name, "researcher");
6975        assert_eq!(cfg.prompt, "You are a research assistant.");
6976        assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6977        assert_eq!(
6978            cfg.description.as_deref(),
6979            Some("Investigates technical questions.")
6980        );
6981        assert_eq!(
6982            cfg.tools.as_deref(),
6983            Some(&["bash".to_string(), "view".to_string()][..])
6984        );
6985        assert!(cfg.mcp_servers.is_some());
6986        assert_eq!(cfg.infer, Some(true));
6987        assert_eq!(
6988            cfg.skills.as_deref(),
6989            Some(&["rust-coding-skill".to_string()][..])
6990        );
6991    }
6992
6993    #[test]
6994    fn mcp_servers_serialize_in_insertion_order() {
6995        use indexmap::IndexMap;
6996
6997        // Regression: `mcp_servers` was a `HashMap`, so the server keys (and
6998        // thus the `session.create` payload) serialized in a per-process
6999        // random order; `IndexMap` pins them to insertion order. The long
7000        // sequence makes a `HashMap` regression reproduce this exact order by
7001        // chance only 1/N!, avoiding a flaky false pass.
7002        let order = [
7003            "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7004            "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7005        ];
7006        let mut servers = IndexMap::new();
7007        for name in order {
7008            servers.insert(
7009                name.to_string(),
7010                McpServerConfig::Stdio(McpStdioServerConfig {
7011                    command: "run".to_string(),
7012                    ..Default::default()
7013                }),
7014            );
7015        }
7016
7017        let (wire, _runtime) = SessionConfig::default()
7018            .with_mcp_servers(servers)
7019            .into_wire(None)
7020            .expect("into_wire should succeed");
7021        let json = serde_json::to_string(&wire).expect("serialize wire");
7022
7023        let positions: Vec<usize> = order
7024            .iter()
7025            .map(|name| {
7026                json.find(&format!("\"{name}\""))
7027                    .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7028            })
7029            .collect();
7030        let mut ascending = positions.clone();
7031        ascending.sort_unstable();
7032        assert_eq!(
7033            positions, ascending,
7034            "mcp server keys must serialize in insertion order: {json}"
7035        );
7036    }
7037
7038    #[test]
7039    fn infinite_session_config_builder_composes() {
7040        let cfg = InfiniteSessionConfig::new()
7041            .with_enabled(true)
7042            .with_background_compaction_threshold(0.75)
7043            .with_buffer_exhaustion_threshold(0.92);
7044
7045        assert_eq!(cfg.enabled, Some(true));
7046        assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7047        assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7048    }
7049
7050    #[test]
7051    fn provider_config_builder_composes() {
7052        use std::collections::HashMap;
7053
7054        let mut headers = HashMap::new();
7055        headers.insert("X-Custom".to_string(), "value".to_string());
7056
7057        let cfg = ProviderConfig::new("https://api.example.com")
7058            .with_provider_type("openai")
7059            .with_wire_api("completions")
7060            .with_transport("websockets")
7061            .with_api_key("sk-test")
7062            .with_bearer_token("bearer-test")
7063            .with_headers(headers)
7064            .with_model_id("gpt-4")
7065            .with_wire_model("azure-gpt-4-deployment")
7066            .with_max_prompt_tokens(8192)
7067            .with_max_output_tokens(2048);
7068
7069        assert_eq!(cfg.base_url, "https://api.example.com");
7070        assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7071        assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7072        assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7073        assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7074        assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7075        assert_eq!(
7076            cfg.headers
7077                .as_ref()
7078                .and_then(|h| h.get("X-Custom"))
7079                .map(String::as_str),
7080            Some("value"),
7081        );
7082        assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7083        assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7084        assert_eq!(cfg.max_prompt_tokens, Some(8192));
7085        assert_eq!(cfg.max_output_tokens, Some(2048));
7086
7087        // Wire-shape: camelCase, skip_serializing_if when unset.
7088        let wire = serde_json::to_value(&cfg).unwrap();
7089        assert_eq!(wire["modelId"], "gpt-4");
7090        assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7091        assert_eq!(wire["maxPromptTokens"], 8192);
7092        assert_eq!(wire["maxOutputTokens"], 2048);
7093
7094        let unset = ProviderConfig::new("https://api.example.com");
7095        let wire_unset = serde_json::to_value(&unset).unwrap();
7096        assert!(wire_unset.get("modelId").is_none());
7097        assert!(wire_unset.get("wireModel").is_none());
7098        assert!(wire_unset.get("maxPromptTokens").is_none());
7099        assert!(wire_unset.get("maxOutputTokens").is_none());
7100    }
7101
7102    #[test]
7103    fn capi_session_options_builder_composes_and_serializes() {
7104        let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7105
7106        assert_eq!(cfg.enable_web_socket_responses, Some(false));
7107
7108        let wire = serde_json::to_value(&cfg).unwrap();
7109        assert_eq!(
7110            wire,
7111            serde_json::json!({ "enableWebSocketResponses": false })
7112        );
7113
7114        let unset = CapiSessionOptions::new();
7115        let wire_unset = serde_json::to_value(&unset).unwrap();
7116        assert!(wire_unset.get("enableWebSocketResponses").is_none());
7117    }
7118
7119    #[test]
7120    fn session_config_with_capi_serializes() {
7121        let (wire, _) = SessionConfig::default()
7122            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7123            .into_wire(Some(SessionId::from("capi-create")))
7124            .expect("no duplicate handlers");
7125        let json = serde_json::to_value(&wire).unwrap();
7126        assert_eq!(
7127            json["capi"],
7128            serde_json::json!({ "enableWebSocketResponses": false })
7129        );
7130
7131        let (empty_wire, _) = SessionConfig::default()
7132            .into_wire(Some(SessionId::from("capi-create-unset")))
7133            .expect("no duplicate handlers");
7134        let empty_json = serde_json::to_value(&empty_wire).unwrap();
7135        assert!(empty_json.get("capi").is_none());
7136    }
7137
7138    #[test]
7139    fn resume_session_config_with_capi_serializes() {
7140        let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7141            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7142            .into_wire()
7143            .expect("no duplicate handlers");
7144        let json = serde_json::to_value(&wire).unwrap();
7145        assert_eq!(
7146            json["capi"],
7147            serde_json::json!({ "enableWebSocketResponses": false })
7148        );
7149
7150        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7151            .into_wire()
7152            .expect("no duplicate handlers");
7153        let empty_json = serde_json::to_value(&empty_wire).unwrap();
7154        assert!(empty_json.get("capi").is_none());
7155    }
7156
7157    #[test]
7158    fn system_message_config_builder_composes() {
7159        use std::collections::HashMap;
7160
7161        let cfg = SystemMessageConfig::new()
7162            .with_mode("replace")
7163            .with_content("Custom system message.")
7164            .with_sections(HashMap::new());
7165
7166        assert_eq!(cfg.mode.as_deref(), Some("replace"));
7167        assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7168        assert!(cfg.sections.is_some());
7169    }
7170
7171    #[test]
7172    fn delivery_mode_serializes_to_kebab_case_strings() {
7173        assert_eq!(
7174            serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7175            "\"enqueue\""
7176        );
7177        assert_eq!(
7178            serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7179            "\"immediate\""
7180        );
7181        let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7182        assert_eq!(parsed, DeliveryMode::Immediate);
7183    }
7184
7185    #[test]
7186    fn agent_mode_serializes_to_kebab_case_strings() {
7187        assert_eq!(
7188            serde_json::to_string(&AgentMode::Interactive).unwrap(),
7189            "\"interactive\""
7190        );
7191        assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7192        assert_eq!(
7193            serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7194            "\"autopilot\""
7195        );
7196        assert_eq!(
7197            serde_json::to_string(&AgentMode::Shell).unwrap(),
7198            "\"shell\""
7199        );
7200        let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7201        assert_eq!(parsed, AgentMode::Plan);
7202    }
7203
7204    #[test]
7205    fn connection_state_distinguishes_variants() {
7206        // ConnectionState is now an internal type; verify we can construct
7207        // and compare the variants used by the lifecycle code paths.
7208        assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7209    }
7210
7211    /// `agentId` is the sub-agent attribution field added in copilot-sdk
7212    /// commit f8cf846 ("Derive session event envelopes from schema").
7213    /// Every other SDK (Node, Python, Go, .NET) carries it on the event
7214    /// envelope; Rust must too or sub-agent events lose attribution at
7215    /// the deserialization boundary. Cross-SDK parity test.
7216    #[test]
7217    fn session_event_round_trips_agent_id_on_envelope() {
7218        let wire = json!({
7219            "id": "evt-1",
7220            "timestamp": "2026-04-30T12:00:00Z",
7221            "parentId": null,
7222            "agentId": "sub-agent-42",
7223            "type": "assistant.message",
7224            "data": { "message": "hi" }
7225        });
7226
7227        let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7228        assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7229
7230        // Round-trip preserves the field on the wire.
7231        let roundtripped = serde_json::to_value(&event).unwrap();
7232        assert_eq!(roundtripped["agentId"], "sub-agent-42");
7233
7234        // Absent agentId remains absent (skip_serializing_if).
7235        let main_agent_event: SessionEvent = serde_json::from_value(json!({
7236            "id": "evt-2",
7237            "timestamp": "2026-04-30T12:00:01Z",
7238            "parentId": null,
7239            "type": "session.idle",
7240            "data": {}
7241        }))
7242        .unwrap();
7243        assert!(main_agent_event.agent_id.is_none());
7244        let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7245        assert!(roundtripped.get("agentId").is_none());
7246    }
7247
7248    /// Same parity for the typed event envelope produced by the codegen.
7249    #[test]
7250    fn typed_session_event_round_trips_agent_id_on_envelope() {
7251        let wire = json!({
7252            "id": "evt-1",
7253            "timestamp": "2026-04-30T12:00:00Z",
7254            "parentId": null,
7255            "agentId": "sub-agent-42",
7256            "type": "session.idle",
7257            "data": {}
7258        });
7259
7260        let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7261        assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7262
7263        let roundtripped = serde_json::to_value(&event).unwrap();
7264        assert_eq!(roundtripped["agentId"], "sub-agent-42");
7265    }
7266
7267    #[test]
7268    fn connection_state_variants_compile() {
7269        // Defensive smoke test: all variants must be constructable from
7270        // within the crate. (The enum was demoted from pub to pub(crate)
7271        // in Phase D; this test guards against accidental removal.)
7272        let _ = ConnectionState::Disconnected;
7273        let _ = ConnectionState::Connecting;
7274        let _ = ConnectionState::Connected;
7275        let _ = ConnectionState::Error;
7276    }
7277
7278    #[test]
7279    fn deserializes_runtime_attachment_variants() {
7280        let attachments: Vec<Attachment> = serde_json::from_value(json!([
7281            {
7282                "type": "file",
7283                "path": "/tmp/file.rs",
7284                "displayName": "file.rs",
7285                "lineRange": { "start": 7, "end": 12 }
7286            },
7287            {
7288                "type": "directory",
7289                "path": "/tmp/project",
7290                "displayName": "project"
7291            },
7292            {
7293                "type": "selection",
7294                "filePath": "/tmp/lib.rs",
7295                "displayName": "lib.rs",
7296                "text": "fn main() {}",
7297                "selection": {
7298                    "start": { "line": 1, "character": 2 },
7299                    "end": { "line": 3, "character": 4 }
7300                }
7301            },
7302            {
7303                "type": "blob",
7304                "data": "Zm9v",
7305                "mimeType": "image/png",
7306                "displayName": "image.png"
7307            },
7308            {
7309                "type": "github_reference",
7310                "number": 42,
7311                "title": "Fix rendering",
7312                "referenceType": "issue",
7313                "state": "open",
7314                "url": "https://github.com/example/repo/issues/42"
7315            }
7316        ]))
7317        .expect("attachments should deserialize");
7318
7319        assert_eq!(attachments.len(), 5);
7320        assert!(matches!(
7321            &attachments[0],
7322            Attachment::File {
7323                path,
7324                display_name,
7325                line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7326            } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7327        ));
7328        assert!(matches!(
7329            &attachments[1],
7330            Attachment::Directory { path, display_name }
7331                if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7332        ));
7333        assert!(matches!(
7334            &attachments[2],
7335            Attachment::Selection {
7336                file_path,
7337                display_name,
7338                selection:
7339                    AttachmentSelectionRange {
7340                        start: AttachmentSelectionPosition { line: 1, character: 2 },
7341                        end: AttachmentSelectionPosition { line: 3, character: 4 },
7342                    },
7343                ..
7344            } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7345        ));
7346        assert!(matches!(
7347            &attachments[3],
7348            Attachment::Blob {
7349                data,
7350                mime_type,
7351                display_name,
7352            } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7353        ));
7354        assert!(matches!(
7355            &attachments[4],
7356            Attachment::GitHubReference {
7357                number: 42,
7358                title,
7359                reference_type: GitHubReferenceType::Issue,
7360                state,
7361                url,
7362            } if title == "Fix rendering"
7363                && state == "open"
7364                && url == "https://github.com/example/repo/issues/42"
7365        ));
7366    }
7367
7368    #[test]
7369    fn ensures_display_names_for_variants_that_support_them() {
7370        let mut attachments = vec![
7371            Attachment::File {
7372                path: PathBuf::from("/tmp/file.rs"),
7373                display_name: None,
7374                line_range: None,
7375            },
7376            Attachment::Selection {
7377                file_path: PathBuf::from("/tmp/src/lib.rs"),
7378                display_name: None,
7379                text: "fn main() {}".to_string(),
7380                selection: AttachmentSelectionRange {
7381                    start: AttachmentSelectionPosition {
7382                        line: 0,
7383                        character: 0,
7384                    },
7385                    end: AttachmentSelectionPosition {
7386                        line: 0,
7387                        character: 10,
7388                    },
7389                },
7390            },
7391            Attachment::Blob {
7392                data: "Zm9v".to_string(),
7393                mime_type: "image/png".to_string(),
7394                display_name: None,
7395            },
7396            Attachment::GitHubReference {
7397                number: 7,
7398                title: "Track regressions".to_string(),
7399                reference_type: GitHubReferenceType::Issue,
7400                state: "open".to_string(),
7401                url: "https://example.com/issues/7".to_string(),
7402            },
7403        ];
7404
7405        ensure_attachment_display_names(&mut attachments);
7406
7407        assert_eq!(attachments[0].display_name(), Some("file.rs"));
7408        assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7409        assert_eq!(attachments[2].display_name(), Some("attachment"));
7410        assert_eq!(attachments[3].display_name(), None);
7411        assert_eq!(
7412            attachments[3].label(),
7413            Some("Track regressions".to_string())
7414        );
7415    }
7416
7417    #[test]
7418    fn github_anchored_attachment_variants_round_trip() {
7419        let cases = vec![
7420            (
7421                "github_commit",
7422                json!({
7423                    "type": "github_commit",
7424                    "message": "Fix the thing",
7425                    "oid": "abc123",
7426                    "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7427                    "url": "https://github.com/octocat/repo/commit/abc123"
7428                }),
7429            ),
7430            (
7431                "github_release",
7432                json!({
7433                    "type": "github_release",
7434                    "name": "v1.2.3",
7435                    "repo": { "name": "repo", "owner": "octocat" },
7436                    "tagName": "v1.2.3",
7437                    "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7438                }),
7439            ),
7440            (
7441                "github_actions_job",
7442                json!({
7443                    "type": "github_actions_job",
7444                    "conclusion": "failure",
7445                    "jobId": 99,
7446                    "jobName": "build",
7447                    "repo": { "name": "repo", "owner": "octocat" },
7448                    "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7449                    "workflowName": "CI"
7450                }),
7451            ),
7452            (
7453                "github_repository",
7454                json!({
7455                    "type": "github_repository",
7456                    "description": "An example repository",
7457                    "ref": "main",
7458                    "repo": { "name": "repo", "owner": "octocat" },
7459                    "url": "https://github.com/octocat/repo"
7460                }),
7461            ),
7462            (
7463                "github_file_diff",
7464                json!({
7465                    "type": "github_file_diff",
7466                    "base": {
7467                        "path": "src/lib.rs",
7468                        "ref": "main",
7469                        "repo": { "name": "repo", "owner": "octocat" }
7470                    },
7471                    "head": {
7472                        "path": "src/lib.rs",
7473                        "ref": "feature",
7474                        "repo": { "name": "repo", "owner": "octocat" }
7475                    },
7476                    "url": "https://github.com/octocat/repo/compare/main...feature"
7477                }),
7478            ),
7479            (
7480                "github_tree_comparison",
7481                json!({
7482                    "type": "github_tree_comparison",
7483                    "base": {
7484                        "repo": { "name": "repo", "owner": "octocat" },
7485                        "revision": "main"
7486                    },
7487                    "head": {
7488                        "repo": { "name": "repo", "owner": "octocat" },
7489                        "revision": "feature"
7490                    },
7491                    "url": "https://github.com/octocat/repo/compare/main...feature"
7492                }),
7493            ),
7494            (
7495                "github_url",
7496                json!({
7497                    "type": "github_url",
7498                    "url": "https://github.com/octocat/repo/wiki"
7499                }),
7500            ),
7501            (
7502                "github_file",
7503                json!({
7504                    "type": "github_file",
7505                    "path": "src/main.rs",
7506                    "ref": "main",
7507                    "repo": { "name": "repo", "owner": "octocat" },
7508                    "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7509                }),
7510            ),
7511            (
7512                "github_snippet",
7513                json!({
7514                    "type": "github_snippet",
7515                    "lineRange": { "start": 10, "end": 20 },
7516                    "path": "src/main.rs",
7517                    "ref": "main",
7518                    "repo": { "name": "repo", "owner": "octocat" },
7519                    "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7520                }),
7521            ),
7522        ];
7523
7524        for (expected_type, input) in cases {
7525            let attachment: Attachment = serde_json::from_value(input.clone())
7526                .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7527
7528            // Serialize to a string first: parsing into `serde_json::Value` would
7529            // silently dedupe a duplicate `type` key, hiding the exact regression
7530            // this test guards against (e.g. a wrapped generated struct emitting its
7531            // own `type` alongside the enum tag).
7532            let serialized_string = serde_json::to_string(&attachment)
7533                .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7534
7535            // Exactly one `type` key, carrying the expected discriminator.
7536            assert_eq!(
7537                serialized_string.matches("\"type\":").count(),
7538                1,
7539                "{expected_type} must serialize a single `type` key"
7540            );
7541
7542            let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7543                .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7544            assert_eq!(
7545                serialized.get("type").and_then(|value| value.as_str()),
7546                Some(expected_type),
7547                "{expected_type} must serialize the correct discriminator"
7548            );
7549
7550            // Round-trips without dropping fields.
7551            assert_eq!(
7552                serialized, input,
7553                "{expected_type} should round-trip without data loss"
7554            );
7555            let reparsed: Attachment = serde_json::from_value(serialized)
7556                .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7557            assert_eq!(
7558                reparsed, attachment,
7559                "{expected_type} should re-deserialize to the same value"
7560            );
7561        }
7562    }
7563}
7564
7565#[cfg(test)]
7566mod permission_builder_tests {
7567    use std::sync::Arc;
7568
7569    use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7570    use crate::permission;
7571    use crate::types::{
7572        PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7573        SessionId,
7574    };
7575
7576    fn data() -> PermissionRequestData {
7577        PermissionRequestData {
7578            extra: serde_json::json!({"tool": "shell"}),
7579            ..Default::default()
7580        }
7581    }
7582
7583    /// Apply the same policy-resolution logic that `Client::create_session`
7584    /// uses, so tests exercise the effective handler.
7585    fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7586        permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7587    }
7588
7589    fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7590        permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7591    }
7592
7593    async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7594        handler
7595            .handle(SessionId::from("s1"), RequestId::new("1"), data())
7596            .await
7597    }
7598
7599    #[tokio::test]
7600    async fn approve_all_with_handler_present_approves() {
7601        let cfg = SessionConfig::default()
7602            .with_permission_handler(Arc::new(ApproveAllHandler))
7603            .approve_all_permissions();
7604        let h = resolve_create(cfg).expect("policy + handler yields handler");
7605        assert!(matches!(
7606            dispatch(&h).await,
7607            PermissionResult::Decision {
7608                decision: PermissionDecision::ApproveOnce(_),
7609                ..
7610            }
7611        ));
7612    }
7613
7614    #[tokio::test]
7615    async fn approve_all_standalone_produces_handler() {
7616        let cfg = SessionConfig::default().approve_all_permissions();
7617        let h = resolve_create(cfg).expect("policy alone yields handler");
7618        assert!(matches!(
7619            dispatch(&h).await,
7620            PermissionResult::Decision {
7621                decision: PermissionDecision::ApproveOnce(_),
7622                ..
7623            }
7624        ));
7625    }
7626
7627    /// Phase I: order between with_permission_handler and the policy
7628    /// builder must not matter.
7629    #[tokio::test]
7630    async fn approve_all_is_order_independent() {
7631        let a = SessionConfig::default()
7632            .with_permission_handler(Arc::new(ApproveAllHandler))
7633            .approve_all_permissions();
7634        let b = SessionConfig::default()
7635            .approve_all_permissions()
7636            .with_permission_handler(Arc::new(ApproveAllHandler));
7637        let ha = resolve_create(a).unwrap();
7638        let hb = resolve_create(b).unwrap();
7639        assert!(matches!(
7640            dispatch(&ha).await,
7641            PermissionResult::Decision {
7642                decision: PermissionDecision::ApproveOnce(_),
7643                ..
7644            }
7645        ));
7646        assert!(matches!(
7647            dispatch(&hb).await,
7648            PermissionResult::Decision {
7649                decision: PermissionDecision::ApproveOnce(_),
7650                ..
7651            }
7652        ));
7653    }
7654
7655    #[tokio::test]
7656    async fn deny_all_is_order_independent() {
7657        let a = SessionConfig::default()
7658            .with_permission_handler(Arc::new(ApproveAllHandler))
7659            .deny_all_permissions();
7660        let b = SessionConfig::default()
7661            .deny_all_permissions()
7662            .with_permission_handler(Arc::new(ApproveAllHandler));
7663        let ha = resolve_create(a).unwrap();
7664        let hb = resolve_create(b).unwrap();
7665        assert!(matches!(
7666            dispatch(&ha).await,
7667            PermissionResult::Decision {
7668                decision: PermissionDecision::Reject(_),
7669                ..
7670            }
7671        ));
7672        assert!(matches!(
7673            dispatch(&hb).await,
7674            PermissionResult::Decision {
7675                decision: PermissionDecision::Reject(_),
7676                ..
7677            }
7678        ));
7679    }
7680
7681    #[tokio::test]
7682    async fn approve_permissions_if_consults_predicate() {
7683        let cfg = SessionConfig::default().approve_permissions_if(|d| {
7684            d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7685        });
7686        let h = resolve_create(cfg).unwrap();
7687        assert!(matches!(
7688            dispatch(&h).await,
7689            PermissionResult::Decision {
7690                decision: PermissionDecision::Reject(_),
7691                ..
7692            }
7693        ));
7694    }
7695
7696    #[tokio::test]
7697    async fn approve_permissions_if_is_order_independent() {
7698        let predicate = |d: &PermissionRequestData| {
7699            d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7700        };
7701        let a = SessionConfig::default()
7702            .with_permission_handler(Arc::new(ApproveAllHandler))
7703            .approve_permissions_if(predicate);
7704        let b = SessionConfig::default()
7705            .approve_permissions_if(predicate)
7706            .with_permission_handler(Arc::new(ApproveAllHandler));
7707        let ha = resolve_create(a).unwrap();
7708        let hb = resolve_create(b).unwrap();
7709        assert!(matches!(
7710            dispatch(&ha).await,
7711            PermissionResult::Decision {
7712                decision: PermissionDecision::Reject(_),
7713                ..
7714            }
7715        ));
7716        assert!(matches!(
7717            dispatch(&hb).await,
7718            PermissionResult::Decision {
7719                decision: PermissionDecision::Reject(_),
7720                ..
7721            }
7722        ));
7723    }
7724
7725    #[tokio::test]
7726    async fn resume_session_config_approve_all_works() {
7727        let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7728            .with_permission_handler(Arc::new(ApproveAllHandler))
7729            .approve_all_permissions();
7730        let h = resolve_resume(cfg).unwrap();
7731        assert!(matches!(
7732            dispatch(&h).await,
7733            PermissionResult::Decision {
7734                decision: PermissionDecision::ApproveOnce(_),
7735                ..
7736            }
7737        ));
7738    }
7739
7740    #[tokio::test]
7741    async fn resume_session_config_approve_all_is_order_independent() {
7742        let a = ResumeSessionConfig::new(SessionId::from("s1"))
7743            .with_permission_handler(Arc::new(ApproveAllHandler))
7744            .approve_all_permissions();
7745        let b = ResumeSessionConfig::new(SessionId::from("s1"))
7746            .approve_all_permissions()
7747            .with_permission_handler(Arc::new(ApproveAllHandler));
7748        let ha = resolve_resume(a).unwrap();
7749        let hb = resolve_resume(b).unwrap();
7750        assert!(matches!(
7751            dispatch(&ha).await,
7752            PermissionResult::Decision {
7753                decision: PermissionDecision::ApproveOnce(_),
7754                ..
7755            }
7756        ));
7757        assert!(matches!(
7758            dispatch(&hb).await,
7759            PermissionResult::Decision {
7760                decision: PermissionDecision::ApproveOnce(_),
7761                ..
7762            }
7763        ));
7764    }
7765
7766    #[test]
7767    fn session_config_enable_experimental_mode_serializes_when_set() {
7768        let cfg = SessionConfig::default().with_enable_experimental_mode(false);
7769        assert_eq!(cfg.enable_experimental_mode, Some(false));
7770
7771        let (wire, _runtime) = cfg
7772            .into_wire(Some(SessionId::from("experimental-mode")))
7773            .expect("enable_experimental_mode config has no duplicate handlers");
7774        assert_eq!(wire.is_experimental_mode, Some(false));
7775
7776        let json = serde_json::to_value(&wire).unwrap();
7777        assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7778    }
7779
7780    #[test]
7781    fn session_config_enable_experimental_mode_omitted_when_none() {
7782        let cfg = SessionConfig::default();
7783        assert_eq!(cfg.enable_experimental_mode, None);
7784
7785        let (wire, _runtime) = cfg
7786            .into_wire(Some(SessionId::from("no-experimental-mode")))
7787            .expect("default config has no duplicate handlers");
7788        assert_eq!(wire.is_experimental_mode, None);
7789
7790        let json = serde_json::to_value(&wire).unwrap();
7791        assert!(json.get("isExperimentalMode").is_none());
7792    }
7793
7794    #[test]
7795    fn resume_session_config_enable_experimental_mode_serializes_when_set() {
7796        let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
7797            .with_enable_experimental_mode(false);
7798        assert_eq!(cfg.enable_experimental_mode, Some(false));
7799
7800        let (wire, _runtime) = cfg
7801            .into_wire()
7802            .expect("resume enable_experimental_mode config has no duplicate handlers");
7803        assert_eq!(wire.is_experimental_mode, Some(false));
7804
7805        let json = serde_json::to_value(&wire).unwrap();
7806        assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7807    }
7808
7809    #[test]
7810    fn resume_session_config_enable_experimental_mode_omitted_when_none() {
7811        let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
7812        assert_eq!(cfg.enable_experimental_mode, None);
7813
7814        let (wire, _runtime) = cfg
7815            .into_wire()
7816            .expect("default resume config has no duplicate handlers");
7817        assert_eq!(wire.is_experimental_mode, None);
7818
7819        let json = serde_json::to_value(&wire).unwrap();
7820        assert!(json.get("isExperimentalMode").is_none());
7821    }
7822}
7823
7824#[cfg(test)]
7825mod is_terminal_tests {
7826    use super::Tool;
7827
7828    #[test]
7829    fn is_terminal_serializes_as_camel_case_when_set() {
7830        let tool = Tool {
7831            name: "clear_context".to_owned(),
7832            is_terminal: true,
7833            ..Default::default()
7834        };
7835        let value = serde_json::to_value(&tool).expect("tool serializes");
7836        assert_eq!(
7837            value.get("isTerminal"),
7838            Some(&serde_json::Value::Bool(true))
7839        );
7840    }
7841
7842    #[test]
7843    fn is_terminal_is_omitted_when_false() {
7844        let tool = Tool {
7845            name: "plain".to_owned(),
7846            ..Default::default()
7847        };
7848        let value = serde_json::to_value(&tool).expect("tool serializes");
7849        assert!(value.get("isTerminal").is_none());
7850    }
7851
7852    /// `Tool` has a hand-written `Debug` impl, so a new field is only reported
7853    /// if it is added there by hand. Guard against that drift.
7854    #[test]
7855    fn is_terminal_appears_in_debug_output() {
7856        let terminal = Tool {
7857            name: "clear_context".to_owned(),
7858            is_terminal: true,
7859            ..Default::default()
7860        };
7861        assert!(format!("{terminal:?}").contains("is_terminal: true"));
7862
7863        let plain = Tool {
7864            name: "plain".to_owned(),
7865            ..Default::default()
7866        };
7867        assert!(format!("{plain:?}").contains("is_terminal: false"));
7868    }
7869}