Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. This change
## [Unreleased]

### Added (v0.2.1 sync)
- **`onElicitationRequest` handler** — new `:on-elicitation-request` option in session config (both `create-session` and `resume-session`). When provided, the session registers as an elicitation provider: the `requestElicitation` capability is advertised on the wire, and incoming `elicitation.requested` broadcast events are dispatched to the handler. The handler receives a request map `{:message :requested-schema :mode :elicitation-source :url}` and an invocation map `{:session-id}`, and must return a result map `{:action "accept"|"decline"|"cancel" :content {..}}`. On handler failure, a cancel response is automatically sent to avoid hanging requests (upstream PR #908).
- **`capabilities.changed` event handling** — sessions now automatically merge incoming `capabilities.changed` broadcast events into their stored capabilities, so `(capabilities session)` reflects the latest state as providers join/leave (upstream PR #908).
- **New event types**: `capabilities.changed`, `sampling.requested`, `sampling.completed`, `session.remote_steerable_changed` added to the event type enum (upstream PR #908).
- **`steerable` field on `session.start` events** — `session.start` event data now includes optional `:steerable?` boolean field indicating whether the session supports remote steering via Mission Control. New `::steerable?` spec added (upstream PR #927).
- **`get-session-metadata`** — new function on client for efficient O(1) session lookup by ID. Returns session metadata map if found, or `nil` if not found. Sends `session.getMetadata` JSON-RPC call. Shared `wire->session-metadata` helper extracted from `list-sessions` to eliminate duplication (upstream PR #899).

Expand Down
62 changes: 61 additions & 1 deletion src/github/copilot_sdk/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,53 @@
:error (ex-message e)}))))
(catch Exception _ nil))))))))

(defn- handle-v3-broadcast-event!
(defn- handle-v3-elicitation-requested!
"Handle v3 elicitation.requested broadcast event.
Calls the session's elicitation handler and responds via the
session.ui.handlePendingElicitation RPC method.
On handler failure, sends a cancel response so the request doesn't hang."
[client session-id event]
(let [data (:data event)
request-id (:request-id data)
request (select-keys data [:message :requested-schema :mode :elicitation-source :url])]
(when request-id
(go
(try
(let [elicit-response (<! (session/handle-elicitation-request!
client session-id request))
result (:result elicit-response)]
(if result
(let [conn (:connection-io @(:state client))]
(when conn
(<! (proto/send-request conn "session.ui.handlePendingElicitation"
{:session-id session-id
:request-id request-id
:result result}))))
;; Handler returned error — cancel so request doesn't hang
(let [conn (:connection-io @(:state client))]
(when conn
(<! (proto/send-request conn "session.ui.handlePendingElicitation"
{:session-id session-id
:request-id request-id
:result {:action "cancel"}}))))))
(catch Exception e
(log/debug "v3 elicitation request error for " request-id ": " (ex-message e))
(try
(let [conn (:connection-io @(:state client))]
(when conn
(<! (proto/send-request conn "session.ui.handlePendingElicitation"
{:session-id session-id
:request-id request-id
:result {:action "cancel"}}))))
(catch Exception _ nil))))))))

(defn- handle-v3-capabilities-changed!
"Handle capabilities.changed event by merging new capabilities into session state."
[client session-id event]
(let [data (:data event)]
(swap! (:state client) update-in [:sessions session-id :capabilities] merge data)))


"Protocol v3: intercept broadcast events for external tools, permissions, and commands.
In v3, tool.call and permission.request server→client RPC methods are replaced
by broadcast events that the SDK handles and responds to via new RPC methods."
Expand All @@ -367,6 +413,12 @@
:copilot/command.execute
(handle-v3-command-execute! client session-id event)

:copilot/elicitation.requested
(handle-v3-elicitation-requested! client session-id event)

:copilot/capabilities.changed
(handle-v3-capabilities-changed! client session-id event)

nil)))

