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