Skip to content
This repository was archived by the owner on Jun 17, 2026. It is now read-only.

Commit b01df0a

Browse files
Copilotedburns
andauthored
Port exit-plan-mode and auto-mode-switch handler APIs from reference implementation
Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent 4f3f3a5 commit b01df0a

15 files changed

Lines changed: 876 additions & 0 deletions

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,20 @@
5757
import com.github.copilot.sdk.json.CommandContext;
5858
import com.github.copilot.sdk.json.CommandDefinition;
5959
import com.github.copilot.sdk.json.CommandHandler;
60+
import com.github.copilot.sdk.json.AutoModeSwitchHandler;
61+
import com.github.copilot.sdk.json.AutoModeSwitchInvocation;
62+
import com.github.copilot.sdk.json.AutoModeSwitchRequest;
63+
import com.github.copilot.sdk.json.AutoModeSwitchResponse;
6064
import com.github.copilot.sdk.json.ElicitationContext;
6165
import com.github.copilot.sdk.json.ElicitationHandler;
6266
import com.github.copilot.sdk.json.ElicitationParams;
6367
import com.github.copilot.sdk.json.ElicitationResult;
6468
import com.github.copilot.sdk.json.ElicitationResultAction;
6569
import com.github.copilot.sdk.json.ElicitationSchema;
70+
import com.github.copilot.sdk.json.ExitPlanModeHandler;
71+
import com.github.copilot.sdk.json.ExitPlanModeInvocation;
72+
import com.github.copilot.sdk.json.ExitPlanModeRequest;
73+
import com.github.copilot.sdk.json.ExitPlanModeResult;
6674
import com.github.copilot.sdk.json.GetMessagesResponse;
6775
import com.github.copilot.sdk.json.HookInvocation;
6876
import com.github.copilot.sdk.json.InputOptions;
@@ -156,6 +164,8 @@ public final class CopilotSession implements AutoCloseable {
156164
private final AtomicReference<PermissionHandler> permissionHandler = new AtomicReference<>();
157165
private final AtomicReference<UserInputHandler> userInputHandler = new AtomicReference<>();
158166
private final AtomicReference<ElicitationHandler> elicitationHandler = new AtomicReference<>();
167+
private final AtomicReference<ExitPlanModeHandler> exitPlanModeHandler = new AtomicReference<>();
168+
private final AtomicReference<AutoModeSwitchHandler> autoModeSwitchHandler = new AtomicReference<>();
159169
private final AtomicReference<SessionHooks> hooksHandler = new AtomicReference<>();
160170
private volatile EventErrorHandler eventErrorHandler;
161171
private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS;
@@ -1317,6 +1327,32 @@ void registerElicitationHandler(ElicitationHandler handler) {
13171327
elicitationHandler.set(handler);
13181328
}
13191329

1330+
/**
1331+
* Registers an exit-plan-mode handler for this session.
1332+
* <p>
1333+
* Called internally when creating or resuming a session with an exit-plan-mode
1334+
* handler.
1335+
*
1336+
* @param handler
1337+
* the handler to invoke when an exit-plan-mode request is received
1338+
*/
1339+
void registerExitPlanModeHandler(ExitPlanModeHandler handler) {
1340+
exitPlanModeHandler.set(handler);
1341+
}
1342+
1343+
/**
1344+
* Registers an auto-mode-switch handler for this session.
1345+
* <p>
1346+
* Called internally when creating or resuming a session with an
1347+
* auto-mode-switch handler.
1348+
*
1349+
* @param handler
1350+
* the handler to invoke when an auto-mode-switch request is received
1351+
*/
1352+
void registerAutoModeSwitchHandler(AutoModeSwitchHandler handler) {
1353+
autoModeSwitchHandler.set(handler);
1354+
}
1355+
13201356
/**
13211357
* Sets the capabilities reported by the host for this session.
13221358
* <p>
@@ -1356,6 +1392,60 @@ CompletableFuture<UserInputResponse> handleUserInputRequest(UserInputRequest req
13561392
}
13571393
}
13581394

1395+
/**
1396+
* Handles an exit-plan-mode request from the Copilot CLI.
1397+
* <p>
1398+
* Called internally when the server requests to exit plan mode.
1399+
*
1400+
* @param request
1401+
* the exit-plan-mode request
1402+
* @return a future that resolves with the user's decision
1403+
*/
1404+
CompletableFuture<ExitPlanModeResult> handleExitPlanModeRequest(ExitPlanModeRequest request) {
1405+
ExitPlanModeHandler handler = exitPlanModeHandler.get();
1406+
if (handler == null) {
1407+
return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true));
1408+
}
1409+
1410+
try {
1411+
var invocation = new ExitPlanModeInvocation().setSessionId(sessionId);
1412+
return handler.handle(request, invocation).exceptionally(ex -> {
1413+
LOG.log(Level.SEVERE, "Exit plan mode handler threw an exception", ex);
1414+
throw new RuntimeException("Exit plan mode handler error", ex);
1415+
});
1416+
} catch (Exception e) {
1417+
LOG.log(Level.SEVERE, "Failed to process exit plan mode request", e);
1418+
return CompletableFuture.failedFuture(e);
1419+
}
1420+
}
1421+
1422+
/**
1423+
* Handles an auto-mode-switch request from the Copilot CLI.
1424+
* <p>
1425+
* Called internally when the server requests to switch to auto mode.
1426+
*
1427+
* @param request
1428+
* the auto-mode-switch request
1429+
* @return a future that resolves with the user's decision
1430+
*/
1431+
CompletableFuture<AutoModeSwitchResponse> handleAutoModeSwitchRequest(AutoModeSwitchRequest request) {
1432+
AutoModeSwitchHandler handler = autoModeSwitchHandler.get();
1433+
if (handler == null) {
1434+
return CompletableFuture.completedFuture(AutoModeSwitchResponse.NO);
1435+
}
1436+
1437+
try {
1438+
var invocation = new AutoModeSwitchInvocation().setSessionId(sessionId);
1439+
return handler.handle(request, invocation).exceptionally(ex -> {
1440+
LOG.log(Level.SEVERE, "Auto mode switch handler threw an exception", ex);
1441+
throw new RuntimeException("Auto mode switch handler error", ex);
1442+
});
1443+
} catch (Exception e) {
1444+
LOG.log(Level.SEVERE, "Failed to process auto mode switch request", e);
1445+
return CompletableFuture.failedFuture(e);
1446+
}
1447+
}
1448+
13591449
/**
13601450
* Registers hook handlers for this session.
13611451
* <p>
@@ -1850,6 +1940,8 @@ public void close() {
18501940
permissionHandler.set(null);
18511941
userInputHandler.set(null);
18521942
elicitationHandler.set(null);
1943+
exitPlanModeHandler.set(null);
1944+
autoModeSwitchHandler.set(null);
18531945
hooksHandler.set(null);
18541946
}
18551947

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

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import com.fasterxml.jackson.databind.JsonNode;
1818
import com.fasterxml.jackson.databind.ObjectMapper;
1919
import com.github.copilot.sdk.generated.SessionEvent;
20+
import com.github.copilot.sdk.json.AutoModeSwitchRequest;
21+
import com.github.copilot.sdk.json.ExitPlanModeRequest;
2022
import com.github.copilot.sdk.json.PermissionRequestResult;
2123
import com.github.copilot.sdk.json.PermissionRequestResultKind;
2224
import com.github.copilot.sdk.json.SessionLifecycleEvent;
@@ -79,6 +81,10 @@ void registerHandlers(JsonRpcClient rpc) {
7981
(requestId, params) -> handlePermissionRequest(rpc, requestId, params));
8082
rpc.registerMethodHandler("userInput.request",
8183
(requestId, params) -> handleUserInputRequest(rpc, requestId, params));
84+
rpc.registerMethodHandler("exitPlanMode.request",
85+
(requestId, params) -> handleExitPlanModeRequest(rpc, requestId, params));
86+
rpc.registerMethodHandler("autoModeSwitch.request",
87+
(requestId, params) -> handleAutoModeSwitchRequest(rpc, requestId, params));
8288
rpc.registerMethodHandler("hooks.invoke", (requestId, params) -> handleHooksInvoke(rpc, requestId, params));
8389
rpc.registerMethodHandler("systemMessage.transform",
8490
(requestId, params) -> handleSystemMessageTransform(rpc, requestId, params));
@@ -283,6 +289,96 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod
283289
});
284290
}
285291

292+
private void handleExitPlanModeRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
293+
runAsync(() -> {
294+
try {
295+
String sessionId = params.get("sessionId").asText();
296+
String summary = params.has("summary") ? params.get("summary").asText() : "";
297+
String planContent = params.has("planContent") && !params.get("planContent").isNull()
298+
? params.get("planContent").asText()
299+
: null;
300+
String recommendedAction = params.has("recommendedAction")
301+
? params.get("recommendedAction").asText()
302+
: "autopilot";
303+
304+
var actions = new ArrayList<String>();
305+
if (params.has("actions") && params.get("actions").isArray()) {
306+
for (JsonNode action : params.get("actions")) {
307+
actions.add(action.asText());
308+
}
309+
}
310+
311+
CopilotSession session = sessions.get(sessionId);
312+
if (session == null) {
313+
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
314+
return;
315+
}
316+
317+
var request = new ExitPlanModeRequest().setSummary(summary).setPlanContent(planContent)
318+
.setActions(actions).setRecommendedAction(recommendedAction);
319+
320+
session.handleExitPlanModeRequest(request).thenAccept(result -> {
321+
try {
322+
rpc.sendResponse(Long.parseLong(requestId), result);
323+
} catch (IOException e) {
324+
LOG.log(Level.SEVERE, "Error sending exit plan mode response", e);
325+
}
326+
}).exceptionally(ex -> {
327+
try {
328+
rpc.sendErrorResponse(Long.parseLong(requestId), -32603,
329+
"Exit plan mode handler error: " + ex.getMessage());
330+
} catch (IOException e) {
331+
LOG.log(Level.SEVERE, "Error sending exit plan mode error", e);
332+
}
333+
return null;
334+
});
335+
} catch (Exception e) {
336+
LOG.log(Level.SEVERE, "Error handling exit plan mode request", e);
337+
}
338+
});
339+
}
340+
341+
private void handleAutoModeSwitchRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
342+
runAsync(() -> {
343+
try {
344+
String sessionId = params.get("sessionId").asText();
345+
String errorCode = params.has("errorCode") && !params.get("errorCode").isNull()
346+
? params.get("errorCode").asText()
347+
: null;
348+
Double retryAfterSeconds = params.has("retryAfterSeconds") && !params.get("retryAfterSeconds").isNull()
349+
? params.get("retryAfterSeconds").asDouble()
350+
: null;
351+
352+
CopilotSession session = sessions.get(sessionId);
353+
if (session == null) {
354+
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
355+
return;
356+
}
357+
358+
var request = new AutoModeSwitchRequest().setErrorCode(errorCode)
359+
.setRetryAfterSeconds(retryAfterSeconds);
360+
361+
session.handleAutoModeSwitchRequest(request).thenAccept(response -> {
362+
try {
363+
rpc.sendResponse(Long.parseLong(requestId), java.util.Map.of("response", response));
364+
} catch (IOException e) {
365+
LOG.log(Level.SEVERE, "Error sending auto mode switch response", e);
366+
}
367+
}).exceptionally(ex -> {
368+
try {
369+
rpc.sendErrorResponse(Long.parseLong(requestId), -32603,
370+
"Auto mode switch handler error: " + ex.getMessage());
371+
} catch (IOException e) {
372+
LOG.log(Level.SEVERE, "Error sending auto mode switch error", e);
373+
}
374+
return null;
375+
});
376+
} catch (Exception e) {
377+
LOG.log(Level.SEVERE, "Error handling auto mode switch request", e);
378+
}
379+
});
380+
}
381+
286382
private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode params) {
287383
runAsync(() -> {
288384
try {

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,12 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
138138
if (config.getOnElicitationRequest() != null) {
139139
request.setRequestElicitation(true);
140140
}
141+
if (config.getOnExitPlanMode() != null) {
142+
request.setRequestExitPlanMode(true);
143+
}
144+
if (config.getOnAutoModeSwitch() != null) {
145+
request.setRequestAutoModeSwitch(true);
146+
}
141147
request.setGitHubToken(config.getGitHubToken());
142148

143149
return request;
@@ -216,6 +222,12 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
216222
if (config.getOnElicitationRequest() != null) {
217223
request.setRequestElicitation(true);
218224
}
225+
if (config.getOnExitPlanMode() != null) {
226+
request.setRequestExitPlanMode(true);
227+
}
228+
if (config.getOnAutoModeSwitch() != null) {
229+
request.setRequestAutoModeSwitch(true);
230+
}
219231
request.setGitHubToken(config.getGitHubToken());
220232

221233
return request;
@@ -252,6 +264,12 @@ static void configureSession(CopilotSession session, SessionConfig config) {
252264
if (config.getOnElicitationRequest() != null) {
253265
session.registerElicitationHandler(config.getOnElicitationRequest());
254266
}
267+
if (config.getOnExitPlanMode() != null) {
268+
session.registerExitPlanModeHandler(config.getOnExitPlanMode());
269+
}
270+
if (config.getOnAutoModeSwitch() != null) {
271+
session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch());
272+
}
255273
if (config.getOnEvent() != null) {
256274
session.on(config.getOnEvent());
257275
}
@@ -288,6 +306,12 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config)
288306
if (config.getOnElicitationRequest() != null) {
289307
session.registerElicitationHandler(config.getOnElicitationRequest());
290308
}
309+
if (config.getOnExitPlanMode() != null) {
310+
session.registerExitPlanModeHandler(config.getOnExitPlanMode());
311+
}
312+
if (config.getOnAutoModeSwitch() != null) {
313+
session.registerAutoModeSwitchHandler(config.getOnAutoModeSwitch());
314+
}
291315
if (config.getOnEvent() != null) {
292316
session.on(config.getOnEvent());
293317
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk.json;
6+
7+
import java.util.concurrent.CompletableFuture;
8+
9+
/**
10+
* Handler for auto-mode-switch requests from the agent.
11+
* <p>
12+
* Implement this interface to handle requests from the agent to switch to an
13+
* alternative model after a rate limit is encountered.
14+
*
15+
* <h2>Example Usage</h2>
16+
*
17+
* <pre>{@code
18+
* AutoModeSwitchHandler handler = (request, invocation) -> {
19+
* System.out.println("Rate limited: " + request.getErrorCode());
20+
* return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES);
21+
* };
22+
*
23+
* var session = client.createSession(new SessionConfig().setOnAutoModeSwitch(handler)).get();
24+
* }</pre>
25+
*
26+
* @since 1.4.0
27+
*/
28+
@FunctionalInterface
29+
public interface AutoModeSwitchHandler {
30+
31+
/**
32+
* Handles an auto-mode-switch request from the agent.
33+
*
34+
* @param request
35+
* the auto-mode-switch request containing the error code and retry
36+
* information
37+
* @param invocation
38+
* context information about the invocation
39+
* @return a future that resolves with the user's decision
40+
*/
41+
CompletableFuture<AutoModeSwitchResponse> handle(AutoModeSwitchRequest request,
42+
AutoModeSwitchInvocation invocation);
43+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk.json;
6+
7+
/**
8+
* Context for an auto-mode-switch request invocation.
9+
*
10+
* @since 1.4.0
11+
*/
12+
public class AutoModeSwitchInvocation {
13+
14+
private String sessionId;
15+
16+
/**
17+
* Gets the session ID.
18+
*
19+
* @return the session ID
20+
*/
21+
public String getSessionId() {
22+
return sessionId;
23+
}
24+
25+
/**
26+
* Sets the session ID.
27+
*
28+
* @param sessionId
29+
* the session ID
30+
* @return this instance for method chaining
31+
*/
32+
public AutoModeSwitchInvocation setSessionId(String sessionId) {
33+
this.sessionId = sessionId;
34+
return this;
35+
}
36+
}

0 commit comments

Comments
 (0)