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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This change

## [Unreleased]

### Added (v0.1.32 sync)
- `:agent` optional string parameter in `create-session` and `resume-session` configs — pre-selects a custom agent by name when the session starts. Must match a name in `:custom-agents`. Equivalent to calling `agent.select` after creation (upstream PR #722).
- `:on-list-models` optional handler in client options — zero-arg function returning model info maps. Bypasses the `models.list` RPC call and does not require `start!`. Results use the same promise-based cache (upstream PR #730).
- `log!` session method — logs a message to the session timeline via `"session.log"` RPC. Accepts optional `:level` (`"info"`, `"warning"`, `"error"`) and `:ephemeral?` (transient, not persisted) options. Returns the event ID string (upstream PR #737).
- `:is-child-process?` client option — when `true`, the SDK connects via its own stdio to a parent Copilot CLI process instead of spawning a new one. Mutually exclusive with `:cli-url`; requires `:use-stdio?` to be `true` (or unset) (upstream PR #737).

## [0.1.30.1] - 2026-03-07
### Added
- `disconnect!` function as the preferred API for closing sessions, matching upstream SDK's `disconnect()` (upstream PR #599). `destroy!` is deprecated but still works as an alias.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Add to your `deps.edn`:

```clojure
;; From Maven Central
io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "0.1.30.1"}
io.github.copilot-community-sdk/copilot-sdk-clojure {:mvn/version "0.1.32.0"}

;; Or git dependency
io.github.copilot-community-sdk/copilot-sdk-clojure {:git/url "https://github.com/copilot-community-sdk/copilot-sdk-clojure.git"
Expand Down
2 changes: 1 addition & 1 deletion build.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
(:import [java.io File]))

(def lib 'io.github.copilot-community-sdk/copilot-sdk-clojure)
(def version "0.1.30.1")
(def version "0.1.32.0")
(def class-dir "target/classes")

(defn- try-sh
Expand Down
24 changes: 23 additions & 1 deletion doc/reference/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ Explicitly shutdown the shared client. Safe to call multiple times.
| `:env` | map | nil | Environment variables |
| `:github-token` | string | nil | GitHub token for authentication. Sets `COPILOT_SDK_AUTH_TOKEN` env var and passes `--auth-token-env` flag |
| `: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` |

### Methods

Expand Down Expand Up @@ -240,6 +242,7 @@ Create a client and session together, ensuring both are cleaned up on exit.
| `:reasoning-effort` | string | Reasoning effort level: `"low"`, `"medium"`, `"high"`, or `"xhigh"` |
| `:on-user-input-request` | fn | Handler for `ask_user` requests (see below) |
| `: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. |

#### `resume-session`

Expand Down Expand Up @@ -340,7 +343,9 @@ Get current authentication status. Returns:
```

List available models with their metadata. Results are cached per client connection.
Requires authentication. Returns a vector of model info maps:
When `:on-list-models` handler is provided in client options, calls the handler
instead of the RPC method (no connection required).
Requires authentication (unless `:on-list-models` is provided). Returns a vector of model info maps:
```clojure
[{:id "gpt-5.2"
:name "GPT-5.2"
Expand Down Expand Up @@ -780,6 +785,23 @@ Alias for `switch-model!`, matching the upstream SDK's `setModel()` API.
;; After: claude-sonnet-4.5
```

#### `log!`

```clojure
(copilot/log! session "Processing started")
(copilot/log! session "Something went wrong" {:level "error"})
(copilot/log! session "Temporary note" {:ephemeral? true})
```

Log a message to the session timeline. Returns the event ID string.

**Options (optional map):**

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `:level` | string | `"info"` | Log severity: `"info"`, `"warning"`, or `"error"` |
| `:ephemeral?` | boolean | `false` | When `true`, the message is transient and not persisted to disk |

#### `disconnect!`

```clojure
Expand Down
16 changes: 16 additions & 0 deletions src/github/copilot_sdk.clj
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,22 @@
[session model-id]
(session/set-model! session model-id))

(defn log!
"Log a message to the session timeline.
Options (optional map):
- :level - \"info\", \"warning\", or \"error\" (default: \"info\")
- :ephemeral? - when true, message is not persisted to disk (default: false)
Returns the event ID string.

Example:
```clojure
(copilot/log! session \"Processing started\")
(copilot/log! session \"Something went wrong\" {:level \"error\"})
(copilot/log! session \"Temporary note\" {:ephemeral? true})
```"
([session message] (session/log! session message))
([session message opts] (session/log! session message opts)))

(defn session-config
"Get the configuration that was passed to create this session.

Expand Down
141 changes: 100 additions & 41 deletions src/github/copilot_sdk/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@
- :tool-timeout-ms - Timeout for tool calls that return a channel (default: 120000)
- :env - Environment variables map
- :github-token - GitHub token for authentication (sets COPILOT_SDK_AUTH_TOKEN env var)
- :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided)"
- :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided)
- :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!"
([]
(client {}))
([opts]
Expand All @@ -133,6 +135,14 @@
{:cli-url (:cli-url opts)
:github-token (when (:github-token opts) "***")
:use-logged-in-user? (:use-logged-in-user? opts)})))
;; Validation: is-child-process? is mutually exclusive with cli-url
(when (and (:is-child-process? opts) (:cli-url opts))
(throw (ex-info "is-child-process? is mutually exclusive with cli-url"
{:is-child-process? true :cli-url (:cli-url opts)})))
;; Validation: is-child-process? requires stdio transport
(when (and (:is-child-process? opts) (= false (:use-stdio? opts)))
(throw (ex-info "is-child-process? requires use-stdio? to be true (or unset)"
{:is-child-process? true :use-stdio? false})))
(when-not (s/valid? ::specs/client-options opts)
(let [unknown (specs/unknown-keys opts specs/client-options-keys)
explain (s/explain-data ::specs/client-options opts)
Expand All @@ -158,18 +168,23 @@
(and (:github-token opts) (nil? (:use-logged-in-user? opts)))
(assoc :use-logged-in-user? false))
merged (merge (default-options) opts-with-defaults)
external? (boolean (:cli-url opts))
{:keys [host port]} (when (:cli-url opts)
child-process? (:is-child-process? opts)
cli-url? (boolean (:cli-url opts))
external? (or cli-url? child-process?)
{:keys [host port]} (when cli-url?
(parse-cli-url (:cli-url opts)))
final-opts (cond-> merged
external? (-> (assoc :use-stdio? false)
(assoc :host host)
(assoc :port port)
(assoc :external-server? true)))]
{:options final-opts
:external-server? external?
:actual-host (or host "localhost")
:state (atom (assoc (initial-state port) :options final-opts))})))
cli-url? (-> (assoc :use-stdio? false)
(assoc :host host)
(assoc :port port)
(assoc :external-server? true))
child-process? (assoc :external-server? true))]
(cond-> {:options final-opts
:external-server? external?
:actual-host (or host "localhost")
:state (atom (assoc (initial-state port) :options final-opts))}
(:on-list-models opts)
(assoc :on-list-models (:on-list-models opts))))))

