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