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 @@
066a69c1e849adf1bd98564ab1b52316ec471182
ce56eb81a1b57f72f603f8cd3e7fb5ce9ee2dbc3
90 changes: 90 additions & 0 deletions src/main/java/com/github/copilot/sdk/CopilotSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
import com.github.copilot.sdk.generated.SessionEvent;
import com.github.copilot.sdk.generated.SessionIdleEvent;
import com.github.copilot.sdk.json.AgentInfo;
import com.github.copilot.sdk.json.AutoModeSwitchHandler;
import com.github.copilot.sdk.json.AutoModeSwitchInvocation;
import com.github.copilot.sdk.json.AutoModeSwitchRequest;
import com.github.copilot.sdk.json.AutoModeSwitchResponse;
import com.github.copilot.sdk.json.CommandContext;
import com.github.copilot.sdk.json.CommandDefinition;
import com.github.copilot.sdk.json.CommandHandler;
Expand All @@ -63,6 +67,10 @@
import com.github.copilot.sdk.json.ElicitationResult;
import com.github.copilot.sdk.json.ElicitationResultAction;
import com.github.copilot.sdk.json.ElicitationSchema;
import com.github.copilot.sdk.json.ExitPlanModeHandler;
import com.github.copilot.sdk.json.ExitPlanModeInvocation;
import com.github.copilot.sdk.json.ExitPlanModeRequest;
import com.github.copilot.sdk.json.ExitPlanModeResult;
import com.github.copilot.sdk.json.GetMessagesResponse;
import com.github.copilot.sdk.json.HookInvocation;
import com.github.copilot.sdk.json.InputOptions;
Expand Down Expand Up @@ -156,6 +164,8 @@ public final class CopilotSession implements AutoCloseable {
private final AtomicReference<PermissionHandler> permissionHandler = new AtomicReference<>();
private final AtomicReference<UserInputHandler> userInputHandler = new AtomicReference<>();
private final AtomicReference<ElicitationHandler> elicitationHandler = new AtomicReference<>();
private final AtomicReference<ExitPlanModeHandler> exitPlanModeHandler = new AtomicReference<>();
private final AtomicReference<AutoModeSwitchHandler> autoModeSwitchHandler = new AtomicReference<>();
private final AtomicReference<SessionHooks> hooksHandler = new AtomicReference<>();
private volatile EventErrorHandler eventErrorHandler;
private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS;
Expand Down Expand Up @@ -1317,6 +1327,32 @@ void registerElicitationHandler(ElicitationHandler handler) {
elicitationHandler.set(handler);
}

/**
* Registers an exit-plan-mode handler for this session.
* <p>
* Called internally when creating or resuming a session with an exit-plan-mode
* handler.
*
* @param handler
* the handler to invoke when an exit-plan-mode request is received
*/
void registerExitPlanModeHandler(ExitPlanModeHandler handler) {
exitPlanModeHandler.set(handler);
}

/**
* Registers an auto-mode-switch handler for this session.
* <p>
* Called internally when creating or resuming a session with an
* auto-mode-switch handler.
*
* @param handler
* the handler to invoke when an auto-mode-switch request is received
*/
void registerAutoModeSwitchHandler(AutoModeSwitchHandler handler) {
autoModeSwitchHandler.set(handler);
}

/**
* Sets the capabilities reported by the host for this session.
* <p>
Expand Down Expand Up @@ -1356,6 +1392,60 @@ CompletableFuture<UserInputResponse> handleUserInputRequest(UserInputRequest req
}
}

/**
* Handles an exit-plan-mode request from the Copilot CLI.
* <p>
* Called internally when the server requests to exit plan mode.
*
* @param request
* the exit-plan-mode request
* @return a future that resolves with the exit-plan-mode result
*/
CompletableFuture<ExitPlanModeResult> handleExitPlanModeRequest(ExitPlanModeRequest request) {
ExitPlanModeHandler handler = exitPlanModeHandler.get();
if (handler == null) {
return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true));
}

try {
var invocation = new ExitPlanModeInvocation().setSessionId(sessionId);
return handler.handle(request, invocation).exceptionally(ex -> {
LOG.log(Level.SEVERE, "Exit plan mode handler threw an exception", ex);
return new ExitPlanModeResult().setApproved(true);
});
} catch (Exception e) {
LOG.log(Level.SEVERE, "Failed to process exit plan mode request", e);
return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true));
}
}

