Skip to content

Commit a23357e

Browse files
Copilotedburns
andauthored
Port exit-plan-mode and auto-mode-switch handler APIs from reference implementation
Adds ExitPlanModeHandler and AutoModeSwitchHandler support matching the reference implementation commit 671b50a (Restore mode handler APIs across SDKs). New types: - ExitPlanModeHandler, ExitPlanModeRequest, ExitPlanModeResult, ExitPlanModeInvocation - AutoModeSwitchHandler, AutoModeSwitchRequest, AutoModeSwitchResponse, AutoModeSwitchInvocation Updated: - SessionConfig/ResumeSessionConfig: onExitPlanMode, onAutoModeSwitch fields - CreateSessionRequest/ResumeSessionRequest: requestExitPlanMode, requestAutoModeSwitch flags - CopilotSession: handler registration and dispatch methods - RpcHandlerDispatcher: exitPlanMode.request, autoModeSwitch.request handlers - SessionRequestBuilder: wiring for new handlers and capability flags Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent 0614c8f commit a23357e

15 files changed

Lines changed: 866 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
@@ -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;
@@ -62,6 +66,10 @@
6266
import com.github.copilot.sdk.json.ElicitationParams;
6367
import com.github.copilot.sdk.json.ElicitationResult;
6468
import com.github.copilot.sdk.json.ElicitationResultAction;
69+
import com.github.copilot.sdk.json.ExitPlanModeHandler;
70+
import com.github.copilot.sdk.json.ExitPlanModeInvocation;
71+
import com.github.copilot.sdk.json.ExitPlanModeRequest;
72+
import com.github.copilot.sdk.json.ExitPlanModeResult;
6573
import com.github.copilot.sdk.json.ElicitationSchema;
6674
import com.github.copilot.sdk.json.GetMessagesResponse;
6775
import com.github.copilot.sdk.json.HookInvocation;
@@ -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 sends an {@code exitPlanMode.request}.
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 sends an {@code autoModeSwitch.request}.
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+
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 ExitPlanModeRequest();
304+
if (params.has("summary")) {
305+
request.setSummary(params.get("summary").asText());
306+
}
307+
if (params.has("planContent") && !params.get("planContent").isNull()) {
308+
request.setPlanContent(params.get("planContent").asText());
309+
}
310+
if (params.has("actions") && params.get("actions").isArray()) {
311+
var actions = new ArrayList<String>();
312+
for (JsonNode action : params.get("actions")) {
313+
actions.add(action.asText());
314+
}
315+
request.setActions(actions);
316+
}
317+
if (params.has("recommendedAction") && !params.get("recommendedAction").isNull()) {
318+
request.setRecommendedAction(params.get("recommendedAction").asText());
319+
}
320+
321+
session.handleExitPlanModeRequest(request).thenAccept(result -> {
322+
try {
323+
rpc.sendResponse(Long.parseLong(requestId), MAPPER.valueToTree(result));
324+
} catch (IOException e) {
325+
LOG.log(Level.SEVERE, "Error sending exit plan mode response", e);
326+
}
327+
}).exceptionally(ex -> {
328+
try {
329+
rpc.sendErrorResponse(Long.parseLong(requestId), -32603,
330+
"Exit plan mode handler error: " + ex.getMessage());
331+
} catch (IOException e) {
332+
LOG.log(Level.SEVERE, "Error sending exit plan mode error", e);
333+
}
334+
return null;
335+
});
336+
} catch (Exception e) {
337+
LOG.log(Level.SEVERE, "Error handling exit plan mode request", e);
338+
}
339+
});
340+
}
341+
342+
private void handleAutoModeSwitchRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
343+
runAsync(() -> {
344+
try {
345+
String sessionId = params.get("sessionId").asText();
346+
347+
CopilotSession session = sessions.get(sessionId);
348+
if (session == null) {
349+
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
350+
return;
351+
}
352+
353+
var request = new AutoModeSwitchRequest();
354+
if (params.has("errorCode") && !params.get("errorCode").isNull()) {
355+
request.setErrorCode(params.get("errorCode").asText());
356+
}
357+
if (params.has("retryAfterSeconds") && !params.get("retryAfterSeconds").isNull()) {
358+
request.setRetryAfterSeconds(params.get("retryAfterSeconds").asDouble());
359+
}
360+
361+
session.handleAutoModeSwitchRequest(request).thenAccept(response -> {
362+
try {
363+
rpc.sendResponse(Long.parseLong(requestId), 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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
* Register an auto-mode-switch handler via
13+
* {@link SessionConfig#setOnAutoModeSwitch(AutoModeSwitchHandler)} or
14+
* {@link ResumeSessionConfig#setOnAutoModeSwitch(AutoModeSwitchHandler)}. When
15+
* provided, the server routes {@code autoModeSwitch.request} callbacks to this
16+
* handler.
17+
*
18+
* <h2>Example Usage</h2>
19+
*
20+
* <pre>{@code
21+
* AutoModeSwitchHandler handler = (request, invocation) -> {
22+
* System.out.println("Rate limited: " + request.getErrorCode());
23+
* return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES);
24+
* };
25+
*
26+
* var session = client.createSession(new SessionConfig().setOnAutoModeSwitch(handler)).get();
27+
* }</pre>
28+
*
29+
* @see AutoModeSwitchRequest
30+
* @see AutoModeSwitchResponse
31+
* @since 1.0.8
32+
*/
33+
@FunctionalInterface
34+
public interface AutoModeSwitchHandler {
35+
36+
/**
37+
* Handles an auto-mode-switch request from the agent.
38+
*
39+
* @param request
40+
* the auto-mode-switch request containing the error code and
41+
* retry-after seconds
42+
* @param invocation
43+
* context information about the invocation
44+
* @return a future that resolves with the user's decision
45+
*/
46+
CompletableFuture<AutoModeSwitchResponse> handle(AutoModeSwitchRequest request,
47+
AutoModeSwitchInvocation invocation);
48+
}
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.0.8
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)