pub trait SessionHandler:
Send
+ Sync
+ 'static {
// Provided methods
fn on_event<'life0, 'async_trait>(
&'life0 self,
event: HandlerEvent,
) -> Pin<Box<dyn Future<Output = HandlerResponse> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait { ... }
fn on_session_event<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_event: SessionEvent,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait { ... }
fn on_permission_request<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_request_id: RequestId,
_data: PermissionRequestData,
) -> Pin<Box<dyn Future<Output = PermissionResult> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait { ... }
fn on_user_input<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_question: String,
_choices: Option<Vec<String>>,
_allow_freeform: Option<bool>,
) -> Pin<Box<dyn Future<Output = Option<UserInputResponse>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait { ... }
fn on_external_tool<'life0, 'async_trait>(
&'life0 self,
invocation: ToolInvocation,
) -> Pin<Box<dyn Future<Output = ToolResult> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait { ... }
fn on_elicitation<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_request_id: RequestId,
_request: ElicitationRequest,
) -> Pin<Box<dyn Future<Output = ElicitationResult> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait { ... }
fn on_exit_plan_mode<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_data: ExitPlanModeData,
) -> Pin<Box<dyn Future<Output = ExitPlanModeResult> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait { ... }
fn on_auto_mode_switch<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_error_code: Option<String>,
_retry_after_seconds: Option<f64>,
) -> Pin<Box<dyn Future<Output = AutoModeSwitchResponse> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait { ... }
}Expand description
Callback trait for session events.
Implement this trait to control how a session responds to CLI events, permission requests, tool calls, user input prompts, elicitations, and plan-mode exits. There are two styles of implementation — pick whichever fits your use case:
- Per-event methods (recommended for most handlers). Override the
specific
on_*methods you care about; every method has a safe default so you only write what you need. This is the pattern used byserenity::EventHandler,lapin, and most Rust SDKs that dispatch broker/client callbacks. - Single
on_eventmethod. Override this one method andmatchonHandlerEventyourself. Useful for logging middleware, custom routing, or when you want an exhaustiveness check across all variants.
When you override on_event directly, the per-event methods are not
called — your implementation is entirely responsible for dispatch. The
default on_event fans out to the per-event methods.
§Default behavior
- Permission requests → denied (safe default).
- User input →
None(no answer available). - External tool calls → failure result with “no handler registered”.
- Elicitation →
"cancel". - Exit plan mode →
ExitPlanModeResult::default. - Auto-mode-switch →
AutoModeSwitchResponse::No(decline by default; the session stays on its current model and surfaces the rate-limit error). - Session events → ignored (fire-and-forget).
§Concurrency
Request-triggered events (UserInput, ExternalTool via tool.call,
ExitPlanMode, PermissionRequest via permission.request) are awaited
inline in the event loop and therefore processed serially per session.
Blocking here pauses that session’s event loop — which is correct, since
the CLI is also blocked waiting for the response.
Notification-triggered events (PermissionRequest via
permission.requested, ExternalTool via external_tool.requested) are
dispatched on spawned tasks and may run concurrently with each other
and with the serial event loop. Implementations must be safe for
concurrent invocation.
§Example
use async_trait::async_trait;
use github_copilot_sdk::handler::{PermissionResult, SessionHandler};
use github_copilot_sdk::types::{PermissionRequestData, RequestId, SessionId};
struct ApproveReadsOnly;
#[async_trait]
impl SessionHandler for ApproveReadsOnly {
async fn on_permission_request(
&self,
_sid: SessionId,
_rid: RequestId,
data: PermissionRequestData,
) -> PermissionResult {
match data.extra.get("tool").and_then(|v| v.as_str()) {
Some("view") | Some("ls") | Some("grep") => PermissionResult::Approved,
_ => PermissionResult::Denied,
}
}
}Provided Methods§
Sourcefn on_event<'life0, 'async_trait>(
&'life0 self,
event: HandlerEvent,
) -> Pin<Box<dyn Future<Output = HandlerResponse> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn on_event<'life0, 'async_trait>(
&'life0 self,
event: HandlerEvent,
) -> Pin<Box<dyn Future<Output = HandlerResponse> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Handle an event from the session.
The default implementation destructures event and calls the
matching per-event method (e.g. on_permission_request
for HandlerEvent::PermissionRequest). Override this method only
if you want a single dispatch point with exhaustive matching — most
handlers should override the per-event methods instead.
See the trait-level docs for details on which events may be dispatched concurrently.
Sourcefn on_session_event<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_event: SessionEvent,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn on_session_event<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_event: SessionEvent,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
Informational timeline event (assistant messages, tool execution markers, session idle, etc.). Fire-and-forget — the return value is ignored.
Default: do nothing.
Sourcefn on_permission_request<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_request_id: RequestId,
_data: PermissionRequestData,
) -> Pin<Box<dyn Future<Output = PermissionResult> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn on_permission_request<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_request_id: RequestId,
_data: PermissionRequestData,
) -> Pin<Box<dyn Future<Output = PermissionResult> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
The CLI is asking whether the agent may perform a privileged action.
Default: PermissionResult::Denied. The default-deny posture
matches the CLI’s safety model; override to implement your own
policy (see the permission module for common
wrappers like approve_all / approve_if).
Sourcefn on_user_input<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_question: String,
_choices: Option<Vec<String>>,
_allow_freeform: Option<bool>,
) -> Pin<Box<dyn Future<Output = Option<UserInputResponse>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn on_user_input<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_question: String,
_choices: Option<Vec<String>>,
_allow_freeform: Option<bool>,
) -> Pin<Box<dyn Future<Output = Option<UserInputResponse>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
The CLI is asking the user a question (optionally with a list of choices).
Default: None — the CLI interprets this as “no answer available”
and falls back to its own prompt behavior.
Sourcefn on_external_tool<'life0, 'async_trait>(
&'life0 self,
invocation: ToolInvocation,
) -> Pin<Box<dyn Future<Output = ToolResult> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn on_external_tool<'life0, 'async_trait>(
&'life0 self,
invocation: ToolInvocation,
) -> Pin<Box<dyn Future<Output = ToolResult> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
The CLI wants to invoke a client-defined (“external”) tool.
Default: a failure ToolResult indicating no tool handler is
registered. Typical implementations route to a
ToolHandlerRouter which
dispatches to tools registered via
define_tool or custom
ToolHandler impls.
Sourcefn on_elicitation<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_request_id: RequestId,
_request: ElicitationRequest,
) -> Pin<Box<dyn Future<Output = ElicitationResult> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn on_elicitation<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_request_id: RequestId,
_request: ElicitationRequest,
) -> Pin<Box<dyn Future<Output = ElicitationResult> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
The CLI is requesting an elicitation (structured form / URL prompt).
Default: cancel.
Sourcefn on_exit_plan_mode<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_data: ExitPlanModeData,
) -> Pin<Box<dyn Future<Output = ExitPlanModeResult> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn on_exit_plan_mode<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_data: ExitPlanModeData,
) -> Pin<Box<dyn Future<Output = ExitPlanModeResult> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
The CLI is asking the user whether to exit plan mode.
Default: ExitPlanModeResult::default (approved with no action).
Sourcefn on_auto_mode_switch<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_error_code: Option<String>,
_retry_after_seconds: Option<f64>,
) -> Pin<Box<dyn Future<Output = AutoModeSwitchResponse> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
fn on_auto_mode_switch<'life0, 'async_trait>(
&'life0 self,
_session_id: SessionId,
_error_code: Option<String>,
_retry_after_seconds: Option<f64>,
) -> Pin<Box<dyn Future<Output = AutoModeSwitchResponse> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
The CLI is asking whether to switch to auto model after an eligible rate limit.
retry_after_seconds, when present, is the number of seconds until the
rate limit resets. Handlers can use it to render a humanized reset time
alongside the prompt.
Default: AutoModeSwitchResponse::No — decline. Override only if
your application surfaces a UX for the rate-limit-recovery prompt.