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 bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1057    #[serde(default, skip_serializing_if = "Option::is_none")]
1058    pub(crate) has_bearer_token_provider: Option<bool>,
1059    /// Azure-specific options.
1060    #[serde(default, skip_serializing_if = "Option::is_none")]
1061    pub azure: Option<AzureProviderOptions>,
1062    /// Custom HTTP headers included in outbound provider requests.
1063    #[serde(default, skip_serializing_if = "Option::is_none")]
1064    pub headers: Option<HashMap<String, String>>,
1065    /// Well-known model ID used to look up agent config and default token
1066    /// limits. Also used as the wire model when [`wire_model`](Self::wire_model)
1067    /// is unset. Falls back to [`SessionConfig::model`](crate::SessionConfig::model).
1068    #[serde(default, skip_serializing_if = "Option::is_none")]
1069    pub model_id: Option<String>,
1070    /// Model name sent to the provider API for inference. Use this when
1071    /// the provider's model name (e.g. an Azure deployment name or a
1072    /// custom fine-tune name) differs from
1073    /// [`model_id`](Self::model_id). Falls back to
1074    /// [`model_id`](Self::model_id), then to
1075    /// [`SessionConfig::model`](crate::SessionConfig::model).
1076    #[serde(default, skip_serializing_if = "Option::is_none")]
1077    pub wire_model: Option<String>,
1078    /// Overrides the resolved model's default max prompt tokens. The
1079    /// runtime triggers conversation compaction before sending a request
1080    /// when the prompt (system message, history, tool definitions, user
1081    /// message) would exceed this limit.
1082    #[serde(default, skip_serializing_if = "Option::is_none")]
1083    pub max_prompt_tokens: Option<i64>,
1084    /// Overrides the resolved model's default max output tokens. When
1085    /// hit, the model stops generating and returns a truncated response.
1086    #[serde(default, skip_serializing_if = "Option::is_none")]
1087    pub max_output_tokens: Option<i64>,
1088}
1089
1090impl std::fmt::Debug for ProviderConfig {
1091    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1092        f.debug_struct("ProviderConfig")
1093            .field("provider_type", &self.provider_type)
1094            .field("wire_api", &self.wire_api)
1095            .field("transport", &self.transport)
1096            .field("base_url", &self.base_url)
1097            .field("api_key", &self.api_key)
1098            .field("bearer_token", &self.bearer_token)
1099            .field(
1100                "bearer_token_provider",
1101                &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1102            )
1103            .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1104            .field("azure", &self.azure)
1105            .field("headers", &self.headers)
1106            .field("model_id", &self.model_id)
1107            .field("wire_model", &self.wire_model)
1108            .field("max_prompt_tokens", &self.max_prompt_tokens)
1109            .field("max_output_tokens", &self.max_output_tokens)
1110            .finish()
1111    }
1112}
1113
1114impl ProviderConfig {
1115    /// Construct a [`ProviderConfig`] with the required `base_url` set;
1116    /// all other fields default to unset.
1117    pub fn new(base_url: impl Into<String>) -> Self {
1118        Self {
1119            base_url: base_url.into(),
1120            ..Self::default()
1121        }
1122    }
1123
1124    /// Set the provider type (`"openai"`, `"azure"`, or `"anthropic"`).
1125    pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1126        self.provider_type = Some(provider_type.into());
1127        self
1128    }
1129
1130    /// Set the API format (`"completions"` or `"responses"`; openai/azure only).
1131    pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1132        self.wire_api = Some(wire_api.into());
1133        self
1134    }
1135
1136    /// Set the transport (`"http"` or `"websockets"`) for OpenAI Responses
1137    /// requests. Defaults to `"http"`.
1138    pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1139        self.transport = Some(transport.into());
1140        self
1141    }
1142
1143    /// Set the API key. Optional for local providers like Ollama.
1144    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1145        self.api_key = Some(api_key.into());
1146        self
1147    }
1148
1149    /// Set the bearer token used to populate the `Authorization` header.
1150    /// Takes precedence over `api_key` when both are set.
1151    pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1152        self.bearer_token = Some(bearer_token.into());
1153        self
1154    }
1155
1156    /// Set the callback used to acquire a bearer token before each outbound
1157    /// request to this provider.
1158    ///
1159    /// **Experimental.** This method is part of an experimental wire-protocol
1160    /// surface and may change or be removed in a future release.
1161    pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1162        self.bearer_token_provider = Some(provider);
1163        self
1164    }
1165
1166    /// Set Azure-specific options.
1167    pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1168        self.azure = Some(azure);
1169        self
1170    }
1171
1172    /// Set the custom HTTP headers attached to outbound provider requests.
1173    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1174        self.headers = Some(headers);
1175        self
1176    }
1177
1178    /// Set the well-known model ID used to look up agent config and default
1179    /// token limits. Falls back to the session's configured model when unset.
1180    pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1181        self.model_id = Some(model_id.into());
1182        self
1183    }
1184
1185    /// Set the model name sent to the provider API for inference. Use this
1186    /// when the provider's model name (e.g. an Azure deployment name or a
1187    /// custom fine-tune name) differs from
1188    /// [`model_id`](Self::model_id).
1189    pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1190        self.wire_model = Some(wire_model.into());
1191        self
1192    }
1193
1194    /// Override the resolved model's default max prompt tokens. The
1195    /// runtime triggers conversation compaction when the prompt would
1196    /// exceed this limit.
1197    pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1198        self.max_prompt_tokens = Some(max);
1199        self
1200    }
1201
1202    /// Override the resolved model's default max output tokens. When
1203    /// hit, the model stops generating and returns a truncated response.
1204    pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1205        self.max_output_tokens = Some(max);
1206        self
1207    }
1208}
1209
1210/// Provider-scoped Copilot API (CAPI) session options.
1211///
1212/// WebSocket transport is the default for the CAPI Responses API whenever
1213/// the model advertises the `ws:/responses` endpoint. Set
1214/// [`enable_web_socket_responses`](Self::enable_web_socket_responses) to
1215/// `false` to force the HTTP Responses transport instead, which is useful
1216/// for users behind proxies where WebSockets fail.
1217///
1218/// Setting it to `false` is equivalent to setting the
1219/// `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. The option
1220/// is scoped under the `capi` namespace because a single session can host
1221/// multiple providers, so transport choice is provider-level.
1222#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1223#[serde(rename_all = "camelCase")]
1224#[non_exhaustive]
1225pub struct CapiSessionOptions {
1226    /// Whether to use WebSocket transport for CAPI Responses API calls.
1227    ///
1228    /// When `Some(false)`, the runtime uses HTTP Responses transport even if
1229    /// the selected model advertises `ws:/responses`. When unset, the runtime
1230    /// default applies (WebSocket transport when advertised).
1231    #[serde(default, skip_serializing_if = "Option::is_none")]
1232    pub enable_web_socket_responses: Option<bool>,
1233}
1234
1235impl CapiSessionOptions {
1236    /// Construct CAPI session options with all fields unset.
1237    pub fn new() -> Self {
1238        Self::default()
1239    }
1240
1241    /// Set whether to use WebSocket transport for CAPI Responses API calls.
1242    pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1243        self.enable_web_socket_responses = Some(enable);
1244        self
1245    }
1246}
1247
1248/// Azure-specific provider options.
1249#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1250#[serde(rename_all = "camelCase")]
1251pub struct AzureProviderOptions {
1252    /// Azure API version. Defaults to `"2024-10-21"`.
1253    #[serde(default, skip_serializing_if = "Option::is_none")]
1254    pub api_version: Option<String>,
1255}
1256
1257/// A named BYOK provider connection in the multi-provider registry.
1258///
1259/// **Experimental.** Multi-provider BYOK configuration is part of an
1260/// experimental surface and may change or be removed in a future release.
1261///
1262/// Unlike [`ProviderConfig`], which routes the whole session through a
1263/// single provider, named providers are additive: the session keeps its
1264/// default Copilot routing and exposes these providers' models alongside
1265/// it. Models are attached via [`ProviderModelConfig`], which references a
1266/// provider by [`name`](Self::name).
1267#[derive(Clone, Default, Serialize, Deserialize)]
1268#[serde(rename_all = "camelCase")]
1269#[non_exhaustive]
1270pub struct NamedProviderConfig {
1271    /// Unique name used by [`ProviderModelConfig::provider`] to reference
1272    /// this connection.
1273    pub name: String,
1274    /// Provider type: `"openai"`, `"azure"`, or `"anthropic"`. Defaults to
1275    /// `"openai"` on the CLI.
1276    #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1277    pub provider_type: Option<String>,
1278    /// API format (openai/azure only): `"completions"` or `"responses"`.
1279    /// Defaults to `"completions"`.
1280    #[serde(default, skip_serializing_if = "Option::is_none")]
1281    pub wire_api: Option<String>,
1282    /// API endpoint URL.
1283    pub base_url: String,
1284    /// API key. Optional for local providers like Ollama.
1285    #[serde(default, skip_serializing_if = "Option::is_none")]
1286    pub api_key: Option<String>,
1287    /// Bearer token for authentication. Sets the `Authorization` header
1288    /// directly. Takes precedence over `api_key` when both are set.
1289    #[serde(default, skip_serializing_if = "Option::is_none")]
1290    pub bearer_token: Option<String>,
1291    /// **Experimental.** Callback used to acquire a bearer token before each
1292    /// outbound request to this provider.
1293    #[serde(skip)]
1294    pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1295    #[serde(default, skip_serializing_if = "Option::is_none")]
1296    pub(crate) has_bearer_token_provider: Option<bool>,
1297    /// Azure-specific options.
1298    #[serde(default, skip_serializing_if = "Option::is_none")]
1299    pub azure: Option<AzureProviderOptions>,
1300    /// Custom HTTP headers included in outbound provider requests.
1301    #[serde(default, skip_serializing_if = "Option::is_none")]
1302    pub headers: Option<HashMap<String, String>>,
1303}
1304
1305impl std::fmt::Debug for NamedProviderConfig {
1306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1307        f.debug_struct("NamedProviderConfig")
1308            .field("name", &self.name)
1309            .field("provider_type", &self.provider_type)
1310            .field("wire_api", &self.wire_api)
1311            .field("base_url", &self.base_url)
1312            .field("api_key", &self.api_key)
1313            .field("bearer_token", &self.bearer_token)
1314            .field(
1315                "bearer_token_provider",
1316                &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1317            )
1318            .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1319            .field("azure", &self.azure)
1320            .field("headers", &self.headers)
1321            .finish()
1322    }
1323}
1324
1325impl NamedProviderConfig {
1326    /// Construct a [`NamedProviderConfig`] with the required `name` and
1327    /// `base_url` set; all other fields default to unset.
1328    pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1329        Self {
1330            name: name.into(),
1331            base_url: base_url.into(),
1332            ..Self::default()
1333        }
1334    }
1335
1336    /// Set the provider type (`"openai"`, `"azure"`, or `"anthropic"`).
1337    pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1338        self.provider_type = Some(provider_type.into());
1339        self
1340    }
1341
1342    /// Set the API format (`"completions"` or `"responses"`; openai/azure only).
1343    pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1344        self.wire_api = Some(wire_api.into());
1345        self
1346    }
1347
1348    /// Set the API key. Optional for local providers like Ollama.
1349    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1350        self.api_key = Some(api_key.into());
1351        self
1352    }
1353
1354    /// Set the bearer token used to populate the `Authorization` header.
1355    /// Takes precedence over `api_key` when both are set.
1356    pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1357        self.bearer_token = Some(bearer_token.into());
1358        self
1359    }
1360
1361    /// Set the callback used to acquire a bearer token before each outbound
1362    /// request to this provider.
1363    ///
1364    /// **Experimental.** This method is part of an experimental wire-protocol
1365    /// surface and may change or be removed in a future release.
1366    pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1367        self.bearer_token_provider = Some(provider);
1368        self
1369    }
1370
1371    /// Set Azure-specific options.
1372    pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1373        self.azure = Some(azure);
1374        self
1375    }
1376
1377    /// Set the custom HTTP headers attached to outbound provider requests.
1378    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1379        self.headers = Some(headers);
1380        self
1381    }
1382}
1383
1384fn prepare_bearer_token_providers(
1385    provider: &mut Option<ProviderConfig>,
1386    providers: &mut Option<Vec<NamedProviderConfig>>,
1387) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1388    let mut bearer_token_providers = HashMap::new();
1389
1390    if let Some(provider) = provider.as_mut()
1391        && let Some(token_provider) = provider.bearer_token_provider.take()
1392    {
1393        provider.has_bearer_token_provider = Some(true);
1394        bearer_token_providers.insert("default".to_string(), token_provider);
1395    }
1396
1397    if let Some(providers) = providers.as_mut() {
1398        for provider in providers {
1399            if let Some(token_provider) = provider.bearer_token_provider.take() {
1400                provider.has_bearer_token_provider = Some(true);
1401                bearer_token_providers.insert(provider.name.clone(), token_provider);
1402            }
1403        }
1404    }
1405
1406    bearer_token_providers
1407}
1408
1409/// A BYOK model definition in the multi-provider registry.
1410///
1411/// **Experimental.** Multi-provider BYOK configuration is part of an
1412/// experimental surface and may change or be removed in a future release.
1413///
1414/// References a [`NamedProviderConfig`] by [`provider`](Self::provider) and
1415/// becomes selectable under the provider-qualified id `provider/id`.
1416#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1417#[serde(rename_all = "camelCase")]
1418#[non_exhaustive]
1419pub struct ProviderModelConfig {
1420    /// Model identifier, unique within its provider. Combined with
1421    /// [`provider`](Self::provider) to form the selection id `provider/id`.
1422    pub id: String,
1423    /// Name of the [`NamedProviderConfig`] this model is served by.
1424    pub provider: String,
1425    /// Model name sent to the provider API for inference. Use when the
1426    /// provider's model name differs from [`id`](Self::id).
1427    #[serde(default, skip_serializing_if = "Option::is_none")]
1428    pub wire_model: Option<String>,
1429    /// Well-known model ID used to look up agent config and default token
1430    /// limits.
1431    #[serde(default, skip_serializing_if = "Option::is_none")]
1432    pub model_id: Option<String>,
1433    /// Human-readable display name.
1434    #[serde(default, skip_serializing_if = "Option::is_none")]
1435    pub name: Option<String>,
1436    /// Overrides the resolved model's default max prompt tokens.
1437    #[serde(default, skip_serializing_if = "Option::is_none")]
1438    pub max_prompt_tokens: Option<i64>,
1439    /// Overrides the resolved model's default max context window tokens.
1440    #[serde(default, skip_serializing_if = "Option::is_none")]
1441    pub max_context_window_tokens: Option<i64>,
1442    /// Overrides the resolved model's default max output tokens.
1443    #[serde(default, skip_serializing_if = "Option::is_none")]
1444    pub max_output_tokens: Option<i64>,
1445    /// Per-property overrides for model capabilities, deep-merged over
1446    /// runtime defaults.
1447    #[serde(default, skip_serializing_if = "Option::is_none")]
1448    pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1449}
1450
1451impl ProviderModelConfig {
1452    /// Construct a [`ProviderModelConfig`] with the required `id` and
1453    /// `provider` set; all other fields default to unset.
1454    pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1455        Self {
1456            id: id.into(),
1457            provider: provider.into(),
1458            ..Self::default()
1459        }
1460    }
1461
1462    /// Set the model name sent to the provider API for inference.
1463    pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1464        self.wire_model = Some(wire_model.into());
1465        self
1466    }
1467
1468    /// Set the well-known model ID used to look up agent config and default
1469    /// token limits.
1470    pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1471        self.model_id = Some(model_id.into());
1472        self
1473    }
1474
1475    /// Set the human-readable display name.
1476    pub fn with_name(mut self, name: impl Into<String>) -> Self {
1477        self.name = Some(name.into());
1478        self
1479    }
1480
1481    /// Override the resolved model's default max prompt tokens.
1482    pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1483        self.max_prompt_tokens = Some(max);
1484        self
1485    }
1486
1487    /// Override the resolved model's default max context window tokens.
1488    pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1489        self.max_context_window_tokens = Some(max);
1490        self
1491    }
1492
1493    /// Override the resolved model's default max output tokens.
1494    pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1495        self.max_output_tokens = Some(max);
1496        self
1497    }
1498
1499    /// Set per-property model capability overrides.
1500    pub fn with_capabilities(
1501        mut self,
1502        capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1503    ) -> Self {
1504        self.capabilities = Some(capabilities);
1505        self
1506    }
1507}
1508
1509/// Configuration for creating a new session via the `session.create` RPC.
1510///
1511/// All fields are optional — the CLI applies sensible defaults.
1512///
1513/// # Construction
1514///
1515/// Two equivalent shapes are supported:
1516///
1517/// 1. **Chained builder** (preferred for compile-time-known values):
1518///
1519///    ```
1520///    # use github_copilot_sdk::types::SessionConfig;
1521///    let cfg = SessionConfig::default()
1522///        .with_client_name("my-app")
1523///        .with_streaming(true)
1524///        .with_enable_config_discovery(true);
1525///    ```
1526///
1527/// 2. **Direct field assignment** (preferred when forwarding `Option<T>`
1528///    from upstream code, since `with_<field>` setters take the inner
1529///    `T`, not `Option<T>`):
1530///
1531///    ```
1532///    # use github_copilot_sdk::types::SessionConfig;
1533///    # let upstream_model: Option<String> = None;
1534///    # let upstream_system_message: Option<github_copilot_sdk::types::SystemMessageConfig> = None;
1535///    let mut cfg = SessionConfig::default()
1536///        .with_client_name("my-app")
1537///        .with_streaming(true);
1538///    cfg.model = upstream_model;
1539///    cfg.system_message = upstream_system_message;
1540///    ```
1541///
1542///    Mixing the two is fine: chain the fields you know at compile time,
1543///    then assign the `Option<T>` pass-through fields directly. All
1544///    fields on this struct are `pub`. This pattern matches the
1545///    `http::request::Parts` / `hyper::Body::Builder` convention in the
1546///    wider Rust ecosystem.
1547///
1548/// # Field naming across SDKs
1549///
1550/// Rust field names are snake_case (`available_tools`, `system_message`);
1551/// the wire protocol uses camelCase (`availableTools`, `systemMessage`).
1552/// The mapping happens inside `SessionConfig::into_wire` (crate-private),
1553/// which builds a separate `SessionCreateWire` payload. This config
1554/// struct is no longer itself serializable — the trait-object handler
1555/// fields (e.g. [`permission_handler`](Self::permission_handler)) could
1556/// never round-trip through serde, so the only legitimate serialization
1557/// path is now `into_wire`. When porting code from the TypeScript, Go,
1558/// Python, or .NET SDKs — or reading the raw JSON-RPC traces — fields
1559/// appear as `availableTools`, `systemMessage`, etc.
1560#[derive(Clone)]
1561#[non_exhaustive]
1562pub struct SessionConfig {
1563    /// Custom session ID. When unset, the CLI generates one.
1564    pub session_id: Option<SessionId>,
1565    /// Model to use (e.g. `"gpt-4"`, `"claude-sonnet-4"`).
1566    pub model: Option<String>,
1567    /// Application name sent as `User-Agent` context.
1568    pub client_name: Option<String>,
1569    /// Reasoning effort level (e.g. `"low"`, `"medium"`, `"high"`).
1570    pub reasoning_effort: Option<String>,
1571    /// Reasoning summary mode for models that support configurable
1572    /// reasoning summaries. Use [`ReasoningSummary::None`] to suppress
1573    /// summary output regardless of whether reasoning is enabled.
1574    pub reasoning_summary: Option<ReasoningSummary>,
1575    /// Context window tier for models that support it. Use `"long_context"`
1576    /// to pin the session to the long-context tier.
1577    pub context_tier: Option<String>,
1578    /// Enable streaming token deltas via `assistant.message_delta` events.
1579    pub streaming: Option<bool>,
1580    /// Custom system message configuration.
1581    pub system_message: Option<SystemMessageConfig>,
1582    /// Client-defined tool declarations to expose to the agent.
1583    pub tools: Option<Vec<Tool>>,
1584    /// Canvas declarations this connection provides to the runtime.
1585    pub canvases: Option<Vec<CanvasDeclaration>>,
1586    /// Provider-side canvas lifecycle handler. The SDK routes inbound
1587    /// `canvas.open` / `canvas.close` / `canvas.action.invoke` requests to
1588    /// this handler. Use [`with_canvas_handler`](Self::with_canvas_handler)
1589    /// to install one.
1590    pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1591    /// Request canvas renderer tools for this connection.
1592    pub request_canvas_renderer: Option<bool>,
1593    /// Request extension tools and dispatch for this connection.
1594    pub request_extensions: Option<bool>,
1595    /// Optional override path to a `copilot-sdk/` folder to inject into
1596    /// extension subprocesses for this session. Invalid paths fall back
1597    /// to the bundled SDK; takes precedence over the host's default.
1598    pub extension_sdk_path: Option<String>,
1599    /// Stable extension identity for canvas/tool providers on this connection.
1600    pub extension_info: Option<ExtensionInfo>,
1601    /// Allowlist of built-in tool names the agent may use.
1602    pub available_tools: Option<Vec<String>>,
1603    /// Blocklist of built-in tool names the agent must not use.
1604    pub excluded_tools: Option<Vec<String>>,
1605    /// 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/// Pointer to a GitHub repository (owner/name plus optional numeric id).
3960///
3961/// Used by the GitHub-anchored [`Attachment`] variants. Mirrors the field
3962/// shape of the generated `GitHubRepoRef`, but defined locally so it can
3963/// derive `Eq` for use inside the `Attachment` enum.
3964#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3965#[serde(rename_all = "camelCase")]
3966pub struct GitHubRepoPointer {
3967    /// Numeric GitHub repository id.
3968    #[serde(skip_serializing_if = "Option::is_none")]
3969    pub id: Option<i64>,
3970    /// Repository name (without owner).
3971    pub name: String,
3972    /// Repository owner login (user or organization).
3973    pub owner: String,
3974}
3975
3976/// One side (head or base) of a GitHub single-file diff.
3977#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3978#[serde(rename_all = "camelCase")]
3979pub struct GitHubFileDiffSide {
3980    /// Repository-relative path to the file.
3981    pub path: String,
3982    /// Git ref (branch, tag, or commit SHA) the file is read at.
3983    pub r#ref: String,
3984    /// Repository the file lives in.
3985    pub repo: GitHubRepoPointer,
3986}
3987
3988/// One side (head or base) of a GitHub tree comparison.
3989#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3990#[serde(rename_all = "camelCase")]
3991pub struct GitHubTreeComparisonSide {
3992    /// Repository the revision belongs to.
3993    pub repo: GitHubRepoPointer,
3994    /// Git revision (branch, tag, or commit SHA).
3995    pub revision: String,
3996}
3997
3998/// Line range covered by a GitHub snippet attachment (1-based, inclusive end).
3999#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4000#[serde(rename_all = "camelCase")]
4001pub struct GitHubSnippetLineRange {
4002    /// Start line number (1-based).
4003    pub start: i64,
4004    /// End line number (1-based, inclusive).
4005    pub end: i64,
4006}
4007
4008/// An attachment included with a user message.
4009#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4010#[serde(
4011    tag = "type",
4012    rename_all = "camelCase",
4013    rename_all_fields = "camelCase"
4014)]
4015#[non_exhaustive]
4016pub enum Attachment {
4017    /// A file path, optionally with a line range.
4018    File {
4019        /// Absolute path to the file.
4020        path: PathBuf,
4021        /// Label shown in the UI.
4022        #[serde(skip_serializing_if = "Option::is_none")]
4023        display_name: Option<String>,
4024        /// Optional line range to focus on.
4025        #[serde(skip_serializing_if = "Option::is_none")]
4026        line_range: Option<AttachmentLineRange>,
4027    },
4028    /// A directory path.
4029    Directory {
4030        /// Absolute path to the directory.
4031        path: PathBuf,
4032        /// Label shown in the UI.
4033        #[serde(skip_serializing_if = "Option::is_none")]
4034        display_name: Option<String>,
4035    },
4036    /// A text selection within a file.
4037    Selection {
4038        /// Path to the file containing the selection.
4039        file_path: PathBuf,
4040        /// The selected text content.
4041        text: String,
4042        /// Label shown in the UI.
4043        #[serde(skip_serializing_if = "Option::is_none")]
4044        display_name: Option<String>,
4045        /// Character range of the selection.
4046        selection: AttachmentSelectionRange,
4047    },
4048    /// Raw binary data (e.g. an image).
4049    Blob {
4050        /// Base64-encoded data.
4051        data: String,
4052        /// MIME type of the data.
4053        mime_type: String,
4054        /// Label shown in the UI.
4055        #[serde(skip_serializing_if = "Option::is_none")]
4056        display_name: Option<String>,
4057    },
4058    /// A reference to a GitHub issue, PR, or discussion.
4059    #[serde(rename = "github_reference")]
4060    GitHubReference {
4061        /// Issue/PR/discussion number.
4062        number: u64,
4063        /// Title of the referenced item.
4064        title: String,
4065        /// Kind of reference.
4066        reference_type: GitHubReferenceType,
4067        /// Current state (e.g. "open", "closed").
4068        state: String,
4069        /// URL to the referenced item.
4070        url: String,
4071    },
4072    /// A pointer to a GitHub commit.
4073    #[serde(rename = "github_commit")]
4074    GitHubCommit {
4075        /// First line of the commit message.
4076        message: String,
4077        /// Full commit SHA.
4078        oid: String,
4079        /// Repository the commit belongs to.
4080        repo: GitHubRepoPointer,
4081        /// URL to the commit on GitHub.
4082        url: String,
4083    },
4084    /// A pointer to a GitHub release.
4085    #[serde(rename = "github_release")]
4086    GitHubRelease {
4087        /// Human-readable release name.
4088        name: String,
4089        /// Repository the release belongs to.
4090        repo: GitHubRepoPointer,
4091        /// Git tag the release is anchored to.
4092        tag_name: String,
4093        /// URL to the release on GitHub.
4094        url: String,
4095    },
4096    /// A pointer to a GitHub Actions job.
4097    #[serde(rename = "github_actions_job")]
4098    GitHubActionsJob {
4099        /// Terminal conclusion of the job when finished (e.g. "success",
4100        /// "failure", "cancelled"). Absent for in-progress jobs.
4101        #[serde(skip_serializing_if = "Option::is_none")]
4102        conclusion: Option<String>,
4103        /// Job id within the workflow run.
4104        job_id: i64,
4105        /// Display name of the job.
4106        job_name: String,
4107        /// Repository the workflow run belongs to.
4108        repo: GitHubRepoPointer,
4109        /// URL to the job on GitHub.
4110        url: String,
4111        /// Display name of the workflow the job ran in.
4112        workflow_name: String,
4113    },
4114    /// A pointer to a GitHub repository.
4115    #[serde(rename = "github_repository")]
4116    GitHubRepository {
4117        /// Short description of the repository.
4118        #[serde(skip_serializing_if = "Option::is_none")]
4119        description: Option<String>,
4120        /// Git ref this attachment is anchored at (branch, tag, or commit).
4121        /// When absent the default branch is implied.
4122        #[serde(skip_serializing_if = "Option::is_none")]
4123        r#ref: Option<String>,
4124        /// Repository pointer.
4125        repo: GitHubRepoPointer,
4126        /// URL to the repository on GitHub.
4127        url: String,
4128    },
4129    /// A pointer to a single-file diff. At least one of `head` and `base` is present.
4130    #[serde(rename = "github_file_diff")]
4131    GitHubFileDiff {
4132        /// File location on the base side of the diff. Absent for additions.
4133        #[serde(skip_serializing_if = "Option::is_none")]
4134        base: Option<GitHubFileDiffSide>,
4135        /// File location on the head side of the diff. Absent for deletions.
4136        #[serde(skip_serializing_if = "Option::is_none")]
4137        head: Option<GitHubFileDiffSide>,
4138        /// URL to the diff on GitHub (e.g. a commit, compare, or PR-file URL).
4139        url: String,
4140    },
4141    /// A pointer to a comparison between two git revisions.
4142    #[serde(rename = "github_tree_comparison")]
4143    GitHubTreeComparison {
4144        /// Base side of the comparison.
4145        base: GitHubTreeComparisonSide,
4146        /// Head side of the comparison.
4147        head: GitHubTreeComparisonSide,
4148        /// URL to the comparison on GitHub.
4149        url: String,
4150    },
4151    /// A generic GitHub URL reference.
4152    #[serde(rename = "github_url")]
4153    GitHubUrl {
4154        /// URL to the GitHub resource.
4155        url: String,
4156    },
4157    /// A pointer to a file in a GitHub repository at a specific ref.
4158    #[serde(rename = "github_file")]
4159    GitHubFile {
4160        /// Repository-relative path to the file.
4161        path: String,
4162        /// Git ref the file is read at (branch, tag, or commit SHA).
4163        r#ref: String,
4164        /// Repository the file lives in.
4165        repo: GitHubRepoPointer,
4166        /// URL to the file on GitHub.
4167        url: String,
4168    },
4169    /// A pointer to a line range inside a file in a GitHub repository.
4170    #[serde(rename = "github_snippet")]
4171    GitHubSnippet {
4172        /// Line range the snippet covers.
4173        line_range: GitHubSnippetLineRange,
4174        /// Repository-relative path to the file.
4175        path: String,
4176        /// Git ref the file is read at (branch, tag, or commit SHA).
4177        r#ref: String,
4178        /// Repository the file lives in.
4179        repo: GitHubRepoPointer,
4180        /// URL to the snippet on GitHub (with line anchor).
4181        url: String,
4182    },
4183}
4184
4185impl Attachment {
4186    /// Returns the display name, if set.
4187    pub fn display_name(&self) -> Option<&str> {
4188        match self {
4189            Self::File { display_name, .. }
4190            | Self::Directory { display_name, .. }
4191            | Self::Selection { display_name, .. }
4192            | Self::Blob { display_name, .. } => display_name.as_deref(),
4193            Self::GitHubReference { .. }
4194            | Self::GitHubCommit { .. }
4195            | Self::GitHubRelease { .. }
4196            | Self::GitHubActionsJob { .. }
4197            | Self::GitHubRepository { .. }
4198            | Self::GitHubFileDiff { .. }
4199            | Self::GitHubTreeComparison { .. }
4200            | Self::GitHubUrl { .. }
4201            | Self::GitHubFile { .. }
4202            | Self::GitHubSnippet { .. } => None,
4203        }
4204    }
4205
4206    /// Returns a human-readable label, deriving one from the path if needed.
4207    pub fn label(&self) -> Option<String> {
4208        if let Some(display_name) = self
4209            .display_name()
4210            .map(str::trim)
4211            .filter(|name| !name.is_empty())
4212        {
4213            return Some(display_name.to_string());
4214        }
4215
4216        match self {
4217            Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4218                format!("#{}", number)
4219            } else {
4220                title.trim().to_string()
4221            }),
4222            _ => self.derived_display_name(),
4223        }
4224    }
4225
4226    /// Ensure `display_name` is populated when the variant supports one.
4227    pub fn ensure_display_name(&mut self) {
4228        if self
4229            .display_name()
4230            .map(str::trim)
4231            .is_some_and(|name| !name.is_empty())
4232        {
4233            return;
4234        }
4235
4236        let Some(derived_display_name) = self.derived_display_name() else {
4237            return;
4238        };
4239
4240        match self {
4241            Self::File { display_name, .. }
4242            | Self::Directory { display_name, .. }
4243            | Self::Selection { display_name, .. }
4244            | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4245            Self::GitHubReference { .. }
4246            | Self::GitHubCommit { .. }
4247            | Self::GitHubRelease { .. }
4248            | Self::GitHubActionsJob { .. }
4249            | Self::GitHubRepository { .. }
4250            | Self::GitHubFileDiff { .. }
4251            | Self::GitHubTreeComparison { .. }
4252            | Self::GitHubUrl { .. }
4253            | Self::GitHubFile { .. }
4254            | Self::GitHubSnippet { .. } => {}
4255        }
4256    }
4257
4258    fn derived_display_name(&self) -> Option<String> {
4259        match self {
4260            Self::File { path, .. } | Self::Directory { path, .. } => {
4261                Some(attachment_name_from_path(path))
4262            }
4263            Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4264            Self::Blob { .. } => Some("attachment".to_string()),
4265            Self::GitHubReference { .. }
4266            | Self::GitHubCommit { .. }
4267            | Self::GitHubRelease { .. }
4268            | Self::GitHubActionsJob { .. }
4269            | Self::GitHubRepository { .. }
4270            | Self::GitHubFileDiff { .. }
4271            | Self::GitHubTreeComparison { .. }
4272            | Self::GitHubUrl { .. }
4273            | Self::GitHubFile { .. }
4274            | Self::GitHubSnippet { .. } => None,
4275        }
4276    }
4277}
4278
4279fn attachment_name_from_path(path: &Path) -> String {
4280    path.file_name()
4281        .map(|name| name.to_string_lossy().into_owned())
4282        .filter(|name| !name.is_empty())
4283        .unwrap_or_else(|| {
4284            let full = path.to_string_lossy();
4285            if full.is_empty() {
4286                "attachment".to_string()
4287            } else {
4288                full.into_owned()
4289            }
4290        })
4291}
4292
4293/// Normalize a list of attachments so every entry has a `display_name`.
4294pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4295    for attachment in attachments {
4296        attachment.ensure_display_name();
4297    }
4298}
4299
4300/// Message delivery mode for [`MessageOptions::mode`].
4301///
4302/// Controls how a prompt is delivered relative to in-flight session work.
4303/// Wire values: `"enqueue"` and `"immediate"`.
4304#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4305#[serde(rename_all = "lowercase")]
4306#[non_exhaustive]
4307pub enum DeliveryMode {
4308    /// Queue the prompt behind any in-flight work (default).
4309    Enqueue,
4310    /// Interrupt the session and run the prompt immediately.
4311    Immediate,
4312}
4313
4314/// The UI mode the agent is in for a given turn, used by
4315/// [`MessageOptions::agent_mode`].
4316///
4317/// Wire values: `"interactive"`, `"plan"`, `"autopilot"`, `"shell"`.
4318#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4319#[serde(rename_all = "lowercase")]
4320#[non_exhaustive]
4321pub enum AgentMode {
4322    /// The agent is responding interactively to the user.
4323    Interactive,
4324    /// The agent is preparing a plan before making changes.
4325    Plan,
4326    /// The agent is working autonomously toward task completion.
4327    Autopilot,
4328    /// The agent is in shell-focused UI mode.
4329    Shell,
4330}
4331
4332/// Options for sending a user message to the agent.
4333///
4334/// Used by both [`Session::send`](crate::session::Session::send) and
4335/// [`Session::send_and_wait`](crate::session::Session::send_and_wait); the
4336/// `wait_timeout` field is honored only by `send_and_wait` and is ignored by
4337/// `send`.
4338///
4339/// `MessageOptions` is `#[non_exhaustive]` and constructed via [`MessageOptions::new`]
4340/// plus the `with_*` chain so future fields can land without breaking callers.
4341/// For the trivial case, both `&str` and `String` implement `Into<MessageOptions>`,
4342/// so:
4343///
4344/// ```no_run
4345/// # use github_copilot_sdk::session::Session;
4346/// # async fn run(session: Session) -> Result<(), github_copilot_sdk::Error> {
4347/// session.send("hello").await?;
4348/// # Ok(()) }
4349/// ```
4350///
4351/// is equivalent to:
4352///
4353/// ```no_run
4354/// # use github_copilot_sdk::session::Session;
4355/// # use github_copilot_sdk::types::MessageOptions;
4356/// # async fn run(session: Session) -> Result<(), github_copilot_sdk::Error> {
4357/// session.send(MessageOptions::new("hello")).await?;
4358/// # Ok(()) }
4359/// ```
4360#[derive(Debug, Clone)]
4361#[non_exhaustive]
4362pub struct MessageOptions {
4363    /// The user prompt to send.
4364    pub prompt: String,
4365    /// Optional message delivery mode for this turn.
4366    ///
4367    /// Controls whether the prompt is queued behind in-flight work
4368    /// ([`DeliveryMode::Enqueue`], default) or interrupts the session and
4369    /// runs immediately ([`DeliveryMode::Immediate`]).
4370    pub mode: Option<DeliveryMode>,
4371    /// Optional UI mode the agent was in when this message was sent
4372    /// (for example [`AgentMode::Plan`] or [`AgentMode::Autopilot`]).
4373    /// Defaults to the session's current mode when `None`.
4374    pub agent_mode: Option<AgentMode>,
4375    /// Optional attachments to include with the message.
4376    pub attachments: Option<Vec<Attachment>>,
4377    /// Maximum time to wait for the session to go idle. Honored only by
4378    /// `send_and_wait`. Defaults to 60 seconds when unset.
4379    pub wait_timeout: Option<Duration>,
4380    /// Custom HTTP headers to include in outbound model requests for this
4381    /// turn. When `None` or empty, no `requestHeaders` field is sent on
4382    /// the wire.
4383    pub request_headers: Option<HashMap<String, String>>,
4384    /// W3C Trace Context `traceparent` header for this turn.
4385    ///
4386    /// Per-turn override that takes precedence over
4387    /// [`ClientOptions::on_get_trace_context`](crate::ClientOptions::on_get_trace_context).
4388    /// When `None`, the SDK falls back to the provider (if configured)
4389    /// before omitting the field.
4390    pub traceparent: Option<String>,
4391    /// W3C Trace Context `tracestate` header for this turn.
4392    ///
4393    /// Per-turn override paired with [`traceparent`](Self::traceparent).
4394    pub tracestate: Option<String>,
4395    /// If provided, this is shown in the timeline instead of `prompt`.
4396    pub display_prompt: Option<String>,
4397}
4398
4399impl MessageOptions {
4400    /// Build a new `MessageOptions` with just a prompt.
4401    pub fn new(prompt: impl Into<String>) -> Self {
4402        Self {
4403            prompt: prompt.into(),
4404            mode: None,
4405            agent_mode: None,
4406            attachments: None,
4407            wait_timeout: None,
4408            request_headers: None,
4409            traceparent: None,
4410            tracestate: None,
4411            display_prompt: None,
4412        }
4413    }
4414
4415    /// Set the message delivery mode for this turn.
4416    ///
4417    /// Pass [`DeliveryMode::Immediate`] to interrupt the session and run
4418    /// the prompt now; the default ([`DeliveryMode::Enqueue`]) queues the
4419    /// prompt behind in-flight work.
4420    pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4421        self.mode = Some(mode);
4422        self
4423    }
4424
4425    /// Set the per-message agent UI mode for this turn.
4426    ///
4427    /// When `None`, the session's current mode is used.
4428    pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4429        self.agent_mode = Some(agent_mode);
4430        self
4431    }
4432
4433    /// Attach files / selections / blobs to the message.
4434    pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4435        self.attachments = Some(attachments);
4436        self
4437    }
4438
4439    /// Override the default 60-second wait timeout for `send_and_wait`.
4440    pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4441        self.wait_timeout = Some(timeout);
4442        self
4443    }
4444
4445    /// Set custom HTTP headers for outbound model requests for this turn.
4446    pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4447        self.request_headers = Some(headers);
4448        self
4449    }
4450
4451    /// Set both `traceparent` and `tracestate` from a [`TraceContext`].
4452    /// Either field may remain `None` if the [`TraceContext`] has no value
4453    /// for it. Use [`with_traceparent`](Self::with_traceparent) or
4454    /// [`with_tracestate`](Self::with_tracestate) to set them individually.
4455    pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4456        self.traceparent = ctx.traceparent;
4457        self.tracestate = ctx.tracestate;
4458        self
4459    }
4460
4461    /// Set the W3C `traceparent` header for this turn.
4462    pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4463        self.traceparent = Some(traceparent.into());
4464        self
4465    }
4466
4467    /// Set the W3C `tracestate` header for this turn.
4468    pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4469        self.tracestate = Some(tracestate.into());
4470        self
4471    }
4472
4473    /// Set the display prompt shown in the timeline instead of `prompt`.
4474    pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4475        self.display_prompt = Some(display_prompt.into());
4476        self
4477    }
4478}
4479
4480impl From<&str> for MessageOptions {
4481    fn from(prompt: &str) -> Self {
4482        Self::new(prompt)
4483    }
4484}
4485
4486impl From<String> for MessageOptions {
4487    fn from(prompt: String) -> Self {
4488        Self::new(prompt)
4489    }
4490}
4491
4492impl From<&String> for MessageOptions {
4493    fn from(prompt: &String) -> Self {
4494        Self::new(prompt.clone())
4495    }
4496}
4497
4498/// Response from [`Client::get_status`](crate::Client::get_status).
4499#[derive(Debug, Clone, Serialize, Deserialize)]
4500#[serde(rename_all = "camelCase")]
4501#[non_exhaustive]
4502pub struct GetStatusResponse {
4503    /// Package version (e.g. `"1.0.0"`).
4504    pub version: String,
4505    /// Protocol version for SDK compatibility.
4506    pub protocol_version: u32,
4507}
4508
4509/// Response from [`Client::get_auth_status`](crate::Client::get_auth_status).
4510#[derive(Debug, Clone, Serialize, Deserialize)]
4511#[serde(rename_all = "camelCase")]
4512#[non_exhaustive]
4513pub struct GetAuthStatusResponse {
4514    /// Whether the user is authenticated.
4515    pub is_authenticated: bool,
4516    /// Authentication type (e.g. `"user"`, `"env"`, `"gh-cli"`, `"hmac"`,
4517    /// `"api-key"`, `"token"`).
4518    #[serde(skip_serializing_if = "Option::is_none")]
4519    pub auth_type: Option<String>,
4520    /// GitHub host URL.
4521    #[serde(skip_serializing_if = "Option::is_none")]
4522    pub host: Option<String>,
4523    /// User login name.
4524    #[serde(skip_serializing_if = "Option::is_none")]
4525    pub login: Option<String>,
4526    /// Human-readable status message.
4527    #[serde(skip_serializing_if = "Option::is_none")]
4528    pub status_message: Option<String>,
4529}
4530
4531/// Wrapper for session event notifications received from the CLI.
4532///
4533/// The CLI sends these as JSON-RPC notifications on the `session.event` method.
4534#[derive(Debug, Clone, Serialize, Deserialize)]
4535#[serde(rename_all = "camelCase")]
4536pub struct SessionEventNotification {
4537    /// The session this event belongs to.
4538    pub session_id: SessionId,
4539    /// The event payload.
4540    pub event: SessionEvent,
4541}
4542
4543/// A single event in a session's timeline.
4544///
4545/// Events form a linked chain via `parent_id`. The `event_type` string
4546/// identifies the kind (e.g. `"assistant.message_delta"`, `"session.idle"`,
4547/// `"tool.execution_start"`). Event-specific payload is in `data` as
4548/// untyped JSON.
4549#[derive(Debug, Clone, Serialize, Deserialize)]
4550#[serde(rename_all = "camelCase")]
4551pub struct SessionEvent {
4552    /// Unique event ID (UUID v4).
4553    pub id: String,
4554    /// ISO 8601 timestamp.
4555    pub timestamp: String,
4556    /// ID of the preceding event in the chain.
4557    pub parent_id: Option<String>,
4558    /// Transient events that are not persisted to disk.
4559    #[serde(skip_serializing_if = "Option::is_none")]
4560    pub ephemeral: Option<bool>,
4561    /// Sub-agent instance identifier. Absent for events emitted by the
4562    /// root/main agent and for session-level events.
4563    #[serde(skip_serializing_if = "Option::is_none")]
4564    pub agent_id: Option<String>,
4565    /// Debug timestamp: when the CLI received this event (ms since epoch).
4566    #[serde(skip_serializing_if = "Option::is_none")]
4567    pub debug_cli_received_at_ms: Option<i64>,
4568    /// Debug timestamp: when the event was forwarded over WebSocket.
4569    #[serde(skip_serializing_if = "Option::is_none")]
4570    pub debug_ws_forwarded_at_ms: Option<i64>,
4571    /// Event type string (e.g. `"assistant.message"`, `"session.idle"`).
4572    #[serde(rename = "type")]
4573    pub event_type: String,
4574    /// Event-specific data. Structure depends on `event_type`.
4575    pub data: Value,
4576}
4577
4578impl SessionEvent {
4579    /// Parse the string `event_type` into a typed [`SessionEventType`](crate::session_events::SessionEventType) enum.
4580    ///
4581    /// Returns `SessionEventType::Unknown` for unrecognized event types,
4582    /// ensuring forward compatibility with newer CLI versions.
4583    pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4584        use serde::de::IntoDeserializer;
4585        let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4586            self.event_type.as_str().into_deserializer();
4587        crate::generated::SessionEventType::deserialize(deserializer)
4588            .unwrap_or(crate::generated::SessionEventType::Unknown)
4589    }
4590
4591    /// Deserialize the event `data` field into a typed struct.
4592    ///
4593    /// Returns `None` if deserialization fails (e.g. unknown event type
4594    /// or schema mismatch). Prefer typed data accessors for specific
4595    /// event types where you need strongly-typed field access.
4596    pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4597        serde_json::from_value(self.data.clone()).ok()
4598    }
4599
4600    /// `model_call` errors are transient — the CLI agent loop continues
4601    /// after them and may succeed on the next turn. These should not be
4602    /// treated as session-ending errors.
4603    pub fn is_transient_error(&self) -> bool {
4604        self.event_type == "session.error"
4605            && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4606    }
4607}
4608
4609/// A request from the CLI to invoke a client-defined tool.
4610///
4611/// Received as a JSON-RPC request on the `tool.call` method. The client
4612/// must respond with a [`ToolResultResponse`].
4613#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4614#[serde(rename_all = "camelCase")]
4615#[non_exhaustive]
4616pub struct ToolInvocation {
4617    /// Session that owns this tool call.
4618    pub session_id: SessionId,
4619    /// Unique ID for this tool call, used to correlate the response.
4620    pub tool_call_id: String,
4621    /// Name of the tool being invoked.
4622    pub tool_name: String,
4623    /// Tool arguments as JSON.
4624    pub arguments: Value,
4625    /// W3C Trace Context `traceparent` header propagated from the CLI's
4626    /// `execute_tool` span. Pass through to OpenTelemetry-aware code so
4627    /// child spans created inside the handler are parented to the CLI
4628    /// span. `None` when the CLI has no trace context for this call.
4629    #[serde(default, skip_serializing_if = "Option::is_none")]
4630    pub traceparent: Option<String>,
4631    /// W3C Trace Context `tracestate` paired with
4632    /// [`traceparent`](Self::traceparent).
4633    #[serde(default, skip_serializing_if = "Option::is_none")]
4634    pub tracestate: Option<String>,
4635}
4636
4637impl ToolInvocation {
4638    /// Deserialize this invocation's [`arguments`](Self::arguments) into a
4639    /// strongly-typed parameter struct.
4640    ///
4641    /// Idiomatic way to extract typed parameters when implementing
4642    /// [`ToolHandler`](crate::tool::ToolHandler) directly. Equivalent to
4643    /// `serde_json::from_value(invocation.arguments.clone())` with the SDK's
4644    /// error type.
4645    ///
4646    /// # Example
4647    ///
4648    /// ```rust,no_run
4649    /// # use github_copilot_sdk::{Error, types::ToolInvocation, ToolResult};
4650    /// # use serde::Deserialize;
4651    /// # #[derive(Deserialize)] struct MyParams { city: String }
4652    /// # async fn example(inv: ToolInvocation) -> Result<ToolResult, Error> {
4653    /// let params: MyParams = inv.params()?;
4654    /// // …use `inv.session_id` / `inv.tool_call_id` alongside `params`…
4655    /// # let _ = params; Ok(ToolResult::Text(String::new()))
4656    /// # }
4657    /// ```
4658    pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4659        serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4660    }
4661
4662    /// Returns the propagated [`TraceContext`] for this invocation, or
4663    /// [`TraceContext::default()`] when the CLI sent no headers.
4664    pub fn trace_context(&self) -> TraceContext {
4665        TraceContext {
4666            traceparent: self.traceparent.clone(),
4667            tracestate: self.tracestate.clone(),
4668        }
4669    }
4670}
4671
4672/// Binary content returned by a tool.
4673#[derive(Debug, Clone, Serialize, Deserialize)]
4674#[serde(rename_all = "camelCase")]
4675pub struct ToolBinaryResult {
4676    /// Base64-encoded binary data.
4677    pub data: String,
4678    /// MIME type for the binary data.
4679    pub mime_type: String,
4680    /// Type identifier for the binary result.
4681    pub r#type: String,
4682    /// Optional description shown alongside the binary result.
4683    #[serde(default, skip_serializing_if = "Option::is_none")]
4684    pub description: Option<String>,
4685}
4686
4687/// Expanded tool result with metadata for the LLM and session log.
4688#[derive(Debug, Clone, Serialize, Deserialize)]
4689#[serde(rename_all = "camelCase")]
4690pub struct ToolResultExpanded {
4691    /// Result text sent back to the LLM.
4692    pub text_result_for_llm: String,
4693    /// `"success"` or `"failure"`.
4694    pub result_type: String,
4695    /// Binary payloads sent back to the LLM.
4696    #[serde(default, skip_serializing_if = "Option::is_none")]
4697    pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4698    /// Optional log message for the session timeline.
4699    #[serde(skip_serializing_if = "Option::is_none")]
4700    pub session_log: Option<String>,
4701    /// Error message, if the tool failed.
4702    #[serde(skip_serializing_if = "Option::is_none")]
4703    pub error: Option<String>,
4704    /// Tool-specific telemetry emitted with the result.
4705    #[serde(default, skip_serializing_if = "Option::is_none")]
4706    pub tool_telemetry: Option<HashMap<String, Value>>,
4707}
4708
4709/// Result of a tool invocation — either a plain text string or an expanded result.
4710#[derive(Debug, Clone, Serialize, Deserialize)]
4711#[serde(untagged)]
4712#[non_exhaustive]
4713pub enum ToolResult {
4714    /// Simple text result passed directly to the LLM.
4715    Text(String),
4716    /// Structured result with metadata.
4717    Expanded(ToolResultExpanded),
4718}
4719
4720/// JSON-RPC response wrapper for a tool result, sent back to the CLI.
4721#[derive(Debug, Clone, Serialize, Deserialize)]
4722#[serde(rename_all = "camelCase")]
4723pub struct ToolResultResponse {
4724    /// The tool result payload.
4725    pub result: ToolResult,
4726}
4727
4728/// Metadata for a persisted session, returned by `session.list`.
4729#[derive(Debug, Clone, Serialize, Deserialize)]
4730#[serde(rename_all = "camelCase")]
4731pub struct SessionMetadata {
4732    /// The session's unique identifier.
4733    pub session_id: SessionId,
4734    /// ISO 8601 timestamp when the session was created.
4735    pub start_time: String,
4736    /// ISO 8601 timestamp of the last modification.
4737    pub modified_time: String,
4738    /// Agent-generated session summary.
4739    #[serde(skip_serializing_if = "Option::is_none")]
4740    pub summary: Option<String>,
4741    /// Whether the session is running remotely.
4742    pub is_remote: bool,
4743}
4744
4745/// Response from `session.list`.
4746#[derive(Debug, Clone, Serialize, Deserialize)]
4747#[serde(rename_all = "camelCase")]
4748pub struct ListSessionsResponse {
4749    /// The list of session metadata entries.
4750    pub sessions: Vec<SessionMetadata>,
4751}
4752
4753/// Filter options for [`Client::list_sessions`](crate::Client::list_sessions).
4754///
4755/// All fields are optional; unset fields don't constrain the result.
4756#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4757#[serde(rename_all = "camelCase")]
4758pub struct SessionListFilter {
4759    /// Filter by exact `cwd` match.
4760    #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4761    pub working_directory: Option<String>,
4762    /// Filter by git root path.
4763    #[serde(default, skip_serializing_if = "Option::is_none")]
4764    pub git_root: Option<String>,
4765    /// Filter by repository in `owner/repo` form.
4766    #[serde(default, skip_serializing_if = "Option::is_none")]
4767    pub repository: Option<String>,
4768    /// Filter by git branch name.
4769    #[serde(default, skip_serializing_if = "Option::is_none")]
4770    pub branch: Option<String>,
4771}
4772
4773/// Response from `session.getMetadata`.
4774#[derive(Debug, Clone, Serialize, Deserialize)]
4775#[serde(rename_all = "camelCase")]
4776pub struct GetSessionMetadataResponse {
4777    /// The session metadata, or `None` if the session was not found.
4778    #[serde(skip_serializing_if = "Option::is_none")]
4779    pub session: Option<SessionMetadata>,
4780}
4781
4782/// Response from `session.getLastId`.
4783#[derive(Debug, Clone, Serialize, Deserialize)]
4784#[serde(rename_all = "camelCase")]
4785pub struct GetLastSessionIdResponse {
4786    /// The most recently updated session ID, or `None` if no sessions exist.
4787    #[serde(skip_serializing_if = "Option::is_none")]
4788    pub session_id: Option<SessionId>,
4789}
4790
4791/// Response from `session.getForeground`.
4792#[derive(Debug, Clone, Serialize, Deserialize)]
4793#[serde(rename_all = "camelCase")]
4794pub struct GetForegroundSessionResponse {
4795    /// The current foreground session ID, or `None` if no foreground session.
4796    #[serde(skip_serializing_if = "Option::is_none")]
4797    pub session_id: Option<SessionId>,
4798}
4799
4800/// Response from `session.getMessages`.
4801#[derive(Debug, Clone, Serialize, Deserialize)]
4802#[serde(rename_all = "camelCase")]
4803pub struct GetMessagesResponse {
4804    /// Timeline events for the session.
4805    pub events: Vec<SessionEvent>,
4806}
4807
4808/// Result of an elicitation (interactive UI form) request.
4809#[derive(Debug, Clone, Serialize, Deserialize)]
4810#[serde(rename_all = "camelCase")]
4811pub struct ElicitationResult {
4812    /// User's action: `"accept"`, `"decline"`, or `"cancel"`.
4813    pub action: String,
4814    /// Form data submitted by the user (present when action is `"accept"`).
4815    #[serde(skip_serializing_if = "Option::is_none")]
4816    pub content: Option<Value>,
4817}
4818
4819/// Elicitation display mode.
4820///
4821/// New modes may be added by the CLI in future protocol versions; the
4822/// `Unknown` variant keeps deserialization from failing on unrecognised
4823/// values so the SDK can still surface the request to callers.
4824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4825#[serde(rename_all = "camelCase")]
4826#[non_exhaustive]
4827pub enum ElicitationMode {
4828    /// Structured form input rendered by the host.
4829    Form,
4830    /// Browser redirect to a URL.
4831    Url,
4832    /// A mode not yet known to this SDK version.
4833    #[serde(other)]
4834    Unknown,
4835}
4836
4837/// An incoming elicitation request from the CLI (provider side).
4838///
4839/// Received via `elicitation.requested` session event when the session has
4840/// an [`ElicitationHandler`] installed.
4841/// The provider should render a form or dialog and return an
4842/// [`ElicitationResult`].
4843#[derive(Debug, Clone, Serialize, Deserialize)]
4844#[serde(rename_all = "camelCase")]
4845pub struct ElicitationRequest {
4846    /// Message describing what information is needed from the user.
4847    pub message: String,
4848    /// JSON Schema describing the form fields to present.
4849    #[serde(skip_serializing_if = "Option::is_none")]
4850    pub requested_schema: Option<Value>,
4851    /// Elicitation display mode.
4852    #[serde(skip_serializing_if = "Option::is_none")]
4853    pub mode: Option<ElicitationMode>,
4854    /// The source that initiated the request (e.g. MCP server name).
4855    #[serde(skip_serializing_if = "Option::is_none")]
4856    pub elicitation_source: Option<String>,
4857    /// URL to open in the user's browser (url mode only).
4858    #[serde(skip_serializing_if = "Option::is_none")]
4859    pub url: Option<String>,
4860}
4861
4862/// Session-level capabilities reported by the CLI after session creation.
4863///
4864/// Capabilities indicate which features the CLI host supports for this session.
4865/// Updated at runtime via `capabilities.changed` events.
4866#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4867#[serde(rename_all = "camelCase")]
4868pub struct SessionCapabilities {
4869    /// UI capabilities (elicitation support, etc.).
4870    #[serde(skip_serializing_if = "Option::is_none")]
4871    pub ui: Option<UiCapabilities>,
4872}
4873
4874/// UI-specific capabilities for a session.
4875#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4876#[serde(rename_all = "camelCase")]
4877pub struct UiCapabilities {
4878    /// Whether the host supports interactive elicitation dialogs.
4879    #[serde(skip_serializing_if = "Option::is_none")]
4880    pub elicitation: Option<bool>,
4881    /// **Experimental.** This field is part of an experimental wire-protocol
4882    /// surface (SEP-1865) and may change or be removed in a future release.
4883    ///
4884    /// Whether the runtime has accepted the session's MCP Apps (SEP-1865)
4885    /// opt-in. `Some(true)` when the consumer set
4886    /// [`SessionConfig::enable_mcp_apps`] / [`ResumeSessionConfig::enable_mcp_apps`]
4887    /// to `true` on create/resume **and** the runtime's `MCP_APPS` feature
4888    /// flag (or `COPILOT_MCP_APPS=true` env override) is on. Otherwise
4889    /// absent or `Some(false)`, indicating the runtime silently dropped the
4890    /// opt-in.
4891    #[serde(skip_serializing_if = "Option::is_none")]
4892    pub mcp_apps: Option<bool>,
4893    /// Host-specific canvas capabilities.
4894    #[serde(skip_serializing_if = "Option::is_none")]
4895    pub canvases: Option<bool>,
4896}
4897
4898/// Options for the [`SessionUi::input`](crate::session::SessionUi::input) convenience method.
4899#[derive(Debug, Clone, Default)]
4900pub struct UiInputOptions<'a> {
4901    /// Title label for the input field.
4902    pub title: Option<&'a str>,
4903    /// Descriptive text shown below the field.
4904    pub description: Option<&'a str>,
4905    /// Minimum character length.
4906    pub min_length: Option<u64>,
4907    /// Maximum character length.
4908    pub max_length: Option<u64>,
4909    /// Semantic format hint.
4910    pub format: Option<InputFormat>,
4911    /// Default value pre-populated in the field.
4912    pub default: Option<&'a str>,
4913}
4914
4915/// Semantic format hints for text input fields.
4916#[derive(Debug, Clone, Copy)]
4917#[non_exhaustive]
4918pub enum InputFormat {
4919    /// Email address.
4920    Email,
4921    /// URI.
4922    Uri,
4923    /// Calendar date.
4924    Date,
4925    /// Date and time.
4926    DateTime,
4927}
4928
4929impl InputFormat {
4930    /// Returns the JSON Schema format string for this variant.
4931    pub fn as_str(&self) -> &'static str {
4932        match self {
4933            Self::Email => "email",
4934            Self::Uri => "uri",
4935            Self::Date => "date",
4936            Self::DateTime => "date-time",
4937        }
4938    }
4939}
4940
4941/// Re-exports of generated protocol types that are part of the SDK's
4942/// public API surface. The canonical definitions live in
4943/// [`crate::rpc`]; they live here so the crate-root
4944/// `pub use types::*` surfaces them alongside hand-written SDK types.
4945pub use crate::generated::api_types::{
4946    Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
4947    ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
4948    ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
4949    PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
4950};
4951
4952/// Permission categories the CLI may request approval for.
4953///
4954/// Wire values are the lower-kebab strings the CLI sends as the `kind`
4955/// discriminator on a permission request. Marked `#[non_exhaustive]`
4956/// because the CLI may add new kinds; matches must include a `_` arm.
4957#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4958#[serde(rename_all = "kebab-case")]
4959#[non_exhaustive]
4960pub enum PermissionRequestKind {
4961    /// Run a shell command.
4962    Shell,
4963    /// Write to a file.
4964    Write,
4965    /// Read a file.
4966    Read,
4967    /// Open a URL.
4968    Url,
4969    /// Invoke an MCP server tool.
4970    Mcp,
4971    /// Invoke a client-defined custom tool.
4972    CustomTool,
4973    /// Update agent memory.
4974    Memory,
4975    /// Run a hook callback.
4976    Hook,
4977    /// Unrecognized kind. The original wire string is available in
4978    /// [`PermissionRequestData::extra`] under the `kind` key.
4979    #[serde(other)]
4980    Unknown,
4981}
4982
4983/// Data sent by the CLI for permission-related events.
4984///
4985/// Used for both the `permission.request` RPC call (which expects a response)
4986/// and `permission.requested` notifications (fire-and-forget). Contains the
4987/// full params object.
4988#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4989#[serde(rename_all = "camelCase")]
4990pub struct PermissionRequestData {
4991    /// The permission category being requested. `None` means the CLI did
4992    /// not include a `kind` field. Use this to branch on common cases
4993    /// (shell, write, etc.) without parsing [`extra`](Self::extra).
4994    #[serde(default, skip_serializing_if = "Option::is_none")]
4995    pub kind: Option<PermissionRequestKind>,
4996    /// The originating tool-call ID, if this permission request is tied
4997    /// to a specific tool invocation.
4998    #[serde(default, skip_serializing_if = "Option::is_none")]
4999    pub tool_call_id: Option<String>,
5000    /// The full permission request params from the CLI. The shape varies by
5001    /// permission type and CLI version, so we preserve it as `Value`.
5002    #[serde(flatten)]
5003    pub extra: Value,
5004}
5005
5006/// Data sent by the CLI with an `exitPlanMode.request` RPC call.
5007#[derive(Debug, Clone, Serialize, Deserialize)]
5008#[serde(rename_all = "camelCase")]
5009pub struct ExitPlanModeData {
5010    /// Markdown summary of the plan presented to the user.
5011    #[serde(default)]
5012    pub summary: String,
5013    /// Full plan content (e.g. the plan.md body), if available.
5014    #[serde(default, skip_serializing_if = "Option::is_none")]
5015    pub plan_content: Option<String>,
5016    /// Allowed exit actions (e.g. "interactive", "autopilot", "autopilot_fleet").
5017    #[serde(default)]
5018    pub actions: Vec<String>,
5019    /// Which action the CLI recommends, defaults to "autopilot".
5020    #[serde(default = "default_recommended_action")]
5021    pub recommended_action: String,
5022}
5023
5024fn default_recommended_action() -> String {
5025    "autopilot".to_string()
5026}
5027
5028impl Default for ExitPlanModeData {
5029    fn default() -> Self {
5030        Self {
5031            summary: String::new(),
5032            plan_content: None,
5033            actions: Vec::new(),
5034            recommended_action: default_recommended_action(),
5035        }
5036    }
5037}
5038
5039#[cfg(test)]
5040mod tests {
5041    use std::path::PathBuf;
5042
5043    use serde_json::json;
5044
5045    use super::{
5046        AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5047        AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5048        CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5049        LargeToolOutputConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig,
5050        ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
5051        SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5052        ToolResultResponse, ensure_attachment_display_names,
5053    };
5054    use crate::generated::session_events::TypedSessionEvent;
5055
5056    #[test]
5057    fn tool_builder_composes() {
5058        let tool = Tool::new("greet")
5059            .with_description("Say hello")
5060            .with_namespaced_name("hello/greet")
5061            .with_instructions("Pass the user's name")
5062            .with_parameters(json!({
5063                "type": "object",
5064                "properties": { "name": { "type": "string" } },
5065                "required": ["name"]
5066            }))
5067            .with_overrides_built_in_tool(true)
5068            .with_skip_permission(true);
5069        assert_eq!(tool.name, "greet");
5070        assert_eq!(tool.description, "Say hello");
5071        assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5072        assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5073        assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5074        assert!(tool.overrides_built_in_tool);
5075        assert!(tool.skip_permission);
5076    }
5077
5078    #[test]
5079    fn tool_defer_serialization() {
5080        let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5081        assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5082        let value = serde_json::to_value(&tool).unwrap();
5083        assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5084
5085        let plain = Tool::new("plain");
5086        let value = serde_json::to_value(&plain).unwrap();
5087        assert!(value.get("defer").is_none());
5088    }
5089
5090    #[test]
5091    fn custom_agent_config_builder_with_model() {
5092        let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5093            .with_model("claude-haiku-4.5")
5094            .with_display_name("My Agent");
5095        assert_eq!(agent.name, "my-agent");
5096        assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5097        assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5098    }
5099
5100    #[test]
5101    fn custom_agent_config_serializes_model() {
5102        let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5103        let wire = serde_json::to_value(&agent).unwrap();
5104        assert_eq!(wire["model"], "claude-haiku-4.5");
5105        assert_eq!(wire["name"], "model-agent");
5106    }
5107
5108    #[test]
5109    fn custom_agent_config_omits_model_when_none() {
5110        let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5111        let wire = serde_json::to_value(&agent).unwrap();
5112        assert!(wire.get("model").is_none());
5113    }
5114
5115    #[test]
5116    #[should_panic(expected = "tool parameter schema must be a JSON object")]
5117    fn tool_with_parameters_panics_on_non_object_value() {
5118        let _ = Tool::new("noop").with_parameters(json!(null));
5119    }
5120
5121    #[test]
5122    fn tool_result_expanded_serializes_binary_results_for_llm() {
5123        let response = ToolResultResponse {
5124            result: ToolResult::Expanded(ToolResultExpanded {
5125                text_result_for_llm: "rendered chart".to_string(),
5126                result_type: "success".to_string(),
5127                binary_results_for_llm: Some(vec![ToolBinaryResult {
5128                    data: "aW1n".to_string(),
5129                    mime_type: "image/png".to_string(),
5130                    r#type: "image".to_string(),
5131                    description: Some("chart preview".to_string()),
5132                }]),
5133                session_log: None,
5134                error: None,
5135                tool_telemetry: None,
5136            }),
5137        };
5138
5139        let wire = serde_json::to_value(&response).unwrap();
5140
5141        assert_eq!(
5142            wire,
5143            json!({
5144                "result": {
5145                    "textResultForLlm": "rendered chart",
5146                    "resultType": "success",
5147                    "binaryResultsForLlm": [
5148                        {
5149                            "data": "aW1n",
5150                            "mimeType": "image/png",
5151                            "type": "image",
5152                            "description": "chart preview"
5153                        }
5154                    ]
5155                }
5156            })
5157        );
5158    }
5159
5160    #[test]
5161    fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5162        let response = ToolResultResponse {
5163            result: ToolResult::Expanded(ToolResultExpanded {
5164                text_result_for_llm: "ok".to_string(),
5165                result_type: "success".to_string(),
5166                binary_results_for_llm: None,
5167                session_log: None,
5168                error: None,
5169                tool_telemetry: None,
5170            }),
5171        };
5172
5173        let wire = serde_json::to_value(&response).unwrap();
5174
5175        assert_eq!(wire["result"]["textResultForLlm"], "ok");
5176        assert!(wire["result"].get("binaryResultsForLlm").is_none());
5177    }
5178
5179    #[test]
5180    fn session_config_default_wire_flags_off_without_handlers() {
5181        let cfg = SessionConfig::default();
5182        assert_eq!(cfg.mcp_oauth_token_storage, None);
5183        // Wire flags are derived from handler presence at create_session
5184        // time, not stored on the config. With no handlers installed, every
5185        // request_* flag should serialize as false.
5186        let (wire, _runtime) = cfg
5187            .into_wire(Some(SessionId::from("default-flags")))
5188            .expect("default config has no duplicate handlers");
5189        assert!(!wire.request_user_input);
5190        assert!(!wire.request_permission);
5191        assert!(!wire.request_elicitation);
5192        assert!(!wire.request_exit_plan_mode);
5193        assert!(!wire.request_auto_mode_switch);
5194        assert!(!wire.hooks);
5195        assert!(!wire.request_mcp_apps);
5196    }
5197
5198    #[test]
5199    fn resume_session_config_new_wire_flags_off_without_handlers() {
5200        let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5201        assert_eq!(cfg.mcp_oauth_token_storage, None);
5202        let (wire, _runtime) = cfg
5203            .into_wire()
5204            .expect("default resume config has no duplicate handlers");
5205        assert!(!wire.request_user_input);
5206        assert!(!wire.request_permission);
5207        assert!(!wire.request_elicitation);
5208        assert!(!wire.request_exit_plan_mode);
5209        assert!(!wire.request_auto_mode_switch);
5210        assert!(!wire.hooks);
5211        assert!(!wire.request_mcp_apps);
5212    }
5213
5214    #[test]
5215    fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5216        let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5217        assert_eq!(cfg.enable_mcp_apps, Some(true));
5218
5219        let (wire, _runtime) = cfg
5220            .into_wire(Some(SessionId::from("enable-mcp-apps")))
5221            .expect("enable_mcp_apps config has no duplicate handlers");
5222        assert!(wire.request_mcp_apps);
5223
5224        let json = serde_json::to_value(&wire).unwrap();
5225        assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5226    }
5227
5228    #[test]
5229    fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5230        let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5231            .with_enable_mcp_apps(true);
5232        assert_eq!(cfg.enable_mcp_apps, Some(true));
5233
5234        let (wire, _runtime) = cfg
5235            .into_wire()
5236            .expect("resume enable_mcp_apps config has no duplicate handlers");
5237        assert!(wire.request_mcp_apps);
5238
5239        let json = serde_json::to_value(&wire).unwrap();
5240        assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5241    }
5242
5243    #[test]
5244    fn memory_configuration_constructors_and_serde() {
5245        assert!(MemoryConfiguration::enabled().enabled);
5246        assert!(!MemoryConfiguration::disabled().enabled);
5247        assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5248
5249        let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5250        assert_eq!(json, serde_json::json!({ "enabled": true }));
5251    }
5252
5253    #[test]
5254    fn session_config_with_memory_serializes() {
5255        let (wire, _runtime) = SessionConfig::default()
5256            .with_memory(MemoryConfiguration::enabled())
5257            .into_wire(Some(SessionId::from("memory-on")))
5258            .expect("no duplicate handlers");
5259        let json = serde_json::to_value(&wire).unwrap();
5260        assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5261
5262        let (wire_off, _) = SessionConfig::default()
5263            .with_memory(MemoryConfiguration::disabled())
5264            .into_wire(Some(SessionId::from("memory-off")))
5265            .expect("no duplicate handlers");
5266        let json_off = serde_json::to_value(&wire_off).unwrap();
5267        assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5268
5269        // Unset memory is omitted on the wire.
5270        let (empty_wire, _) = SessionConfig::default()
5271            .into_wire(Some(SessionId::from("memory-unset")))
5272            .expect("no duplicate handlers");
5273        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5274        assert!(empty_json.get("memory").is_none());
5275    }
5276
5277    #[test]
5278    fn resume_session_config_with_memory_serializes() {
5279        let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5280            .with_memory(MemoryConfiguration::enabled())
5281            .into_wire()
5282            .expect("no duplicate handlers");
5283        let json = serde_json::to_value(&wire).unwrap();
5284        assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5285
5286        // Unset memory is omitted on the wire.
5287        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5288            .into_wire()
5289            .expect("no duplicate handlers");
5290        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5291        assert!(empty_json.get("memory").is_none());
5292    }
5293
5294    #[test]
5295    fn session_config_with_exp_assignments_serializes() {
5296        let assignments = serde_json::json!({
5297            "Parameters": { "copilot_exp_flag": "treatment" },
5298            "AssignmentContext": "ctx-123",
5299        });
5300        let (wire, _runtime) = SessionConfig::default()
5301            .with_exp_assignments(assignments.clone())
5302            .into_wire(Some(SessionId::from("exp-on")))
5303            .expect("no duplicate handlers");
5304        let json = serde_json::to_value(&wire).unwrap();
5305        assert_eq!(json["expAssignments"], assignments);
5306
5307        // Unset exp assignments are omitted on the wire.
5308        let (empty_wire, _) = SessionConfig::default()
5309            .into_wire(Some(SessionId::from("exp-unset")))
5310            .expect("no duplicate handlers");
5311        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5312        assert!(empty_json.get("expAssignments").is_none());
5313    }
5314
5315    #[test]
5316    fn resume_session_config_with_exp_assignments_serializes() {
5317        let assignments = serde_json::json!({
5318            "Parameters": { "copilot_exp_flag": "treatment" },
5319            "AssignmentContext": "ctx-456",
5320        });
5321        let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5322            .with_exp_assignments(assignments.clone())
5323            .into_wire()
5324            .expect("no duplicate handlers");
5325        let json = serde_json::to_value(&wire).unwrap();
5326        assert_eq!(json["expAssignments"], assignments);
5327
5328        // Unset exp assignments are omitted on the wire.
5329        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5330            .into_wire()
5331            .expect("no duplicate handlers");
5332        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5333        assert!(empty_json.get("expAssignments").is_none());
5334    }
5335
5336    #[test]
5337    fn session_config_clone_preserves_exp_assignments() {
5338        let assignments = serde_json::json!({
5339            "Parameters": { "copilot_exp_flag": "treatment" },
5340            "AssignmentContext": "ctx-clone",
5341        });
5342        let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5343        let cloned = config.clone();
5344
5345        assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5346
5347        let (wire, _runtime) = cloned
5348            .into_wire(Some(SessionId::from("exp-clone")))
5349            .expect("no duplicate handlers");
5350        let json = serde_json::to_value(&wire).unwrap();
5351        assert_eq!(json["expAssignments"], assignments);
5352    }
5353
5354    #[test]
5355    fn resume_session_config_clone_preserves_exp_assignments() {
5356        let assignments = serde_json::json!({
5357            "Parameters": { "copilot_exp_flag": "treatment" },
5358            "AssignmentContext": "ctx-clone-resume",
5359        });
5360        let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5361            .with_exp_assignments(assignments.clone());
5362        let cloned = config.clone();
5363
5364        assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5365
5366        let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5367        let json = serde_json::to_value(&wire).unwrap();
5368        assert_eq!(json["expAssignments"], assignments);
5369    }
5370
5371    #[test]
5372    #[allow(clippy::field_reassign_with_default)]
5373    fn session_config_into_wire_serializes_bucket_b_fields() {
5374        use std::path::PathBuf;
5375
5376        use super::{CloudSessionOptions, CloudSessionRepository};
5377
5378        let mut cfg = SessionConfig::default();
5379        cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5380        cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5381        cfg.github_token = Some("ghs_secret".to_string());
5382        cfg.include_sub_agent_streaming_events = Some(false);
5383        cfg.enable_session_telemetry = Some(false);
5384        cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5385        cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5386        cfg.enable_on_demand_instruction_discovery = Some(false);
5387        cfg.cloud = Some(CloudSessionOptions::with_repository(
5388            CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5389        ));
5390
5391        let (wire, _runtime) = cfg
5392            .into_wire(Some(SessionId::from("custom-id")))
5393            .expect("no duplicate handlers");
5394        let wire_json = serde_json::to_value(&wire).unwrap();
5395        assert_eq!(wire_json["sessionId"], "custom-id");
5396        assert_eq!(wire_json["configDir"], "/tmp/cfg");
5397        assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5398        assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5399        assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5400        assert_eq!(wire_json["enableSessionTelemetry"], false);
5401        assert_eq!(wire_json["reasoningSummary"], "concise");
5402        assert_eq!(wire_json["remoteSession"], "export");
5403        assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5404        assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5405        assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5406        assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5407
5408        // Unset fields are omitted on the wire.
5409        let (empty_wire, _) = SessionConfig::default()
5410            .into_wire(Some(SessionId::from("empty")))
5411            .expect("default has no duplicate handlers");
5412        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5413        assert!(empty_json.get("gitHubToken").is_none());
5414        assert!(empty_json.get("enableSessionTelemetry").is_none());
5415        assert!(empty_json.get("reasoningSummary").is_none());
5416        assert!(empty_json.get("remoteSession").is_none());
5417        assert!(
5418            empty_json
5419                .get("enableOnDemandInstructionDiscovery")
5420                .is_none()
5421        );
5422        assert!(empty_json.get("cloud").is_none());
5423    }
5424
5425    #[test]
5426    fn session_config_into_wire_serializes_named_providers_and_models() {
5427        let cfg = SessionConfig::default()
5428            .with_providers(vec![
5429                NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5430                    .with_provider_type("openai")
5431                    .with_wire_api("responses")
5432                    .with_api_key("sk-test"),
5433            ])
5434            .with_models(vec![
5435                ProviderModelConfig::new("gpt-x", "my-openai")
5436                    .with_wire_model("gpt-x-2025")
5437                    .with_max_output_tokens(2048),
5438            ]);
5439
5440        let (wire, _) = cfg
5441            .into_wire(Some(SessionId::from("sess-providers")))
5442            .expect("no duplicate handlers");
5443        let wire_json = serde_json::to_value(&wire).unwrap();
5444        assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5445        assert_eq!(
5446            wire_json["providers"][0]["baseUrl"],
5447            "https://api.example.com/v1"
5448        );
5449        assert_eq!(wire_json["providers"][0]["type"], "openai");
5450        assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5451        assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5452        assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5453        assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5454        assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5455        assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5456
5457        let (empty_wire, _) = SessionConfig::default()
5458            .into_wire(Some(SessionId::from("empty")))
5459            .expect("default has no duplicate handlers");
5460        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5461        assert!(empty_json.get("providers").is_none());
5462        assert!(empty_json.get("models").is_none());
5463    }
5464
5465    #[test]
5466    fn resume_config_into_wire_serializes_named_providers_and_models() {
5467        let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5468            .with_providers(vec![
5469                NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5470                    .with_provider_type("azure")
5471                    .with_azure(AzureProviderOptions {
5472                        api_version: Some("2024-10-21".to_string()),
5473                    }),
5474            ])
5475            .with_models(vec![
5476                ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5477            ]);
5478
5479        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5480        let wire_json = serde_json::to_value(&wire).unwrap();
5481        assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5482        assert_eq!(wire_json["providers"][0]["type"], "azure");
5483        assert_eq!(
5484            wire_json["providers"][0]["azure"]["apiVersion"],
5485            "2024-10-21"
5486        );
5487        assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5488        assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5489        assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5490
5491        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5492            .into_wire()
5493            .expect("default has no duplicate handlers");
5494        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5495        assert!(empty_json.get("providers").is_none());
5496        assert!(empty_json.get("models").is_none());
5497    }
5498
5499    #[test]
5500    fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5501        use std::path::PathBuf;
5502
5503        let cfg = SessionConfig {
5504            plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5505            large_output: Some(
5506                LargeToolOutputConfig::new()
5507                    .with_enabled(true)
5508                    .with_max_size_bytes(1024)
5509                    .with_output_directory(PathBuf::from("/tmp/large-output")),
5510            ),
5511            ..Default::default()
5512        };
5513
5514        let (wire, _) = cfg
5515            .into_wire(Some(SessionId::from("sess-1")))
5516            .expect("no duplicate handlers");
5517        let wire_json = serde_json::to_value(&wire).unwrap();
5518        assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5519        assert_eq!(wire_json["largeOutput"]["enabled"], true);
5520        assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5521        assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5522
5523        let (empty_wire, _) = SessionConfig::default()
5524            .into_wire(Some(SessionId::from("empty")))
5525            .expect("default has no duplicate handlers");
5526        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5527        assert!(empty_json.get("pluginDirectories").is_none());
5528        assert!(empty_json.get("largeOutput").is_none());
5529    }
5530
5531    #[test]
5532    fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5533        use std::path::PathBuf;
5534
5535        let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5536        cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5537        cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5538        cfg.github_token = Some("ghs_secret".to_string());
5539        cfg.include_sub_agent_streaming_events = Some(true);
5540        cfg.enable_session_telemetry = Some(false);
5541        cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5542        cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5543        cfg.enable_on_demand_instruction_discovery = Some(false);
5544
5545        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5546        let wire_json = serde_json::to_value(&wire).unwrap();
5547        assert_eq!(wire_json["sessionId"], "sess-1");
5548        assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5549        assert_eq!(wire_json["configDir"], "/tmp/cfg");
5550        assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5551        assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5552        assert_eq!(wire_json["enableSessionTelemetry"], false);
5553        assert_eq!(wire_json["reasoningSummary"], "detailed");
5554        assert_eq!(wire_json["remoteSession"], "on");
5555        assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5556
5557        // Unset remote_session is omitted on the wire.
5558        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5559            .into_wire()
5560            .expect("default resume has no duplicate handlers");
5561        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5562        assert!(empty_json.get("reasoningSummary").is_none());
5563        assert!(empty_json.get("remoteSession").is_none());
5564        assert!(
5565            empty_json
5566                .get("enableOnDemandInstructionDiscovery")
5567                .is_none()
5568        );
5569    }
5570
5571    #[test]
5572    fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5573        use std::path::PathBuf;
5574
5575        let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5576        cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
5577        cfg.large_output = Some(
5578            LargeToolOutputConfig::new()
5579                .with_enabled(false)
5580                .with_max_size_bytes(2048)
5581                .with_output_directory(PathBuf::from("/tmp/large-output-r")),
5582        );
5583
5584        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5585        let wire_json = serde_json::to_value(&wire).unwrap();
5586        assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
5587        assert_eq!(wire_json["largeOutput"]["enabled"], false);
5588        assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
5589        assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
5590
5591        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5592            .into_wire()
5593            .expect("default resume has no duplicate handlers");
5594        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5595        assert!(empty_json.get("pluginDirectories").is_none());
5596        assert!(empty_json.get("largeOutput").is_none());
5597    }
5598
5599    #[test]
5600    fn session_config_builder_composes() {
5601        use std::collections::HashMap;
5602
5603        let cfg = SessionConfig::default()
5604            .with_session_id(SessionId::from("sess-1"))
5605            .with_model("claude-sonnet-4")
5606            .with_client_name("test-app")
5607            .with_reasoning_effort("medium")
5608            .with_reasoning_summary(ReasoningSummary::Concise)
5609            .with_context_tier("long_context")
5610            .with_streaming(true)
5611            .with_tools([Tool::new("greet")])
5612            .with_available_tools(["bash", "view"])
5613            .with_excluded_tools(["dangerous"])
5614            .with_mcp_servers(HashMap::new())
5615            .with_mcp_oauth_token_storage("persistent")
5616            .with_enable_config_discovery(true)
5617            .with_enable_on_demand_instruction_discovery(true)
5618            .with_skill_directories([PathBuf::from("/tmp/skills")])
5619            .with_disabled_skills(["broken-skill"])
5620            .with_agent("researcher")
5621            .with_config_directory(PathBuf::from("/tmp/config"))
5622            .with_working_directory(PathBuf::from("/tmp/work"))
5623            .with_github_token("ghp_test")
5624            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5625            .with_enable_session_telemetry(false)
5626            .with_include_sub_agent_streaming_events(false)
5627            .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5628
5629        assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
5630        assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
5631        assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5632        assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
5633        assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
5634        assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
5635        assert_eq!(cfg.streaming, Some(true));
5636        assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5637        assert_eq!(
5638            cfg.available_tools.as_deref(),
5639            Some(&["bash".to_string(), "view".to_string()][..])
5640        );
5641        assert_eq!(
5642            cfg.excluded_tools.as_deref(),
5643            Some(&["dangerous".to_string()][..])
5644        );
5645        assert!(cfg.mcp_servers.is_some());
5646        assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5647        assert_eq!(cfg.enable_config_discovery, Some(true));
5648        assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
5649        assert_eq!(
5650            cfg.skill_directories.as_deref(),
5651            Some(&[PathBuf::from("/tmp/skills")][..])
5652        );
5653        assert_eq!(
5654            cfg.disabled_skills.as_deref(),
5655            Some(&["broken-skill".to_string()][..])
5656        );
5657        assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5658        assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5659        assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5660        assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5661        assert_eq!(
5662            cfg.capi,
5663            Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5664        );
5665        assert_eq!(cfg.enable_session_telemetry, Some(false));
5666        assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
5667        assert_eq!(
5668            cfg.extension_info,
5669            Some(ExtensionInfo::new("github-app", "counter"))
5670        );
5671    }
5672
5673    #[test]
5674    fn resume_session_config_builder_composes() {
5675        use std::collections::HashMap;
5676
5677        let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
5678            .with_client_name("test-app")
5679            .with_reasoning_summary(ReasoningSummary::None)
5680            .with_context_tier("default")
5681            .with_streaming(true)
5682            .with_tools([Tool::new("greet")])
5683            .with_available_tools(["bash", "view"])
5684            .with_excluded_tools(["dangerous"])
5685            .with_mcp_servers(HashMap::new())
5686            .with_mcp_oauth_token_storage("persistent")
5687            .with_enable_config_discovery(true)
5688            .with_enable_on_demand_instruction_discovery(false)
5689            .with_skill_directories([PathBuf::from("/tmp/skills")])
5690            .with_disabled_skills(["broken-skill"])
5691            .with_agent("researcher")
5692            .with_config_directory(PathBuf::from("/tmp/config"))
5693            .with_working_directory(PathBuf::from("/tmp/work"))
5694            .with_github_token("ghp_test")
5695            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5696            .with_enable_session_telemetry(false)
5697            .with_include_sub_agent_streaming_events(true)
5698            .with_suppress_resume_event(true)
5699            .with_continue_pending_work(true)
5700            .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5701
5702        assert_eq!(cfg.session_id.as_str(), "sess-2");
5703        assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5704        assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
5705        assert_eq!(cfg.context_tier.as_deref(), Some("default"));
5706        assert_eq!(cfg.streaming, Some(true));
5707        assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5708        assert_eq!(
5709            cfg.available_tools.as_deref(),
5710            Some(&["bash".to_string(), "view".to_string()][..])
5711        );
5712        assert_eq!(
5713            cfg.excluded_tools.as_deref(),
5714            Some(&["dangerous".to_string()][..])
5715        );
5716        assert!(cfg.mcp_servers.is_some());
5717        assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5718        assert_eq!(cfg.enable_config_discovery, Some(true));
5719        assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
5720        assert_eq!(
5721            cfg.skill_directories.as_deref(),
5722            Some(&[PathBuf::from("/tmp/skills")][..])
5723        );
5724        assert_eq!(
5725            cfg.disabled_skills.as_deref(),
5726            Some(&["broken-skill".to_string()][..])
5727        );
5728        assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5729        assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5730        assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5731        assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5732        assert_eq!(
5733            cfg.capi,
5734            Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5735        );
5736        assert_eq!(cfg.enable_session_telemetry, Some(false));
5737        assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
5738        assert_eq!(cfg.suppress_resume_event, Some(true));
5739        assert_eq!(cfg.continue_pending_work, Some(true));
5740        assert_eq!(
5741            cfg.extension_info,
5742            Some(ExtensionInfo::new("github-app", "counter"))
5743        );
5744    }
5745
5746    /// `continue_pending_work` must serialize to wire as `continuePendingWork`
5747    /// — the runtime keys off this exact field name to opt into the
5748    /// pending-work-handoff pattern.
5749    #[test]
5750    fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
5751        let cfg =
5752            ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
5753        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5754        let json = serde_json::to_value(&wire).unwrap();
5755        assert_eq!(json["continuePendingWork"], true);
5756
5757        // Unset case — skip_serializing_if must omit the field.
5758        let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5759            .into_wire()
5760            .expect("no duplicate handlers");
5761        let json = serde_json::to_value(&wire).unwrap();
5762        assert!(json.get("continuePendingWork").is_none());
5763    }
5764
5765    /// The Rust field is `suppress_resume_event`, but the wire field stays
5766    /// `disableResume` to preserve compatibility with the runtime and other
5767    /// SDKs.
5768    #[test]
5769    fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
5770        let cfg =
5771            ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
5772        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5773        let json = serde_json::to_value(&wire).unwrap();
5774        assert_eq!(json["disableResume"], true);
5775        assert!(json.get("suppressResumeEvent").is_none());
5776    }
5777
5778    /// `instruction_directories` must serialize to wire as
5779    /// `instructionDirectories` on `SessionConfig`.
5780    #[test]
5781    fn session_config_serializes_instruction_directories_to_camel_case() {
5782        let cfg =
5783            SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
5784        let (wire, _) = cfg
5785            .into_wire(Some(SessionId::from("instr-on")))
5786            .expect("no duplicate handlers");
5787        let json = serde_json::to_value(&wire).unwrap();
5788        assert_eq!(
5789            json["instructionDirectories"],
5790            serde_json::json!(["/tmp/instr"])
5791        );
5792
5793        // Unset case — skip_serializing_if must omit the field.
5794        let (wire, _) = SessionConfig::default()
5795            .into_wire(Some(SessionId::from("instr-off")))
5796            .expect("no duplicate handlers");
5797        let json = serde_json::to_value(&wire).unwrap();
5798        assert!(json.get("instructionDirectories").is_none());
5799    }
5800
5801    /// Same check on the resume path. Forwarded to the CLI on
5802    /// `session.resume`.
5803    #[test]
5804    fn resume_session_config_serializes_instruction_directories_to_camel_case() {
5805        let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
5806            .with_instruction_directories([PathBuf::from("/tmp/instr")]);
5807        let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5808        let json = serde_json::to_value(&wire).unwrap();
5809        assert_eq!(
5810            json["instructionDirectories"],
5811            serde_json::json!(["/tmp/instr"])
5812        );
5813
5814        let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5815            .into_wire()
5816            .expect("no duplicate handlers");
5817        let json = serde_json::to_value(&wire).unwrap();
5818        assert!(json.get("instructionDirectories").is_none());
5819    }
5820
5821    #[test]
5822    fn custom_agent_config_builder_composes() {
5823        use std::collections::HashMap;
5824
5825        let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
5826            .with_display_name("Research Assistant")
5827            .with_description("Investigates technical questions.")
5828            .with_tools(["bash", "view"])
5829            .with_mcp_servers(HashMap::new())
5830            .with_infer(true)
5831            .with_skills(["rust-coding-skill"]);
5832
5833        assert_eq!(cfg.name, "researcher");
5834        assert_eq!(cfg.prompt, "You are a research assistant.");
5835        assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
5836        assert_eq!(
5837            cfg.description.as_deref(),
5838            Some("Investigates technical questions.")
5839        );
5840        assert_eq!(
5841            cfg.tools.as_deref(),
5842            Some(&["bash".to_string(), "view".to_string()][..])
5843        );
5844        assert!(cfg.mcp_servers.is_some());
5845        assert_eq!(cfg.infer, Some(true));
5846        assert_eq!(
5847            cfg.skills.as_deref(),
5848            Some(&["rust-coding-skill".to_string()][..])
5849        );
5850    }
5851
5852    #[test]
5853    fn infinite_session_config_builder_composes() {
5854        let cfg = InfiniteSessionConfig::new()
5855            .with_enabled(true)
5856            .with_background_compaction_threshold(0.75)
5857            .with_buffer_exhaustion_threshold(0.92);
5858
5859        assert_eq!(cfg.enabled, Some(true));
5860        assert_eq!(cfg.background_compaction_threshold, Some(0.75));
5861        assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
5862    }
5863
5864    #[test]
5865    fn provider_config_builder_composes() {
5866        use std::collections::HashMap;
5867
5868        let mut headers = HashMap::new();
5869        headers.insert("X-Custom".to_string(), "value".to_string());
5870
5871        let cfg = ProviderConfig::new("https://api.example.com")
5872            .with_provider_type("openai")
5873            .with_wire_api("completions")
5874            .with_transport("websockets")
5875            .with_api_key("sk-test")
5876            .with_bearer_token("bearer-test")
5877            .with_headers(headers)
5878            .with_model_id("gpt-4")
5879            .with_wire_model("azure-gpt-4-deployment")
5880            .with_max_prompt_tokens(8192)
5881            .with_max_output_tokens(2048);
5882
5883        assert_eq!(cfg.base_url, "https://api.example.com");
5884        assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
5885        assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
5886        assert_eq!(cfg.transport.as_deref(), Some("websockets"));
5887        assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
5888        assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
5889        assert_eq!(
5890            cfg.headers
5891                .as_ref()
5892                .and_then(|h| h.get("X-Custom"))
5893                .map(String::as_str),
5894            Some("value"),
5895        );
5896        assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
5897        assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
5898        assert_eq!(cfg.max_prompt_tokens, Some(8192));
5899        assert_eq!(cfg.max_output_tokens, Some(2048));
5900
5901        // Wire-shape: camelCase, skip_serializing_if when unset.
5902        let wire = serde_json::to_value(&cfg).unwrap();
5903        assert_eq!(wire["modelId"], "gpt-4");
5904        assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
5905        assert_eq!(wire["maxPromptTokens"], 8192);
5906        assert_eq!(wire["maxOutputTokens"], 2048);
5907
5908        let unset = ProviderConfig::new("https://api.example.com");
5909        let wire_unset = serde_json::to_value(&unset).unwrap();
5910        assert!(wire_unset.get("modelId").is_none());
5911        assert!(wire_unset.get("wireModel").is_none());
5912        assert!(wire_unset.get("maxPromptTokens").is_none());
5913        assert!(wire_unset.get("maxOutputTokens").is_none());
5914    }
5915
5916    #[test]
5917    fn capi_session_options_builder_composes_and_serializes() {
5918        let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
5919
5920        assert_eq!(cfg.enable_web_socket_responses, Some(false));
5921
5922        let wire = serde_json::to_value(&cfg).unwrap();
5923        assert_eq!(
5924            wire,
5925            serde_json::json!({ "enableWebSocketResponses": false })
5926        );
5927
5928        let unset = CapiSessionOptions::new();
5929        let wire_unset = serde_json::to_value(&unset).unwrap();
5930        assert!(wire_unset.get("enableWebSocketResponses").is_none());
5931    }
5932
5933    #[test]
5934    fn session_config_with_capi_serializes() {
5935        let (wire, _) = SessionConfig::default()
5936            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5937            .into_wire(Some(SessionId::from("capi-create")))
5938            .expect("no duplicate handlers");
5939        let json = serde_json::to_value(&wire).unwrap();
5940        assert_eq!(
5941            json["capi"],
5942            serde_json::json!({ "enableWebSocketResponses": false })
5943        );
5944
5945        let (empty_wire, _) = SessionConfig::default()
5946            .into_wire(Some(SessionId::from("capi-create-unset")))
5947            .expect("no duplicate handlers");
5948        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5949        assert!(empty_json.get("capi").is_none());
5950    }
5951
5952    #[test]
5953    fn resume_session_config_with_capi_serializes() {
5954        let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
5955            .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5956            .into_wire()
5957            .expect("no duplicate handlers");
5958        let json = serde_json::to_value(&wire).unwrap();
5959        assert_eq!(
5960            json["capi"],
5961            serde_json::json!({ "enableWebSocketResponses": false })
5962        );
5963
5964        let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
5965            .into_wire()
5966            .expect("no duplicate handlers");
5967        let empty_json = serde_json::to_value(&empty_wire).unwrap();
5968        assert!(empty_json.get("capi").is_none());
5969    }
5970
5971    #[test]
5972    fn system_message_config_builder_composes() {
5973        use std::collections::HashMap;
5974
5975        let cfg = SystemMessageConfig::new()
5976            .with_mode("replace")
5977            .with_content("Custom system message.")
5978            .with_sections(HashMap::new());
5979
5980        assert_eq!(cfg.mode.as_deref(), Some("replace"));
5981        assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
5982        assert!(cfg.sections.is_some());
5983    }
5984
5985    #[test]
5986    fn delivery_mode_serializes_to_kebab_case_strings() {
5987        assert_eq!(
5988            serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
5989            "\"enqueue\""
5990        );
5991        assert_eq!(
5992            serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
5993            "\"immediate\""
5994        );
5995        let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
5996        assert_eq!(parsed, DeliveryMode::Immediate);
5997    }
5998
5999    #[test]
6000    fn agent_mode_serializes_to_kebab_case_strings() {
6001        assert_eq!(
6002            serde_json::to_string(&AgentMode::Interactive).unwrap(),
6003            "\"interactive\""
6004        );
6005        assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6006        assert_eq!(
6007            serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6008            "\"autopilot\""
6009        );
6010        assert_eq!(
6011            serde_json::to_string(&AgentMode::Shell).unwrap(),
6012            "\"shell\""
6013        );
6014        let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6015        assert_eq!(parsed, AgentMode::Plan);
6016    }
6017
6018    #[test]
6019    fn connection_state_distinguishes_variants() {
6020        // ConnectionState is now an internal type; verify we can construct
6021        // and compare the variants used by the lifecycle code paths.
6022        assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6023    }
6024
6025    /// `agentId` is the sub-agent attribution field added in copilot-sdk
6026    /// commit f8cf846 ("Derive session event envelopes from schema").
6027    /// Every other SDK (Node, Python, Go, .NET) carries it on the event
6028    /// envelope; Rust must too or sub-agent events lose attribution at
6029    /// the deserialization boundary. Cross-SDK parity test.
6030    #[test]
6031    fn session_event_round_trips_agent_id_on_envelope() {
6032        let wire = json!({
6033            "id": "evt-1",
6034            "timestamp": "2026-04-30T12:00:00Z",
6035            "parentId": null,
6036            "agentId": "sub-agent-42",
6037            "type": "assistant.message",
6038            "data": { "message": "hi" }
6039        });
6040
6041        let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6042        assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6043
6044        // Round-trip preserves the field on the wire.
6045        let roundtripped = serde_json::to_value(&event).unwrap();
6046        assert_eq!(roundtripped["agentId"], "sub-agent-42");
6047
6048        // Absent agentId remains absent (skip_serializing_if).
6049        let main_agent_event: SessionEvent = serde_json::from_value(json!({
6050            "id": "evt-2",
6051            "timestamp": "2026-04-30T12:00:01Z",
6052            "parentId": null,
6053            "type": "session.idle",
6054            "data": {}
6055        }))
6056        .unwrap();
6057        assert!(main_agent_event.agent_id.is_none());
6058        let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6059        assert!(roundtripped.get("agentId").is_none());
6060    }
6061
6062    /// Same parity for the typed event envelope produced by the codegen.
6063    #[test]
6064    fn typed_session_event_round_trips_agent_id_on_envelope() {
6065        let wire = json!({
6066            "id": "evt-1",
6067            "timestamp": "2026-04-30T12:00:00Z",
6068            "parentId": null,
6069            "agentId": "sub-agent-42",
6070            "type": "session.idle",
6071            "data": {}
6072        });
6073
6074        let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6075        assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6076
6077        let roundtripped = serde_json::to_value(&event).unwrap();
6078        assert_eq!(roundtripped["agentId"], "sub-agent-42");
6079    }
6080
6081    #[test]
6082    fn connection_state_variants_compile() {
6083        // Defensive smoke test: all variants must be constructable from
6084        // within the crate. (The enum was demoted from pub to pub(crate)
6085        // in Phase D; this test guards against accidental removal.)
6086        let _ = ConnectionState::Disconnected;
6087        let _ = ConnectionState::Connecting;
6088        let _ = ConnectionState::Connected;
6089        let _ = ConnectionState::Error;
6090    }
6091
6092    #[test]
6093    fn deserializes_runtime_attachment_variants() {
6094        let attachments: Vec<Attachment> = serde_json::from_value(json!([
6095            {
6096                "type": "file",
6097                "path": "/tmp/file.rs",
6098                "displayName": "file.rs",
6099                "lineRange": { "start": 7, "end": 12 }
6100            },
6101            {
6102                "type": "directory",
6103                "path": "/tmp/project",
6104                "displayName": "project"
6105            },
6106            {
6107                "type": "selection",
6108                "filePath": "/tmp/lib.rs",
6109                "displayName": "lib.rs",
6110                "text": "fn main() {}",
6111                "selection": {
6112                    "start": { "line": 1, "character": 2 },
6113                    "end": { "line": 3, "character": 4 }
6114                }
6115            },
6116            {
6117                "type": "blob",
6118                "data": "Zm9v",
6119                "mimeType": "image/png",
6120                "displayName": "image.png"
6121            },
6122            {
6123                "type": "github_reference",
6124                "number": 42,
6125                "title": "Fix rendering",
6126                "referenceType": "issue",
6127                "state": "open",
6128                "url": "https://github.com/example/repo/issues/42"
6129            }
6130        ]))
6131        .expect("attachments should deserialize");
6132
6133        assert_eq!(attachments.len(), 5);
6134        assert!(matches!(
6135            &attachments[0],
6136            Attachment::File {
6137                path,
6138                display_name,
6139                line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6140            } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6141        ));
6142        assert!(matches!(
6143            &attachments[1],
6144            Attachment::Directory { path, display_name }
6145                if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6146        ));
6147        assert!(matches!(
6148            &attachments[2],
6149            Attachment::Selection {
6150                file_path,
6151                display_name,
6152                selection:
6153                    AttachmentSelectionRange {
6154                        start: AttachmentSelectionPosition { line: 1, character: 2 },
6155                        end: AttachmentSelectionPosition { line: 3, character: 4 },
6156                    },
6157                ..
6158            } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6159        ));
6160        assert!(matches!(
6161            &attachments[3],
6162            Attachment::Blob {
6163                data,
6164                mime_type,
6165                display_name,
6166            } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6167        ));
6168        assert!(matches!(
6169            &attachments[4],
6170            Attachment::GitHubReference {
6171                number: 42,
6172                title,
6173                reference_type: GitHubReferenceType::Issue,
6174                state,
6175                url,
6176            } if title == "Fix rendering"
6177                && state == "open"
6178                && url == "https://github.com/example/repo/issues/42"
6179        ));
6180    }
6181
6182    #[test]
6183    fn ensures_display_names_for_variants_that_support_them() {
6184        let mut attachments = vec![
6185            Attachment::File {
6186                path: PathBuf::from("/tmp/file.rs"),
6187                display_name: None,
6188                line_range: None,
6189            },
6190            Attachment::Selection {
6191                file_path: PathBuf::from("/tmp/src/lib.rs"),
6192                display_name: None,
6193                text: "fn main() {}".to_string(),
6194                selection: AttachmentSelectionRange {
6195                    start: AttachmentSelectionPosition {
6196                        line: 0,
6197                        character: 0,
6198                    },
6199                    end: AttachmentSelectionPosition {
6200                        line: 0,
6201                        character: 10,
6202                    },
6203                },
6204            },
6205            Attachment::Blob {
6206                data: "Zm9v".to_string(),
6207                mime_type: "image/png".to_string(),
6208                display_name: None,
6209            },
6210            Attachment::GitHubReference {
6211                number: 7,
6212                title: "Track regressions".to_string(),
6213                reference_type: GitHubReferenceType::Issue,
6214                state: "open".to_string(),
6215                url: "https://example.com/issues/7".to_string(),
6216            },
6217        ];
6218
6219        ensure_attachment_display_names(&mut attachments);
6220
6221        assert_eq!(attachments[0].display_name(), Some("file.rs"));
6222        assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6223        assert_eq!(attachments[2].display_name(), Some("attachment"));
6224        assert_eq!(attachments[3].display_name(), None);
6225        assert_eq!(
6226            attachments[3].label(),
6227            Some("Track regressions".to_string())
6228        );
6229    }
6230
6231    #[test]
6232    fn github_anchored_attachment_variants_round_trip() {
6233        let cases = vec![
6234            (
6235                "github_commit",
6236                json!({
6237                    "type": "github_commit",
6238                    "message": "Fix the thing",
6239                    "oid": "abc123",
6240                    "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6241                    "url": "https://github.com/octocat/repo/commit/abc123"
6242                }),
6243            ),
6244            (
6245                "github_release",
6246                json!({
6247                    "type": "github_release",
6248                    "name": "v1.2.3",
6249                    "repo": { "name": "repo", "owner": "octocat" },
6250                    "tagName": "v1.2.3",
6251                    "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6252                }),
6253            ),
6254            (
6255                "github_actions_job",
6256                json!({
6257                    "type": "github_actions_job",
6258                    "conclusion": "failure",
6259                    "jobId": 99,
6260                    "jobName": "build",
6261                    "repo": { "name": "repo", "owner": "octocat" },
6262                    "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6263                    "workflowName": "CI"
6264                }),
6265            ),
6266            (
6267                "github_repository",
6268                json!({
6269                    "type": "github_repository",
6270                    "description": "An example repository",
6271                    "ref": "main",
6272                    "repo": { "name": "repo", "owner": "octocat" },
6273                    "url": "https://github.com/octocat/repo"
6274                }),
6275            ),
6276            (
6277                "github_file_diff",
6278                json!({
6279                    "type": "github_file_diff",
6280                    "base": {
6281                        "path": "src/lib.rs",
6282                        "ref": "main",
6283                        "repo": { "name": "repo", "owner": "octocat" }
6284                    },
6285                    "head": {
6286                        "path": "src/lib.rs",
6287                        "ref": "feature",
6288                        "repo": { "name": "repo", "owner": "octocat" }
6289                    },
6290                    "url": "https://github.com/octocat/repo/compare/main...feature"
6291                }),
6292            ),
6293            (
6294                "github_tree_comparison",
6295                json!({
6296                    "type": "github_tree_comparison",
6297                    "base": {
6298                        "repo": { "name": "repo", "owner": "octocat" },
6299                        "revision": "main"
6300                    },
6301                    "head": {
6302                        "repo": { "name": "repo", "owner": "octocat" },
6303                        "revision": "feature"
6304                    },
6305                    "url": "https://github.com/octocat/repo/compare/main...feature"
6306                }),
6307            ),
6308            (
6309                "github_url",
6310                json!({
6311                    "type": "github_url",
6312                    "url": "https://github.com/octocat/repo/wiki"
6313                }),
6314            ),
6315            (
6316                "github_file",
6317                json!({
6318                    "type": "github_file",
6319                    "path": "src/main.rs",
6320                    "ref": "main",
6321                    "repo": { "name": "repo", "owner": "octocat" },
6322                    "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6323                }),
6324            ),
6325            (
6326                "github_snippet",
6327                json!({
6328                    "type": "github_snippet",
6329                    "lineRange": { "start": 10, "end": 20 },
6330                    "path": "src/main.rs",
6331                    "ref": "main",
6332                    "repo": { "name": "repo", "owner": "octocat" },
6333                    "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6334                }),
6335            ),
6336        ];
6337
6338        for (expected_type, input) in cases {
6339            let attachment: Attachment = serde_json::from_value(input.clone())
6340                .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6341
6342            // Serialize to a string first: parsing into `serde_json::Value` would
6343            // silently dedupe a duplicate `type` key, hiding the exact regression
6344            // this test guards against (e.g. a wrapped generated struct emitting its
6345            // own `type` alongside the enum tag).
6346            let serialized_string = serde_json::to_string(&attachment)
6347                .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6348
6349            // Exactly one `type` key, carrying the expected discriminator.
6350            assert_eq!(
6351                serialized_string.matches("\"type\":").count(),
6352                1,
6353                "{expected_type} must serialize a single `type` key"
6354            );
6355
6356            let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6357                .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6358            assert_eq!(
6359                serialized.get("type").and_then(|value| value.as_str()),
6360                Some(expected_type),
6361                "{expected_type} must serialize the correct discriminator"
6362            );
6363
6364            // Round-trips without dropping fields.
6365            assert_eq!(
6366                serialized, input,
6367                "{expected_type} should round-trip without data loss"
6368            );
6369            let reparsed: Attachment = serde_json::from_value(serialized)
6370                .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
6371            assert_eq!(
6372                reparsed, attachment,
6373                "{expected_type} should re-deserialize to the same value"
6374            );
6375        }
6376    }
6377}
6378
6379#[cfg(test)]
6380mod permission_builder_tests {
6381    use std::sync::Arc;
6382
6383    use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6384    use crate::permission;
6385    use crate::types::{
6386        PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6387        SessionId,
6388    };
6389
6390    fn data() -> PermissionRequestData {
6391        PermissionRequestData {
6392            extra: serde_json::json!({"tool": "shell"}),
6393            ..Default::default()
6394        }
6395    }
6396
6397    /// Apply the same policy-resolution logic that `Client::create_session`
6398    /// uses, so tests exercise the effective handler.
6399    fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6400        permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6401    }
6402
6403    fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6404        permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6405    }
6406
6407    async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6408        handler
6409            .handle(SessionId::from("s1"), RequestId::new("1"), data())
6410            .await
6411    }
6412
6413    #[tokio::test]
6414    async fn approve_all_with_handler_present_approves() {
6415        let cfg = SessionConfig::default()
6416            .with_permission_handler(Arc::new(ApproveAllHandler))
6417            .approve_all_permissions();
6418        let h = resolve_create(cfg).expect("policy + handler yields handler");
6419        assert!(matches!(
6420            dispatch(&h).await,
6421            PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6422        ));
6423    }
6424
6425    #[tokio::test]
6426    async fn approve_all_standalone_produces_handler() {
6427        let cfg = SessionConfig::default().approve_all_permissions();
6428        let h = resolve_create(cfg).expect("policy alone yields handler");
6429        assert!(matches!(
6430            dispatch(&h).await,
6431            PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6432        ));
6433    }
6434
6435    /// Phase I: order between with_permission_handler and the policy
6436    /// builder must not matter.
6437    #[tokio::test]
6438    async fn approve_all_is_order_independent() {
6439        let a = SessionConfig::default()
6440            .with_permission_handler(Arc::new(ApproveAllHandler))
6441            .approve_all_permissions();
6442        let b = SessionConfig::default()
6443            .approve_all_permissions()
6444            .with_permission_handler(Arc::new(ApproveAllHandler));
6445        let ha = resolve_create(a).unwrap();
6446        let hb = resolve_create(b).unwrap();
6447        assert!(matches!(
6448            dispatch(&ha).await,
6449            PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6450        ));
6451        assert!(matches!(
6452            dispatch(&hb).await,
6453            PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6454        ));
6455    }
6456
6457    #[tokio::test]
6458    async fn deny_all_is_order_independent() {
6459        let a = SessionConfig::default()
6460            .with_permission_handler(Arc::new(ApproveAllHandler))
6461            .deny_all_permissions();
6462        let b = SessionConfig::default()
6463            .deny_all_permissions()
6464            .with_permission_handler(Arc::new(ApproveAllHandler));
6465        let ha = resolve_create(a).unwrap();
6466        let hb = resolve_create(b).unwrap();
6467        assert!(matches!(
6468            dispatch(&ha).await,
6469            PermissionResult::Decision(PermissionDecision::Reject(_))
6470        ));
6471        assert!(matches!(
6472            dispatch(&hb).await,
6473            PermissionResult::Decision(PermissionDecision::Reject(_))
6474        ));
6475    }
6476
6477    #[tokio::test]
6478    async fn approve_permissions_if_consults_predicate() {
6479        let cfg = SessionConfig::default().approve_permissions_if(|d| {
6480            d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6481        });
6482        let h = resolve_create(cfg).unwrap();
6483        assert!(matches!(
6484            dispatch(&h).await,
6485            PermissionResult::Decision(PermissionDecision::Reject(_))
6486        ));
6487    }
6488
6489    #[tokio::test]
6490    async fn approve_permissions_if_is_order_independent() {
6491        let predicate = |d: &PermissionRequestData| {
6492            d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6493        };
6494        let a = SessionConfig::default()
6495            .with_permission_handler(Arc::new(ApproveAllHandler))
6496            .approve_permissions_if(predicate);
6497        let b = SessionConfig::default()
6498            .approve_permissions_if(predicate)
6499            .with_permission_handler(Arc::new(ApproveAllHandler));
6500        let ha = resolve_create(a).unwrap();
6501        let hb = resolve_create(b).unwrap();
6502        assert!(matches!(
6503            dispatch(&ha).await,
6504            PermissionResult::Decision(PermissionDecision::Reject(_))
6505        ));
6506        assert!(matches!(
6507            dispatch(&hb).await,
6508            PermissionResult::Decision(PermissionDecision::Reject(_))
6509        ));
6510    }
6511
6512    #[tokio::test]
6513    async fn resume_session_config_approve_all_works() {
6514        let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
6515            .with_permission_handler(Arc::new(ApproveAllHandler))
6516            .approve_all_permissions();
6517        let h = resolve_resume(cfg).unwrap();
6518        assert!(matches!(
6519            dispatch(&h).await,
6520            PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6521        ));
6522    }
6523
6524    #[tokio::test]
6525    async fn resume_session_config_approve_all_is_order_independent() {
6526        let a = ResumeSessionConfig::new(SessionId::from("s1"))
6527            .with_permission_handler(Arc::new(ApproveAllHandler))
6528            .approve_all_permissions();
6529        let b = ResumeSessionConfig::new(SessionId::from("s1"))
6530            .approve_all_permissions()
6531            .with_permission_handler(Arc::new(ApproveAllHandler));
6532        let ha = resolve_resume(a).unwrap();
6533        let hb = resolve_resume(b).unwrap();
6534        assert!(matches!(
6535            dispatch(&ha).await,
6536            PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6537        ));
6538        assert!(matches!(
6539            dispatch(&hb).await,
6540            PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6541        ));
6542    }
6543}