Skip to content

Commit 0f2be9d

Browse files
Copilotedburns
andauthored
Port mode handler APIs (ExitPlanMode, AutoModeSwitch) and nullable ModelBilling.multiplier from the reference implementation
Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent 59de244 commit 0f2be9d

16 files changed

Lines changed: 765 additions & 3 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;
@@ -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 the user 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 to auto 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>
@@ -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: 94 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,94 @@ 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+
request.setSummary(params.has("summary") ? params.get("summary").asText() : "");
305+
if (params.has("planContent") && !params.get("planContent").isNull()) {
306+
request.setPlanContent(params.get("planContent").asText());
307+
}
308+
if (params.has("actions") && params.get("actions").isArray()) {
309+
var actions = new ArrayList<String>();
310+
for (JsonNode action : params.get("actions")) {
311+
actions.add(action.asText());
312+
}
313+
request.setActions(actions);
314+
}
315+
if (params.has("recommendedAction") && !params.get("recommendedAction").isNull()) {
316+
request.setRecommendedAction(params.get("recommendedAction").asText());
317+
}
318+
319+
session.handleExitPlanModeRequest(request).thenAccept(result -> {
320+
try {
321+
rpc.sendResponse(Long.parseLong(requestId), result);
322+
} catch (IOException e) {
323+
LOG.log(Level.SEVERE, "Error sending exit plan mode response", e);
324+
}
325+
}).exceptionally(ex -> {
326+
try {
327+
rpc.sendErrorResponse(Long.parseLong(requestId), -32603,
328+
"Exit plan mode handler error: " + ex.getMessage());
329+
} catch (IOException e) {
330+
LOG.log(Level.SEVERE, "Error sending exit plan mode error", e);
331+
}
332+
return null;
333+
});
334+
} catch (Exception e) {
335+
LOG.log(Level.SEVERE, "Error handling exit plan mode request", e);
336+
}
337+
});
338+
}
339+
340+
private void handleAutoModeSwitchRequest(JsonRpcClient rpc, String requestId, JsonNode params) {
341+
runAsync(() -> {
342+
try {
343+
String sessionId = params.get("sessionId").asText();
344+
345+
CopilotSession session = sessions.get(sessionId);
346+
if (session == null) {
347+
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session " + sessionId);
348+
return;
349+
}
350+
351+
var request = new AutoModeSwitchRequest();
352+
if (params.has("errorCode") && !params.get("errorCode").isNull()) {
353+
request.setErrorCode(params.get("errorCode").asText());
354+
}
355+
if (params.has("retryAfterSeconds") && !params.get("retryAfterSeconds").isNull()) {
356+
request.setRetryAfterSeconds(params.get("retryAfterSeconds").asDouble());
357+
}
358+
359+
session.handleAutoModeSwitchRequest(request).thenAccept(response -> {
360+
try {
361+
rpc.sendResponse(Long.parseLong(requestId), java.util.Map.of("response", response.getValue()));
362+
} catch (IOException e) {
363+
LOG.log(Level.SEVERE, "Error sending auto mode switch response", e);
364+
}
365+
}).exceptionally(ex -> {
366+
try {
367+
rpc.sendErrorResponse(Long.parseLong(requestId), -32603,
368+
"Auto mode switch handler error: " + ex.getMessage());
369+
} catch (IOException e) {
370+
LOG.log(Level.SEVERE, "Error sending auto mode switch error", e);
371+
}
372+
return null;
373+
});
374+
} catch (Exception e) {
375+
LOG.log(Level.SEVERE, "Error handling auto mode switch request", e);
376+
}
377+
});
378+
}
379+
286380
private void handleHooksInvoke(JsonRpcClient rpc, String requestId, JsonNode params) {
287381
runAsync(() -> {
288382
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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 to switch to auto mode when a
13+
* rate limit is encountered. The handler decides whether to approve the switch.
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(
24+
* new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnAutoModeSwitch(handler))
25+
* .get();
26+
* }</pre>
27+
*
28+
* @since 1.0.7
29+
*/
30+
@FunctionalInterface
31+
public interface AutoModeSwitchHandler {
32+
33+
/**
34+
* Handles an auto-mode-switch request from the agent.
35+
*
36+
* @param request
37+
* the auto-mode-switch request containing the error code and
38+
* retry-after information
39+
* @param invocation
40+
* context information about the invocation
41+
* @return a future that resolves with the user's decision
42+
*/
43+
CompletableFuture<AutoModeSwitchResponse> handle(AutoModeSwitchRequest request,
44+
AutoModeSwitchInvocation invocation);
45+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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.7
11+
*/
12+
public class AutoModeSwitchInvocation {
13+
14+
private String sessionId = "";
15+
16+
/** Gets the session ID that triggered the request. @return the session ID */
17+
public String getSessionId() {
18+
return sessionId;
19+
}
20+
21+
/** Sets the session ID. @param sessionId the session ID @return this */
22+
public AutoModeSwitchInvocation setSessionId(String sessionId) {
23+
this.sessionId = sessionId;
24+
return this;
25+
}
26+
}

0 commit comments

Comments
 (0)