Skip to content

Commit 6cb978b

Browse files
authored
Merge pull request #81 from copilot-community-sdk/upstream-sync/v0.2.1-batch
Port upstream PRs #908, #916, #917, #970: elicitation provider, sessionFs, event fields
2 parents 0ad7ce1 + d7f45f3 commit 6cb978b

10 files changed

Lines changed: 720 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@ All notable changes to this project will be documented in this file. This change
44
## [Unreleased]
55

66
### Added (v0.2.1 sync)
7-
- **`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).
7+
- **`remote-steerable?` field on `session.start` and `session.resume` events**event data now includes optional `:remote-steerable?` boolean field indicating whether the session supports remote steering via Mission Control. Replaces previous `:steerable?` (upstream PRs #927, #908).
88
- **`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).
9+
- **Elicitation provider support** — new `:on-elicitation-request` handler on `SessionConfig` and `ResumeSessionConfig`. When provided, sends `requestElicitation: true` in the session create/resume RPC. The runtime routes `elicitation.requested` broadcast events to the handler, and results are sent back via `session.ui.handlePendingElicitation` RPC. Handler errors automatically send a cancel response. New `::elicitation-request` and `::on-elicitation-request` specs (upstream PR #908).
10+
- **`capabilities.changed` event handling** — session capabilities are dynamically updated when `capabilities.changed` broadcast events are received, e.g. when another client joins with elicitation support (upstream PR #908).
11+
- **New event types**`sampling.requested`, `sampling.completed`, `session.remote_steerable_changed`, `capabilities.changed` added to event type enum and event sets (upstream PRs #908, #916).
12+
- **Subagent event data fields**`subagent.started`, `subagent.completed`, `subagent.failed` events now include optional `:model`, `:total-tool-calls`, `:total-tokens`, `:duration-ms` fields. New `::subagent.started-data`, `::subagent.completed-data`, `::subagent.failed-data` specs (upstream PR #916).
13+
- **`skill.invoked` event `:description` field** — optional `:description` from SKILL.md frontmatter (upstream PR #916).
14+
- **`session.custom_agents_updated` payload spec** — full `::session.custom_agents_updated-data` spec with `:agents` (array of agent metadata), `:warnings`, `:errors`. New `::custom-agent-info` spec (upstream PR #916).
15+
- **SessionFs virtual filesystem** — new `:session-fs` client option with `:initial-cwd`, `:session-state-path`, `:conventions`. Client calls `sessionFs.setProvider` RPC on connect. New `:create-session-fs-handler` on session config provides a per-session FS handler factory. The SDK dispatches incoming `sessionFs.*` RPC requests (10 operations: `readFile`, `writeFile`, `appendFile`, `exists`, `stat`, `mkdir`, `readdir`, `readdirWithTypes`, `rm`, `rename`) to the session's handler. Enables custom session storage backends (upstream PR #917).
16+
- **`aborted?` on `session.task_complete`** — optional boolean indicating the preceding agentic loop was cancelled via abort signal. New `::aborted?` spec (upstream PR #917).
17+
- **`timeout` tool result type**`::result-type` now accepts `:timeout` / `"timeout"` for tool calls that timed out (upstream PR #970).
18+
- Integration tests for elicitation provider routing, handler error→cancel fallback, capabilities.changed updates, and requestElicitation wire flag.
919

1020
### Changed (v0.2.1 sync)
21+
- **BREAKING**: `::steerable?` renamed to `::remote-steerable?` on `session.start` and `session.resume` event data, matching upstream wire field rename from `steerable` to `remoteSteerable` (upstream PR #908).
1122
- **`session.idle` is now ephemeral** — the runtime no longer persists `session.idle` events in session history. `get-messages` will no longer return `session.idle` events. Live event listeners (used by `send-and-wait!` and `send!`) are unaffected and still receive it (upstream PR #927).
1223

1324
## [0.2.1.1-SNAPSHOT] - 2026-03-26

doc/reference/API.md

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ Get information about the current shared client state. Returns `nil` if no share
130130
| `:use-logged-in-user?` | boolean | `true` | Use logged-in user auth. Defaults to `false` when `:github-token` is provided. Cannot be used with `:cli-url` |
131131
| `:on-list-models` | fn | nil | Zero-arg function returning model info maps. Bypasses `models.list` RPC; does not require `start!`. Results are cached the same way as RPC results |
132132
| `:is-child-process?` | boolean | `false` | When `true`, connect via own stdio to a parent Copilot CLI process (no process spawning). Requires `:use-stdio?` `true`; mutually exclusive with `:cli-url` |
133+
| `:session-fs` | map | nil | Session filesystem provider config. Keys: `:initial-cwd` (string, required), `:session-state-path` (string, required), `:conventions` (`"windows"` or `"posix"`, required). When set, the client calls `sessionFs.setProvider` on connect and routes filesystem operations through per-session handlers. See [Session Filesystem](#session-filesystem) |
133134

134135
### Methods
135136

@@ -254,6 +255,8 @@ Create a client and session together, ensuring both are cleaned up on exit.
254255
| `:hooks` | map | Lifecycle hooks (see below) |
255256
| `:agent` | string | Name of a custom agent to activate at session start. Must match a name in `:custom-agents`. Equivalent to calling `agent.select` after creation. |
256257
| `:on-event` | fn | Event handler (1-arg fn receiving event maps). Registered before the RPC call, guaranteeing early events like `session.start` are not missed. |
258+
| `:on-elicitation-request` | fn | Handler for elicitation requests from the agent. When provided, advertises `requestElicitation=true` and handles `elicitation.requested` broadcast events. Receives `(request ctx)` where request has `:message`, `:requested-schema`, `:mode`, `:elicitation-source`, `:url`. Returns an `ElicitationResult` map `{:action "accept"/"decline"/"cancel" :content {...}}`. See [Elicitation Provider](#elicitation-provider) |
259+
| `:create-session-fs-handler` | fn | Factory for session filesystem handlers. Required when `:session-fs` is set on the client. Called as `(factory session)`, returns a map of FS handler functions. See [Session Filesystem](#session-filesystem) |
257260

258261
#### `resume-session`
259262

@@ -1135,8 +1138,8 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor
11351138
| `:copilot/session.mode_changed` | Session agent mode changed; data: `{:previous-mode "...", :new-mode "..."}` |
11361139
| `:copilot/session.plan_changed` | Session plan created/updated/deleted; data: `{:operation "create"/"update"/"delete"}` |
11371140
| `:copilot/session.workspace_file_changed` | Workspace file created or updated; data: `{:path "...", :operation "create"/"update"}` |
1138-
| `:copilot/session.task_complete` | Task completed by the session agent; data: `{:summary "..."}` (optional) |
1139-
| `:copilot/skill.invoked` | Skill invocation triggered |
1141+
| `:copilot/session.task_complete` | Task completed by the session agent; data: `{:summary "..." :aborted? false}` (both optional) |
1142+
| `:copilot/skill.invoked` | Skill invocation triggered; data includes :name, :path, :content, optional :description, :plugin-name, :plugin-version |
11401143
| `:copilot/user.message` | User message added |
11411144
| `:copilot/pending_messages.modified` | Pending message queue updated |
11421145
| `:copilot/assistant.turn_start` | Assistant turn started |
@@ -1154,9 +1157,9 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor
11541157
| `:copilot/tool.execution_progress` | Tool execution progress update |
11551158
| `:copilot/tool.execution_partial_result` | Tool execution partial result |
11561159
| `:copilot/tool.execution_complete` | Tool execution completed |
1157-
| `:copilot/subagent.started` | Subagent started |
1158-
| `:copilot/subagent.completed` | Subagent completed |
1159-
| `:copilot/subagent.failed` | Subagent failed |
1160+
| `:copilot/subagent.started` | Subagent started; data includes :tool-call-id, :agent-name, :agent-display-name, :agent-description |
1161+
| `:copilot/subagent.completed` | Subagent completed; data includes :tool-call-id, :agent-name, :agent-display-name, optional :model, :total-tool-calls, :total-tokens, :duration-ms |
1162+
| `:copilot/subagent.failed` | Subagent failed; data includes :tool-call-id, :agent-name, :agent-display-name, :error, optional :model, :total-tool-calls, :total-tokens, :duration-ms |
11601163
| `:copilot/subagent.selected` | Subagent selected |
11611164
| `:copilot/subagent.deselected` | Subagent deselected |
11621165
| `:copilot/hook.start` | Hook invocation started |
@@ -1186,6 +1189,10 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor
11861189
| `:copilot/session.mcp_server_status_changed` | MCP server status changed |
11871190
| `:copilot/session.extensions_loaded` | Extensions loaded for the session |
11881191
| `:copilot/session.custom_agents_updated` | Custom agents list updated |
1192+
| `:copilot/sampling.requested` | MCP sampling request initiated; ephemeral |
1193+
| `:copilot/sampling.completed` | MCP sampling request completed; ephemeral |
1194+
| `:copilot/session.remote_steerable_changed` | Session remote steering capability changed; data: `{:remote-steerable true/false}` |
1195+
| `:copilot/capabilities.changed` | Session capabilities dynamically changed (e.g., elicitation support); ephemeral. Data: `{:ui {:elicitation true/false}}` |
11891196

11901197
### Example: Handling Events
11911198

@@ -1673,6 +1680,95 @@ The response map should include:
16731680
- `:answer` - The user's answer (string, required). `:response` is also accepted for convenience.
16741681
- `:was-freeform` - Whether the answer was freeform (boolean, defaults to true)
16751682

1683+
### Elicitation Provider
1684+
1685+
Provide a handler for elicitation requests from the agent. This enables the SDK client to act as a UI provider for form-based dialogs.
1686+
1687+
```clojure
1688+
(require '[github.copilot-sdk :as copilot])
1689+
1690+
(def session
1691+
(copilot/create-session client
1692+
{:on-permission-request copilot/approve-all
1693+
:on-elicitation-request
1694+
(fn [request {:keys [session-id]}]
1695+
;; request keys: :message, :requested-schema, :mode, :elicitation-source, :url
1696+
(println "Elicitation:" (:message request))
1697+
{:action "accept"
1698+
:content {:name "user-input"}})}))
1699+
```
1700+
1701+
The handler receives two arguments:
1702+
1703+
| Argument | Description |
1704+
|----------|-------------|
1705+
| `request` | Map with `:message` (string), optional `:requested-schema` (JSON Schema map), `:mode` (`"form"` or `"url"`), `:elicitation-source` (string), `:url` (string) |
1706+
| `ctx` | Map with `:session-id` (string) |
1707+
1708+
Return an `ElicitationResult` map:
1709+
1710+
| Key | Type | Description |
1711+
|-----|------|-------------|
1712+
| `:action` | string | `"accept"`, `"decline"`, or `"cancel"` |
1713+
| `:content` | map | Field values when action is `"accept"` |
1714+
1715+
If the handler throws, the SDK sends `{:action "cancel"}` to prevent the request from hanging.
1716+
1717+
When `:on-elicitation-request` is set, the session advertises `requestElicitation=true` in the create/resume RPC. Capabilities are updated dynamically via `capabilities.changed` events.
1718+
1719+
### Session Filesystem
1720+
1721+
Virtualize per-session storage with custom filesystem handlers. The runtime routes all session-scoped file I/O (event logs, large outputs, checkpoints) through the provided callbacks.
1722+
1723+
Configure the client with `:session-fs`:
1724+
1725+
```clojure
1726+
(require '[github.copilot-sdk :as copilot])
1727+
1728+
(def client
1729+
(copilot/client {:session-fs {:initial-cwd "/home/user/project"
1730+
:session-state-path "/sessions"
1731+
:conventions "posix"}}))
1732+
```
1733+
1734+
Provide a handler factory per session:
1735+
1736+
```clojure
1737+
(def session
1738+
(copilot/create-session client
1739+
{:on-permission-request copilot/approve-all
1740+
:create-session-fs-handler
1741+
(fn [session]
1742+
(let [store (atom {})]
1743+
{:read-file (fn [{:keys [path]}] {:content (get @store path "")})
1744+
:write-file (fn [{:keys [path content]}] (swap! store assoc path content) nil)
1745+
:append-file (fn [{:keys [path content]}] (swap! store update path str content) nil)
1746+
:exists (fn [{:keys [path]}] {:exists (contains? @store path)})
1747+
:stat (fn [{:keys [path]}] {:is-file true :is-directory false :size (count (get @store path "")) :mtime "2026-01-01T00:00:00Z"})
1748+
:mkdir (fn [_] nil)
1749+
:readdir (fn [_] {:entries []})
1750+
:readdir-with-types (fn [_] {:entries []})
1751+
:rm (fn [{:keys [path]}] (swap! store dissoc path) nil)
1752+
:rename (fn [{:keys [old-path new-path]}] (swap! store (fn [s] (-> s (assoc new-path (get s old-path "")) (dissoc old-path)))) nil)}))}))
1753+
```
1754+
1755+
The handler map requires all 10 operations:
1756+
1757+
| Key | Params | Returns |
1758+
|-----|--------|---------|
1759+
| `:read-file` | `{:session-id :path}` | `{:content "..."}` |
1760+
| `:write-file` | `{:session-id :path :content :mode}` | nil |
1761+
| `:append-file` | `{:session-id :path :content}` | nil |
1762+
| `:exists` | `{:session-id :path}` | `{:exists true/false}` |
1763+
| `:stat` | `{:session-id :path}` | `{:is-file :is-directory :size :mtime}` |
1764+
| `:mkdir` | `{:session-id :path :recursive}` | nil |
1765+
| `:readdir` | `{:session-id :path}` | `{:entries [...]}` |
1766+
| `:readdir-with-types` | `{:session-id :path}` | `{:entries [...]}` |
1767+
| `:rm` | `{:session-id :path :recursive :force}` | nil |
1768+
| `:rename` | `{:session-id :old-path :new-path}` | nil |
1769+
1770+
Handler functions may return values directly or via core.async channels.
1771+
16761772
### Session Hooks
16771773

16781774
Lifecycle hooks allow custom logic at various points during the session:

examples/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,39 @@ clojure -A:examples -X ask-user-failure/run
812812

813813
---
814814

815+
## Example 18: Elicitation Provider (`elicitation_provider.clj`)
816+
817+
**Difficulty:** Intermediate
818+
**Concepts:** Elicitation requests, provider callbacks, MCP OAuth, capabilities
819+
820+
Demonstrates how to act as an elicitation provider — handling form-based or URL-based input requests from MCP servers and sub-agents.
821+
822+
### What It Demonstrates
823+
824+
- Registering an `:on-elicitation-request` handler
825+
- Inspecting elicitation mode (`"form"` vs `"url"`)
826+
- Auto-filling form fields from a JSON Schema
827+
- Observing `elicitation.requested` and `capabilities.changed` events
828+
829+
### Usage
830+
831+
```bash
832+
clojure -A:examples -X elicitation-provider/run
833+
```
834+
835+
### Code Walkthrough
836+
837+
The handler receives a request map with `:message`, optional `:requested-schema` (JSON Schema), `:mode` (`"form"` or `"url"`), `:elicitation-source`, and `:url`. It returns an `ElicitationResult`:
838+
839+
```clojure
840+
{:action "accept" ;; or "decline" or "cancel"
841+
:content {:field-name "value"}}
842+
```
843+
844+
If the handler throws, the SDK sends `{:action "cancel"}` to prevent hanging. In a real application, the handler would render a UI dialog or open a browser for OAuth flows.
845+
846+
---
847+
815848
## Clojure vs JavaScript Comparison
816849

817850
Here's how common patterns compare between the Clojure and JavaScript SDKs:

examples/elicitation_provider.clj

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
(ns elicitation-provider
2+
"Example: Acting as an elicitation provider.
3+
4+
When an MCP server or sub-agent needs user input (e.g., OAuth consent,
5+
configuration choices), the runtime sends an elicitation request to the
6+
SDK client. This example shows how to handle those requests.
7+
8+
The example simulates a scenario where an MCP server triggers an OAuth
9+
consent flow — the handler prints the request and auto-approves it."
10+
(:require [clojure.core.async :refer [chan tap go-loop <!]]
11+
[github.copilot-sdk :as copilot :refer [evt]]))
12+
13+
;; See examples/README.md for usage
14+
15+
(defn handle-elicitation
16+
"Handle an elicitation request from the runtime.
17+
In a real app this would render a UI dialog or open a browser.
18+
Here we print the request and auto-approve."
19+
[request {:keys [session-id]}]
20+
(println "\n📋 Elicitation request received!")
21+
(println " Session:" session-id)
22+
(println " Message:" (:message request))
23+
(when-let [mode (:mode request)]
24+
(println " Mode:" mode))
25+
(when-let [source (:elicitation-source request)]
26+
(println " Source:" source))
27+
(when-let [url (:url request)]
28+
(println " URL:" url))
29+
(when-let [schema (:requested-schema request)]
30+
(println " Schema:" (pr-str schema)))
31+
32+
;; Decide how to respond based on mode
33+
(case (:mode request)
34+
;; URL mode: the server wants us to open a browser
35+
"url"
36+
(do
37+
(println " → Auto-approving URL-based elicitation (would open browser)")
38+
{:action "accept"})
39+
40+
;; Form mode (or nil): the server wants form field values
41+
(if-let [props (get-in request [:requested-schema :properties])]
42+
(do
43+
(println " → Auto-filling form fields:")
44+
(let [content (reduce-kv
45+
(fn [acc field-name field-schema]
46+
(let [field-type (get field-schema "type" (:type field-schema))
47+
value (case field-type
48+
"boolean" true
49+
"string" "auto-filled"
50+
("number" "integer") 42
51+
"auto-filled")]
52+
(println (str " " field-name " (" field-type "): " value))
53+
(assoc acc (keyword field-name) value)))
54+
{}
55+
props)]
56+
{:action "accept" :content content}))
57+
(do
58+
(println " → No schema provided, approving without content")
59+
{:action "accept"}))))
60+
61+
(defn run
62+
"Run a session configured as an elicitation provider.
63+
64+
The agent is asked to interact with an MCP server that may trigger
65+
elicitation requests. Even if no elicitation is triggered (model's
66+
choice), the handler is registered and ready."
67+
[_]
68+
(println "=== Elicitation Provider Example ===")
69+
(println "This example shows how to handle elicitation requests from MCP servers.\n")
70+
(println "The session registers an :on-elicitation-request handler that auto-approves")
71+
(println "any elicitation requests (OAuth consent, form inputs, etc.).\n")
72+
73+
(copilot/with-client-session [session {:on-permission-request copilot/approve-all
74+
:model "claude-haiku-4.5"
75+
:on-elicitation-request handle-elicitation}]
76+
;; Check if elicitation capability is advertised
77+
(println "Elicitation supported:" (copilot/elicitation-supported? session))
78+
79+
(let [events-ch (chan 256)
80+
done (promise)]
81+
(tap (copilot/events session) events-ch)
82+
83+
(go-loop []
84+
(when-let [event (<! events-ch)]
85+
(condp = (:type event)
86+
(evt :assistant.message)
87+
(println "\n🤖 Agent:" (get-in event [:data :content]))
88+
89+
(evt :elicitation.requested)
90+
(println "\n🔔 Elicitation event observed:" (get-in event [:data :message]))
91+
92+
(evt :capabilities.changed)
93+
(println "\n🔄 Capabilities changed:" (:data event))
94+
95+
(evt :session.idle)
96+
(deliver done true)
97+
98+
(evt :session.error)
99+
(do
100+
(println "❌ Error:" (get-in event [:data :message]))
101+
(deliver done (ex-info "Session error" {:event event})))
102+
103+
nil)
104+
(recur)))
105+
106+
(let [prompt "Say hello and tell me what capabilities this session has."]
107+
(println "📤 You:" prompt)
108+
(copilot/send! session {:prompt prompt}))
109+
110+
(let [result @done]
111+
(when (instance? Exception result)
112+
(throw result))
113+
(println "\n=== Session Complete ===")))))

run-all-examples.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ clojure -A:examples -X lifecycle-hooks/run
6161
echo ""
6262
echo "=== reasoning-effort ==="
6363
clojure -A:examples -X reasoning-effort/run
64+
65+
echo ""
66+
echo "=== elicitation-provider ==="
67+
clojure -A:examples -X elicitation-provider/run

0 commit comments

Comments
 (0)