Skip to content
Merged
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
57 changes: 56 additions & 1 deletion src/main/java/com/github/copilot/sdk/CopilotSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public final class CopilotSession implements AutoCloseable {
private volatile EventErrorHandler eventErrorHandler;
private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS;

/** Tracks whether this session instance has been terminated via close(). */
private volatile boolean isTerminated = false;

/**
* Creates a new session with the given ID and RPC client.
* <p>
Expand Down Expand Up @@ -186,11 +189,14 @@ public String getWorkspacePath() {
* @param handler
* the error handler, or {@code null} to use only the default logging
* behavior
* @throws IllegalStateException
* if this session has been terminated
* @see EventErrorHandler
* @see #setEventErrorPolicy(EventErrorPolicy)
* @since 1.0.8
*/
public void setEventErrorHandler(EventErrorHandler handler) {
ensureNotTerminated();
this.eventErrorHandler = handler;
}

Expand Down Expand Up @@ -224,11 +230,14 @@ public void setEventErrorHandler(EventErrorHandler handler) {
* @param policy
* the error policy (default is
* {@link EventErrorPolicy#PROPAGATE_AND_LOG_ERRORS})
* @throws IllegalStateException
* if this session has been terminated
* @see EventErrorPolicy
* @see #setEventErrorHandler(EventErrorHandler)
* @since 1.0.8
*/
public void setEventErrorPolicy(EventErrorPolicy policy) {
ensureNotTerminated();
if (policy == null) {
throw new NullPointerException("policy must not be null");
}
Expand All @@ -244,9 +253,12 @@ public void setEventErrorPolicy(EventErrorPolicy policy) {
* @param prompt
* the message text to send
* @return a future that resolves with the message ID assigned by the server
* @throws IllegalStateException
* if this session has been terminated
* @see #send(MessageOptions)
*/
public CompletableFuture<String> send(String prompt) {
ensureNotTerminated();
return send(new MessageOptions().setPrompt(prompt));
}

Expand All @@ -260,9 +272,12 @@ public CompletableFuture<String> send(String prompt) {
* the message text to send
* @return a future that resolves with the final assistant message event, or
* {@code null} if no assistant message was received
* @throws IllegalStateException
* if this session has been terminated
* @see #sendAndWait(MessageOptions)
*/
public CompletableFuture<AssistantMessageEvent> sendAndWait(String prompt) {
ensureNotTerminated();
return sendAndWait(new MessageOptions().setPrompt(prompt));
}

Expand All @@ -275,10 +290,13 @@ public CompletableFuture<AssistantMessageEvent> sendAndWait(String prompt) {
* @param options
* the message options containing the prompt and attachments
* @return a future that resolves with the message ID assigned by the server
* @throws IllegalStateException
* if this session has been terminated
* @see #sendAndWait(MessageOptions)
* @see #send(String)
*/
public CompletableFuture<String> send(MessageOptions options) {
ensureNotTerminated();
var request = new SendMessageRequest();
request.setSessionId(sessionId);
request.setPrompt(options.getPrompt());
Expand All @@ -304,10 +322,13 @@ public CompletableFuture<String> send(MessageOptions options) {
* {@code null} if no assistant message was received. The future
* completes exceptionally with a TimeoutException if the timeout
* expires.
* @throws IllegalStateException
* if this session has been terminated
* @see #sendAndWait(MessageOptions)
* @see #send(MessageOptions)
*/
public CompletableFuture<AssistantMessageEvent> sendAndWait(MessageOptions options, long timeoutMs) {
ensureNotTerminated();
var future = new CompletableFuture<AssistantMessageEvent>();
var lastAssistantMessage = new AtomicReference<AssistantMessageEvent>();

Expand Down Expand Up @@ -365,9 +386,12 @@ public CompletableFuture<AssistantMessageEvent> sendAndWait(MessageOptions optio
* the message options containing the prompt and attachments
* @return a future that resolves with the final assistant message event, or
* {@code null} if no assistant message was received
* @throws IllegalStateException
* if this session has been terminated
* @see #sendAndWait(MessageOptions, long)
*/
public CompletableFuture<AssistantMessageEvent> sendAndWait(MessageOptions options) {
ensureNotTerminated();
return sendAndWait(options, 60000);
}

Expand Down Expand Up @@ -397,11 +421,14 @@ public CompletableFuture<AssistantMessageEvent> sendAndWait(MessageOptions optio
* @param handler
* a callback to be invoked when a session event occurs
* @return a Closeable that, when closed, unsubscribes the handler
* @throws IllegalStateException
* if this session has been terminated
* @see #on(Class, Consumer)
* @see AbstractSessionEvent
* @see #setEventErrorPolicy(EventErrorPolicy)
*/
public Closeable on(Consumer<AbstractSessionEvent> handler) {
ensureNotTerminated();
eventHandlers.add(handler);
return () -> eventHandlers.remove(handler);
}
Expand Down Expand Up @@ -447,10 +474,13 @@ public Closeable on(Consumer<AbstractSessionEvent> handler) {
* @param handler
* a callback invoked when events of this type occur
* @return a Closeable that unsubscribes the handler when closed
* @throws IllegalStateException
* if this session has been terminated
* @see #on(Consumer)
* @see AbstractSessionEvent
*/
public <T extends AbstractSessionEvent> Closeable on(Class<T> eventType, Consumer<T> handler) {
ensureNotTerminated();
Consumer<AbstractSessionEvent> wrapper = event -> {
if (eventType.isInstance(event)) {
handler.accept(eventType.cast(event));
Expand Down Expand Up @@ -708,9 +738,12 @@ CompletableFuture<Object> handleHooksInvoke(String hookType, JsonNode input) {
* assistant responses, tool invocations, and other session events.
*
* @return a future that resolves with a list of all session events
* @throws IllegalStateException
* if this session has been terminated
* @see AbstractSessionEvent
*/
public CompletableFuture<List<AbstractSessionEvent>> getMessages() {
ensureNotTerminated();
return rpc.invoke("session.getMessages", Map.of("sessionId", sessionId), GetMessagesResponse.class)
.thenApply(response -> {
var events = new ArrayList<AbstractSessionEvent>();
Expand All @@ -737,20 +770,42 @@ public CompletableFuture<List<AbstractSessionEvent>> getMessages() {
* continuing to generate a response.
*
* @return a future that completes when the abort is acknowledged
* @throws IllegalStateException
* if this session has been terminated
*/
public CompletableFuture<Void> abort() {
ensureNotTerminated();
return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class);
}

/**
* Verifies that this session has not yet been terminated.
*
* @throws IllegalStateException
* if close() has already been invoked
*/
private void ensureNotTerminated() {
if (isTerminated) {
throw new IllegalStateException("Session is closed");
}
}

/**
* Disposes the session and releases all associated resources.
* <p>
* This destroys the session on the server, clears all event handlers, and
* releases tool and permission handlers. After calling this method, the session
* cannot be used again.
* cannot be used again. Subsequent calls to this method have no effect.
*/
@Override
public void close() {
synchronized (this) {
if (isTerminated) {
return; // Already terminated - no-op
}
isTerminated = true;
}

try {
rpc.invoke("session.destroy", Map.of("sessionId", sessionId), Void.class).get(5, TimeUnit.SECONDS);
} catch (Exception e) {
Expand Down
Loading
Loading