--- applyTo: '**.java, **/pom.xml' description: 'This file provides guidance on building Java applications using GitHub Copilot SDK for Java.' name: 'GitHub Copilot SDK Java Instructions' --- ## Core Principles - The SDK is in public preview and may have breaking changes - Requires Java 17 or later - Requires GitHub Copilot CLI installed and in PATH - Uses `CompletableFuture` for all async operations - Implements `AutoCloseable` for resource cleanup (try-with-resources) ## Installation ### Maven ```xml com.github copilot-sdk-java ${copilot-sdk-java.version} ``` ### Gradle ```groovy implementation "com.github:copilot-sdk-java:${copilotSdkJavaVersion}" ``` ## Client Initialization ### Basic Client Setup ```java try (var client = new CopilotClient()) { client.start().get(); // Use client... } ``` ### Client Configuration Options When creating a CopilotClient, use `CopilotClientOptions`: - `cliPath` - Path to CLI executable (default: "copilot" from PATH) - `cliArgs` - Extra arguments prepended before SDK-managed flags - `cliUrl` - URL of existing CLI server (e.g., "localhost:8080"). When provided, client won't spawn a process - `port` - Server port (default: 0 for random, only when `useStdio` is false) - `useStdio` - Use stdio transport instead of TCP (default: true) - `logLevel` - Log level: "error", "warn", "info", "debug", "trace" (default: "info") - `autoStart` - Auto-start server on first request (default: true) - `autoRestart` - Auto-restart on crash (default: true) - `cwd` - Working directory for the CLI process - `environment` - Environment variables for the CLI process - `gitHubToken` - GitHub token for authentication - `useLoggedInUser` - Use logged-in `gh` CLI auth (default: true unless token provided) - `onListModels` - Custom model list handler for BYOK scenarios ```java var options = new CopilotClientOptions() .setCliPath("/path/to/copilot") .setLogLevel("debug") .setAutoStart(true) .setAutoRestart(true) .setGitHubToken(System.getenv("GITHUB_TOKEN")); try (var client = new CopilotClient(options)) { client.start().get(); // Use client... } ``` ### Manual Server Control For explicit control: ```java var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false)); client.start().get(); // Use client... client.stop().get(); ``` Use `forceStop()` when `stop()` takes too long. ## Session Management ### Creating Sessions Use `SessionConfig` for configuration. The permission handler is **required**: ```java var session = client.createSession(new SessionConfig() .setModel("gpt-5") .setStreaming(true) .setTools(List.of(...)) .setSystemMessage(new SystemMessageConfig() .setMode(SystemMessageMode.APPEND) .setContent("Custom instructions")) .setAvailableTools(List.of("tool1", "tool2")) .setExcludedTools(List.of("tool3")) .setProvider(new ProviderConfig().setType("openai")) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); ``` ### Session Config Options - `sessionId` - Custom session ID - `clientName` - Application name - `model` - Model name ("gpt-5", "claude-sonnet-4.5", etc.) - `reasoningEffort` - "low", "medium", "high", "xhigh" - `tools` - Custom tools exposed to the CLI - `systemMessage` - System message customization - `availableTools` - Allowlist of tool names - `excludedTools` - Blocklist of tool names - `provider` - Custom API provider configuration (BYOK) - `streaming` - Enable streaming response chunks (default: false) - `workingDirectory` - Session working directory - `mcpServers` - MCP server configurations - `customAgents` - Custom agent configurations - `agent` - Pre-select agent by name - `infiniteSessions` - Infinite sessions configuration - `skillDirectories` - Skill SKILL.md directories - `disabledSkills` - Skills to disable - `configDir` - Config directory path - `hooks` - Session lifecycle hooks - `onPermissionRequest` - **REQUIRED** permission handler - `onUserInputRequest` - User input handler - `onEvent` - Event handler registered before session creation All setters return `SessionConfig` for method chaining. ### Resuming Sessions ```java var session = client.resumeSession(sessionId, new ResumeSessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); ``` ### Session Operations - `session.getSessionId()` - Get session identifier - `session.send(prompt)` / `session.send(MessageOptions)` - Send message, returns message ID - `session.sendAndWait(prompt)` / `session.sendAndWait(MessageOptions)` - Send and wait for response (60s timeout) - `session.sendAndWait(options, timeoutMs)` - Send and wait with custom timeout - `session.abort()` - Abort current processing - `session.getMessages()` - Get all events/messages - `session.setModel(modelId)` - Switch to a different model - `session.log(message)` / `session.log(message, "warning", false)` / `session.log(message, "error", false)` - Log to session timeline with level `"info"`, `"warning"`, or `"error"` - `session.close()` - Clean up resources ## Event Handling ### Event Subscription Pattern Use `CompletableFuture` for waiting on session events: ```java var done = new CompletableFuture(); session.on(event -> { if (event instanceof AssistantMessageEvent msg) { System.out.println(msg.getData().content()); } else if (event instanceof SessionIdleEvent) { done.complete(null); } }); session.send(new MessageOptions().setPrompt("Hello")); done.get(); ``` ### Type-Safe Event Handling Use the typed `on()` overload for compile-time safety: ```java session.on(AssistantMessageEvent.class, msg -> { System.out.println(msg.getData().content()); }); session.on(SessionIdleEvent.class, idle -> { done.complete(null); }); ``` ### Unsubscribing from Events The `on()` method returns a `Closeable`: ```java var subscription = session.on(event -> { /* handler */ }); // Later... subscription.close(); ``` ### Event Types Use pattern matching (Java 17+) for event handling: ```java session.on(event -> { if (event instanceof UserMessageEvent userMsg) { // Handle user message } else if (event instanceof AssistantMessageEvent assistantMsg) { System.out.println(assistantMsg.getData().content()); } else if (event instanceof AssistantMessageDeltaEvent delta) { System.out.print(delta.getData().deltaContent()); } else if (event instanceof ToolExecutionStartEvent toolStart) { // Tool execution started } else if (event instanceof ToolExecutionCompleteEvent toolComplete) { // Tool execution completed } else if (event instanceof SessionStartEvent start) { // Session started } else if (event instanceof SessionIdleEvent idle) { // Session is idle (processing complete) } else if (event instanceof SessionErrorEvent error) { System.err.println("Error: " + error.getData().message()); } }); ``` ### Event Error Handling Control how errors in event handlers are handled: ```java // Set a custom error handler session.setEventErrorHandler(ex -> { logger.error("Event handler error", ex); }); // Or set the error propagation policy session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); ``` ## Streaming Responses ### Enabling Streaming Set `streaming(true)` in SessionConfig: ```java var session = client.createSession(new SessionConfig() .setModel("gpt-5") .setStreaming(true) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); ``` ### Handling Streaming Events Handle both delta events (incremental) and final events: ```java var done = new CompletableFuture(); session.on(event -> { switch (event) { case AssistantMessageDeltaEvent delta -> // Incremental text chunk System.out.print(delta.getData().deltaContent()); case AssistantReasoningDeltaEvent reasoningDelta -> // Incremental reasoning chunk (model-dependent) System.out.print(reasoningDelta.getData().deltaContent()); case AssistantMessageEvent msg -> // Final complete message System.out.println("\n--- Final ---\n" + msg.getData().content()); case AssistantReasoningEvent reasoning -> // Final reasoning content System.out.println("--- Reasoning ---\n" + reasoning.getData().content()); case SessionIdleEvent idle -> done.complete(null); default -> { } } }); session.send(new MessageOptions().setPrompt("Tell me a story")); done.get(); ``` Note: Final events (`AssistantMessageEvent`, `AssistantReasoningEvent`) are ALWAYS sent regardless of streaming setting. ## Custom Tools ### Defining Tools Use `ToolDefinition.create()` with JSON Schema parameters and a `ToolHandler`: ```java var tool = ToolDefinition.create( "get_weather", "Get weather for a location", Map.of( "type", "object", "properties", Map.of( "location", Map.of("type", "string", "description", "City name") ), "required", List.of("location") ), invocation -> { String location = (String) invocation.getArguments().get("location"); return CompletableFuture.completedFuture("Sunny in " + location); } ); var session = client.createSession(new SessionConfig() .setModel("gpt-5") .setTools(List.of(tool)) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); ``` ### Type-Safe Tool Arguments Use `getArgumentsAs()` for deserialization into a typed record or class: ```java record WeatherArgs(String location, String unit) {} var tool = ToolDefinition.create( "get_weather", "Get weather for a location", Map.of( "type", "object", "properties", Map.of( "location", Map.of("type", "string"), "unit", Map.of("type", "string", "enum", List.of("celsius", "fahrenheit")) ), "required", List.of("location") ), invocation -> { var args = invocation.getArgumentsAs(WeatherArgs.class); return CompletableFuture.completedFuture( Map.of("temp", 72, "unit", args.unit(), "location", args.location()) ); } ); ``` ### Overriding Built-In Tools ```java var override = ToolDefinition.createOverride( "built_in_tool_name", "Custom description", Map.of("type", "object", "properties", Map.of(...)), invocation -> CompletableFuture.completedFuture("custom result") ); ``` ### Tool Return Types - Return any JSON-serializable value (String, Map, List, record, POJO) - The SDK automatically serializes the return value and sends it back to the CLI ### Tool Execution Flow When Copilot invokes a tool, the client automatically: 1. Deserializes the arguments 2. Runs your handler function 3. Serializes the return value 4. Responds to the CLI ## Permission Handling ### Required Permission Handler A permission handler is **mandatory** when creating or resuming sessions: ```java // Approve all requests (for development/testing) new SessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) // Custom permission logic new SessionConfig() .setOnPermissionRequest((request, invocation) -> { if ("dangerous-action".equals(request.getKind())) { return CompletableFuture.completedFuture( new PermissionRequestResult().setKind(PermissionRequestResultKind.DENIED) ); } return CompletableFuture.completedFuture( new PermissionRequestResult().setKind(PermissionRequestResultKind.APPROVED) ); }) ``` ## User Input Handling Handle user input requests from the agent: ```java new SessionConfig() .setOnUserInputRequest((request, invocation) -> { System.out.println("Agent asks: " + request.getQuestion()); String answer = scanner.nextLine(); return CompletableFuture.completedFuture( new UserInputResponse() .setAnswer(answer) .setWasFreeform(true) ); }) ``` ## System Message Customization ### Append Mode (Default - Preserves Guardrails) ```java var session = client.createSession(new SessionConfig() .setModel("gpt-5") .setSystemMessage(new SystemMessageConfig() .setMode(SystemMessageMode.APPEND) .setContent(""" - Always check for security vulnerabilities - Suggest performance improvements when applicable """)) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); ``` ### Replace Mode (Full Control - Removes Guardrails) ```java var session = client.createSession(new SessionConfig() .setModel("gpt-5") .setSystemMessage(new SystemMessageConfig() .setMode(SystemMessageMode.REPLACE) .setContent("You are a helpful assistant.")) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); ``` ## File Attachments Attach files to messages using `Attachment`: ```java session.send(new MessageOptions() .setPrompt("Analyze this file") .setAttachments(List.of( new Attachment("file", "/path/to/file.java", "My File") )) ); ``` ## Message Delivery Modes Use the `mode` property in `MessageOptions`: - `"enqueue"` - Queue message for processing (default) - `"immediate"` - Process message immediately ```java session.send(new MessageOptions() .setPrompt("...") .setMode("enqueue") ); ``` ## Convenience: Send and Wait Use `sendAndWait()` to send a message and block until the assistant responds: ```java // With default 60-second timeout AssistantMessageEvent response = session.sendAndWait("What is 2+2?").get(); System.out.println(response.getData().content()); // With custom timeout AssistantMessageEvent response = session.sendAndWait( new MessageOptions().setPrompt("Write a long story"), 120_000 // 120 seconds ).get(); ``` ## Multiple Sessions Sessions are independent and can run concurrently: ```java var session1 = client.createSession(new SessionConfig() .setModel("gpt-5") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); var session2 = client.createSession(new SessionConfig() .setModel("claude-sonnet-4.5") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); session1.send(new MessageOptions().setPrompt("Hello from session 1")); session2.send(new MessageOptions().setPrompt("Hello from session 2")); ``` ## Bring Your Own Key (BYOK) Use custom API providers via `ProviderConfig`: ```java // OpenAI var session = client.createSession(new SessionConfig() .setProvider(new ProviderConfig() .setType("openai") .setBaseUrl("https://api.openai.com/v1") .setApiKey("sk-...")) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); // Azure OpenAI var session = client.createSession(new SessionConfig() .setProvider(new ProviderConfig() .setType("azure") .setAzure(new AzureOptions() .setEndpoint("https://my-resource.openai.azure.com") .setDeployment("gpt-4")) .setBearerToken("...")) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); ``` ## Session Lifecycle Management ### Listing Sessions ```java var sessions = client.listSessions().get(); for (var metadata : sessions) { System.out.println("Session: " + metadata.getSessionId()); } ``` ### Deleting Sessions ```java client.deleteSession(sessionId).get(); ``` ### Checking Connection State ```java var state = client.getState(); ``` ### Lifecycle Event Subscription ```java AutoCloseable subscription = client.onLifecycle(event -> { System.out.println("Lifecycle event: " + event); }); // Later... subscription.close(); ``` ## Error Handling ### Standard Exception Handling ```java try { var session = client.createSession(new SessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) ).get(); session.sendAndWait("Hello").get(); } catch (ExecutionException ex) { Throwable cause = ex.getCause(); System.err.println("Error: " + cause.getMessage()); } catch (Exception ex) { System.err.println("Error: " + ex.getMessage()); } ``` ### Session Error Events Monitor `SessionErrorEvent` for runtime errors: ```java session.on(SessionErrorEvent.class, error -> { System.err.println("Session Error: " + error.getData().message()); }); ``` ## Connectivity Testing Use `ping()` to verify server connectivity: ```java var response = client.ping("test message").get(); ``` ## Status and Authentication ```java // Get CLI version and protocol info var status = client.getStatus().get(); // Check authentication status var authStatus = client.getAuthStatus().get(); // List available models var models = client.listModels().get(); ``` ## Resource Cleanup ### Automatic Cleanup with try-with-resources ALWAYS use try-with-resources for automatic disposal: ```java try (var client = new CopilotClient()) { client.start().get(); try (var session = client.createSession(new SessionConfig() .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { // Use session... } } // Resources automatically cleaned up ``` ### Manual Cleanup If not using try-with-resources: ```java var client = new CopilotClient(); try { client.start().get(); // Use client... } finally { client.stop().get(); } ``` ## Best Practices 1. **Always use try-with-resources** for `CopilotClient` and `CopilotSession` 2. **Always provide a permission handler** - it is required for `createSession` and `resumeSession` 3. **Use `CompletableFuture`** properly - call `.get()` to block, or chain with `.thenApply()`/`.thenCompose()` 4. **Use `sendAndWait()`** for simple request-response patterns instead of manual event handling 5. **Handle `SessionErrorEvent`** for robust error handling 6. **Use pattern matching** (switch with sealed types) for event handling 7. **Enable streaming** for better UX in interactive scenarios 8. **Close event subscriptions** (`Closeable`) when no longer needed 9. **Use `SystemMessageMode.APPEND`** to preserve safety guardrails 10. **Provide descriptive tool names and descriptions** for better model understanding 11. **Handle both delta and final events** when streaming is enabled 12. **Use `getArgumentsAs()`** for type-safe tool argument deserialization ## Common Patterns ### Simple Query-Response ```java try (var client = new CopilotClient()) { client.start().get(); try (var session = client.createSession(new SessionConfig() .setModel("gpt-5") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { var response = session.sendAndWait("What is 2+2?").get(); System.out.println(response.getData().content()); } } ``` ### Event-Driven Conversation ```java try (var client = new CopilotClient()) { client.start().get(); try (var session = client.createSession(new SessionConfig() .setModel("gpt-5") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { var done = new CompletableFuture(); session.on(AssistantMessageEvent.class, msg -> System.out.println(msg.getData().content())); session.on(SessionIdleEvent.class, idle -> done.complete(null)); session.send(new MessageOptions().setPrompt("What is 2+2?")); done.get(); } } ``` ### Multi-Turn Conversation ```java try (var session = client.createSession(new SessionConfig() .setModel("gpt-5") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get()) { var response1 = session.sendAndWait("What is the capital of France?").get(); System.out.println(response1.getData().content()); var response2 = session.sendAndWait("What is its population?").get(); System.out.println(response2.getData().content()); } ``` ### Tool with Complex Return Type ```java record UserInfo(String id, String name, String email, String role) {} var tool = ToolDefinition.create( "get_user", "Retrieve user information", Map.of( "type", "object", "properties", Map.of( "userId", Map.of("type", "string", "description", "User ID") ), "required", List.of("userId") ), invocation -> { String userId = (String) invocation.getArguments().get("userId"); return CompletableFuture.completedFuture( new UserInfo(userId, "John Doe", "john@example.com", "Developer") ); } ); ``` ### Session Hooks ```java var session = client.createSession(new SessionConfig() .setModel("gpt-5") .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setHooks(new SessionHooks() .setOnPreToolUse((input, invocation) -> { System.out.println("About to execute tool: " + input); var decision = new PreToolUseHookOutput().setKind("allow"); return CompletableFuture.completedFuture(decision); }) .setOnPostToolUse((output, invocation) -> { System.out.println("Tool execution complete: " + output); return CompletableFuture.completedFuture(null); })) ).get(); ```