/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ package com.github.copilot.sdk; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.copilot.sdk.events.AbstractSessionEvent; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.events.SessionErrorEvent; import com.github.copilot.sdk.events.SessionEventParser; import com.github.copilot.sdk.events.SessionIdleEvent; import com.github.copilot.sdk.json.GetMessagesResponse; import com.github.copilot.sdk.json.HookInvocation; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.PermissionInvocation; import com.github.copilot.sdk.json.PermissionRequest; import com.github.copilot.sdk.json.PermissionRequestResult; import com.github.copilot.sdk.json.PostToolUseHookInput; import com.github.copilot.sdk.json.PreToolUseHookInput; import com.github.copilot.sdk.json.SendMessageRequest; import com.github.copilot.sdk.json.SendMessageResponse; import com.github.copilot.sdk.json.SessionEndHookInput; import com.github.copilot.sdk.json.SessionHooks; import com.github.copilot.sdk.json.SessionStartHookInput; import com.github.copilot.sdk.json.ToolDefinition; import com.github.copilot.sdk.json.UserInputHandler; import com.github.copilot.sdk.json.UserInputInvocation; import com.github.copilot.sdk.json.UserInputRequest; import com.github.copilot.sdk.json.UserInputResponse; import com.github.copilot.sdk.json.UserPromptSubmittedHookInput; /** * Represents a single conversation session with the Copilot CLI. *
* A session maintains conversation state, handles events, and manages tool * execution. Sessions are created via {@link CopilotClient#createSession} or * resumed via {@link CopilotClient#resumeSession}. * *
{@code
* // Create a session
* var session = client.createSession(new SessionConfig().setModel("gpt-5")).get();
*
* // Register type-safe event handlers
* session.on(AssistantMessageEvent.class, msg -> {
* System.out.println(msg.getData().content());
* });
* session.on(SessionIdleEvent.class, idle -> {
* System.out.println("Session is idle");
* });
*
* // Send messages
* session.sendAndWait(new MessageOptions().setPrompt("Hello!")).get();
*
* // Clean up
* session.close();
* }
*
* @see CopilotClient#createSession(com.github.copilot.sdk.json.SessionConfig)
* @see CopilotClient#resumeSession(String,
* com.github.copilot.sdk.json.ResumeSessionConfig)
* @see AbstractSessionEvent
* @since 1.0.0
*/
public final class CopilotSession implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(CopilotSession.class.getName());
private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper();
private final String sessionId;
private final String workspacePath;
private final JsonRpcClient rpc;
private final Set* This constructor is package-private. Sessions should be created via * {@link CopilotClient#createSession} or {@link CopilotClient#resumeSession}. * * @param sessionId * the unique session identifier * @param rpc * the JSON-RPC client for communication */ CopilotSession(String sessionId, JsonRpcClient rpc) { this(sessionId, rpc, null); } /** * Creates a new session with the given ID, RPC client, and workspace path. *
* This constructor is package-private. Sessions should be created via * {@link CopilotClient#createSession} or {@link CopilotClient#resumeSession}. * * @param sessionId * the unique session identifier * @param rpc * the JSON-RPC client for communication * @param workspacePath * the workspace path if infinite sessions are enabled */ CopilotSession(String sessionId, JsonRpcClient rpc, String workspacePath) { this.sessionId = sessionId; this.rpc = rpc; this.workspacePath = workspacePath; } /** * Gets the unique identifier for this session. * * @return the session ID */ public String getSessionId() { return sessionId; } /** * Gets the path to the session workspace directory when infinite sessions are * enabled. *
* The workspace directory contains checkpoints/, plan.md, and files/ * subdirectories. * * @return the workspace path, or {@code null} if infinite sessions are disabled */ public String getWorkspacePath() { return workspacePath; } /** * Sets a custom error handler for exceptions thrown by event handlers. *
* When an event handler registered via {@link #on(Consumer)} or * {@link #on(Class, Consumer)} throws an exception during event dispatch, the * error handler is invoked with the event and exception. The error is always * logged at {@link Level#WARNING} regardless of whether a custom handler is * set. * *
* Whether dispatch continues or stops after an error is controlled by the * {@link EventErrorPolicy} set via {@link #setEventErrorPolicy}. The error * handler is always invoked regardless of the policy. * *
* If the error handler itself throws an exception, that exception is caught and * logged at {@link Level#SEVERE}, and dispatch is stopped regardless of the * configured policy. * *
* Example: * *
{@code
* session.setEventErrorHandler((event, exception) -> {
* metrics.increment("handler.errors");
* logger.error("Handler failed on {}: {}", event.getType(), exception.getMessage());
* });
* }
*
* @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;
}
/**
* Sets the error propagation policy for event dispatch.
* * Controls whether remaining event listeners continue to execute when a * preceding listener throws an exception. Errors are always logged at * {@link Level#WARNING} regardless of the policy. * *
* The configured {@link EventErrorHandler} (if any) is always invoked * regardless of the policy. * *
* Example: * *
{@code
* // Opt-in to suppress errors (continue dispatching despite errors)
* session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);
* session.setEventErrorHandler((event, ex) -> logger.error("Handler failed, continuing: {}", ex.getMessage(), ex));
* }
*
* @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");
}
this.eventErrorPolicy = policy;
}
/**
* Sends a simple text message to the Copilot session.
*
* This is a convenience method equivalent to
* {@code send(new MessageOptions().setPrompt(prompt))}.
*
* @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
* This is a convenience method equivalent to
* {@code sendAndWait(new MessageOptions().setPrompt(prompt))}.
*
* @param 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
* This method sends a message asynchronously and returns immediately. Use
* {@link #sendAndWait(MessageOptions)} to wait for the response.
*
* @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
* This method blocks until the assistant finishes processing the message or
* until the timeout expires. It's suitable for simple request/response
* interactions where you don't need to process streaming events.
*
* The returned future can be cancelled via
* {@link java.util.concurrent.Future#cancel(boolean)}. If cancelled externally,
* the future completes with {@link java.util.concurrent.CancellationException}.
* If the timeout expires first, the future completes exceptionally with a
* {@link TimeoutException}.
*
* @param options
* the message options containing the prompt and attachments
* @param timeoutMs
* timeout in milliseconds (0 or negative for no timeout)
* @return a future that resolves with the final assistant message event, or
* {@code null} if no assistant message was received. The future
* completes exceptionally with a TimeoutException if the timeout
* expires, or with CancellationException if cancelled externally.
* @throws IllegalStateException
* if this session has been terminated
* @see #sendAndWait(MessageOptions)
* @see #send(MessageOptions)
*/
public CompletableFuture
* The handler will be invoked for every event in this session, including
* assistant messages, tool calls, and session state changes. For type-safe
* handling of specific event types, prefer {@link #on(Class, Consumer)}
* instead.
*
*
* Exception handling: If a handler throws an exception, the error is
* routed to the configured {@link EventErrorHandler} (if set). Whether
* remaining handlers execute depends on the configured
* {@link EventErrorPolicy}.
*
*
* Example:
*
*
* This provides a type-safe way to handle specific events without needing
* {@code instanceof} checks. The handler will only be called for events
* matching the specified type.
*
*
* Exception handling: If a handler throws an exception, the error is
* routed to the configured {@link EventErrorHandler} (if set). Whether
* remaining handlers execute depends on the configured
* {@link EventErrorPolicy}.
*
*
* Example Usage
*
* This is called internally when events are received from the server. Each
* handler is invoked in its own try/catch block. Errors are always logged at
* {@link Level#WARNING}. Whether dispatch continues after a handler error
* depends on the configured {@link EventErrorPolicy}:
*
* The configured {@link EventErrorHandler} is always invoked (if set),
* regardless of the policy. If the error handler itself throws, dispatch stops
* regardless of policy and the error is logged at {@link Level#SEVERE}.
*
* @param event
* the event to dispatch
* @see #setEventErrorHandler(EventErrorHandler)
* @see #setEventErrorPolicy(EventErrorPolicy)
*/
void dispatchEvent(AbstractSessionEvent event) {
for (Consumer
* Called internally when creating or resuming a session with tools.
*
* @param tools
* the list of tool definitions with handlers
*/
void registerTools(List
* Called internally when creating or resuming a session with permission
* handling.
*
* @param handler
* the permission handler
*/
void registerPermissionHandler(PermissionHandler handler) {
permissionHandler.set(handler);
}
/**
* Handles a permission request from the Copilot CLI.
*
* Called internally when the server requests permission for an operation.
*
* @param permissionRequestData
* the JSON data for the permission request
* @return a future that resolves with the permission result
*/
CompletableFuture
* Called internally when creating or resuming a session with user input
* handling.
*
* @param handler
* the user input handler
*/
void registerUserInputHandler(UserInputHandler handler) {
userInputHandler.set(handler);
}
/**
* Handles a user input request from the Copilot CLI.
*
* Called internally when the server requests user input.
*
* @param request
* the user input request
* @return a future that resolves with the user input response
*/
CompletableFuture
* Called internally when creating or resuming a session with hooks.
*
* @param hooks
* the hooks configuration
*/
void registerHooks(SessionHooks hooks) {
hooksHandler.set(hooks);
}
/**
* Handles a hook invocation from the Copilot CLI.
*
* Called internally when the server invokes a hook.
*
* @param hookType
* the type of hook to invoke
* @param input
* the hook input data
* @return a future that resolves with the hook output
*/
CompletableFuture{@code
* // Collect all events
* var events = new ArrayList
*
* @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{@code
* // Handle assistant messages
* session.on(AssistantMessageEvent.class, msg -> {
* System.out.println(msg.getData().content());
* });
*
* // Handle session idle
* session.on(SessionIdleEvent.class, idle -> {
* done.complete(null);
* });
*
* // Handle streaming deltas
* session.on(AssistantMessageDeltaEvent.class, delta -> {
* System.out.print(delta.getData().deltaContent());
* });
* }
*
* @param
*
*