# Advanced Usage
This guide covers advanced scenarios for extending and customizing your Copilot integration.
## Table of Contents
- [Custom Tools](#Custom_Tools)
- [Overriding Built-in Tools](#Overriding_Built-in_Tools)
- [Skipping Permission for Safe Tools](#Skipping_Permission_for_Safe_Tools)
- [Switching Models Mid-Session](#Switching_Models_Mid-Session)
- [System Messages](#System_Messages)
- [Adding Rules](#Adding_Rules)
- [Full Control](#Full_Control)
- [Fine-grained Customization](#Fine-grained_Customization)
- [File Attachments](#File_Attachments)
- [Inline Blob Attachments](#Inline_Blob_Attachments)
- [OpenTelemetry](#OpenTelemetry)
- [Bring Your Own Key (BYOK)](#Bring_Your_Own_Key_BYOK)
- [Infinite Sessions](#Infinite_Sessions)
- [Manual Compaction](#Manual_Compaction)
- [Compaction Events](#Compaction_Events)
- [MCP Servers](#MCP_Servers)
- [Custom Agents](#Custom_Agents)
- [Programmatic Agent Selection](#Programmatic_Agent_Selection)
- [Skills Configuration](#Skills_Configuration)
- [Loading Skills](#Loading_Skills)
- [Disabling Skills](#Disabling_Skills)
- [Custom Configuration Directory](#Custom_Configuration_Directory)
- [Session Logging](#Session_Logging)
- [Early Event Registration](#Early_Event_Registration)
- [User Input Handling](#User_Input_Handling)
- [Permission Handling](#Permission_Handling)
- [Session Hooks](#Session_Hooks)
- [Manual Server Control](#Manual_Server_Control)
- [Session Context and Filtering](#Session_Context_and_Filtering)
- [Listing Sessions with Context](#Listing_Sessions_with_Context)
- [Filtering Sessions by Context](#Filtering_Sessions_by_Context)
- [Context Changed Events](#Context_Changed_Events)
- [Session Lifecycle Events](#Session_Lifecycle_Events)
- [Subscribing to All Lifecycle Events](#Subscribing_to_All_Lifecycle_Events)
- [Subscribing to Specific Event Types](#Subscribing_to_Specific_Event_Types)
- [Foreground Session Control (TUI+Server Mode)](#Foreground_Session_Control_TUIServer_Mode)
- [Getting the Foreground Session](#Getting_the_Foreground_Session)
- [Setting the Foreground Session](#Setting_the_Foreground_Session)
- [Error Handling](#Error_Handling)
- [Event Handler Exceptions](#Event_Handler_Exceptions)
- [Custom Event Error Handler](#Custom_Event_Error_Handler)
- [Event Error Policy](#Event_Error_Policy)
- [OpenTelemetry](#OpenTelemetry)
- [Slash Commands](#Slash_Commands)
- [Registering Commands](#Registering_Commands)
- [Elicitation (UI Dialogs)](#Elicitation_UI_Dialogs)
- [Incoming Elicitation Handler](#Incoming_Elicitation_Handler)
- [Session Capabilities](#Session_Capabilities)
- [Outgoing Elicitation via session.getUi()](#Outgoing_Elicitation_via_session.getUi)
- [Getting Session Metadata by ID](#Getting_Session_Metadata_by_ID)
---
## Custom Tools
Let the AI call back into your application to fetch data or perform actions.
```java
// Define strongly-typed arguments with a record
record IssueArgs(String id) {}
var lookupTool = ToolDefinition.create(
"lookup_issue",
"Fetch issue details from our tracker",
Map.of(
"type", "object",
"properties", Map.of(
"id", Map.of("type", "string", "description", "Issue identifier")
),
"required", List.of("id")
),
invocation -> {
IssueArgs args = invocation.getArgumentsAs(IssueArgs.class);
return CompletableFuture.completedFuture(fetchIssue(args.id()));
}
);
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setTools(List.of(lookupTool))
).get();
```
See [ToolDefinition](apidocs/com/github/copilot/sdk/json/ToolDefinition.html) Javadoc for schema details.
### Overriding Built-in Tools
You can replace a built-in CLI tool (such as `grep` or `read_file`) with your own implementation
by using `ToolDefinition.createOverride()`. This signals to the CLI that the name collision is
intentional and your custom implementation should be used instead.
```java
var customGrep = ToolDefinition.createOverride(
"grep",
"Project-aware search with custom filtering",
Map.of(
"type", "object",
"properties", Map.of(
"query", Map.of("type", "string", "description", "Search query")
),
"required", List.of("query")
),
invocation -> {
String query = (String) invocation.getArguments().get("query");
// Your custom search logic here
return CompletableFuture.completedFuture("Results for: " + query);
}
);
var session = client.createSession(
new SessionConfig()
.setTools(List.of(customGrep))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
```
### Skipping Permission for Safe Tools
When a tool performs only read-only or non-destructive operations, you can mark it to skip the
permission prompt entirely using `ToolDefinition.createSkipPermission()`:
```java
var safeLookup = ToolDefinition.createSkipPermission(
"safe_lookup",
"Look up a record by ID (read-only, no side effects)",
Map.of(
"type", "object",
"properties", Map.of(
"id", Map.of("type", "string")
),
"required", List.of("id")
),
invocation -> {
String id = (String) invocation.getArguments().get("id");
return CompletableFuture.completedFuture("Record: " + lookupRecord(id));
}
);
```
The CLI bypasses the permission request for this tool invocation, so no `PermissionRequestedEvent`
is emitted and the `onPermissionRequest` handler is not called.
See [ToolDefinition](apidocs/com/github/copilot/sdk/json/ToolDefinition.html) Javadoc for details.
---
## Switching Models Mid-Session
You can change the model used by an existing session without losing conversation history.
The new model takes effect starting with the next message sent.
```java
var session = client.createSession(
new SessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
// Switch to a different model mid-conversation
session.setModel("gpt-4.1").get();
// Switch with a specific reasoning effort level
session.setModel("claude-sonnet-4.6", "high").get();
// Next message will use the new model
session.sendAndWait(new MessageOptions().setPrompt("Continue with the new model")).get();
```
The `reasoningEffort` parameter accepts `"low"`, `"medium"`, `"high"`, or `"xhigh"` for models
that support reasoning. Pass `null` (or use the single-argument overload) to use the default.
The session emits a [`SessionModelChangeEvent`](apidocs/com/github/copilot/sdk/generated/SessionModelChangeEvent.html)
when the switch completes, which you can observe with `session.on(SessionModelChangeEvent.class, event -> ...)`.
See [CopilotSession.setModel()](apidocs/com/github/copilot/sdk/CopilotSession.html#setModel(java.lang.String)) Javadoc for details.
---
## System Messages
Customize the AI's behavior by adding rules or replacing the default prompt.
### Adding Rules
Use `APPEND` mode to add constraints while keeping default guardrails:
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setSystemMessage(new SystemMessageConfig()
.setMode(SystemMessageMode.APPEND)
.setContent("""
- Always check for security vulnerabilities
- Suggest performance improvements
"""))
).get();
```
### Full Control
Use `REPLACE` mode for complete control (removes default guardrails):
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setSystemMessage(new SystemMessageConfig()
.setMode(SystemMessageMode.REPLACE)
.setContent("You are a helpful coding assistant."))
).get();
```
### Fine-grained Customization
Use `CUSTOMIZE` mode to override individual sections of the default system prompt without
replacing it entirely. You can replace, remove, append, prepend, or transform specific sections
using the section identifiers from `SystemPromptSections`.
**Static overrides:**
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setSystemMessage(new SystemMessageConfig()
.setMode(SystemMessageMode.CUSTOMIZE)
.setSections(Map.of(
// Replace the tone section
SystemPromptSections.TONE,
new SectionOverride()
.setAction(SectionOverrideAction.REPLACE)
.setContent("Be concise and formal in all responses."),
// Remove the code-change-rules section entirely
SystemPromptSections.CODE_CHANGE_RULES,
new SectionOverride()
.setAction(SectionOverrideAction.REMOVE)
))
// Optional: extra content appended after all sections
.setContent("Always mention quarterly earnings."))
).get();
```
**Transform callbacks** let you inspect and modify section content at runtime:
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setSystemMessage(new SystemMessageConfig()
.setMode(SystemMessageMode.CUSTOMIZE)
.setSections(Map.of(
SystemPromptSections.IDENTITY,
new SectionOverride()
.setTransform(content ->
CompletableFuture.completedFuture(
content + "\nAlways end your reply with DONE."))
)))
).get();
```
See [SystemMessageConfig](apidocs/com/github/copilot/sdk/json/SystemMessageConfig.html),
[SectionOverride](apidocs/com/github/copilot/sdk/json/SectionOverride.html), and
[SystemPromptSections](apidocs/com/github/copilot/sdk/json/SystemPromptSections.html) Javadoc for details.
---
## File Attachments
Include files as context for the AI to analyze. The `Attachment` record takes three parameters:
| Parameter | Type | Description |
|-----------|------|-------------|
| `type` | String | The attachment type — use `"file"` for filesystem files |
| `path` | String | The absolute path to the file on disk |
| `displayName` | String | A human-readable label shown to the AI (e.g., the filename or a description) |
```java
session.send(new MessageOptions()
.setPrompt("Review this file for bugs")
.setAttachments(List.of(
new Attachment("file", "/path/to/file.java", "MyService.java")
))
).get();
```
You can attach multiple files in a single message:
```java
session.send(new MessageOptions()
.setPrompt("Compare these two implementations")
.setAttachments(List.of(
new Attachment("file", "/src/main/OldImpl.java", "Old Implementation"),
new Attachment("file", "/src/main/NewImpl.java", "New Implementation")
))
).get();
```
### Inline Blob Attachments
Use `BlobAttachment` to pass inline base64-encoded binary data — for example, an image captured
at runtime — without writing it to disk first:
```java
// Load image bytes and base64-encode them
byte[] imageBytes = Files.readAllBytes(Path.of("/path/to/screenshot.png"));
String base64Data = Base64.getEncoder().encodeToString(imageBytes);
session.send(new MessageOptions()
.setPrompt("Describe this screenshot")
.setAttachments(List.of(
new BlobAttachment()
.setData(base64Data)
.setMimeType("image/png")
.setDisplayName("screenshot.png")
))
).get();
```
See [BlobAttachment](apidocs/com/github/copilot/sdk/json/BlobAttachment.html) Javadoc for details.
Both `Attachment` and `BlobAttachment` implement the sealed `MessageAttachment` interface.
For a mixed list with both types, use an explicit type hint:
```java
session.send(new MessageOptions()
.setPrompt("Analyze these")
.setAttachments(List.of(
new Attachment("file", "/path/to/file.java", "Source"),
new BlobAttachment()
.setData(base64Data)
.setMimeType("image/png")
.setDisplayName("screenshot.png")
))
).get();
```
---
## Bring Your Own Key (BYOK)
Use your own OpenAI or Azure OpenAI API key instead of GitHub Copilot.
Supported providers:
| Provider | Type value | Notes |
|----------|-----------|-------|
| OpenAI | `"openai"` | Standard OpenAI API |
| Azure OpenAI / Azure AI Foundry | `"azure"` | Azure-hosted models |
| Anthropic | `"anthropic"` | Claude models |
| Ollama | `"openai"` | Local models via OpenAI-compatible API |
| Microsoft Foundry Local | `"openai"` | Run AI models locally on your device via OpenAI-compatible API |
| Other OpenAI-compatible | `"openai"` | vLLM, LiteLLM, etc. |
### API Key Authentication
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setProvider(new ProviderConfig()
.setType("openai")
.setBaseUrl("https://api.openai.com/v1")
.setApiKey("sk-..."))
).get();
```
### Bearer Token Authentication
Some providers require bearer token authentication instead of API keys:
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setProvider(new ProviderConfig()
.setType("openai")
.setBaseUrl("https://my-custom-endpoint.example.com/v1")
.setBearerToken(System.getenv("MY_BEARER_TOKEN")))
).get();
```
> **Note:** The `bearerToken` option accepts a **static token string** only. The SDK does not refresh this token automatically. If your token expires, requests will fail and you'll need to create a new session with a fresh token.
### Microsoft Foundry Local
[Microsoft Foundry Local](https://foundrylocal.ai) lets you run AI models locally on your own device with an OpenAI-compatible API. Install it via the Foundry Local CLI, then point the SDK at your local endpoint:
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setProvider(new ProviderConfig()
.setType("openai")
.setBaseUrl("http://localhost:/v1"))
// No apiKey needed for local Foundry Local
).get();
```
> **Note:** Foundry Local starts on a **dynamic port** — the port is not fixed. Use `foundry service status` to confirm the port the service is currently listening on, then use that port in your `baseUrl`.
To get started with Foundry Local:
```bash
# Windows: Install Foundry Local CLI (requires winget)
winget install Microsoft.FoundryLocal
# macOS / Linux: see https://foundrylocal.ai for installation instructions
# List available models
foundry model list
# Run a model (starts the local server automatically)
foundry model run phi-4-mini
# Check the port the service is running on
foundry service status
```
### Limitations
When using BYOK, be aware of these limitations:
#### Identity Limitations
BYOK authentication uses **static credentials only**. The following identity providers are NOT supported:
- ❌ **Microsoft Entra ID (Azure AD)** - No support for Entra managed identities or service principals
- ❌ **Third-party identity providers** - No OIDC, SAML, or other federated identity
- ❌ **Managed identities** - Azure Managed Identity is not supported
You must use an API key or static bearer token that you manage yourself.
**Why not Entra ID?** While Entra ID does issue bearer tokens, these tokens are short-lived (typically 1 hour) and require automatic refresh via the Azure Identity SDK. The `bearerToken` option only accepts a static string—there is no callback mechanism for the SDK to request fresh tokens. For long-running workloads requiring Entra authentication, you would need to implement your own token refresh logic and create new sessions with updated tokens.
---
## Infinite Sessions
Run long conversations without hitting context limits.
When enabled (default), the session automatically compacts older messages as the context window fills up.
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setInfiniteSessions(new InfiniteSessionConfig()
.setEnabled(true)
.setBackgroundCompactionThreshold(0.80) // Start compacting at 80%
.setBufferExhaustionThreshold(0.95)) // Block at 95%
).get();
// Access the workspace where session state is persisted
var workspace = session.getWorkspacePath();
```
### Manual Compaction
Trigger compaction immediately when you want to reduce context usage before the automatic threshold is reached:
```java
session.compact().get();
```
### Compaction Events
When compaction occurs, the session emits events that you can listen for:
```java
session.on(SessionCompactionStartEvent.class, start -> {
System.out.println("Compaction started");
});
session.on(SessionCompactionCompleteEvent.class, complete -> {
var data = complete.getData();
System.out.println("Compaction completed - success: " + data.success()
+ ", tokens removed: " + data.tokensRemoved());
});
```
For short conversations, disable to avoid overhead:
```java
new InfiniteSessionConfig().setEnabled(false)
```
---
## MCP Servers
Extend the AI with external tools via the Model Context Protocol.
```java
Map server = Map.of(
"type", "local",
"command", "npx",
"args", List.of("-y", "@modelcontextprotocol/server-filesystem", "/tmp"),
"tools", List.of("*")
);
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setMcpServers(Map.of("filesystem", server))
).get();
```
📖 **[Full MCP documentation →](mcp.html)** for local/remote servers and all options.
---
## Custom Agents
Extend the base Copilot assistant with specialized agents that have their own tools, prompts, and behavior. Users can invoke agents using the `@agent-name` mention syntax in messages.
```java
var reviewer = new CustomAgentConfig()
.setName("code-reviewer")
.setDisplayName("Code Reviewer")
.setDescription("Reviews code for best practices and security")
.setPrompt("You are a code review expert. Focus on security, performance, and maintainability.")
.setTools(List.of("read_file", "search_code"));
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setCustomAgents(List.of(reviewer))
).get();
// The user can now mention @code-reviewer in messages
session.send("@code-reviewer Review src/Main.java").get();
```
### Configuration Options
| Option | Type | Description |
|--------|------|-------------|
| `name` | String | Unique identifier used for `@mentions` (alphanumeric and hyphens) |
| `displayName` | String | Human-readable name shown to users |
| `description` | String | Describes the agent's capabilities |
| `prompt` | String | System prompt that defines the agent's behavior |
| `tools` | List<String> | Tool names available to this agent |
| `mcpServers` | Map | MCP servers available to this agent |
| `infer` | Boolean | Whether the agent can be auto-selected based on context |
### Multiple Agents
Register multiple agents for different tasks:
```java
var agents = List.of(
new CustomAgentConfig()
.setName("reviewer")
.setDescription("Code review")
.setPrompt("You review code for issues."),
new CustomAgentConfig()
.setName("documenter")
.setDescription("Documentation writer")
.setPrompt("You write clear documentation.")
.setInfer(true) // Auto-select when appropriate
);
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCustomAgents(agents)
).get();
```
See [CustomAgentConfig](apidocs/com/github/copilot/sdk/json/CustomAgentConfig.html) Javadoc for full details.
### Programmatic Agent Selection
You can inspect and switch agents at runtime:
```java
var available = session.listAgents().get();
var current = session.getCurrentAgent().get();
if (current != null) {
System.out.println("Current agent: " + current.name());
}
var selected = session.selectAgent("reviewer").get();
System.out.println("Selected: " + selected.name());
session.deselectAgent().get(); // Return to the default agent
```
---
## Skills Configuration
Load custom skills from directories to extend the AI's capabilities with domain-specific knowledge.
### Loading Skills
Skills are loaded from `SKILL.md` files in subdirectories of the specified skill directories:
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setSkillDirectories(List.of("/path/to/skills"))
).get();
```
Each skill subdirectory should contain a `SKILL.md` file with YAML frontmatter:
```markdown
---
name: my-skill
description: A skill that provides domain-specific knowledge
---
# Skill Instructions
Your skill instructions go here...
```
### Disabling Skills
Disable specific skills by name:
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setSkillDirectories(List.of("/path/to/skills"))
.setDisabledSkills(List.of("my-skill"))
).get();
```
---
## Custom Configuration Directory
Use a custom configuration directory for session settings:
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setConfigDir("/path/to/custom/config")
).get();
```
This is useful when you need to isolate session configuration or use different settings for different environments.
---
## Session Logging
Send log messages to the session for debugging, status updates, or UI feedback.
```java
// Simple log message (defaults to "info" level)
session.log("Processing step 1 of 3").get();
// Log with explicit level and ephemeral flag
session.log("Downloading dependencies...", "info", true).get();
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `message` | String | The log message text |
| `level` | String | Log level: `"info"`, `"warning"`, `"error"` |
| `ephemeral` | Boolean | If `true`, the message is transient and may not be persisted |
Use cases:
- Displaying progress in a UI while the session processes a request
- Sending status updates to the session log
- Debugging session behavior with contextual messages
See [CopilotSession.log()](apidocs/com/github/copilot/sdk/CopilotSession.html#log(java.lang.String)) Javadoc for details.
---
## Early Event Registration
Register an event handler *before* the `session.create` RPC is issued, ensuring no early events are missed.
When you register handlers with `session.on()` after `createSession()` returns, you may miss events emitted during session creation (e.g., `SessionStartEvent`). Use `SessionConfig.setOnEvent()` to guarantee delivery of all events from the very start:
```java
var events = new CopyOnWriteArrayList();
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setOnEvent(events::add) // Registered before session.create RPC
).get();
// events list now includes SessionStartEvent and any other early events
```
This is equivalent to calling `session.on(handler)` immediately after creation, but executes earlier in the lifecycle. The same option is available on `ResumeSessionConfig.setOnEvent()` for resumed sessions.
---
## User Input Handling
Handle user input requests when the AI uses the `ask_user` tool to gather information from the user.
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setOnUserInputRequest((request, invocation) -> {
System.out.println("Agent asks: " + request.getQuestion());
// Check if choices are provided
if (request.getChoices() != null && !request.getChoices().isEmpty()) {
System.out.println("Options: " + request.getChoices());
// Return one of the provided choices
var selectedChoice = request.getChoices().get(0);
return CompletableFuture.completedFuture(
new UserInputResponse()
.setAnswer(selectedChoice)
.setWasFreeform(false)
);
}
// Freeform input
var userAnswer = getUserInput(); // your input method
return CompletableFuture.completedFuture(
new UserInputResponse()
.setAnswer(userAnswer)
.setWasFreeform(true)
);
})
).get();
```
The `UserInputRequest` contains:
- `getQuestion()` - The question the AI is asking
- `getChoices()` - Optional list of choices for the user to select from
The `UserInputResponse` should include:
- `setAnswer(String)` - The user's answer
- `setWasFreeform(boolean)` - `true` if the answer was freeform text, `false` if it was from the provided choices
See [UserInputHandler](apidocs/com/github/copilot/sdk/json/UserInputHandler.html) Javadoc for more details.
---
## Permission Handling
Approve or deny permission requests from the AI.
```java
var session = client.createSession(
new SessionConfig().setOnPermissionRequest((request, invocation) -> {
// Inspect request and approve/deny using typed constants
var result = new PermissionRequestResult();
result.setKind(PermissionRequestResultKind.APPROVED);
return CompletableFuture.completedFuture(result);
})
).get();
```
The `PermissionRequestResultKind` class provides well-known constants for common outcomes:
| Constant | Value | Meaning |
|---|---|---|
| `PermissionRequestResultKind.APPROVED` | `"approve-once"` | The permission was approved for this one instance |
| `PermissionRequestResultKind.REJECTED` | `"reject"` | The permission was denied interactively by the user |
| `PermissionRequestResultKind.USER_NOT_AVAILABLE` | `"user-not-available"` | Denied because user confirmation was unavailable |
| `PermissionRequestResultKind.NO_RESULT` | `"no-result"` | No permission decision was made (protocol v3 only) |
You can also pass a raw string to `setKind(String)` for custom or extension values. Use
[`PermissionHandler.APPROVE_ALL`](apidocs/com/github/copilot/sdk/json/PermissionHandler.html) to approve all
requests without writing a handler.
---
## Session Hooks
Intercept tool execution and session lifecycle events using hooks.
```java
var hooks = new SessionHooks()
.setOnPreToolUse((input, invocation) -> {
System.out.println("Tool: " + input.getToolName());
return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
})
.setOnPostToolUse((input, invocation) -> {
System.out.println("Result: " + input.getToolResult());
return CompletableFuture.completedFuture(null);
});
var session = client.createSession(
new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setHooks(hooks)
).get();
```
📖 **[Full Session Hooks documentation →](hooks.html)** for all 5 hook types, inputs/outputs, and examples.
---
## Manual Server Control
Control the CLI lifecycle yourself instead of auto-start.
```java
var client = new CopilotClient(
new CopilotClientOptions().setAutoStart(false)
);
client.start().get(); // Start manually
// ... use client ...
client.stop().get(); // Stop manually
```
### Graceful Stop vs Force Stop
The SDK provides two shutdown methods:
| Method | Behavior |
|--------|----------|
| `stop()` | Gracefully closes all open sessions, then shuts down the connection |
| `forceStop()` | Immediately clears sessions and shuts down — no graceful session cleanup |
Use `stop()` for normal shutdown — it ensures each session is properly closed (flushing pending operations) before terminating the connection:
```java
// Graceful: closes all sessions, then disconnects
client.stop().get();
```
Use `forceStop()` when you need to terminate immediately, such as during error recovery or when the server is unresponsive:
```java
// Immediate: skips session cleanup, kills connection
client.forceStop().get();
```
> **Tip:** In `try-with-resources` blocks, `close()` delegates to `stop()`, so graceful session cleanup happens automatically.
> `close()` is blocking and waits up to `CopilotClient.AUTOCLOSEABLE_TIMEOUT_SECONDS` seconds for shutdown to complete.
---
## Session Context and Filtering
Track and filter sessions by their working directory context including the current directory, git repository, and branch information.
### Listing Sessions with Context
Session metadata may include context information for persisted sessions:
```java
var sessions = client.listSessions().get();
for (var session : sessions) {
var context = session.getContext();
if (context != null) {
System.out.println("Session: " + session.getSessionId());
System.out.println(" Working dir: " + context.getCwd());
System.out.println(" Repository: " + context.getRepository());
System.out.println(" Branch: " + context.getBranch());
System.out.println(" Git root: " + context.getGitRoot());
}
}
```
### Filtering Sessions by Context
Use `SessionListFilter` to filter sessions by context fields:
```java
// Find sessions for a specific repository
var filter = new SessionListFilter()
.setRepository("owner/myproject")
.setBranch("main");
var sessions = client.listSessions(filter).get();
```
Filter options:
- `setCwd(String)` - Filter by exact working directory match
- `setGitRoot(String)` - Filter by git repository root
- `setRepository(String)` - Filter by repository in "owner/repo" format
- `setBranch(String)` - Filter by git branch name
### Context Changed Events
Listen for changes to the working directory context:
```java
session.on(SessionContextChangedEvent.class, event -> {
var newContext = event.getData();
System.out.println("Context changed:");
System.out.println(" New CWD: " + newContext.getCwd());
System.out.println(" Repository: " + newContext.getRepository());
System.out.println(" Branch: " + newContext.getBranch());
});
```
The `session.context_changed` event fires when the working directory context changes between conversation turns.
---
## Session Lifecycle Events
Subscribe to lifecycle events to be notified when sessions are created, deleted, updated, or change foreground/background state.
### Subscribing to All Lifecycle Events
```java
var subscription = client.onLifecycle(event -> {
System.out.println("Session " + event.getSessionId() + ": " + event.getType());
if (event.getMetadata() != null) {
System.out.println(" Summary: " + event.getMetadata().getSummary());
}
});
// Later, when done listening:
subscription.close();
```
### Subscribing to Specific Event Types
```java
import com.github.copilot.sdk.json.SessionLifecycleEventTypes;
// Listen only for session creation
var subscription = client.onLifecycle(
SessionLifecycleEventTypes.CREATED,
event -> System.out.println("New session: " + event.getSessionId())
);
```
Available event types:
- `SessionLifecycleEventTypes.CREATED` - Session was created
- `SessionLifecycleEventTypes.DELETED` - Session was deleted
- `SessionLifecycleEventTypes.UPDATED` - Session was updated
- `SessionLifecycleEventTypes.FOREGROUND` - Session moved to foreground (TUI+server mode)
- `SessionLifecycleEventTypes.BACKGROUND` - Session moved to background (TUI+server mode)
---
## Foreground Session Control (TUI+Server Mode)
When connecting to a server running in TUI+server mode (`--ui-server`), you can control which session is displayed in the TUI.
### Getting the Foreground Session
```java
var sessionId = client.getForegroundSessionId().get();
if (sessionId != null) {
System.out.println("TUI is displaying session: " + sessionId);
}
```
### Setting the Foreground Session
```java
client.setForegroundSessionId("session-123").get();
```
---
## Error Handling
All SDK methods return `CompletableFuture`. Errors surface via `ExecutionException`:
```java
try {
session.send(new MessageOptions().setPrompt("Hello")).get();
} catch (ExecutionException ex) {
System.err.println("Error: " + ex.getCause().getMessage());
}
```
For reactive error handling, use `exceptionally()` or `handle()`:
```java
session.send(new MessageOptions().setPrompt("Hello"))
.exceptionally(ex -> {
System.err.println("Failed: " + ex.getMessage());
return null;
});
```
### Event Handler Exceptions
If an event handler registered via `session.on()` throws an exception, the SDK
catches it and logs it at `WARNING` level. By default, dispatch **stops** after
the first handler error (`PROPAGATE_AND_LOG_ERRORS` policy). You can opt in to
continue dispatching despite errors using `SUPPRESS_AND_LOG_ERRORS`:
```java
// With SUPPRESS_AND_LOG_ERRORS, second handler still runs
session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);
session.on(AssistantMessageEvent.class, msg -> {
throw new RuntimeException("bug in handler 1");
});
session.on(AssistantMessageEvent.class, msg -> {
// This handler executes normally despite the exception above
System.out.println(msg.getData().content());
});
```
Errors are **always logged** at `WARNING` level regardless of the policy or
whether a custom error handler is set.
### Custom Event Error Handler
Set a custom `EventErrorHandler` for additional handling beyond the default
logging — such as metrics, alerts, or integration with external
error-reporting systems:
```java
session.setEventErrorHandler((event, exception) -> {
metrics.increment("handler.errors");
logger.error("Handler failed on {}: {}",
event.getType(), exception.getMessage());
});
```
The error handler receives both the event that was being dispatched and the
exception that was thrown. If the error handler itself throws, that exception
is caught and logged at `SEVERE`, and dispatch is stopped to prevent cascading
failures.
Pass `null` to use only the default logging behavior:
```java
session.setEventErrorHandler(null);
```
### Event Error Policy
By default, the SDK propagates errors and stops dispatch on the first handler
error (`EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS`). You can opt in to
**suppress** errors so that all handlers execute despite errors:
```java
session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);
```
The `EventErrorHandler` (if set) is always invoked regardless of the policy —
the policy only controls whether remaining handlers execute after the error
handler returns. Errors are always logged at `WARNING` level.
| Policy | Behavior |
|---|---|
| `PROPAGATE_AND_LOG_ERRORS` (default) | Log the error; dispatch halts after the first error |
| `SUPPRESS_AND_LOG_ERRORS` | Log the error; all remaining handlers execute |
You can combine both for full control:
```java
// Log errors via custom handler and suppress (continue dispatching)
session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);
session.setEventErrorHandler((event, ex) ->
logger.error("Handler failed, continuing: {}", ex.getMessage(), ex));
```
Or switch policies dynamically:
```java
// Start strict (propagate errors, stop dispatch)
session.setEventErrorPolicy(EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS);
// Later, switch to lenient mode (suppress errors, continue)
session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);
```
See [EventErrorPolicy](apidocs/com/github/copilot/sdk/EventErrorPolicy.html) and [EventErrorHandler](apidocs/com/github/copilot/sdk/EventErrorHandler.html) Javadoc for details.
---
## OpenTelemetry
Enable OpenTelemetry tracing in the Copilot CLI server by configuring a `TelemetryConfig`
on the `CopilotClientOptions`. This is useful for observability, performance monitoring,
and debugging.
```java
var options = new CopilotClientOptions()
.setTelemetry(new TelemetryConfig()
.setOtlpEndpoint("http://localhost:4318") // OTLP/HTTP exporter
.setSourceName("my-app"));
var client = new CopilotClient(options);
```
To export to a local file instead:
```java
var options = new CopilotClientOptions()
.setTelemetry(new TelemetryConfig()
.setExporterType("file")
.setFilePath("/tmp/copilot-traces.json")
.setCaptureContent(true)); // include message content in spans
```
| Property | Environment Variable | Description |
|----------|---------------------|-------------|
| `otlpEndpoint` | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP exporter endpoint URL |
| `filePath` | `COPILOT_OTEL_FILE_EXPORTER_PATH` | File path for the file exporter |
| `exporterType` | `COPILOT_OTEL_EXPORTER_TYPE` | `"otlp-http"` or `"file"` |
| `sourceName` | `COPILOT_OTEL_SOURCE_NAME` | Source name for telemetry spans |
| `captureContent` | `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | Whether to capture message content |
See [TelemetryConfig](apidocs/com/github/copilot/sdk/json/TelemetryConfig.html) Javadoc for details.
---
## Slash Commands
Register custom slash commands that users can invoke from the CLI TUI with `/commandname`.
### Registering Commands
```java
var config = new SessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setCommands(List.of(
new CommandDefinition()
.setName("deploy")
.setDescription("Deploy the current branch")
.setHandler(context -> {
System.out.println("Deploying with args: " + context.getArgs());
// perform deployment ...
return CompletableFuture.completedFuture(null);
}),
new CommandDefinition()
.setName("rollback")
.setDescription("Roll back the last deployment")
.setHandler(context -> {
// perform rollback ...
return CompletableFuture.completedFuture(null);
})
));
try (CopilotClient client = new CopilotClient()) {
client.start().get();
var session = client.createSession(config).get();
// Users can now type /deploy or /rollback in the TUI
}
```
Each `CommandDefinition` requires a `name` (without the leading `/`), an optional `description` shown in the TUI's command completion UI, and a `CommandHandler` that is invoked when the user executes the command.
The `CommandContext` passed to the handler provides:
- `getSessionId()` — the ID of the session where the command was invoked
- `getCommand()` — the full command text (e.g., `/deploy production`)
- `getCommandName()` — command name without the leading `/` (e.g., `deploy`)
- `getArgs()` — the argument string after the command name (e.g., `production`)
---
## Elicitation (UI Dialogs)
Elicitation allows your application to present structured UI dialogs to the user. There are two directions:
1. **Incoming** — The server or an MCP tool requests input from the user via your `onElicitationRequest` handler.
2. **Outgoing** — Your session-side code proactively requests input via `session.getUi()`.
### Incoming Elicitation Handler
Register a handler to receive elicitation requests from the server:
```java
var config = new SessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setOnElicitationRequest(context -> {
System.out.println("Elicitation request: " + context.getMessage());
// Show the form to the user ...
var content = Map.of("confirmed", true);
return CompletableFuture.completedFuture(
new ElicitationResult()
.setAction(ElicitationResultAction.ACCEPT)
.setContent(content)
);
});
```
When `onElicitationRequest` is set, the SDK reports elicitation as a supported capability and the server will route elicitation requests to your handler.
### Session Capabilities
After `createSession` or `resumeSession`, check `session.getCapabilities()` to see what the host supports:
```java
var session = client.createSession(config).get();
var caps = session.getCapabilities();
if (caps.getUi() != null && Boolean.TRUE.equals(caps.getUi().getElicitation())) {
System.out.println("Elicitation is supported");
}
```
Capabilities are updated in real time when a `capabilities.changed` event is received.
### Outgoing Elicitation via `session.getUi()`
If the host reports elicitation support, you can call the convenience methods on `session.getUi()`:
```java
var ui = session.getUi();
// Boolean confirmation
boolean confirmed = ui.confirm("Are you sure you want to proceed?").get();
// Selection from options
String choice = ui.select("Choose an environment", new String[]{"dev", "staging", "prod"}).get();
// Text input
String value = ui.input("Enter your name", null).get();
// Custom schema
var result = ui.elicitation(new ElicitationParams()
.setMessage("Enter deployment details")
.setRequestedSchema(new ElicitationSchema()
.setProperties(Map.of(
"branch", Map.of("type", "string"),
"environment", Map.of("type", "string", "enum", List.of("dev", "staging", "prod"))
))
.setRequired(List.of("branch", "environment"))
)).get();
```
All `getUi()` methods throw `IllegalStateException` if the host does not support elicitation. Always check capabilities first.
---
## Getting Session Metadata by ID
Retrieve metadata for a specific session without listing all sessions:
```java
SessionMetadata metadata = client.getSessionMetadata("session-123").get();
if (metadata != null) {
System.out.println("Session: " + metadata.getSessionId());
System.out.println("Started: " + metadata.getStartTime());
} else {
System.out.println("Session not found");
}
```
This is more efficient than `listSessions()` when you already know the session ID, as it performs a direct O(1) lookup instead of scanning all sessions.
---
## Next Steps
- 📖 **[Documentation](documentation.html)** - Core concepts, events, streaming, models, tool filtering, reasoning effort
- 📖 **[Session Hooks](hooks.html)** - All 5 hook types with inputs, outputs, and examples
- 📖 **[MCP Servers](mcp.html)** - Local and remote MCP server integration
- 📖 **[Setup & Deployment](setup.html)** - OAuth, backend services, scaling, configuration reference
- 📖 **[API Javadoc](apidocs/index.html)** - Complete API reference