Skip to main content

github_copilot_sdk/
types.rs

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