(defn state
"Get the current connection state."
Expand Down Expand Up @@ -513,6 +528,32 @@
(let [conn (proto/connect (:stdout process) (:stdin process) (:state client))]
(swap! (:state client) assoc :connection-io conn))))

(defn- non-closing-input-stream
"Wrap an InputStream so that .close is a no-op.
Prevents proto/disconnect from closing System/in."
^java.io.InputStream [^java.io.InputStream in]
(proxy [java.io.FilterInputStream] [in]
(close [] nil)))

(defn- non-closing-output-stream
"Wrap an OutputStream so that .close flushes but does not close.
Prevents proto/disconnect from closing System/out while ensuring
buffered bytes are sent."
^java.io.OutputStream [^java.io.OutputStream out]
(proxy [java.io.FilterOutputStream] [out]
(close [] (.flush ^java.io.OutputStream out))))

(defn- connect-parent-stdio!
"Connect via own stdio to a parent Copilot CLI process (child process mode).
Wraps System/in and System/out in non-closing wrappers so that
proto/disconnect does not close the JVM's global stdio streams."
[client]
(swap! (:state client) assoc :connection (proto/initial-connection-state))
(let [in (non-closing-input-stream System/in)
out (non-closing-output-stream System/out)
conn (proto/connect in out (:state client))]
(swap! (:state client) assoc :connection-io conn)))

(defn- connect-tcp!
"Connect via TCP to the CLI server."
[client]
Expand Down Expand Up @@ -658,11 +699,22 @@
(swap! (:state client) assoc :actual-port port)))))

