Skip to content

Commit 33c081f

Browse files
authored
Merge pull request #20 from github/copilot/sync-upstream-39-new-commits
Upstream sync: Port 39 new commits from copilot-sdk (2026-03-24)
2 parents cbf4362 + 698da99 commit 33c081f

34 files changed

+1735
-38
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ blog-copilotsdk/
66
.claude/worktrees
77
smoke-test
88
*job-logs.txt
9+
temporary-prompts/
10+
changebundle.txt*

.lastmerge

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
062b61c8aa63b9b5d45fa1d7b01723e6660ffa83
1+
40887393a9e687dacc141a645799441b0313ff15

CHANGELOG.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,36 @@ 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@062b61c`](https://github.com/github/copilot-sdk/commit/062b61c8aa63b9b5d45fa1d7b01723e6660ffa83)
11+
> **Upstream sync:** [`github/copilot-sdk@4088739`](https://github.com/github/copilot-sdk/commit/40887393a9e687dacc141a645799441b0313ff15)
12+
13+
### Added
14+
15+
- `UnknownSessionEvent` — forward-compatible placeholder for event types not yet known to the SDK; unknown events are now dispatched to handlers instead of being silently dropped (upstream: [`d82fd62`](https://github.com/github/copilot-sdk/commit/d82fd62))
16+
- `PermissionRequestResultKind.NO_RESULT` — new constant that signals the handler intentionally abstains from answering a permission request, leaving it unanswered for another client (upstream: [`df59a0e`](https://github.com/github/copilot-sdk/commit/df59a0e))
17+
- `ToolDefinition.skipPermission` field and `ToolDefinition.createSkipPermission()` factory — marks a tool to skip the permission prompt (upstream: [`10c4d02`](https://github.com/github/copilot-sdk/commit/10c4d02))
18+
- `SystemMessageMode.CUSTOMIZE` — new enum value for fine-grained system prompt customization (upstream: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780))
19+
- `SectionOverrideAction` enum — specifies the operation on a system prompt section (replace, remove, append, prepend, transform) (upstream: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780))
20+
- `SectionOverride` class — describes how one section of the system prompt should be modified, with optional transform callback (upstream: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780))
21+
- `SystemPromptSections` constants — well-known section identifier strings for use with CUSTOMIZE mode (upstream: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780))
22+
- `SystemMessageConfig.setSections(Map<String,SectionOverride>)` — section-level overrides for CUSTOMIZE mode (upstream: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780))
23+
- `systemMessage.transform` RPC handler — the SDK now registers a handler that invokes transform callbacks registered in the session config (upstream: [`005b780`](https://github.com/github/copilot-sdk/commit/005b780))
24+
- `CopilotSession.setModel(String, String)` — new overload that accepts an optional reasoning effort level (upstream: [`ea90f07`](https://github.com/github/copilot-sdk/commit/ea90f07))
25+
- `CopilotSession.log(String, String, Boolean, String)` — new overload with an optional `url` parameter (minor addition)
26+
- `BlobAttachment` class — inline base64-encoded binary attachment for messages (e.g., images) (upstream: [`698b259`](https://github.com/github/copilot-sdk/commit/698b259))
27+
- `MessageAttachment` sealed interface — type-safe base for all attachment types (`Attachment`, `BlobAttachment`), with Jackson polymorphic serialization support
28+
- `TelemetryConfig` class — OpenTelemetry configuration for the CLI server; set on `CopilotClientOptions.setTelemetry()` (upstream: [`f2d21a0`](https://github.com/github/copilot-sdk/commit/f2d21a0))
29+
- `CopilotClientOptions.setTelemetry(TelemetryConfig)` — enables OpenTelemetry instrumentation in the CLI server (upstream: [`f2d21a0`](https://github.com/github/copilot-sdk/commit/f2d21a0))
30+
31+
### Changed
32+
33+
- `Attachment` record now implements `MessageAttachment` sealed interface
34+
- `BlobAttachment` class now implements `MessageAttachment` sealed interface and is `final`
35+
- `MessageOptions.setAttachments(List<? extends MessageAttachment>)` — parameter type changed from `List<Attachment>` to `List<? extends MessageAttachment>` to support both `Attachment` and `BlobAttachment` in the same list with full compile-time safety
36+
- `SendMessageRequest.setAttachments(List<MessageAttachment>)` — matching change for the internal request type
37+
38+
### Deprecated
39+
40+
- `CopilotClientOptions.setAutoRestart(boolean)` — this option has no effect and will be removed in a future release
1241

1342
## [0.1.32-java.0] - 2026-03-17
1443

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,28 @@ ProcessInfo startCliServer() throws IOException, InterruptedException {
110110
pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken());
111111
}
112112

113+
// Set telemetry environment variables if configured
114+
if (options.getTelemetry() != null) {
115+
var telemetry = options.getTelemetry();
116+
pb.environment().put("COPILOT_OTEL_ENABLED", "true");
117+
if (telemetry.getOtlpEndpoint() != null) {
118+
pb.environment().put("OTEL_EXPORTER_OTLP_ENDPOINT", telemetry.getOtlpEndpoint());
119+
}
120+
if (telemetry.getFilePath() != null) {
121+
pb.environment().put("COPILOT_OTEL_FILE_EXPORTER_PATH", telemetry.getFilePath());
122+
}
123+
if (telemetry.getExporterType() != null) {
124+
pb.environment().put("COPILOT_OTEL_EXPORTER_TYPE", telemetry.getExporterType());
125+
}
126+
if (telemetry.getSourceName() != null) {
127+
pb.environment().put("COPILOT_OTEL_SOURCE_NAME", telemetry.getSourceName());
128+
}
129+
if (telemetry.getCaptureContent() != null) {
130+
pb.environment().put("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT",
131+
telemetry.getCaptureContent() ? "true" : "false");
132+
}
133+
}
134+
113135
Process process = pb.start();
114136

115137
// Forward stderr to logger in background

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,19 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
332332
SessionRequestBuilder.configureSession(session, config);
333333
sessions.put(sessionId, session);
334334

335+
// Extract transform callbacks from the system message config.
336+
// Callbacks are registered with the session; a wire-safe copy of the
337+
// system message (with transform sections replaced by action="transform")
338+
// is used in the RPC request.
339+
var extracted = SessionRequestBuilder.extractTransformCallbacks(config.getSystemMessage());
340+
if (extracted.transformCallbacks() != null) {
341+
session.registerTransformCallbacks(extracted.transformCallbacks());
342+
}
343+
335344
var request = SessionRequestBuilder.buildCreateRequest(config, sessionId);
345+
if (extracted.wireSystemMessage() != config.getSystemMessage()) {
346+
request.setSystemMessage(extracted.wireSystemMessage());
347+
}
336348

337349
return connection.rpc.invoke("session.create", request, CreateSessionResponse.class).thenApply(response -> {
338350
session.setWorkspacePath(response.workspacePath());
@@ -390,7 +402,16 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
390402
SessionRequestBuilder.configureSession(session, config);
391403
sessions.put(sessionId, session);
392404

405+
// Extract transform callbacks from the system message config.
406+
var extracted = SessionRequestBuilder.extractTransformCallbacks(config.getSystemMessage());
407+
if (extracted.transformCallbacks() != null) {
408+
session.registerTransformCallbacks(extracted.transformCallbacks());
409+
}
410+
393411
var request = SessionRequestBuilder.buildResumeRequest(sessionId, config);
412+
if (extracted.wireSystemMessage() != config.getSystemMessage()) {
413+
request.setSystemMessage(extracted.wireSystemMessage());
414+
}
394415

395416
return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class).thenApply(response -> {
396417
session.setWorkspacePath(response.workspacePath());

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

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ public final class CopilotSession implements AutoCloseable {
120120
private final AtomicReference<SessionHooks> hooksHandler = new AtomicReference<>();
121121
private volatile EventErrorHandler eventErrorHandler;
122122
private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS;
123+
private volatile Map<String, java.util.function.Function<String, CompletableFuture<String>>> transformCallbacks;
123124

124125
/** Tracks whether this session instance has been terminated via close(). */
125126
private volatile boolean isTerminated = false;
@@ -709,6 +710,12 @@ private void executePermissionAndRespondAsync(String requestId, PermissionReques
709710
invocation.setSessionId(sessionId);
710711
handler.handle(permissionRequest, invocation).thenAccept(result -> {
711712
try {
713+
PermissionRequestResultKind kind = new PermissionRequestResultKind(result.getKind());
714+
if (PermissionRequestResultKind.NO_RESULT.equals(kind)) {
715+
// Handler explicitly abstains — leave the request unanswered
716+
// so another client can handle it.
717+
return;
718+
}
712719
rpc.invoke("session.permissions.handlePendingPermissionRequest",
713720
Map.of("sessionId", sessionId, "requestId", requestId, "result", result), Object.class);
714721
} catch (Exception e) {
@@ -867,6 +874,67 @@ void registerHooks(SessionHooks hooks) {
867874
hooksHandler.set(hooks);
868875
}
869876

877+
/**
878+
* Registers transform callbacks for system message sections.
879+
* <p>
880+
* Called internally when creating or resuming a session with
881+
* {@link com.github.copilot.sdk.SystemMessageMode#CUSTOMIZE} and transform
882+
* callbacks.
883+
*
884+
* @param callbacks
885+
* the transform callbacks keyed by section identifier; {@code null}
886+
* clears any previously registered callbacks
887+
*/
888+
void registerTransformCallbacks(
889+
Map<String, java.util.function.Function<String, CompletableFuture<String>>> callbacks) {
890+
this.transformCallbacks = callbacks;
891+
}
892+
893+
/**
894+
* Handles a {@code systemMessage.transform} RPC call from the Copilot CLI.
895+
* <p>
896+
* The CLI sends section content; the SDK invokes the registered transform
897+
* callbacks and returns the transformed sections.
898+
*
899+
* @param sections
900+
* JSON node containing sections keyed by section identifier
901+
* @return a future resolving with a map of transformed sections
902+
*/
903+
CompletableFuture<Map<String, Object>> handleSystemMessageTransform(JsonNode sections) {
904+
var callbacks = this.transformCallbacks;
905+
var result = new java.util.LinkedHashMap<String, Object>();
906+
var futures = new ArrayList<CompletableFuture<Void>>();
907+
908+
if (sections != null && sections.isObject()) {
909+
sections.fields().forEachRemaining(entry -> {
910+
String sectionId = entry.getKey();
911+
String content = entry.getValue().has("content") ? entry.getValue().get("content").asText("") : "";
912+
913+
java.util.function.Function<String, CompletableFuture<String>> cb = callbacks != null
914+
? callbacks.get(sectionId)
915+
: null;
916+
917+
if (cb != null) {
918+
CompletableFuture<Void> f = cb.apply(content).exceptionally(ex -> content)
919+
.thenAccept(transformed -> {
920+
synchronized (result) {
921+
result.put(sectionId, Map.of("content", transformed != null ? transformed : ""));
922+
}
923+
});
924+
futures.add(f);
925+
} else {
926+
result.put(sectionId, Map.of("content", content));
927+
}
928+
});
929+
}
930+
931+
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenApply(v -> {
932+
Map<String, Object> response = new java.util.LinkedHashMap<>();
933+
response.put("sections", result);
934+
return response;
935+
});
936+
}
937+
870938
/**
871939
* Handles a hook invocation from the Copilot CLI.
872940
* <p>
@@ -982,6 +1050,38 @@ public CompletableFuture<Void> abort() {
9821050
return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class);
9831051
}
9841052

1053+
/**
1054+
* Changes the model for this session with an optional reasoning effort level.
1055+
* <p>
1056+
* The new model takes effect for the next message. Conversation history is
1057+
* preserved.
1058+
*
1059+
* <pre>{@code
1060+
* session.setModel("gpt-4.1").get();
1061+
* session.setModel("claude-sonnet-4.6", "high").get();
1062+
* }</pre>
1063+
*
1064+
* @param model
1065+
* the model ID to switch to (e.g., {@code "gpt-4.1"})
1066+
* @param reasoningEffort
1067+
* reasoning effort level (e.g., {@code "low"}, {@code "medium"},
1068+
* {@code "high"}, {@code "xhigh"}); {@code null} to use default
1069+
* @return a future that completes when the model switch is acknowledged
1070+
* @throws IllegalStateException
1071+
* if this session has been terminated
1072+
* @since 1.2.0
1073+
*/
1074+
public CompletableFuture<Void> setModel(String model, String reasoningEffort) {
1075+
ensureNotTerminated();
1076+
var params = new java.util.HashMap<String, Object>();
1077+
params.put("sessionId", sessionId);
1078+
params.put("modelId", model);
1079+
if (reasoningEffort != null) {
1080+
params.put("reasoningEffort", reasoningEffort);
1081+
}
1082+
return rpc.invoke("session.model.switchTo", params, Void.class);
1083+
}
1084+
9851085
/**
9861086
* Changes the model for this session.
9871087
* <p>
@@ -1000,8 +1100,7 @@ public CompletableFuture<Void> abort() {
10001100
* @since 1.0.11
10011101
*/
10021102
public CompletableFuture<Void> setModel(String model) {
1003-
ensureNotTerminated();
1004-
return rpc.invoke("session.model.switchTo", Map.of("sessionId", sessionId, "modelId", model), Void.class);
1103+
return setModel(model, null);
10051104
}
10061105

10071106
/**
@@ -1017,6 +1116,7 @@ public CompletableFuture<Void> setModel(String model) {
10171116
* session.log("Build completed successfully").get();
10181117
* session.log("Disk space low", "warning", null).get();
10191118
* session.log("Temporary status", null, true).get();
1119+
* session.log("Details at link", "info", null, "https://example.com").get();
10201120
* }</pre>
10211121
*
10221122
* @param message
@@ -1028,11 +1128,14 @@ public CompletableFuture<Void> setModel(String model) {
10281128
* @param ephemeral
10291129
* when {@code true}, the message is transient and not persisted to
10301130
* disk; {@code null} uses default behavior
1131+
* @param url
1132+
* optional URL to associate with the log entry; {@code null} to omit
10311133
* @return a future that completes when the message is logged
10321134
* @throws IllegalStateException
10331135
* if this session has been terminated
1136+
* @since 1.2.0
10341137
*/
1035-
public CompletableFuture<Void> log(String message, String level, Boolean ephemeral) {
1138+
public CompletableFuture<Void> log(String message, String level, Boolean ephemeral, String url) {
10361139
ensureNotTerminated();
10371140
var params = new java.util.HashMap<String, Object>();
10381141
params.put("sessionId", sessionId);
@@ -1043,9 +1146,44 @@ public CompletableFuture<Void> log(String message, String level, Boolean ephemer
10431146
if (ephemeral != null) {
10441147
params.put("ephemeral", ephemeral);
10451148
}
1149+
if (url != null) {
1150+
params.put("url", url);
1151+
}
10461152
return rpc.invoke("session.log", params, Void.class);
10471153
}
10481154

1155+
/**
1156+
* Logs a message to the session timeline.
1157+
* <p>
1158+
* The message appears in the session event stream and is visible to SDK
1159+
* consumers. Non-ephemeral messages are also persisted to the session event log
1160+
* on disk.
1161+
*
1162+
* <h2>Example Usage</h2>
1163+
*
1164+
* <pre>{@code
1165+
* session.log("Build completed successfully").get();
1166+
* session.log("Disk space low", "warning", null).get();
1167+
* session.log("Temporary status", null, true).get();
1168+
* }</pre>
1169+
*
1170+
* @param message
1171+
* the message to log
1172+
* @param level
1173+
* the log severity level ({@code "info"}, {@code "warning"},
1174+
* {@code "error"}), or {@code null} to use the default
1175+
* ({@code "info"})
1176+
* @param ephemeral
1177+
* when {@code true}, the message is transient and not persisted to
1178+
* disk; {@code null} uses default behavior
1179+
* @return a future that completes when the message is logged
1180+
* @throws IllegalStateException
1181+
* if this session has been terminated
1182+
*/
1183+
public CompletableFuture<Void> log(String message, String level, Boolean ephemeral) {
1184+
return log(message, level, ephemeral, null);
1185+
}
1186+
10491187
/**
10501188
* Logs an informational message to the session timeline.
10511189
*
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk;
6+
7+
import java.util.Map;
8+
import java.util.concurrent.CompletableFuture;
9+
import java.util.function.Function;
10+
11+
import com.github.copilot.sdk.json.SystemMessageConfig;
12+
13+
/**
14+
* Result of extracting transform callbacks from a {@link SystemMessageConfig}.
15+
* <p>
16+
* Holds a wire-safe copy of the system message config (with transform callbacks
17+
* replaced by {@code action="transform"}) alongside the extracted callbacks
18+
* that must be registered with the session.
19+
*
20+
* @param wireSystemMessage
21+
* the system message config safe for JSON serialization; may be
22+
* {@code null} when the input config was {@code null}
23+
* @param transformCallbacks
24+
* transform callbacks keyed by section identifier; {@code null} when
25+
* no transforms were present
26+
* @see SessionRequestBuilder#extractTransformCallbacks(SystemMessageConfig)
27+
*/
28+
record ExtractedTransforms(SystemMessageConfig wireSystemMessage,
29+
Map<String, Function<String, CompletableFuture<String>>> transformCallbacks) {
30+
}

0 commit comments

Comments
 (0)