Skip to main content

github_copilot_sdk/
types.rs

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