;; Connect to server
(if (or (:external-server? client)
(not (:use-stdio? (:options client))))
(cond
;; Child process mode: use own stdin/stdout to talk to parent
(:is-child-process? (:options client))
(do
(log/debug "Connecting via parent stdio (child process mode)")
(connect-parent-stdio! client))

;; External server (cli-url) or TCP mode
(or (:external-server? client)
(not (:use-stdio? (:options client))))
(do
(log/debug "Connecting via TCP")
(connect-tcp! client))

;; Normal stdio to spawned process
:else
(do
(log/debug "Connecting via stdio")
(connect-stdio! client)))
Expand Down Expand Up @@ -924,7 +976,9 @@
"List available models with their metadata.
Results are cached per client connection to prevent rate limiting under concurrency.
Cache is cleared on stop!/force-stop!.
Requires authentication.
When :on-list-models handler is provided in client options, calls the handler
instead of the RPC method. The handler does not require a CLI connection.
Requires authentication (unless :on-list-models handler is provided).
Returns a vector of model info maps with keys:
:id :name :vendor :family :version :max-input-tokens :max-output-tokens
:preview? :default-temperature :model-picker-priority
Expand All @@ -939,33 +993,36 @@
:supports-reasoning-effort (legacy flat key)
:vision-limits {:supported-media-types :max-prompt-images :max-prompt-image-size} (legacy)"
[client]
(ensure-connected! client)
(let [p (promise)
entry (swap! (:state client) update :models-cache #(or % p))
cached (:models-cache entry)]
(cond
;; Already cached result (immutable, no need to copy)
(vector? cached)
cached

;; We won the race and must fetch
(identical? cached p)
(try
(let [models (fetch-models! client)]
(deliver p models)
(swap! (:state client) assoc :models-cache models)
models)
(catch Exception e
(deliver p e)
(swap! (:state client) assoc :models-cache nil)
(throw e)))

;; Another thread is fetching, wait on promise
:else
(let [result @cached]
(if (instance? Exception result)
(throw result)
result)))))
(let [handler (:on-list-models client)]
(when-not handler (ensure-connected! client))
(let [p (promise)
entry (swap! (:state client) update :models-cache #(or % p))
cached (:models-cache entry)]
(cond
;; Already cached result (immutable, no need to copy)
(vector? cached)
cached

;; We won the race and must fetch
(identical? cached p)
(try
(let [models (if handler
(vec (handler))
(fetch-models! client))]
(deliver p models)
(swap! (:state client) assoc :models-cache models)
models)
(catch Exception e
(deliver p e)
(swap! (:state client) assoc :models-cache nil)
(throw e)))

;; Another thread is fetching, wait on promise
:else
(let [result @cached]
(if (instance? Exception result)
(throw result)
result))))))

(defn list-tools
"List available tools with their metadata.
Expand Down Expand Up @@ -1079,6 +1136,7 @@
(:working-directory config) (assoc :working-directory (:working-directory config))
wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions)
(: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 :hooks (boolean (:hooks config)))
true (assoc :env-value-mode "direct"))))
Expand Down Expand Up @@ -1127,6 +1185,7 @@
(:disabled-skills config) (assoc :disabled-skills (:disabled-skills config))
wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions)
(: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 :hooks (boolean (:hooks config)))
(:working-directory config) (assoc :working-directory (:working-directory config))
Expand Down
8 changes: 8 additions & 0 deletions src/github/copilot_sdk/instrument.clj
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@
:model-id string?)
:ret ::specs/model-id)

(s/fdef github.copilot-sdk.session/log!
:args (s/cat :session ::specs/session
:message string?
:opts (s/? (s/nilable ::specs/log-options)))
:ret ::specs/event-id)

;; -----------------------------------------------------------------------------
;; Function specs for helpers namespace
;; -----------------------------------------------------------------------------
Expand Down Expand Up @@ -288,6 +294,7 @@
github.copilot-sdk.session/get-current-model
github.copilot-sdk.session/switch-model!
github.copilot-sdk.session/set-model!
github.copilot-sdk.session/log!
github.copilot-sdk.session/events
github.copilot-sdk.session/subscribe-events
github.copilot-sdk.session/unsubscribe-events
Expand Down Expand Up @@ -337,6 +344,7 @@
github.copilot-sdk.session/get-current-model
github.copilot-sdk.session/switch-model!
github.copilot-sdk.session/set-model!
github.copilot-sdk.session/log!
github.copilot-sdk.session/events
github.copilot-sdk.session/subscribe-events
github.copilot-sdk.session/unsubscribe-events
Expand Down
16 changes: 16 additions & 0 deletions src/github/copilot_sdk/session.clj
Original file line number Diff line number Diff line change
Expand Up @@ -740,3 +740,19 @@
See switch-model! for details."
[session model-id]
(switch-model! session model-id))

(defn log!
"Log a message to the session timeline.
Options (optional map):
- :level - \"info\", \"warning\", or \"error\" (default: \"info\")
- :ephemeral? - when true, message is not persisted to disk (default: false)
Returns the event ID string."
([session message] (log! session message nil))
([session message opts]
(let [{:keys [session-id client]} session
conn (connection-io client)
params (cond-> {:sessionId session-id :message message}
(:level opts) (assoc :level (:level opts))
(:ephemeral? opts) (assoc :ephemeral (:ephemeral? opts)))
result (proto/send-request! conn "session.log" params)]
(:event-id result))))
Loading