Skip to main content

github_copilot_sdk/
types.rs

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