(defn- start-notification-router!
Expand Down Expand Up @@ -1263,6 +1315,7 @@
(:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config))
(:agent config) (assoc :agent (:agent config))
true (assoc :request-user-input (boolean (:on-user-input-request config)))
true (assoc :request-elicitation (boolean (:on-elicitation-request config)))
true (assoc :hooks (boolean (:hooks config)))
true (assoc :env-value-mode "direct"))))

Expand Down Expand Up @@ -1317,6 +1370,7 @@
(:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config))
(:agent config) (assoc :agent (:agent config))
true (assoc :request-user-input (boolean (:on-user-input-request config)))
true (assoc :request-elicitation (boolean (:on-elicitation-request config)))
true (assoc :hooks (boolean (:hooks config)))
(:working-directory config) (assoc :working-directory (:working-directory config))
(:disable-resume? config) (assoc :disable-resume (:disable-resume? config))
Expand All @@ -1332,6 +1386,7 @@
:commands (:commands config)
:on-permission-request (:on-permission-request config)
:on-user-input-request (:on-user-input-request config)
:on-elicitation-request (:on-elicitation-request config)
:hooks (:hooks config)
:on-event (:on-event config)
:config config}))
Expand Down Expand Up @@ -1365,6 +1420,10 @@
:buffer-exhaustion-threshold (0.0-1.0, default 0.95)}
- :reasoning-effort - Reasoning effort level: \"low\", \"medium\", \"high\", or \"xhigh\" (PR #302)
- :on-user-input-request - Handler for ask_user requests (PR #269)
- :on-elicitation-request - Handler for elicitation requests (PR #908).
Enables the session as an elicitation provider.
Receives {:message :requested-schema :mode :elicitation-source :url} and
{:session-id}. Returns {:action \"accept\"|\"decline\"|\"cancel\" :content {..}}
- :hooks - Lifecycle hooks map (PR #269):
{:on-pre-tool-use, :on-post-tool-use, :on-user-prompt-submitted,
:on-session-start, :on-session-end, :on-error-occurred}
Expand Down Expand Up @@ -1416,6 +1475,7 @@
- :infinite-sessions - Infinite session configuration
- :reasoning-effort - Reasoning effort level: \"low\", \"medium\", \"high\", or \"xhigh\"
- :on-user-input-request - Handler for ask_user requests
- :on-elicitation-request - Handler for elicitation requests (PR #908)
- :hooks - Lifecycle hooks map
- :on-event - Event handler (1-arg fn) registered before the RPC call.
Guarantees early events like session.start are not missed.
Expand Down
27 changes: 26 additions & 1 deletion src/github/copilot_sdk/session.clj
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
If :on-event is provided, taps a subscriber that forwards events to the handler
on a dedicated thread. Uses a sliding buffer, so events may be dropped under
extreme backpressure if the handler cannot keep up with the event rate."
[client session-id {:keys [tools on-permission-request on-user-input-request hooks workspace-path on-event config commands]}]
[client session-id {:keys [tools on-permission-request on-user-input-request on-elicitation-request hooks workspace-path on-event config commands]}]
(log/debug "Creating session: " session-id)
(let [event-chan (chan (async/sliding-buffer 4096))
event-mult (mult event-chan)
Expand All @@ -66,6 +66,7 @@
:command-handlers command-handlers
:permission-handler on-permission-request
:user-input-handler on-user-input-request
:elicitation-handler on-elicitation-request
:hooks hooks
:destroyed? false
:workspace-path workspace-path
Expand Down Expand Up @@ -323,6 +324,30 @@
{:error {:code -32001 :message (str "User input handler error: " (ex-message e))}})))))
:io))

