diff --git a/CHANGELOG.md b/CHANGELOG.md index 4696e3d..59236c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,15 @@ All notable changes to this project will be documented in this file. This change ## [Unreleased] -### Added (v0.2.1 sync) +### Added (upstream PR #917) +- **Session filesystem provider** — new `:session-fs` option in `create-client` enables applications to virtualize per-session file I/O (event log, large output, etc.) through custom callbacks instead of the server's default local filesystem. Provide a `SessionFsConfig` map with `:initial-cwd`, `:session-state-path`, and `:conventions` (`"windows"` or `"posix"`). On connect, the client sends a `sessionFs.setProvider` RPC to register itself as the provider. + - New `:create-session-fs-handler` option in `create-session` / `resume-session` config — required when `:session-fs` is set on the client. A factory `(fn [session] -> handler-map)` called once per session, returning a map implementing the file operations: `:read-file`, `:write-file`, `:append-file`, `:exists`, `:stat`, `:mkdir`, `:readdir`, `:readdir-with-types`, `:rm`, `:rename`. Handler functions receive normalized (kebab-case) params maps and may return values directly or via core.async channels. + - New specs: `::session-fs-config`, `::session-fs`, `::create-session-fs-handler`. + +### Changed (upstream PR #917) +- **`session.task_complete` event data** now includes optional `:aborted?` boolean field — `true` when the preceding agentic loop was cancelled via abort signal. New `::aborted` spec added; `::session.task_complete-data` updated to include it. +- **MCP server connection status** enum extended with `"needs-auth"` value for `mcp.server_status_changed` and `mcp.server_connected` events (in addition to existing `"connected"`, `"failed"`, `"pending"`, `"disabled"`, `"not_configured"`). + - **`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). diff --git a/doc/reference/API.md b/doc/reference/API.md index 3e4c91a..bbfaca6 100644 --- a/doc/reference/API.md +++ b/doc/reference/API.md @@ -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 | Custom session filesystem provider config (upstream PR #917). Map with `:initial-cwd` (string), `:session-state-path` (string), `:conventions` (`"windows"` or `"posix"`). When set, registers the client as the session filesystem provider on connect; each session config must include `:create-session-fs-handler` | ### Methods @@ -254,6 +255,7 @@ 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. | +| `:create-session-fs-handler` | fn | `(fn [session] -> handler-map)` Factory for the session filesystem handler. Required when `:session-fs` is configured in client options (upstream PR #917). The returned map implements file operations: `:read-file`, `:write-file`, `:append-file`, `:exists`, `:stat`, `:mkdir`, `:readdir`, `:readdir-with-types`, `:rm`, `:rename`. | #### `resume-session` diff --git a/src/github/copilot_sdk/client.clj b/src/github/copilot_sdk/client.clj index ba2a4e2..f8ce530 100644 --- a/src/github/copilot_sdk/client.clj +++ b/src/github/copilot_sdk/client.clj @@ -5,6 +5,7 @@ [clojure.java.io :as io] [clojure.string :as str] [clojure.data.json :as json] + [camel-snake-kebab.core :as csk] [github.copilot-sdk.protocol :as proto] [github.copilot-sdk.process :as proc] [github.copilot-sdk.specs :as specs] @@ -137,7 +138,11 @@ - :is-child-process? - When true, SDK is a child of an existing Copilot CLI process and uses stdio to communicate with it (no process spawning) - :on-list-models - Zero-arg fn returning a seq of model info maps; bypasses the RPC call and does not require start! - :telemetry - OpenTelemetry config map with optional keys :otlp-endpoint, :file-path, :exporter-type, :source-name, :capture-content? - - :on-get-trace-context - Zero-arg fn returning {:traceparent ... :tracestate ...} for distributed trace propagation" + - :on-get-trace-context - Zero-arg fn returning {:traceparent ... :tracestate ...} for distributed trace propagation + - :session-fs - Custom session filesystem provider config map (upstream PR #917): + {:initial-cwd \"...\" :session-state-path \"...\" :conventions \"windows\"|\"posix\"} + When provided, registers the client as the session filesystem provider on connect. + Each session config must include :create-session-fs-handler when this option is set." ([] (client {})) ([opts] @@ -531,6 +536,32 @@ (when (seq lines) (str/join "\n" lines))))) +(defn- handle-session-fs-request! + "Handle a sessionFs.* RPC request from the server. + Dispatches to the appropriate function in the session's :session-fs-handler map. + The handler map keys are kebab-case keywords matching the camelCase operation name, + e.g. :read-file for sessionFs.readFile. + Handler functions receive the normalized (kebab-case) params map and may return a + value directly or a channel. Returns a channel delivering {:result ...} or {:error ...}." + [client method params] + (go + (let [{:keys [session-id]} params + handler (get-in @(:state client) [:sessions session-id :session-fs-handler])] + (if-not handler + {:error {:code -32001 :message (str "No sessionFs handler registered for session: " session-id)}} + (let [op (subs method (count "sessionFs.")) + op-kw (csk/->kebab-case-keyword op) + f (get handler op-kw)] + (if-not f + {:error {:code -32601 :message (str "SessionFs handler does not implement: " op)}} + (try + (let [result-or-ch (f params) + result (if (instance? clojure.core.async.impl.channels.ManyToManyChannel result-or-ch) + (wire session-fs)))) + ;; Set up notification routing and request handling (start-notification-router! client) (setup-request-handler! client) @@ -1327,14 +1367,23 @@ This ensures early events (e.g. session.start) are not dropped. Returns the CopilotSession handle." [client session-id config] - (session/create-session client session-id + (let [session (session/create-session client session-id {:tools (:tools config) :commands (:commands config) :on-permission-request (:on-permission-request config) :on-user-input-request (:on-user-input-request config) :hooks (:hooks config) :on-event (:on-event config) - :config config})) + :config config})] + ;; If sessionFs is configured in client options, set up per-session handler + (when (get-in client [:options :session-fs]) + (if-let [factory (:create-session-fs-handler config)] + (session/set-session-fs-handler! client session-id (factory session)) + (throw (ex-info + (str "A :create-session-fs-handler is required in session config " + "when :session-fs is configured in client options.") + {:config config})))) + session)) (defn create-session "Create a new conversation session. @@ -1370,6 +1419,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. + - :create-session-fs-handler - (fn [session] -> handler-map) Factory for the sessionFs handler. + Required when `:session-fs` is configured in client options. + The handler map must implement keys for each file operation: + :read-file, :write-file, :append-file, :exists, :stat, :mkdir, + :readdir, :readdir-with-types, :rm, :rename (upstream PR #917). Returns a CopilotSession." [client config] @@ -1419,6 +1473,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. + - :create-session-fs-handler - (fn [session] -> handler-map) Factory for the sessionFs handler. + Required when `:session-fs` is configured in client options (upstream PR #917). Returns a CopilotSession." [client session-id config] diff --git a/src/github/copilot_sdk/session.clj b/src/github/copilot_sdk/session.clj index bd8f84d..15b5fbd 100644 --- a/src/github/copilot_sdk/session.clj +++ b/src/github/copilot_sdk/session.clj @@ -70,6 +70,7 @@ :destroyed? false :workspace-path workspace-path :capabilities {} + :session-fs-handler nil :config config}) (assoc-in [:session-io session-id] {:event-chan event-chan @@ -111,6 +112,11 @@ [client session-id capabilities] (swap! (:state client) assoc-in [:sessions session-id :capabilities] (or capabilities {}))) +(defn set-session-fs-handler! + "Store the sessionFs handler in session state. Called by client during session create/resume." + [client session-id handler] + (swap! (:state client) assoc-in [:sessions session-id :session-fs-handler] handler)) + (defn register-transform-callbacks! "Store system message transform callbacks on a session. Callbacks is a map of wire section ID strings to 1-arity functions diff --git a/src/github/copilot_sdk/specs.clj b/src/github/copilot_sdk/specs.clj index 03b1444..92b5d1a 100644 --- a/src/github/copilot_sdk/specs.clj +++ b/src/github/copilot_sdk/specs.clj @@ -65,12 +65,32 @@ ;; Trace context provider: 0-arity fn returning {:traceparent ... :tracestate ...} (s/def ::on-get-trace-context fn?) +;; ----------------------------------------------------------------------------- +;; Session filesystem config (upstream PR #917) +;; ----------------------------------------------------------------------------- + +(s/def ::initial-cwd ::non-blank-string) +(s/def ::session-state-path ::non-blank-string) +(s/def ::conventions #{"windows" "posix"}) + +(s/def ::session-fs-config + (s/keys :req-un [::initial-cwd ::session-state-path ::conventions])) + +(s/def ::session-fs ::session-fs-config) + +(s/def ::create-session-fs-handler fn?) + +;; ----------------------------------------------------------------------------- +;; Client options +;; ----------------------------------------------------------------------------- + (def client-options-keys #{:cli-path :cli-args :cli-url :cwd :port :use-stdio? :log-level :auto-start? :auto-restart? :notification-queue-size :router-queue-size :tool-timeout-ms :env :github-token :use-logged-in-user? - :is-child-process? :on-list-models :telemetry :on-get-trace-context}) + :is-child-process? :on-list-models :telemetry :on-get-trace-context + :session-fs}) (s/def ::client-options (closed-keys @@ -78,7 +98,8 @@ ::use-stdio? ::log-level ::auto-start? ::auto-restart? ::notification-queue-size ::router-queue-size ::tool-timeout-ms ::env ::github-token ::use-logged-in-user? - ::is-child-process? ::on-list-models ::telemetry ::on-get-trace-context]) + ::is-child-process? ::on-list-models ::telemetry ::on-get-trace-context + ::session-fs]) client-options-keys)) ;; ----------------------------------------------------------------------------- @@ -316,7 +337,7 @@ :custom-agents :config-dir :skill-directories :disabled-skills :large-output :infinite-sessions :reasoning-effort :on-user-input-request :hooks - :working-directory :agent :on-event}) + :working-directory :agent :on-event :create-session-fs-handler}) (s/def ::session-config (closed-keys @@ -327,7 +348,7 @@ ::custom-agents ::config-dir ::skill-directories ::disabled-skills ::large-output ::infinite-sessions ::reasoning-effort ::on-user-input-request ::hooks - ::working-directory ::agent ::on-event]) + ::working-directory ::agent ::on-event ::create-session-fs-handler]) session-config-keys)) (def ^:private resume-session-config-keys @@ -335,7 +356,8 @@ :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 :hooks :working-directory :disable-resume? :agent :on-event + :create-session-fs-handler}) (s/def ::resume-session-config (closed-keys @@ -345,7 +367,7 @@ ::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-event ::create-session-fs-handler]) resume-session-config-keys)) ;; join-session config: same as resume-session-config but :on-permission-request is optional. @@ -358,7 +380,7 @@ ::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-event ::create-session-fs-handler]) resume-session-config-keys)) ;; ----------------------------------------------------------------------------- @@ -684,8 +706,9 @@ #(contains? #{"create" "update"} (:operation %)))) ;; Session task complete event +(s/def ::aborted boolean?) (s/def ::session.task_complete-data - (s/keys :opt-un [::summary])) + (s/keys :opt-un [::summary ::aborted])) ;; Skill invoked event (s/def ::allowed-tools (s/coll-of string?))