diff --git a/CHANGELOG.md b/CHANGELOG.md index b7c3f47..4208f5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] +### Added (v0.2.2 sync) +- **`enableConfigDiscovery` session option** — new boolean `:enable-config-discovery` on session and resume configs. Auto-discovers `.mcp.json`, `.vscode/mcp.json`, skills, etc. Instruction files are always loaded regardless. (upstream PR #1044) +- **`modelCapabilities` override** — new `:model-capabilities` option on session config, resume config, and `switch-model!`/`set-model!`. Pass a partial capabilities map (e.g. `{:model-supports {:supports-vision true}}`) to override model capabilities for the session. (upstream PR #1029) +- **`history-truncate!`** — new experimental function to trigger manual truncation of session context (upstream PR #1039) +- **`sessions-fork!`** — new experimental function to fork the current session (upstream PR #1039) +- Integration tests for all new features (wire param verification, RPC routing) + +### Changed (v0.2.2 sync) +- **`compaction-compact!` RPC renamed** — underlying JSON-RPC method changed from `session.compaction.compact` to `session.history.compact` (upstream PR #1039). The Clojure function name is unchanged for backward compatibility. + ## [0.2.1.1] - 2026-04-04 ### Added - **Session RPC wrappers** — new experimental functions for session-level RPCs previously only accessible via `proto/send-request!`: diff --git a/README.md b/README.md index 04daa9c..71ca9f0 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Add to your `deps.edn`: ```clojure ;; From Maven Central -io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "0.2.1.1"} +io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "0.2.2.0"} ;; Or git dependency io.github.copilot-community-sdk/copilot-sdk-clojure {:git/url "https://github.com/copilot-community-sdk/copilot-sdk-clojure.git" diff --git a/build.clj b/build.clj index 3ef0dc1..2673881 100644 --- a/build.clj +++ b/build.clj @@ -7,7 +7,7 @@ (:import [java.io File])) (def lib 'io.github.copilot-community-sdk/copilot-sdk-clojure) -(def version "0.2.1.1") +(def version "0.2.2.0") (def class-dir "target/classes") (defn- try-sh diff --git a/doc/reference/API.md b/doc/reference/API.md index 75fa5c0..e8aed62 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -256,6 +256,8 @@ Create a client and session together, ensuring both are cleaned up on exit. | `: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. Single-arg handler receives an `ElicitationContext` map with `:session-id`, `: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) | +| `:enable-config-discovery` | boolean | Auto-discover `.mcp.json`, `.vscode/mcp.json`, skills, etc. Instruction files always load regardless. (upstream PR #1044) | +| `:model-capabilities` | map | Model capabilities override. DeepPartial of model capabilities, e.g. `{:model-supports {:supports-vision true}}`. (upstream PR #1029) | #### `resume-session` @@ -846,10 +848,18 @@ Get the current model for this session. Returns the model ID string, or nil if n ```clojure (copilot/switch-model! session "claude-sonnet-4.5") ;; => "claude-sonnet-4.5" + +;; With model capabilities override (upstream PR #1029): +(copilot/switch-model! session "gpt-5.4" + {:model-capabilities {:model-supports {:supports-vision true}}}) ``` Switch the model for this session mid-conversation. Returns the new model ID string, or nil. +Optional opts map: +- `:reasoning-effort` — Reasoning effort level ("low", "medium", "high", "xhigh") +- `:model-capabilities` — Model capabilities override map, e.g. `{:model-supports {:supports-vision true}}` + #### `set-model!` ```clojure @@ -1051,7 +1061,9 @@ Get the client that owns this session. | Function | Description | |----------|-------------| | `session/plugins-list` | List plugins. | -| `session/compaction-compact!` | Trigger manual context compaction. | +| `session/compaction-compact!` | Trigger manual context compaction (uses `session.history.compact` RPC). | +| `session/history-truncate!` | Trigger manual context truncation. | +| `session/sessions-fork!` | Fork the current session. | | `session/shell-exec!` | Execute a shell command. | | `session/shell-kill!` | Kill a running shell process. | diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj index 2a0fece..07b913d 100644 --- a/src/github/copilot_sdk/client.clj +++ b/src/github/copilot_sdk/client.clj @@ -1380,6 +1380,10 @@ 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))) + (some? (:enable-config-discovery config)) + (assoc :enable-config-discovery (:enable-config-discovery config)) + (:model-capabilities config) + (assoc :model-capabilities (util/clj->wire (:model-capabilities config))) true (assoc :env-value-mode "direct")))) (defn- build-resume-session-params @@ -1437,6 +1441,10 @@ true (assoc :hooks (boolean (:hooks config))) (:working-directory config) (assoc :working-directory (:working-directory config)) (:disable-resume? config) (assoc :disable-resume (:disable-resume? config)) + (some? (:enable-config-discovery config)) + (assoc :enable-config-discovery (:enable-config-discovery config)) + (:model-capabilities config) + (assoc :model-capabilities (util/clj->wire (:model-capabilities config))) true (assoc :env-value-mode "direct")))) (defn- pre-register-session @@ -1493,6 +1501,11 @@ :on-session-start, :on-session-end, :on-error-occurred} - :on-event - Event handler (1-arg fn) registered before the RPC call. Guarantees early events like session.start are not missed. + - :enable-config-discovery - Boolean. Auto-discover .mcp.json, .vscode/mcp.json, skills, etc. + Instruction files are always loaded regardless. (upstream PR #1044) + - :model-capabilities - Model capabilities override map (upstream PR #1029). + DeepPartial of model capabilities, e.g. + {:model-supports {:supports-vision true}} Returns a CopilotSession." [client config] @@ -1553,6 +1566,8 @@ - :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. + - :enable-config-discovery - Boolean. Auto-discover .mcp.json, skills, etc. (upstream PR #1044) + - :model-capabilities - Model capabilities override map (upstream PR #1029). Returns a CopilotSession." [client session-id config] diff --git a/src/github/copilot_sdk/instrument.clj b/src/github/copilot_sdk/instrument.clj index af2df33..f70b258 100644 --- a/src/github/copilot_sdk/instrument.clj +++ b/src/github/copilot_sdk/instrument.clj @@ -296,6 +296,14 @@ :args (s/cat :session ::specs/session) :ret map?) +(s/fdef github.copilot-sdk.session/history-truncate! + :args (s/cat :session ::specs/session) + :ret map?) + +(s/fdef github.copilot-sdk.session/sessions-fork! + :args (s/cat :session ::specs/session) + :ret map?) + (s/fdef github.copilot-sdk.session/shell-exec! :args (s/cat :session ::specs/session :command string?) :ret map?) @@ -490,6 +498,8 @@ github.copilot-sdk.session/extensions-reload! github.copilot-sdk.session/plugins-list github.copilot-sdk.session/compaction-compact! + github.copilot-sdk.session/history-truncate! + github.copilot-sdk.session/sessions-fork! github.copilot-sdk.session/shell-exec! github.copilot-sdk.session/shell-kill! github.copilot-sdk.session/mode-get @@ -582,6 +592,8 @@ github.copilot-sdk.session/extensions-reload! github.copilot-sdk.session/plugins-list github.copilot-sdk.session/compaction-compact! + github.copilot-sdk.session/history-truncate! + github.copilot-sdk.session/sessions-fork! github.copilot-sdk.session/shell-exec! github.copilot-sdk.session/shell-kill! github.copilot-sdk.session/mode-get diff --git a/src/github/copilot_sdk/session.clj b/src/github/copilot_sdk/session.clj index 8c74921..5c89247 100644 --- a/src/github/copilot_sdk/session.clj +++ b/src/github/copilot_sdk/session.clj @@ -944,7 +944,9 @@ The new model takes effect for the next message. Conversation history is preserved. Optional opts map: - - :reasoning-effort - Reasoning effort level for the new model (\"low\", \"medium\", \"high\", \"xhigh\") + - :reasoning-effort - Reasoning effort level for the new model (\"low\", \"medium\", \"high\", \"xhigh\") + - :model-capabilities - Model capabilities override map (upstream PR #1029) + e.g. {:model-supports {:supports-vision true}} Returns the new model ID string, or nil." ([session model-id] (switch-model! session model-id nil)) @@ -953,7 +955,9 @@ conn (connection-io client) params (cond-> {:sessionId session-id :modelId model-id} - (:reasoning-effort opts) (assoc :reasoningEffort (:reasoning-effort opts))) + (:reasoning-effort opts) (assoc :reasoningEffort (:reasoning-effort opts)) + (:model-capabilities opts) (assoc :modelCapabilities + (util/clj->wire (:model-capabilities opts)))) result (proto/send-request! conn "session.model.switchTo" params)] (:model-id result)))) @@ -1100,15 +1104,33 @@ (util/wire->clj (proto/send-request! conn "session.plugins.list" {:sessionId session-id})))) -;; -- Compaction -------------------------------------------------------------- +;; -- History (compaction / truncation) ---------------------------------------- (defn ^:experimental compaction-compact! - "Trigger manual compaction of the session context." + "Trigger manual compaction of the session context. + Note: renamed from session.compaction.compact to session.history.compact in upstream #1039." [session] (let [{:keys [session-id client]} session conn (connection-io client)] (util/wire->clj - (proto/send-request! conn "session.compaction.compact" {:sessionId session-id})))) + (proto/send-request! conn "session.history.compact" {:sessionId session-id})))) + +(defn ^:experimental history-truncate! + "Trigger manual truncation of the session context (upstream #1039)." + [session] + (let [{:keys [session-id client]} session + conn (connection-io client)] + (util/wire->clj + (proto/send-request! conn "session.history.truncate" {:sessionId session-id})))) + +(defn ^:experimental sessions-fork! + "Fork the current session (upstream #1039). + This is a server-scoped RPC." + [session] + (let [{:keys [session-id client]} session + conn (connection-io client)] + (util/wire->clj + (proto/send-request! conn "sessions.fork" {:sessionId session-id})))) ;; -- Shell ------------------------------------------------------------------- diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj index 32ab405..c947dd8 100644 --- a/src/github/copilot_sdk/specs.clj +++ b/src/github/copilot_sdk/specs.clj @@ -352,6 +352,12 @@ (s/def ::client-name ::non-blank-string) +;; enableConfigDiscovery: auto-discover MCP configs, skills, instruction files (upstream PR #1044) +(s/def ::enable-config-discovery boolean?) + +;; modelCapabilities override for session config / setModel (upstream PR #1029). +;; DeepPartial — same shape as ::model-capabilities since all fields are already optional. + (def session-config-keys #{:session-id :client-name :model :tools :commands :system-message :available-tools :excluded-tools :provider @@ -359,7 +365,8 @@ :custom-agents :config-dir :skill-directories :disabled-skills :large-output :infinite-sessions :reasoning-effort :on-user-input-request :on-elicitation-request :hooks - :working-directory :agent :on-event :create-session-fs-handler}) + :working-directory :agent :on-event :create-session-fs-handler + :enable-config-discovery :model-capabilities}) (s/def ::session-config (closed-keys @@ -370,7 +377,8 @@ ::custom-agents ::config-dir ::skill-directories ::disabled-skills ::large-output ::infinite-sessions ::reasoning-effort ::on-user-input-request ::on-elicitation-request ::hooks - ::working-directory ::agent ::on-event ::create-session-fs-handler]) + ::working-directory ::agent ::on-event ::create-session-fs-handler + ::enable-config-discovery ::model-capabilities]) session-config-keys)) (def ^:private resume-session-config-keys @@ -379,7 +387,7 @@ :mcp-servers :custom-agents :config-dir :skill-directories :disabled-skills :infinite-sessions :reasoning-effort :on-user-input-request :on-elicitation-request :hooks :working-directory :disable-resume? :agent :on-event - :create-session-fs-handler}) + :create-session-fs-handler :enable-config-discovery :model-capabilities}) (s/def ::resume-session-config (closed-keys @@ -389,7 +397,8 @@ ::mcp-servers ::custom-agents ::config-dir ::skill-directories ::disabled-skills ::infinite-sessions ::reasoning-effort ::on-user-input-request ::on-elicitation-request ::hooks ::working-directory ::disable-resume? ::agent - ::on-event ::create-session-fs-handler]) + ::on-event ::create-session-fs-handler + ::enable-config-discovery ::model-capabilities]) resume-session-config-keys)) ;; join-session config: same as resume-session-config but :on-permission-request is optional. @@ -402,7 +411,8 @@ ::mcp-servers ::custom-agents ::config-dir ::skill-directories ::disabled-skills ::infinite-sessions ::reasoning-effort ::on-user-input-request ::on-elicitation-request ::hooks ::working-directory ::disable-resume? ::agent - ::on-event ::create-session-fs-handler]) + ::on-event ::create-session-fs-handler + ::enable-config-discovery ::model-capabilities]) resume-session-config-keys)) ;; ----------------------------------------------------------------------------- diff --git a/test/github/copilot_sdk/integration_test.clj b/test/github/copilot_sdk/integration_test.clj index 20c5166..7d54ddb 100644 --- a/test/github/copilot_sdk/integration_test.clj +++ b/test/github/copilot_sdk/integration_test.clj @@ -2164,3 +2164,132 @@ {:on-permission-request sdk/approve-all})] (session/agent-reload! session) (is (some #(= "session.agent.reload" (:method %)) @requests))))) + +;; --------------------------------------------------------------------------- +;; v0.2.2 sync tests +;; --------------------------------------------------------------------------- + +(deftest test-enable-config-discovery-on-wire + (testing "enableConfigDiscovery is forwarded in session.create (upstream PR #1044)" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all + :enable-config-discovery true}) + create-params (get @seen "session.create")] + (is (true? (:enableConfigDiscovery create-params))))) + + (testing "enableConfigDiscovery is forwarded in session.resume (upstream PR #1044)" + (let [seen (atom {}) + session-id (sdk/session-id (sdk/create-session *test-client* {:on-permission-request sdk/approve-all})) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.resume"} method) + (swap! seen assoc method params)))) + _ (sdk/resume-session *test-client* session-id + {:on-permission-request sdk/approve-all + :enable-config-discovery false}) + resume-params (get @seen "session.resume")] + (is (false? (:enableConfigDiscovery resume-params))))) + + (testing "enableConfigDiscovery is omitted when not set" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all}) + create-params (get @seen "session.create")] + (is (not (contains? create-params :enableConfigDiscovery)))))) + +(deftest test-model-capabilities-on-wire + (testing "modelCapabilities is forwarded in session.create (upstream PR #1029)" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all + :model-capabilities {:model-supports {:supports-vision true}}}) + create-params (get @seen "session.create")] + (is (= true (get-in create-params [:modelCapabilities :modelSupports :supportsVision]))))) + + (testing "modelCapabilities is forwarded in session.resume (upstream PR #1029)" + (let [seen (atom {}) + session-id (sdk/session-id (sdk/create-session *test-client* {:on-permission-request sdk/approve-all})) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.resume"} method) + (swap! seen assoc method params)))) + _ (sdk/resume-session *test-client* session-id + {:on-permission-request sdk/approve-all + :model-capabilities {:model-supports {:supports-reasoning-effort true}}}) + resume-params (get @seen "session.resume")] + (is (= true (get-in resume-params [:modelCapabilities :modelSupports :supportsReasoningEffort]))))) + + (testing "modelCapabilities is omitted when not set" + (let [seen (atom {}) + _ (mock/set-request-hook! *mock-server* (fn [method params] + (when (#{"session.create"} method) + (swap! seen assoc method params)))) + _ (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all}) + create-params (get @seen "session.create")] + (is (not (contains? create-params :modelCapabilities)))))) + +(deftest test-switch-model-with-model-capabilities + (testing "switch-model! forwards modelCapabilities (upstream PR #1029)" + (let [captured-params (atom nil) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (when (= method "session.model.switchTo") + (reset! captured-params params)))) + session (sdk/create-session *test-client* {:on-permission-request sdk/approve-all}) + _ (sdk/switch-model! session "gpt-5.4" + {:model-capabilities {:model-supports {:supports-vision false}}})] + (is (= false (get-in @captured-params [:modelCapabilities :modelSupports :supportsVision]))))) + + (testing "set-model! forwards modelCapabilities (alias for switch-model!)" + (let [captured-params (atom nil) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (when (= method "session.model.switchTo") + (reset! captured-params params)))) + session (sdk/create-session *test-client* {:on-permission-request sdk/approve-all}) + _ (sdk/set-model! session "gpt-5.4" + {:model-capabilities {:model-supports {:supports-vision true}}})] + (is (= true (get-in @captured-params [:modelCapabilities :modelSupports :supportsVision])))))) + +(deftest test-history-compact-rpc-name + (testing "compaction-compact! uses session.history.compact RPC (upstream #1039)" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all})] + (session/compaction-compact! session) + (is (some #(= "session.history.compact" (:method %)) @requests)) + (is (not (some #(= "session.compaction.compact" (:method %)) @requests)))))) + +(deftest test-history-truncate-rpc + (testing "history-truncate! calls session.history.truncate RPC (upstream #1039)" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all})] + (session/history-truncate! session) + (is (some #(= "session.history.truncate" (:method %)) @requests))))) + +(deftest test-sessions-fork-rpc + (testing "sessions-fork! calls sessions.fork RPC (upstream #1039)" + (let [requests (atom []) + _ (mock/set-request-hook! *mock-server* + (fn [method params] + (swap! requests conj {:method method :params params}))) + session (sdk/create-session *test-client* + {:on-permission-request sdk/approve-all})] + (session/sessions-fork! session) + (is (some #(= "sessions.fork" (:method %)) @requests))))) diff --git a/test/github/copilot_sdk/mock_server.clj b/test/github/copilot_sdk/mock_server.clj index a594e64..d57abcd 100644 --- a/test/github/copilot_sdk/mock_server.clj +++ b/test/github/copilot_sdk/mock_server.clj @@ -346,6 +346,9 @@ "session.extensions.reload" {:success true} "session.plugins.list" {:plugins []} "session.compaction.compact" {:success true} + "session.history.compact" {:success true} + "session.history.truncate" {:success true} + "sessions.fork" {:sessionId (str (java.util.UUID/randomUUID))} "session.shell.exec" {:exitCode 0 :stdout "" :stderr ""} "session.shell.kill" {:success true} "session.ui.elicitation" {:action "accept" :content {}}