Skip to content

Commit c448d8e

Browse files
authored
Merge pull request #48 from copilot-community-sdk/upstream-sync/v0.1.32
feat: port upstream v0.1.31–v0.1.32 features
2 parents 91241b3 + 431f52d commit c448d8e

12 files changed

Lines changed: 303 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This change
33

44
## [Unreleased]
55

6+
### Added (v0.1.32 sync)
7+
- `: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).
8+
- `: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).
9+
- `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).
10+
- `: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).
11+
612
## [0.1.30.1] - 2026-03-07
713
### Added
814
- `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.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Add to your `deps.edn`:
2626

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

3131
;; Or git dependency
3232
io.github.copilot-community-sdk/copilot-sdk-clojure {:git/url "https://github.com/copilot-community-sdk/copilot-sdk-clojure.git"

build.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
(:import [java.io File]))
88

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

1313
(defn- try-sh

doc/reference/API.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ Explicitly shutdown the shared client. Safe to call multiple times.
119119
| `:env` | map | nil | Environment variables |
120120
| `:github-token` | string | nil | GitHub token for authentication. Sets `COPILOT_SDK_AUTH_TOKEN` env var and passes `--auth-token-env` flag |
121121
| `: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` |
122+
| `: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 |
123+
| `: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` |
122124

123125
### Methods
124126

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

244247
#### `resume-session`
245248

@@ -340,7 +343,9 @@ Get current authentication status. Returns:
340343
```
341344

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

788+
#### `log!`
789+
790+
```clojure
791+
(copilot/log! session "Processing started")
792+
(copilot/log! session "Something went wrong" {:level "error"})
793+
(copilot/log! session "Temporary note" {:ephemeral? true})
794+
```
795+
796+
Log a message to the session timeline. Returns the event ID string.
797+
798+
**Options (optional map):**
799+
800+
| Key | Type | Default | Description |
801+
|-----|------|---------|-------------|
802+
| `:level` | string | `"info"` | Log severity: `"info"`, `"warning"`, or `"error"` |
803+
| `:ephemeral?` | boolean | `false` | When `true`, the message is transient and not persisted to disk |
804+
783805
#### `disconnect!`
784806

785807
```clojure

src/github/copilot_sdk.clj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,22 @@
802802
[session model-id]
803803
(session/set-model! session model-id))
804804

805+
(defn log!
806+
"Log a message to the session timeline.
807+
Options (optional map):
808+
- :level - \"info\", \"warning\", or \"error\" (default: \"info\")
809+
- :ephemeral? - when true, message is not persisted to disk (default: false)
810+
Returns the event ID string.
811+
812+
Example:
813+
```clojure
814+
(copilot/log! session \"Processing started\")
815+
(copilot/log! session \"Something went wrong\" {:level \"error\"})
816+
(copilot/log! session \"Temporary note\" {:ephemeral? true})
817+
```"
818+
([session message] (session/log! session message))
819+
([session message opts] (session/log! session message opts)))
820+
805821
(defn session-config
806822
"Get the configuration that was passed to create this session.
807823

src/github/copilot_sdk/client.clj

Lines changed: 100 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@
119119
- :tool-timeout-ms - Timeout for tool calls that return a channel (default: 120000)
120120
- :env - Environment variables map
121121
- :github-token - GitHub token for authentication (sets COPILOT_SDK_AUTH_TOKEN env var)
122-
- :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided)"
122+
- :use-logged-in-user? - Whether to use logged-in user auth (default: true, false when github-token provided)
123+
- :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)
124+
- :on-list-models - Zero-arg fn returning a seq of model info maps; bypasses the RPC call and does not require start!"
123125
([]
124126
(client {}))
125127
([opts]
@@ -133,6 +135,14 @@
133135
{:cli-url (:cli-url opts)
134136
:github-token (when (:github-token opts) "***")
135137
:use-logged-in-user? (:use-logged-in-user? opts)})))
138+
;; Validation: is-child-process? is mutually exclusive with cli-url
139+
(when (and (:is-child-process? opts) (:cli-url opts))
140+
(throw (ex-info "is-child-process? is mutually exclusive with cli-url"
141+
{:is-child-process? true :cli-url (:cli-url opts)})))
142+
;; Validation: is-child-process? requires stdio transport
143+
(when (and (:is-child-process? opts) (= false (:use-stdio? opts)))
144+
(throw (ex-info "is-child-process? requires use-stdio? to be true (or unset)"
145+
{:is-child-process? true :use-stdio? false})))
136146
(when-not (s/valid? ::specs/client-options opts)
137147
(let [unknown (specs/unknown-keys opts specs/client-options-keys)
138148
explain (s/explain-data ::specs/client-options opts)
@@ -158,18 +168,23 @@
158168
(and (:github-token opts) (nil? (:use-logged-in-user? opts)))
159169
(assoc :use-logged-in-user? false))
160170
merged (merge (default-options) opts-with-defaults)
161-
external? (boolean (:cli-url opts))
162-
{:keys [host port]} (when (:cli-url opts)
171+
child-process? (:is-child-process? opts)
172+
cli-url? (boolean (:cli-url opts))
173+
external? (or cli-url? child-process?)
174+
{:keys [host port]} (when cli-url?
163175
(parse-cli-url (:cli-url opts)))
164176
final-opts (cond-> merged
165-
external? (-> (assoc :use-stdio? false)
166-
(assoc :host host)
167-
(assoc :port port)
168-
(assoc :external-server? true)))]
169-
{:options final-opts
170-
:external-server? external?
171-
:actual-host (or host "localhost")
172-
:state (atom (assoc (initial-state port) :options final-opts))})))
177+
cli-url? (-> (assoc :use-stdio? false)
178+
(assoc :host host)
179+
(assoc :port port)
180+
(assoc :external-server? true))
181+
child-process? (assoc :external-server? true))]
182+
(cond-> {:options final-opts
183+
:external-server? external?
184+
:actual-host (or host "localhost")
185+
:state (atom (assoc (initial-state port) :options final-opts))}
186+
(:on-list-models opts)
187+
(assoc :on-list-models (:on-list-models opts))))))
173188

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

