Skip to main content

github_copilot_sdk/
types.rs

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