Skip to content
Merged
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
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@ All notable changes to this project will be documented in this file. This change
## [Unreleased]

### Added (v0.2.1 sync)
- **`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).
- **`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).
- **`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).
- **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).
- **`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).
- **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).
- **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).
- **`skill.invoked` event `:description` field** — optional `:description` from SKILL.md frontmatter (upstream PR #916).
- **`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).
- **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).
- **`aborted?` on `session.task_complete`** — optional boolean indicating the preceding agentic loop was cancelled via abort signal. New `::aborted?` spec (upstream PR #917).
- **`timeout` tool result type** — `::result-type` now accepts `:timeout` / `"timeout"` for tool calls that timed out (upstream PR #970).
- Integration tests for elicitation provider routing, handler error→cancel fallback, capabilities.changed updates, and requestElicitation wire flag.

### Changed (v0.2.1 sync)
- **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).
- **`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).

## [0.2.1.1-SNAPSHOT] - 2026-03-26
Expand Down
106 changes: 101 additions & 5 deletions doc/reference/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ Get information about the current shared client state. Returns `nil` if no share
| `: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` |
| `: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 |
| `: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` |
| `: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) |

### Methods

Expand Down Expand Up @@ -254,6 +255,8 @@ Create a client and session together, ensuring both are cleaned up on exit.
| `:hooks` | map | Lifecycle hooks (see below) |
| `: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. |
| `: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. |
| `: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) |
| `: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) |

#### `resume-session`

