Skip to main content

github_copilot_sdk/
types.rs

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