531+
(defn- non-closing-input-stream
532+
"Wrap an InputStream so that .close is a no-op.
533+
Prevents proto/disconnect from closing System/in."
534+
^java.io.InputStream [^java.io.InputStream in]
535+
(proxy [java.io.FilterInputStream] [in]
536+
(close [] nil)))
537+
538+
(defn- non-closing-output-stream
539+
"Wrap an OutputStream so that .close flushes but does not close.
540+
Prevents proto/disconnect from closing System/out while ensuring
541+
buffered bytes are sent."
542+
^java.io.OutputStream [^java.io.OutputStream out]
543+
(proxy [java.io.FilterOutputStream] [out]
544+
(close [] (.flush ^java.io.OutputStream out))))
545+
546+
(defn- connect-parent-stdio!
547+
"Connect via own stdio to a parent Copilot CLI process (child process mode).
548+
Wraps System/in and System/out in non-closing wrappers so that
549+
proto/disconnect does not close the JVM's global stdio streams."
550+
[client]
551+
(swap! (:state client) assoc :connection (proto/initial-connection-state))
552+
(let [in (non-closing-input-stream System/in)
553+
out (non-closing-output-stream System/out)
554+
conn (proto/connect in out (:state client))]
555+
(swap! (:state client) assoc :connection-io conn)))
556+
516557
(defn- connect-tcp!
517558
"Connect via TCP to the CLI server."
518559
[client]
@@ -658,11 +699,22 @@
658699
(swap! (:state client) assoc :actual-port port)))))
659700

