Skip to main content

github_copilot_sdk/
types.rs

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