diff --git a/.lastmerge b/.lastmerge index 142f4f7ab..4471f7c2b 100644 --- a/.lastmerge +++ b/.lastmerge @@ -1 +1 @@ -066a69c1e849adf1bd98564ab1b52316ec471182 +f6257791b9dab133358456caa4ba86f20f833101 diff --git a/pom.xml b/pom.xml index 832f2f2c9..c8930b3d1 100644 --- a/pom.xml +++ b/pom.xml @@ -94,7 +94,7 @@ reference-impl-sync workflow and deal with the subsequent PR. --> - ^1.0.44-2 + ^1.0.44-3 diff --git a/scripts/codegen/package-lock.json b/scripts/codegen/package-lock.json index f414fec71..242ba1747 100644 --- a/scripts/codegen/package-lock.json +++ b/scripts/codegen/package-lock.json @@ -6,7 +6,7 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.44-2", + "@github/copilot": "^1.0.44-3", "json-schema": "^0.4.0", "tsx": "^4.20.6" } @@ -428,26 +428,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.44-2", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.44-2.tgz", - "integrity": "sha512-MUIR4w+oXjbg1jwUS8B86eMd/bV2gVKZ61a/aEUE4gUrFFpGXO0tNk9OkfLSH5cmlhJY6lzMzb+kKQWoeAbbNQ==", + "version": "1.0.44", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.44.tgz", + "integrity": "sha512-wr/GmNOUaJK/giJK5abyB1oTpEowgFKLi+NJnlyAymKiK/GKCaRlJqiX23H2RetM8vD2hDYUFUFm9lTCooGy0g==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.44-2", - "@github/copilot-darwin-x64": "1.0.44-2", - "@github/copilot-linux-arm64": "1.0.44-2", - "@github/copilot-linux-x64": "1.0.44-2", - "@github/copilot-win32-arm64": "1.0.44-2", - "@github/copilot-win32-x64": "1.0.44-2" + "@github/copilot-darwin-arm64": "1.0.44", + "@github/copilot-darwin-x64": "1.0.44", + "@github/copilot-linux-arm64": "1.0.44", + "@github/copilot-linux-x64": "1.0.44", + "@github/copilot-win32-arm64": "1.0.44", + "@github/copilot-win32-x64": "1.0.44" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.44-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.44-2.tgz", - "integrity": "sha512-6o/pvew0FZJG+8saG1K/L1pUIvpz4AWkZitiqH36tDfXdXKx/PUQ+zaFg/KPeHNnxtal5OdE/7iyrJwIqm2gPg==", + "version": "1.0.44", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.44.tgz", + "integrity": "sha512-9NqA5sT2spmNsehxhs51GhXRZIZga5nq+WcMl4LG2QrUPJRDwvHf1bDKqETJUBbYvBY8jONGuTKMRofkMI68YQ==", "cpu": [ "arm64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.44-2", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.44-2.tgz", - "integrity": "sha512-OMNoLNFYUynB4wiplSh4gtD5zVlvfWMKc0jKQ0oItJLGO8GRL9X0ZB2ONB+7JpVvPidz0Yy4+jU0zWNXEjMM5g==", + "version": "1.0.44", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.44.tgz", + "integrity": "sha512-QPD8KtXx07SIKILGBl4JDhPyL2Qo0FMmaTYVxR6nkyHkHnFPsUZD6VWGR+T/KMLkcUXFM85Xc1ba9Y27s4nRrQ==", "cpu": [ "x64" ], @@ -477,9 +477,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.44-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.44-2.tgz", - "integrity": "sha512-5WGRADU08hqBTWmQ6JVOYMximzsXGuOdFF4GFRQqfsCR8k4RE8fdPWQJa92BpqMgGWwEVPemq0wB3D4hDM5eWw==", + "version": "1.0.44", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.44.tgz", + "integrity": "sha512-Z8ScIUP433xS18f68NP9jM9zW320Xzpi2wf7Nig/VyfrwupBy25UTezydQMT0KQHLWTEleHOPcYnASY3HgJXnQ==", "cpu": [ "arm64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.44-2", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.44-2.tgz", - "integrity": "sha512-4ZnA2QxEwgrdCePdS5OjuksEGFpJrXgofuELANCpDSHwR3eTV7PynVyqhG6Et7ktN2KzHk7zf8kvtiWVCOxvFg==", + "version": "1.0.44", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.44.tgz", + "integrity": "sha512-KUl6lvJt0HNKaXSx0T0bIWJ3rvrGwgZYMlkDfqMbuMnZatEQJbjPwxmL/IDfp/c0DyKd7K+ajl17wHYcN/hJIQ==", "cpu": [ "x64" ], @@ -509,9 +509,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.44-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.44-2.tgz", - "integrity": "sha512-klgSdBZblz9O8BRnTh9uk9uO/INQwVeTBagXuJO7MrZ7JCfBVJyFUYky2tKIjFxlwefyhrRZuniqYeOI9fQc+A==", + "version": "1.0.44", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.44.tgz", + "integrity": "sha512-JVJxZJwAc95ZfapgOXjNFwSqrWlvC3heo128L+CDkdZ6lwpD1dTGMHT/6rMMEeo3xjZmMm8tiynfwsHLDgTtvQ==", "cpu": [ "arm64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.44-2", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.44-2.tgz", - "integrity": "sha512-ziq3abdbMCqtAqdiEWWf6cn0whlWss7rC9VMsO/Vx2gjSEVCeJkmIiRiQO45WikheyXyxEmCTAvOwZLQvs+I9g==", + "version": "1.0.44", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.44.tgz", + "integrity": "sha512-Yj3KQ/DqwS50PwRtyQITX2mWIVZeJeX+y0faVSMwUUzG1qxmMcme7wimhKOyc4LSV11DpgVB9MSiBw2xys7iww==", "cpu": [ "x64" ], diff --git a/scripts/codegen/package.json b/scripts/codegen/package.json index 66d5565e8..6dde79579 100644 --- a/scripts/codegen/package.json +++ b/scripts/codegen/package.json @@ -7,7 +7,7 @@ "generate:java": "tsx java.ts" }, "dependencies": { - "@github/copilot": "^1.0.44-2", + "@github/copilot": "^1.0.44-3", "json-schema": "^0.4.0", "tsx": "^4.20.6" } diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index e4cb98464..abcc39d13 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -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; @@ -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; @@ -156,6 +164,8 @@ public final class CopilotSession implements AutoCloseable { private final AtomicReference permissionHandler = new AtomicReference<>(); private final AtomicReference userInputHandler = new AtomicReference<>(); private final AtomicReference elicitationHandler = new AtomicReference<>(); + private final AtomicReference exitPlanModeHandler = new AtomicReference<>(); + private final AtomicReference autoModeSwitchHandler = new AtomicReference<>(); private final AtomicReference hooksHandler = new AtomicReference<>(); private volatile EventErrorHandler eventErrorHandler; private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS; @@ -1317,6 +1327,32 @@ void registerElicitationHandler(ElicitationHandler handler) { elicitationHandler.set(handler); } + /** + * Registers an exit-plan-mode handler for this session. + *

+ * 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. + *

+ * 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. *

@@ -1356,6 +1392,60 @@ CompletableFuture handleUserInputRequest(UserInputRequest req } } + /** + * Handles an exit-plan-mode request from the Copilot CLI. + *

+ * Called internally when the server requests the user to exit plan mode. + * + * @param request + * the exit-plan-mode request + * @return a future that resolves with the exit-plan-mode result + */ + CompletableFuture 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. + *

+ * Called internally when the server requests to switch to auto mode. + * + * @param request + * the auto-mode-switch request + * @return a future that resolves with the auto-mode-switch response + */ + CompletableFuture 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. *

@@ -1850,6 +1940,8 @@ public void close() { permissionHandler.set(null); userInputHandler.set(null); elicitationHandler.set(null); + exitPlanModeHandler.set(null); + autoModeSwitchHandler.set(null); hooksHandler.set(null); } diff --git a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java index d085f7fce..53c491930 100644 --- a/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java +++ b/src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java @@ -17,6 +17,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.copilot.sdk.generated.SessionEvent; +import com.github.copilot.sdk.json.AutoModeSwitchRequest; +import com.github.copilot.sdk.json.ExitPlanModeRequest; import com.github.copilot.sdk.json.PermissionRequestResult; import com.github.copilot.sdk.json.PermissionRequestResultKind; import com.github.copilot.sdk.json.SessionLifecycleEvent; @@ -79,6 +81,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)); @@ -283,6 +289,94 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod }); } + private void handleExitPlanModeRequest(JsonRpcClient rpc, String requestId, JsonNode 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 ExitPlanModeRequest(); + request.setSummary(params.has("summary") ? params.get("summary").asText() : ""); + if (params.has("planContent") && !params.get("planContent").isNull()) { + request.setPlanContent(params.get("planContent").asText()); + } + if (params.has("actions") && params.get("actions").isArray()) { + var actions = new ArrayList(); + for (JsonNode action : params.get("actions")) { + actions.add(action.asText()); + } + request.setActions(actions); + } + if (params.has("recommendedAction") && !params.get("recommendedAction").isNull()) { + request.setRecommendedAction(params.get("recommendedAction").asText()); + } + + session.handleExitPlanModeRequest(request).thenAccept(result -> { + try { + rpc.sendResponse(Long.parseLong(requestId), result); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending exit plan mode response", e); + } + }).exceptionally(ex -> { + try { + rpc.sendErrorResponse(Long.parseLong(requestId), -32603, + "Exit plan mode handler error: " + ex.getMessage()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending exit plan mode error", 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) { + 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 AutoModeSwitchRequest(); + if (params.has("errorCode") && !params.get("errorCode").isNull()) { + request.setErrorCode(params.get("errorCode").asText()); + } + if (params.has("retryAfterSeconds") && !params.get("retryAfterSeconds").isNull()) { + request.setRetryAfterSeconds(params.get("retryAfterSeconds").asDouble()); + } + + session.handleAutoModeSwitchRequest(request).thenAccept(response -> { + try { + rpc.sendResponse(Long.parseLong(requestId), java.util.Map.of("response", response.getValue())); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending auto mode switch response", e); + } + }).exceptionally(ex -> { + try { + rpc.sendErrorResponse(Long.parseLong(requestId), -32603, + "Auto mode switch handler error: " + ex.getMessage()); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Error sending auto mode switch error", 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 { diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java index fd9696690..d9752db7e 100644 --- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java +++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java @@ -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; @@ -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; @@ -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()); } @@ -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()); } diff --git a/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchHandler.java b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchHandler.java new file mode 100644 index 000000000..d0d75089e --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchHandler.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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. + *

+ * Implement this interface to handle requests to switch to auto mode when a + * rate limit is encountered. The handler decides whether to approve the switch. + * + *

Example Usage

+ * + *
{@code
+ * AutoModeSwitchHandler handler = (request, invocation) -> {
+ * 	System.out.println("Rate limited: " + request.getErrorCode());
+ * 	return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES);
+ * };
+ *
+ * var session = client.createSession(
+ * 		new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnAutoModeSwitch(handler))
+ * 		.get();
+ * }
+ * + * @since 1.0.7 + */ +@FunctionalInterface +public interface AutoModeSwitchHandler { + + /** + * Handles an auto-mode-switch request from the agent. + * + * @param request + * the auto-mode-switch request containing the error code and + * retry-after information + * @param invocation + * context information about the invocation + * @return a future that resolves with the user's decision + */ + CompletableFuture handle(AutoModeSwitchRequest request, + AutoModeSwitchInvocation invocation); +} diff --git a/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchInvocation.java b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchInvocation.java new file mode 100644 index 000000000..cc70e234e --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchInvocation.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for an auto-mode-switch request invocation. + * + * @since 1.0.7 + */ +public class AutoModeSwitchInvocation { + + private String sessionId = ""; + + /** Gets the session ID that triggered the request. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID @return this */ + public AutoModeSwitchInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchRequest.java b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchRequest.java new file mode 100644 index 000000000..595da27e7 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchRequest.java @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request to switch to auto mode after an eligible rate limit. + * + * @since 1.0.7 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AutoModeSwitchRequest { + + @JsonProperty("errorCode") + private String errorCode; + + @JsonProperty("retryAfterSeconds") + private Double retryAfterSeconds; + + /** + * Gets the rate-limit error code that triggered the request. @return the error + * code + */ + public String getErrorCode() { + return errorCode; + } + + /** Sets the error code. @param errorCode the error code @return this */ + public AutoModeSwitchRequest setErrorCode(String errorCode) { + this.errorCode = errorCode; + return this; + } + + /** + * Gets the seconds until the rate limit resets, when known. @return the + * retry-after seconds + */ + public Double getRetryAfterSeconds() { + return retryAfterSeconds; + } + + /** + * Sets the retry-after seconds. @param retryAfterSeconds the seconds @return + * this + */ + public AutoModeSwitchRequest setRetryAfterSeconds(Double retryAfterSeconds) { + this.retryAfterSeconds = retryAfterSeconds; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchResponse.java b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchResponse.java new file mode 100644 index 000000000..0e2db280b --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/AutoModeSwitchResponse.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Response to an auto-mode-switch request. + * + * @since 1.0.7 + */ +public enum AutoModeSwitchResponse { + + /** Approve the switch for this rate-limit cycle. */ + YES("yes"), + + /** Approve and remember the choice for this session. */ + YES_ALWAYS("yes_always"), + + /** Decline the switch. */ + NO("no"); + + private final String value; + + AutoModeSwitchResponse(String value) { + this.value = value; + } + + /** Gets the JSON string value. @return the value */ + @JsonValue + public String getValue() { + return value; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java index 3a0b90f19..12bab4154 100644 --- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java @@ -112,6 +112,12 @@ public final class CreateSessionRequest { @JsonProperty("requestElicitation") private Boolean requestElicitation; + @JsonProperty("requestExitPlanMode") + private Boolean requestExitPlanMode; + + @JsonProperty("requestAutoModeSwitch") + private Boolean requestAutoModeSwitch; + @JsonProperty("modelCapabilities") private ModelCapabilitiesOverride modelCapabilities; @@ -419,6 +425,28 @@ public void setRequestElicitation(Boolean requestElicitation) { this.requestElicitation = requestElicitation; } + /** Gets the requestExitPlanMode flag. @return the flag */ + public Boolean getRequestExitPlanMode() { + return requestExitPlanMode; + } + + /** Sets the requestExitPlanMode flag. @param requestExitPlanMode the flag */ + public void setRequestExitPlanMode(Boolean requestExitPlanMode) { + this.requestExitPlanMode = requestExitPlanMode; + } + + /** Gets the requestAutoModeSwitch flag. @return the flag */ + public Boolean getRequestAutoModeSwitch() { + return requestAutoModeSwitch; + } + + /** + * Sets the requestAutoModeSwitch flag. @param requestAutoModeSwitch the flag + */ + public void setRequestAutoModeSwitch(Boolean requestAutoModeSwitch) { + this.requestAutoModeSwitch = requestAutoModeSwitch; + } + /** Gets the model capabilities override. @return the override */ public ModelCapabilitiesOverride getModelCapabilities() { return modelCapabilities; diff --git a/src/main/java/com/github/copilot/sdk/json/ExitPlanModeHandler.java b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeHandler.java new file mode 100644 index 000000000..0455d68bd --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeHandler.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.concurrent.CompletableFuture; + +/** + * Handler for exit-plan-mode requests from the agent. + *

+ * Implement this interface to handle requests to exit plan mode. When the agent + * finishes planning and wants to proceed, this handler is invoked to get the + * user's approval and action selection. + * + *

Example Usage

+ * + *
{@code
+ * ExitPlanModeHandler handler = (request, invocation) -> {
+ * 	System.out.println("Plan summary: " + request.getSummary());
+ * 	return CompletableFuture
+ * 			.completedFuture(new ExitPlanModeResult().setApproved(true).setSelectedAction("interactive"));
+ * };
+ *
+ * var session = client
+ * 		.createSession(
+ * 				new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnExitPlanMode(handler))
+ * 		.get();
+ * }
+ * + * @since 1.0.7 + */ +@FunctionalInterface +public interface ExitPlanModeHandler { + + /** + * Handles an exit-plan-mode request from the agent. + * + * @param request + * the exit-plan-mode request containing the plan summary and + * available actions + * @param invocation + * context information about the invocation + * @return a future that resolves with the user's decision + */ + CompletableFuture handle(ExitPlanModeRequest request, ExitPlanModeInvocation invocation); +} diff --git a/src/main/java/com/github/copilot/sdk/json/ExitPlanModeInvocation.java b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeInvocation.java new file mode 100644 index 000000000..7adaa7fbd --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeInvocation.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +/** + * Context for an exit-plan-mode request invocation. + * + * @since 1.0.7 + */ +public class ExitPlanModeInvocation { + + private String sessionId = ""; + + /** Gets the session ID that triggered the request. @return the session ID */ + public String getSessionId() { + return sessionId; + } + + /** Sets the session ID. @param sessionId the session ID @return this */ + public ExitPlanModeInvocation setSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ExitPlanModeRequest.java b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeRequest.java new file mode 100644 index 000000000..5a1c7ff09 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeRequest.java @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Request to exit plan mode and continue with a selected action. + * + * @since 1.0.7 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExitPlanModeRequest { + + @JsonProperty("summary") + private String summary = ""; + + @JsonProperty("planContent") + private String planContent; + + @JsonProperty("actions") + private List actions; + + @JsonProperty("recommendedAction") + private String recommendedAction = "autopilot"; + + /** Gets the summary of the plan or proposed next step. @return the summary */ + public String getSummary() { + return summary; + } + + /** Sets the summary. @param summary the summary @return this */ + public ExitPlanModeRequest setSummary(String summary) { + this.summary = summary; + return this; + } + + /** Gets the full plan content, when available. @return the plan content */ + public String getPlanContent() { + return planContent; + } + + /** Sets the plan content. @param planContent the plan content @return this */ + public ExitPlanModeRequest setPlanContent(String planContent) { + this.planContent = planContent; + return this; + } + + /** Gets the available actions the user can select. @return the actions */ + public List getActions() { + return actions; + } + + /** Sets the actions. @param actions the actions @return this */ + public ExitPlanModeRequest setActions(List actions) { + this.actions = actions; + return this; + } + + /** + * Gets the action recommended by the runtime. @return the recommended action + */ + public String getRecommendedAction() { + return recommendedAction; + } + + /** + * Sets the recommended action. @param recommendedAction the recommended + * action @return this + */ + public ExitPlanModeRequest setRecommendedAction(String recommendedAction) { + this.recommendedAction = recommendedAction; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ExitPlanModeResult.java b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeResult.java new file mode 100644 index 000000000..08e1ef2f6 --- /dev/null +++ b/src/main/java/com/github/copilot/sdk/json/ExitPlanModeResult.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Response to an exit-plan-mode request. + * + * @since 1.0.7 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExitPlanModeResult { + + @JsonProperty("approved") + private boolean approved = true; + + @JsonProperty("selectedAction") + private String selectedAction; + + @JsonProperty("feedback") + private String feedback; + + /** + * Gets whether the user approved exiting plan mode. @return true if approved + */ + public boolean isApproved() { + return approved; + } + + /** Sets whether approved. @param approved the flag @return this */ + public ExitPlanModeResult setApproved(boolean approved) { + this.approved = approved; + return this; + } + + /** + * Gets the selected action, if the user chose one. @return the selected action + */ + public String getSelectedAction() { + return selectedAction; + } + + /** + * Sets the selected action. @param selectedAction the selected action @return + * this + */ + public ExitPlanModeResult setSelectedAction(String selectedAction) { + this.selectedAction = selectedAction; + return this; + } + + /** Gets optional feedback provided by the user. @return the feedback */ + public String getFeedback() { + return feedback; + } + + /** Sets the feedback. @param feedback the feedback @return this */ + public ExitPlanModeResult setFeedback(String feedback) { + this.feedback = feedback; + return this; + } +} diff --git a/src/main/java/com/github/copilot/sdk/json/ModelBilling.java b/src/main/java/com/github/copilot/sdk/json/ModelBilling.java index d04ef0d3e..874ec6c10 100644 --- a/src/main/java/com/github/copilot/sdk/json/ModelBilling.java +++ b/src/main/java/com/github/copilot/sdk/json/ModelBilling.java @@ -16,13 +16,13 @@ public class ModelBilling { @JsonProperty("multiplier") - private double multiplier; + private Double multiplier; - public double getMultiplier() { + public Double getMultiplier() { return multiplier; } - public ModelBilling setMultiplier(double multiplier) { + public ModelBilling setMultiplier(Double multiplier) { this.multiplier = multiplier; return this; } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java index cb52c62e0..56476cbd5 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java @@ -66,6 +66,8 @@ public class ResumeSessionConfig { private Consumer onEvent; private List commands; private ElicitationHandler onElicitationRequest; + private ExitPlanModeHandler onExitPlanMode; + private AutoModeSwitchHandler onAutoModeSwitch; private String gitHubToken; /** @@ -766,6 +768,60 @@ public ResumeSessionConfig setOnElicitationRequest(ElicitationHandler onElicitat return this; } + /** + * Gets the exit-plan-mode handler. + * + * @return the exit-plan-mode handler, or {@code null} + * @since 1.0.7 + */ + public ExitPlanModeHandler getOnExitPlanMode() { + return onExitPlanMode; + } + + /** + * Sets a handler for exit-plan-mode requests from the server. + *

+ * When provided, the server will route {@code exitPlanMode.request} callbacks + * to this handler. + * + * @param onExitPlanMode + * the exit-plan-mode handler + * @return this config instance for method chaining + * @see ExitPlanModeHandler + * @since 1.0.7 + */ + public ResumeSessionConfig setOnExitPlanMode(ExitPlanModeHandler onExitPlanMode) { + this.onExitPlanMode = onExitPlanMode; + return this; + } + + /** + * Gets the auto-mode-switch handler. + * + * @return the auto-mode-switch handler, or {@code null} + * @since 1.0.7 + */ + public AutoModeSwitchHandler getOnAutoModeSwitch() { + return onAutoModeSwitch; + } + + /** + * Sets a handler for auto-mode-switch requests from the server. + *

+ * When provided, the server will route {@code autoModeSwitch.request} callbacks + * to this handler. + * + * @param onAutoModeSwitch + * the auto-mode-switch handler + * @return this config instance for method chaining + * @see AutoModeSwitchHandler + * @since 1.0.7 + */ + public ResumeSessionConfig setOnAutoModeSwitch(AutoModeSwitchHandler onAutoModeSwitch) { + this.onAutoModeSwitch = onAutoModeSwitch; + return this; + } + /** * Gets the GitHub token for per-session authentication. * @@ -839,6 +895,8 @@ public ResumeSessionConfig clone() { copy.onEvent = this.onEvent; copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; + copy.onExitPlanMode = this.onExitPlanMode; + copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.gitHubToken = this.gitHubToken; return copy; } diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java index 9b2c17f1a..a1af26970 100644 --- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java +++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java @@ -116,6 +116,12 @@ public final class ResumeSessionRequest { @JsonProperty("requestElicitation") private Boolean requestElicitation; + @JsonProperty("requestExitPlanMode") + private Boolean requestExitPlanMode; + + @JsonProperty("requestAutoModeSwitch") + private Boolean requestAutoModeSwitch; + @JsonProperty("modelCapabilities") private ModelCapabilitiesOverride modelCapabilities; @@ -439,6 +445,28 @@ public void setRequestElicitation(Boolean requestElicitation) { this.requestElicitation = requestElicitation; } + /** Gets the requestExitPlanMode flag. @return the flag */ + public Boolean getRequestExitPlanMode() { + return requestExitPlanMode; + } + + /** Sets the requestExitPlanMode flag. @param requestExitPlanMode the flag */ + public void setRequestExitPlanMode(Boolean requestExitPlanMode) { + this.requestExitPlanMode = requestExitPlanMode; + } + + /** Gets the requestAutoModeSwitch flag. @return the flag */ + public Boolean getRequestAutoModeSwitch() { + return requestAutoModeSwitch; + } + + /** + * Sets the requestAutoModeSwitch flag. @param requestAutoModeSwitch the flag + */ + public void setRequestAutoModeSwitch(Boolean requestAutoModeSwitch) { + this.requestAutoModeSwitch = requestAutoModeSwitch; + } + /** Gets the model capabilities override. @return the override */ public ModelCapabilitiesOverride getModelCapabilities() { return modelCapabilities; diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java index a4b2769b7..c26c1b5ba 100644 --- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java +++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java @@ -66,6 +66,8 @@ public class SessionConfig { private Consumer onEvent; private List commands; private ElicitationHandler onElicitationRequest; + private ExitPlanModeHandler onExitPlanMode; + private AutoModeSwitchHandler onAutoModeSwitch; private String gitHubToken; /** @@ -818,6 +820,60 @@ public SessionConfig setOnElicitationRequest(ElicitationHandler onElicitationReq return this; } + /** + * Gets the exit-plan-mode handler. + * + * @return the exit-plan-mode handler, or {@code null} + * @since 1.0.7 + */ + public ExitPlanModeHandler getOnExitPlanMode() { + return onExitPlanMode; + } + + /** + * Sets a handler for exit-plan-mode requests from the server. + *

+ * When provided, the server will route {@code exitPlanMode.request} callbacks + * to this handler. + * + * @param onExitPlanMode + * the exit-plan-mode handler + * @return this config instance for method chaining + * @see ExitPlanModeHandler + * @since 1.0.7 + */ + public SessionConfig setOnExitPlanMode(ExitPlanModeHandler onExitPlanMode) { + this.onExitPlanMode = onExitPlanMode; + return this; + } + + /** + * Gets the auto-mode-switch handler. + * + * @return the auto-mode-switch handler, or {@code null} + * @since 1.0.7 + */ + public AutoModeSwitchHandler getOnAutoModeSwitch() { + return onAutoModeSwitch; + } + + /** + * Sets a handler for auto-mode-switch requests from the server. + *

+ * When provided, the server will route {@code autoModeSwitch.request} callbacks + * to this handler. + * + * @param onAutoModeSwitch + * the auto-mode-switch handler + * @return this config instance for method chaining + * @see AutoModeSwitchHandler + * @since 1.0.7 + */ + public SessionConfig setOnAutoModeSwitch(AutoModeSwitchHandler onAutoModeSwitch) { + this.onAutoModeSwitch = onAutoModeSwitch; + return this; + } + /** * Gets the GitHub token for per-session authentication. * @@ -891,6 +947,8 @@ public SessionConfig clone() { copy.onEvent = this.onEvent; copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; + copy.onExitPlanMode = this.onExitPlanMode; + copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.gitHubToken = this.gitHubToken; return copy; } diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java index f7ce3aa4d..b93e3c74f 100644 --- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java +++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java @@ -366,6 +366,34 @@ void resumeSessionConfigNewFieldsCloned() { assertSame(defaultAgent, cloned.getDefaultAgent()); } + @Test + void sessionConfigCloneCopiesModeSwitchHandlers() { + SessionConfig original = new SessionConfig(); + original.setOnExitPlanMode((request, invocation) -> CompletableFuture + .completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult())); + original.setOnAutoModeSwitch((request, invocation) -> CompletableFuture + .completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO)); + + SessionConfig cloned = original.clone(); + + assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode()); + assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch()); + } + + @Test + void resumeSessionConfigCloneCopiesModeSwitchHandlers() { + ResumeSessionConfig original = new ResumeSessionConfig(); + original.setOnExitPlanMode((request, invocation) -> CompletableFuture + .completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult())); + original.setOnAutoModeSwitch((request, invocation) -> CompletableFuture + .completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO)); + + ResumeSessionConfig cloned = original.clone(); + + assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode()); + assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch()); + } + @Test void copilotClientOptionsSessionIdleTimeoutCloned() { CopilotClientOptions original = new CopilotClientOptions(); diff --git a/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java b/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java index 847734b4a..6b941f77c 100644 --- a/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionHandlerTest.java @@ -341,4 +341,80 @@ void testRegisterToolsEmptyListClearsTools() { session.registerTools(List.of()); assertNull(session.getTool("my_tool")); } + + // ===== Exit Plan Mode handler ===== + + @Test + void testHandleExitPlanModeRequestNoHandler() throws Exception { + var request = new com.github.copilot.sdk.json.ExitPlanModeRequest().setSummary("Test plan"); + + var result = session.handleExitPlanModeRequest(request).get(); + + assertTrue(result.isApproved()); + } + + @Test + void testHandleExitPlanModeRequestWithHandler() throws Exception { + session.registerExitPlanModeHandler((req, inv) -> { + assertEquals("handler-test-session", inv.getSessionId()); + return CompletableFuture.completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult() + .setApproved(false).setSelectedAction("autopilot").setFeedback("Not yet")); + }); + var request = new com.github.copilot.sdk.json.ExitPlanModeRequest().setSummary("Test plan") + .setActions(List.of("interactive", "autopilot")).setRecommendedAction("interactive"); + + var result = session.handleExitPlanModeRequest(request).get(); + + assertFalse(result.isApproved()); + assertEquals("autopilot", result.getSelectedAction()); + assertEquals("Not yet", result.getFeedback()); + } + + @Test + void testHandleExitPlanModeRequestHandlerException() throws Exception { + session.registerExitPlanModeHandler((req, inv) -> CompletableFuture.failedFuture(new RuntimeException("fail"))); + var request = new com.github.copilot.sdk.json.ExitPlanModeRequest().setSummary("Plan"); + + var result = session.handleExitPlanModeRequest(request).get(); + + assertTrue(result.isApproved()); + } + + // ===== Auto Mode Switch handler ===== + + @Test + void testHandleAutoModeSwitchRequestNoHandler() throws Exception { + var request = new com.github.copilot.sdk.json.AutoModeSwitchRequest().setErrorCode("rate_limited"); + + var result = session.handleAutoModeSwitchRequest(request).get(); + + assertEquals(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO, result); + } + + @Test + void testHandleAutoModeSwitchRequestWithHandler() throws Exception { + session.registerAutoModeSwitchHandler((req, inv) -> { + assertEquals("handler-test-session", inv.getSessionId()); + assertEquals("user_weekly_rate_limited", req.getErrorCode()); + assertEquals(1.0, req.getRetryAfterSeconds()); + return CompletableFuture.completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.YES); + }); + var request = new com.github.copilot.sdk.json.AutoModeSwitchRequest().setErrorCode("user_weekly_rate_limited") + .setRetryAfterSeconds(1.0); + + var result = session.handleAutoModeSwitchRequest(request).get(); + + assertEquals(com.github.copilot.sdk.json.AutoModeSwitchResponse.YES, result); + } + + @Test + void testHandleAutoModeSwitchRequestHandlerException() throws Exception { + session.registerAutoModeSwitchHandler( + (req, inv) -> CompletableFuture.failedFuture(new RuntimeException("fail"))); + var request = new com.github.copilot.sdk.json.AutoModeSwitchRequest().setErrorCode("rate_limited"); + + var result = session.handleAutoModeSwitchRequest(request).get(); + + assertEquals(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO, result); + } } diff --git a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java index 0d13576eb..a0a4e2b1e 100644 --- a/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java +++ b/src/test/java/com/github/copilot/sdk/SessionRequestBuilderTest.java @@ -535,4 +535,144 @@ void testResumeRequestOmitsEnableSessionTelemetryWhenNull() throws Exception { var json = mapper.writeValueAsString(request); assertFalse(json.contains("enableSessionTelemetry"), "enableSessionTelemetry should be omitted when null"); } + + // ========================================================================= + // mode handler request flags + // ========================================================================= + + @Test + void testBuildCreateRequestSetsRequestExitPlanMode() { + var config = new SessionConfig().setOnExitPlanMode((request, invocation) -> CompletableFuture + .completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult())); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals(Boolean.TRUE, request.getRequestExitPlanMode()); + } + + @Test + void testBuildCreateRequestSetsRequestAutoModeSwitch() { + var config = new SessionConfig().setOnAutoModeSwitch((request, invocation) -> CompletableFuture + .completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO)); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertEquals(Boolean.TRUE, request.getRequestAutoModeSwitch()); + } + + @Test + void testBuildCreateRequestOmitsModeRequestFlagsWhenNull() { + var config = new SessionConfig(); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + + assertNull(request.getRequestExitPlanMode()); + assertNull(request.getRequestAutoModeSwitch()); + } + + @Test + void testBuildResumeRequestSetsRequestExitPlanMode() { + var config = new ResumeSessionConfig().setOnExitPlanMode((request, invocation) -> CompletableFuture + .completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult())); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("test-session", config); + + assertEquals(Boolean.TRUE, request.getRequestExitPlanMode()); + } + + @Test + void testBuildResumeRequestSetsRequestAutoModeSwitch() { + var config = new ResumeSessionConfig().setOnAutoModeSwitch((request, invocation) -> CompletableFuture + .completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO)); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("test-session", config); + + assertEquals(Boolean.TRUE, request.getRequestAutoModeSwitch()); + } + + @Test + void configureSessionWithExitPlanModeHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new SessionConfig().setOnExitPlanMode((request, invocation) -> CompletableFuture + .completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult())); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureSessionWithAutoModeSwitchHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new SessionConfig().setOnAutoModeSwitch((request, invocation) -> CompletableFuture + .completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO)); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithExitPlanModeHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new ResumeSessionConfig().setOnExitPlanMode((request, invocation) -> CompletableFuture + .completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult())); + + SessionRequestBuilder.configureSession(session, config); + } + + @Test + void configureResumedSessionWithAutoModeSwitchHandler_registersHandler() { + CopilotSession session = new CopilotSession("session-1", null); + + var config = new ResumeSessionConfig().setOnAutoModeSwitch((request, invocation) -> CompletableFuture + .completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO)); + + SessionRequestBuilder.configureSession(session, config); + } + + // ========================================================================= + // mode request flags serialization + // ========================================================================= + + @Test + void testCreateRequestSerializesModeRequestFlags() throws Exception { + var config = new SessionConfig() + .setOnExitPlanMode((request, invocation) -> CompletableFuture + .completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult())) + .setOnAutoModeSwitch((request, invocation) -> CompletableFuture + .completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO)); + + CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + + assertTrue(json.contains("\"requestExitPlanMode\":true")); + assertTrue(json.contains("\"requestAutoModeSwitch\":true")); + } + + @Test + void testResumeRequestSerializesModeRequestFlags() throws Exception { + var config = new ResumeSessionConfig() + .setOnExitPlanMode((request, invocation) -> CompletableFuture + .completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult())) + .setOnAutoModeSwitch((request, invocation) -> CompletableFuture + .completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO)); + + ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("test-session", config); + var mapper = JsonRpcClient.getObjectMapper(); + var json = mapper.writeValueAsString(request); + + assertTrue(json.contains("\"requestExitPlanMode\":true")); + assertTrue(json.contains("\"requestAutoModeSwitch\":true")); + } + + @Test + void testAutoModeSwitchResponseSerialization() throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + + assertEquals("\"yes\"", mapper.writeValueAsString(com.github.copilot.sdk.json.AutoModeSwitchResponse.YES)); + assertEquals("\"yes_always\"", + mapper.writeValueAsString(com.github.copilot.sdk.json.AutoModeSwitchResponse.YES_ALWAYS)); + assertEquals("\"no\"", mapper.writeValueAsString(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO)); + } }