Skip to main content

github_copilot_sdk/
types.rs

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