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