(defn handle-elicitation-request!
"Handle an incoming elicitation.requested broadcast event. Returns a channel with the result.
PR #908 feature (onElicitationRequest handler).

The handler receives the request map (with :message, :requested-schema, :mode, :elicitation-source,
:url) and should return an elicitation result map with :action (\"accept\", \"decline\", or \"cancel\")
and optionally :content (map of form values when action is \"accept\")."
[client session-id request]
(async/thread-call
(fn []
(let [handler (:elicitation-handler (session-state client session-id))]
(if-not handler
{:error {:code -32001 :message "Elicitation requested but no handler registered"}}
(try
(let [result (handler request {:session-id session-id})
result (if (channel? result)
(<!! result)
result)]
{:result result})
(catch Exception e
(log/error "Elicitation handler error for session " session-id ": " (ex-message e))
{:error {:code -32001 :message (str "Elicitation handler error: " (ex-message e))}}))))))
:io))

(defn handle-hooks-invoke!
"Handle an incoming hooks invocation. Returns a channel with the result.
PR #269 feature."
Expand Down
18 changes: 12 additions & 6 deletions src/github/copilot_sdk/specs.clj
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@

;; Hooks and user input handlers (PR #269)
(s/def ::on-user-input-request fn?)
;; Elicitation request handler (PR #908)
(s/def ::on-elicitation-request fn?)
(s/def ::on-pre-tool-use fn?)
(s/def ::on-post-tool-use fn?)
(s/def ::on-user-prompt-submitted fn?)
Expand Down Expand Up @@ -315,7 +317,7 @@
:on-permission-request :streaming? :mcp-servers
:custom-agents :config-dir :skill-directories
:disabled-skills :large-output :infinite-sessions
:reasoning-effort :on-user-input-request :hooks
:reasoning-effort :on-user-input-request :on-elicitation-request :hooks
:working-directory :agent :on-event})

(s/def ::session-config
Expand All @@ -326,7 +328,7 @@
::streaming? ::mcp-servers
::custom-agents ::config-dir ::skill-directories
::disabled-skills ::large-output ::infinite-sessions
::reasoning-effort ::on-user-input-request ::hooks
::reasoning-effort ::on-user-input-request ::on-elicitation-request ::hooks
::working-directory ::agent ::on-event])
session-config-keys))

Expand All @@ -335,7 +337,7 @@
:provider :streaming? :on-permission-request
:mcp-servers :custom-agents :config-dir :skill-directories
:disabled-skills :infinite-sessions :reasoning-effort
:on-user-input-request :hooks :working-directory :disable-resume? :agent :on-event})
:on-user-input-request :on-elicitation-request :hooks :working-directory :disable-resume? :agent :on-event})

(s/def ::resume-session-config
(closed-keys
Expand All @@ -344,7 +346,7 @@
::provider ::streaming?
::mcp-servers ::custom-agents ::config-dir ::skill-directories
::disabled-skills ::infinite-sessions ::reasoning-effort
::on-user-input-request ::hooks ::working-directory ::disable-resume? ::agent
::on-user-input-request ::on-elicitation-request ::hooks ::working-directory ::disable-resume? ::agent
::on-event])
resume-session-config-keys))

Expand All @@ -357,7 +359,7 @@
::provider ::streaming?
::mcp-servers ::custom-agents ::config-dir ::skill-directories
::disabled-skills ::infinite-sessions ::reasoning-effort
::on-user-input-request ::hooks ::working-directory ::disable-resume? ::agent
::on-user-input-request ::on-elicitation-request ::hooks ::working-directory ::disable-resume? ::agent
::on-event])
resume-session-config-keys))

Expand Down Expand Up @@ -555,7 +557,11 @@
:copilot/session.tools_updated :copilot/session.background_tasks_changed
:copilot/session.skills_loaded :copilot/session.mcp_servers_loaded
:copilot/session.mcp_server_status_changed :copilot/session.extensions_loaded
:copilot/session.custom_agents_updated})
:copilot/session.custom_agents_updated
;; Capability and sampling events (PR #908)
:copilot/capabilities.changed
:copilot/sampling.requested :copilot/sampling.completed
:copilot/session.remote_steerable_changed})

;; Session events
(s/def ::already-in-use? boolean?)
Expand Down