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