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