Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .lastmerge
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4e1499dd23709022c720eaaa5457d00bf0cb3977
9a0a1a5f21111f4ad02b5ce911750ecc75e054c3
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

> **Upstream sync:** [`github/copilot-sdk@dcd86c1`](https://github.com/github/copilot-sdk/commit/dcd86c189501ce1b46b787ca60d90f3f315f3079)
> **Upstream sync:** [`github/copilot-sdk@9a0a1a5`](https://github.com/github/copilot-sdk/commit/9a0a1a5f21111f4ad02b5ce911750ecc75e054c3)

### Added

- `CopilotClientOptions.setOnListModels(Supplier<CompletableFuture<List<ModelInfo>>>)` — custom handler for `listModels()` used in BYOK mode to return models from a custom provider instead of querying the CLI (upstream: [`e478657`](https://github.com/github/copilot-sdk/commit/e478657))
- `SessionConfig.setAgent(String)` — pre-selects a custom agent by name when creating a session (upstream: [`7766b1a`](https://github.com/github/copilot-sdk/commit/7766b1a))
- `ResumeSessionConfig.setAgent(String)` — pre-selects a custom agent by name when resuming a session (upstream: [`7766b1a`](https://github.com/github/copilot-sdk/commit/7766b1a))
- `SessionConfig.setOnEvent(Consumer<AbstractSessionEvent>)` — registers an event handler before the `session.create` RPC is issued, ensuring no early events are missed (upstream: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7))
- `ResumeSessionConfig.setOnEvent(Consumer<AbstractSessionEvent>)` — registers an event handler before the `session.resume` RPC is issued (upstream: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7))
- New broadcast session event types (protocol v3): `ExternalToolRequestedEvent` (`external_tool.requested`), `ExternalToolCompletedEvent` (`external_tool.completed`), `PermissionRequestedEvent` (`permission.requested`), `PermissionCompletedEvent` (`permission.completed`), `CommandQueuedEvent` (`command.queued`), `CommandCompletedEvent` (`command.completed`), `ExitPlanModeRequestedEvent` (`exit_plan_mode.requested`), `ExitPlanModeCompletedEvent` (`exit_plan_mode.completed`), `SystemNotificationEvent` (`system.notification`) (upstream: [`1653812`](https://github.com/github/copilot-sdk/commit/1653812), [`396e8b3`](https://github.com/github/copilot-sdk/commit/396e8b3))
- `CopilotSession.log(String)` and `CopilotSession.log(String, String, Boolean)` — log a message to the session timeline (upstream: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7))

### Changed

- **Protocol version bumped to v3.** The SDK now supports CLI servers running v2 or v3 (backward-compatible range). Sessions are now registered in the client's session map *before* the `session.create`/`session.resume` RPC is issued, ensuring broadcast events emitted immediately on session start are never dropped (upstream: [`4125fe7`](https://github.com/github/copilot-sdk/commit/4125fe7), [`1653812`](https://github.com/github/copilot-sdk/commit/1653812))
- In protocol v3, tool calls and permission requests that have a registered handler are now handled automatically via `ExternalToolRequestedEvent` and `PermissionRequestedEvent` broadcast events; results are sent back via `session.tools.handlePendingToolCall` and `session.permissions.handlePendingPermissionRequest` RPC calls (upstream: [`1653812`](https://github.com/github/copilot-sdk/commit/1653812))

## [1.0.10] - 2026-03-03

Expand Down
73 changes: 64 additions & 9 deletions src/main/java/com/github/copilot/sdk/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ private CompletableFuture<Connection> startCore() {
});
}

private static final int MIN_PROTOCOL_VERSION = 2;

private void verifyProtocolVersion(Connection connection) throws Exception {
int expectedVersion = SdkProtocolVersion.get();
var params = new HashMap<String, Object>();
Expand All @@ -200,9 +202,10 @@ private void verifyProtocolVersion(Connection connection) throws Exception {
+ "Please update your server to ensure compatibility.");
}

if (pingResponse.protocolVersion() != expectedVersion) {
int serverVersion = pingResponse.protocolVersion();
if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > expectedVersion) {
throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion
+ ", but server reports version " + pingResponse.protocolVersion() + ". "
+ " (minimum " + MIN_PROTOCOL_VERSION + "), but server reports version " + serverVersion + ". "
+ "Please update your SDK or server to ensure compatibility.");
}
}
Expand Down Expand Up @@ -319,13 +322,32 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
+ "new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)"));
}
return ensureConnected().thenCompose(connection -> {
var request = SessionRequestBuilder.buildCreateRequest(config);
// Pre-generate session ID so the session can be registered before the RPC call,
// ensuring no events emitted by the CLI during creation are lost.
String sessionId = config.getSessionId() != null
? config.getSessionId()
: java.util.UUID.randomUUID().toString();

var session = new CopilotSession(sessionId, connection.rpc);
SessionRequestBuilder.configureSession(session, config);
sessions.put(sessionId, session);

var request = SessionRequestBuilder.buildCreateRequest(config, sessionId);

return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> {
var session = new CopilotSession(response.sessionId(), connection.rpc, response.workspacePath());
SessionRequestBuilder.configureSession(session, config);
sessions.put(response.sessionId(), session);
session.setWorkspacePath(response.workspacePath());
// If the server returned a different sessionId (e.g. a v2 CLI that ignores
// the client-supplied ID), re-key the sessions map.
String returnedId = response.sessionId();
if (returnedId != null && !returnedId.equals(sessionId)) {
sessions.remove(sessionId);
session.setActiveSessionId(returnedId);
sessions.put(returnedId, session);
}
return session;
}).exceptionally(ex -> {
sessions.remove(sessionId);
throw ex instanceof RuntimeException re ? re : new RuntimeException(ex);
});
});
}
Expand Down Expand Up @@ -363,13 +385,26 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
+ "new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)"));
}
return ensureConnected().thenCompose(connection -> {
// Register the session before the RPC call to avoid missing early events.
var session = new CopilotSession(sessionId, connection.rpc);
SessionRequestBuilder.configureSession(session, config);
sessions.put(sessionId, session);

var request = SessionRequestBuilder.buildResumeRequest(sessionId, config);

return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> {
var session = new CopilotSession(response.sessionId(), connection.rpc, response.workspacePath());
SessionRequestBuilder.configureSession(session, config);
sessions.put(response.sessionId(), session);
session.setWorkspacePath(response.workspacePath());
// If the server returned a different sessionId than what was requested, re-key.
String returnedId = response.sessionId();
if (returnedId != null && !returnedId.equals(sessionId)) {
sessions.remove(sessionId);
session.setActiveSessionId(returnedId);
sessions.put(returnedId, session);
}
return session;
}).exceptionally(ex -> {
sessions.remove(sessionId);
throw ex instanceof RuntimeException re ? re : new RuntimeException(ex);
});
});
}
Expand Down Expand Up @@ -434,6 +469,10 @@ public CompletableFuture<GetAuthStatusResponse> getAuthStatus() {
* <p>
* Results are cached after the first successful call to avoid rate limiting.
* The cache is cleared when the client disconnects.
* <p>
* If an {@code onListModels} handler was provided in
* {@link com.github.copilot.sdk.json.CopilotClientOptions}, it is called
* instead of querying the CLI server. This is useful in BYOK mode.
*
* @return a future that resolves with a list of available models
* @see ModelInfo
Expand All @@ -445,6 +484,22 @@ public CompletableFuture<List<ModelInfo>> listModels() {
return CompletableFuture.completedFuture(new ArrayList<>(cached));
}

// If a custom handler is configured, use it instead of querying the CLI server
var onListModels = options.getOnListModels();
if (onListModels != null) {
synchronized (modelsCacheLock) {
if (modelsCache != null) {
return CompletableFuture.completedFuture(new ArrayList<>(modelsCache));
}
}
return onListModels.get().thenApply(models -> {
synchronized (modelsCacheLock) {
modelsCache = models;
}
return new ArrayList<>(models);
});
}

return ensureConnected().thenCompose(connection -> {
// Double-check cache inside lock
synchronized (modelsCacheLock) {
Expand Down
Loading
Loading