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