Skip to main content

github_copilot_sdk/
types.rs

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