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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).

## [Unreleased]
### Added (upstream sync)
- Session pre-registration: sessions are now created and registered in client state **before** the RPC call, preventing early events (e.g. `session.start`) from being dropped. Session IDs are generated client-side via `java.util.UUID/randomUUID` when not explicitly provided. On RPC failure, sessions are automatically cleaned up (upstream PR #664).
- `:on-event` optional handler in `create-session` and `resume-session` configs — a 1-arity function receiving event maps, registered before the RPC call so no events are missed. Equivalent to calling `subscribe-events` immediately after creation, but executes earlier in the lifecycle (upstream PR #664).
- `join-session` function — convenience for extensions running as child processes of the Copilot CLI. Reads `SESSION_ID` from environment, creates a child-process client, and resumes the session with `:disable-resume? true`. Returns `{:client ... :session ...}` (upstream PR #737).
- `:copilot/system.notification` event type — structured notification events with `:kind` discriminator (`agent_completed`, `shell_completed`, `shell_detached_completed`) (upstream PR #737).

### Changed
- `CopilotSession` record no longer includes `workspace-path` as a field. Use `(workspace-path session)` accessor which reads from mutable session state. This enables the pre-registration flow where workspace-path is set after the RPC response.

## [0.1.32.0] - 2026-03-10
### Added (v0.1.32 sync)
Expand Down
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Key features:
- **Multi-session support** — Run multiple independent conversations concurrently
- **Session hooks** — Lifecycle callbacks for pre/post tool use, prompts, errors
- **User input handling** — Handle `ask_user` requests from the agent
- **Authentication options** — GitHub token auth or logged-in user
- **Event callbacks** — Register `:on-event` handlers to receive all session events
- **Child process mode** — Join existing sessions via `join-session` for extensions
- **Authentication options** — GitHub token auth or logged-in user

See [`examples/`](./examples/) for working code demonstrating common patterns.
Expand Down Expand Up @@ -133,19 +134,32 @@ See the [`examples/`](./examples/) directory for complete working examples:
| Example | Difficulty | Description |
|---------|------------|-------------|
| [`basic_chat.clj`](./examples/basic_chat.clj) | Beginner | Simple Q&A conversation with multi-turn context |
| [`helpers_query.clj`](./examples/helpers_query.clj) | Beginner | Stateless query API with blocking and streaming modes |
| [`reasoning_effort.clj`](./examples/reasoning_effort.clj) | Beginner | Control reasoning effort level |
| [`tool_integration.clj`](./examples/tool_integration.clj) | Intermediate | Custom tools that the LLM can invoke |
| [`multi_agent.clj`](./examples/multi_agent.clj) | Advanced | Multi-agent orchestration with core.async |
| [`config_skill_output.clj`](./examples/config_skill_output.clj) | Intermediate | Config dir, skills, and large output settings |
| [`permission_bash.clj`](./examples/permission_bash.clj) | Intermediate | Permission handling with bash |
| [`permission_bash.clj`](./examples/permission_bash.clj) | Intermediate | Permission handling with bash tool |
| [`session_events.clj`](./examples/session_events.clj) | Intermediate | Monitor session state events and their flow |
| [`session_resume.clj`](./examples/session_resume.clj) | Intermediate | Save and resume sessions by ID |
| [`file_attachments.clj`](./examples/file_attachments.clj) | Intermediate | Send file attachments for analysis |
| [`infinite_sessions.clj`](./examples/infinite_sessions.clj) | Intermediate | Infinite sessions with context compaction |
| [`lifecycle_hooks.clj`](./examples/lifecycle_hooks.clj) | Intermediate | Lifecycle hooks for tool use, prompts, errors |
| [`user_input.clj`](./examples/user_input.clj) | Intermediate | Handle ask_user requests from the agent |
| [`metadata_api.clj`](./examples/metadata_api.clj) | Intermediate | List sessions, tools, and quota |
| [`multi_agent.clj`](./examples/multi_agent.clj) | Advanced | Multi-agent orchestration with core.async |
| [`ask_user_failure.clj`](./examples/ask_user_failure.clj) | Advanced | User cancellation (Esc) with event tracing |
| [`mcp_local_server.clj`](./examples/mcp_local_server.clj) | Advanced | Model Context Protocol server integration |
| [`byok_provider.clj`](./examples/byok_provider.clj) | Advanced | Bring Your Own Key provider configuration |

Run examples:

```bash
clojure -A:examples -M -m basic-chat
clojure -A:examples -M -m helpers-query
clojure -A:examples -M -m tool-integration
clojure -A:examples -M -m session-events
clojure -A:examples -M -m multi-agent
clojure -A:examples -M -m config-skill-output
clojure -A:examples -M -m permission-bash
clojure -A:examples -M -m byok-provider
```

See [`examples/README.md`](./examples/README.md) for detailed walkthroughs and explanations.
Expand Down
1 change: 1 addition & 0 deletions doc/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ Pass `:client-name` to identify your application in API requests (included in th
| **Session** | A conversation with context, model, and tools |
| **Tools** | Functions that Copilot can call in your code |
| **Events** | Streaming updates via core.async channels |
| **On-Event** | Optional callback receiving all session events, registered before RPC |
| **Helpers** | High-level stateless API with automatic lifecycle management |

### Comparison with JavaScript SDK
Expand Down
2 changes: 1 addition & 1 deletion doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Clojure SDK for programmatic control of the GitHub Copilot CLI via JSON-RPC.
## Getting Started

- [Getting Started](getting-started.md) — Step-by-step tutorial building a weather assistant
- [Examples](../examples/README.md) — 11 working examples with walkthroughs
- [Examples](../examples/README.md) — 17 working examples with walkthroughs

## Guides

Expand Down
40 changes: 40 additions & 0 deletions doc/reference/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ or want to stop reading early without leaking session resources.

Explicitly shutdown the shared client. Safe to call multiple times.

### `client-info`

```clojure
(h/client-info)
;; => {:client-opts {:log-level :info, ...} :connected? true}
```

Get information about the current shared client state. Returns `nil` if no shared client exists, otherwise a map with `:client-opts` and `:connected?` keys.

---

## CopilotClient
Expand Down Expand Up @@ -243,6 +252,7 @@ Create a client and session together, ensuring both are cleaned up on exit.
| `: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. |
| `: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. |

#### `resume-session`

Expand Down Expand Up @@ -304,6 +314,26 @@ Same config options as `resume-session`. Safe for use inside `go` blocks. On RPC
))
```

#### `join-session`

```clojure
(copilot/join-session config)
```

Join the current foreground session from an extension running as a child process of the Copilot CLI. Reads the `SESSION_ID` environment variable, creates a child-process client, and resumes the session with `:disable-resume?` defaulting to `true`.

Returns a map with `:client` and `:session` keys. The caller is responsible for stopping the client when done.

Throws if `SESSION_ID` is not set in the environment.

```clojure
(let [{:keys [client session]} (copilot/join-session
{:on-permission-request copilot/approve-all
:tools [my-tool]})]
;; use session...
(copilot/stop! client))
```

#### `ping`

```clojure
Expand Down Expand Up @@ -884,6 +914,15 @@ copilot/interaction-events
;; :copilot/external_tool.requested}
```

### `evt` — Event Keyword Helper

```clojure
(copilot/evt :session.info) ;; => :copilot/session.info
(copilot/evt :assistant.message) ;; => :copilot/assistant.message
```

Convert an unqualified event keyword to a namespace-qualified `:copilot/` keyword. Throws `IllegalArgumentException` if the keyword is not a valid event type.

### Event Reference

| Event Type | Description |
Expand Down Expand Up @@ -934,6 +973,7 @@ copilot/interaction-events
| `:copilot/hook.start` | Hook invocation started |
| `:copilot/hook.end` | Hook invocation finished |
| `:copilot/system.message` | System message emitted |
| `:copilot/system.notification` | System notification with structured `:kind` discriminator (e.g. `agent_completed`, `shell_completed`, `shell_detached_completed`) |
| `:copilot/permission.requested` | Permission request initiated |
| `:copilot/permission.completed` | Permission request resolved |
| `:copilot/user_input.requested` | User input requested from agent |
Expand Down
56 changes: 56 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ clojure -A:examples -X session-events/run
clojure -A:examples -X user-input/run
clojure -A:examples -X user-input/run-simple

# Ask user cancellation (simulates Esc)
clojure -A:examples -X ask-user-failure/run

# BYOK provider (requires API key, see example docs)
OPENAI_API_KEY=sk-... clojure -A:examples -X byok-provider/run
clojure -A:examples -X byok-provider/run :provider-name '"ollama"'
Expand Down Expand Up @@ -756,6 +759,59 @@ clojure -A:examples -X reasoning-effort/run :effort '"high"'

---

## Example 17: Ask User Failure (`ask_user_failure.clj`)

**Difficulty:** Intermediate
**Concepts:** User input cancellation, ask_user tool, error handling, event tracing

Demonstrates what happens when a user cancels an `ask_user` request (simulating pressing Esc). This is a 1:1 port of the upstream `basic-example.ts`.

### What It Demonstrates

- Handling user cancellation by throwing from `:on-user-input-request`
- Event tracing: subscribing to all events via `tap` on the session events mult
- Graceful degradation when the user skips a question
- Full event stream logging for debugging

### Usage

```bash
clojure -A:examples -X ask-user-failure/run
```

### Code Walkthrough

```clojure
(require '[clojure.core.async :refer [chan tap go-loop <!]])
(require '[github.copilot-sdk :as copilot])

;; Track cancelled requests
(let [cancelled-requests (atom [])]
(copilot/with-client [client]
(copilot/with-session [session client
{:on-permission-request copilot/approve-all
:model "claude-haiku-4.5"
:on-user-input-request
(fn [request _invocation]
(swap! cancelled-requests conj request)
;; Throwing simulates Esc — the SDK sends a failure
;; result back to the ask_user tool automatically.
(throw (RuntimeException. "User skipped question")))}]
;; Subscribe to all events for tracing
(let [events-ch (chan 256)]
(tap (copilot/events session) events-ch)
(go-loop []
(when-let [event (<! events-ch)]
(println event)
(recur)))

(let [result (copilot/send-and-wait! session
{:prompt "Use the ask_user tool to ask me to pick between 'Red' and 'Blue'."})]
(println "Response:" (get-in result [:data :content])))))))
```

---

## Clojure vs JavaScript Comparison

Here's how common patterns compare between the Clojure and JavaScript SDKs:
Expand Down
28 changes: 28 additions & 0 deletions src/github/copilot_sdk.clj
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
:copilot/hook.start
:copilot/hook.end
:copilot/system.message
:copilot/system.notification
:copilot/permission.requested
:copilot/permission.completed
:copilot/user_input.requested
Expand Down Expand Up @@ -555,6 +556,33 @@
[client session-id config]
(client/<resume-session client session-id config))

(defn join-session
"Join the current foreground session from an extension running as a child process.

Reads the SESSION_ID environment variable and connects to the parent CLI process
via stdio. Intended for extensions spawned by the Copilot CLI.

Config is the same as `resume-session` (`:on-permission-request` is **required**).
The `:disable-resume?` option defaults to true.

Returns a map with `:client` and `:session` keys. The caller is responsible for
stopping the client when done.

Throws if SESSION_ID is not set in the environment.

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

(let [{:keys [client session]} (copilot/join-session
{:on-permission-request copilot/approve-all
:tools [my-tool]})]
;; use session...
(copilot/stop! client))
```"
[config]
(client/join-session config))

(defn list-sessions
"List all available sessions.
Returns a vector of session metadata maps:
Expand Down
Loading
Loading