Skip to main content

github_copilot_sdk/
types.rs

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