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}