Skip to content

Commit a781790

Browse files
authored
Merge pull request #157 from copilot-community-sdk/copilot/sync-upstream-28-commits
[upstream-sync] Port 28 upstream commits from github/copilot-sdk (f0909a7→b9f746a)
2 parents ab08971 + af0e78c commit a781790

31 files changed

+814
-174
lines changed

.lastmerge

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
f0909a78ce6c242f1f07e91b72ab7a7d8c910531
1+
b9f746ab1b9c5f31af03a6f8a6cf4e680b3fd6b8

CHANGELOG.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88
99
## [Unreleased]
1010

11-
> **Upstream sync:** [`github/copilot-sdk@c263dfc`](https://github.com/github/copilot-sdk/commit/c263dfc69055f9f28ee2d4b121cf617fca5a42dc)
11+
> **Upstream sync:** [`github/copilot-sdk@b9f746a`](https://github.com/github/copilot-sdk/commit/b9f746ab1b9c5f31af03a6f8a6cf4e680b3fd6b8)
1212
1313
### Added
1414

15-
- `SessionConfig.setClientName(String)` / `getClientName()` — identifies the application using the SDK; included in the User-Agent header for API requests (upstream: [`397ef66`](https://github.com/github/copilot-sdk/commit/397ef66))
16-
- `ResumeSessionConfig.setClientName(String)` / `getClientName()` — same for resumed sessions
17-
- `PermissionHandler.APPROVE_ALL` — pre-built handler that approves all permission requests (upstream: [`3e2d2b2`](https://github.com/github/copilot-sdk/commit/3e2d2b2))
15+
- `CopilotSession.listAgents()` — lists custom agents available for selection (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb))
16+
- `CopilotSession.getCurrentAgent()` — gets the currently selected custom agent (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb))
17+
- `CopilotSession.selectAgent(String)` — selects a custom agent for the session (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb))
18+
- `CopilotSession.deselectAgent()` — deselects the current custom agent (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb))
19+
- `CopilotSession.compact()` — triggers immediate session context compaction (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb))
20+
- `AgentInfo` — new JSON type representing a custom agent with `name`, `displayName`, and `description` (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb))
21+
- New event types: `SessionTaskCompleteEvent` (`session.task_complete`), `AssistantStreamingDeltaEvent` (`assistant.streaming_delta`), `SubagentDeselectedEvent` (`subagent.deselected`) (upstream: various commits)
22+
- `AssistantTurnStartEvent` data now includes `interactionId` field
23+
- `AssistantMessageEvent` data now includes `interactionId` field
24+
- `ToolExecutionCompleteEvent` data now includes `model` and `interactionId` fields
25+
- `SkillInvokedEvent` data now includes `pluginName` and `pluginVersion` fields
26+
- `AssistantUsageEvent` data now includes `copilotUsage` field with `CopilotUsage` and `TokenDetails` nested types
27+
- E2E tests for custom tool permission approval and denial flows (upstream: [`388f2f3`](https://github.com/github/copilot-sdk/commit/388f2f3))
1828

1929
### Changed
2030

21-
- **Breaking:** permissions are now denied by default when no `OnPermissionRequest` handler is provided. The `requestPermission` flag is always sent as `true` so the server calls back for every permission request; the SDK returns a deny result when no handler is registered (upstream: [`3e2d2b2`](https://github.com/github/copilot-sdk/commit/3e2d2b2))
31+
- **Breaking:** `createSession(SessionConfig)` now requires a non-null `onPermissionRequest` handler; throws `IllegalArgumentException` if not provided (upstream: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4))
32+
- **Breaking:** `resumeSession(String, ResumeSessionConfig)` now requires a non-null `onPermissionRequest` handler; throws `IllegalArgumentException` if not provided (upstream: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4))
33+
- **Breaking:** The no-arg `createSession()` and `resumeSession(String)` overloads were removed (upstream: [`279f6c4`](https://github.com/github/copilot-sdk/commit/279f6c4))
34+
- `AssistantMessageDeltaEvent` data: `totalResponseSizeBytes` field moved to new `AssistantStreamingDeltaEvent` (upstream: various)
35+
36+
### Fixed
37+
38+
- Permission checks now also apply to SDK-registered custom tools, invoking the `onPermissionRequest` handler with `kind="custom-tool"` before executing tools (upstream: [`388f2f3`](https://github.com/github/copilot-sdk/commit/388f2f3))
2239

2340
## [1.0.9] - 2026-02-16
2441

src/main/java/com/github/copilot/sdk/CopilotClient.java

Lines changed: 50 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@
4747
* try (var client = new CopilotClient()) {
4848
* client.start().get();
4949
*
50-
* var session = client.createSession(new SessionConfig().setModel("gpt-5")).get();
50+
* var session = client
51+
* .createSession(
52+
* new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5"))
53+
* .get();
5154
*
5255
* session.on(AssistantMessageEvent.class, msg -> {
5356
* System.out.println(msg.getData().content());
@@ -273,14 +276,37 @@ private CompletableFuture<Void> cleanupConnection() {
273276
* <p>
274277
* The session maintains conversation state and can be used to send messages and
275278
* receive responses. Remember to close the session when done.
279+
* <p>
280+
* A permission handler is required when creating a session. Use
281+
* {@link com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL} to approve
282+
* all permission requests, or provide a custom handler to control permissions
283+
* selectively.
284+
*
285+
* <p>
286+
* Example:
287+
*
288+
* <pre>{@code
289+
* var session = client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
290+
* }</pre>
276291
*
277292
* @param config
278-
* configuration for the session (model, tools, etc.)
293+
* configuration for the session, including the required
294+
* {@link SessionConfig#setOnPermissionRequest(com.github.copilot.sdk.json.PermissionHandler)}
295+
* handler
279296
* @return a future that resolves with the created CopilotSession
280-
* @see #createSession()
297+
* @throws IllegalArgumentException
298+
* if {@code config} is {@code null} or does not have a permission
299+
* handler set
281300
* @see SessionConfig
301+
* @see com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL
282302
*/
283303
public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
304+
if (config == null || config.getOnPermissionRequest() == null) {
305+
return CompletableFuture.failedFuture(
306+
new IllegalArgumentException("An onPermissionRequest handler is required when creating a session. "
307+
+ "For example, to allow all permissions, use: "
308+
+ "new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)"));
309+
}
284310
return ensureConnected().thenCompose(connection -> {
285311
var request = SessionRequestBuilder.buildCreateRequest(config);
286312

@@ -293,32 +319,38 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
293319
});
294320
}
295321

296-
/**
297-
* Creates a new Copilot session with default configuration.
298-
*
299-
* @return a future that resolves with the created CopilotSession
300-
* @see #createSession(SessionConfig)
301-
*/
302-
public CompletableFuture<CopilotSession> createSession() {
303-
return createSession(null);
304-
}
305-
306322
/**
307323
* Resumes an existing Copilot session.
308324
* <p>
309325
* This restores a previously saved session, allowing you to continue a
310326
* conversation. The session's history is preserved.
327+
* <p>
328+
* A permission handler is required when resuming a session. Use
329+
* {@link com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL} to approve
330+
* all permission requests, or provide a custom handler to control permissions
331+
* selectively.
311332
*
312333
* @param sessionId
313334
* the ID of the session to resume
314335
* @param config
315-
* configuration for the resumed session
336+
* configuration for the resumed session, including the required
337+
* {@link ResumeSessionConfig#setOnPermissionRequest(com.github.copilot.sdk.json.PermissionHandler)}
338+
* handler
316339
* @return a future that resolves with the resumed CopilotSession
317-
* @see #resumeSession(String)
340+
* @throws IllegalArgumentException
341+
* if {@code config} is {@code null} or does not have a permission
342+
* handler set
318343
* @see #listSessions()
319344
* @see #getLastSessionId()
345+
* @see com.github.copilot.sdk.json.PermissionHandler#APPROVE_ALL
320346
*/
321347
public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeSessionConfig config) {
348+
if (config == null || config.getOnPermissionRequest() == null) {
349+
return CompletableFuture.failedFuture(
350+
new IllegalArgumentException("An onPermissionRequest handler is required when resuming a session. "
351+
+ "For example, to allow all permissions, use: "
352+
+ "new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)"));
353+
}
322354
return ensureConnected().thenCompose(connection -> {
323355
var request = SessionRequestBuilder.buildResumeRequest(sessionId, config);
324356

@@ -331,18 +363,6 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
331363
});
332364
}
333365

334-
/**
335-
* Resumes an existing session with default configuration.
336-
*
337-
* @param sessionId
338-
* the ID of the session to resume
339-
* @return a future that resolves with the resumed CopilotSession
340-
* @see #resumeSession(String, ResumeSessionConfig)
341-
*/
342-
public CompletableFuture<CopilotSession> resumeSession(String sessionId) {
343-
return resumeSession(sessionId, null);
344-
}
345-
346366
/**
347367
* Gets the current connection state.
348368
*
@@ -440,7 +460,7 @@ public CompletableFuture<List<ModelInfo>> listModels() {
440460
*
441461
* @return a future that resolves with the last session ID, or {@code null} if
442462
* no sessions exist
443-
* @see #resumeSession(String)
463+
* @see #resumeSession(String, com.github.copilot.sdk.json.ResumeSessionConfig)
444464
*/
445465
public CompletableFuture<String> getLastSessionId() {
446466
return ensureConnected().thenCompose(
@@ -478,7 +498,7 @@ public CompletableFuture<Void> deleteSession(String sessionId) {
478498
*
479499
* @return a future that resolves with a list of session metadata
480500
* @see SessionMetadata
481-
* @see #resumeSession(String)
501+
* @see #resumeSession(String, com.github.copilot.sdk.json.ResumeSessionConfig)
482502
*/
483503
public CompletableFuture<List<SessionMetadata>> listSessions() {
484504
return listSessions(null);
@@ -508,7 +528,7 @@ public CompletableFuture<List<SessionMetadata>> listSessions() {
508528
* @return a future that resolves with a list of session metadata
509529
* @see SessionMetadata
510530
* @see SessionListFilter
511-
* @see #resumeSession(String)
531+
* @see #resumeSession(String, com.github.copilot.sdk.json.ResumeSessionConfig)
512532
*/
513533
public CompletableFuture<List<SessionMetadata>> listSessions(SessionListFilter filter) {
514534
return ensureConnected().thenCompose(connection -> {

src/main/java/com/github/copilot/sdk/CopilotSession.java

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.io.Closeable;
88
import java.io.IOException;
99
import java.util.ArrayList;
10+
import java.util.Collections;
1011
import java.util.List;
1112
import java.util.Map;
1213
import java.util.Set;
@@ -20,13 +21,16 @@
2021
import java.util.logging.Level;
2122
import java.util.logging.Logger;
2223

24+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
25+
import com.fasterxml.jackson.annotation.JsonProperty;
2326
import com.fasterxml.jackson.databind.JsonNode;
2427
import com.fasterxml.jackson.databind.ObjectMapper;
2528
import com.github.copilot.sdk.events.AbstractSessionEvent;
2629
import com.github.copilot.sdk.events.AssistantMessageEvent;
2730
import com.github.copilot.sdk.events.SessionErrorEvent;
2831
import com.github.copilot.sdk.events.SessionEventParser;
2932
import com.github.copilot.sdk.events.SessionIdleEvent;
33+
import com.github.copilot.sdk.json.AgentInfo;
3034
import com.github.copilot.sdk.json.GetMessagesResponse;
3135
import com.github.copilot.sdk.json.HookInvocation;
3236
import com.github.copilot.sdk.json.MessageOptions;
@@ -58,8 +62,10 @@
5862
* <h2>Example Usage</h2>
5963
*
6064
* <pre>{@code
61-
* // Create a session
62-
* var session = client.createSession(new SessionConfig().setModel("gpt-5")).get();
65+
* // Create a session with a permission handler (required)
66+
* var session = client
67+
* .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-5"))
68+
* .get();
6369
*
6470
* // Register type-safe event handlers
6571
* session.on(AssistantMessageEvent.class, msg -> {
@@ -802,6 +808,84 @@ public CompletableFuture<Void> abort() {
802808
return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class);
803809
}
804810

811+
/**
812+
* Lists the custom agents available for selection in this session.
813+
*
814+
* @return a future that resolves with the list of available agents
815+
* @throws IllegalStateException
816+
* if this session has been terminated
817+
* @since 1.0.11
818+
*/
819+
public CompletableFuture<List<AgentInfo>> listAgents() {
820+
ensureNotTerminated();
821+
return rpc.invoke("session.agent.list", Map.of("sessionId", sessionId), AgentListResponse.class)
822+
.thenApply(response -> response.agents() != null
823+
? Collections.unmodifiableList(response.agents())
824+
: Collections.emptyList());
825+
}
826+
827+
/**
828+
* Gets the currently selected custom agent for this session, or {@code null} if
829+
* no custom agent is selected.
830+
*
831+
* @return a future that resolves with the current agent, or {@code null} if
832+
* using the default agent
833+
* @throws IllegalStateException
834+
* if this session has been terminated
835+
* @since 1.0.11
836+
*/
837+
public CompletableFuture<AgentInfo> getCurrentAgent() {
838+
ensureNotTerminated();
839+
return rpc.invoke("session.agent.getCurrent", Map.of("sessionId", sessionId), AgentGetCurrentResponse.class)
840+
.thenApply(AgentGetCurrentResponse::agent);
841+
}
842+
843+
/**
844+
* Selects a custom agent for this session.
845+
*
846+
* @param agentName
847+
* the name/identifier of the agent to select
848+
* @return a future that resolves with the selected agent information
849+
* @throws IllegalStateException
850+
* if this session has been terminated
851+
* @since 1.0.11
852+
*/
853+
public CompletableFuture<AgentInfo> selectAgent(String agentName) {
854+
ensureNotTerminated();
855+
return rpc.invoke("session.agent.select", Map.of("sessionId", sessionId, "name", agentName),
856+
AgentSelectResponse.class).thenApply(AgentSelectResponse::agent);
857+
}
858+
859+
/**
860+
* Deselects the currently selected custom agent, returning to the default
861+
* agent.
862+
*
863+
* @return a future that completes when the agent is deselected
864+
* @throws IllegalStateException
865+
* if this session has been terminated
866+
* @since 1.0.11
867+
*/
868+
public CompletableFuture<Void> deselectAgent() {
869+
ensureNotTerminated();
870+
return rpc.invoke("session.agent.deselect", Map.of("sessionId", sessionId), Void.class);
871+
}
872+
873+
/**
874+
* Compacts the session context to reduce token usage.
875+
* <p>
876+
* This triggers an immediate session compaction, summarizing the conversation
877+
* history to free up context window space.
878+
*
879+
* @return a future that completes when compaction finishes
880+
* @throws IllegalStateException
881+
* if this session has been terminated
882+
* @since 1.0.11
883+
*/
884+
public CompletableFuture<Void> compact() {
885+
ensureNotTerminated();
886+
return rpc.invoke("session.compaction.compact", Map.of("sessionId", sessionId), Void.class);
887+
}
888+
805889
/**
806890
* Verifies that this session has not yet been terminated.
807891
*
@@ -843,4 +927,18 @@ public void close() {
843927
hooksHandler.set(null);
844928
}
845929

930+
// ===== Internal response types for agent API =====
931+
932+
@JsonIgnoreProperties(ignoreUnknown = true)
933+
private record AgentListResponse(@JsonProperty("agents") List<AgentInfo> agents) {
934+
}
935+
936+
@JsonIgnoreProperties(ignoreUnknown = true)
937+
private record AgentGetCurrentResponse(@JsonProperty("agent") AgentInfo agent) {
938+
}
939+
940+
@JsonIgnoreProperties(ignoreUnknown = true)
941+
private record AgentSelectResponse(@JsonProperty("agent") AgentInfo agent) {
942+
}
943+
846944
}

src/main/java/com/github/copilot/sdk/events/AbstractSessionEvent.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ public abstract sealed class AbstractSessionEvent permits
5555
SessionModelChangeEvent, SessionModeChangedEvent, SessionPlanChangedEvent, SessionWorkspaceFileChangedEvent,
5656
SessionHandoffEvent, SessionTruncationEvent, SessionSnapshotRewindEvent, SessionUsageInfoEvent,
5757
SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionShutdownEvent, SessionContextChangedEvent,
58+
SessionTaskCompleteEvent,
5859
// Assistant events
5960
AssistantTurnStartEvent, AssistantIntentEvent, AssistantReasoningEvent, AssistantReasoningDeltaEvent,
60-
AssistantMessageEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, AssistantUsageEvent, AbortEvent,
61+
AssistantMessageEvent, AssistantMessageDeltaEvent, AssistantStreamingDeltaEvent, AssistantTurnEndEvent,
62+
AssistantUsageEvent, AbortEvent,
6163
// Tool events
6264
ToolUserRequestedEvent, ToolExecutionStartEvent, ToolExecutionPartialResultEvent, ToolExecutionProgressEvent,
6365
ToolExecutionCompleteEvent,
@@ -66,8 +68,8 @@ public abstract sealed class AbstractSessionEvent permits
6668
// Skill events
6769
SkillInvokedEvent,
6870
// Other events
69-
SubagentStartedEvent, SubagentCompletedEvent, SubagentFailedEvent, SubagentSelectedEvent, HookStartEvent,
70-
HookEndEvent, SystemMessageEvent {
71+
SubagentStartedEvent, SubagentCompletedEvent, SubagentFailedEvent, SubagentSelectedEvent,
72+
SubagentDeselectedEvent, HookStartEvent, HookEndEvent, SystemMessageEvent {
7173

7274
@JsonProperty("id")
7375
private UUID id;

src/main/java/com/github/copilot/sdk/events/AssistantMessageDeltaEvent.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ public void setData(AssistantMessageDeltaData data) {
3434
@JsonIgnoreProperties(ignoreUnknown = true)
3535
public record AssistantMessageDeltaData(@JsonProperty("messageId") String messageId,
3636
@JsonProperty("deltaContent") String deltaContent,
37-
@JsonProperty("totalResponseSizeBytes") Double totalResponseSizeBytes,
3837
@JsonProperty("parentToolCallId") String parentToolCallId) {
3938
}
4039
}

src/main/java/com/github/copilot/sdk/events/AssistantMessageEvent.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public void setData(AssistantMessageData data) {
7373
public record AssistantMessageData(@JsonProperty("messageId") String messageId,
7474
@JsonProperty("content") String content, @JsonProperty("toolRequests") List<ToolRequest> toolRequests,
7575
@JsonProperty("parentToolCallId") String parentToolCallId,
76+
@JsonProperty("interactionId") String interactionId,
7677
@JsonProperty("reasoningOpaque") String reasoningOpaque,
7778
@JsonProperty("reasoningText") String reasoningText,
7879
@JsonProperty("encryptedContent") String encryptedContent) {

0 commit comments

Comments
 (0)