Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .lastmerge
Original file line number Diff line number Diff line change
@@ -1 +1 @@
c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1
16f0ba278ebb25e2cd6326f932d60517ea926431
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Java SDK for programmatic control of GitHub Copilot CLI, enabling you to build A
### Requirements

- Java 17 or later. **JDK 25 recommended**. Selecting JDK 25 enables the use of virtual threads, as shown in the [Quick Start](#quick-start).
- GitHub Copilot CLI 1.0.17 or later installed and in `PATH` (or provide custom `cliPath`)
- GitHub Copilot CLI 1.0.22 or later installed and in `PATH` (or provide custom `cliPath`)

### Maven

Expand Down
38 changes: 38 additions & 0 deletions src/main/java/com/github/copilot/sdk/CopilotClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import com.github.copilot.sdk.json.ResumeSessionConfig;
import com.github.copilot.sdk.json.ResumeSessionResponse;
import com.github.copilot.sdk.json.SessionConfig;
import com.github.copilot.sdk.json.SessionFsConventions;
import com.github.copilot.sdk.json.SessionFsHandler;
import com.github.copilot.sdk.json.SessionLifecycleHandler;
import com.github.copilot.sdk.json.SessionListFilter;
import com.github.copilot.sdk.json.SessionMetadata;
Expand Down Expand Up @@ -189,6 +191,9 @@ private Connection startCoreBody() {
// Verify protocol version
verifyProtocolVersion(connection);

// Register as sessionFs provider if configured
configureSessionFs(connection);

LOG.info("Copilot client connected");
return connection;
} catch (Exception e) {
Expand All @@ -202,6 +207,37 @@ private Connection startCoreBody() {

private static final int MIN_PROTOCOL_VERSION = 2;

private void configureSessionFs(Connection connection) throws Exception {
var sessionFsOptions = options.getSessionFs();
if (sessionFsOptions == null) {
return;
}
var params = new HashMap<String, Object>();
params.put("initialCwd", sessionFsOptions.getInitialCwd());
params.put("sessionStatePath", sessionFsOptions.getSessionStatePath());
SessionFsConventions conventions = sessionFsOptions.getConventions();
if (conventions != null) {
params.put("conventions", conventions == SessionFsConventions.POSIX ? "posix" : "windows");
}
connection.rpc.invoke("sessionFs.setProvider", params, Void.class).get(30, TimeUnit.SECONDS);
}

private void configureSessionFsHandler(CopilotSession session,
java.util.function.Function<CopilotSession, SessionFsHandler> createSessionFsHandler) {
if (options.getSessionFs() == null) {
return;
}
if (createSessionFsHandler == null) {
throw new IllegalArgumentException(
"CreateSessionFsHandler is required in the session config when CopilotClientOptions.sessionFs is configured.");
}
SessionFsHandler handler = createSessionFsHandler.apply(session);
if (handler == null) {
throw new IllegalArgumentException("createSessionFsHandler returned null.");
}
session.registerSessionFsHandler(handler);
}

private void verifyProtocolVersion(Connection connection) throws Exception {
int expectedVersion = SdkProtocolVersion.get();
var params = new HashMap<String, Object>();
Expand Down Expand Up @@ -357,6 +393,7 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
session.setExecutor(options.getExecutor());
}
SessionRequestBuilder.configureSession(session, config);
configureSessionFsHandler(session, config.getCreateSessionFsHandler());
sessions.put(sessionId, session);

// Extract transform callbacks from the system message config.
Expand Down Expand Up @@ -431,6 +468,7 @@ public CompletableFuture<CopilotSession> resumeSession(String sessionId, ResumeS
session.setExecutor(options.getExecutor());
}
SessionRequestBuilder.configureSession(session, config);
configureSessionFsHandler(session, config.getCreateSessionFsHandler());
sessions.put(sessionId, session);

// Extract transform callbacks from the system message config.
Expand Down
56 changes: 55 additions & 1 deletion src/main/java/com/github/copilot/sdk/CopilotSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@
import com.github.copilot.sdk.json.HookInvocation;
import com.github.copilot.sdk.json.InputOptions;
import com.github.copilot.sdk.json.MessageOptions;
import com.github.copilot.sdk.json.ModelCapabilitiesOverride;
import com.github.copilot.sdk.json.PermissionHandler;
import com.github.copilot.sdk.json.SessionFsHandler;
import com.github.copilot.sdk.json.PermissionInvocation;
import com.github.copilot.sdk.json.PermissionRequest;
import com.github.copilot.sdk.json.PermissionRequestResult;
Expand Down Expand Up @@ -142,6 +144,7 @@ public final class CopilotSession implements AutoCloseable {
private final AtomicReference<UserInputHandler> userInputHandler = new AtomicReference<>();
private final AtomicReference<ElicitationHandler> elicitationHandler = new AtomicReference<>();
private final AtomicReference<SessionHooks> hooksHandler = new AtomicReference<>();
private final AtomicReference<SessionFsHandler> sessionFsHandler = new AtomicReference<>();
private volatile EventErrorHandler eventErrorHandler;
private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS;
private volatile Map<String, java.util.function.Function<String, CompletableFuture<String>>> transformCallbacks;
Expand Down Expand Up @@ -1298,6 +1301,24 @@ void registerHooks(SessionHooks hooks) {
hooksHandler.set(hooks);
}

/**
* Registers the session filesystem handler for this session.
*
* @param handler
* the handler to register
*/
void registerSessionFsHandler(SessionFsHandler handler) {
sessionFsHandler.set(handler);
}

/**
* Returns the registered session filesystem handler, or {@code null} if none is
* registered.
*/
SessionFsHandler getSessionFsHandler() {
return sessionFsHandler.get();
}

/**
* Registers transform callbacks for system message sections.
* <p>
Expand Down Expand Up @@ -1496,13 +1517,46 @@ public CompletableFuture<Void> abort() {
* @since 1.2.0
*/
public CompletableFuture<Void> setModel(String model, String reasoningEffort) {
return setModel(model, reasoningEffort, null);
}

/**
* Changes the model for this session with optional reasoning effort level and
* model capabilities overrides.
* <p>
* The new model takes effect for the next message. Conversation history is
* preserved.
*
* <pre>{@code
* session.setModel("claude-sonnet-4.5", null,
* new ModelCapabilitiesOverride().setSupports(new ModelCapabilitiesOverrideSupports().setVision(true))).get();
* }</pre>
*
* @param model
* the model ID to switch to (e.g., {@code "gpt-4.1"})
* @param reasoningEffort
* reasoning effort level (e.g., {@code "low"}, {@code "medium"},
* {@code "high"}, {@code "xhigh"}); {@code null} to use default
* @param modelCapabilities
* per-property overrides for model capabilities, deep-merged over
* runtime defaults; {@code null} to use runtime defaults
* @return a future that completes when the model switch is acknowledged
* @throws IllegalStateException
* if this session has been terminated
* @since 1.4.0
*/
public CompletableFuture<Void> setModel(String model, String reasoningEffort,
ModelCapabilitiesOverride modelCapabilities) {
ensureNotTerminated();
var params = new java.util.HashMap<String, Object>();
params.put("sessionId", sessionId);
params.put("modelId", model);
if (reasoningEffort != null) {
params.put("reasoningEffort", reasoningEffort);
}
if (modelCapabilities != null) {
params.put("modelCapabilities", modelCapabilities);
}
return rpc.invoke("session.model.switchTo", params, Void.class);
}

Expand All @@ -1524,7 +1578,7 @@ public CompletableFuture<Void> setModel(String model, String reasoningEffort) {
* @since 1.0.11
*/
public CompletableFuture<Void> setModel(String model) {
return setModel(model, null);
return setModel(model, null, null);
}

/**
Expand Down
92 changes: 92 additions & 0 deletions src/main/java/com/github/copilot/sdk/RpcHandlerDispatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@
import com.github.copilot.sdk.events.SessionEventParser;
import com.github.copilot.sdk.json.PermissionRequestResult;
import com.github.copilot.sdk.json.PermissionRequestResultKind;
import com.github.copilot.sdk.json.SessionFsAppendFileParams;
import com.github.copilot.sdk.json.SessionFsCopyDirParams;
import com.github.copilot.sdk.json.SessionFsCpParams;
import com.github.copilot.sdk.json.SessionFsExistsParams;
import com.github.copilot.sdk.json.SessionFsGlobParams;
import com.github.copilot.sdk.json.SessionFsHandler;
import com.github.copilot.sdk.json.SessionFsMkdirParams;
import com.github.copilot.sdk.json.SessionFsReaddirParams;
import com.github.copilot.sdk.json.SessionFsReadFileParams;
import com.github.copilot.sdk.json.SessionFsRenameParams;
import com.github.copilot.sdk.json.SessionFsRmParams;
import com.github.copilot.sdk.json.SessionFsStatParams;
import com.github.copilot.sdk.json.SessionFsWriteFileParams;
import com.github.copilot.sdk.json.SessionLifecycleEvent;
import com.github.copilot.sdk.json.SessionLifecycleEventMetadata;
import com.github.copilot.sdk.json.ToolDefinition;
Expand Down Expand Up @@ -83,6 +96,32 @@ void registerHandlers(JsonRpcClient rpc) {
rpc.registerMethodHandler("hooks.invoke", (requestId, params) -> handleHooksInvoke(rpc, requestId, params));
rpc.registerMethodHandler("systemMessage.transform",
(requestId, params) -> handleSystemMessageTransform(rpc, requestId, params));
rpc.registerMethodHandler("sessionFs.readFile", (requestId, params) -> handleSessionFsCall(rpc, requestId,
params, "readFile", SessionFsReadFileParams.class, (h, p) -> h.readFile(p)));
rpc.registerMethodHandler("sessionFs.writeFile", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
params, "writeFile", SessionFsWriteFileParams.class, (h, p) -> h.writeFile(p)));
rpc.registerMethodHandler("sessionFs.appendFile", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
params, "appendFile", SessionFsAppendFileParams.class, (h, p) -> h.appendFile(p)));
rpc.registerMethodHandler("sessionFs.exists", (requestId, params) -> handleSessionFsCall(rpc, requestId, params,
"exists", SessionFsExistsParams.class, (h, p) -> h.exists(p)));
rpc.registerMethodHandler("sessionFs.stat", (requestId, params) -> handleSessionFsCall(rpc, requestId, params,
"stat", SessionFsStatParams.class, (h, p) -> h.stat(p)));
rpc.registerMethodHandler("sessionFs.mkdir", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
params, "mkdir", SessionFsMkdirParams.class, (h, p) -> h.mkdir(p)));
rpc.registerMethodHandler("sessionFs.readdir", (requestId, params) -> handleSessionFsCall(rpc, requestId,
params, "readdir", SessionFsReaddirParams.class, (h, p) -> h.readdir(p)));
rpc.registerMethodHandler("sessionFs.readdirWithTypes", (requestId, params) -> handleSessionFsCall(rpc,
requestId, params, "readdirWithTypes", SessionFsReaddirParams.class, (h, p) -> h.readdirWithTypes(p)));
rpc.registerMethodHandler("sessionFs.rm", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId, params,
"rm", SessionFsRmParams.class, (h, p) -> h.rm(p)));
rpc.registerMethodHandler("sessionFs.rename", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
params, "rename", SessionFsRenameParams.class, (h, p) -> h.rename(p)));
rpc.registerMethodHandler("sessionFs.cp", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId, params,
"cp", SessionFsCpParams.class, (h, p) -> h.cp(p)));
rpc.registerMethodHandler("sessionFs.copyDir", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
params, "copyDir", SessionFsCopyDirParams.class, (h, p) -> h.copyDir(p)));
rpc.registerMethodHandler("sessionFs.glob", (requestId, params) -> handleSessionFsCall(rpc, requestId, params,
"glob", SessionFsGlobParams.class, (h, p) -> h.glob(p)));
}

private void handleSessionEvent(JsonNode params) {
Expand Down Expand Up @@ -379,4 +418,57 @@ private void runAsync(Runnable task) {
task.run();
}
}

@FunctionalInterface
private interface SessionFsOp<P, R> {

CompletableFuture<R> call(SessionFsHandler handler, P params);
}

private <P, R> void handleSessionFsCall(JsonRpcClient rpc, String requestId, JsonNode params, String opName,
Class<P> paramsClass, SessionFsOp<P, R> op) {
runAsync(() -> {
try {
P p = MAPPER.treeToValue(params, paramsClass);
String sessionId = params.has("sessionId") ? params.get("sessionId").asText() : null;
CopilotSession session = sessionId != null ? sessions.get(sessionId) : null;
if (session == null) {
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session: " + sessionId);
return;
}
SessionFsHandler handler = session.getSessionFsHandler();
if (handler == null) {
rpc.sendErrorResponse(Long.parseLong(requestId), -32603,
"No sessionFs handler registered for session: " + sessionId);
return;
}
op.call(handler, p).thenAccept(result -> {
try {
rpc.sendResponse(Long.parseLong(requestId), result);
} catch (Exception e) {
LOG.log(Level.SEVERE, "Error sending sessionFs." + opName + " response", e);
}
}).exceptionally(ex -> {
try {
rpc.sendErrorResponse(Long.parseLong(requestId), -32603, ex.getMessage());
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to send sessionFs." + opName + " error", e);
}
return null;
});
} catch (Exception e) {
LOG.log(Level.SEVERE, "Error handling sessionFs." + opName, e);
try {
rpc.sendErrorResponse(Long.parseLong(requestId), -32603, e.getMessage());
} catch (IOException ioe) {
LOG.log(Level.SEVERE, "Failed to send error response", ioe);
}
}
});
}

private <P> void handleSessionFsVoidCall(JsonRpcClient rpc, String requestId, JsonNode params, String opName,
Class<P> paramsClass, SessionFsOp<P, Void> op) {
handleSessionFsCall(rpc, requestId, params, opName, paramsClass, (h, p) -> op.call(h, p).thenApply(v -> null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
request.setSkillDirectories(config.getSkillDirectories());
request.setDisabledSkills(config.getDisabledSkills());
request.setConfigDir(config.getConfigDir());
request.setEnableConfigDiscovery(config.getEnableConfigDiscovery());
request.setModelCapabilities(config.getModelCapabilities());

if (config.getCommands() != null && !config.getCommands().isEmpty()) {
var wireCommands = config.getCommands().stream()
Expand Down Expand Up @@ -193,6 +195,8 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
request.setSkillDirectories(config.getSkillDirectories());
request.setDisabledSkills(config.getDisabledSkills());
request.setInfiniteSessions(config.getInfiniteSessions());
request.setEnableConfigDiscovery(config.getEnableConfigDiscovery());
request.setModelCapabilities(config.getModelCapabilities());

if (config.getCommands() != null && !config.getCommands().isEmpty()) {
var wireCommands = config.getCommands().stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class CopilotClientOptions {
private String logLevel = "info";
private Supplier<CompletableFuture<List<ModelInfo>>> onListModels;
private int port;
private SessionFsConfig sessionFs;
private TelemetryConfig telemetry;
private Boolean useLoggedInUser;
private boolean useStdio = true;
Expand Down Expand Up @@ -404,6 +405,36 @@ public CopilotClientOptions setPort(int port) {
return this;
}

/**
* Gets the session filesystem configuration.
*
* @return the session filesystem config, or {@code null} if not set
* @since 1.4.0
*/
public SessionFsConfig getSessionFs() {
return sessionFs;
}

/**
* Sets the session filesystem provider configuration.
* <p>
* When set, the client registers as the session filesystem provider on connect,
* routing session-scoped file I/O through per-session handlers created via
* {@link SessionConfig#setCreateSessionFsHandler(java.util.function.Function)
* SessionConfig.createSessionFsHandler} or
* {@link ResumeSessionConfig#setCreateSessionFsHandler(java.util.function.Function)
* ResumeSessionConfig.createSessionFsHandler}.
*
* @param sessionFs
* the session filesystem configuration
* @return this options instance for method chaining
* @since 1.4.0
*/
public CopilotClientOptions setSessionFs(SessionFsConfig sessionFs) {
this.sessionFs = sessionFs;
return this;
}

/**
* Gets the OpenTelemetry configuration for the CLI server.
*
Expand Down Expand Up @@ -508,6 +539,7 @@ public CopilotClientOptions clone() {
copy.logLevel = this.logLevel;
copy.onListModels = this.onListModels;
copy.port = this.port;
copy.sessionFs = this.sessionFs;
copy.telemetry = this.telemetry;
copy.useLoggedInUser = this.useLoggedInUser;
copy.useStdio = this.useStdio;
Expand Down
Loading
Loading