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