Skip to content

Commit aa17d3a

Browse files
Copilotedburns
andauthored
Port Session FS support: SessionFsHandler, types, config, and RPC registration
Agent-Logs-Url: https://github.com/github/copilot-sdk-java/sessions/6440ac65-a609-41e9-8afc-718bccbef08a Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent 89cc261 commit aa17d3a

29 files changed

+1304
-1
lines changed

.lastmerge

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
c3fa6cbfb83d4a20b7912b1a17013d48f5a277a1
1+
16f0ba278ebb25e2cd6326f932d60517ea926431

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import com.github.copilot.sdk.json.ResumeSessionConfig;
3434
import com.github.copilot.sdk.json.ResumeSessionResponse;
3535
import com.github.copilot.sdk.json.SessionConfig;
36+
import com.github.copilot.sdk.json.SessionFsConventions;
37+
import com.github.copilot.sdk.json.SessionFsHandler;
3638
import com.github.copilot.sdk.json.SessionLifecycleHandler;
3739
import com.github.copilot.sdk.json.SessionListFilter;
3840
import com.github.copilot.sdk.json.SessionMetadata;
@@ -189,6 +191,9 @@ private Connection startCoreBody() {
189191
// Verify protocol version
190192
verifyProtocolVersion(connection);
191193

194+
// Register as sessionFs provider if configured
195+
configureSessionFs(connection);
196+
192197
LOG.info("Copilot client connected");
193198
return connection;
194199
} catch (Exception e) {
@@ -202,6 +207,37 @@ private Connection startCoreBody() {
202207

203208
private static final int MIN_PROTOCOL_VERSION = 2;
204209

210+
private void configureSessionFs(Connection connection) throws Exception {
211+
var sessionFsOptions = options.getSessionFs();
212+
if (sessionFsOptions == null) {
213+
return;
214+
}
215+
var params = new HashMap<String, Object>();
216+
params.put("initialCwd", sessionFsOptions.getInitialCwd());
217+
params.put("sessionStatePath", sessionFsOptions.getSessionStatePath());
218+
SessionFsConventions conventions = sessionFsOptions.getConventions();
219+
if (conventions != null) {
220+
params.put("conventions", conventions == SessionFsConventions.POSIX ? "posix" : "windows");
221+
}
222+
connection.rpc.invoke("sessionFs.setProvider", params, Void.class).get(30, TimeUnit.SECONDS);
223+
}
224+
225+
private void configureSessionFsHandler(CopilotSession session,
226+
java.util.function.Function<CopilotSession, SessionFsHandler> createSessionFsHandler) {
227+
if (options.getSessionFs() == null) {
228+
return;
229+
}
230+
if (createSessionFsHandler == null) {
231+
throw new IllegalArgumentException(
232+
"CreateSessionFsHandler is required in the session config when CopilotClientOptions.sessionFs is configured.");
233+
}
234+
SessionFsHandler handler = createSessionFsHandler.apply(session);
235+
if (handler == null) {
236+
throw new IllegalArgumentException("createSessionFsHandler returned null.");
237+
}
238+
session.registerSessionFsHandler(handler);
239+
}
240+
205241
private void verifyProtocolVersion(Connection connection) throws Exception {
206242
int expectedVersion = SdkProtocolVersion.get();
207243
var params = new HashMap<String, Object>();
@@ -357,6 +393,7 @@ public CompletableFuture<CopilotSession> createSession(SessionConfig config) {
357393
session.setExecutor(options.getExecutor());
358394
}
359395
SessionRequestBuilder.configureSession(session, config);
396+
configureSessionFsHandler(session, config.getCreateSessionFsHandler());
360397
sessions.put(sessionId, session);
361398

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

436474
// Extract transform callbacks from the system message config.

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import com.github.copilot.sdk.json.MessageOptions;
5656
import com.github.copilot.sdk.json.ModelCapabilitiesOverride;
5757
import com.github.copilot.sdk.json.PermissionHandler;
58+
import com.github.copilot.sdk.json.SessionFsHandler;
5859
import com.github.copilot.sdk.json.PermissionInvocation;
5960
import com.github.copilot.sdk.json.PermissionRequest;
6061
import com.github.copilot.sdk.json.PermissionRequestResult;
@@ -143,6 +144,7 @@ public final class CopilotSession implements AutoCloseable {
143144
private final AtomicReference<UserInputHandler> userInputHandler = new AtomicReference<>();
144145
private final AtomicReference<ElicitationHandler> elicitationHandler = new AtomicReference<>();
145146
private final AtomicReference<SessionHooks> hooksHandler = new AtomicReference<>();
147+
private final AtomicReference<SessionFsHandler> sessionFsHandler = new AtomicReference<>();
146148
private volatile EventErrorHandler eventErrorHandler;
147149
private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS;
148150
private volatile Map<String, java.util.function.Function<String, CompletableFuture<String>>> transformCallbacks;
@@ -1299,6 +1301,24 @@ void registerHooks(SessionHooks hooks) {
12991301
hooksHandler.set(hooks);
13001302
}
13011303

1304+
/**
1305+
* Registers the session filesystem handler for this session.
1306+
*
1307+
* @param handler
1308+
* the handler to register
1309+
*/
1310+
void registerSessionFsHandler(SessionFsHandler handler) {
1311+
sessionFsHandler.set(handler);
1312+
}
1313+
1314+
/**
1315+
* Returns the registered session filesystem handler, or {@code null} if none is
1316+
* registered.
1317+
*/
1318+
SessionFsHandler getSessionFsHandler() {
1319+
return sessionFsHandler.get();
1320+
}
1321+
13021322
/**
13031323
* Registers transform callbacks for system message sections.
13041324
* <p>

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

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@
2020
import com.github.copilot.sdk.events.SessionEventParser;
2121
import com.github.copilot.sdk.json.PermissionRequestResult;
2222
import com.github.copilot.sdk.json.PermissionRequestResultKind;
23+
import com.github.copilot.sdk.json.SessionFsAppendFileParams;
24+
import com.github.copilot.sdk.json.SessionFsCopyDirParams;
25+
import com.github.copilot.sdk.json.SessionFsCpParams;
26+
import com.github.copilot.sdk.json.SessionFsExistsParams;
27+
import com.github.copilot.sdk.json.SessionFsGlobParams;
28+
import com.github.copilot.sdk.json.SessionFsHandler;
29+
import com.github.copilot.sdk.json.SessionFsMkdirParams;
30+
import com.github.copilot.sdk.json.SessionFsReaddirParams;
31+
import com.github.copilot.sdk.json.SessionFsReadFileParams;
32+
import com.github.copilot.sdk.json.SessionFsRenameParams;
33+
import com.github.copilot.sdk.json.SessionFsRmParams;
34+
import com.github.copilot.sdk.json.SessionFsStatParams;
35+
import com.github.copilot.sdk.json.SessionFsWriteFileParams;
2336
import com.github.copilot.sdk.json.SessionLifecycleEvent;
2437
import com.github.copilot.sdk.json.SessionLifecycleEventMetadata;
2538
import com.github.copilot.sdk.json.ToolDefinition;
@@ -83,6 +96,32 @@ void registerHandlers(JsonRpcClient rpc) {
8396
rpc.registerMethodHandler("hooks.invoke", (requestId, params) -> handleHooksInvoke(rpc, requestId, params));
8497
rpc.registerMethodHandler("systemMessage.transform",
8598
(requestId, params) -> handleSystemMessageTransform(rpc, requestId, params));
99+
rpc.registerMethodHandler("sessionFs.readFile", (requestId, params) -> handleSessionFsCall(rpc, requestId,
100+
params, "readFile", SessionFsReadFileParams.class, (h, p) -> h.readFile(p)));
101+
rpc.registerMethodHandler("sessionFs.writeFile", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
102+
params, "writeFile", SessionFsWriteFileParams.class, (h, p) -> h.writeFile(p)));
103+
rpc.registerMethodHandler("sessionFs.appendFile", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
104+
params, "appendFile", SessionFsAppendFileParams.class, (h, p) -> h.appendFile(p)));
105+
rpc.registerMethodHandler("sessionFs.exists", (requestId, params) -> handleSessionFsCall(rpc, requestId, params,
106+
"exists", SessionFsExistsParams.class, (h, p) -> h.exists(p)));
107+
rpc.registerMethodHandler("sessionFs.stat", (requestId, params) -> handleSessionFsCall(rpc, requestId, params,
108+
"stat", SessionFsStatParams.class, (h, p) -> h.stat(p)));
109+
rpc.registerMethodHandler("sessionFs.mkdir", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
110+
params, "mkdir", SessionFsMkdirParams.class, (h, p) -> h.mkdir(p)));
111+
rpc.registerMethodHandler("sessionFs.readdir", (requestId, params) -> handleSessionFsCall(rpc, requestId,
112+
params, "readdir", SessionFsReaddirParams.class, (h, p) -> h.readdir(p)));
113+
rpc.registerMethodHandler("sessionFs.readdirWithTypes", (requestId, params) -> handleSessionFsCall(rpc,
114+
requestId, params, "readdirWithTypes", SessionFsReaddirParams.class, (h, p) -> h.readdirWithTypes(p)));
115+
rpc.registerMethodHandler("sessionFs.rm", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId, params,
116+
"rm", SessionFsRmParams.class, (h, p) -> h.rm(p)));
117+
rpc.registerMethodHandler("sessionFs.rename", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
118+
params, "rename", SessionFsRenameParams.class, (h, p) -> h.rename(p)));
119+
rpc.registerMethodHandler("sessionFs.cp", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId, params,
120+
"cp", SessionFsCpParams.class, (h, p) -> h.cp(p)));
121+
rpc.registerMethodHandler("sessionFs.copyDir", (requestId, params) -> handleSessionFsVoidCall(rpc, requestId,
122+
params, "copyDir", SessionFsCopyDirParams.class, (h, p) -> h.copyDir(p)));
123+
rpc.registerMethodHandler("sessionFs.glob", (requestId, params) -> handleSessionFsCall(rpc, requestId, params,
124+
"glob", SessionFsGlobParams.class, (h, p) -> h.glob(p)));
86125
}
87126

88127
private void handleSessionEvent(JsonNode params) {
@@ -379,4 +418,57 @@ private void runAsync(Runnable task) {
379418
task.run();
380419
}
381420
}
421+
422+
@FunctionalInterface
423+
private interface SessionFsOp<P, R> {
424+
425+
CompletableFuture<R> call(SessionFsHandler handler, P params);
426+
}
427+
428+
private <P, R> void handleSessionFsCall(JsonRpcClient rpc, String requestId, JsonNode params, String opName,
429+
Class<P> paramsClass, SessionFsOp<P, R> op) {
430+
runAsync(() -> {
431+
try {
432+
P p = MAPPER.treeToValue(params, paramsClass);
433+
String sessionId = params.has("sessionId") ? params.get("sessionId").asText() : null;
434+
CopilotSession session = sessionId != null ? sessions.get(sessionId) : null;
435+
if (session == null) {
436+
rpc.sendErrorResponse(Long.parseLong(requestId), -32602, "Unknown session: " + sessionId);
437+
return;
438+
}
439+
SessionFsHandler handler = session.getSessionFsHandler();
440+
if (handler == null) {
441+
rpc.sendErrorResponse(Long.parseLong(requestId), -32603,
442+
"No sessionFs handler registered for session: " + sessionId);
443+
return;
444+
}
445+
op.call(handler, p).thenAccept(result -> {
446+
try {
447+
rpc.sendResponse(Long.parseLong(requestId), result);
448+
} catch (Exception e) {
449+
LOG.log(Level.SEVERE, "Error sending sessionFs." + opName + " response", e);
450+
}
451+
}).exceptionally(ex -> {
452+
try {
453+
rpc.sendErrorResponse(Long.parseLong(requestId), -32603, ex.getMessage());
454+
} catch (IOException e) {
455+
LOG.log(Level.SEVERE, "Failed to send sessionFs." + opName + " error", e);
456+
}
457+
return null;
458+
});
459+
} catch (Exception e) {
460+
LOG.log(Level.SEVERE, "Error handling sessionFs." + opName, e);
461+
try {
462+
rpc.sendErrorResponse(Long.parseLong(requestId), -32603, e.getMessage());
463+
} catch (IOException ioe) {
464+
LOG.log(Level.SEVERE, "Failed to send error response", ioe);
465+
}
466+
}
467+
});
468+
}
469+
470+
private <P> void handleSessionFsVoidCall(JsonRpcClient rpc, String requestId, JsonNode params, String opName,
471+
Class<P> paramsClass, SessionFsOp<P, Void> op) {
472+
handleSessionFsCall(rpc, requestId, params, opName, paramsClass, (h, p) -> op.call(h, p).thenApply(v -> null));
473+
}
382474
}

src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public class CopilotClientOptions {
5151
private String logLevel = "info";
5252
private Supplier<CompletableFuture<List<ModelInfo>>> onListModels;
5353
private int port;
54+
private SessionFsConfig sessionFs;
5455
private TelemetryConfig telemetry;
5556
private Boolean useLoggedInUser;
5657
private boolean useStdio = true;
@@ -404,6 +405,36 @@ public CopilotClientOptions setPort(int port) {
404405
return this;
405406
}
406407

408+
/**
409+
* Gets the session filesystem configuration.
410+
*
411+
* @return the session filesystem config, or {@code null} if not set
412+
* @since 1.4.0
413+
*/
414+
public SessionFsConfig getSessionFs() {
415+
return sessionFs;
416+
}
417+
418+
/**
419+
* Sets the session filesystem provider configuration.
420+
* <p>
421+
* When set, the client registers as the session filesystem provider on connect,
422+
* routing session-scoped file I/O through per-session handlers created via
423+
* {@link SessionConfig#setCreateSessionFsHandler(java.util.function.Function)
424+
* SessionConfig.createSessionFsHandler} or
425+
* {@link ResumeSessionConfig#setCreateSessionFsHandler(java.util.function.Function)
426+
* ResumeSessionConfig.createSessionFsHandler}.
427+
*
428+
* @param sessionFs
429+
* the session filesystem configuration
430+
* @return this options instance for method chaining
431+
* @since 1.4.0
432+
*/
433+
public CopilotClientOptions setSessionFs(SessionFsConfig sessionFs) {
434+
this.sessionFs = sessionFs;
435+
return this;
436+
}
437+
407438
/**
408439
* Gets the OpenTelemetry configuration for the CLI server.
409440
*
@@ -508,6 +539,7 @@ public CopilotClientOptions clone() {
508539
copy.logLevel = this.logLevel;
509540
copy.onListModels = this.onListModels;
510541
copy.port = this.port;
542+
copy.sessionFs = this.sessionFs;
511543
copy.telemetry = this.telemetry;
512544
copy.useLoggedInUser = this.useLoggedInUser;
513545
copy.useStdio = this.useStdio;

src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.util.List;
1010
import java.util.Map;
1111
import java.util.function.Consumer;
12+
import java.util.function.Function;
1213

1314
import com.fasterxml.jackson.annotation.JsonInclude;
1415

@@ -59,6 +60,7 @@ public class ResumeSessionConfig {
5960
private InfiniteSessionConfig infiniteSessions;
6061
private Boolean enableConfigDiscovery;
6162
private ModelCapabilitiesOverride modelCapabilities;
63+
private Function<com.github.copilot.sdk.CopilotSession, SessionFsHandler> createSessionFsHandler;
6264
private Consumer<AbstractSessionEvent> onEvent;
6365
private List<CommandDefinition> commands;
6466
private ElicitationHandler onElicitationRequest;
@@ -587,6 +589,36 @@ public ResumeSessionConfig setModelCapabilities(ModelCapabilitiesOverride modelC
587589
return this;
588590
}
589591

592+
/**
593+
* Gets the session filesystem handler factory.
594+
*
595+
* @return the handler factory, or {@code null} if not set
596+
* @since 1.4.0
597+
*/
598+
public Function<com.github.copilot.sdk.CopilotSession, SessionFsHandler> getCreateSessionFsHandler() {
599+
return createSessionFsHandler;
600+
}
601+
602+
/**
603+
* Sets a factory function that creates a {@link SessionFsHandler} for each
604+
* session.
605+
* <p>
606+
* This is only used when
607+
* {@link com.github.copilot.sdk.json.CopilotClientOptions#setSessionFs(SessionFsConfig)
608+
* CopilotClientOptions.sessionFs} is configured.
609+
*
610+
* @param createSessionFsHandler
611+
* the handler factory
612+
* @return this config for method chaining
613+
* @see SessionFsHandler
614+
* @since 1.4.0
615+
*/
616+
public ResumeSessionConfig setCreateSessionFsHandler(
617+
Function<com.github.copilot.sdk.CopilotSession, SessionFsHandler> createSessionFsHandler) {
618+
this.createSessionFsHandler = createSessionFsHandler;
619+
return this;
620+
}
621+
590622
/**
591623
* Gets the event handler registered before the session.resume RPC is issued.
592624
*
@@ -700,6 +732,7 @@ public ResumeSessionConfig clone() {
700732
copy.infiniteSessions = this.infiniteSessions;
701733
copy.enableConfigDiscovery = this.enableConfigDiscovery;
702734
copy.modelCapabilities = this.modelCapabilities;
735+
copy.createSessionFsHandler = this.createSessionFsHandler;
703736
copy.onEvent = this.onEvent;
704737
copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null;
705738
copy.onElicitationRequest = this.onElicitationRequest;

0 commit comments

Comments
 (0)