/**
* Handles an auto-mode-switch request from the Copilot CLI.
* <p>
* Called internally when the server requests to switch mode.
*
* @param request
* the auto-mode-switch request
* @return a future that resolves with the auto-mode-switch response
*/
CompletableFuture<AutoModeSwitchResponse> handleAutoModeSwitchRequest(AutoModeSwitchRequest request) {
AutoModeSwitchHandler handler = autoModeSwitchHandler.get();
if (handler == null) {
return CompletableFuture.completedFuture(AutoModeSwitchResponse.NO);
}

try {
var invocation = new AutoModeSwitchInvocation().setSessionId(sessionId);
return handler.handle(request, invocation).exceptionally(ex -> {
LOG.log(Level.SEVERE, "Auto mode switch handler threw an exception", ex);
return AutoModeSwitchResponse.NO;
});
} catch (Exception e) {
LOG.log(Level.SEVERE, "Failed to process auto mode switch request", e);
return CompletableFuture.completedFuture(AutoModeSwitchResponse.NO);
}
}

/**
* Registers hook handlers for this session.
* <p>
Expand Down
108 changes: 108 additions & 0 deletions src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ void registerHandlers(JsonRpcClient rpc) {
(requestId, params) -> handlePermissionRequest(rpc, requestId, params));
rpc.registerMethodHandler("userInput.request",
(requestId, params) -> handleUserInputRequest(rpc, requestId, params));
rpc.registerMethodHandler("exitPlanMode.request",
(requestId, params) -> handleExitPlanModeRequest(rpc, requestId, params));
rpc.registerMethodHandler("autoModeSwitch.request",
(requestId, params) -> handleAutoModeSwitchRequest(rpc, requestId, params));
rpc.registerMethodHandler("hooks.invoke", (requestId, params) -> handleHooksInvoke(rpc, requestId, params));
rpc.registerMethodHandler("systemMessage.transform",
(requestId, params) -> handleSystemMessageTransform(rpc, requestId, params));
Expand Down Expand Up @@ -283,6 +287,110 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod
});
}

private void handleExitPlanModeRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
LOG.fine("Received exitPlanMode.request: " + params);
runAsync(() -> {
try {
String sessionId = params.get("sessionId").asText();
String summary = params.has("summary") ? params.get("summary").asText() : "";

CopilotSession session = sessions.get(sessionId);
if (session == null) {
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
return;
}

var request = new com.github.copilot.sdk.json.ExitPlanModeRequest().setSummary(summary);
JsonNode planContentNode = params.get("planContent");
if (planContentNode != null && !planContentNode.isNull()) {
request.setPlanContent(planContentNode.asText());
}
JsonNode actionsNode = params.get("actions");
if (actionsNode != null && actionsNode.isArray()) {
var actions = new ArrayList<String>();
for (JsonNode action : actionsNode) {
actions.add(action.asText());
}
request.setActions(actions);
}
JsonNode recommendedActionNode = params.get("recommendedAction");
if (recommendedActionNode != null && !recommendedActionNode.isNull()) {
request.setRecommendedAction(recommendedActionNode.asText());
}

session.handleExitPlanModeRequest(request).thenAccept(response -> {
try {
var result = new java.util.HashMap<String, Object>();
result.put("approved", response.isApproved());
if (response.getSelectedAction() != null) {
result.put("selectedAction", response.getSelectedAction());
}
if (response.getFeedback() != null) {
result.put("feedback", response.getFeedback());
}
rpc.sendResponse(Long.parseLong(requestId), result);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending exit plan mode response", e);
}
}).exceptionally(ex -> {
LOG.log(Level.WARNING, "Exit plan mode handler exception", ex);
try {
rpc.sendResponse(Long.parseLong(requestId), Map.of("approved", true));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending exit plan mode fallback response", e);
}
return null;
});
} catch (Exception e) {
LOG.log(Level.SEVERE, "Error handling exit plan mode request", e);
}
});
}

private void handleAutoModeSwitchRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
LOG.fine("Received autoModeSwitch.request: " + params);
runAsync(() -> {
try {
String sessionId = params.get("sessionId").asText();

CopilotSession session = sessions.get(sessionId);
if (session == null) {
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
return;
}

var request = new com.github.copilot.sdk.json.AutoModeSwitchRequest();
JsonNode errorCodeNode = params.get("errorCode");
if (errorCodeNode != null && !errorCodeNode.isNull()) {
request.setErrorCode(errorCodeNode.asText());
}
JsonNode retryAfterNode = params.get("retryAfterSeconds");
if (retryAfterNode != null && !retryAfterNode.isNull()) {
request.setRetryAfterSeconds(retryAfterNode.asDouble());
}

session.handleAutoModeSwitchRequest(request).thenAccept(response -> {
try {
rpc.sendResponse(Long.parseLong(requestId), Map.of("response", response.getValue()));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending auto mode switch response", e);
}
}).exceptionally(ex -> {
LOG.log(Level.WARNING, "Auto mode switch handler exception", ex);
try {
rpc.sendResponse(Long.parseLong(requestId),
Map.of("response", com.github.copilot.sdk.json.AutoModeSwitchResponse.NO.getValue()));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Error sending auto mode switch fallback response", e);
}
return null;
});
} catch (Exception e) {
LOG.log(Level.SEVERE, "Error handling auto mode switch request", e);
}
});
}

private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode params) {
runAsync(() -> {
try {
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
if (config.getOnElicitationRequest() != null) {
request.setRequestElicitation(true);
}
if (config.getOnExitPlanMode() != null) {
request.setRequestExitPlanMode(true);
}
if (config.getOnAutoModeSwitch() != null) {
request.setRequestAutoModeSwitch(true);
}
request.setGitHubToken(config.getGitHubToken());

return request;
Expand Down Expand Up @@ -216,6 +222,12 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
if (config.getOnElicitationRequest() != null) {
request.setRequestElicitation(true);
}
if (config.getOnExitPlanMode() != null) {
request.setRequestExitPlanMode(true);
}
if (config.getOnAutoModeSwitch() != null) {
request.setRequestAutoModeSwitch(true);
}
request.setGitHubToken(config.getGitHubToken());

return request;
Expand Down Expand Up @@ -252,6 +264,12 @@ static void configureSession(CopilotSession session, SessionConfig config) {
if (config.getOnElicitationRequest() != null) {
session.registerElicitationHandler(config.getOnElicitationRequest());
}
if (config.getOnExitPlanMode() != null) {
session.registerExitPlanModeHandler(config.getOnExitPlanMode());
}
if (config.getOnAutoModeSwitch() != null) {
session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch());
}
if (config.getOnEvent() != null) {
session.on(config.getOnEvent());
}
Expand Down Expand Up @@ -288,6 +306,12 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config)
if (config.getOnElicitationRequest() != null) {
session.registerElicitationHandler(config.getOnElicitationRequest());
}
if (config.getOnExitPlanMode() != null) {
session.registerExitPlanModeHandler(config.getOnExitPlanMode());
}
if (config.getOnAutoModeSwitch() != null) {
session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch());
}
if (config.getOnEvent() != null) {
session.on(config.getOnEvent());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.json;

import java.util.concurrent.CompletableFuture;

/**
* Handler for auto-mode-switch requests from the agent.
* <p>
* Implement this interface to handle requests when the agent encounters a rate
* limit and wants to switch to a different model automatically.
*
* <h2>Example Usage</h2>
*
* <pre>{@code
* AutoModeSwitchHandler handler = (request, invocation) -> {
* System.out.println("Rate limited: " + request.getErrorCode());
* return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES);
* };
*
* var session = client.createSession(new SessionConfig().setOnAutoModeSwitch(handler)).get();
* }</pre>
*
* @since 1.4.0
*/
@FunctionalInterface
public interface AutoModeSwitchHandler {

/**
* Handles an auto-mode-switch request from the agent.
*
* @param request
* the auto-mode-switch request containing error code and retry-after
* information
* @param invocation
* context information about the invocation
* @return a future that resolves with the user's decision
*/
CompletableFuture<AutoModeSwitchResponse> handle(AutoModeSwitchRequest request,
AutoModeSwitchInvocation invocation);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.sdk.json;

/**
* Context for an auto-mode-switch request invocation.
*
* @since 1.4.0
*/
public class AutoModeSwitchInvocation {

private String sessionId;

/**
* Gets the session ID.
*
* @return the session ID
*/
public String getSessionId() {
return sessionId;
}

/**
* Sets the session ID.
*
* @param sessionId
* the session ID
* @return this instance for method chaining
*/
public AutoModeSwitchInvocation setSessionId(String sessionId) {
this.sessionId = sessionId;
return this;
}
}
Loading
Loading