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