Expand Down Expand Up @@ -1135,8 +1138,8 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor
| `:copilot/session.mode_changed` | Session agent mode changed; data: `{:previous-mode "...", :new-mode "..."}` |
| `:copilot/session.plan_changed` | Session plan created/updated/deleted; data: `{:operation "create"/"update"/"delete"}` |
| `:copilot/session.workspace_file_changed` | Workspace file created or updated; data: `{:path "...", :operation "create"/"update"}` |
| `:copilot/session.task_complete` | Task completed by the session agent; data: `{:summary "..."}` (optional) |
| `:copilot/skill.invoked` | Skill invocation triggered |
| `:copilot/session.task_complete` | Task completed by the session agent; data: `{:summary "..." :aborted? false}` (both optional) |
| `:copilot/skill.invoked` | Skill invocation triggered; data includes :name, :path, :content, optional :description, :plugin-name, :plugin-version |
| `:copilot/user.message` | User message added |
| `:copilot/pending_messages.modified` | Pending message queue updated |
| `:copilot/assistant.turn_start` | Assistant turn started |
Expand All @@ -1154,9 +1157,9 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor
| `:copilot/tool.execution_progress` | Tool execution progress update |
| `:copilot/tool.execution_partial_result` | Tool execution partial result |
| `:copilot/tool.execution_complete` | Tool execution completed |
| `:copilot/subagent.started` | Subagent started |
| `:copilot/subagent.completed` | Subagent completed |
| `:copilot/subagent.failed` | Subagent failed |
| `:copilot/subagent.started` | Subagent started; data includes :tool-call-id, :agent-name, :agent-display-name, :agent-description |
| `:copilot/subagent.completed` | Subagent completed; data includes :tool-call-id, :agent-name, :agent-display-name, optional :model, :total-tool-calls, :total-tokens, :duration-ms |
| `: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 |
| `:copilot/subagent.selected` | Subagent selected |
| `:copilot/subagent.deselected` | Subagent deselected |
| `:copilot/hook.start` | Hook invocation started |
Expand Down Expand Up @@ -1186,6 +1189,10 @@ Convert an unqualified event keyword to a namespace-qualified `:copilot/` keywor
| `:copilot/session.mcp_server_status_changed` | MCP server status changed |
| `:copilot/session.extensions_loaded` | Extensions loaded for the session |
| `:copilot/session.custom_agents_updated` | Custom agents list updated |
| `:copilot/sampling.requested` | MCP sampling request initiated; ephemeral |
| `:copilot/sampling.completed` | MCP sampling request completed; ephemeral |
| `:copilot/session.remote_steerable_changed` | Session remote steering capability changed; data: `{:remote-steerable true/false}` |
| `:copilot/capabilities.changed` | Session capabilities dynamically changed (e.g., elicitation support); ephemeral. Data: `{:ui {:elicitation true/false}}` |

### Example: Handling Events

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

### Elicitation Provider

Provide a handler for elicitation requests from the agent. This enables the SDK client to act as a UI provider for form-based dialogs.

```clojure
(require '[github.copilot-sdk :as copilot])

(def session
(copilot/create-session client
{:on-permission-request copilot/approve-all
:on-elicitation-request
(fn [request {:keys [session-id]}]
;; request keys: :message, :requested-schema, :mode, :elicitation-source, :url
(println "Elicitation:" (:message request))
{:action "accept"
:content {:name "user-input"}})}))
```

The handler receives two arguments:

| Argument | Description |
|----------|-------------|
| `request` | Map with `:message` (string), optional `:requested-schema` (JSON Schema map), `:mode` (`"form"` or `"url"`), `:elicitation-source` (string), `:url` (string) |
| `ctx` | Map with `:session-id` (string) |

Return an `ElicitationResult` map:

| Key | Type | Description |
|-----|------|-------------|
| `:action` | string | `"accept"`, `"decline"`, or `"cancel"` |
| `:content` | map | Field values when action is `"accept"` |

If the handler throws, the SDK sends `{:action "cancel"}` to prevent the request from hanging.

When `:on-elicitation-request` is set, the session advertises `requestElicitation=true` in the create/resume RPC. Capabilities are updated dynamically via `capabilities.changed` events.

### Session Filesystem

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.

Configure the client with `:session-fs`:

```clojure
(require '[github.copilot-sdk :as copilot])

(def client
(copilot/client {:session-fs {:initial-cwd "/home/user/project"
:session-state-path "/sessions"
:conventions "posix"}}))
```

Provide a handler factory per session:

```clojure
(def session
(copilot/create-session client
{:on-permission-request copilot/approve-all
:create-session-fs-handler
(fn [session]
(let [store (atom {})]
{:read-file (fn [{:keys [path]}] {:content (get @store path "")})
:write-file (fn [{:keys [path content]}] (swap! store assoc path content) nil)
:append-file (fn [{:keys [path content]}] (swap! store update path str content) nil)
:exists (fn [{:keys [path]}] {:exists (contains? @store path)})
:stat (fn [{:keys [path]}] {:is-file true :is-directory false :size (count (get @store path "")) :mtime "2026-01-01T00:00:00Z"})
:mkdir (fn [_] nil)
:readdir (fn [_] {:entries []})
:readdir-with-types (fn [_] {:entries []})
:rm (fn [{:keys [path]}] (swap! store dissoc path) nil)
:rename (fn [{:keys [old-path new-path]}] (swap! store (fn [s] (-> s (assoc new-path (get s old-path "")) (dissoc old-path)))) nil)}))}))
```

The handler map requires all 10 operations:

| Key | Params | Returns |
|-----|--------|---------|
| `:read-file` | `{:session-id :path}` | `{:content "..."}` |
| `:write-file` | `{:session-id :path :content :mode}` | nil |
| `:append-file` | `{:session-id :path :content}` | nil |
| `:exists` | `{:session-id :path}` | `{:exists true/false}` |
| `:stat` | `{:session-id :path}` | `{:is-file :is-directory :size :mtime}` |
| `:mkdir` | `{:session-id :path :recursive}` | nil |
| `:readdir` | `{:session-id :path}` | `{:entries [...]}` |
| `:readdir-with-types` | `{:session-id :path}` | `{:entries [...]}` |
| `:rm` | `{:session-id :path :recursive :force}` | nil |
| `:rename` | `{:session-id :old-path :new-path}` | nil |

Handler functions may return values directly or via core.async channels.

### Session Hooks

Lifecycle hooks allow custom logic at various points during the session:
Expand Down
33 changes: 33 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,39 @@ clojure -A:examples -X ask-user-failure/run

---

## Example 18: Elicitation Provider (`elicitation_provider.clj`)

**Difficulty:** Intermediate
**Concepts:** Elicitation requests, provider callbacks, MCP OAuth, capabilities

Demonstrates how to act as an elicitation provider — handling form-based or URL-based input requests from MCP servers and sub-agents.

### What It Demonstrates

- Registering an `:on-elicitation-request` handler
- Inspecting elicitation mode (`"form"` vs `"url"`)
- Auto-filling form fields from a JSON Schema
- Observing `elicitation.requested` and `capabilities.changed` events

### Usage

```bash
clojure -A:examples -X elicitation-provider/run
```

### Code Walkthrough

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`:

```clojure
{:action "accept" ;; or "decline" or "cancel"
:content {:field-name "value"}}
```

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.

---

## Clojure vs JavaScript Comparison

Here's how common patterns compare between the Clojure and JavaScript SDKs:
Expand Down
113 changes: 113 additions & 0 deletions examples/elicitation_provider.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
(ns elicitation-provider
"Example: Acting as an elicitation provider.

When an MCP server or sub-agent needs user input (e.g., OAuth consent,
configuration choices), the runtime sends an elicitation request to the
SDK client. This example shows how to handle those requests.

The example simulates a scenario where an MCP server triggers an OAuth
consent flow — the handler prints the request and auto-approves it."
(:require [clojure.core.async :refer [chan tap go-loop <!]]
[github.copilot-sdk :as copilot :refer [evt]]))

;; See examples/README.md for usage

(defn handle-elicitation
"Handle an elicitation request from the runtime.
In a real app this would render a UI dialog or open a browser.
Here we print the request and auto-approve."
[request {:keys [session-id]}]
(println "\n📋 Elicitation request received!")
(println " Session:" session-id)
(println " Message:" (:message request))
(when-let [mode (:mode request)]
(println " Mode:" mode))
(when-let [source (:elicitation-source request)]
(println " Source:" source))
(when-let [url (:url request)]
(println " URL:" url))
(when-let [schema (:requested-schema request)]
(println " Schema:" (pr-str schema)))

;; Decide how to respond based on mode
(case (:mode request)
;; URL mode: the server wants us to open a browser
"url"
(do
(println " → Auto-approving URL-based elicitation (would open browser)")
{:action "accept"})

;; Form mode (or nil): the server wants form field values
(if-let [props (get-in request [:requested-schema :properties])]
(do
(println " → Auto-filling form fields:")
(let [content (reduce-kv
(fn [acc field-name field-schema]
(let [field-type (get field-schema "type" (:type field-schema))
value (case field-type
"boolean" true
"string" "auto-filled"
("number" "integer") 42
"auto-filled")]
(println (str " " field-name " (" field-type "): " value))
(assoc acc (keyword field-name) value)))
{}
props)]
{:action "accept" :content content}))
(do
(println " → No schema provided, approving without content")
{:action "accept"}))))

(defn run
"Run a session configured as an elicitation provider.

The agent is asked to interact with an MCP server that may trigger
elicitation requests. Even if no elicitation is triggered (model's
choice), the handler is registered and ready."
[_]
(println "=== Elicitation Provider Example ===")
(println "This example shows how to handle elicitation requests from MCP servers.\n")
(println "The session registers an :on-elicitation-request handler that auto-approves")
(println "any elicitation requests (OAuth consent, form inputs, etc.).\n")

(copilot/with-client-session [session {:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"
:on-elicitation-request handle-elicitation}]
;; Check if elicitation capability is advertised
(println "Elicitation supported:" (copilot/elicitation-supported? session))

(let [events-ch (chan 256)
done (promise)]
(tap (copilot/events session) events-ch)

(go-loop []
(when-let [event (<! events-ch)]
(condp = (:type event)
(evt :assistant.message)
(println "\n🤖 Agent:" (get-in event [:data :content]))

(evt :elicitation.requested)
(println "\n🔔 Elicitation event observed:" (get-in event [:data :message]))

(evt :capabilities.changed)
(println "\n🔄 Capabilities changed:" (:data event))

(evt :session.idle)
(deliver done true)

(evt :session.error)
(do
(println "❌ Error:" (get-in event [:data :message]))
(deliver done (ex-info "Session error" {:event event})))

nil)
(recur)))

(let [prompt "Say hello and tell me what capabilities this session has."]
(println "📤 You:" prompt)
(copilot/send! session {:prompt prompt}))

(let [result @done]
(when (instance? Exception result)
(throw result))
(println "\n=== Session Complete ===")))))
4 changes: 4 additions & 0 deletions run-all-examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@ clojure -A:examples -X lifecycle-hooks/run
echo ""
echo "=== reasoning-effort ==="
clojure -A:examples -X reasoning-effort/run

echo ""
echo "=== elicitation-provider ==="
clojure -A:examples -X elicitation-provider/run
Loading
Loading