Skip to main content

github_copilot_sdk/
types.rs

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