Skip to main content

github_copilot_sdk/
types.rs

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