Skip to main content

github_copilot_sdk/
handler.rs

1//! Optional session-callback traits.
2//!
3//! Each callback the CLI may dispatch (permission requests, elicitation
4//! prompts, user-input questions, exit-plan-mode prompts,
5//! auto-mode-switch prompts) has its own focused trait with a single
6//! `handle` method.
7//!
8//! Handlers are **optional**: install only the ones the application cares
9//! about. The SDK derives the corresponding wire flag on
10//! `session.create` / `session.resume` from the presence of each handler,
11//! so the runtime does not emit broadcasts this client would never
12//! respond to.
13//!
14//! Tool dispatch uses its own per-tool registry built from
15//! [`Tool::with_handler`](crate::types::Tool::with_handler) on entries passed to
16//! [`SessionConfig::with_tools`](crate::types::SessionConfig::with_tools).
17
18use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20
21use crate::generated::api_types::{
22    PermissionDecision, PermissionDecisionApproveOnce, PermissionDecisionReject,
23    PermissionDecisionUserNotAvailable,
24};
25use crate::types::{
26    ElicitationRequest, ElicitationResult, ExitPlanModeData, PermissionRequestData, RequestId,
27    SessionId,
28};
29
30/// Decision returned by a [`PermissionHandler`].
31///
32/// Either a concrete wire-level [`PermissionDecision`] (approve, reject,
33/// approve-for-session, approve-permanently, user-not-available, …) or
34/// [`PermissionResult::NoResult`], which tells the SDK to suppress its
35/// response so another connected client can answer instead.
36#[derive(Debug, Clone)]
37pub enum PermissionResult {
38    /// Send a permission decision on the wire.
39    Decision(PermissionDecision),
40    /// Decline to respond to this request, allowing another connected
41    /// client to answer instead. The SDK suppresses the response.
42    NoResult,
43}
44
45impl PermissionResult {
46    /// Approve this single request.
47    pub fn approve_once() -> Self {
48        Self::Decision(PermissionDecision::ApproveOnce(
49            PermissionDecisionApproveOnce::default(),
50        ))
51    }
52
53    /// Reject the request, optionally forwarding feedback to the LLM.
54    pub fn reject(feedback: impl Into<Option<String>>) -> Self {
55        Self::Decision(PermissionDecision::Reject(PermissionDecisionReject {
56            feedback: feedback.into(),
57            ..Default::default()
58        }))
59    }
60
61    /// Deny because no user is available to confirm.
62    pub fn user_not_available() -> Self {
63        Self::Decision(PermissionDecision::UserNotAvailable(
64            PermissionDecisionUserNotAvailable::default(),
65        ))
66    }
67
68    /// Decline to respond, allowing another connected client to answer
69    /// instead.
70    pub fn no_result() -> Self {
71        Self::NoResult
72    }
73}
74
75impl From<PermissionDecision> for PermissionResult {
76    fn from(value: PermissionDecision) -> Self {
77        Self::Decision(value)
78    }
79}
80
81/// Response to a user input request.
82#[derive(Debug, Clone)]
83pub struct UserInputResponse {
84    /// The user's answer text.
85    pub answer: String,
86    /// Whether the answer was free-form (not a preset choice).
87    pub was_freeform: bool,
88}
89
90/// Result of an exit-plan-mode request.
91#[derive(Debug, Clone, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct ExitPlanModeResult {
94    /// Whether the user approved exiting plan mode.
95    pub approved: bool,
96    /// The action the user selected (if any).
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub selected_action: Option<String>,
99    /// Optional feedback text from the user.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub feedback: Option<String>,
102}
103
104impl Default for ExitPlanModeResult {
105    fn default() -> Self {
106        Self {
107            approved: true,
108            selected_action: None,
109            feedback: None,
110        }
111    }
112}
113
114/// Response to an auto-mode-switch request.
115#[non_exhaustive]
116#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
117#[serde(rename_all = "snake_case")]
118pub enum AutoModeSwitchResponse {
119    /// Approve the auto-mode switch for this rate-limit cycle only.
120    Yes,
121    /// Approve and remember -- auto-accept future auto-mode switches in
122    /// this session without prompting.
123    YesAlways,
124    /// Decline the auto-mode switch. The session stays on the current
125    /// model and surfaces the rate-limit error.
126    No,
127}
128
129/// Handler for `permission.requested` broadcasts.
130///
131/// Install via
132/// [`SessionConfig::with_permission_handler`](crate::types::SessionConfig::with_permission_handler)
133/// (or the matching method on [`ResumeSessionConfig`](crate::types::ResumeSessionConfig)).
134/// When no permission handler is supplied, the SDK sends
135/// `requestPermission: false` on the wire and the runtime short-circuits
136/// permission prompts for this client.
137#[async_trait]
138pub trait PermissionHandler: Send + Sync + 'static {
139    /// Resolve a permission request.
140    async fn handle(
141        &self,
142        session_id: SessionId,
143        request_id: RequestId,
144        data: PermissionRequestData,
145    ) -> PermissionResult;
146}
147
148/// Handler for `elicitation.requested` broadcasts.
149///
150/// When unset, `requestElicitation: false` goes on the wire.
151#[async_trait]
152pub trait ElicitationHandler: Send + Sync + 'static {
153    /// Respond to an elicitation prompt (form, URL confirm, etc.).
154    async fn handle(
155        &self,
156        session_id: SessionId,
157        request_id: RequestId,
158        request: ElicitationRequest,
159    ) -> ElicitationResult;
160}
161
162/// Handler for `user_input.requested` events from the `ask_user` tool.
163///
164/// When unset, `requestUserInput: false` goes on the wire and the
165/// `ask_user` tool is disabled for the session.
166#[async_trait]
167pub trait UserInputHandler: Send + Sync + 'static {
168    /// Answer a question on behalf of the user. Return `None` to signal
169    /// "no answer available".
170    async fn handle(
171        &self,
172        session_id: SessionId,
173        question: String,
174        choices: Option<Vec<String>>,
175        allow_freeform: Option<bool>,
176    ) -> Option<UserInputResponse>;
177}
178
179/// Handler for `exit_plan_mode.requested` events. When unset,
180/// `requestExitPlanMode: false` goes on the wire.
181#[async_trait]
182pub trait ExitPlanModeHandler: Send + Sync + 'static {
183    /// Decide whether to leave plan mode.
184    async fn handle(&self, session_id: SessionId, data: ExitPlanModeData) -> ExitPlanModeResult;
185}
186
187/// Handler for `auto_mode_switch.requested` events. When unset,
188/// `requestAutoModeSwitch: false` goes on the wire.
189#[async_trait]
190pub trait AutoModeSwitchHandler: Send + Sync + 'static {
191    /// Decide whether to fall back to the auto model after an eligible
192    /// rate-limit error. `retry_after_seconds`, when present, is the
193    /// number of seconds until the rate limit resets.
194    async fn handle(
195        &self,
196        session_id: SessionId,
197        error_code: Option<String>,
198        retry_after_seconds: Option<f64>,
199    ) -> AutoModeSwitchResponse;
200}
201
202/// A [`PermissionHandler`] that approves every request. Useful for CLI
203/// tools, scripts, and tests that don't need interactive permission
204/// prompts.
205#[derive(Debug, Clone)]
206pub struct ApproveAllHandler;
207
208#[async_trait]
209impl PermissionHandler for ApproveAllHandler {
210    async fn handle(
211        &self,
212        _session_id: SessionId,
213        _request_id: RequestId,
214        _data: PermissionRequestData,
215    ) -> PermissionResult {
216        PermissionResult::approve_once()
217    }
218}
219
220/// A [`PermissionHandler`] that denies every request.
221#[derive(Debug, Clone)]
222pub struct DenyAllHandler;
223
224#[async_trait]
225impl PermissionHandler for DenyAllHandler {
226    async fn handle(
227        &self,
228        _session_id: SessionId,
229        _request_id: RequestId,
230        _data: PermissionRequestData,
231    ) -> PermissionResult {
232        PermissionResult::reject(None)
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[tokio::test]
241    async fn approve_all_handler_returns_approved() {
242        let result = ApproveAllHandler
243            .handle(
244                SessionId::from("s1"),
245                RequestId::new("1"),
246                PermissionRequestData::default(),
247            )
248            .await;
249        assert!(matches!(
250            result,
251            PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
252        ));
253    }
254
255    #[tokio::test]
256    async fn deny_all_handler_returns_denied() {
257        let result = DenyAllHandler
258            .handle(
259                SessionId::from("s1"),
260                RequestId::new("1"),
261                PermissionRequestData::default(),
262            )
263            .await;
264        assert!(matches!(
265            result,
266            PermissionResult::Decision(PermissionDecision::Reject(_))
267        ));
268    }
269}