Skip to main content

SessionHandler

Trait SessionHandler 

Source
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<u64>,
    ) -> 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:

  1. 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 by serenity::EventHandler, lapin, and most Rust SDKs that dispatch broker/client callbacks.
  2. Single on_event method. Override this one method and match on HandlerEvent yourself. 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§

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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).

Source

fn on_auto_mode_switch<'life0, 'async_trait>( &'life0 self, _session_id: SessionId, _error_code: Option<String>, _retry_after_seconds: Option<u64>, ) -> 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 (RFC 9110 Retry-After delta-seconds). 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.

Implementors§