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