660701
;; Connect to server
661-
(if (or (:external-server? client)
662-
(not (:use-stdio? (:options client))))
702+
(cond
703+
;; Child process mode: use own stdin/stdout to talk to parent
704+
(:is-child-process? (:options client))
705+
(do
706+
(log/debug "Connecting via parent stdio (child process mode)")
707+
(connect-parent-stdio! client))
708+
709+
;; External server (cli-url) or TCP mode
710+
(or (:external-server? client)
711+
(not (:use-stdio? (:options client))))
663712
(do
664713
(log/debug "Connecting via TCP")
665714
(connect-tcp! client))
715+
716+
;; Normal stdio to spawned process
717+
:else
666718
(do
667719
(log/debug "Connecting via stdio")
668720
(connect-stdio! client)))
@@ -924,7 +976,9 @@
924976
"List available models with their metadata.
925977
Results are cached per client connection to prevent rate limiting under concurrency.
926978
Cache is cleared on stop!/force-stop!.
927-
Requires authentication.
979+
When :on-list-models handler is provided in client options, calls the handler
980+
instead of the RPC method. The handler does not require a CLI connection.
981+
Requires authentication (unless :on-list-models handler is provided).
928982
Returns a vector of model info maps with keys:
929983
:id :name :vendor :family :version :max-input-tokens :max-output-tokens
930984
:preview? :default-temperature :model-picker-priority
@@ -939,33 +993,36 @@
939993
:supports-reasoning-effort (legacy flat key)
940994
:vision-limits {:supported-media-types :max-prompt-images :max-prompt-image-size} (legacy)"
941995
[client]
942-
(ensure-connected! client)
943-
(let [p (promise)
944-
entry (swap! (:state client) update :models-cache #(or % p))
945-
cached (:models-cache entry)]
946-
(cond
947-
;; Already cached result (immutable, no need to copy)
948-
(vector? cached)
949-
cached
950-
951-
;; We won the race and must fetch
952-
(identical? cached p)
953-
(try
954-
(let [models (fetch-models! client)]
955-
(deliver p models)
956-
(swap! (:state client) assoc :models-cache models)
957-
models)
958-
(catch Exception e
959-
(deliver p e)
960-
(swap! (:state client) assoc :models-cache nil)
961-
(throw e)))
962-
963-
;; Another thread is fetching, wait on promise
964-
:else
965-
(let [result @cached]
966-
(if (instance? Exception result)
967-
(throw result)
968-
result)))))
996+
(let [handler (:on-list-models client)]
997+
(when-not handler (ensure-connected! client))
998+
(let [p (promise)
999+
entry (swap! (:state client) update :models-cache #(or % p))
1000+
cached (:models-cache entry)]
1001+
(cond
1002+
;; Already cached result (immutable, no need to copy)
1003+
(vector? cached)
1004+
cached
1005+
1006+
;; We won the race and must fetch
1007+
(identical? cached p)
1008+
(try
1009+
(let [models (if handler
1010+
(vec (handler))
1011+
(fetch-models! client))]
1012+
(deliver p models)
1013+
(swap! (:state client) assoc :models-cache models)
1014+
models)
1015+
(catch Exception e
1016+
(deliver p e)
1017+
(swap! (:state client) assoc :models-cache nil)
1018+
(throw e)))
1019+
1020+
;; Another thread is fetching, wait on promise
1021+
:else
1022+
(let [result @cached]
1023+
(if (instance? Exception result)
1024+
(throw result)
1025+
result))))))
9691026

9701027
(defn list-tools
9711028
"List available tools with their metadata.
@@ -1079,6 +1136,7 @@
10791136
(:working-directory config) (assoc :working-directory (:working-directory config))
10801137
wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions)
10811138
(:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config))
1139+
(:agent config) (assoc :agent (:agent config))
10821140
true (assoc :request-user-input (boolean (:on-user-input-request config)))
10831141
true (assoc :hooks (boolean (:hooks config)))
10841142
true (assoc :env-value-mode "direct"))))
@@ -1127,6 +1185,7 @@
11271185
(:disabled-skills config) (assoc :disabled-skills (:disabled-skills config))
11281186
wire-infinite-sessions (assoc :infinite-sessions wire-infinite-sessions)
11291187
(:reasoning-effort config) (assoc :reasoning-effort (:reasoning-effort config))
1188+
(:agent config) (assoc :agent (:agent config))
11301189
true (assoc :request-user-input (boolean (:on-user-input-request config)))
11311190
true (assoc :hooks (boolean (:hooks config)))
11321191
(:working-directory config) (assoc :working-directory (:working-directory config))

src/github/copilot_sdk/instrument.clj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@
221221
:model-id string?)
222222
:ret ::specs/model-id)
223223

224+
(s/fdef github.copilot-sdk.session/log!
225+
:args (s/cat :session ::specs/session
226+
:message string?
227+
:opts (s/? (s/nilable ::specs/log-options)))
228+
:ret ::specs/event-id)
229+
224230
;; -----------------------------------------------------------------------------
225231
;; Function specs for helpers namespace
226232
;; -----------------------------------------------------------------------------
@@ -288,6 +294,7 @@
288294
github.copilot-sdk.session/get-current-model
289295
github.copilot-sdk.session/switch-model!
290296
github.copilot-sdk.session/set-model!
297+
github.copilot-sdk.session/log!
291298
github.copilot-sdk.session/events
292299
github.copilot-sdk.session/subscribe-events
293300
github.copilot-sdk.session/unsubscribe-events
@@ -337,6 +344,7 @@
337344
github.copilot-sdk.session/get-current-model
338345
github.copilot-sdk.session/switch-model!
339346
github.copilot-sdk.session/set-model!
347+
github.copilot-sdk.session/log!
340348
github.copilot-sdk.session/events
341349
github.copilot-sdk.session/subscribe-events
342350
github.copilot-sdk.session/unsubscribe-events

src/github/copilot_sdk/session.clj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,3 +740,19 @@
740740
See switch-model! for details."
741741
[session model-id]
742742
(switch-model! session model-id))
743+
744+
(defn log!
745+
"Log a message to the session timeline.
746+
Options (optional map):
747+
- :level - \"info\", \"warning\", or \"error\" (default: \"info\")
748+
- :ephemeral? - when true, message is not persisted to disk (default: false)
749+
Returns the event ID string."
750+
([session message] (log! session message nil))
751+
([session message opts]
752+
(let [{:keys [session-id client]} session
753+
conn (connection-io client)
754+
params (cond-> {:sessionId session-id :message message}
755+
(:level opts) (assoc :level (:level opts))
756+
(:ephemeral? opts) (assoc :ephemeral (:ephemeral? opts)))
757+
result (proto/send-request! conn "session.log" params)]
758+
(:event-id result))))

0 commit comments

Comments
 (0)