Skip to content
Draft
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 @@
062b61c8aa63b9b5d45fa1d7b01723e6660ffa83
485ea5ed1ce43125075bab2f3d2681f1816a4f9a
22 changes: 22 additions & 0 deletions src/main/java/com/github/copilot/sdk/CliServerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,28 @@ ProcessInfo startCliServer() throws IOException, InterruptedException {
pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken());
}

// Set telemetry environment variables if configured
var telemetry = options.getTelemetry();
if (telemetry != null) {
pb.environment().put("COPILOT_OTEL_ENABLED", "true");
if (telemetry.getOtlpEndpoint() != null) {
pb.environment().put("OTEL_EXPORTER_OTLP_ENDPOINT", telemetry.getOtlpEndpoint());
}
if (telemetry.getFilePath() != null) {
pb.environment().put("COPILOT_OTEL_FILE_EXPORTER_PATH", telemetry.getFilePath());
}
if (telemetry.getExporterType() != null) {
pb.environment().put("COPILOT_OTEL_EXPORTER_TYPE", telemetry.getExporterType());
}
if (telemetry.getSourceName() != null) {
pb.environment().put("COPILOT_OTEL_SOURCE_NAME", telemetry.getSourceName());
}
if (telemetry.getCaptureContent() != null) {
pb.environment().put("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT",
telemetry.getCaptureContent() ? "true" : "false");
}
}

Process process = pb.start();

