This guide covers common use cases for the GitHub Copilot SDK for Java. For complete API details, see the Javadoc.
Create a client, start a session, and send a message:
import com.github.copilot.sdk.CopilotClient; import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; try (var client = new CopilotClient()) { client.start().get(); var session = client.createSession( new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("gpt-4.1") ).get(); var response = session.sendAndWait("Explain Java records in one sentence").get(); System.out.println(response.getData().content()); session.close(); }The client manages the connection to the Copilot CLI. Sessions are independent conversations that can run concurrently.
For straightforward interactions, use sendAndWait():
var response = session.sendAndWait("What is the capital of France?").get(); System.out.println(response.getData().content());For more control, subscribe to events and use send():
Exception isolation: If a handler throws an exception, the SDK logs the error. By default, dispatch stops after the first handler error (PROPAGATE_AND_LOG_ERRORS). To continue dispatching to remaining handlers, set EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS. You can customize error handling with session.setEventErrorHandler() — see the Advanced Usage guide.
Set the event error policy to suppress-and-continue:
session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);Optionally add a custom error handler for observability:
session.setEventErrorHandler((event, exception) -> { System.err.println("Handler failed for event " + event.getType() + ": " + exception.getMessage()); });Use PROPAGATE_AND_LOG_ERRORS when you want fail-fast behavior.
var done = new CompletableFuture<Void>(); // Type-safe event handlers (recommended) session.on(AssistantMessageEvent.class, msg -> { System.out.println("Response: " + msg.getData().content()); }); session.on(SessionErrorEvent.class, err -> { System.err.println("Error: " + err.getData().message()); }); session.on(SessionIdleEvent.class, idle -> { done.complete(null); }); session.send("Tell me a joke").get(); done.get(); // Wait for completionYou can also use a single handler for all events:
session.on(event -> { switch (event) { case AssistantMessageEvent msg -> System.out.println("Response: " + msg.getData().content()); case SessionErrorEvent err -> System.err.println("Error: " + err.getData().message()); case SessionIdleEvent idle -> done.complete(null); default -> { } } });| AssistantMessageEvent | Complete assistant response |
| AssistantMessageDeltaEvent | Streaming chunk (when streaming enabled) |
| SessionIdleEvent | Session finished processing |
| SessionErrorEvent | An error occurred |
For the complete list of all event types, see Event Types Reference below.
The SDK supports event types organized by category. All events extend SessionEvent.
| SessionStartEvent | session.start | Session has started |
| SessionResumeEvent | session.resume | Session was resumed |
| SessionIdleEvent | session.idle | Session finished processing, ready for input |
| SessionErrorEvent | session.error | An error occurred in the session |
| SessionInfoEvent | session.info | Informational message from the session |
| SessionShutdownEvent | session.shutdown | Session is shutting down (includes reason and exit code) |
| SessionModelChangeEvent | session.model_change | The model was changed mid-session |
| SessionModeChangedEvent | session.mode_changed | Session mode changed (e.g., plan mode) |
| SessionPlanChangedEvent | session.plan_changed | Session plan was updated |
| SessionWorkspaceFileChangedEvent | session.workspace_file_changed | A file in the workspace was modified |
| SessionHandoffEvent | session.handoff | Session handed off to another agent |
| SessionTruncationEvent | session.truncation | Context was truncated due to limits |
| SessionSnapshotRewindEvent | session.snapshot_rewind | Session rewound to a previous snapshot |
| SessionUsageInfoEvent | session.usage_info | Token usage information |
| SessionCompactionStartEvent | session.compaction_start | Context compaction started (infinite sessions) |
| SessionCompactionCompleteEvent | session.compaction_complete | Context compaction completed |
| SessionContextChangedEvent | session.context_changed | Working directory context changed |
| SessionTaskCompleteEvent | session.task_complete | Task completed with summary |
| AssistantTurnStartEvent | assistant.turn_start | Assistant began processing |
| AssistantIntentEvent | assistant.intent | Assistant's detected intent |
| AssistantReasoningEvent | assistant.reasoning | Full reasoning content (reasoning models) |
| AssistantReasoningDeltaEvent | assistant.reasoning_delta | Streaming reasoning chunk |
| AssistantMessageEvent | assistant.message | Complete assistant message |
| AssistantMessageDeltaEvent | assistant.message_delta | Streaming message chunk |
| AssistantStreamingDeltaEvent | assistant.streaming_delta | Streaming progress with size metrics |
| AssistantTurnEndEvent | assistant.turn_end | Assistant finished processing |
| AssistantUsageEvent | assistant.usage | Token usage for this turn |
| ToolUserRequestedEvent | tool.user_requested | User requested a tool invocation |
| ToolExecutionStartEvent | tool.execution_start | Tool execution started |
| ToolExecutionProgressEvent | tool.execution_progress | Tool execution progress update |
| ToolExecutionPartialResultEvent | tool.execution_partial_result | Partial result from tool |
| ToolExecutionCompleteEvent | tool.execution_complete | Tool execution completed |
| UserMessageEvent | user.message | User sent a message |
| PendingMessagesModifiedEvent | pending_messages.modified | Pending message queue changed |
| SubagentStartedEvent | subagent.started | Subagent was spawned |
| SubagentSelectedEvent | subagent.selected | Subagent was selected for task |
| SubagentDeselectedEvent | subagent.deselected | Subagent was deselected |
| SubagentCompletedEvent | subagent.completed | Subagent completed its task |
| SubagentFailedEvent | subagent.failed | Subagent failed |
| AbortEvent | abort | Operation was aborted |
| HookStartEvent | hook.start | Hook execution started |
| HookEndEvent | hook.end | Hook execution completed |
| SystemMessageEvent | system.message | System-level message |
| SystemNotificationEvent | system.notification | System notification (informational) |
| SkillInvokedEvent | skill.invoked | A skill was invoked |
| ExternalToolRequestedEvent | external_tool.requested | An external tool invocation was requested |
| ExternalToolCompletedEvent | external_tool.completed | An external tool invocation completed |
| PermissionRequestedEvent | permission.requested | A permission request was issued |
| PermissionCompletedEvent | permission.completed | A permission request was resolved |
| CommandQueuedEvent | command.queued | A command was queued for execution |
| CommandCompletedEvent | command.completed | A queued command completed |
| CommandExecuteEvent | command.execute | A registered slash command was dispatched for execution |
| ElicitationRequestedEvent | elicitation.requested | An elicitation (UI dialog) request was received |
| CapabilitiesChangedEvent | capabilities.changed | Session capabilities were updated |
| ExitPlanModeRequestedEvent | exit_plan_mode.requested | Exit from plan mode was requested |
| ExitPlanModeCompletedEvent | exit_plan_mode.completed | Exit from plan mode completed |
See the generated package Javadoc for detailed event data structures.
Enable streaming to receive response chunks as they're generated:
var session = client.createSession( new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-4.1") .setStreaming(true) ).get(); var done = new CompletableFuture<Void>(); session.on(AssistantMessageDeltaEvent.class, delta -> { // Print each chunk as it arrives System.out.print(delta.getData().deltaContent()); }); session.on(SessionIdleEvent.class, idle -> { System.out.println(); // Newline at end done.complete(null); }); session.send("Write a haiku about Java").get(); done.get();Retrieve all messages and events from a session using getMessages():
var history = session.getMessages().get(); for (var event : history) { switch (event) { case UserMessageEvent user -> System.out.println("User: " + user.getData().content()); case AssistantMessageEvent assistant -> System.out.println("Assistant: " + assistant.getData().content()); case ToolExecutionCompleteEvent tool -> System.out.println("Tool: " + tool.getData().toolCallId()); default -> { } } }This is useful for:
Cancel a long-running operation using abort():
// Start a potentially long operation var messageFuture = session.send("Analyze this large codebase..."); // User clicks cancel button session.abort().get(); // The session will emit an AbortEvent session.on(AbortEvent.class, evt -> { System.out.println("Operation was cancelled"); });Use cases:
Use sendAndWait with a custom timeout for CI/CD pipelines:
try { // 30-second timeout var response = session.sendAndWait( new MessageOptions().setPrompt("Quick question"), 30000 // timeout in milliseconds ).get(); } catch (ExecutionException e) { if (e.getCause() instanceof TimeoutException) { System.err.println("Request timed out"); session.abort().get(); } }Query available models before creating a session:
var models = client.listModels().get(); for (var model : models) { System.out.printf("%s (%s)%n", model.getId(), model.getName()); }For models that support it, control how much effort the model spends reasoning before responding:
var session = client.createSession( new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("o3-mini") .setReasoningEffort("high") ).get();| "low" | Fastest responses, less detailed reasoning |
| "medium" | Balanced speed and reasoning depth |
| "high" | Thorough reasoning, slower responses |
| "xhigh" | Maximum reasoning effort |
Note: Only applies to reasoning models (e.g., o3-mini). Non-reasoning models ignore this setting.
Control which built-in tools the assistant can use with allowlists and blocklists.
Restrict the session to only specific tools:
var session = client.createSession( new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setAvailableTools(List.of("read_file", "search_code", "list_dir")) ).get();When availableTools is set, the assistant can only use tools in this list.
Allow all tools except specific ones:
var session = client.createSession( new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setExcludedTools(List.of("execute_command", "write_file")) ).get();Tools in the excludedTools list will not be available to the assistant.
Tool filtering applies to built-in tools. Your custom tools (registered via setTools()) are always available:
var lookupTool = ToolDefinition.create("lookup_issue", "Fetch issue", schema, handler); var session = client.createSession( new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setTools(List.of(lookupTool)) // Custom tool always available .setAvailableTools(List.of("read_file")) // Only allow read_file from built-ins ).get();Set the working directory for file operations in the session:
var session = client.createSession( new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setWorkingDirectory("/path/to/project") ).get();This affects how the assistant resolves relative file paths when using tools like read_file, write_file, and search_code.
Query the client's connection state at any time without making a server call:
ConnectionState state = client.getState(); switch (state) { case CONNECTED -> System.out.println("Ready"); case CONNECTING -> System.out.println("Starting up..."); case DISCONNECTED -> System.out.println("Not connected"); case ERROR -> System.out.println("Connection failed"); }The four states are:
| DISCONNECTED | Client has not been started yet |
| CONNECTING | start() was called but hasn't completed |
| CONNECTED | Client is connected and ready |
| ERROR | Connection failed (check logs for details) |
Get CLI version and protocol information:
var status = client.getStatus().get(); System.out.println("CLI version: " + status.getVersion()); System.out.println("Protocol version: " + status.getProtocolVersion());Check whether the current connection is authenticated and how:
var auth = client.getAuthStatus().get(); if (auth.isAuthenticated()) { System.out.println("Logged in as: " + auth.getLogin()); System.out.println("Auth type: " + auth.getAuthType()); System.out.println("Host: " + auth.getHost()); } else { System.out.println("Not authenticated: " + auth.getStatusMessage()); }Verify server connectivity:
var pong = client.ping("hello").get(); System.out.println("Server responded, protocol version: " + pong.protocolVersion());See ConnectionState, GetStatusResponse, and GetAuthStatusResponse Javadoc for details.
Control how messages are delivered to the session:
// Default: message is enqueued for processing session.send(new MessageOptions() .setPrompt("Analyze this codebase") ).get(); // Immediate: process the message right away session.send(new MessageOptions() .setPrompt("Quick question") .setMode("immediate") ).get();| "enqueue" | Queue the message for processing (default) |
| "immediate" | Process the message immediately |
When resuming a session, you can optionally reconfigure many settings. This is useful when you need to change the model, update tool configurations, or modify behavior.
| model | Change the model for the resumed session |
| systemMessage | Override or extend the system prompt |
| availableTools | Restrict which tools are available |
| excludedTools | Disable specific tools |
| provider | Re-provide BYOK credentials (required for BYOK sessions) |
| reasoningEffort | Adjust reasoning effort level |
| streaming | Enable/disable streaming responses |
| workingDirectory | Change the working directory |
| configDir | Override configuration directory |
| mcpServers | Configure MCP servers |
| customAgents | Configure custom agents |
| agent | Pre-select a custom agent at session start |
| skillDirectories | Directories to load skills from |
| disabledSkills | Skills to disable |
| infiniteSessions | Configure infinite session behavior |
| commands | Slash command definitions for the resumed session |
| onElicitationRequest | Handler for incoming elicitation requests |
| disableResume | When true, resumes without emitting a session.resume event |
| onEvent | Event handler registered before session resumption |
Example: Changing Model on Resume
// Resume with a different model var config = new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("claude-sonnet-4") // Switch to a different model .setReasoningEffort("high"); // Increase reasoning effort var session = client.resumeSession("user-123-task-456", config).get();See ResumeSessionConfig Javadoc for complete options.
Closing a session releases its in-memory resources but preserves session data on disk, so it can be resumed later. Use deleteSession() to permanently remove session data from disk:
// Close a session (releases in-memory resources; session can be resumed later) session.close(); // Permanently delete a session and all its data from disk (cannot be resumed) client.deleteSession(sessionId).get();Complete list of all SessionConfig options for createSession():
| sessionId | String | Custom session ID (auto-generated if omitted) | — |
| clientName | String | Client name for User-Agent identification | — |
| model | String | AI model to use | Choosing a Model |
| reasoningEffort | String | Reasoning depth: "low", "medium", "high", "xhigh" | Reasoning Effort |
| tools | List<ToolDefinition> | Custom tools the assistant can invoke | Custom Tools |
| systemMessage | SystemMessageConfig | Customize assistant behavior | System Messages |
| availableTools | List<String> | Allowlist of built-in tool names | Tool Filtering |
| excludedTools | List<String> | Blocklist of built-in tool names | Tool Filtering |
| provider | ProviderConfig | BYOK API provider configuration | BYOK |
| onPermissionRequest | PermissionHandler | Handler for permission requests | Permission Handling |
| onUserInputRequest | UserInputHandler | Handler for user input requests | User Input Handling |
| hooks | SessionHooks | Lifecycle and tool execution hooks | Session Hooks |
| workingDirectory | String | Working directory for file operations | Working Directory |
| streaming | boolean | Enable streaming response chunks | Streaming Responses |
| mcpServers | Map<String, Object> | MCP server configurations | MCP Servers |
| customAgents | List<CustomAgentConfig> | Custom agent definitions | Custom Agents |
| agent | String | Pre-select a custom agent at session start | Custom Agents |
| infiniteSessions | InfiniteSessionConfig | Auto-compaction for long conversations | Infinite Sessions |
| skillDirectories | List<String> | Directories to load skills from | Skills |
| disabledSkills | List<String> | Skills to disable by name | Skills |
| configDir | String | Custom configuration directory | Config Dir |
| commands | List<CommandDefinition> | Slash command definitions | Slash Commands |
| onElicitationRequest | ElicitationHandler | Handler for incoming elicitation requests | Elicitation |
| onEvent | Consumer<SessionEvent> | Event handler registered before session creation | Early Event Registration |
Use clone() to copy a base configuration before making per-session changes:
var base = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setModel("gpt-4.1") .setStreaming(true); var derived = base.clone() .setWorkingDirectory("/repo-a");clone() creates a shallow copy. Collection fields are copied into new collection instances, while nested objects/handlers are shared references.
See SessionConfig Javadoc for full details.
© 2026