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