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