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    /// Context captured from an extension-owned canvas.
5159    #[serde(rename = "extension_context")]
5160    ExtensionContext {
5161        /// ISO 8601 timestamp when the context was captured.
5162        captured_at: String,
5163        /// Extension that owns the canvas.
5164        extension_id: String,
5165        /// Canvas declaration identifier when the context is bound to a canvas.
5166        #[serde(skip_serializing_if = "Option::is_none")]
5167        canvas_id: Option<String>,
5168        /// Open canvas instance identifier when the context is bound to a canvas.
5169        #[serde(skip_serializing_if = "Option::is_none")]
5170        instance_id: Option<String>,
5171        /// Human-readable context title.
5172        title: String,
5173        /// Extension-defined structured context payload.
5174        #[serde(skip_serializing_if = "Option::is_none")]
5175        payload: Option<Value>,
5176    },
5177    /// A reference to a GitHub issue, PR, or discussion.
5178    #[serde(rename = "github_reference")]
5179    GitHubReference {
5180        /// Issue/PR/discussion number.
5181        number: u64,
5182        /// Title of the referenced item.
5183        title: String,
5184        /// Kind of reference.
5185        reference_type: GitHubReferenceType,
5186        /// Current state (e.g. "open", "closed").
5187        state: String,
5188        /// URL to the referenced item.
5189        url: String,
5190    },
5191    /// A pointer to a GitHub commit.
5192    #[serde(rename = "github_commit")]
5193    GitHubCommit {
5194        /// First line of the commit message.
5195        message: String,
5196        /// Full commit SHA.
5197        oid: String,
5198        /// Repository the commit belongs to.
5199        repo: GitHubRepoPointer,
5200        /// URL to the commit on GitHub.
5201        url: String,
5202    },
5203    /// A pointer to a GitHub release.
5204    #[serde(rename = "github_release")]
5205    GitHubRelease {
5206        /// Human-readable release name.
5207        name: String,
5208        /// Repository the release belongs to.
5209        repo: GitHubRepoPointer,
5210        /// Git tag the release is anchored to.
5211        tag_name: String,
5212        /// URL to the release on GitHub.
5213        url: String,
5214    },
5215    /// A pointer to a GitHub Actions job.
5216    #[serde(rename = "github_actions_job")]
5217    GitHubActionsJob {
5218        /// Terminal conclusion of the job when finished (e.g. "success",
5219        /// "failure", "cancelled"). Absent for in-progress jobs.
5220        #[serde(skip_serializing_if = "Option::is_none")]
5221        conclusion: Option<String>,
5222        /// Job id within the workflow run.
5223        job_id: i64,
5224        /// Display name of the job.
5225        job_name: String,
5226        /// Repository the workflow run belongs to.
5227        repo: GitHubRepoPointer,
5228        /// URL to the job on GitHub.
5229        url: String,
5230        /// Display name of the workflow the job ran in.
5231        workflow_name: String,
5232    },
5233    /// A pointer to a GitHub repository.
5234    #[serde(rename = "github_repository")]
5235    GitHubRepository {
5236        /// Short description of the repository.
5237        #[serde(skip_serializing_if = "Option::is_none")]
5238        description: Option<String>,
5239        /// Git ref this attachment is anchored at (branch, tag, or commit).
5240        /// When absent the default branch is implied.
5241        #[serde(skip_serializing_if = "Option::is_none")]
5242        r#ref: Option<String>,
5243        /// Repository pointer.
5244        repo: GitHubRepoPointer,
5245        /// URL to the repository on GitHub.
5246        url: String,
5247    },
5248    /// A pointer to a single-file diff. At least one of `head` and `base` is present.
5249    #[serde(rename = "github_file_diff")]
5250    GitHubFileDiff {
5251        /// File location on the base side of the diff. Absent for additions.
5252        #[serde(skip_serializing_if = "Option::is_none")]
5253        base: Option<GitHubFileDiffSide>,
5254        /// File location on the head side of the diff. Absent for deletions.
5255        #[serde(skip_serializing_if = "Option::is_none")]
5256        head: Option<GitHubFileDiffSide>,
5257        /// URL to the diff on GitHub (e.g. a commit, compare, or PR-file URL).
5258        url: String,
5259    },
5260    /// A pointer to a comparison between two git revisions.
5261    #[serde(rename = "github_tree_comparison")]
5262    GitHubTreeComparison {
5263        /// Base side of the comparison.
5264        base: GitHubTreeComparisonSide,
5265        /// Head side of the comparison.
5266        head: GitHubTreeComparisonSide,
5267        /// URL to the comparison on GitHub.
5268        url: String,
5269    },
5270    /// A generic GitHub URL reference.
5271    #[serde(rename = "github_url")]
5272    GitHubUrl {
5273        /// URL to the GitHub resource.
5274        url: String,
5275    },
5276    /// A pointer to a file in a GitHub repository at a specific ref.
5277    #[serde(rename = "github_file")]
5278    GitHubFile {
5279        /// Repository-relative path to the file.
5280        path: String,
5281        /// Git ref the file is read at (branch, tag, or commit SHA).
5282        r#ref: String,
5283        /// Repository the file lives in.
5284        repo: GitHubRepoPointer,
5285        /// URL to the file on GitHub.
5286        url: String,
5287    },
5288    /// A pointer to a line range inside a file in a GitHub repository.
5289    #[serde(rename = "github_snippet")]
5290    GitHubSnippet {
5291        /// Line range the snippet covers.
5292        line_range: GitHubSnippetLineRange,
5293        /// Repository-relative path to the file.
5294        path: String,
5295        /// Git ref the file is read at (branch, tag, or commit SHA).
5296        r#ref: String,
5297        /// Repository the file lives in.
5298        repo: GitHubRepoPointer,
5299        /// URL to the snippet on GitHub (with line anchor).
5300        url: String,
5301    },
5302}
5303
5304impl Attachment {
5305    /// Returns the display name, if set.
5306    pub fn display_name(&self) -> Option<&str> {
5307        match self {
5308            Self::File { display_name, .. }
5309            | Self::Directory { display_name, .. }
5310            | Self::Selection { display_name, .. }
5311            | Self::Blob { display_name, .. } => display_name.as_deref(),
5312            Self::GitHubReference { .. }
5313            | Self::GitHubCommit { .. }
5314            | Self::GitHubRelease { .. }
5315            | Self::GitHubActionsJob { .. }
5316            | Self::GitHubRepository { .. }
5317            | Self::GitHubFileDiff { .. }
5318            | Self::GitHubTreeComparison { .. }
5319            | Self::GitHubUrl { .. }
5320            | Self::GitHubFile { .. }
5321            | Self::GitHubSnippet { .. }
5322            | Self::ExtensionContext { .. } => None,
5323        }
5324    }
5325
5326    /// Returns a human-readable label, deriving one from the path if needed.
5327    pub fn label(&self) -> Option<String> {
5328        if let Some(display_name) = self
5329            .display_name()
5330            .map(str::trim)
5331            .filter(|name| !name.is_empty())
5332        {
5333            return Some(display_name.to_string());
5334        }
5335
5336        match self {
5337            Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5338                format!("#{}", number)
5339            } else {
5340                title.trim().to_string()
5341            }),
5342            Self::ExtensionContext { title, .. } if !title.trim().is_empty() => {
5343                Some(title.trim().to_string())
5344            }
5345            _ => self.derived_display_name(),
5346        }
5347    }
5348
5349    /// Ensure `display_name` is populated when the variant supports one.
5350    pub fn ensure_display_name(&mut self) {
5351        if self
5352            .display_name()
5353            .map(str::trim)
5354            .is_some_and(|name| !name.is_empty())
5355        {
5356            return;
5357        }
5358
5359        let Some(derived_display_name) = self.derived_display_name() else {
5360            return;
5361        };
5362
5363        match self {
5364            Self::File { display_name, .. }
5365            | Self::Directory { display_name, .. }
5366            | Self::Selection { display_name, .. }
5367            | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5368            Self::GitHubReference { .. }
5369            | Self::GitHubCommit { .. }
5370            | Self::GitHubRelease { .. }
5371            | Self::GitHubActionsJob { .. }
5372            | Self::GitHubRepository { .. }
5373            | Self::GitHubFileDiff { .. }
5374            | Self::GitHubTreeComparison { .. }
5375            | Self::GitHubUrl { .. }
5376            | Self::GitHubFile { .. }
5377            | Self::GitHubSnippet { .. }
5378            | Self::ExtensionContext { .. } => {}
5379        }
5380    }
5381
5382    fn derived_display_name(&self) -> Option<String> {
5383        match self {
5384            Self::File { path, .. } | Self::Directory { path, .. } => {
5385                Some(attachment_name_from_path(path))
5386            }
5387            Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5388            Self::Blob { .. } => Some("attachment".to_string()),
5389            Self::GitHubReference { .. }
5390            | Self::GitHubCommit { .. }
5391            | Self::GitHubRelease { .. }
5392            | Self::GitHubActionsJob { .. }
5393            | Self::GitHubRepository { .. }
5394            | Self::GitHubFileDiff { .. }
5395            | Self::GitHubTreeComparison { .. }
5396            | Self::GitHubUrl { .. }
5397            | Self::GitHubFile { .. }
5398            | Self::GitHubSnippet { .. }
5399            | Self::ExtensionContext { .. } => None,
5400        }
5401    }
5402}
5403
5404fn attachment_name_from_path(path: &Path) -> String {
5405    path.file_name()
5406        .map(|name| name.to_string_lossy().into_owned())
5407        .filter(|name| !name.is_empty())
5408        .unwrap_or_else(|| {
5409            let full = path.to_string_lossy();
5410            if full.is_empty() {
5411                "attachment".to_string()
5412            } else {
5413                full.into_owned()
5414            }
5415        })
5416}
5417
5418/// Normalize a list of attachments so every entry has a `display_name`.
5419pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5420    for attachment in attachments {
5421        attachment.ensure_display_name();
5422    }
5423}
5424
5425/// Provenance of a message sent through `session.send`.
5426///
5427/// Source is independent of delivery mode. Leaving [`MessageOptions::source`]
5428/// unset omits the field and preserves the runtime's default for user messages.
5429#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5430#[non_exhaustive]
5431pub enum MessageSource {
5432    /// A message from a human user.
5433    User,
5434    /// An automated message from the integrating application.
5435    System,
5436    /// A message from the agent with this opaque sender ID.
5437    Agent(String),
5438}
5439
5440impl std::fmt::Display for MessageSource {
5441    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5442        match self {
5443            Self::User => f.write_str("user"),
5444            Self::System => f.write_str("system"),
5445            Self::Agent(id) => write!(f, "agent-{id}"),
5446        }
5447    }
5448}
5449
5450impl Serialize for MessageSource {
5451    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
5452        serializer.collect_str(self)
5453    }
5454}
5455
5456impl<'de> Deserialize<'de> for MessageSource {
5457    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5458        let value = String::deserialize(deserializer)?;
5459        match value.as_str() {
5460            "user" => Ok(Self::User),
5461            "system" => Ok(Self::System),
5462            value => value
5463                .strip_prefix("agent-")
5464                .map(|id| Self::Agent(id.to_owned()))
5465                .ok_or_else(|| serde::de::Error::custom("expected user, system, or agent-<id>")),
5466        }
5467    }
5468}
5469
5470/// Message delivery mode for [`MessageOptions::mode`].
5471///
5472/// Controls how a prompt is delivered relative to in-flight session work.
5473/// Wire values: `"enqueue"` and `"immediate"`.
5474#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5475#[serde(rename_all = "lowercase")]
5476#[non_exhaustive]
5477pub enum DeliveryMode {
5478    /// Queue the prompt behind any in-flight work (default).
5479    Enqueue,
5480    /// Interrupt the session and run the prompt immediately.
5481    Immediate,
5482}
5483
5484/// The UI mode the agent is in for a given turn, used by
5485/// [`MessageOptions::agent_mode`].
5486///
5487/// Wire values: `"interactive"`, `"plan"`, `"autopilot"`, `"shell"`.
5488#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5489#[serde(rename_all = "lowercase")]
5490#[non_exhaustive]
5491pub enum AgentMode {
5492    /// The agent is responding interactively to the user.
5493    Interactive,
5494    /// The agent is preparing a plan before making changes.
5495    Plan,
5496    /// The agent is working autonomously toward task completion.
5497    Autopilot,
5498    /// The agent is in shell-focused UI mode.
5499    Shell,
5500}
5501
5502/// Options for sending a user message to the agent.
5503///
5504/// Used by both [`Session::send`](crate::session::Session::send) and
5505/// [`Session::send_and_wait`](crate::session::Session::send_and_wait); the
5506/// `wait_timeout` field is honored only by `send_and_wait` and is ignored by
5507/// `send`.
5508///
5509/// `MessageOptions` is `#[non_exhaustive]` and constructed via [`MessageOptions::new`]
5510/// plus the `with_*` chain so future fields can land without breaking callers.
5511/// For the trivial case, both `&str` and `String` implement `Into<MessageOptions>`,
5512/// so:
5513///
5514/// ```no_run
5515/// # use github_copilot_sdk::session::Session;
5516/// # async fn run(session: Session) -> Result<(), github_copilot_sdk::Error> {
5517/// session.send("hello").await?;
5518/// # Ok(()) }
5519/// ```
5520///
5521/// is equivalent to:
5522///
5523/// ```no_run
5524/// # use github_copilot_sdk::session::Session;
5525/// # use github_copilot_sdk::types::MessageOptions;
5526/// # async fn run(session: Session) -> Result<(), github_copilot_sdk::Error> {
5527/// session.send(MessageOptions::new("hello")).await?;
5528/// # Ok(()) }
5529/// ```
5530#[derive(Debug, Clone)]
5531#[non_exhaustive]
5532pub struct MessageOptions {
5533    /// Per-run JSON Schema. Independent sends and subagents do not inherit it.
5534    /// Immediate steering must not specify a schema. Streaming events remain text.
5535    pub response_schema: Option<Value>,
5536    /// The user prompt to send.
5537    pub prompt: String,
5538    /// Optional message provenance. When `None`, the field is omitted,
5539    /// preserving the runtime's default for user messages.
5540    pub source: Option<MessageSource>,
5541    /// Optional message delivery mode for this turn.
5542    ///
5543    /// Controls whether the prompt is queued behind in-flight work
5544    /// ([`DeliveryMode::Enqueue`], default) or interrupts the session and
5545    /// runs immediately ([`DeliveryMode::Immediate`]).
5546    pub mode: Option<DeliveryMode>,
5547    /// Optional UI mode the agent was in when this message was sent
5548    /// (for example [`AgentMode::Plan`] or [`AgentMode::Autopilot`]).
5549    /// Defaults to the session's current mode when `None`.
5550    pub agent_mode: Option<AgentMode>,
5551    /// Optional attachments to include with the message.
5552    pub attachments: Option<Vec<Attachment>>,
5553    /// Maximum time to wait for the session to go idle. Honored only by
5554    /// `send_and_wait`. Defaults to 60 seconds when unset.
5555    pub wait_timeout: Option<Duration>,
5556    /// Custom HTTP headers to include in outbound model requests for this
5557    /// turn. When `None` or empty, no `requestHeaders` field is sent on
5558    /// the wire.
5559    pub request_headers: Option<HashMap<String, String>>,
5560    /// W3C Trace Context `traceparent` header for this turn.
5561    ///
5562    /// Per-turn override that takes precedence over
5563    /// [`ClientOptions::on_get_trace_context`](crate::ClientOptions::on_get_trace_context).
5564    /// When `None`, the SDK falls back to the provider (if configured)
5565    /// before omitting the field.
5566    pub traceparent: Option<String>,
5567    /// W3C Trace Context `tracestate` header for this turn.
5568    ///
5569    /// Per-turn override paired with [`traceparent`](Self::traceparent).
5570    pub tracestate: Option<String>,
5571    /// If provided, this is shown in the timeline instead of `prompt`.
5572    pub display_prompt: Option<String>,
5573}
5574
5575impl MessageOptions {
5576    /// Build a new `MessageOptions` with just a prompt.
5577    pub fn new(prompt: impl Into<String>) -> Self {
5578        Self {
5579            prompt: prompt.into(),
5580            response_schema: None,
5581            source: None,
5582            mode: None,
5583            agent_mode: None,
5584            attachments: None,
5585            wait_timeout: None,
5586            request_headers: None,
5587            traceparent: None,
5588            tracestate: None,
5589            display_prompt: None,
5590        }
5591    }
5592
5593    /// Set the message provenance without changing its delivery mode.
5594    pub fn with_source(mut self, source: MessageSource) -> Self {
5595        self.source = Some(source);
5596        self
5597    }
5598
5599    /// Request provider-native structured output for this run.
5600    pub fn with_response_schema(mut self, schema: Value) -> Self {
5601        self.response_schema = Some(schema);
5602        self
5603    }
5604
5605    /// Set the message delivery mode for this turn.
5606    ///
5607    /// Pass [`DeliveryMode::Immediate`] to interrupt the session and run
5608    /// the prompt now; the default ([`DeliveryMode::Enqueue`]) queues the
5609    /// prompt behind in-flight work.
5610    pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5611        self.mode = Some(mode);
5612        self
5613    }
5614
5615    /// Set the per-message agent UI mode for this turn.
5616    ///
5617    /// When `None`, the session's current mode is used.
5618    pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5619        self.agent_mode = Some(agent_mode);
5620        self
5621    }
5622
5623    /// Attach files / selections / blobs to the message.
5624    pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5625        self.attachments = Some(attachments);
5626        self
5627    }
5628
5629    /// Override the default 60-second wait timeout for `send_and_wait`.
5630    pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5631        self.wait_timeout = Some(timeout);
5632        self
5633    }
5634
5635    /// Set custom HTTP headers for outbound model requests for this turn.
5636    pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5637        self.request_headers = Some(headers);
5638        self
5639    }
5640
5641    /// Set both `traceparent` and `tracestate` from a [`TraceContext`].
5642    /// Either field may remain `None` if the [`TraceContext`] has no value
5643    /// for it. Use [`with_traceparent`](Self::with_traceparent) or
5644    /// [`with_tracestate`](Self::with_tracestate) to set them individually.
5645    pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5646        self.traceparent = ctx.traceparent;
5647        self.tracestate = ctx.tracestate;
5648        self
5649    }
5650
5651    /// Set the W3C `traceparent` header for this turn.
5652    pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5653        self.traceparent = Some(traceparent.into());
5654        self
5655    }
5656
5657    /// Set the W3C `tracestate` header for this turn.
5658    pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5659        self.tracestate = Some(tracestate.into());
5660        self
5661    }
5662
5663    /// Set the display prompt shown in the timeline instead of `prompt`.
5664    pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5665        self.display_prompt = Some(display_prompt.into());
5666        self
5667    }
5668}
5669
5670impl From<&str> for MessageOptions {
5671    fn from(prompt: &str) -> Self {
5672        Self::new(prompt)
5673    }
5674}
5675
5676impl From<String> for MessageOptions {
5677    fn from(prompt: String) -> Self {
5678        Self::new(prompt)
5679    }
5680}
5681
5682impl From<&String> for MessageOptions {
5683    fn from(prompt: &String) -> Self {
5684        Self::new(prompt.clone())
5685    }
5686}
5687
5688/// Response from [`Client::get_status`](crate::Client::get_status).
5689#[derive(Debug, Clone, Serialize, Deserialize)]
5690#[serde(rename_all = "camelCase")]
5691#[non_exhaustive]
5692pub struct GetStatusResponse {
5693    /// Package version (e.g. `"1.0.0"`).
5694    pub version: String,
5695    /// Protocol version for SDK compatibility.
5696    pub protocol_version: u32,
5697}
5698
5699/// Response from [`Client::get_auth_status`](crate::Client::get_auth_status).
5700#[derive(Debug, Clone, Serialize, Deserialize)]
5701#[serde(rename_all = "camelCase")]
5702#[non_exhaustive]
5703pub struct GetAuthStatusResponse {
5704    /// Whether the user is authenticated.
5705    pub is_authenticated: bool,
5706    /// Authentication type (e.g. `"user"`, `"env"`, `"gh-cli"`, `"hmac"`,
5707    /// `"api-key"`, `"token"`).
5708    #[serde(skip_serializing_if = "Option::is_none")]
5709    pub auth_type: Option<String>,
5710    /// GitHub host URL.
5711    #[serde(skip_serializing_if = "Option::is_none")]
5712    pub host: Option<String>,
5713    /// User login name.
5714    #[serde(skip_serializing_if = "Option::is_none")]
5715    pub login: Option<String>,
5716    /// Human-readable status message.
5717    #[serde(skip_serializing_if = "Option::is_none")]
5718    pub status_message: Option<String>,
5719}
5720
5721/// Wrapper for session event notifications received from the CLI.
5722///
5723/// The CLI sends these as JSON-RPC notifications on the `session.event` method.
5724#[derive(Debug, Clone, Serialize, Deserialize)]
5725#[serde(rename_all = "camelCase")]
5726pub struct SessionEventNotification {
5727    /// The session this event belongs to.
5728    pub session_id: SessionId,
5729    /// The event payload.
5730    pub event: SessionEvent,
5731}
5732
5733/// A single event in a session's timeline.
5734///
5735/// Events form a linked chain via `parent_id`. The `event_type` string
5736/// identifies the kind (e.g. `"assistant.message_delta"`, `"session.idle"`,
5737/// `"tool.execution_start"`). Event-specific payload is in `data` as
5738/// untyped JSON.
5739#[derive(Debug, Clone, Serialize, Deserialize)]
5740#[serde(rename_all = "camelCase")]
5741pub struct SessionEvent {
5742    /// Unique event ID (UUID v4).
5743    pub id: String,
5744    /// ISO 8601 timestamp.
5745    pub timestamp: String,
5746    /// ID of the preceding event in the chain.
5747    pub parent_id: Option<String>,
5748    /// Transient events that are not persisted to disk.
5749    #[serde(skip_serializing_if = "Option::is_none")]
5750    pub ephemeral: Option<bool>,
5751    /// Sub-agent instance identifier. Absent for events emitted by the
5752    /// root/main agent and for session-level events.
5753    #[serde(skip_serializing_if = "Option::is_none")]
5754    pub agent_id: Option<String>,
5755    /// Debug timestamp: when the CLI received this event (ms since epoch).
5756    #[serde(skip_serializing_if = "Option::is_none")]
5757    pub debug_cli_received_at_ms: Option<i64>,
5758    /// Debug timestamp: when the event was forwarded over WebSocket.
5759    #[serde(skip_serializing_if = "Option::is_none")]
5760    pub debug_ws_forwarded_at_ms: Option<i64>,
5761    /// Event type string (e.g. `"assistant.message"`, `"session.idle"`).
5762    #[serde(rename = "type")]
5763    pub event_type: String,
5764    /// Event-specific data. Structure depends on `event_type`.
5765    pub data: Value,
5766}
5767
5768impl SessionEvent {
5769    /// Parse the string `event_type` into a typed [`SessionEventType`](crate::session_events::SessionEventType) enum.
5770    ///
5771    /// Returns `SessionEventType::Unknown` for unrecognized event types,
5772    /// ensuring forward compatibility with newer CLI versions.
5773    pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5774        use serde::de::IntoDeserializer;
5775        let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5776            self.event_type.as_str().into_deserializer();
5777        crate::generated::SessionEventType::deserialize(deserializer)
5778            .unwrap_or(crate::generated::SessionEventType::Unknown)
5779    }
5780
5781    /// Deserialize the event `data` field into a typed struct.
5782    ///
5783    /// Returns `None` if deserialization fails (e.g. unknown event type
5784    /// or schema mismatch). Prefer typed data accessors for specific
5785    /// event types where you need strongly-typed field access.
5786    pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5787        serde_json::from_value(self.data.clone()).ok()
5788    }
5789
5790    /// `model_call` errors are transient — the CLI agent loop continues
5791    /// after them and may succeed on the next turn. These should not be
5792    /// treated as session-ending errors.
5793    pub fn is_transient_error(&self) -> bool {
5794        self.event_type == "session.error"
5795            && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5796    }
5797}
5798
5799/// A request from the CLI to invoke a client-defined tool.
5800///
5801/// Received as a JSON-RPC request on the `tool.call` method. The client
5802/// must respond with a [`ToolResultResponse`].
5803#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5804#[serde(rename_all = "camelCase")]
5805#[non_exhaustive]
5806pub struct ToolInvocation {
5807    /// Session that owns this tool call.
5808    pub session_id: SessionId,
5809    /// Unique ID for this tool call, used to correlate the response.
5810    pub tool_call_id: String,
5811    /// Name of the tool being invoked.
5812    pub tool_name: String,
5813    /// Tool arguments as JSON.
5814    pub arguments: Value,
5815    /// Snapshot of the session's currently initialized tools.
5816    ///
5817    /// The SDK populates this only when the invocation targets the built-in
5818    /// tool-search tool (`tool_search_tool`), so a tool-search override can
5819    /// rank/filter the live catalog — including MCP tools configured in
5820    /// settings — without issuing its own RPC. `None` for every other tool
5821    /// invocation. This field is not part of the wire protocol.
5822    #[serde(skip)]
5823    pub available_tools: Option<Vec<CurrentToolMetadata>>,
5824    /// W3C Trace Context `traceparent` header propagated from the CLI's
5825    /// `execute_tool` span. Pass through to OpenTelemetry-aware code so
5826    /// child spans created inside the handler are parented to the CLI
5827    /// span. `None` when the CLI has no trace context for this call.
5828    #[serde(default, skip_serializing_if = "Option::is_none")]
5829    pub traceparent: Option<String>,
5830    /// W3C Trace Context `tracestate` paired with
5831    /// [`traceparent`](Self::traceparent).
5832    #[serde(default, skip_serializing_if = "Option::is_none")]
5833    pub tracestate: Option<String>,
5834}
5835
5836impl ToolInvocation {
5837    /// Deserialize this invocation's [`arguments`](Self::arguments) into a
5838    /// strongly-typed parameter struct.
5839    ///
5840    /// Idiomatic way to extract typed parameters when implementing
5841    /// [`ToolHandler`](crate::tool::ToolHandler) directly. Equivalent to
5842    /// `serde_json::from_value(invocation.arguments.clone())` with the SDK's
5843    /// error type.
5844    ///
5845    /// # Example
5846    ///
5847    /// ```rust,no_run
5848    /// # use github_copilot_sdk::{Error, types::ToolInvocation, ToolResult};
5849    /// # use serde::Deserialize;
5850    /// # #[derive(Deserialize)] struct MyParams { city: String }
5851    /// # async fn example(inv: ToolInvocation) -> Result<ToolResult, Error> {
5852    /// let params: MyParams = inv.params()?;
5853    /// // …use `inv.session_id` / `inv.tool_call_id` alongside `params`…
5854    /// # let _ = params; Ok(ToolResult::Text(String::new()))
5855    /// # }
5856    /// ```
5857    pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5858        serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5859    }
5860
5861    /// Returns the propagated [`TraceContext`] for this invocation, or
5862    /// [`TraceContext::default()`] when the CLI sent no headers.
5863    pub fn trace_context(&self) -> TraceContext {
5864        TraceContext {
5865            traceparent: self.traceparent.clone(),
5866            tracestate: self.tracestate.clone(),
5867        }
5868    }
5869}
5870
5871/// Binary content returned by a tool.
5872#[derive(Debug, Clone, Serialize, Deserialize)]
5873#[serde(rename_all = "camelCase")]
5874pub struct ToolBinaryResult {
5875    /// Base64-encoded binary data.
5876    pub data: String,
5877    /// MIME type for the binary data.
5878    pub mime_type: String,
5879    /// Type identifier for the binary result.
5880    pub r#type: String,
5881    /// Optional description shown alongside the binary result.
5882    #[serde(default, skip_serializing_if = "Option::is_none")]
5883    pub description: Option<String>,
5884}
5885
5886/// Expanded tool result with metadata for the LLM and session log.
5887///
5888/// This type is `#[non_exhaustive]`: it mirrors a growing wire shape, so
5889/// construct it via [`ToolResultExpanded::new`] plus the `with_*` chain
5890/// rather than a struct literal, allowing new fields to land without
5891/// breaking callers.
5892#[derive(Debug, Clone, Serialize, Deserialize)]
5893#[serde(rename_all = "camelCase")]
5894#[non_exhaustive]
5895pub struct ToolResultExpanded {
5896    /// Result text sent back to the LLM.
5897    pub text_result_for_llm: String,
5898    /// `"success"` or `"failure"`.
5899    pub result_type: String,
5900    /// Binary payloads sent back to the LLM.
5901    #[serde(default, skip_serializing_if = "Option::is_none")]
5902    pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5903    /// Optional log message for the session timeline.
5904    #[serde(skip_serializing_if = "Option::is_none")]
5905    pub session_log: Option<String>,
5906    /// Error message, if the tool failed.
5907    #[serde(skip_serializing_if = "Option::is_none")]
5908    pub error: Option<String>,
5909    /// Tool-specific telemetry emitted with the result.
5910    #[serde(default, skip_serializing_if = "Option::is_none")]
5911    pub tool_telemetry: Option<HashMap<String, Value>>,
5912    /// Names of tools returned by a tool-search tool.
5913    #[serde(default, skip_serializing_if = "Option::is_none")]
5914    pub tool_references: Option<Vec<String>>,
5915}
5916
5917impl ToolResultExpanded {
5918    /// Construct an expanded result with the required `text_result_for_llm`
5919    /// and `result_type` (`"success"` or `"failure"`). All optional metadata
5920    /// fields start unset; populate them with the `with_*` builders.
5921    pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5922        Self {
5923            text_result_for_llm: text_result_for_llm.into(),
5924            result_type: result_type.into(),
5925            binary_results_for_llm: None,
5926            session_log: None,
5927            error: None,
5928            tool_telemetry: None,
5929            tool_references: None,
5930        }
5931    }
5932
5933    /// Set the binary payloads returned to the LLM.
5934    pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5935        self.binary_results_for_llm = Some(results);
5936        self
5937    }
5938
5939    /// Set the log message for the session timeline.
5940    pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5941        self.session_log = Some(session_log.into());
5942        self
5943    }
5944
5945    /// Set the error message, marking the tool as failed.
5946    pub fn with_error(mut self, error: impl Into<String>) -> Self {
5947        self.error = Some(error.into());
5948        self
5949    }
5950
5951    /// Set the tool-specific telemetry emitted with the result.
5952    pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5953        self.tool_telemetry = Some(telemetry);
5954        self
5955    }
5956
5957    /// Set the names of tools returned by a tool-search tool.
5958    pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5959    where
5960        I: IntoIterator<Item = S>,
5961        S: Into<String>,
5962    {
5963        self.tool_references = Some(references.into_iter().map(Into::into).collect());
5964        self
5965    }
5966}
5967
5968/// Result of a tool invocation — either a plain text string or an expanded result.
5969#[derive(Debug, Clone, Serialize, Deserialize)]
5970#[serde(untagged)]
5971#[non_exhaustive]
5972pub enum ToolResult {
5973    /// Simple text result passed directly to the LLM.
5974    Text(String),
5975    /// Structured result with metadata.
5976    Expanded(ToolResultExpanded),
5977}
5978
5979/// JSON-RPC response wrapper for a tool result, sent back to the CLI.
5980#[derive(Debug, Clone, Serialize, Deserialize)]
5981#[serde(rename_all = "camelCase")]
5982pub struct ToolResultResponse {
5983    /// The tool result payload.
5984    pub result: ToolResult,
5985}
5986
5987/// Metadata for a persisted session, returned by `session.list`.
5988#[derive(Debug, Clone, Serialize, Deserialize)]
5989#[serde(rename_all = "camelCase")]
5990pub struct SessionMetadata {
5991    /// The session's unique identifier.
5992    pub session_id: SessionId,
5993    /// ISO 8601 timestamp when the session was created.
5994    pub start_time: String,
5995    /// ISO 8601 timestamp of the last modification.
5996    pub modified_time: String,
5997    /// Agent-generated session summary.
5998    #[serde(skip_serializing_if = "Option::is_none")]
5999    pub summary: Option<String>,
6000    /// Whether the session is running remotely.
6001    pub is_remote: bool,
6002}
6003
6004/// Response from `session.list`.
6005#[derive(Debug, Clone, Serialize, Deserialize)]
6006#[serde(rename_all = "camelCase")]
6007pub struct ListSessionsResponse {
6008    /// The list of session metadata entries.
6009    pub sessions: Vec<SessionMetadata>,
6010}
6011
6012/// Filter options for [`Client::list_sessions`](crate::Client::list_sessions).
6013///
6014/// All fields are optional; unset fields don't constrain the result.
6015#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6016#[serde(rename_all = "camelCase")]
6017pub struct SessionListFilter {
6018    /// Filter by exact `cwd` match.
6019    #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
6020    pub working_directory: Option<String>,
6021    /// Filter by git root path.
6022    #[serde(default, skip_serializing_if = "Option::is_none")]
6023    pub git_root: Option<String>,
6024    /// Filter by repository in `owner/repo` form.
6025    #[serde(default, skip_serializing_if = "Option::is_none")]
6026    pub repository: Option<String>,
6027    /// Filter by git branch name.
6028    #[serde(default, skip_serializing_if = "Option::is_none")]
6029    pub branch: Option<String>,
6030}
6031
6032/// Response from `session.getMetadata`.
6033#[derive(Debug, Clone, Serialize, Deserialize)]
6034#[serde(rename_all = "camelCase")]
6035pub struct GetSessionMetadataResponse {
6036    /// The session metadata, or `None` if the session was not found.
6037    #[serde(skip_serializing_if = "Option::is_none")]
6038    pub session: Option<SessionMetadata>,
6039}
6040
6041/// Response from `session.getLastId`.
6042#[derive(Debug, Clone, Serialize, Deserialize)]
6043#[serde(rename_all = "camelCase")]
6044pub struct GetLastSessionIdResponse {
6045    /// The most recently updated session ID, or `None` if no sessions exist.
6046    #[serde(skip_serializing_if = "Option::is_none")]
6047    pub session_id: Option<SessionId>,
6048}
6049
6050/// Response from `session.getForeground`.
6051#[derive(Debug, Clone, Serialize, Deserialize)]
6052#[serde(rename_all = "camelCase")]
6053pub struct GetForegroundSessionResponse {
6054    /// The current foreground session ID, or `None` if no foreground session.
6055    #[serde(skip_serializing_if = "Option::is_none")]
6056    pub session_id: Option<SessionId>,
6057}
6058
6059/// Response from `session.getMessages`.
6060#[derive(Debug, Clone, Serialize, Deserialize)]
6061#[serde(rename_all = "camelCase")]
6062pub struct GetMessagesResponse {
6063    /// Timeline events for the session.
6064    pub events: Vec<SessionEvent>,
6065}
6066
6067/// Result of an elicitation (interactive UI form) request.
6068#[derive(Debug, Clone, Serialize, Deserialize)]
6069#[serde(rename_all = "camelCase")]
6070pub struct ElicitationResult {
6071    /// User's action: `"accept"`, `"decline"`, or `"cancel"`.
6072    pub action: String,
6073    /// Form data submitted by the user (present when action is `"accept"`).
6074    #[serde(skip_serializing_if = "Option::is_none")]
6075    pub content: Option<Value>,
6076}
6077
6078/// Elicitation display mode.
6079///
6080/// New modes may be added by the CLI in future protocol versions; the
6081/// `Unknown` variant keeps deserialization from failing on unrecognised
6082/// values so the SDK can still surface the request to callers.
6083#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6084#[serde(rename_all = "camelCase")]
6085#[non_exhaustive]
6086pub enum ElicitationMode {
6087    /// Structured form input rendered by the host.
6088    Form,
6089    /// Browser redirect to a URL.
6090    Url,
6091    /// A mode not yet known to this SDK version.
6092    #[serde(other)]
6093    Unknown,
6094}
6095
6096/// An incoming elicitation request from the CLI (provider side).
6097///
6098/// Received via `elicitation.requested` session event when the session has
6099/// an [`ElicitationHandler`] installed.
6100/// The provider should render a form or dialog and return an
6101/// [`ElicitationResult`].
6102#[derive(Debug, Clone, Serialize, Deserialize)]
6103#[serde(rename_all = "camelCase")]
6104pub struct ElicitationRequest {
6105    /// Message describing what information is needed from the user.
6106    pub message: String,
6107    /// JSON Schema describing the form fields to present.
6108    #[serde(skip_serializing_if = "Option::is_none")]
6109    pub requested_schema: Option<Value>,
6110    /// Elicitation display mode.
6111    #[serde(skip_serializing_if = "Option::is_none")]
6112    pub mode: Option<ElicitationMode>,
6113    /// The source that initiated the request (e.g. MCP server name).
6114    #[serde(skip_serializing_if = "Option::is_none")]
6115    pub elicitation_source: Option<String>,
6116    /// URL to open in the user's browser (url mode only).
6117    #[serde(skip_serializing_if = "Option::is_none")]
6118    pub url: Option<String>,
6119}
6120
6121/// Session-level capabilities reported by the CLI after session creation.
6122///
6123/// Capabilities indicate which features the CLI host supports for this session.
6124/// Updated at runtime via `capabilities.changed` events.
6125#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6126#[serde(rename_all = "camelCase")]
6127pub struct SessionCapabilities {
6128    /// UI capabilities (elicitation support, etc.).
6129    #[serde(skip_serializing_if = "Option::is_none")]
6130    pub ui: Option<UiCapabilities>,
6131}
6132
6133/// UI-specific capabilities for a session.
6134#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6135#[serde(rename_all = "camelCase")]
6136pub struct UiCapabilities {
6137    /// Whether the host supports interactive elicitation dialogs.
6138    #[serde(skip_serializing_if = "Option::is_none")]
6139    pub elicitation: Option<bool>,
6140    /// **Experimental.** This field is part of an experimental wire-protocol
6141    /// surface (SEP-1865) and may change or be removed in a future release.
6142    ///
6143    /// Whether the runtime has accepted the session's MCP Apps (SEP-1865)
6144    /// opt-in. `Some(true)` when the consumer set
6145    /// [`SessionConfig::enable_mcp_apps`] / [`ResumeSessionConfig::enable_mcp_apps`]
6146    /// to `true` on create/resume **and** the runtime's `MCP_APPS` feature
6147    /// flag (or `COPILOT_MCP_APPS=true` env override) is on. Otherwise
6148    /// absent or `Some(false)`, indicating the runtime silently dropped the
6149    /// opt-in.
6150    #[serde(skip_serializing_if = "Option::is_none")]
6151    pub mcp_apps: Option<bool>,
6152    /// Host-specific canvas capabilities.
6153    #[serde(skip_serializing_if = "Option::is_none")]
6154    pub canvases: Option<bool>,
6155}
6156
6157/// Options for the [`SessionUi::input`](crate::session::SessionUi::input) convenience method.
6158#[derive(Debug, Clone, Default)]
6159pub struct UiInputOptions<'a> {
6160    /// Title label for the input field.
6161    pub title: Option<&'a str>,
6162    /// Descriptive text shown below the field.
6163    pub description: Option<&'a str>,
6164    /// Minimum character length.
6165    pub min_length: Option<u64>,
6166    /// Maximum character length.
6167    pub max_length: Option<u64>,
6168    /// Semantic format hint.
6169    pub format: Option<InputFormat>,
6170    /// Default value pre-populated in the field.
6171    pub default: Option<&'a str>,
6172}
6173
6174/// Semantic format hints for text input fields.
6175#[derive(Debug, Clone, Copy)]
6176#[non_exhaustive]
6177pub enum InputFormat {
6178    /// Email address.
6179    Email,
6180    /// URI.
6181    Uri,
6182    /// Calendar date.
6183    Date,
6184    /// Date and time.
6185    DateTime,
6186}
6187
6188impl InputFormat {
6189    /// Returns the JSON Schema format string for this variant.
6190    pub fn as_str(&self) -> &'static str {
6191        match self {
6192            Self::Email => "email",
6193            Self::Uri => "uri",
6194            Self::Date => "date",
6195            Self::DateTime => "date-time",
6196        }
6197    }
6198}
6199
6200/// Re-exports of generated protocol types that are part of the SDK's
6201/// public API surface. The canonical definitions live in
6202/// [`crate::rpc`]; they live here so the crate-root
6203/// `pub use types::*` surfaces them alongside hand-written SDK types.
6204pub use crate::generated::api_types::{
6205    Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
6206    ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
6207    ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
6208    PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
6209    PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
6210    PermissionDecisionUserNotAvailable, PermissionResponseCapability,
6211};
6212
6213/// Permission categories the CLI may request approval for.
6214///
6215/// Wire values are the lower-kebab strings the CLI sends as the `kind`
6216/// discriminator on a permission request. Marked `#[non_exhaustive]`
6217/// because the CLI may add new kinds; matches must include a `_` arm.
6218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6219#[serde(rename_all = "kebab-case")]
6220#[non_exhaustive]
6221pub enum PermissionRequestKind {
6222    /// Run a shell command.
6223    Shell,
6224    /// Write to a file.
6225    Write,
6226    /// Read a file.
6227    Read,
6228    /// Open a URL.
6229    Url,
6230    /// Invoke an MCP server tool.
6231    Mcp,
6232    /// Invoke a client-defined custom tool.
6233    CustomTool,
6234    /// Update agent memory.
6235    Memory,
6236    /// Run a hook callback.
6237    Hook,
6238    /// Unrecognized kind. The original wire string is available in
6239    /// [`PermissionRequestData::extra`] under the `kind` key.
6240    #[serde(other)]
6241    Unknown,
6242}
6243
6244/// Data sent by the CLI for permission-related events.
6245///
6246/// Used for both the `permission.request` RPC call (which expects a response)
6247/// and `permission.requested` notifications (fire-and-forget). Contains the
6248/// full params object.
6249#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6250#[serde(rename_all = "camelCase")]
6251pub struct PermissionRequestData {
6252    /// The permission category being requested. `None` means the CLI did
6253    /// not include a `kind` field. Use this to branch on common cases
6254    /// (shell, write, etc.) without parsing [`extra`](Self::extra).
6255    #[serde(default, skip_serializing_if = "Option::is_none")]
6256    pub kind: Option<PermissionRequestKind>,
6257    /// The originating tool-call ID, if this permission request is tied
6258    /// to a specific tool invocation.
6259    #[serde(default, skip_serializing_if = "Option::is_none")]
6260    pub tool_call_id: Option<String>,
6261    /// Whether managed policy requires an explicit human decision.
6262    #[serde(default, skip_serializing_if = "Option::is_none")]
6263    pub managed_approval_required: Option<bool>,
6264    /// Whether managed settings are enabled for this session.
6265    #[serde(default, skip_serializing_if = "is_false")]
6266    pub managed_settings_enabled: bool,
6267    /// The full permission event params from the CLI, including the request ID
6268    /// and nested permission request. The shape varies by permission type and
6269    /// CLI version, so we preserve it as `Value`.
6270    #[serde(flatten)]
6271    pub extra: Value,
6272}
6273
6274/// Data sent by the CLI with an `exitPlanMode.request` RPC call.
6275#[derive(Debug, Clone, Serialize, Deserialize)]
6276#[serde(rename_all = "camelCase")]
6277pub struct ExitPlanModeData {
6278    /// Markdown summary of the plan presented to the user.
6279    #[serde(default)]
6280    pub summary: String,
6281    /// Full plan content (e.g. the plan.md body), if available.
6282    #[serde(default, skip_serializing_if = "Option::is_none")]
6283    pub plan_content: Option<String>,
6284    /// Allowed exit actions (e.g. "interactive", "autopilot", "autopilot_fleet").
6285    #[serde(default)]
6286    pub actions: Vec<String>,
6287    /// Which action the CLI recommends, defaults to "autopilot".
6288    #[serde(default = "default_recommended_action")]
6289    pub recommended_action: String,
6290}
6291
6292fn default_recommended_action() -> String {
6293    "autopilot".to_string()
6294}
6295
6296impl Default for ExitPlanModeData {
6297    fn default() -> Self {
6298        Self {
6299            summary: String::new(),
6300            plan_content: None,
6301            actions: Vec::new(),
6302            recommended_action: default_recommended_action(),
6303        }
6304    }
6305}
6306
6307#[cfg(test)]
6308mod tests {
6309    use std::collections::HashMap;
6310    use std::path::PathBuf;
6311
6312    use serde_json::json;
6313
6314    use super::{
6315        AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
6316        AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions,
6317        ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode,
6318        ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
6319        InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
6320        MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
6321        ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
6322        SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
6323        ToolResultResponse, ensure_attachment_display_names,
6324    };
6325    use crate::generated::session_events::TypedSessionEvent;
6326
6327    #[test]
6328    fn permission_response_capability_is_publicly_exported() {
6329        assert_eq!(
6330            serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(),
6331            json!("interactive")
6332        );
6333    }
6334
6335    #[test]
6336    fn tool_builder_composes() {
6337        let tool = Tool::new("greet")
6338            .with_description("Say hello")
6339            .with_namespaced_name("hello/greet")
6340            .with_instructions("Pass the user's name")
6341            .with_parameters(json!({
6342                "type": "object",
6343                "properties": { "name": { "type": "string" } },
6344                "required": ["name"]
6345            }))
6346            .with_overrides_built_in_tool(true)
6347            .with_skip_permission(true);
6348        assert_eq!(tool.name, "greet");
6349        assert_eq!(tool.description, "Say hello");
6350        assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
6351        assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
6352        assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
6353        assert!(tool.overrides_built_in_tool);
6354        assert!(tool.skip_permission);
6355    }
6356
6357    #[test]
6358    fn tool_defer_serialization() {
6359        let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
6360        assert_eq!(tool.defer, Some(super::DeferMode::Auto));
6361        let value = serde_json::to_value(&tool).unwrap();
6362        assert_eq!(value.get("defer").unwrap(), &json!("auto"));
6363
6364        let plain = Tool::new("plain");
6365        let value = serde_json::to_value(&plain).unwrap();
6366        assert!(value.get("defer").is_none());
6367    }
6368
6369    #[test]
6370    fn tool_metadata_serialization() {
6371        use indexmap::IndexMap;
6372
6373        let mut metadata = IndexMap::new();
6374        metadata.insert(
6375            "github.com/copilot:safeForTelemetry".to_string(),
6376            json!({ "name": true, "inputsNames": false }),
6377        );
6378        let tool = Tool::new("lookup").with_metadata(metadata);
6379        let value = serde_json::to_value(&tool).unwrap();
6380        assert_eq!(
6381            value
6382                .get("metadata")
6383                .unwrap()
6384                .get("github.com/copilot:safeForTelemetry")
6385                .unwrap(),
6386            &json!({ "name": true, "inputsNames": false })
6387        );
6388
6389        // Empty metadata is omitted on the wire.
6390        let plain = Tool::new("plain");
6391        let value = serde_json::to_value(&plain).unwrap();
6392        assert!(value.get("metadata").is_none());
6393    }
6394
6395    #[test]
6396    fn custom_agent_config_builder_with_model() {
6397        let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
6398            .with_model("claude-haiku-4.5")
6399            .with_display_name("My Agent");
6400        assert_eq!(agent.name, "my-agent");
6401        assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
6402        assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
6403    }
6404
6405    #[test]
6406    fn custom_agent_config_serializes_model() {
6407        let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
6408        let wire = serde_json::to_value(&agent).unwrap();
6409        assert_eq!(wire["model"], "claude-haiku-4.5");
6410        assert_eq!(wire["name"], "model-agent");
6411    }
6412
6413    #[test]
6414    fn custom_agent_config_omits_model_when_none() {
6415        let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6416        let wire = serde_json::to_value(&agent).unwrap();
6417        assert!(wire.get("model").is_none());
6418    }
6419
6420    #[test]
6421    fn custom_agent_config_builder_with_reasoning_effort() {
6422        let agent =
6423            CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6424        assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6425    }
6426
6427    #[test]
6428    fn custom_agent_config_serializes_reasoning_effort() {
6429        let agent =
6430            CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6431        let wire = serde_json::to_value(&agent).unwrap();
6432        assert_eq!(wire["reasoningEffort"], "high");
6433    }
6434
6435    #[test]
6436    fn custom_agent_config_omits_reasoning_effort_when_none() {
6437        let agent = CustomAgentConfig::new("default-agent", "prompt");
6438        let wire = serde_json::to_value(&agent).unwrap();
6439        assert!(wire.get("reasoningEffort").is_none());
6440    }
6441
6442    #[test]
6443    #[should_panic(expected = "tool parameter schema must be a JSON object")]
6444    fn tool_with_parameters_panics_on_non_object_value() {
6445        let _ = Tool::new("noop").with_parameters(json!(null));
6446    }
6447
6448    #[test]
6449    fn tool_result_expanded_serializes_binary_results_for_llm() {
6450        let response = ToolResultResponse {
6451            result: ToolResult::Expanded(ToolResultExpanded {
6452                text_result_for_llm: "rendered chart".to_string(),
6453                result_type: "success".to_string(),
6454                binary_results_for_llm: Some(vec![ToolBinaryResult {
6455                    data: "aW1n".to_string(),
6456                    mime_type: "image/png".to_string(),
6457                    r#type: "image".to_string(),
6458                    description: Some("chart preview".to_string()),
6459                }]),
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!(
6470            wire,
6471            json!({
6472                "result": {
6473                    "textResultForLlm": "rendered chart",
6474                    "resultType": "success",
6475                    "binaryResultsForLlm": [
6476                        {
6477                            "data": "aW1n",
6478                            "mimeType": "image/png",
6479                            "type": "image",
6480                            "description": "chart preview"
6481                        }
6482                    ]
6483                }
6484            })
6485        );
6486    }
6487
6488    #[test]
6489    fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6490        let response = ToolResultResponse {
6491            result: ToolResult::Expanded(ToolResultExpanded {
6492                text_result_for_llm: "ok".to_string(),
6493                result_type: "success".to_string(),
6494                binary_results_for_llm: None,
6495                session_log: None,
6496                error: None,
6497                tool_telemetry: None,
6498                tool_references: None,
6499            }),
6500        };
6501
6502        let wire = serde_json::to_value(&response).unwrap();
6503
6504        assert_eq!(wire["result"]["textResultForLlm"], "ok");
6505        assert!(wire["result"].get("binaryResultsForLlm").is_none());
6506    }
6507
6508    #[test]
6509    fn tool_result_expanded_serializes_tool_references() {
6510        let response = ToolResultResponse {
6511            result: ToolResult::Expanded(
6512                ToolResultExpanded::new("found 2 tools", "success")
6513                    .with_tool_references(["get_weather", "check_status"]),
6514            ),
6515        };
6516
6517        let wire = serde_json::to_value(&response).unwrap();
6518
6519        assert_eq!(
6520            wire,
6521            json!({
6522                "result": {
6523                    "textResultForLlm": "found 2 tools",
6524                    "resultType": "success",
6525                    "toolReferences": ["get_weather", "check_status"]
6526                }
6527            })
6528        );
6529    }
6530
6531    #[test]
6532    fn tool_result_expanded_omits_tool_references_when_none() {
6533        let response = ToolResultResponse {
6534            result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6535        };
6536
6537        let wire = serde_json::to_value(&response).unwrap();
6538
6539        assert_eq!(wire["result"]["textResultForLlm"], "ok");
6540        assert!(wire["result"].get("toolReferences").is_none());
6541    }
6542
6543    #[test]
6544    fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6545        // The builder is generic over `Into<String>`, so an owned `Vec<String>`
6546        // must compile and populate the field just like a `&str` array.
6547        let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6548        let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6549
6550        assert_eq!(
6551            expanded.tool_references.as_deref(),
6552            Some(["alpha".to_string(), "beta".to_string()].as_slice())
6553        );
6554    }
6555
6556    #[test]
6557    fn tool_result_expanded_deserializes_tool_references() {
6558        let wire = json!({
6559            "textResultForLlm": "found tools",
6560            "resultType": "success",
6561            "toolReferences": ["alpha", "beta"]
6562        });
6563
6564        let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6565
6566        assert_eq!(
6567            expanded.tool_references.as_deref(),
6568            Some(["alpha".to_string(), "beta".to_string()].as_slice())
6569        );
6570    }
6571
6572    #[test]
6573    fn session_config_default_wire_flags_off_without_handlers() {
6574        let cfg = SessionConfig::default();
6575        assert_eq!(cfg.mcp_oauth_token_storage, None);
6576        assert_eq!(cfg.allowed_models, None);
6577        // Wire flags are derived from handler presence at create_session
6578        // time, not stored on the config. With no handlers installed, every
6579        // request_* flag should serialize as false.
6580        let (wire, _runtime) = cfg
6581            .into_wire(Some(SessionId::from("default-flags")))
6582            .expect("default config has no duplicate handlers");
6583        assert!(!wire.request_user_input);
6584        assert!(!wire.request_permission);
6585        assert!(!wire.request_elicitation);
6586        assert!(!wire.request_exit_plan_mode);
6587        assert!(!wire.request_auto_mode_switch);
6588        assert!(!wire.hooks);
6589        assert!(!wire.request_mcp_apps);
6590        let json = serde_json::to_value(&wire).unwrap();
6591        assert!(json.get("askUserVariant").is_none());
6592        assert!(json.get("allowedModels").is_none());
6593    }
6594
6595    #[test]
6596    fn resume_session_config_new_wire_flags_off_without_handlers() {
6597        let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6598        assert_eq!(cfg.mcp_oauth_token_storage, None);
6599        assert_eq!(cfg.allowed_models, None);
6600        let (wire, _runtime) = cfg
6601            .into_wire()
6602            .expect("default resume config has no duplicate handlers");
6603        assert!(!wire.request_user_input);
6604        assert!(!wire.request_permission);
6605        assert!(!wire.request_elicitation);
6606        assert!(!wire.request_exit_plan_mode);
6607        assert!(!wire.request_auto_mode_switch);
6608        assert!(!wire.hooks);
6609        assert!(!wire.request_mcp_apps);
6610        let json = serde_json::to_value(&wire).unwrap();
6611        assert!(json.get("askUserVariant").is_none());
6612        assert!(json.get("allowedModels").is_none());
6613    }
6614
6615    #[test]
6616    fn session_configs_build_debug_and_serialize_allowed_models() {
6617        let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]);
6618        assert_eq!(
6619            create.allowed_models.as_deref(),
6620            Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..])
6621        );
6622        assert!(format!("{create:?}").contains("allowed_models"));
6623
6624        let (create_wire, _) = create
6625            .into_wire(Some(SessionId::from("create-allowed-models")))
6626            .expect("allowed model config has no duplicate handlers");
6627        let create_json = serde_json::to_value(&create_wire).unwrap();
6628        assert_eq!(
6629            create_json["allowedModels"],
6630            json!(["gpt-5.4", "claude-sonnet-4"])
6631        );
6632
6633        let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models"))
6634            .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]);
6635        assert_eq!(
6636            resume.allowed_models.as_deref(),
6637            Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..])
6638        );
6639        assert!(format!("{resume:?}").contains("allowed_models"));
6640
6641        let (resume_wire, _) = resume
6642            .into_wire()
6643            .expect("resume allowed model config has no duplicate handlers");
6644        let resume_json = serde_json::to_value(&resume_wire).unwrap();
6645        assert_eq!(
6646            resume_json["allowedModels"],
6647            json!(["gpt-5.4", "gpt-5-mini"])
6648        );
6649    }
6650
6651    #[test]
6652    fn custom_agents_local_only_serializes_on_create_and_resume() {
6653        let (create_wire, _) = SessionConfig::default()
6654            .with_custom_agents_local_only(false)
6655            .into_wire(Some(SessionId::from("create-locality")))
6656            .expect("create config has no duplicate handlers");
6657        let create_json = serde_json::to_value(&create_wire).unwrap();
6658        assert_eq!(create_json["customAgentsLocalOnly"], false);
6659
6660        let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6661            .with_custom_agents_local_only(false)
6662            .into_wire()
6663            .expect("resume config has no duplicate handlers");
6664        let resume_json = serde_json::to_value(&resume_wire).unwrap();
6665        assert_eq!(resume_json["customAgentsLocalOnly"], false);
6666
6667        let (unset_create_wire, _) = SessionConfig::default()
6668            .into_wire(Some(SessionId::from("create-unset")))
6669            .expect("create config has no duplicate handlers");
6670        let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6671        assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6672
6673        let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6674            .into_wire()
6675            .expect("resume config has no duplicate handlers");
6676        let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6677        assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6678    }
6679
6680    #[test]
6681    fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6682        let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6683        assert_eq!(cfg.enable_mcp_apps, Some(true));
6684
6685        let (wire, _runtime) = cfg
6686            .into_wire(Some(SessionId::from("enable-mcp-apps")))
6687            .expect("enable_mcp_apps config has no duplicate handlers");
6688        assert!(wire.request_mcp_apps);
6689
6690        let json = serde_json::to_value(&wire).unwrap();
6691        assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6692    }
6693
6694    #[test]
6695    fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6696        let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6697            .with_enable_mcp_apps(true);
6698        assert_eq!(cfg.enable_mcp_apps, Some(true));
6699
6700        let (wire, _runtime) = cfg
6701            .into_wire()
6702            .expect("resume enable_mcp_apps config has no duplicate handlers");
6703        assert!(wire.request_mcp_apps);
6704
6705        let json = serde_json::to_value(&wire).unwrap();
6706        assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6707    }
6708
6709    #[test]
6710    fn github_mcp_tool_config_serializes_for_create_and_resume() {
6711        let github_config = GitHubMcpToolConfig::new()
6712            .with_enable_all_tools(true)
6713            .with_additional_toolsets(["repos"])
6714            .with_additional_tools(["get_issue"])
6715            .with_enable_insiders_mode(true)
6716            .with_disable_form_deferral(true);
6717
6718        let (create_wire, _) = SessionConfig::default()
6719            .with_github_mcp_tool_config(github_config.clone())
6720            .into_wire(Some(SessionId::from("github-mcp")))
6721            .expect("create config has no duplicate handlers");
6722        assert_eq!(
6723            serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6724            serde_json::json!({
6725                "enableAllTools": true,
6726                "additionalToolsets": ["repos"],
6727                "additionalTools": ["get_issue"],
6728                "enableInsidersMode": true,
6729                "disableFormDeferral": true,
6730            })
6731        );
6732
6733        let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6734            .with_github_mcp_tool_config(github_config)
6735            .into_wire()
6736            .expect("resume config has no duplicate handlers");
6737        assert!(resume_wire.github_mcp_tool_config.is_some());
6738
6739        let (unset_wire, _) = SessionConfig::default()
6740            .into_wire(Some(SessionId::from("github-mcp-unset")))
6741            .expect("default config has no duplicate handlers");
6742        assert!(
6743            serde_json::to_value(&unset_wire)
6744                .unwrap()
6745                .get("githubMcpToolConfig")
6746                .is_none()
6747        );
6748    }
6749
6750    #[test]
6751    fn memory_configuration_constructors_and_serde() {
6752        assert!(MemoryConfiguration::enabled().enabled);
6753        assert!(!MemoryConfiguration::disabled().enabled);
6754        assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6755
6756        let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6757        assert_eq!(json, serde_json::json!({ "enabled": true }));
6758    }
6759
6760    #[test]
6761    fn session_config_with_memory_serializes() {
6762        let (wire, _runtime) = SessionConfig::default()
6763            .with_memory(MemoryConfiguration::enabled())
6764            .into_wire(Some(SessionId::from("memory-on")))
6765            .expect("no duplicate handlers");
6766        let json = serde_json::to_value(&wire).unwrap();
6767        assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6768
6769        let (wire_off, _) = SessionConfig::default()
6770            .with_memory(MemoryConfiguration::disabled())
6771            .into_wire(Some(SessionId::from("memory-off")))
6772            .expect("no duplicate handlers");
6773        let json_off = serde_json::to_value(&wire_off).unwrap();
6774        assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6775
6776        // Unset memory is omitted on the wire.
6777        let (empty_wire, _) = SessionConfig::default()
6778            .into_wire(Some(SessionId::from("memory-unset")))
6779            .expect("no duplicate handlers");
6780        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6781        assert!(empty_json.get("memory").is_none());
6782    }
6783
6784    #[test]
6785    fn resume_session_config_with_memory_serializes() {
6786        let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6787            .with_memory(MemoryConfiguration::enabled())
6788            .into_wire()
6789            .expect("no duplicate handlers");
6790        let json = serde_json::to_value(&wire).unwrap();
6791        assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6792
6793        // Unset memory is omitted on the wire.
6794        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6795            .into_wire()
6796            .expect("no duplicate handlers");
6797        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6798        assert!(empty_json.get("memory").is_none());
6799    }
6800
6801    #[test]
6802    fn feature_flags_serialize_on_create_and_resume() {
6803        let feature_flags = HashMap::from([
6804            ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true),
6805            ("DISABLED_TEST_FLAG".to_string(), false),
6806        ]);
6807        let expected = serde_json::json!({
6808            "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true,
6809            "DISABLED_TEST_FLAG": false,
6810        });
6811
6812        let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone());
6813        assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags));
6814        let (create_wire, _) = create_config
6815            .into_wire(Some(SessionId::from("feature-flags-create")))
6816            .expect("no duplicate handlers");
6817        let create_json = serde_json::to_value(&create_wire).unwrap();
6818        assert_eq!(create_json["featureFlags"], expected);
6819
6820        let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume"))
6821            .with_feature_flags(feature_flags)
6822            .into_wire()
6823            .expect("no duplicate handlers");
6824        let resume_json = serde_json::to_value(&resume_wire).unwrap();
6825        assert_eq!(resume_json["featureFlags"], expected);
6826
6827        let (unset_create_wire, _) = SessionConfig::default()
6828            .into_wire(Some(SessionId::from("feature-flags-create-unset")))
6829            .expect("no duplicate handlers");
6830        let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6831        assert!(unset_create_json.get("featureFlags").is_none());
6832
6833        let (unset_resume_wire, _) =
6834            ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset"))
6835                .into_wire()
6836                .expect("no duplicate handlers");
6837        let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6838        assert!(unset_resume_json.get("featureFlags").is_none());
6839    }
6840
6841    fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6842        CopilotExpAssignmentResponse {
6843            features: vec!["copilot_exp_flag".to_string()],
6844            flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6845            configs: vec![ExpConfigEntry {
6846                id: "cfg-1".to_string(),
6847                parameters: HashMap::from([
6848                    ("threshold".to_string(), ExpFlagValue::Integer(5)),
6849                    ("enabled".to_string(), ExpFlagValue::Bool(true)),
6850                ]),
6851            }],
6852            assignment_context: context.to_string(),
6853            ..Default::default()
6854        }
6855    }
6856
6857    #[test]
6858    fn exp_flag_value_round_trips_all_variants() {
6859        let values = serde_json::json!({
6860            "s": "text",
6861            "i": 7,
6862            "f": 1.5,
6863            "b": true,
6864            "n": null,
6865        });
6866        let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6867        assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6868        assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6869        assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6870        assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6871        assert_eq!(parsed["n"], ExpFlagValue::Null);
6872        assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6873    }
6874
6875    #[test]
6876    fn session_config_with_exp_assignments_serializes() {
6877        let assignments = sample_exp_assignments("ctx-123");
6878        let expected = serde_json::to_value(&assignments).unwrap();
6879        let (wire, _runtime) = SessionConfig::default()
6880            .with_exp_assignments(assignments)
6881            .into_wire(Some(SessionId::from("exp-on")))
6882            .expect("no duplicate handlers");
6883        let json = serde_json::to_value(&wire).unwrap();
6884        assert_eq!(json["expAssignments"], expected);
6885        assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6886        assert_eq!(
6887            json["expAssignments"]["Flights"]["copilot_exp_flag"],
6888            "treatment"
6889        );
6890
6891        // Unset exp assignments are omitted on the wire.
6892        let (empty_wire, _) = SessionConfig::default()
6893            .into_wire(Some(SessionId::from("exp-unset")))
6894            .expect("no duplicate handlers");
6895        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6896        assert!(empty_json.get("expAssignments").is_none());
6897    }
6898
6899    #[test]
6900    fn resume_session_config_with_exp_assignments_serializes() {
6901        let assignments = sample_exp_assignments("ctx-456");
6902        let expected = serde_json::to_value(&assignments).unwrap();
6903        let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6904            .with_exp_assignments(assignments)
6905            .into_wire()
6906            .expect("no duplicate handlers");
6907        let json = serde_json::to_value(&wire).unwrap();
6908        assert_eq!(json["expAssignments"], expected);
6909
6910        // Unset exp assignments are omitted on the wire.
6911        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6912            .into_wire()
6913            .expect("no duplicate handlers");
6914        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6915        assert!(empty_json.get("expAssignments").is_none());
6916    }
6917
6918    #[test]
6919    fn session_config_clone_preserves_exp_assignments() {
6920        let assignments = sample_exp_assignments("ctx-clone");
6921        let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6922        let cloned = config.clone();
6923
6924        assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6925
6926        let (wire, _runtime) = cloned
6927            .into_wire(Some(SessionId::from("exp-clone")))
6928            .expect("no duplicate handlers");
6929        let json = serde_json::to_value(&wire).unwrap();
6930        assert_eq!(
6931            json["expAssignments"],
6932            serde_json::to_value(&assignments).unwrap()
6933        );
6934    }
6935
6936    #[test]
6937    fn resume_session_config_clone_preserves_exp_assignments() {
6938        let assignments = sample_exp_assignments("ctx-clone-resume");
6939        let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6940            .with_exp_assignments(assignments.clone());
6941        let cloned = config.clone();
6942
6943        assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6944
6945        let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6946        let json = serde_json::to_value(&wire).unwrap();
6947        assert_eq!(
6948            json["expAssignments"],
6949            serde_json::to_value(&assignments).unwrap()
6950        );
6951    }
6952
6953    #[test]
6954    #[allow(clippy::field_reassign_with_default)]
6955    fn session_config_into_wire_serializes_bucket_b_fields() {
6956        use std::path::PathBuf;
6957
6958        use super::{CloudSessionOptions, CloudSessionRepository};
6959
6960        let mut cfg = SessionConfig::default();
6961        cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6962        cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6963        cfg.github_token = Some("ghs_secret".to_string());
6964        cfg.include_sub_agent_streaming_events = Some(false);
6965        cfg.enable_session_telemetry = Some(false);
6966        cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6967        cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6968        cfg.enable_on_demand_instruction_discovery = Some(false);
6969        cfg.cloud = Some(CloudSessionOptions::with_repository(
6970            CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6971        ));
6972
6973        let (wire, _runtime) = cfg
6974            .into_wire(Some(SessionId::from("custom-id")))
6975            .expect("no duplicate handlers");
6976        let wire_json = serde_json::to_value(&wire).unwrap();
6977        assert_eq!(wire_json["sessionId"], "custom-id");
6978        assert_eq!(wire_json["configDir"], "/tmp/cfg");
6979        assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6980        assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6981        assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6982        assert_eq!(wire_json["enableSessionTelemetry"], false);
6983        assert_eq!(wire_json["reasoningSummary"], "concise");
6984        assert_eq!(wire_json["remoteSession"], "export");
6985        assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6986        assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6987        assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6988        assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6989
6990        // Unset fields are omitted on the wire.
6991        let (empty_wire, _) = SessionConfig::default()
6992            .into_wire(Some(SessionId::from("empty")))
6993            .expect("default has no duplicate handlers");
6994        let empty_json = serde_json::to_value(&empty_wire).unwrap();
6995        assert!(empty_json.get("gitHubToken").is_none());
6996        assert!(empty_json.get("enableSessionTelemetry").is_none());
6997        assert!(empty_json.get("reasoningSummary").is_none());
6998        assert!(empty_json.get("remoteSession").is_none());
6999        assert!(
7000            empty_json
7001                .get("enableOnDemandInstructionDiscovery")
7002                .is_none()
7003        );
7004        assert!(empty_json.get("cloud").is_none());
7005    }
7006
7007    #[test]
7008    fn session_config_into_wire_serializes_named_providers_and_models() {
7009        let cfg = SessionConfig::default()
7010            .with_providers(vec![
7011                NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
7012                    .with_provider_type("openai")
7013                    .with_wire_api("responses")
7014                    .with_api_key("sk-test"),
7015            ])
7016            .with_models(vec![
7017                ProviderModelConfig::new("gpt-x", "my-openai")
7018                    .with_wire_model("gpt-x-2025")
7019                    .with_max_output_tokens(2048),
7020            ]);
7021
7022        let (wire, _) = cfg
7023            .into_wire(Some(SessionId::from("sess-providers")))
7024            .expect("no duplicate handlers");
7025        let wire_json = serde_json::to_value(&wire).unwrap();
7026        assert_eq!(wire_json["providers"][0]["name"], "my-openai");
7027        assert_eq!(
7028            wire_json["providers"][0]["baseUrl"],
7029            "https://api.example.com/v1"
7030        );
7031        assert_eq!(wire_json["providers"][0]["type"], "openai");
7032        assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
7033        assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
7034        assert_eq!(wire_json["models"][0]["id"], "gpt-x");
7035        assert_eq!(wire_json["models"][0]["provider"], "my-openai");
7036        assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
7037        assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
7038
7039        let (empty_wire, _) = SessionConfig::default()
7040            .into_wire(Some(SessionId::from("empty")))
7041            .expect("default has no duplicate handlers");
7042        let empty_json = serde_json::to_value(&empty_wire).unwrap();
7043        assert!(empty_json.get("providers").is_none());
7044        assert!(empty_json.get("models").is_none());
7045    }
7046
7047    #[test]
7048    fn resume_config_into_wire_serializes_named_providers_and_models() {
7049        let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
7050            .with_providers(vec![
7051                NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
7052                    .with_provider_type("azure")
7053                    .with_azure(AzureProviderOptions {
7054                        api_version: Some("2024-10-21".to_string()),
7055                    }),
7056            ])
7057            .with_models(vec![
7058                ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
7059            ]);
7060
7061        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7062        let wire_json = serde_json::to_value(&wire).unwrap();
7063        assert_eq!(wire_json["providers"][0]["name"], "my-azure");
7064        assert_eq!(wire_json["providers"][0]["type"], "azure");
7065        assert_eq!(
7066            wire_json["providers"][0]["azure"]["apiVersion"],
7067            "2024-10-21"
7068        );
7069        assert_eq!(wire_json["models"][0]["id"], "deploy-1");
7070        assert_eq!(wire_json["models"][0]["provider"], "my-azure");
7071        assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
7072
7073        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
7074            .into_wire()
7075            .expect("default has no duplicate handlers");
7076        let empty_json = serde_json::to_value(&empty_wire).unwrap();
7077        assert!(empty_json.get("providers").is_none());
7078        assert!(empty_json.get("models").is_none());
7079    }
7080
7081    #[test]
7082    fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
7083        use std::path::PathBuf;
7084
7085        let cfg = SessionConfig {
7086            plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
7087            disabled_mcp_servers: Some(vec![
7088                "local-files".to_string(),
7089                "remote-github".to_string(),
7090            ]),
7091            large_output: Some(
7092                LargeToolOutputConfig::new()
7093                    .with_enabled(true)
7094                    .with_max_size_bytes(1024)
7095                    .with_output_directory(PathBuf::from("/tmp/large-output")),
7096            ),
7097            ..Default::default()
7098        };
7099
7100        let (wire, _) = cfg
7101            .into_wire(Some(SessionId::from("sess-1")))
7102            .expect("no duplicate handlers");
7103        let wire_json = serde_json::to_value(&wire).unwrap();
7104        assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
7105        assert_eq!(
7106            wire_json["disabledMcpServers"],
7107            serde_json::json!(["local-files", "remote-github"])
7108        );
7109        assert_eq!(wire_json["largeOutput"]["enabled"], true);
7110        assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
7111        assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
7112
7113        let (empty_wire, _) = SessionConfig::default()
7114            .into_wire(Some(SessionId::from("empty")))
7115            .expect("default has no duplicate handlers");
7116        let empty_json = serde_json::to_value(&empty_wire).unwrap();
7117        assert!(empty_json.get("pluginDirectories").is_none());
7118        assert!(empty_json.get("disabledMcpServers").is_none());
7119        assert!(empty_json.get("largeOutput").is_none());
7120    }
7121
7122    #[test]
7123    fn resume_session_config_into_wire_serializes_bucket_b_fields() {
7124        use std::path::PathBuf;
7125
7126        let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7127        cfg.working_directory = Some(PathBuf::from("/tmp/work"));
7128        cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
7129        cfg.github_token = Some("ghs_secret".to_string());
7130        cfg.include_sub_agent_streaming_events = Some(true);
7131        cfg.enable_session_telemetry = Some(false);
7132        cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
7133        cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
7134        cfg.enable_on_demand_instruction_discovery = Some(false);
7135
7136        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7137        let wire_json = serde_json::to_value(&wire).unwrap();
7138        assert_eq!(wire_json["sessionId"], "sess-1");
7139        assert_eq!(wire_json["workingDirectory"], "/tmp/work");
7140        assert_eq!(wire_json["configDir"], "/tmp/cfg");
7141        assert_eq!(wire_json["gitHubToken"], "ghs_secret");
7142        assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
7143        assert_eq!(wire_json["enableSessionTelemetry"], false);
7144        assert_eq!(wire_json["reasoningSummary"], "detailed");
7145        assert_eq!(wire_json["remoteSession"], "on");
7146        assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
7147
7148        // Unset remote_session is omitted on the wire.
7149        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7150            .into_wire()
7151            .expect("default resume has no duplicate handlers");
7152        let empty_json = serde_json::to_value(&empty_wire).unwrap();
7153        assert!(empty_json.get("reasoningSummary").is_none());
7154        assert!(empty_json.get("remoteSession").is_none());
7155        assert!(
7156            empty_json
7157                .get("enableOnDemandInstructionDiscovery")
7158                .is_none()
7159        );
7160    }
7161
7162    #[test]
7163    fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
7164        use std::path::PathBuf;
7165
7166        let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7167        cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
7168        cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
7169        cfg.large_output = Some(
7170            LargeToolOutputConfig::new()
7171                .with_enabled(false)
7172                .with_max_size_bytes(2048)
7173                .with_output_directory(PathBuf::from("/tmp/large-output-r")),
7174        );
7175
7176        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7177        let wire_json = serde_json::to_value(&wire).unwrap();
7178        assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
7179        assert_eq!(
7180            wire_json["disabledMcpServers"],
7181            serde_json::json!(["local-files-r"])
7182        );
7183        assert_eq!(wire_json["largeOutput"]["enabled"], false);
7184        assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
7185        assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
7186
7187        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7188            .into_wire()
7189            .expect("default resume has no duplicate handlers");
7190        let empty_json = serde_json::to_value(&empty_wire).unwrap();
7191        assert!(empty_json.get("pluginDirectories").is_none());
7192        assert!(empty_json.get("disabledMcpServers").is_none());
7193        assert!(empty_json.get("largeOutput").is_none());
7194    }
7195
7196    #[test]
7197    fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() {
7198        let url = "https://example.com/oauth/client-metadata.json";
7199
7200        let (create_wire, _) = SessionConfig::default()
7201            .with_auth_client_id_metadata_url(url)
7202            .into_wire(None)
7203            .expect("default create has no duplicate handlers");
7204        let create_json = serde_json::to_value(&create_wire).unwrap();
7205        assert_eq!(create_json["authClientIdMetadataUrl"], url);
7206
7207        let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1"))
7208            .with_auth_client_id_metadata_url(url)
7209            .into_wire()
7210            .expect("default resume has no duplicate handlers");
7211        let resume_json = serde_json::to_value(&resume_wire).unwrap();
7212        assert_eq!(resume_json["authClientIdMetadataUrl"], url);
7213
7214        let (empty_create_wire, _) = SessionConfig::default()
7215            .into_wire(None)
7216            .expect("default create has no duplicate handlers");
7217        let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap();
7218        assert!(empty_create_json.get("authClientIdMetadataUrl").is_none());
7219
7220        let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7221            .into_wire()
7222            .expect("default resume has no duplicate handlers");
7223        let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap();
7224        assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none());
7225    }
7226
7227    #[test]
7228    fn session_config_clones_disabled_mcp_servers() {
7229        let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
7230        let mut create_clone = create.clone();
7231        create_clone
7232            .disabled_mcp_servers
7233            .as_mut()
7234            .expect("configured disabled MCP servers")
7235            .push("remote-github".to_string());
7236        assert_eq!(
7237            create.disabled_mcp_servers.as_deref(),
7238            Some(&["local-files".to_string()][..])
7239        );
7240
7241        let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7242            .with_disabled_mcp_servers(["local-files"]);
7243        let mut resume_clone = resume.clone();
7244        resume_clone
7245            .disabled_mcp_servers
7246            .as_mut()
7247            .expect("configured disabled MCP servers")
7248            .push("remote-github".to_string());
7249        assert_eq!(
7250            resume.disabled_mcp_servers.as_deref(),
7251            Some(&["local-files".to_string()][..])
7252        );
7253    }
7254
7255    #[test]
7256    fn session_config_builder_composes() {
7257        use indexmap::IndexMap;
7258
7259        let cfg = SessionConfig::default()
7260            .with_session_id(SessionId::from("sess-1"))
7261            .with_model("claude-sonnet-4")
7262            .with_client_name("test-app")
7263            .with_reasoning_effort("medium")
7264            .with_reasoning_summary(ReasoningSummary::Concise)
7265            .with_context_tier("long_context")
7266            .with_streaming(true)
7267            .with_tools([Tool::new("greet")])
7268            .with_available_tools(["bash", "view"])
7269            .with_excluded_tools(["dangerous"])
7270            .with_mcp_servers(IndexMap::new())
7271            .with_mcp_oauth_token_storage("persistent")
7272            .with_enable_config_discovery(true)
7273            .with_enable_on_demand_instruction_discovery(true)
7274            .with_skill_directories([PathBuf::from("/tmp/skills")])
7275            .with_disabled_skills(["broken-skill"])
7276            .with_disabled_mcp_servers(["local-files"])
7277            .with_agent("researcher")
7278            .with_config_directory(PathBuf::from("/tmp/config"))
7279            .with_working_directory(PathBuf::from("/tmp/work"))
7280            .with_additional_directories([PathBuf::from("/tmp/shared")])
7281            .with_github_token("ghp_test")
7282            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7283            .with_enable_session_telemetry(false)
7284            .with_include_sub_agent_streaming_events(false)
7285            .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7286
7287        assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
7288        assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
7289        assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7290        assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
7291        assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
7292        assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
7293        assert_eq!(cfg.streaming, Some(true));
7294        assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7295        assert_eq!(
7296            cfg.available_tools.as_deref(),
7297            Some(&["bash".to_string(), "view".to_string()][..])
7298        );
7299        assert_eq!(
7300            cfg.excluded_tools.as_deref(),
7301            Some(&["dangerous".to_string()][..])
7302        );
7303        assert!(cfg.mcp_servers.is_some());
7304        assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7305        assert_eq!(cfg.enable_config_discovery, Some(true));
7306        assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
7307        assert_eq!(
7308            cfg.skill_directories.as_deref(),
7309            Some(&[PathBuf::from("/tmp/skills")][..])
7310        );
7311        assert_eq!(
7312            cfg.disabled_skills.as_deref(),
7313            Some(&["broken-skill".to_string()][..])
7314        );
7315        assert_eq!(
7316            cfg.disabled_mcp_servers.as_deref(),
7317            Some(&["local-files".to_string()][..])
7318        );
7319        assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7320        assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7321        assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7322        assert_eq!(
7323            cfg.additional_directories.as_deref(),
7324            Some(&[PathBuf::from("/tmp/shared")][..])
7325        );
7326        assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7327        assert_eq!(
7328            cfg.capi,
7329            Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7330        );
7331        assert_eq!(cfg.enable_session_telemetry, Some(false));
7332        assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
7333        assert_eq!(
7334            cfg.extension_info,
7335            Some(ExtensionInfo::new("github-app", "counter"))
7336        );
7337    }
7338
7339    #[test]
7340    fn resume_session_config_builder_composes() {
7341        use indexmap::IndexMap;
7342
7343        let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
7344            .with_client_name("test-app")
7345            .with_reasoning_summary(ReasoningSummary::None)
7346            .with_context_tier("default")
7347            .with_streaming(true)
7348            .with_tools([Tool::new("greet")])
7349            .with_available_tools(["bash", "view"])
7350            .with_excluded_tools(["dangerous"])
7351            .with_mcp_servers(IndexMap::new())
7352            .with_mcp_oauth_token_storage("persistent")
7353            .with_enable_config_discovery(true)
7354            .with_enable_on_demand_instruction_discovery(false)
7355            .with_skill_directories([PathBuf::from("/tmp/skills")])
7356            .with_disabled_skills(["broken-skill"])
7357            .with_disabled_mcp_servers(["local-files"])
7358            .with_agent("researcher")
7359            .with_config_directory(PathBuf::from("/tmp/config"))
7360            .with_working_directory(PathBuf::from("/tmp/work"))
7361            .with_additional_directories([PathBuf::from("/tmp/shared")])
7362            .with_github_token("ghp_test")
7363            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7364            .with_enable_session_telemetry(false)
7365            .with_include_sub_agent_streaming_events(true)
7366            .with_suppress_resume_event(true)
7367            .with_continue_pending_work(true)
7368            .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7369
7370        assert_eq!(cfg.session_id.as_str(), "sess-2");
7371        assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7372        assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
7373        assert_eq!(cfg.context_tier.as_deref(), Some("default"));
7374        assert_eq!(cfg.streaming, Some(true));
7375        assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7376        assert_eq!(
7377            cfg.available_tools.as_deref(),
7378            Some(&["bash".to_string(), "view".to_string()][..])
7379        );
7380        assert_eq!(
7381            cfg.excluded_tools.as_deref(),
7382            Some(&["dangerous".to_string()][..])
7383        );
7384        assert!(cfg.mcp_servers.is_some());
7385        assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7386        assert_eq!(cfg.enable_config_discovery, Some(true));
7387        assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
7388        assert_eq!(
7389            cfg.skill_directories.as_deref(),
7390            Some(&[PathBuf::from("/tmp/skills")][..])
7391        );
7392        assert_eq!(
7393            cfg.disabled_skills.as_deref(),
7394            Some(&["broken-skill".to_string()][..])
7395        );
7396        assert_eq!(
7397            cfg.disabled_mcp_servers.as_deref(),
7398            Some(&["local-files".to_string()][..])
7399        );
7400        assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7401        assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7402        assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7403        assert_eq!(
7404            cfg.additional_directories.as_deref(),
7405            Some(&[PathBuf::from("/tmp/shared")][..])
7406        );
7407        assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7408        assert_eq!(
7409            cfg.capi,
7410            Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7411        );
7412        assert_eq!(cfg.enable_session_telemetry, Some(false));
7413        assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
7414        assert_eq!(cfg.suppress_resume_event, Some(true));
7415        assert_eq!(cfg.continue_pending_work, Some(true));
7416        assert_eq!(
7417            cfg.extension_info,
7418            Some(ExtensionInfo::new("github-app", "counter"))
7419        );
7420    }
7421
7422    /// `continue_pending_work` must serialize to wire as `continuePendingWork`
7423    /// — the runtime keys off this exact field name to opt into the
7424    /// pending-work-handoff pattern.
7425    #[test]
7426    fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
7427        let cfg =
7428            ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
7429        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7430        let json = serde_json::to_value(&wire).unwrap();
7431        assert_eq!(json["continuePendingWork"], true);
7432
7433        // Unset case — skip_serializing_if must omit the field.
7434        let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7435            .into_wire()
7436            .expect("no duplicate handlers");
7437        let json = serde_json::to_value(&wire).unwrap();
7438        assert!(json.get("continuePendingWork").is_none());
7439    }
7440
7441    #[test]
7442    fn session_configs_serialize_additional_directories() {
7443        let create = SessionConfig::default().with_additional_directories([
7444            PathBuf::from("/tmp/shared"),
7445            PathBuf::from("/tmp/generated"),
7446        ]);
7447        let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
7448        let create_json = serde_json::to_value(&create_wire).unwrap();
7449        assert_eq!(
7450            create_json["additionalDirectories"],
7451            serde_json::json!(["/tmp/shared", "/tmp/generated"])
7452        );
7453
7454        let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7455            .with_additional_directories([PathBuf::from("/tmp/resumed")]);
7456        let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
7457        let resume_json = serde_json::to_value(&resume_wire).unwrap();
7458        assert_eq!(
7459            resume_json["additionalDirectories"],
7460            serde_json::json!(["/tmp/resumed"])
7461        );
7462    }
7463
7464    /// The Rust field is `suppress_resume_event`, but the wire field stays
7465    /// `disableResume` to preserve compatibility with the runtime and other
7466    /// SDKs.
7467    #[test]
7468    fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
7469        let cfg =
7470            ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
7471        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7472        let json = serde_json::to_value(&wire).unwrap();
7473        assert_eq!(json["disableResume"], true);
7474        assert!(json.get("suppressResumeEvent").is_none());
7475    }
7476
7477    /// `instruction_directories` must serialize to wire as
7478    /// `instructionDirectories` on `SessionConfig`.
7479    #[test]
7480    fn session_config_serializes_instruction_directories_to_camel_case() {
7481        let cfg =
7482            SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
7483        let (wire, _) = cfg
7484            .into_wire(Some(SessionId::from("instr-on")))
7485            .expect("no duplicate handlers");
7486        let json = serde_json::to_value(&wire).unwrap();
7487        assert_eq!(
7488            json["instructionDirectories"],
7489            serde_json::json!(["/tmp/instr"])
7490        );
7491
7492        // Unset case — skip_serializing_if must omit the field.
7493        let (wire, _) = SessionConfig::default()
7494            .into_wire(Some(SessionId::from("instr-off")))
7495            .expect("no duplicate handlers");
7496        let json = serde_json::to_value(&wire).unwrap();
7497        assert!(json.get("instructionDirectories").is_none());
7498    }
7499
7500    /// Same check on the resume path. Forwarded to the CLI on
7501    /// `session.resume`.
7502    #[test]
7503    fn resume_session_config_serializes_instruction_directories_to_camel_case() {
7504        let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
7505            .with_instruction_directories([PathBuf::from("/tmp/instr")]);
7506        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7507        let json = serde_json::to_value(&wire).unwrap();
7508        assert_eq!(
7509            json["instructionDirectories"],
7510            serde_json::json!(["/tmp/instr"])
7511        );
7512
7513        let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7514            .into_wire()
7515            .expect("no duplicate handlers");
7516        let json = serde_json::to_value(&wire).unwrap();
7517        assert!(json.get("instructionDirectories").is_none());
7518    }
7519
7520    #[test]
7521    fn custom_agent_config_builder_composes() {
7522        use indexmap::IndexMap;
7523
7524        let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
7525            .with_display_name("Research Assistant")
7526            .with_description("Investigates technical questions.")
7527            .with_tools(["bash", "view"])
7528            .with_mcp_servers(IndexMap::new())
7529            .with_infer(true)
7530            .with_skills(["rust-coding-skill"]);
7531
7532        assert_eq!(cfg.name, "researcher");
7533        assert_eq!(cfg.prompt, "You are a research assistant.");
7534        assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7535        assert_eq!(
7536            cfg.description.as_deref(),
7537            Some("Investigates technical questions.")
7538        );
7539        assert_eq!(
7540            cfg.tools.as_deref(),
7541            Some(&["bash".to_string(), "view".to_string()][..])
7542        );
7543        assert!(cfg.mcp_servers.is_some());
7544        assert_eq!(cfg.infer, Some(true));
7545        assert_eq!(
7546            cfg.skills.as_deref(),
7547            Some(&["rust-coding-skill".to_string()][..])
7548        );
7549    }
7550
7551    #[test]
7552    fn mcp_servers_serialize_in_insertion_order() {
7553        use indexmap::IndexMap;
7554
7555        // Regression: `mcp_servers` was a `HashMap`, so the server keys (and
7556        // thus the `session.create` payload) serialized in a per-process
7557        // random order; `IndexMap` pins them to insertion order. The long
7558        // sequence makes a `HashMap` regression reproduce this exact order by
7559        // chance only 1/N!, avoiding a flaky false pass.
7560        let order = [
7561            "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7562            "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7563        ];
7564        let mut servers = IndexMap::new();
7565        for name in order {
7566            servers.insert(
7567                name.to_string(),
7568                McpServerConfig::Stdio(McpStdioServerConfig {
7569                    command: "run".to_string(),
7570                    ..Default::default()
7571                }),
7572            );
7573        }
7574
7575        let (wire, _runtime) = SessionConfig::default()
7576            .with_mcp_servers(servers)
7577            .into_wire(None)
7578            .expect("into_wire should succeed");
7579        let json = serde_json::to_string(&wire).expect("serialize wire");
7580
7581        let positions: Vec<usize> = order
7582            .iter()
7583            .map(|name| {
7584                json.find(&format!("\"{name}\""))
7585                    .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7586            })
7587            .collect();
7588        let mut ascending = positions.clone();
7589        ascending.sort_unstable();
7590        assert_eq!(
7591            positions, ascending,
7592            "mcp server keys must serialize in insertion order: {json}"
7593        );
7594    }
7595
7596    #[test]
7597    fn infinite_session_config_builder_composes() {
7598        let cfg = InfiniteSessionConfig::new()
7599            .with_enabled(true)
7600            .with_background_compaction_threshold(0.75)
7601            .with_buffer_exhaustion_threshold(0.92);
7602
7603        assert_eq!(cfg.enabled, Some(true));
7604        assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7605        assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7606    }
7607
7608    #[test]
7609    fn provider_config_builder_composes() {
7610        use std::collections::HashMap;
7611
7612        let mut headers = HashMap::new();
7613        headers.insert("X-Custom".to_string(), "value".to_string());
7614
7615        let cfg = ProviderConfig::new("https://api.example.com")
7616            .with_provider_type("openai")
7617            .with_wire_api("completions")
7618            .with_transport("websockets")
7619            .with_api_key("sk-test")
7620            .with_bearer_token("bearer-test")
7621            .with_headers(headers)
7622            .with_model_id("gpt-4")
7623            .with_wire_model("azure-gpt-4-deployment")
7624            .with_max_prompt_tokens(8192)
7625            .with_max_output_tokens(2048);
7626
7627        assert_eq!(cfg.base_url, "https://api.example.com");
7628        assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7629        assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7630        assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7631        assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7632        assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7633        assert_eq!(
7634            cfg.headers
7635                .as_ref()
7636                .and_then(|h| h.get("X-Custom"))
7637                .map(String::as_str),
7638            Some("value"),
7639        );
7640        assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7641        assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7642        assert_eq!(cfg.max_prompt_tokens, Some(8192));
7643        assert_eq!(cfg.max_output_tokens, Some(2048));
7644
7645        // Wire-shape: camelCase, skip_serializing_if when unset.
7646        let wire = serde_json::to_value(&cfg).unwrap();
7647        assert_eq!(wire["modelId"], "gpt-4");
7648        assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7649        assert_eq!(wire["maxPromptTokens"], 8192);
7650        assert_eq!(wire["maxOutputTokens"], 2048);
7651
7652        let unset = ProviderConfig::new("https://api.example.com");
7653        let wire_unset = serde_json::to_value(&unset).unwrap();
7654        assert!(wire_unset.get("modelId").is_none());
7655        assert!(wire_unset.get("wireModel").is_none());
7656        assert!(wire_unset.get("maxPromptTokens").is_none());
7657        assert!(wire_unset.get("maxOutputTokens").is_none());
7658    }
7659
7660    #[test]
7661    fn capi_session_options_builder_composes_and_serializes() {
7662        let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7663
7664        assert_eq!(cfg.enable_web_socket_responses, Some(false));
7665
7666        let wire = serde_json::to_value(&cfg).unwrap();
7667        assert_eq!(
7668            wire,
7669            serde_json::json!({ "enableWebSocketResponses": false })
7670        );
7671
7672        let unset = CapiSessionOptions::new();
7673        let wire_unset = serde_json::to_value(&unset).unwrap();
7674        assert!(wire_unset.get("enableWebSocketResponses").is_none());
7675        assert!(wire_unset.get("autoTier").is_none());
7676        assert_eq!(wire_unset, json!({}));
7677    }
7678
7679    #[test]
7680    fn capi_auto_tier_canonical_values_round_trip_and_forward() {
7681        for (tier, value) in [
7682            (AutoTier::Efficiency, "efficiency"),
7683            (AutoTier::Balance, "balance"),
7684            (AutoTier::Intelligence, "intelligence"),
7685            (AutoTier::Fast, "fast"),
7686        ] {
7687            let exported: crate::AutoTier = tier.clone();
7688            let capi = CapiSessionOptions::new().with_auto_tier(exported);
7689            assert_eq!(capi.auto_tier, Some(tier));
7690            assert_eq!(
7691                serde_json::to_value(&capi).unwrap(),
7692                json!({"autoTier": value})
7693            );
7694            assert_eq!(
7695                serde_json::from_value::<CapiSessionOptions>(json!({"autoTier": value})).unwrap(),
7696                capi
7697            );
7698
7699            let capi = capi.with_enable_web_socket_responses(false);
7700            let expected = json!({"autoTier": value, "enableWebSocketResponses": false});
7701            let (create, _) = SessionConfig::default()
7702                .with_model("auto")
7703                .with_capi(capi.clone())
7704                .into_wire(Some(SessionId::from("capi-create")))
7705                .unwrap();
7706            assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected);
7707
7708            let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7709                .with_capi(capi)
7710                .into_wire()
7711                .unwrap();
7712            assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected);
7713        }
7714    }
7715
7716    #[test]
7717    fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() {
7718        for value in ["balanced", "Balance", "unknown"] {
7719            assert_eq!(
7720                serde_json::from_value::<AutoTier>(json!(value)).unwrap(),
7721                AutoTier::Unknown
7722            );
7723        }
7724        let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap();
7725        assert_eq!(capi.auto_tier, None);
7726    }
7727
7728    #[test]
7729    fn session_config_with_capi_serializes() {
7730        let (wire, _) = SessionConfig::default()
7731            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7732            .into_wire(Some(SessionId::from("capi-create")))
7733            .expect("no duplicate handlers");
7734        let json = serde_json::to_value(&wire).unwrap();
7735        assert_eq!(
7736            json["capi"],
7737            serde_json::json!({ "enableWebSocketResponses": false })
7738        );
7739
7740        let (empty_wire, _) = SessionConfig::default()
7741            .into_wire(Some(SessionId::from("capi-create-unset")))
7742            .expect("no duplicate handlers");
7743        let empty_json = serde_json::to_value(&empty_wire).unwrap();
7744        assert!(empty_json.get("capi").is_none());
7745    }
7746
7747    #[test]
7748    fn resume_session_config_with_capi_serializes() {
7749        let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7750            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7751            .into_wire()
7752            .expect("no duplicate handlers");
7753        let json = serde_json::to_value(&wire).unwrap();
7754        assert_eq!(
7755            json["capi"],
7756            serde_json::json!({ "enableWebSocketResponses": false })
7757        );
7758
7759        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7760            .into_wire()
7761            .expect("no duplicate handlers");
7762        let empty_json = serde_json::to_value(&empty_wire).unwrap();
7763        assert!(empty_json.get("capi").is_none());
7764    }
7765
7766    #[test]
7767    fn system_message_config_builder_composes() {
7768        use std::collections::HashMap;
7769
7770        let cfg = SystemMessageConfig::new()
7771            .with_mode("replace")
7772            .with_content("Custom system message.")
7773            .with_sections(HashMap::new());
7774
7775        assert_eq!(cfg.mode.as_deref(), Some("replace"));
7776        assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7777        assert!(cfg.sections.is_some());
7778    }
7779
7780    #[test]
7781    fn delivery_mode_serializes_to_kebab_case_strings() {
7782        assert_eq!(
7783            serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7784            "\"enqueue\""
7785        );
7786        assert_eq!(
7787            serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7788            "\"immediate\""
7789        );
7790        let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7791        assert_eq!(parsed, DeliveryMode::Immediate);
7792    }
7793
7794    #[test]
7795    fn agent_mode_serializes_to_kebab_case_strings() {
7796        assert_eq!(
7797            serde_json::to_string(&AgentMode::Interactive).unwrap(),
7798            "\"interactive\""
7799        );
7800        assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7801        assert_eq!(
7802            serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7803            "\"autopilot\""
7804        );
7805        assert_eq!(
7806            serde_json::to_string(&AgentMode::Shell).unwrap(),
7807            "\"shell\""
7808        );
7809        let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7810        assert_eq!(parsed, AgentMode::Plan);
7811    }
7812
7813    #[test]
7814    fn connection_state_distinguishes_variants() {
7815        // ConnectionState is now an internal type; verify we can construct
7816        // and compare the variants used by the lifecycle code paths.
7817        assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7818    }
7819
7820    /// `agentId` is the sub-agent attribution field added in copilot-sdk
7821    /// commit f8cf846 ("Derive session event envelopes from schema").
7822    /// Every other SDK (Node, Python, Go, .NET) carries it on the event
7823    /// envelope; Rust must too or sub-agent events lose attribution at
7824    /// the deserialization boundary. Cross-SDK parity test.
7825    #[test]
7826    fn session_event_round_trips_agent_id_on_envelope() {
7827        let wire = json!({
7828            "id": "evt-1",
7829            "timestamp": "2026-04-30T12:00:00Z",
7830            "parentId": null,
7831            "agentId": "sub-agent-42",
7832            "type": "assistant.message",
7833            "data": { "message": "hi" }
7834        });
7835
7836        let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7837        assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7838
7839        // Round-trip preserves the field on the wire.
7840        let roundtripped = serde_json::to_value(&event).unwrap();
7841        assert_eq!(roundtripped["agentId"], "sub-agent-42");
7842
7843        // Absent agentId remains absent (skip_serializing_if).
7844        let main_agent_event: SessionEvent = serde_json::from_value(json!({
7845            "id": "evt-2",
7846            "timestamp": "2026-04-30T12:00:01Z",
7847            "parentId": null,
7848            "type": "session.idle",
7849            "data": {}
7850        }))
7851        .unwrap();
7852        assert!(main_agent_event.agent_id.is_none());
7853        let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7854        assert!(roundtripped.get("agentId").is_none());
7855    }
7856
7857    /// Same parity for the typed event envelope produced by the codegen.
7858    #[test]
7859    fn typed_session_event_round_trips_agent_id_on_envelope() {
7860        let wire = json!({
7861            "id": "evt-1",
7862            "timestamp": "2026-04-30T12:00:00Z",
7863            "parentId": null,
7864            "agentId": "sub-agent-42",
7865            "type": "session.idle",
7866            "data": {}
7867        });
7868
7869        let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7870        assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7871
7872        let roundtripped = serde_json::to_value(&event).unwrap();
7873        assert_eq!(roundtripped["agentId"], "sub-agent-42");
7874    }
7875
7876    #[test]
7877    fn connection_state_variants_compile() {
7878        // Defensive smoke test: all variants must be constructable from
7879        // within the crate. (The enum was demoted from pub to pub(crate)
7880        // in Phase D; this test guards against accidental removal.)
7881        let _ = ConnectionState::Disconnected;
7882        let _ = ConnectionState::Connecting;
7883        let _ = ConnectionState::Connected;
7884        let _ = ConnectionState::Error;
7885    }
7886
7887    #[test]
7888    fn deserializes_runtime_attachment_variants() {
7889        let attachments: Vec<Attachment> = serde_json::from_value(json!([
7890            {
7891                "type": "file",
7892                "path": "/tmp/file.rs",
7893                "displayName": "file.rs",
7894                "lineRange": { "start": 7, "end": 12 }
7895            },
7896            {
7897                "type": "directory",
7898                "path": "/tmp/project",
7899                "displayName": "project"
7900            },
7901            {
7902                "type": "selection",
7903                "filePath": "/tmp/lib.rs",
7904                "displayName": "lib.rs",
7905                "text": "fn main() {}",
7906                "selection": {
7907                    "start": { "line": 1, "character": 2 },
7908                    "end": { "line": 3, "character": 4 }
7909                }
7910            },
7911            {
7912                "type": "blob",
7913                "data": "Zm9v",
7914                "mimeType": "image/png",
7915                "displayName": "image.png"
7916            },
7917            {
7918                "type": "github_reference",
7919                "number": 42,
7920                "title": "Fix rendering",
7921                "referenceType": "issue",
7922                "state": "open",
7923                "url": "https://github.com/example/repo/issues/42"
7924            },
7925            {
7926                "type": "extension_context",
7927                "capturedAt": "2026-09-18T11:00:00Z",
7928                "extensionId": "example:extension",
7929                "title": "Unbound context"
7930            }
7931        ]))
7932        .expect("attachments should deserialize");
7933
7934        assert_eq!(attachments.len(), 6);
7935        assert!(matches!(
7936            &attachments[0],
7937            Attachment::File {
7938                path,
7939                display_name,
7940                line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7941            } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7942        ));
7943        assert!(matches!(
7944            &attachments[1],
7945            Attachment::Directory { path, display_name }
7946                if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7947        ));
7948        assert!(matches!(
7949            &attachments[2],
7950            Attachment::Selection {
7951                file_path,
7952                display_name,
7953                selection:
7954                    AttachmentSelectionRange {
7955                        start: AttachmentSelectionPosition { line: 1, character: 2 },
7956                        end: AttachmentSelectionPosition { line: 3, character: 4 },
7957                    },
7958                ..
7959            } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7960        ));
7961        assert!(matches!(
7962            &attachments[3],
7963            Attachment::Blob {
7964                data,
7965                mime_type,
7966                display_name,
7967            } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7968        ));
7969        assert!(matches!(
7970            &attachments[4],
7971            Attachment::GitHubReference {
7972                number: 42,
7973                title,
7974                reference_type: GitHubReferenceType::Issue,
7975                state,
7976                url,
7977            } if title == "Fix rendering"
7978                && state == "open"
7979                && url == "https://github.com/example/repo/issues/42"
7980        ));
7981        assert!(matches!(
7982            &attachments[5],
7983            Attachment::ExtensionContext {
7984                captured_at,
7985                extension_id,
7986                canvas_id: None,
7987                instance_id: None,
7988                title,
7989                payload: None,
7990            } if captured_at == "2026-09-18T11:00:00Z"
7991                && extension_id == "example:extension"
7992                && title == "Unbound context"
7993        ));
7994        assert_eq!(
7995            serde_json::to_value(&attachments[5]).expect("serialize extension context"),
7996            json!({
7997                "type": "extension_context",
7998                "capturedAt": "2026-09-18T11:00:00Z",
7999                "extensionId": "example:extension",
8000                "title": "Unbound context"
8001            })
8002        );
8003    }
8004
8005    #[test]
8006    fn ensures_display_names_for_variants_that_support_them() {
8007        let mut attachments = vec![
8008            Attachment::File {
8009                path: PathBuf::from("/tmp/file.rs"),
8010                display_name: None,
8011                line_range: None,
8012            },
8013            Attachment::Selection {
8014                file_path: PathBuf::from("/tmp/src/lib.rs"),
8015                display_name: None,
8016                text: "fn main() {}".to_string(),
8017                selection: AttachmentSelectionRange {
8018                    start: AttachmentSelectionPosition {
8019                        line: 0,
8020                        character: 0,
8021                    },
8022                    end: AttachmentSelectionPosition {
8023                        line: 0,
8024                        character: 10,
8025                    },
8026                },
8027            },
8028            Attachment::Blob {
8029                data: "Zm9v".to_string(),
8030                mime_type: "image/png".to_string(),
8031                display_name: None,
8032            },
8033            Attachment::GitHubReference {
8034                number: 7,
8035                title: "Track regressions".to_string(),
8036                reference_type: GitHubReferenceType::Issue,
8037                state: "open".to_string(),
8038                url: "https://example.com/issues/7".to_string(),
8039            },
8040        ];
8041
8042        ensure_attachment_display_names(&mut attachments);
8043
8044        assert_eq!(attachments[0].display_name(), Some("file.rs"));
8045        assert_eq!(attachments[1].display_name(), Some("lib.rs"));
8046        assert_eq!(attachments[2].display_name(), Some("attachment"));
8047        assert_eq!(attachments[3].display_name(), None);
8048        assert_eq!(
8049            attachments[3].label(),
8050            Some("Track regressions".to_string())
8051        );
8052    }
8053
8054    #[test]
8055    fn github_anchored_attachment_variants_round_trip() {
8056        let cases = vec![
8057            (
8058                "github_commit",
8059                json!({
8060                    "type": "github_commit",
8061                    "message": "Fix the thing",
8062                    "oid": "abc123",
8063                    "repo": { "id": 1, "name": "repo", "owner": "octocat" },
8064                    "url": "https://github.com/octocat/repo/commit/abc123"
8065                }),
8066            ),
8067            (
8068                "github_release",
8069                json!({
8070                    "type": "github_release",
8071                    "name": "v1.2.3",
8072                    "repo": { "name": "repo", "owner": "octocat" },
8073                    "tagName": "v1.2.3",
8074                    "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
8075                }),
8076            ),
8077            (
8078                "github_actions_job",
8079                json!({
8080                    "type": "github_actions_job",
8081                    "conclusion": "failure",
8082                    "jobId": 99,
8083                    "jobName": "build",
8084                    "repo": { "name": "repo", "owner": "octocat" },
8085                    "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
8086                    "workflowName": "CI"
8087                }),
8088            ),
8089            (
8090                "github_repository",
8091                json!({
8092                    "type": "github_repository",
8093                    "description": "An example repository",
8094                    "ref": "main",
8095                    "repo": { "name": "repo", "owner": "octocat" },
8096                    "url": "https://github.com/octocat/repo"
8097                }),
8098            ),
8099            (
8100                "github_file_diff",
8101                json!({
8102                    "type": "github_file_diff",
8103                    "base": {
8104                        "path": "src/lib.rs",
8105                        "ref": "main",
8106                        "repo": { "name": "repo", "owner": "octocat" }
8107                    },
8108                    "head": {
8109                        "path": "src/lib.rs",
8110                        "ref": "feature",
8111                        "repo": { "name": "repo", "owner": "octocat" }
8112                    },
8113                    "url": "https://github.com/octocat/repo/compare/main...feature"
8114                }),
8115            ),
8116            (
8117                "github_tree_comparison",
8118                json!({
8119                    "type": "github_tree_comparison",
8120                    "base": {
8121                        "repo": { "name": "repo", "owner": "octocat" },
8122                        "revision": "main"
8123                    },
8124                    "head": {
8125                        "repo": { "name": "repo", "owner": "octocat" },
8126                        "revision": "feature"
8127                    },
8128                    "url": "https://github.com/octocat/repo/compare/main...feature"
8129                }),
8130            ),
8131            (
8132                "github_url",
8133                json!({
8134                    "type": "github_url",
8135                    "url": "https://github.com/octocat/repo/wiki"
8136                }),
8137            ),
8138            (
8139                "github_file",
8140                json!({
8141                    "type": "github_file",
8142                    "path": "src/main.rs",
8143                    "ref": "main",
8144                    "repo": { "name": "repo", "owner": "octocat" },
8145                    "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
8146                }),
8147            ),
8148            (
8149                "github_snippet",
8150                json!({
8151                    "type": "github_snippet",
8152                    "lineRange": { "start": 10, "end": 20 },
8153                    "path": "src/main.rs",
8154                    "ref": "main",
8155                    "repo": { "name": "repo", "owner": "octocat" },
8156                    "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
8157                }),
8158            ),
8159        ];
8160
8161        for (expected_type, input) in cases {
8162            let attachment: Attachment = serde_json::from_value(input.clone())
8163                .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
8164
8165            // Serialize to a string first: parsing into `serde_json::Value` would
8166            // silently dedupe a duplicate `type` key, hiding the exact regression
8167            // this test guards against (e.g. a wrapped generated struct emitting its
8168            // own `type` alongside the enum tag).
8169            let serialized_string = serde_json::to_string(&attachment)
8170                .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
8171
8172            // Exactly one `type` key, carrying the expected discriminator.
8173            assert_eq!(
8174                serialized_string.matches("\"type\":").count(),
8175                1,
8176                "{expected_type} must serialize a single `type` key"
8177            );
8178
8179            let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
8180                .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
8181            assert_eq!(
8182                serialized.get("type").and_then(|value| value.as_str()),
8183                Some(expected_type),
8184                "{expected_type} must serialize the correct discriminator"
8185            );
8186
8187            // Round-trips without dropping fields.
8188            assert_eq!(
8189                serialized, input,
8190                "{expected_type} should round-trip without data loss"
8191            );
8192            let reparsed: Attachment = serde_json::from_value(serialized)
8193                .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
8194            assert_eq!(
8195                reparsed, attachment,
8196                "{expected_type} should re-deserialize to the same value"
8197            );
8198        }
8199    }
8200}
8201
8202#[cfg(test)]
8203mod permission_builder_tests {
8204    use std::sync::Arc;
8205
8206    use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
8207    use crate::permission;
8208    use crate::types::{
8209        PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
8210        SessionId,
8211    };
8212
8213    fn data() -> PermissionRequestData {
8214        PermissionRequestData {
8215            extra: serde_json::json!({"tool": "shell"}),
8216            ..Default::default()
8217        }
8218    }
8219
8220    /// Apply the same policy-resolution logic that `Client::create_session`
8221    /// uses, so tests exercise the effective handler.
8222    fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8223        permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8224    }
8225
8226    fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8227        permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8228    }
8229
8230    async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
8231        handler
8232            .handle(SessionId::from("s1"), RequestId::new("1"), data())
8233            .await
8234    }
8235
8236    #[tokio::test]
8237    async fn approve_all_with_handler_present_approves() {
8238        let cfg = SessionConfig::default()
8239            .with_permission_handler(Arc::new(ApproveAllHandler))
8240            .approve_all_permissions();
8241        let h = resolve_create(cfg).expect("policy + handler yields handler");
8242        assert!(matches!(
8243            dispatch(&h).await,
8244            PermissionResult::Decision {
8245                decision: PermissionDecision::ApproveOnce(_),
8246                ..
8247            }
8248        ));
8249    }
8250
8251    #[tokio::test]
8252    async fn approve_all_standalone_produces_handler() {
8253        let cfg = SessionConfig::default().approve_all_permissions();
8254        let h = resolve_create(cfg).expect("policy alone yields handler");
8255        assert!(matches!(
8256            dispatch(&h).await,
8257            PermissionResult::Decision {
8258                decision: PermissionDecision::ApproveOnce(_),
8259                ..
8260            }
8261        ));
8262    }
8263
8264    /// Phase I: order between with_permission_handler and the policy
8265    /// builder must not matter.
8266    #[tokio::test]
8267    async fn approve_all_is_order_independent() {
8268        let a = SessionConfig::default()
8269            .with_permission_handler(Arc::new(ApproveAllHandler))
8270            .approve_all_permissions();
8271        let b = SessionConfig::default()
8272            .approve_all_permissions()
8273            .with_permission_handler(Arc::new(ApproveAllHandler));
8274        let ha = resolve_create(a).unwrap();
8275        let hb = resolve_create(b).unwrap();
8276        assert!(matches!(
8277            dispatch(&ha).await,
8278            PermissionResult::Decision {
8279                decision: PermissionDecision::ApproveOnce(_),
8280                ..
8281            }
8282        ));
8283        assert!(matches!(
8284            dispatch(&hb).await,
8285            PermissionResult::Decision {
8286                decision: PermissionDecision::ApproveOnce(_),
8287                ..
8288            }
8289        ));
8290    }
8291
8292    #[tokio::test]
8293    async fn deny_all_is_order_independent() {
8294        let a = SessionConfig::default()
8295            .with_permission_handler(Arc::new(ApproveAllHandler))
8296            .deny_all_permissions();
8297        let b = SessionConfig::default()
8298            .deny_all_permissions()
8299            .with_permission_handler(Arc::new(ApproveAllHandler));
8300        let ha = resolve_create(a).unwrap();
8301        let hb = resolve_create(b).unwrap();
8302        assert!(matches!(
8303            dispatch(&ha).await,
8304            PermissionResult::Decision {
8305                decision: PermissionDecision::Reject(_),
8306                ..
8307            }
8308        ));
8309        assert!(matches!(
8310            dispatch(&hb).await,
8311            PermissionResult::Decision {
8312                decision: PermissionDecision::Reject(_),
8313                ..
8314            }
8315        ));
8316    }
8317
8318    #[tokio::test]
8319    async fn approve_permissions_if_consults_predicate() {
8320        let cfg = SessionConfig::default().approve_permissions_if(|d| {
8321            d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8322        });
8323        let h = resolve_create(cfg).unwrap();
8324        assert!(matches!(
8325            dispatch(&h).await,
8326            PermissionResult::Decision {
8327                decision: PermissionDecision::Reject(_),
8328                ..
8329            }
8330        ));
8331    }
8332
8333    #[tokio::test]
8334    async fn approve_permissions_if_is_order_independent() {
8335        let predicate = |d: &PermissionRequestData| {
8336            d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8337        };
8338        let a = SessionConfig::default()
8339            .with_permission_handler(Arc::new(ApproveAllHandler))
8340            .approve_permissions_if(predicate);
8341        let b = SessionConfig::default()
8342            .approve_permissions_if(predicate)
8343            .with_permission_handler(Arc::new(ApproveAllHandler));
8344        let ha = resolve_create(a).unwrap();
8345        let hb = resolve_create(b).unwrap();
8346        assert!(matches!(
8347            dispatch(&ha).await,
8348            PermissionResult::Decision {
8349                decision: PermissionDecision::Reject(_),
8350                ..
8351            }
8352        ));
8353        assert!(matches!(
8354            dispatch(&hb).await,
8355            PermissionResult::Decision {
8356                decision: PermissionDecision::Reject(_),
8357                ..
8358            }
8359        ));
8360    }
8361
8362    #[tokio::test]
8363    async fn resume_session_config_approve_all_works() {
8364        let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
8365            .with_permission_handler(Arc::new(ApproveAllHandler))
8366            .approve_all_permissions();
8367        let h = resolve_resume(cfg).unwrap();
8368        assert!(matches!(
8369            dispatch(&h).await,
8370            PermissionResult::Decision {
8371                decision: PermissionDecision::ApproveOnce(_),
8372                ..
8373            }
8374        ));
8375    }
8376
8377    #[tokio::test]
8378    async fn resume_session_config_approve_all_is_order_independent() {
8379        let a = ResumeSessionConfig::new(SessionId::from("s1"))
8380            .with_permission_handler(Arc::new(ApproveAllHandler))
8381            .approve_all_permissions();
8382        let b = ResumeSessionConfig::new(SessionId::from("s1"))
8383            .approve_all_permissions()
8384            .with_permission_handler(Arc::new(ApproveAllHandler));
8385        let ha = resolve_resume(a).unwrap();
8386        let hb = resolve_resume(b).unwrap();
8387        assert!(matches!(
8388            dispatch(&ha).await,
8389            PermissionResult::Decision {
8390                decision: PermissionDecision::ApproveOnce(_),
8391                ..
8392            }
8393        ));
8394        assert!(matches!(
8395            dispatch(&hb).await,
8396            PermissionResult::Decision {
8397                decision: PermissionDecision::ApproveOnce(_),
8398                ..
8399            }
8400        ));
8401    }
8402
8403    #[test]
8404    fn session_config_enable_experimental_mode_serializes_when_set() {
8405        let cfg = SessionConfig::default().with_enable_experimental_mode(false);
8406        assert_eq!(cfg.enable_experimental_mode, Some(false));
8407
8408        let (wire, _runtime) = cfg
8409            .into_wire(Some(SessionId::from("experimental-mode")))
8410            .expect("enable_experimental_mode config has no duplicate handlers");
8411        assert_eq!(wire.is_experimental_mode, Some(false));
8412
8413        let json = serde_json::to_value(&wire).unwrap();
8414        assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8415    }
8416
8417    #[test]
8418    fn session_config_enable_experimental_mode_omitted_when_none() {
8419        let cfg = SessionConfig::default();
8420        assert_eq!(cfg.enable_experimental_mode, None);
8421
8422        let (wire, _runtime) = cfg
8423            .into_wire(Some(SessionId::from("no-experimental-mode")))
8424            .expect("default config has no duplicate handlers");
8425        assert_eq!(wire.is_experimental_mode, None);
8426
8427        let json = serde_json::to_value(&wire).unwrap();
8428        assert!(json.get("isExperimentalMode").is_none());
8429    }
8430
8431    #[test]
8432    fn resume_session_config_enable_experimental_mode_serializes_when_set() {
8433        let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
8434            .with_enable_experimental_mode(false);
8435        assert_eq!(cfg.enable_experimental_mode, Some(false));
8436
8437        let (wire, _runtime) = cfg
8438            .into_wire()
8439            .expect("resume enable_experimental_mode config has no duplicate handlers");
8440        assert_eq!(wire.is_experimental_mode, Some(false));
8441
8442        let json = serde_json::to_value(&wire).unwrap();
8443        assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8444    }
8445
8446    #[test]
8447    fn resume_session_config_enable_experimental_mode_omitted_when_none() {
8448        let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
8449        assert_eq!(cfg.enable_experimental_mode, None);
8450
8451        let (wire, _runtime) = cfg
8452            .into_wire()
8453            .expect("default resume config has no duplicate handlers");
8454        assert_eq!(wire.is_experimental_mode, None);
8455
8456        let json = serde_json::to_value(&wire).unwrap();
8457        assert!(json.get("isExperimentalMode").is_none());
8458    }
8459}
8460
8461#[cfg(test)]
8462mod is_terminal_tests {
8463    use super::Tool;
8464
8465    #[test]
8466    fn is_terminal_serializes_as_camel_case_when_set() {
8467        let tool = Tool {
8468            name: "clear_context".to_owned(),
8469            is_terminal: true,
8470            ..Default::default()
8471        };
8472        let value = serde_json::to_value(&tool).expect("tool serializes");
8473        assert_eq!(
8474            value.get("isTerminal"),
8475            Some(&serde_json::Value::Bool(true))
8476        );
8477    }
8478
8479    #[test]
8480    fn is_terminal_is_omitted_when_false() {
8481        let tool = Tool {
8482            name: "plain".to_owned(),
8483            ..Default::default()
8484        };
8485        let value = serde_json::to_value(&tool).expect("tool serializes");
8486        assert!(value.get("isTerminal").is_none());
8487    }
8488
8489    /// `Tool` has a hand-written `Debug` impl, so a new field is only reported
8490    /// if it is added there by hand. Guard against that drift.
8491    #[test]
8492    fn is_terminal_appears_in_debug_output() {
8493        let terminal = Tool {
8494            name: "clear_context".to_owned(),
8495            is_terminal: true,
8496            ..Default::default()
8497        };
8498        assert!(format!("{terminal:?}").contains("is_terminal: true"));
8499
8500        let plain = Tool {
8501            name: "plain".to_owned(),
8502            ..Default::default()
8503        };
8504        assert!(format!("{plain:?}").contains("is_terminal: false"));
8505    }
8506}