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