Skip to main content

github_copilot_sdk/
types.rs

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