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