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