// Forward stderr to logger in background
Expand Down
50 changes: 48 additions & 2 deletions src/main/java/com/github/copilot/sdk/CopilotSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,11 @@ private void executePermissionAndRespondAsync(String requestId, PermissionReques
invocation.setSessionId(sessionId);
handler.handle(permissionRequest, invocation).thenAccept(result -> {
try {
if (PermissionRequestResultKind.NO_RESULT
.equals(new PermissionRequestResultKind(result.getKind()))) {
// "no-result" means leave the request unanswered — no RPC call.
return;
}
rpc.invoke("session.permissions.handlePendingPermissionRequest",
Map.of("sessionId", sessionId, "requestId", requestId, "result", result), Object.class);
} catch (Exception e) {
Expand Down Expand Up @@ -801,7 +806,17 @@ CompletableFuture<PermissionRequestResult> handlePermissionRequest(JsonNode perm
PermissionRequest request = MAPPER.treeToValue(permissionRequestData, PermissionRequest.class);
var invocation = new PermissionInvocation();
invocation.setSessionId(sessionId);
return handler.handle(request, invocation).exceptionally(ex -> {
return handler.handle(request, invocation).thenApply(result -> {
if (PermissionRequestResultKind.NO_RESULT.equals(new PermissionRequestResultKind(result.getKind()))) {
throw new IllegalStateException(
"Permission handlers cannot return 'no-result' when connected to a protocol v2 server.");
}
return result;
}).exceptionally(ex -> {
if (ex instanceof IllegalStateException ise && ise.getMessage() != null
&& ise.getMessage().contains("no-result")) {
throw (IllegalStateException) ex;
}
LOG.log(Level.SEVERE, "Permission handler threw an exception", ex);
PermissionRequestResult result = new PermissionRequestResult();
result.setKind("denied-no-approval-rule-and-could-not-request-from-user");
Expand Down Expand Up @@ -1000,8 +1015,39 @@ public CompletableFuture<Void> abort() {
* @since 1.0.11
*/
public CompletableFuture<Void> setModel(String model) {
return setModel(model, null);
}

/**
* Changes the model and reasoning effort for this session.
* <p>
* The new model and reasoning effort take effect for the next message.
* Conversation history is preserved.
*
* <pre>{@code
* session.setModel("claude-sonnet-4.6", "high").get();
* }</pre>
*
* @param model
* the model ID to switch to (e.g., {@code "gpt-4.1"})
* @param reasoningEffort
* the reasoning effort level ({@code "low"}, {@code "medium"},
* {@code "high"}, {@code "xhigh"}), or {@code null} to use the
* model's default
* @return a future that completes when the model switch is acknowledged
* @throws IllegalStateException
* if this session has been terminated
* @since 1.1.0
*/
public CompletableFuture<Void> setModel(String model, String reasoningEffort) {
ensureNotTerminated();
return rpc.invoke("session.model.switchTo", Map.of("sessionId", sessionId, "modelId", model), Void.class);
var params = new java.util.LinkedHashMap<String, Object>();
params.put("sessionId", sessionId);
params.put("modelId", model);
if (reasoningEffort != null) {
params.put("reasoningEffort", reasoningEffort);
}
return rpc.invoke("session.model.switchTo", params, Void.class);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public abstract sealed class AbstractSessionEvent permits
SessionModelChangeEvent, SessionModeChangedEvent, SessionPlanChangedEvent, SessionWorkspaceFileChangedEvent,
SessionHandoffEvent, SessionTruncationEvent, SessionSnapshotRewindEvent, SessionUsageInfoEvent,
SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionShutdownEvent, SessionContextChangedEvent,
SessionTaskCompleteEvent,
SessionTaskCompleteEvent, SessionBackgroundTasksChangedEvent, SessionToolsUpdatedEvent,
// Assistant events
AssistantTurnStartEvent, AssistantIntentEvent, AssistantReasoningEvent, AssistantReasoningDeltaEvent,
AssistantMessageEvent, AssistantMessageDeltaEvent, AssistantStreamingDeltaEvent, AssistantTurnEndEvent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public record AssistantUsageData(@JsonProperty("model") String model,
@JsonProperty("apiCallId") String apiCallId, @JsonProperty("providerCallId") String providerCallId,
@JsonProperty("parentToolCallId") String parentToolCallId,
@JsonProperty("quotaSnapshots") Map<String, Object> quotaSnapshots,
@JsonProperty("copilotUsage") CopilotUsage copilotUsage) {
@JsonProperty("copilotUsage") CopilotUsage copilotUsage,
@JsonProperty("reasoningEffort") String reasoningEffort) {

/** Returns a defensive copy of the quota snapshots map. */
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public void setData(ExternalToolRequestedData data) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record ExternalToolRequestedData(@JsonProperty("requestId") String requestId,
@JsonProperty("sessionId") String sessionId, @JsonProperty("toolCallId") String toolCallId,
@JsonProperty("toolName") String toolName, @JsonProperty("arguments") Object arguments) {
@JsonProperty("toolName") String toolName, @JsonProperty("arguments") Object arguments,
@JsonProperty("traceparent") String traceparent, @JsonProperty("tracestate") String tracestate) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.events;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Event: session.background_tasks_changed
*
* @since 1.1.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class SessionBackgroundTasksChangedEvent extends AbstractSessionEvent {

@JsonProperty("data")
private SessionBackgroundTasksChangedData data;

@Override
public String getType() {
return "session.background_tasks_changed";
}

public SessionBackgroundTasksChangedData getData() {
return data;
}

public void setData(SessionBackgroundTasksChangedData data) {
this.data = data;
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record SessionBackgroundTasksChangedData() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public class SessionEventParser {
TYPE_MAP.put("session.compaction_complete", SessionCompactionCompleteEvent.class);
TYPE_MAP.put("session.context_changed", SessionContextChangedEvent.class);
TYPE_MAP.put("session.task_complete", SessionTaskCompleteEvent.class);
TYPE_MAP.put("session.background_tasks_changed", SessionBackgroundTasksChangedEvent.class);
TYPE_MAP.put("session.tools_updated", SessionToolsUpdatedEvent.class);
TYPE_MAP.put("user.message", UserMessageEvent.class);
TYPE_MAP.put("pending_messages.modified", PendingMessagesModifiedEvent.class);
TYPE_MAP.put("assistant.turn_start", AssistantTurnStartEvent.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public void setData(SessionModelChangeData data) {

@JsonIgnoreProperties(ignoreUnknown = true)
public record SessionModelChangeData(@JsonProperty("previousModel") String previousModel,
@JsonProperty("newModel") String newModel) {
@JsonProperty("newModel") String newModel,
@JsonProperty("previousReasoningEffort") String previousReasoningEffort,
@JsonProperty("reasoningEffort") String reasoningEffort) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public void setData(SessionResumeData data) {

@JsonIgnoreProperties(ignoreUnknown = true)
public record SessionResumeData(@JsonProperty("resumeTime") OffsetDateTime resumeTime,
@JsonProperty("eventCount") double eventCount) {
@JsonProperty("eventCount") double eventCount, @JsonProperty("selectedModel") String selectedModel,
@JsonProperty("reasoningEffort") String reasoningEffort) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public void setData(SessionStartData data) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record SessionStartData(@JsonProperty("sessionId") String sessionId, @JsonProperty("version") double version,
@JsonProperty("producer") String producer, @JsonProperty("copilotVersion") String copilotVersion,
@JsonProperty("startTime") OffsetDateTime startTime, @JsonProperty("selectedModel") String selectedModel) {
@JsonProperty("startTime") OffsetDateTime startTime, @JsonProperty("selectedModel") String selectedModel,
@JsonProperty("reasoningEffort") String reasoningEffort) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.events;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Event: session.tools_updated
*
* @since 1.1.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class SessionToolsUpdatedEvent extends AbstractSessionEvent {

@JsonProperty("data")
private SessionToolsUpdatedData data;

@Override
public String getType() {
return "session.tools_updated";
}

public SessionToolsUpdatedData getData() {
return data;
}

public void setData(SessionToolsUpdatedData data) {
this.data = data;
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record SessionToolsUpdatedData(@JsonProperty("model") String model) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ public class CopilotClientOptions {
private String cliUrl;
private String logLevel = "info";
private boolean autoStart = true;
private boolean autoRestart = true;
@Deprecated
private boolean autoRestart;
private Map<String, String> environment;
private String gitHubToken;
private Boolean useLoggedInUser;
private Supplier<CompletableFuture<List<ModelInfo>>> onListModels;
private TelemetryConfig telemetry;

/**
* Gets the path to the Copilot CLI executable.
Expand Down Expand Up @@ -236,8 +238,11 @@ public CopilotClientOptions setAutoStart(boolean autoStart) {
/**
* Returns whether the client should automatically restart the server on crash.
*
* @return {@code true} to auto-restart (default), {@code false} otherwise
* @return {@code true} to auto-restart, {@code false} otherwise
* @deprecated {@code autoRestart} has no effect and will be removed in a future
* release.
*/
@Deprecated
public boolean isAutoRestart() {
return autoRestart;
}
Expand All @@ -249,7 +254,10 @@ public boolean isAutoRestart() {
* @param autoRestart
* {@code true} to auto-restart, {@code false} otherwise
* @return this options instance for method chaining
* @deprecated {@code autoRestart} has no effect and will be removed in a future
* release.
*/
@Deprecated
public CopilotClientOptions setAutoRestart(boolean autoRestart) {
this.autoRestart = autoRestart;
return this;
Expand Down Expand Up @@ -378,6 +386,34 @@ public CopilotClientOptions setOnListModels(Supplier<CompletableFuture<List<Mode
return this;
}

/**
* Gets the OpenTelemetry configuration for the CLI server.
*
* @return the telemetry configuration, or {@code null} if not set
*/
public TelemetryConfig getTelemetry() {
return telemetry;
}

/**
* Sets the OpenTelemetry configuration for the CLI server.
* <p>
* When set to a non-{@code null} instance, the CLI server is started with
* OpenTelemetry instrumentation enabled. The configuration controls the
* exporter endpoint, file path, exporter type, source name, and content capture
* settings.
*
* @param telemetry
* the telemetry configuration, or {@code null} to disable
* OpenTelemetry instrumentation
* @return this options instance for method chaining
* @see TelemetryConfig
*/
public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) {
this.telemetry = telemetry;
return this;
}

/**
* Creates a shallow clone of this {@code CopilotClientOptions} instance.
* <p>
Expand All @@ -404,6 +440,7 @@ public CopilotClientOptions clone() {
copy.gitHubToken = this.gitHubToken;
copy.useLoggedInUser = this.useLoggedInUser;
copy.onListModels = this.onListModels;
copy.telemetry = this.telemetry;
return copy;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
* because no approval rule was found and the user could not be prompted.</li>
* <li>{@link #DENIED_INTERACTIVELY_BY_USER} — the permission was denied
* interactively by the user.</li>
* <li>{@link #NO_RESULT} — leave the pending permission request unanswered (v3
* protocol only).</li>
* </ul>
*
* @see PermissionRequestResult
Expand All @@ -51,6 +53,20 @@ public final class PermissionRequestResultKind {
public static final PermissionRequestResultKind DENIED_INTERACTIVELY_BY_USER = new PermissionRequestResultKind(
"denied-interactively-by-user");

/**
* Leave the pending permission request unanswered (no-op).
* <p>
* When a permission handler returns this kind, the SDK does not send any
* response to the server — the permission request stays pending. This is useful
* in multi-client scenarios where a secondary client wants to signal that it
* does not own the decision.
* <p>
* <strong>Note:</strong> Returning this kind is only valid when the session is
* connected to a protocol v3 server (broadcast model). Returning it against a
* v2 server will result in an error.
*/
public static final PermissionRequestResultKind NO_RESULT = new PermissionRequestResultKind("no-result");

private final String value;

/**
Expand Down
Loading
Loading