Skip to content

Commit 5d7a410

Browse files
Copilotedburns
andauthored
Port ExitPlanMode and AutoModeSwitch handler APIs from reference implementation
Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent 229e2f0 commit 5d7a410

15 files changed

Lines changed: 862 additions & 0 deletions

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@
5454
import com.github.copilot.sdk.generated.SessionEvent;
5555
import com.github.copilot.sdk.generated.SessionIdleEvent;
5656
import com.github.copilot.sdk.json.AgentInfo;
57+
import com.github.copilot.sdk.json.AutoModeSwitchHandler;
58+
import com.github.copilot.sdk.json.AutoModeSwitchInvocation;
59+
import com.github.copilot.sdk.json.AutoModeSwitchRequest;
60+
import com.github.copilot.sdk.json.AutoModeSwitchResponse;
5761
import com.github.copilot.sdk.json.CommandContext;
5862
import com.github.copilot.sdk.json.CommandDefinition;
5963
import com.github.copilot.sdk.json.CommandHandler;
@@ -63,6 +67,10 @@
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 exit-plan-mode result
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+
return new ExitPlanModeResult().setApproved(true);
1415+
});
1416+
} catch (Exception e) {
1417+
LOG.log(Level.SEVERE, "Failed to process exit plan mode request", e);
1418+
return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true));
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 mode.
1426+
*
1427+
* @param request
1428+
* the auto-mode-switch request
1429+
* @return a future that resolves with the auto-mode-switch response
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+
return AutoModeSwitchResponse.NO;
1442+
});
1443+
} catch (Exception e) {
1444+
LOG.log(Level.SEVERE, "Failed to process auto mode switch request", e);
1445+
return CompletableFuture.completedFuture(AutoModeSwitchResponse.NO);
1446+
}
1447+
}
1448+
13591449
/**
13601450
* Registers hook handlers for this session.
13611451
* <p>

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ void registerHandlers(JsonRpcClient rpc) {
7979
(requestId, params) -> handlePermissionRequest(rpc, requestId, params));
8080
rpc.registerMethodHandler("userInput.request",
8181
(requestId, params) -> handleUserInputRequest(rpc, requestId, params));
82+
rpc.registerMethodHandler("exitPlanMode.request",
83+
(requestId, params) -> handleExitPlanModeRequest(rpc, requestId, params));
84+
rpc.registerMethodHandler("autoModeSwitch.request",
85+
(requestId, params) -> handleAutoModeSwitchRequest(rpc, requestId, params));
8286
rpc.registerMethodHandler("hooks.invoke", (requestId, params) -> handleHooksInvoke(rpc, requestId, params));
8387
rpc.registerMethodHandler("systemMessage.transform",
8488
(requestId, params) -> handleSystemMessageTransform(rpc, requestId, params));
@@ -283,6 +287,110 @@ private void handleUserInputRequest(JsonRpcClient rpc, String requestId, JsonNod
283287
});
284288
}
285289

290+
private void handleExitPlanModeRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
291+
LOG.fine("Received exitPlanMode.request: " + params);
292+
runAsync(() -> {
293+
try {
294+
String sessionId = params.get("sessionId").asText();
295+
String summary = params.has("summary") ? params.get("summary").asText() : "";
296+
297+
CopilotSession session = sessions.get(sessionId);
298+
if (session == null) {
299+
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
300+
return;
301+
}
302+
303+
var request = new com.github.copilot.sdk.json.ExitPlanModeRequest().setSummary(summary);
304+
JsonNode planContentNode = params.get("planContent");
305+
if (planContentNode != null && !planContentNode.isNull()) {
306+
request.setPlanContent(planContentNode.asText());
307+
}
308+
JsonNode actionsNode = params.get("actions");
309+
if (actionsNode != null && actionsNode.isArray()) {
310+
var actions = new ArrayList<String>();
311+
for (JsonNode action : actionsNode) {
312+
actions.add(action.asText());
313+
}
314+
request.setActions(actions);
315+
}
316+
JsonNode recommendedActionNode = params.get("recommendedAction");
317+
if (recommendedActionNode != null && !recommendedActionNode.isNull()) {
318+
request.setRecommendedAction(recommendedActionNode.asText());
319+
}
320+
321+
session.handleExitPlanModeRequest(request).thenAccept(response -> {
322+
try {
323+
var result = new java.util.HashMap<String, Object>();
324+
result.put("approved", response.isApproved());
325+
if (response.getSelectedAction() != null) {
326+
result.put("selectedAction", response.getSelectedAction());
327+
}
328+
if (response.getFeedback() != null) {
329+
result.put("feedback", response.getFeedback());
330+
}
331+
rpc.sendResponse(Long.parseLong(requestId), result);
332+
} catch (IOException e) {
333+
LOG.log(Level.SEVERE, "Error sending exit plan mode response", e);
334+
}
335+
}).exceptionally(ex -> {
336+
LOG.log(Level.WARNING, "Exit plan mode handler exception", ex);
337+
try {
338+
rpc.sendResponse(Long.parseLong(requestId), Map.of("approved", true));
339+
} catch (IOException e) {
340+
LOG.log(Level.SEVERE, "Error sending exit plan mode fallback response", e);
341+
}
342+
return null;
343+
});
344+
} catch (Exception e) {
345+
LOG.log(Level.SEVERE, "Error handling exit plan mode request", e);
346+
}
347+
});
348+
}
349+
350+
private void handleAutoModeSwitchRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
351+
LOG.fine("Received autoModeSwitch.request: " + params);
352+
runAsync(() -> {
353+
try {
354+
String sessionId = params.get("sessionId").asText();
355+
356+
CopilotSession session = sessions.get(sessionId);
357+
if (session == null) {
358+
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
359+
return;
360+
}
361+
362+
var request = new com.github.copilot.sdk.json.AutoModeSwitchRequest();
363+
JsonNode errorCodeNode = params.get("errorCode");
364+
if (errorCodeNode != null && !errorCodeNode.isNull()) {
365+
request.setErrorCode(errorCodeNode.asText());
366+
}
367+
JsonNode retryAfterNode = params.get("retryAfterSeconds");
368+
if (retryAfterNode != null && !retryAfterNode.isNull()) {
369+
request.setRetryAfterSeconds(retryAfterNode.asDouble());
370+
}
371+
372+
session.handleAutoModeSwitchRequest(request).thenAccept(response -> {
373+
try {
374+
rpc.sendResponse(Long.parseLong(requestId), Map.of("response", response.getValue()));
375+
} catch (IOException e) {
376+
LOG.log(Level.SEVERE, "Error sending auto mode switch response", e);
377+
}
378+
}).exceptionally(ex -> {
379+
LOG.log(Level.WARNING, "Auto mode switch handler exception", ex);
380+
try {
381+
rpc.sendResponse(Long.parseLong(requestId),
382+
Map.of("response", com.github.copilot.sdk.json.AutoModeSwitchResponse.NO.getValue()));
383+
} catch (IOException e) {
384+
LOG.log(Level.SEVERE, "Error sending auto mode switch fallback response", e);
385+
}
386+
return null;
387+
});
388+
} catch (Exception e) {
389+
LOG.log(Level.SEVERE, "Error handling auto mode switch request", e);
390+
}
391+
});
392+
}
393+
286394
private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode params) {
287395
runAsync(() -> {
288396
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 when the agent encounters a rate
13+
* limit and wants to switch to a different model automatically.
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 error code and retry-after
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)